#!/usr/bin/env python3 """Build a static review and preservation receipt for the three refreshed cards.""" from pathlib import Path import hashlib,json,sys import numpy as np from PIL import Image,ImageDraw,ImageFont R=next(p for p in Path(__file__).resolve().parents if (p/'docs/card-workspace.md').is_file());OUT=Path(__file__).resolve().parent FONT=ImageFont.truetype(str(R/'fonts/P052-Bold.otf'),23) cards=[('Uncommon','BE-042-calling-of-the-first-disciples','v03',R/'in-progress/cards/BE-042-calling-of-the-first-disciples/revisions/v02'),('Rare','BE-010-burning-bush','v02',R/'artifacts/cards/BE-010-burning-bush'),('Extraordinary','BE-002-the-fall','v02',R/'artifacts/cards/BE-002-the-fall')] sys.path.insert(0,str(R/'tools/card-production'));from card_workspace import validate report={'status':'pending','cards':[],'movingLight':'not performed; pending harness review','finalCardApproval':'pending for all refreshed revisions'} for printing in ['normal','borderless']: board=Image.new('RGB',(1560,770),'#11161c');d=ImageDraw.Draw(board) for col,(tier,slug,rev,_) in enumerate(cards): root=R/'in-progress/cards'/slug/'revisions'/rev;d.text((col*520+10,10),tier+' · '+rev,font=FONT,fill='#f4ead4');board.paste(Image.open(root/'low'/printing/'card.png').convert('RGB'),(col*520+10,50)) board.save(OUT/f'cards-{printing}.png') board=Image.new('RGB',(1040,2310),'#11161c');d=ImageDraw.Draw(board);panels=Image.new('RGB',(1740,455),'#11161c');pd=ImageDraw.Draw(panels) for row,(tier,slug,rev,old) in enumerate(cards): root=R/'in-progress/cards'/slug/'revisions'/rev;validate(root) for col,(label,folder) in enumerate([('Previous',old),('Updated',root)]): d.text((col*520+10,row*770+10),tier+' · '+label,font=FONT,fill='#f4ead4');board.paste(Image.open(folder/'low/normal/card.png').convert('RGB'),(col*520+10,row*770+50)) im=Image.open(root/'high/borderless/card.png').convert('RGB') for i,crop in enumerate([(70,45,1930,375),(70,2165,1930,2755)]): pane=im.crop(crop);pane.thumbnail((560,197));pd.text((row*580+10,i*205+8),tier+(' · title' if i==0 else ' · verse / reference'),font=FONT,fill='#f4ead4');panels.paste(pane,(row*580+10,i*205+42)) v=json.loads((root/'review/build-validation.json').read_text());check=v['typography'];assert check['referenceLineBoxToFlourish']==20 and check['centeringError']<=1 # Final text should retain the same reviewed bounds as the approved baseline trial. trial=json.loads((R/'spikes/flourish-spacing-20px-proof/output-baseline/validation.json').read_text());expected=next(t for t in trial['tiers'] if t['tier']==tier.lower());assert check['referenceBaseline']==expected['referenceBaseline'];assert abs(check['upperFlourishToVerse']-expected['gapAboveVerse'])<=1 and abs(check['verseToReferenceLineBox']-expected['gapBelowVerseToLineBox'])<=1 v['visualReview']['static']='All four composed printings inspected; faces and governing gestures remain clear of the panels. Master/standard/low exports structurally checked; final user acceptance pending.';(root/'review/build-validation.json').write_text(json.dumps(v,indent=2)+'\n') report['cards'].append({'cardId':v['cardId'],'revision':rev,'stage':'assembly-review','sharedLayerVersion':'v3','referenceBaseline':check['referenceBaseline'],'lineBoxToFlourishGap':20,'verseGapAbove':check['upperFlourishToVerse'],'verseGapBelow':check['verseToReferenceLineBox'],'pngExports':36,'editableMasterSVGs':4,'buildValidation':str((root/'review/build-validation.json').relative_to(R)),'workspaceValidation':'passed','approvedBaselineComparison':{'referenceBaselineMatches':True,'verseGapRoundingDifferences':[check['upperFlourishToVerse']-expected['gapAboveVerse'],check['verseToReferenceLineBox']-expected['gapBelowVerseToLineBox']],'maximumAllowedDifference':1}}) board.save(OUT/'before-after.png');panels.save(OUT/'typography-comparison.png') # Source packages and old pinned layers were never rewritten by this update. receipt=json.loads((OUT/'preserved-input-hashes.json').read_text());assert all(hashlib.sha256(Path(p).read_bytes()).hexdigest()==sha for p,sha in receipt.items());report['preservedInputFiles']=len(receipt) # Bush has no artwork/frame change: all its text-free data must remain identical. bush=R/'in-progress/cards/BE-010-burning-bush/revisions/v02';prior=R/'artifacts/cards/BE-010-burning-bush';checks=0 for res in ['low','med','high']: for printing in ['boundless','textless']: for name in ['card.png','finish-mask.png','text-mask.png']: assert np.array_equal(np.asarray(Image.open(bush/res/printing/name).convert('RGBA')),np.asarray(Image.open(prior/res/printing/name).convert('RGBA'))),(res,printing,name);checks+=1 report.update({'status':'passed','unchangedBurningBushTextFreeFiles':checks,'totalPNGExports':108,'totalMasterSVGs':12});(OUT/'validation.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report,indent=2))