app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
Bind calculated mechanical reviews to active board and external geometry hashes
2 files changed
+106−3
crates/adom-aiflow/src/reviews.rs+92−3@@ -89,6 +89,49 @@ fn resolve(base: &Path, p: &str) -> PathBuf { if pb.is_absolute() { pb } else { base.parent().map(|d| d.join(&pb)).unwrap_or(pb) } } +/// Bind optional calculated geometry to the active board, not merely a historical input file.+fn geometry_binding(input: &Path, iv: &Value, board: &Path) -> Result<Value, String> {+ let evidence = &iv["geometryEvidence"];+ let claimed = iv["fasteners"].as_array().into_iter().flatten()+ .any(|f| f["sweptVolumeVerified"] == true);+ if evidence.is_null() {+ return if claimed { Err("swept-volume claim has no geometryEvidence".into()) } else { Ok(Value::Null) };+ }+ let mut binding = serde_json::Map::new();+ let mut documents = Vec::new();+ for key in ["request", "report"] {+ let name = evidence[key].as_str().filter(|s| !s.trim().is_empty())+ .ok_or_else(|| format!("geometryEvidence.{key} is missing"))?;+ let path = std::fs::canonicalize(resolve(input, name)).map_err(|e| e.to_string())?;+ let value: Value = serde_json::from_slice(&std::fs::read(&path).map_err(|e| e.to_string())?)+ .map_err(|e| e.to_string())?;+ binding.insert(key.into(), json!({"path": path, "sha256": sha256_file(&path)?}));+ documents.push((path, value));+ }+ let (request_path, request) = &documents[0];+ let report = &documents[1].1;+ if request["schemaVersion"] != 1 || report["schemaVersion"] != 1 || report["passed"] != true {+ return Err("geometry evidence must be a passing schemaVersion 1 calculation".into());+ }+ if report["requestSha256"] != binding["request"]["sha256"] {+ return Err("geometry request changed after calculation".into());+ }+ for key in ["board", "assembly"] {+ let name = request[key]["path"].as_str().ok_or_else(|| format!("geometry request lacks {key} path"))?;+ let path = std::fs::canonicalize(resolve(request_path, name)).map_err(|e| e.to_string())?;+ let hash = sha256_file(&path)?;+ if request[key]["sha256"].as_str() != Some(&hash)+ || report["inputs"][key]["sha256"].as_str() != Some(&hash) {+ return Err(format!("{key} geometry changed after calculation"));+ }+ if key == "board" && sha256_file(board)? != hash {+ return Err("geometry evidence describes another board, not the active run board".into());+ }+ binding.insert(key.into(), json!({"path": path, "sha256": hash}));+ }+ Ok(Value::Object(binding))+}+ fn strings(v: &Value) -> Vec<String> { v.as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect() } /// The board references a review names: the declaration's `refs`, each fastener's `ref` and each@@ -224,6 +267,9 @@ pub fn run(r: &mut Run, name: &str, input: Option<&str>, glb: Option<&str>) -> R let (code, out, stderr) = run_tool(&tool, &["lint", &input_s]).map_err(|e| (e, not_installed_hints(k)))?; let mut audit = Value::Null; let mut result = verdict(code, &out, &stderr);+ let geometry = geometry_binding(&input, &iv, &bp);+ if let Err(e) = &geometry { result = Err(e.clone()); }+ let geometry = geometry.unwrap_or(Value::Null); if let (Ok(()), Some(g)) = (&result, &glb) { let gs = g.display().to_string(); let (c2, o2, e2) = run_tool(&tool, &["audit-glb", &gs]).map_err(|e| (e, not_installed_hints(k)))?;@@ -238,15 +284,15 @@ pub fn run(r: &mut Run, name: &str, input: Option<&str>, glb: Option<&str>) -> R "spec": spec, "specSha256": if spec.is_empty() { Value::Null } else { json!(sha256_file(Path::new(&spec)).ok()) }, "requirementsSha256": d.filter(|d| d.in_requirements).and_then(|d| sha256_file(&d.source).ok()), "board": board, "boardSha256": sha256_file(&bp).ok(), "refs": refs, "refPrefixes": prefixes, "serviceFeatures": features,- "exitCode": code, "result": out, "audit": audit, "at": aiflow_run::now(),- "qualification": "declared review completeness and dimensions only; not a swept-volume or collision solver, not physical-fit evidence",+ "exitCode": code, "result": out, "audit": audit, "geometryEvidence": geometry, "at": aiflow_run::now(),+ "qualification": "declared review plus optional tool-calculated geometry bound to active inputs; not physical-fit qualification", }); match result { Ok(()) => { let mut passed = rec.clone(); passed["pass"] = json!(true); r.outcome(k.step, passed);- let mut hints = vec!["This proves the declared review is complete and its stated dimensions and lengths fit; it does not compute swept volumes or collisions. Keep the CAD check reports the review file links to.".to_string()];+ let mut hints = vec!["The tool checks declared review dimensions and any supported geometry evidence. Linked geometry must match the active board and remains subject to the solver limits; this is not physical-fit qualification.".to_string()]; if !added.is_empty() { hints.push(format!("Added {} to this run's steps before finish.", added.join(" and "))); } if d.is_none() { hints.push(format!("Not declared: finish does not require it. Declare spec {} {{\"schemaVersion\": 1, \"input\": ...}} to make it a required step.", k.key)); } else if r.data["currentStep"].as_str() != Some(k.step) { hints.push(format!("Record it during `step {}` so the step is proven (run the review again after the step starts).", k.step)); }@@ -296,6 +342,14 @@ pub fn binding_gaps(data: &Value, d: &Declared, board: &Path) -> Vec<String> { if let Some(g) = o["audit"]["glb"].as_str() { if o["audit"]["glbSha256"].as_str() != sha256_file(Path::new(g)).ok().as_deref() { out.push(format!("{} review: {g} changed since audit-glb ({again})", k.name)); } }+ match read(&input.display().to_string()) {+ Some(iv) => match geometry_binding(&input, &iv, board) {+ Ok(binding) if binding == o["geometryEvidence"] => {},+ Ok(_) => out.push(format!("{} geometry dependencies changed since review ({again})", k.name)),+ Err(e) => out.push(format!("{} geometry evidence: {e} ({again})", k.name)),+ },+ None => out.push(format!("{} review input cannot be read ({again})", k.name)),+ } match o["boardSha256"].as_str() { None => out.push(format!("{} review has no board hash ({again})", k.name)), Some(h) => if let Err(e) = board_bound(data, h, board) { out.push(format!("{} review is of another board ({e}; {again})", k.name)); },@@ -387,6 +441,41 @@ mod tests { "specSha256": sha256_file(Path::new(&spec)).unwrap(), "boardSha256": sha256_file(Path::new(&board)).unwrap(), "refs": ["H1", "J9"]})); } + #[test]+ fn geometry_is_bound_to_active_board_and_all_external_inputs() {+ let t = T::new("geometry-binding");+ let b = t.write("board", "current board");+ let assembly = t.write("assembly.step", "test geometry dependency");+ let request = json!({"schemaVersion":1,+ "board":{"path":"board","sha256":sha256_file(&b).unwrap()},+ "assembly":{"path":"assembly.step","sha256":sha256_file(&assembly).unwrap()}});+ let rp = t.write("request.json", &request.to_string());+ let report = json!({"schemaVersion":1,"passed":true,"requestSha256":sha256_file(&rp).unwrap(),+ "inputs":{"board":request["board"],"assembly":request["assembly"]}});+ let report_path = t.write("report.json", &report.to_string());+ let input = t.dir.join("review.json");+ let iv = json!({"geometryEvidence":{"request":"request.json","report":"report.json"}});+ let initial = geometry_binding(&input, &iv, &b).unwrap();+ assert!(!initial.is_null());+ let other = t.write("other-board", "different board");+ assert!(geometry_binding(&input, &iv, &other).unwrap_err().contains("active run board"));+ std::fs::write(&report_path, format!("{}\n", report)).unwrap();+ assert_ne!(initial, geometry_binding(&input, &iv, &b).unwrap());+ std::fs::write(&assembly, "changed STEP").unwrap();+ assert!(geometry_binding(&input, &iv, &b).unwrap_err().contains("assembly geometry changed"));+ std::fs::write(&rp, "{}").unwrap();+ assert!(geometry_binding(&input, &iv, &b).is_err());+ }++ #[test]+ fn geometry_claim_requires_proof_but_legacy_declarations_remain_compatible() {+ let t = T::new("geometry-required");+ let b = t.write("board", "board");+ let input = t.dir.join("review.json");+ assert_eq!(geometry_binding(&input, &json!({}), &b).unwrap(), Value::Null);+ assert!(geometry_binding(&input, &json!({"fasteners":[{"sweptVolumeVerified":true}]}), &b).is_err());+ }+ #[test] fn not_declared_changes_nothing() { let t = T::new("none");
docs/mechanical-wiring-reviews.md+14@@ -134,3 +134,17 @@ board. - No package release of either tool, so no install verification; AI Flow does not ship them. - No default skill discovery from the wiki for the two prototype skills; AI Flow's own skills (aiflow-sourcing, aiflow-power-layout) say when to declare the reviews.++### Calculated mechanical geometry evidence++A review claiming `sweptVolumeVerified: true` must include+`geometryEvidence: {"request": "sweep-request.json", "report": "sweep-report.json"}`+(paths relative to the review). The mechanical adapter supports conservative+fixed-axis cylindrical tool sweeps against STEP solids; it does not qualify+articulated tools or physical fit. Run the adapter before the review.++AI Flow binds the request, report, assembly STEP and board hashes. The geometry+request must name the active run board, even if an older board file still exists.+Changing any dependency invalidates the review at finish. A copper-only carry+cannot silently carry calculated geometry to a different board hash: recalculate+and review it. Never turn a verification Boolean on without its calculated report.