from pathlib import Path
from html.parser import HTMLParser
import hashlib,json,re,xml.etree.ElementTree as ET,base64
root=Path(__file__).resolve().parent
artbook=root.parents[1]
prior=json.loads((root/'notes/prior-assets-sha256.json').read_text())
changed=[p for p,h in prior.items() if not (artbook/p).is_file() or hashlib.sha256((artbook/p).read_bytes()).hexdigest()!=h]
assert not changed,changed
class Links(HTMLParser):
 def __init__(self):super().__init__();self.links=[];self.ids=[]
 def handle_starttag(self,tag,attrs):
  d=dict(attrs)
  if 'id' in d:self.ids.append(d['id'])
  self.links.extend(v for k,v in attrs if k in ('href','src') and not v.startswith(('data:','#')))
html=(root/'index.html').read_text();parser=Links();parser.feed(html)
assert len(parser.ids)==len(set(parser.ids)),'duplicate HTML ids'
for link in parser.links:assert (root/link).is_file(),link
for p in root.rglob('*.md'):
 for link in re.findall(r'\]\(([^)]+)\)',p.read_text()):
  if not link.startswith(('https:','http:','#')) and (p.parent/link).resolve()!=(root/'notes/verification.json').resolve():assert (p.parent/link).is_file(),str(p)+':'+link
manifest=json.loads((root/'motion-manifest.json').read_text());geometry=json.loads((root/'atlas-geometry.json').read_text())
assert len(manifest['clips'])==18
ns={'s':'http://www.w3.org/2000/svg'}
for clip in manifest['clips']:
 atlas=(root/clip['atlas']).read_bytes()
 geom=geometry[Path(clip['atlas']).stem]
 assert geom['alphaExtrema'][0]==0
 assert len(geom['cells'])==24
 for i in clip['cells']:
  cell=geom['cells'][i];b=cell['solidBounds'];px,py=cell['placement']
  assert px+b[0]>=0 and px+b[2]<=300
  assert py+b[1]>=0 and abs(py+b[3]-282)<.001
 svgpath=root/clip['standalone'];svg=ET.parse(svgpath).getroot()
 ids={e.get('id') for e in svg.iter() if e.get('id')}
 for e in svg.iter():
  href=e.get('href','')
  if href.startswith('#'):assert href[1:] in ids
  elif href.startswith('data:image/png;base64,'):assert base64.b64decode(href.split(',',1)[1])==atlas
  elif href:raise AssertionError(href)
 styles=svg.find('s:style',ns).text
 assert 'prefers-reduced-motion' in styles
 assert len(svg.findall('s:g',ns))==len(clip['cells'])
 if clip['repeat']=='once':assert clip['cells'][0]==clip['cells'][-1]
assert 'aria-pressed' in html and 'Pause motion' in html
assert 'prefers-reduced-motion' in html
assert 'animation-play-state:paused' in html
assert 'Replay celebration' in html
report={'date':'2026-10-08','source':'artbook/editions/soft-3d-motion-v2','sourceAnchors':'first-version/characters/{summit,ridge,terra}-sheet.png','priorFilesUnchanged':len(prior),'changedPriorFiles':changed,'atlases':6,'sourceFrames':144,'standaloneAnimations':18,'alpha':'present, preserved from built-in imagegen','solidFrameBounds':'all selected frames fit 300x300 viewport with baseline 282','embeddedImageHashes':'all match unmodified source atlas','localHTMLLinksChecked':len(parser.links),'localLinks':'pass','XMLAndInternalReferences':'pass','reducedMotionAndPauseRules':'present','artworkVisualInspection':'all six atlases inspected through view_image; mixed thinking/map cells omitted','browserPlayback':'unverified; no file-URL restriction workaround','limitations':['Rendered frame sequences, not a 3D rig.','Eight proposed frames per state; playback uses selected cells and holds.','Writing contact and small material details need final art polish.','No live browser validation of playback, pause, replay, or responsive layout.']}
(root/'notes/verification.json').write_text(json.dumps(report,indent=2)+'\n')
files=[{'path':str(p.relative_to(root)),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in sorted(root.rglob('*')) if p.is_file() and p.name!='edition-manifest.json']
(root/'edition-manifest.json').write_text(json.dumps({'edition':'soft-3d-motion-v2','status':'motion-art-study','files':files},indent=2)+'\n')
print(json.dumps(report))
