Files

35 lines
3.2 KiB
JavaScript

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;