import fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url';const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');const read=r=>JSON.parse(fs.readFileSync(path.join(root,r),'utf8'));const nodes=read('data/nodes.json'),edges=read('data/relationships.json'),candidates=read('data/candidate-ledger.json'),sources=read('research/source-ledger.json'),spreads=read('data/spreads.json'),games=read('data/games.json'),artifacts=read('reference-artifacts/metadata.json');const errors=[];const ids=new Set(nodes.map(n=>n.id));if(candidates.length<1500)errors.push('candidate count < 1500');if(nodes.length<400||nodes.length>700)errors.push('final node count outside 400–700');if(new Set(nodes.map(n=>n.id)).size!==nodes.length)errors.push('duplicate node IDs');if(nodes.some(n=>!n.research_provenance?.length))errors.push('node missing provenance');if(edges.some(e=>!ids.has(e.source)||!ids.has(e.target)))errors.push('dangling edge');if(edges.some(e=>e.predicate==='related_to'||!e.mechanism||!e.evidence_refs?.length))errors.push('invalid stable edge');if(spreads.some(s=>!fs.existsSync(path.join(root,s.tested_reading_ref))))errors.push('missing spread test');if(games.some(g=>!fs.existsSync(path.join(root,g.simulation_ref))))errors.push('missing game test');if(artifacts.length<50)errors.push('fewer than 50 reference artifacts');for(const n of nodes){const p=path.join(root,'nodes',n.node_folder,n.slug+'.md');if(!fs.existsSync(p))errors.push('missing node markdown '+n.id)}for(const s of sources){if(!/^https:\/\//.test(s.url))errors.push('invalid source URL '+s.id)}const site=['site/index.html','site/app.js','site/styles.css','site/data.js'];for(const p of site)if(!fs.existsSync(path.join(root,p)))errors.push('missing '+p);const manual=fs.readdirSync(path.join(root,'manual')).filter(x=>x.endsWith('.md'));if(manual.length<34)errors.push('manual incomplete');const md=[];function walk(d){for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);if(e.isDirectory())walk(p);else if(e.name.endsWith('.md'))md.push(p)}}walk(root);const link=/\[[^\]]+\]\((?!https?:|mailto:|#)([^)]+)\)/g;for(const f of md){const t=fs.readFileSync(f,'utf8');let m;while((m=link.exec(t))){const target=m[1].split('#')[0];if(target&&!fs.existsSync(path.resolve(path.dirname(f),target)))errors.push('broken link '+path.relative(root,f)+' -> '+target)}}console.log(JSON.stringify({result:errors.length?'FAIL':'PASS',nodes:nodes.length,edges:edges.length,candidates:candidates.length,sources:sources.length,spreads:spreads.length,games:games.length,artifacts:artifacts.length,manual_files:manual.length,markdown_files:md.length,errors},null,2));if(errors.length)process.exit(1);
