import fs from 'node:fs';import crypto from 'node:crypto';import {solve,violations,constraints,assertStructure} from '../rules.mjs';import {measure} from './score.mjs';import {signature} from './fingerprint.mjs';const pack=JSON.parse(fs.readFileSync('levels.json')),notes=JSON.parse(fs.readFileSync('design.json')),rows=[],seen=new Set(),introduced=new Set();let checks=0;function check(ok,s){checks++;if(!ok)throw Error(s);}check(pack.levels.length===200,'必须200关');const teacherPositions={walls:5,direction:9,alternation:17,parity:23,'rhythm-door':29,hex:35};let lastUnknown=-100;for(const n of notes){const l=n.level;assertStructure(l);check(!l.cells.some(c=>c.direction&&c.parity),'奇偶方向同格 '+l.order);check(!['big','irregular'].includes(l.tutorial),'不应有大块教学');for(const f of [...l.features,...(l.geometry==='hex'?['hex']:[])])if(teacherPositions[f]&&!introduced.has(f)){check(l.order===teacherPositions[f]&&Boolean(l.tutorial),'首次机制教学 '+f);introduced.add(f);}const answers=solve(l,{limit:2,nodeLimit:1200000});check(answers.length===1&&answers[0].join()===n.author.join(),'非唯一或作者错误 '+l.order);for(const r of constraints(l)){const alt=solve(l,{omit:r.key,limit:2,nodeLimit:1500000});check(alt.length===2,'冗余机制 '+l.order+' '+r.key);const w=n.witnesses.find(w=>w.constraint===r.key);check(w&&!violations(l,w.path,{omit:r.key}).length&&violations(l,w.path).some(v=>v.key===r.key),'反例错误 '+l.order);} const score=measure(l,n.author);check(score.score===l.objectiveScore,'评分不一致 '+l.order);if(!l.tutorial){const fp=signature(l);check(!seen.has(fp),'镜像重复 '+l.order);seen.add(fp);}if(!l.cells.some(c=>c.clue===1)){check(l.order-lastUnknown>=20,'无起点太密集');lastUnknown=l.order;}if(l.geometry==='hex'&&!l.tutorial){check(Math.max(...l.cells.map(c=>c.q))-Math.min(...l.cells.map(c=>c.q))>=3&&Math.max(...l.cells.map(c=>c.r))-Math.min(...l.cells.map(c=>c.r))>=3,'六边条带');check(n.spatial.qReversals>=2&&n.spatial.rReversals>=2&&n.spatial.maxRun<=5,'六边单调路线');}if(l.order>100&&!l.playerBand&&!l.tutorial){check(l.cells.length>=24,'后段棋盘太小');if(l.estimatedBand==='略难候选')check(score.score>=48&&score.score<=72,'略难候选分档');if(l.estimatedBand==='深想候选')check(score.score>=73,'深想候选分档');}rows.push({order:l.order,title:l.title,geometry:l.geometry,nodes:l.cells.length,features:l.features,objectiveScore:score.score,decisions:score.decisions,nonlocalBranches:score.nonlocalBranches,meanWrongDepth:score.meanWrongDepth,unknownStart:!l.cells.some(c=>c.clue===1),playerBand:l.playerBand,estimatedBand:l.estimatedBand,source:n.source});} check(rows.filter(r=>r.unknownStart).length===5,'未知起点数量');for(const filename of ['index.html','NumberSwipe-200关DEMO.html']){const html=fs.readFileSync(filename,'utf8');check(!html.includes('"author":')&&!html.includes('"witnesses":'),'试玩混入答案');check(!/]+src=|fetch\(/.test(html),'不是离线单文件');check(html.includes('.caption,#constraints{display:none!important}'),'常驻提示未隐藏');} const baseline=JSON.parse(fs.readFileSync('reports/previous-before.json'));const changed=baseline.filter(b=>!fs.existsSync(b.path)||crypto.createHash('sha256').update(fs.readFileSync(b.path)).digest('hex')!==b.sha256);check(changed.length===0,'旧200关被修改');const unknown=rows.filter(r=>r.unknownStart).map(r=>r.order);const report={checkedAt:new Date().toISOString(),passed:200,total:200,checks,old200FilesChecked:baseline.length,old200FilesChanged:changed,unknownPositions:unknown,maxNodes:Math.max(...rows.map(r=>r.nodes)),scope:'唯一性、必要性、评分重算、教学时机、重复和UI风险;不是真人通关率预测',rows};fs.writeFileSync('reports/validation.json',JSON.stringify(report,null,2));console.log('PASS 200/200;',checks,'checks; unknown',unknown.join(','),'max nodes',report.maxNodes);fs.writeFileSync('reports/VALIDATION.md','# 评分修订版验收\n\n200关唯一解与每项机制必要;检查'+checks+'项。原200关'+baseline.length+'个文件校验值不变。\n\n未知起点:'+unknown.join('、')+';最大'+report.maxNodes+'格。\n');