← Commit history

Record current-project rules checks without overwriting custom fabrication rules

John Lauer ·542a3c3410 · ·parent 65c0692
3 files changed +67−1
SKILL.md+4
@@ -439,3 +439,7 @@ Read `skills/aiflow-project-page/SKILL.md` and run `adom-aiflow project-page-che For multiple-editor IPC conflicts, read `docs/isolated-kicad-read-socket.md`. `ADOM_AIFLOW_KICAD_READ_SOCKET` selects a verified instance for read/validation and zone-measurement verbs; zone measurement includes its single refill request. It does not redirect route, placement, zone-edit or window-capture calls. Preserve unsaved editors, require live-editor identity, and keep every native DRC gate. A renamed PCB checkpoint must carry matching project/rule sidecars before it is opened.  Copper measurement uses the same explicit `ADOM_AIFLOW_KICAD_READ_SOCKET` as placement/routing reads. This includes its one native refill request and delayed zone snapshots; it does not enable route or placement edits on isolated endpoints. See `docs/isolated-kicad-read-socket.md`.++### Rechecking an adopted project's design rules++After board adoption, `rules check` records a fresh input-bound result when checking the run's current board and the profile named by its spec. It preserves the existing `.kicad_dru`, including justified local constraints. Checking an unrelated board or overriding the spec's profile does not qualify this run. A failure is recorded as failed evidence; native DRC and fabrication review remain required. Do not re-install or overwrite custom rules just to clear a stale timeline requirement.
crates/adom-aiflow/src/main.rs+28−1
@@ -3783,7 +3783,34 @@ fn main() {                     ok(&format!("installed the jlcpcb-2L rules (JLCPCB published 2-layer capabilities, cited in the file) at {}", dru.display()), &hints);                 }                 _ => {-                    match checks::rules_check(&profile, &board_path) {+                    let result = checks::rules_check(&profile, &board_path);+                    // A read-only check of this run's selected project rules is proof too.+                    // Do not rewrite custom rules, or qualify a different board/profile.+                    if let Some(mut r) = run {+                        let same_board = std::fs::canonicalize(&board_path).ok()+                            .zip(std::fs::canonicalize(r.current_board()).ok())+                            .is_some_and(|(a, b)| a == b);+                        let same_profile = checks::rules_profile(&spec_of(&r)).as_deref()+                            == Some(profile.as_str());+                        let dru = Path::new(&board_path).with_extension("kicad_dru");+                        let selected_dru = Path::new(&r.current_board()).with_extension("kicad_dru");+                        let same_rules = dru == selected_dru || std::fs::canonicalize(&dru).ok()+                            .zip(std::fs::canonicalize(&selected_dru).ok())+                            .is_some_and(|(a, b)| a == b);+                        if same_board && same_profile && same_rules {+                            r.outcome("rules", json!({+                                "pass": result.is_ok(), "profile": profile,+                                "method": "existing-project-check", "board": board_path,+                                "boardSha256": sha256_file(&board_path),+                                "file": dru.display().to_string(),+                                "rulesSha256": sha256_file(&dru.display().to_string()),+                                "note": result.as_ref().map(|m| m.as_str())+                                    .unwrap_or_else(|e| e.as_str())+                            }));+                            r.save().unwrap_or_else(|e| err(&e.to_string(), &[]));+                        }+                    }+                    match result {                         Ok(m) => ok(&m, &[]),                         Err(e) => err(&e, &[checks::rules_hint(&profile)]),                     }
tests/rules-workflow.pyadded+35
@@ -0,0 +1,35 @@+"""Existing project rules produce honest run evidence without mutating rule files."""+import hashlib,json,os,pathlib,subprocess,sys,tempfile+binary=str(pathlib.Path(sys.argv[1]).resolve())+with tempfile.TemporaryDirectory(prefix='aiflow-rules-cli-') as td:+ p=pathlib.Path(td);run=p/'run';env=dict(os.environ,AIFLOW_DASHBOARD='off')+ def call(*args):+  return subprocess.run([binary,'--run',str(run),'--ai-thread','rules-regression',*args],env=env,capture_output=True,text=True,timeout=30)+ assert call('intake','--prompt-time','2026-10-10T00:00:00Z','--engine','test').returncode==0+ board=p/'board.kicad_pcb';board.write_text('(kicad_pcb (version 20240108))\n')+ rules=board.with_suffix('.kicad_dru');body='(version 1)\n(rule "project custom" (constraint clearance (min 0.2mm)))\n';rules.write_text(body)+ spec=p/'spec.json';spec.write_text(json.dumps({'fab':{'rules':'fab'}}))+ state=run/'run.json';d=json.loads(state.read_text());d['boards']=[str(board)];d['spec']=str(spec);d['sequence']=['rules'];d['steps']={'rules':{'visits':[{'at':'2026-10-10T00:00:00Z'}]}};state.write_text(json.dumps(d))+ def outcome():return json.loads(state.read_text())['outcomes'].get('rules')+ r=call('rules','check');assert r.returncode==0,(r.stdout,r.stderr)+ o=outcome();assert o['pass'] and o['method']=='existing-project-check'+ assert o['rulesSha256']==hashlib.sha256(body.encode()).hexdigest()+ assert o['boardSha256']==hashlib.sha256(board.read_bytes()).hexdigest()+ assert rules.read_text()==body+ g=json.loads(call('finish-guard','--json').stdout);assert 'rules' not in g['remaining'] and not g['engineeringComplete']+ other=p/'other.kicad_pcb';other.write_text(board.read_text());other.with_suffix('.kicad_dru').write_text(body)+ assert call('rules','check','--board',str(other)).returncode==0+ assert outcome()==o,'unrelated board changed this run proof'+ r=call('rules','check','--profile','jlcpcb');assert r.returncode!=0+ assert outcome()==o,'profile override changed selected-profile proof'+ alias=p/'same-board.kicad_pcb';alias.symlink_to(board);alias.with_suffix('.kicad_dru').write_text(body)+ assert call('rules','check','--board',str(alias)).returncode==0+ assert outcome()==o,'alias with a different rules file changed the selected project proof'+ alias.with_suffix('.kicad_dru').unlink();alias.with_suffix('.kicad_dru').symlink_to(rules)+ assert call('rules','check','--board',str(alias)).returncode==0+ assert outcome()['pass']+ rules.unlink();r=call('rules','check');assert r.returncode!=0+ assert outcome()['pass'] is False,'failed recheck left a stale passing outcome'+ g=json.loads(call('finish-guard','--json').stdout);assert any('rules' in x for x in g['remaining']) and not g['mayStop']+ assert not rules.exists(),'check rewrote rules'+ print('PASS: current-board proof, immutable custom rules, hashes, unrelated-board/profile isolation, alias binding and stale-success invalidation; engineering gates remain open.')