Files
Sanctification/in-progress/review/2026-09-14-card-refresh/build-review.py

40 lines
4.8 KiB
Python

#!/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))