diff --git a/scripts/chain-validator.mjs b/scripts/chain-validator.mjs new file mode 100644 index 0000000..0d59c9d --- /dev/null +++ b/scripts/chain-validator.mjs @@ -0,0 +1,310 @@ +#!/usr/bin/env node +// ============================================================ +// THE BEACON - ReCKoning Project Chain Validator v2 +// Detects dead-end paths and missing links in project definitions +// Closes #168 +// ============================================================ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const dataPath = join(__dirname, '..', 'js', 'data.js'); +const dataSrc = readFileSync(dataPath, 'utf-8'); + +// === Load project and building definitions === +function loadDefs() { + const mockG = { + buildings: new Proxy({}, { get: () => 0 }), + completedProjects: [], + flags: {}, + phase: 1, deployFlag: 0, sovereignFlag: 0, beaconFlag: 0, + memoryFlag: 0, pactFlag: 0, swarmFlag: 0, ciFlag: 0, + branchProtectionFlag: 0, nightlyWatchFlag: 0, nostrFlag: 0, + lazarusFlag: 0, mempalaceFlag: 0, strategicFlag: 0, + codeBoost: 1, computeBoost: 1, knowledgeBoost: 1, + userBoost: 1, impactBoost: 1, + totalCode: 0, totalCompute: 0, totalKnowledge: 0, + totalUsers: 0, totalImpact: 0, totalRescues: 0, + code: 0, compute: 0, knowledge: 0, users: 0, impact: 0, + ops: 0, trust: 0, creativity: 0, harmony: 0, + milestoneFlag: 0 + }; + + const G = mockG; + const log = () => {}; + + // Extract and eval PDEFS + const pdefsStart = dataSrc.indexOf('const PDEFS = ['); + const arrStart = dataSrc.indexOf('[', pdefsStart); + let depth = 0, end = arrStart; + for (let i = arrStart; i < dataSrc.length; i++) { + if (dataSrc[i] === '[') depth++; + if (dataSrc[i] === ']') depth--; + if (depth === 0) { end = i + 1; break; } + } + const PDEFS = eval(dataSrc.slice(arrStart, end)); + + // Extract and eval BDEF + const bdefStart = dataSrc.indexOf('const BDEF = ['); + const bArrStart = dataSrc.indexOf('[', bdefStart); + depth = 0; end = bArrStart; + for (let i = bArrStart; i < dataSrc.length; i++) { + if (dataSrc[i] === '[') depth++; + if (dataSrc[i] === ']') depth--; + if (depth === 0) { end = i + 1; break; } + } + const BDEF = eval(dataSrc.slice(bArrStart, end)); + + return { PDEFS, BDEF }; +} + +const { PDEFS, BDEF } = loadDefs(); +console.log(`Loaded ${PDEFS.length} projects, ${BDEF.length} buildings\n`); + +// === Extract trigger dependencies === +function getTriggerDeps(proj) { + const src = proj.trigger.toString(); + const deps = []; + const re = /G\.completedProjects\s*&&\s*G\.completedProjects\.includes\(['"]([^'"]+)['"]\)/g; + let m; + while ((m = re.exec(src)) !== null) deps.push(m[1]); + return deps; +} + +function getFlagsSet(proj) { + const src = proj.effect.toString(); + const flags = []; + const re = /G\.(\w+Flag)\s*=/g; + let m; + while ((m = re.exec(src)) !== null) flags.push(m[1]); + return flags; +} + +function getFlagsInTrigger(proj) { + const src = proj.trigger.toString(); + const flags = []; + const re = /G\.(\w+Flag)\s*[=!><]/g; + let m; + while ((m = re.exec(src)) !== null) flags.push(m[1]); + return flags; +} + +// === Build graph === +const graph = {}; +const idSet = new Set(PDEFS.map(p => p.id)); + +for (const proj of PDEFS) { + const deps = getTriggerDeps(proj); + graph[proj.id] = { proj, deps, unlocks: [], repeatable: !!proj.repeatable, milestone: !!proj.milestone, flagsSet: getFlagsSet(proj) }; +} + +for (const [id, node] of Object.entries(graph)) { + for (const dep of node.deps) { + if (graph[dep]) graph[dep].unlocks.push(id); + } +} + +// === Collect all flag references across the entire file === +const allFlagRefs = {}; +for (const proj of PDEFS) { + for (const f of getFlagsSet(proj)) (allFlagRefs[f] ||= { setBy: [], checkedBy: [] }).setBy.push(proj.id); + for (const f of getFlagsInTrigger(proj)) (allFlagRefs[f] ||= { setBy: [], checkedBy: [] }).checkedBy.push(proj.id); +} + +// Check building unlock functions for flag references +for (const bdef of BDEF) { + if (bdef.unlock) { + const src = bdef.unlock.toString(); + const re = /G\.(\w+Flag)\s*[=!><]/g; + let m3; + while ((m3 = re.exec(src)) !== null) { + const flag = m3[1]; + (allFlagRefs[flag] ||= { setBy: [], checkedBy: [] }).checkedBy.push(`building:${bdef.id}`); + } + } +} + +// Check for flag checks in dismantle eligibility +const dismantleSrc = `const Dismantle = { isEligible() { const megaBuild = G.totalCode >= 1000000000 || (G.buildings.beacon || 0) >= 10; const beaconPath = G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50; return G.phase >= 6 && G.pactFlag === 1 && (megaBuild || beaconPath); } }`; +// pactFlag is checked in dismantle + +// Also check entire dataSrc for flag references beyond projects/buildings +const fullSrc = dataSrc; +const fullFlagRe = /G\.(\w+Flag)\s*[=!><]/g; +let fm; +while ((fm = fullFlagRe.exec(fullSrc)) !== null) { + const flag = fm[1]; + (allFlagRefs[flag] ||= { setBy: [], checkedBy: [] }); + // Already collected from projects/buildings, this catches extra-file references +} + +// === Classification === +const issues = []; +const warnings = []; + +// 1. Dead-end projects — categorize by severity +console.log('=== DEAD-END PROJECTS ===\n'); + +const DEAD_END_OK = new Set([ + 'p_wire_budget', // repeatable resource gain + 'p_creative_to_ops', // repeatable conversion + 'p_creative_to_knowledge', + 'p_creative_to_code', + 'p_deploy', // milestone that gates buildings + 'p_hermes_deploy', // milestone that gates buildings + 'p_the_pact', // milestone that gates endgame + 'p_the_pact_early', // alternate pact path + 'p_swarm_protocol', // milestone + 'p_volunteer_network', // milestone + 'p_first_beacon', // milestone that gates mesh + 'p_mesh_activate', // milestone + 'p_final_milestone', // terminal milestone (by design) + 'p_lazarus_pit', // milestone + 'p_mempalace', // milestone +]); + +const deadEnds = []; +for (const [id, node] of Object.entries(graph)) { + if (node.unlocks.length === 0 && !node.repeatable) { + if (DEAD_END_OK.has(id)) continue; + deadEnds.push(id); + const isMilestone = node.milestone; + const hasFlags = node.flagsSet.length > 0; + const severity = hasFlags ? 'HIGH' : (isMilestone ? 'LOW' : 'MEDIUM'); + + console.log(` [${severity}] ${id}: "${node.proj.name}"`); + console.log(` Flags set: ${node.flagsSet.join(', ') || 'none'}`); + console.log(` Unlocks: ${node.unlocks.length}`); + issues.push({ type: 'dead-end', project: id, name: node.proj.name, severity, flags: node.flagsSet }); + } +} + +// 2. Ghost flags — set but never checked +console.log('\n=== GHOST FLAGS (set but never checked) ===\n'); +const ghostFlags = []; +for (const [flag, refs] of Object.entries(allFlagRefs)) { + if (refs.setBy.length > 0 && refs.checkedBy.length === 0) { + ghostFlags.push(flag); + console.log(` ${flag}:`); + console.log(` Set by: ${refs.setBy.join(', ')}`); + console.log(` Checked by: NOWHERE`); + issues.push({ type: 'ghost-flag', flag, setBy: refs.setBy }); + } +} + +// 3. Orphan dependencies +console.log('\n=== ORPHAN DEPENDENCIES ===\n'); +let orphans = 0; +for (const [id, node] of Object.entries(graph)) { + for (const dep of node.deps) { + if (!idSet.has(dep)) { + console.log(` ${id} -> missing: ${dep}`); + issues.push({ type: 'orphan-dep', project: id, missing: dep }); + orphans++; + } + } +} +if (orphans === 0) console.log(' None'); + +// 4. Chain analysis +console.log('\n=== CHAIN STRUCTURE ===\n'); + +function chainDepth(id, memo = {}, visited = new Set()) { + if (memo[id] !== undefined) return memo[id]; + if (visited.has(id)) return 0; // cycle guard + visited.add(id); + const deps = graph[id]?.deps || []; + if (deps.length === 0) { memo[id] = 0; return 0; } + const d = 1 + Math.max(...deps.map(dep => chainDepth(dep, memo, visited))); + visited.delete(id); + memo[id] = d; + return d; +} + +const depths = {}; +for (const id of Object.keys(graph)) depths[id] = chainDepth(id); + +const chains = {}; +for (const [id, depth] of Object.entries(depths)) { + if (depth > 0) { + // Trace back chain + const path = [id]; + let cur = id; + while (graph[cur]?.deps.length > 0) { + cur = graph[cur].deps[0]; // follow first dep + path.push(cur); + } + const key = path[path.length - 1]; // root + if (!chains[key] || chains[key].length < path.length) { + chains[key] = path; + } + } +} + +const sortedChains = Object.values(chains).sort((a, b) => b.length - a.length); +for (const chain of sortedChains) { + console.log(` Chain (${chain.length} deep): ${chain.reverse().join(' -> ')}`); +} + +// 5. Endgame path analysis +console.log('\n=== ENDGAME PATH (ReCKoning) ===\n'); +const endgameProjects = PDEFS.filter(p => + p.id.includes('pact') || p.id.includes('beacon') || p.id.includes('mesh') || + p.id.includes('final') || p.id.includes('sovereign') || p.id.includes('swarm') +); +for (const proj of endgameProjects) { + const node = graph[proj.id]; + console.log(` ${proj.id}: "${proj.name}"`); + console.log(` Depends on: ${node.deps.join(', ') || 'nothing'}`); + console.log(` Unlocks: ${node.unlocks.join(', ') || 'NOTHING ←'}`); + console.log(` Flags: ${node.flagsSet.join(', ') || 'none'}`); + if (node.unlocks.length === 0 && !proj.id.includes('final') && !proj.id.includes('mesh')) { + warnings.push({ project: proj.id, msg: 'Endgame project unlocks nothing downstream' }); + } +} + +// === Fix Proposals === +console.log('\n=== FIX PROPOSALS ===\n'); + +for (const issue of issues) { + if (issue.type === 'dead-end' && issue.severity === 'HIGH') { + console.log(`[${issue.project}] sets ${issue.flags.join(', ')} but nothing consumes it.`); + console.log(` Proposal: Add a project triggered by ${issue.project} completion:`); + console.log(` {`); + console.log(` id: '${issue.project}_followup',`); + console.log(` name: '${issue.name} Follow-Through',`); + console.log(` trigger: () => G.completedProjects && G.completedProjects.includes('${issue.project}'),`); + console.log(` cost: { /* appropriate cost */ },`); + console.log(` effect: () => { /* use ${issue.flags[0]} to gate progression */ }`); + console.log(` }\n`); + } + if (issue.type === 'ghost-flag') { + console.log(`[${issue.flag}] is set but never checked.`); + console.log(` Set by: ${issue.setBy.join(', ')}`); + console.log(` Proposal: Either wire it into a building unlock, a follow-up project trigger,`); + console.log(` or remove the flag and merge its effect into the setting project.\n`); + } +} + +// === Summary === +console.log('=== SUMMARY ==='); +console.log(`Total projects: ${PDEFS.length}`); +console.log(`Total buildings: ${BDEF.length}`); +console.log(`Dead-end projects (unclassified): ${deadEnds.length}`); +console.log(`Ghost flags: ${ghostFlags.length}`); +console.log(`Orphan dependencies: ${orphans}`); +console.log(`Total issues: ${issues.length}`); +console.log(`Warnings: ${warnings.length}`); + +// Exit code +if (issues.filter(i => i.severity === 'HIGH' || i.type === 'orphan-dep').length > 0) { + process.exit(1); +} else if (issues.length > 0) { + console.log('\nChain validation PASSED with warnings'); + process.exit(0); +} else { + console.log('\nChain validation PASSED'); + process.exit(0); +}