import {readFileSync,writeFileSync} from 'node:fs'; import {createHash} from 'node:crypto'; const local=p=>new URL(p,import.meta.url); const tokens=JSON.parse(readFileSync(local('tokens.json'),'utf8')); function luminance(hex){ if(!/^#[0-9a-f]{6}$/i.test(hex))throw Error('Expected opaque sRGB: '+hex); const c=[1,3,5].map(i=>parseInt(hex.slice(i,i+2),16)/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4); return c[0]*.2126+c[1]*.7152+c[2]*.0722; } const resolve=k=>tokens.color[k]??k; const rows=tokens.contrastPairs.map(p=>{const foreground=resolve(p.fg),background=resolve(p.bg),a=luminance(foreground),b=luminance(background),ratio=(Math.max(a,b)+.05)/(Math.min(a,b)+.05);return {...p,foreground,background,ratio,pass:p.minimum===null?null:ratio>=p.minimum};}); writeFileSync(local('contrast-audit.json'),JSON.stringify({method:'WCAG relative luminance; opaque sRGB; unrounded thresholds; static token pairs only, not complete UI compliance',rows},null,2)+'\n'); console.table(rows.map(p=>({pair:p.name,ratio:p.ratio.toFixed(3),minimum:p.minimum,result:p.pass===null?'diagnostic':p.pass?'pass':'FAIL'}))); const tracked=[ '../../../README.md', '../../../wechat-campaign-200-r2/README.md', '../../../wechat-campaign-200-r2/src/upstream/levels.json', '../../../wechat-campaign-200-r2/src/upstream/rules.mjs', '../../../wechat-campaign-200-r2/src/main.mjs', '../../../wechat-campaign-200-r2/src/hints.mjs', '../../../modes/01-number-stroke/src/board.ts', '../../../modes/01-number-stroke/src/game.ts', '../../../modes/03-hex-walls/src/board.ts' ]; // Paths above resolve from docs/design/bright-classroom/ to repository root. const sources=tracked.map(path=>{const bytes=readFileSync(local(path));return {path,sha256:createHash('sha256').update(bytes).digest('hex')};}); const p=JSON.parse(readFileSync(local('../../../wechat-campaign-200-r2/src/upstream/levels.json'),'utf8')); const countBy=fn=>p.levels.reduce((out,l)=>{const k=fn(l);out[k]=(out[k]??0)+1;return out;},{}); const extent=l=>{if(l.geometry==='hex')return null;const points=l.cells.flatMap(c=>c.shape?c.shape.map(([x,y])=>[c.x+x,c.y+y]):Array.from({length:(c.w??1)*(c.h??1)},(_,i)=>[c.x+i%(c.w??1),c.y+Math.floor(i/(c.w??1))]));return {cols:Math.max(...points.map(p=>p[0]))-Math.min(...points.map(p=>p[0]))+1,rows:Math.max(...points.map(p=>p[1]))-Math.min(...points.map(p=>p[1]))+1};}; const examples=p.levels.map(l=>({id:l.id,chapter:l.chapter,geometry:l.geometry,features:l.features,cells:l.cells.length,...extent(l)})); const audit={date:'2026-10-21',scope:'Read-only design inventory; not solver validation',sources,campaign:{levels:p.levels.length,chapters:p.chapters.length,playOrder:p.playOrder.length,geometry:countBy(l=>l.geometry),featureCounts:Object.fromEntries([...new Set(p.levels.flatMap(l=>l.features))].map(f=>[f,p.levels.filter(l=>l.features.includes(f)).length])),minCells:Math.min(...p.levels.map(l=>l.cells.length)),maxCells:Math.max(...p.levels.map(l=>l.cells.length)),nonemptyConstraintCounts:Object.fromEntries(['relations','orderChains','windows','walls','doors','gates'].map(k=>[k,p.levels.filter(l=>l[k]?.length).length]))},examples}; writeFileSync(local('project-audit.json'),JSON.stringify(audit,null,2)+'\n'); console.log(JSON.stringify(audit.campaign,null,2)); if(rows.some(p=>p.pass===false))process.exitCode=1;