#!/usr/bin/env node /** * Framework validation — project-status.yaml schema v2. * Docs-only; no business service inspection. */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); const statusPath = path.join(root, "docs", "project-status.yaml"); const mdPath = path.join(root, "docs", "project-status.md"); const errors = []; function fail(msg) { errors.push(msg); } if (!fs.existsSync(statusPath)) fail("missing docs/project-status.yaml"); if (!fs.existsSync(mdPath)) fail("missing docs/project-status.md"); const raw = fs.readFileSync(statusPath, "utf8"); const requiredTop = [ "schema_version:", "project_version:", "last_updated:", "last_completed_phase:", "resume_rules:", "automatic_update_rules:", "critical_path:", "execution_priority:", "services:", "global_statistics:", "deployment_readiness:", "depends_on:", "required_by:", "completed_phase:", "remaining_phase:", "backend_percent:", "frontend_percent:", "overall_percent:", ]; for (const key of requiredTop) { if (!raw.includes(key)) fail(`missing required key/section: ${key}`); } if (raw.match(/^next_recommended_phase:/m)) { fail("next_recommended_phase must not be an active top-level field (deprecated)"); } if (!raw.includes("replaced_by:") || !raw.includes("execution_priority")) { fail("deprecated.next_recommended_phase replacement markers missing"); } const priorities = ["critical:", "high:", "medium:", "low:", "future:"]; for (const p of priorities) { if (!raw.includes(p)) fail(`execution_priority missing bucket: ${p}`); } const readinessValues = ["NOT_STARTED", "IN_PROGRESS", "COMPLETE"]; const readinessKeys = [ "backend:", "frontend:", "database:", "api:", "tests:", "documentation:", "production_ready:", ]; for (const k of readinessKeys) { if (!raw.includes(` ${k}`) && !raw.includes(` ${k}`)) { // soft: at least one deployment_readiness block must exist with keys } } if (!raw.includes("deployment_readiness:")) fail("deployment_readiness missing"); const md = fs.readFileSync(mdPath, "utf8"); if (!md.includes("critical_path") && !md.includes("Critical path")) { fail("project-status.md must document critical path"); } if (!md.includes("execution_priority") && !md.includes("Execution priority")) { fail("project-status.md must document execution priority"); } if (md.includes("Next recommended phase") && !md.includes("Deprecated")) { fail("project-status.md still presents next_recommended_phase as active"); } // Collect phase IDs under execution_priority buckets (naive line scan) const lines = raw.split("\n"); let bucket = null; const seen = new Map(); for (const line of lines) { if (/^\s{2}(critical|high|medium|low|future):\s*$/.test(line)) { bucket = line.trim().replace(":", ""); continue; } if (bucket && /^\s{4}-\s+[\w.-]+/.test(line)) { const id = line.trim().replace(/^- /, ""); if (seen.has(id)) fail(`phase ${id} appears in both ${seen.get(id)} and ${bucket}`); seen.set(id, bucket); } if (/^[a-z_]/.test(line) && !line.startsWith(" ")) bucket = null; } if (seen.size < 10) fail(`expected many priority phases, got ${seen.size}`); if (errors.length) { console.error("project-status validation FAILED:"); for (const e of errors) console.error(" -", e); process.exit(1); } console.log(`project-status validation OK (schema v2, ${seen.size} prioritized phases)`);