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.
main
John Lauer
Publish 0.2.49
4ec8e35
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
"""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',{})
# issue #137: a molecule page's model is the registry's, built from the sources molecule-publish pushes
molecule='molecule' in (page.get('kind'),page.get('component',{}).get('kind'),page.get('hardware_kind'))
suggestions=[]
if molecule and parts.get('model_3d'):
suggestions.append('This is a molecule page: drop component.parts.model_3d ('+str(parts.get('model_3d'))+') and do not push a GLB; the wiki model registry builds the model from the sources molecule-publish publishes (it refuses a pushed GLB: REGISTRY_MODEL_FILE_REFUSED).')
for key,ext in [('model_3d','.glb'),('datasheet','.pdf')]:
if molecule and key=='model_3d':continue
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 ['datasheet'] if molecule else ['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,'suggestions':suggestions,'molecule':molecule,'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())