main
John Lauer Publish 0.2.49 4ec8e35
"""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())