check-consistency.mjs
Valida que cada ID de requisito citado en el prototipo exista en un PRD, y avisa de los requisitos sin ancla.
#!/usr/bin/env node
/* Argui — validación de consistencia prototipo ↔ PRD
*
* Verifica que cada ID de requisito referido en el prototipo (atributos data-prd, p. ej.
* `RF-12 · RDA automática`) esté definido en algún PRD, y avisa de requisitos del PRD que
* no tienen ningún ancla en el prototipo.
*
* Se valida por ID estable y no por número de sección: el ID viaja con el requisito cuando
* el PRD inicial se parte en PRD de negocio y PRD técnico (F3.4).
*
* Uso:
* node check-consistency.mjs --proto prototipo.html --prd prd.md
* node check-consistency.mjs --proto "*.html" --prd prd-business.md prd-technical.md
*
* Sin dependencias externas (Node 18+). Sale con código != 0 si hay referencias rotas.
*/
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, dirname } from "node:path";
function parseArgs(argv) {
const out = { proto: [], prd: [] };
let bucket = null;
for (const a of argv.slice(2)) {
if (a === "--proto") bucket = "proto";
else if (a === "--prd") bucket = "prd";
else if (bucket) out[bucket].push(a);
}
return out;
}
// Expansión mínima de un glob tipo "dir/*.html" o "dir/**.html" (sin dependencias).
function expand(patterns) {
const files = [];
for (const p of patterns) {
if (!/[*?]/.test(p)) { files.push(p); continue; }
const star = p.indexOf("*");
const base = p.slice(0, star).replace(/\/$/, "") || ".";
const recursive = p.includes("**");
const ext = (p.match(/\.([a-z0-9]+)$/i) || [, ""])[1].toLowerCase();
walk(base, recursive, ext, files);
}
return [...new Set(files)];
}
function walk(dir, recursive, ext, acc) {
let entries;
try { entries = readdirSync(dir); } catch { return; }
for (const name of entries) {
const full = join(dir, name);
let st;
try { st = statSync(full); } catch { continue; }
if (st.isDirectory()) { if (recursive) walk(full, recursive, ext, acc); }
else if (!ext || name.toLowerCase().endsWith("." + ext)) acc.push(full);
}
}
// IDs estables de requisito / métrica: RF-12, MET-3, RNF-7…
const ID_RE = /\b([A-Z]{2,5}-\d+)\b/g;
function refsFromProto(files) {
const refs = new Map(); // ID -> [archivos]
for (const f of files) {
let html;
try { html = readFileSync(f, "utf8"); } catch { continue; }
for (const m of html.matchAll(/data-prd\s*=\s*"([^"]*)"/g)) {
for (const s of m[1].matchAll(ID_RE)) {
const ref = s[1];
if (!refs.has(ref)) refs.set(ref, []);
refs.get(ref).push(f);
}
}
}
return refs;
}
// Solo cuentan las DEFINICIONES del ID —fila de tabla, encabezado o ítem en negrita—,
// no las menciones en prosa: así una mención con el ID mal escrito sale como rota.
const DEF_RES = [
/^\s*\|\s*([A-Z]{2,5}-\d+)\s*\|/,
/^#{1,6}\s+([A-Z]{2,5}-\d+)\b/,
/^\s*[-*]\s+\*\*([A-Z]{2,5}-\d+)\*\*/
];
function idsFromPrd(files) {
const ids = new Set();
for (const f of files) {
let md;
try { md = readFileSync(f, "utf8"); } catch { continue; }
for (const line of md.split("\n")) {
for (const re of DEF_RES) {
const m = line.match(re);
if (m) ids.add(m[1]);
}
}
}
return ids;
}
const args = parseArgs(process.argv);
if (!args.proto.length || !args.prd.length) {
console.error("Uso: node check-consistency.mjs --proto <htmls> --prd <mds>");
process.exit(2);
}
const protoFiles = expand(args.proto);
const prdFiles = expand(args.prd);
const refs = refsFromProto(protoFiles);
const ids = idsFromPrd(prdFiles);
console.log(`Prototipo: ${protoFiles.length} archivo(s), ${refs.size} ID(s) referenciado(s).`);
console.log(`PRD: ${prdFiles.length} archivo(s), ${ids.size} ID(s) definido(s).\n`);
const broken = [];
for (const [ref, where] of refs) {
if (!ids.has(ref)) broken.push({ ref, where });
}
const orphans = [];
for (const id of ids) {
if (!refs.has(id)) orphans.push(id);
}
if (broken.length) {
console.log("❌ IDs referidos en el prototipo que NO están definidos en el PRD:");
for (const b of broken) console.log(` ${b.ref} ← ${[...new Set(b.where)].join(", ")}`);
console.log("");
}
if (orphans.length) {
console.log("⚠️ Requisitos del PRD sin ancla en el prototipo (revisar si aplica):");
console.log(" " + orphans.sort().join(", ") + "\n");
}
if (!broken.length) console.log("✅ Todo ID referido en el prototipo está definido en el PRD.");
process.exit(broken.length ? 1 : 0);