116 lines
3.6 KiB
JavaScript
116 lines
3.6 KiB
JavaScript
#!/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)`);
|