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
Guard contributions against stale bases and lost concurrent capabilities
4 files changed
+134
AGENTS.md+4@@ -9,3 +9,7 @@ Run Cargo workspace tests, component/widget integration tests, silkscreen geomet Read skills/aiflow-silkscreen/SKILL.md, docs/silkscreen-priority-pass.md and docs/silkscreen-dashboard.md for silkscreen work. Native EDA primitives belong in their bridges. Keep toolkit dependencies isolated and package all helper commands used by the binary. Source handoff: issue #28 supplied the v0.1.25 base at f31cc251. Preserve its recording, composition, comparison and AI-time session commands when integrating new work.++## Concurrent edits and PR hygiene++Before any source push or PR, read `dev-skills/review-safe-contributions/SKILL.md` and run `tools/pr_preflight.py`. Start from an immutable current wiki HEAD, recheck before submission, review every deletion, and prefer contextual branch PRs over file-set replacements. Never overwrite a newer module/verb registry with an older complete file. Rust changes require production compilation as well as tests so a function hidden under `cfg(test)` cannot pass unnoticed. Record native acceptance separately.
dev-skills/review-safe-contributions/SKILL.mdadded+19@@ -0,0 +1,19 @@+---+name: review-safe-contributions+description: Prepare AI Flow and Fields source changes and bridge PRs against the current wiki head; preserve concurrent modules, verbs and functions; inspect focused diffs and test the combined production build before submission.+---+# Review-safe source contributions++Read the target's latest PR comments, reviews, linked issues and development instructions first. Keep native acceptance failures open. A successful source test is not native acceptance.++1. Read `adom-wiki repo log <repo> --limit 1 --json`; record the full HEAD hash. Clone with `repo clone <repo> --ref <hash> --dir <base>` into an immutable base. Make a separate working copy. Never restore old complete files over current source.+2. Apply only the intended change. For overlapping PRs, wait for the predecessor to merge and use that head, or explicitly stack a branch with its dependency recorded. Never prepare independent full-file snapshots of shared dispatch/module files.+3. Run `tools/pr_preflight.py --repo <repo> --base-sha <hash> --base-dir <base> --work-dir <work> --out <evidence> --files <explicit paths>`. It verifies immutable base blobs, checks live HEAD twice, emits a three-line-context diff and proposed file hashes, and refuses missing Rust declarations, removed manifest verbs or broad replacements. It never writes to the remote. Legitimate removals require a separately explained review; do not hide them by weakening the check.+4. Read EVERY hunk of `review.diff`, especially deleted code, module/verb registration, braces and cfg(test) boundaries. The lexical inventory cannot prove scope. Format changed Rust code consistently, without a repository-wide formatting rewrite.+5. For Rust, run a normal production `cargo check --workspace --bins` with required features, then the combined workspace tests. This catches a production function accidentally nested under cfg(test), which tests alone can miss. Preserve previous capability tests and explicitly inspect their call sites. Run native acceptance for IPC transaction timing, Undo, revision, rendering and DRC claims. Record anything not tested on hardware.+6. Re-read HEAD immediately before submission. If it moved, apply the focused patch onto the new immutable base, resolve conflicts semantically, and repeat the combined checks. Do not just change the recorded base hash.+7. Prefer a wiki branch PR: `branch create <repo> <branch> --from <hash>`, `repo push <repo> --branch <branch> --files <paths> -m <message>`, inspect `branch compare <repo> --head <branch>`, then `pr create <repo> --from <branch> --title ... --body-file ...`. File-set PRs serialize whole-file replacements; they are not an adequate substitute for a reviewed contextual branch diff.+8. Read the actual PR diff after submission. Check that it matches the reviewed paths and hunks. If a race or unexpected diff appears, keep it draft or close it and repair it. A live-head check is not an atomic server lock; branch ancestry preserves the base for the maintainer's merge.+9. PR body: exact base hash, scope, dependencies, preserved capabilities, commands/results, native acceptance status, remaining limitations. Owner review, merge, publication, installation and live verification are distinct states. Do not self-merge another bridge owner's change or replace a shared runtime.++For this repository, the preflight and tests are source tools under `tools/`. This skill is development guidance, so it belongs in `dev-skills/`, not the installed user skills.
tools/pr_preflight.pyadded+89@@ -0,0 +1,89 @@+#!/usr/bin/env python3+"""Read-only wiki PR preflight. A passing report is not native acceptance."""+import argparse+import difflib+import hashlib+import json+from pathlib import Path+import re+import subprocess+import tempfile+++def inspect(path, before, after):+ problems = []+ old, new = before.splitlines(True), after.splitlines(True)+ if path.endswith('.rs'):+ # Conservative lexical inventory; production compilation is still mandatory.+ pattern = r'(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(fn|mod)\s+(\w+)'+ removed = set(re.findall(pattern, before)) - set(re.findall(pattern, after))+ if removed:+ problems.append('removed Rust declarations: ' + repr(sorted(removed)))+ if path.endswith('bridge.json') and before:+ removed = set(json.loads(before).get('verbs', [])) - set(json.loads(after).get('verbs', []))+ if removed:+ problems.append('removed manifest verbs: ' + repr(sorted(removed)))+ deleted = sum(i2-i1 for op,i1,i2,j1,j2 in difflib.SequenceMatcher(None,old,new,autojunk=False).get_opcodes() if op in ('delete','replace'))+ if len(old) >= 30 and deleted / len(old) > 0.5:+ problems.append('more than half of existing lines replaced/deleted; split or review separately')+ diff = ''.join(difflib.unified_diff(old,new,fromfile='a/'+path,tofile='b/'+path,n=3))+ return problems, diff+++def wiki(*args):+ p = subprocess.run(['adom-wiki', *args, '--json'], capture_output=True, text=True)+ if p.returncode:+ raise RuntimeError(p.stdout + p.stderr)+ return json.loads(p.stdout)['data']+++def main():+ p = argparse.ArgumentParser(description=__doc__)+ p.add_argument('--repo', required=True)+ p.add_argument('--base-sha', required=True)+ p.add_argument('--base-dir', type=Path, required=True)+ p.add_argument('--work-dir', type=Path, required=True)+ p.add_argument('--out', type=Path, required=True)+ p.add_argument('--files', nargs='+', required=True)+ a = p.parse_args()+ if not re.fullmatch(r'[0-9a-f]{40}', a.base_sha):+ p.error('--base-sha must be a full immutable commit hash')+ head = lambda: wiki('repo','log',a.repo,'--limit','1')['log'][0]['hash']+ if head() != a.base_sha:+ raise SystemExit('REFUSED: live HEAD moved; rebase and rerun combined checks')+ problems, diffs, hashes = [], [], {}+ with tempfile.TemporaryDirectory() as tmp:+ for i, path in enumerate(a.files):+ rel = Path(path)+ if rel.is_absolute() or '..' in rel.parts:+ raise SystemExit('REFUSED: paths must stay inside the working copy')+ base, work = a.base_dir / rel, a.work_dir / rel+ if base.is_symlink() or work.is_symlink():+ raise SystemExit('REFUSED: symlink input')+ saved = Path(tmp) / str(i)+ # Independently verify each claimed base against the immutable wiki blob.+ result = subprocess.run(['adom-wiki','repo','show',a.repo,path,'--ref',a.base_sha,'--out',str(saved),'--json'],capture_output=True,text=True)+ if result.returncode:+ if base.exists() or 'FILE_NOT_FOUND' not in result.stdout + result.stderr:+ raise RuntimeError(result.stdout + result.stderr)+ before = ''+ else:+ if not base.exists() or saved.read_bytes() != base.read_bytes():+ raise SystemExit('REFUSED: base snapshot mismatch: '+path)+ before = base.read_text()+ after = work.read_text() # Missing proposed files fail; deletions need separate review.+ errors, diff = inspect(path,before,after)+ problems.extend(path+': '+e for e in errors)+ diffs.append(diff)+ hashes[path] = hashlib.sha256(work.read_bytes()).hexdigest()+ if head() != a.base_sha:+ problems.append('live HEAD moved during preflight')+ a.out.mkdir(parents=True,exist_ok=True)+ (a.out/'review.diff').write_text(''.join(diffs))+ (a.out/'report.json').write_text(json.dumps({'repo':a.repo,'base':a.base_sha,'filesSha256':hashes,'problems':problems,'status':'refused' if problems else 'ready-for-human-diff-review','limits':'Lexical checks only. Run production compilation, tests, semantic diff review and native acceptance separately.'},indent=2)+'\n')+ print(json.dumps({'problems':problems,'report':str(a.out/'report.json')}))+ raise SystemExit(1 if problems else 0)+++if __name__ == '__main__':+ main()
tools/test_pr_preflight.pyadded+22@@ -0,0 +1,22 @@+import unittest+from pr_preflight import inspect++class PreflightTests(unittest.TestCase):+ def test_lost_zone_module(self):+ self.assertTrue(inspect('lib.rs','pub mod zone_plan;\npub mod silk_text;\n','pub mod silk_text;\n')[0])+ def test_lost_verb(self):+ self.assertTrue(inspect('bridge.json','{"verbs":["kicad_apply_zone_plan","kicad_silk_text_batch"]}','{"verbs":["kicad_silk_text_batch"]}')[0])+ def test_lost_window_function(self):+ self.assertTrue(inspect('windows.rs','fn restore_placement_one() {}\n','')[0])+ def test_whole_replacement(self):+ self.assertTrue(inspect('x.txt','a\n'*40,'b\n'*40)[0])+ def test_addition_preserves_existing(self):+ errors,diff=inspect('x.rs','pub mod zone_plan;\n','pub mod zone_plan;\npub mod silk_text;\n')+ self.assertEqual(errors,[])+ self.assertIn('+pub mod silk_text;',diff)+ def test_scope_requires_compiler(self):+ # Explicitly document the lexical check's limit: a function moved under+ # cfg(test) retains its name. cargo check without cfg(test) must catch it.+ self.assertEqual(inspect('x.rs','pub fn live() {}\n','#[cfg(test)] mod tests {\npub fn live() {}\n}\n')[0],[])++if __name__=='__main__': unittest.main()