← Commit history

Bind calculated mechanical reviews to active board and external geometry hashes

John Lauer ·4f0f19fe41 · ·parent 68f933c
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.