← Commit history

Add project-page preflight and explicit assembly, datasheet and screenshot review workflow

John Lauer ·4b9096e3b5 · ·parent 409a255
6 files changed +117
SKILL.md+4
@@ -429,3 +429,7 @@ After a live-channel upgrade, restart only the verified server for this run, pre ### Event-driven dashboard state (0.1.90)  The server uses one native filesystem watcher per run and a shared state cache. CLI presentation controls notify SSE clients immediately; external timeline/presentation edits trigger native filesystem notifications, including atomic replacements. Idle SSE connections do not poll or reread state files. The 15-second heartbeat only writes a connection comment. Native watcher failures are reported rather than silently falling back to server-side polling. `/health` reports `stateSource`, `stateFileReads`, and `watcherError` for verification. Keep the existing webview open during server upgrades; SSE reconnects automatically.++## Project wiki presentation before delivery++Read `skills/aiflow-project-page/SKILL.md` and run `adom-aiflow project-page-check --project <root>` before delivering a wiki-backed project. Explicitly bind the assembled board hero and project datasheet, include 5–8 reviewed README screenshots, place the datasheet below the README and verify the actual published page. Automatic asset discovery and upload success are not acceptance.
crates/adom-aiflow/src/main.rs+10
@@ -150,6 +150,8 @@ enum Cmd {     /// The end of the run as the human sees it: the moment you say "done, here is your video". Needs finish first and the video file.     Deliver { #[arg(long)] video: String, #[arg(long)] message: String, /// deliver even though a clip is flagged as having filmed nothing (say why in --message)     #[arg(long)] accept_suspect: bool },+    /// Check explicit project hero/datasheet bindings, README screenshots and optional published API readback. Still requires rendered visual review.+    ProjectPageCheck { #[arg(long)] project: String, #[arg(long)] live_json: Option<String> },     /// The run's own README (the sub-README): steps, times, returns, clips and screenshots so far; --push puts it on a wiki page's repo under docs/runs/<id>/, --refresh reloads it in the human's browser. Runs by itself at every step change and at deliver once a page is set.     Report { #[arg(long)] page: Option<String>, #[arg(long)] push: bool, #[arg(long)] refresh: bool, #[arg(long)] user_machine: Option<String>, /// where the human watches: "browser" (the native browser on --user-machine, through Adom Bridge) or "webview" (a tab in this Hydrogen workspace, for an AI running inside Adom Hydrogen)     #[arg(long)] surface: Option<String> },@@ -4244,6 +4246,14 @@ fn main() {             ok(&format!("step {name}{}{}{}", if *back { " (back)" } else { "" }, prev.map(|p| format!(", leaving {p}")).unwrap_or_default(), if clip_note.is_empty() { String::new() } else { format!("; {clip_note}") }), &[hint, record_hint, back_hint, report_note, clips_note]);             print_next(&cli);         }+        Cmd::ProjectPageCheck { project, live_json } => {+            let mut command = std::process::Command::new(aiflow_python());+            command.arg("-c").arg(include_str!("../../../tools/project-page-check.py")).arg("--project").arg(project);+            if let Some(path) = live_json { command.arg("--live-json").arg(path); }+            let output = command.output().unwrap_or_else(|e| err(&format!("project page check could not run: {e}"), &[]));+            print!("{}", String::from_utf8_lossy(&output.stdout));+            if !output.status.success() { err("project page preflight failed", &["Fix explicit board GLB and project PDF bindings, readable datasheet and 5–8 README screenshots. Read skills/aiflow-project-page/SKILL.md. PDF extraction requires pdftotext.".into()]); }+        }         Cmd::Deliver { video, message, accept_suspect } => {             thread(&cli);             let mut r = load_run(&cli);
skills/adom-aiflow/SKILL.md+4
@@ -403,3 +403,7 @@ When the user explicitly authorizes automatic continuation, read `docs/codex-sup ## External heatsinks and phase scope  For a part cooled directly through its case and heatsink, read `docs/package-cooling-screen.md` and declare `packageCooling` with sourced or explicitly documented design assumptions. AIFlow checks the full-power series path and requires the same-input coupled Fields report, including all package nodes and energy balance. The PCB-only heuristic must not be used as the physical model for that external path. Preserve every power/temperature limit and retain sensitivity failures. A design-screen pass is not physical qualification. If the user schedules the probing simulator and bench tests after board design, track those as later qualification work rather than requiring them inside the design phase.++## Project wiki presentation before delivery++Read `skills/aiflow-project-page/SKILL.md` and run `adom-aiflow project-page-check --project <root>` before delivering a wiki-backed project. Explicitly bind the assembled board hero and project datasheet, include 5–8 reviewed README screenshots, place the datasheet below the README and verify the actual published page. Automatic asset discovery and upload success are not acceptance.
skills/aiflow-project-page/SKILL.mdadded+18
@@ -0,0 +1,18 @@+---+name: aiflow-project-page+description: Publish and review a great AIFlow board project wiki page with an explicit assembled-board hero GLB, 5–8 meaningful screenshots, a real project datasheet at the bottom and verified live asset bindings. Use before final delivery or when a chip or blank datasheet appears on a board page.+user-invocable: true+---+# Deliver the project page++A board project must showcase its board assembly, not one of its component assets. Never rely on recursive repository asset discovery: embedded component GLBs, STEP files and PDFs can silently win.++1. Read the live page and current immutable source HEAD before editing; preserve concurrent work. Confirm which README variant renders. Read wiki-hero-glb and the wiki publishing contract.+2. Explicitly set `component.parts.model_3d` to the reviewed complete board/assembly GLB and `component.parts.datasheet` to the project PDF in page.json. Bind any offered STEP to the real board too, never a component. Inspect the model visually; a nonempty GLB or many meshes do not prove assembly identity. Show connectors, cooling and mounting parts; label candidate revisions, missing details and clearance envelopes. Keep runtime models separate from gentle hero-only state animation.+3. Write a real project-authored engineering datasheet: revision and board identity, operating envelope and derating, modes, connectors/pinout, control protocol, power supplies, protection, mechanical/mounting dimensions, fabrication stack and qualification limits. Clearly separate design limits, simulation and measured specifications. Never repurpose a component-symbol PDF, blank placeholder or unrelated manufacturer datasheet. Inspect every rendered PDF page. Set `sections` to put `readme` before `viewers`, retaining other intended sections, so the datasheet is at the bottom.+4. Put 5–8 distinct real screenshots in the README with descriptive alt text and captions: assembled board, native board/layout, control panel, meaningful analysis, workflow and manufacturing review as available. Inspect each image. Reject blank video posters, duplicate angles, tiny unreadable UI and stale status presented as current. Caption historical revisions and simulated readings. Do not invent test results to fill the gallery.+5. Run `adom-aiflow project-page-check --project <root>`. It checks explicit assets, GLB structure, extractable PDF text (requires pdftotext), screenshot paths/count and section order. It does not certify image quality, model identity or datasheet accuracy; complete the visual review too. Keep generator sources with the project.+6. Push only the reviewed files. Fetch `/api/pages/<owner>/<slug>` into a local JSON and rerun with `--live-json <file>`; require exact model/datasheet binding equality and the intended rendered README. Verify uploaded bytes or hashes. Use one existing Pup window or Hydrogen tab according to the user's preference; never open duplicates.+7. Open the actual page, load the 3D hero, orbit it, inspect all screenshots and read the first and final datasheet pages. Verify the datasheet follows the README in live section order. Record screenshots/evidence and a short action clip where recording was requested. A successful push or HTTP200 is not rendered acceptance. Fix wrong assets before calling the project page delivered.++Call this preflight and review before final delivery whenever the run publishes a project page. It supplements, never replaces, electrical, thermal, mechanical, fabrication or simulation gates. A draft may be published with explicit limitations; do not claim final delivery while page acceptance remains open.
tests/project-page-check.pyadded+30
@@ -0,0 +1,30 @@+import importlib.util,json,struct,tempfile,unittest+from pathlib import Path+from unittest.mock import patch+s=importlib.util.spec_from_file_location('page_check',Path(__file__).resolve().parents[1]/'tools/project-page-check.py');m=importlib.util.module_from_spec(s);s.loader.exec_module(m)+class PageChecks(unittest.TestCase):+ def setUp(self):+  self.tmp=tempfile.TemporaryDirectory();self.r=Path(self.tmp.name)+  g=json.dumps({'meshes':[{}],'nodes':[{}]}).encode();g+=b' '*((-len(g))%4)+  (self.r/'board.glb').write_bytes(struct.pack('<4sIIII',b'glTF',2,20+len(g),len(g),0x4e4f534a)+g)+  (self.r/'project.pdf').write_bytes(b'%PDF-test')+  self.p={'component':{'parts':{'model_3d':'board.glb','datasheet':'project.pdf'}},'sections':['hero','readme','viewers']}+  for i in range(6):(self.r/f'{i}.png').write_bytes(b'fixture')+  (self.r/'README.md').write_text('\n'.join(f'![Review {i}]({i}.png)' for i in range(6)))+  self.mock=patch.object(m.subprocess,'run',return_value=type('Result',(),{'stdout':'Readable project specification '*100})());self.mock.start()+ def tearDown(self):self.mock.stop();self.tmp.cleanup()+ def run_check(self):+  (self.r/'page.json').write_text(json.dumps(self.p));return m.check(self.r)+ def test_complete_structure_still_requires_visual_review(self):+  r=self.run_check();self.assertTrue(r['passed']);self.assertIn('pending',r['visualAcceptance'])+ def test_implicit_component_asset_is_rejected(self):+  self.p['component']['parts']={};self.assertFalse(self.run_check()['passed'])+ def test_datasheet_before_readme_rejected(self):+  self.p['sections']=['viewers','readme'];self.assertFalse(self.run_check()['passed'])+ def test_blank_pdf_rejected(self):+  m.subprocess.run.return_value.stdout='';self.assertFalse(self.run_check()['passed'])+ def test_duplicate_images_not_six_screenshots(self):+  (self.r/'README.md').write_text('![x](0.png)\n'*6);self.assertFalse(self.run_check()['passed'])+ def test_wrong_live_model_rejected(self):+  self.run_check();live=self.r/'live.json';live.write_text(json.dumps({'metadata':{'model_3d_path':'chip.glb','datasheet_path':'project.pdf'}}));self.assertFalse(m.check(self.r,live)['passed'])+if __name__=='__main__':unittest.main()
tools/project-page-check.pyadded+51
@@ -0,0 +1,51 @@+"""Project-page preflight: explicit assets, real PDF text and reviewed screenshot inventory.+Structural checks do not prove visual quality; always inspect the published viewer.+"""+import argparse,json,re,struct,subprocess,sys+from pathlib import Path++def check(root, live=None):+    root=Path(root).resolve(); errors=[]+    def fail(s): errors.append(s)+    def local(rel):+        p=(root/rel).resolve()+        if not p.is_relative_to(root) or not p.is_file(): raise ValueError('missing/outside project: '+rel)+        return p+    page=json.loads((root/'page.json').read_text());parts=page.get('component',{}).get('parts',{})+    for key,ext in [('model_3d','.glb'),('datasheet','.pdf')]:+        val=parts.get(key)+        if not isinstance(val,str) or not val.endswith(ext):fail('Explicit component.parts.'+key+' required');continue+        try:+            p=local(val)+            if key=='model_3d':+                b=p.read_bytes();magic,version,length=struct.unpack_from('<4sII',b)+                if magic!=b'glTF' or version!=2 or length!=len(b):raise ValueError('invalid GLB header')+                n,kind=struct.unpack_from('<II',b,12);g=json.loads(b[20:20+n])+                if not g.get('meshes') or not g.get('nodes'):raise ValueError('empty GLB')+            else:+                out=subprocess.run(['pdftotext',str(p),'-'],capture_output=True,text=True,check=True).stdout+                if len(out.strip())<500:raise ValueError('datasheet has insufficient readable text')+        except Exception as e:fail(key+': '+str(e))+    sections=page.get('sections',[])+    if 'readme' not in sections or 'viewers' not in sections or sections.index('readme')>sections.index('viewers'):fail('Place README before viewers so datasheet appears below it')+    readme=(root/'README.md').read_text()+    if (root/'readme.html').exists() or (root/'readme.public.html').exists():fail('HTML shadows README.md: review rendered source explicitly before using this Markdown preflight')+    shots=re.findall(r'!\[[^\]]+\]\(([^ )]+)\)',readme)+    if not 5<=len(set(shots))<=8:fail('README needs 5–8 distinct captioned project screenshots')+    for s in shots:+        try:local(s)+        except Exception as e:fail(str(e))+    if live:+        data=json.loads(Path(live).read_text());meta=data.get('metadata',{})+        for key in ['model_3d','datasheet']:+            if meta.get(key+'_path')!=parts.get(key):fail('Published '+key+' binding differs from page.json')+        winner=data.get('readme_source_path')+        if winner and winner!='README.md':fail('Published README is shadowed by '+winner)+    return {'passed':not errors,'errors':errors,'screenshots':len(set(shots)), 'visualAcceptance':'pending human/AI inspection of actual rendered assembly, all screenshots and PDF pages; mesh count does not prove board identity'}++def main():+    ap=argparse.ArgumentParser();ap.add_argument('--project',required=True);ap.add_argument('--live-json');a=ap.parse_args()+    try:r=check(a.project,a.live_json)+    except Exception as e:r={'passed':False,'errors':[str(e)]}+    print(json.dumps(r,indent=2));return 0 if r['passed'] else 1+if __name__=='__main__':sys.exit(main())