mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-01 07:23:11 +00:00
Map docStatus through the real update payload, serialize concurrent table patches per work order, and split CompDoc UI for governance.
290 lines
9.1 KiB
JavaScript
290 lines
9.1 KiB
JavaScript
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(SCRIPT_DIR, "..");
|
|
const BASELINE_PATH = path.join(SCRIPT_DIR, "governance-baseline.json");
|
|
|
|
const MAX_FILE_LINES = 500;
|
|
const MAINTAINABILITY_RULES = [
|
|
'complexity: ["error", { "max": 20 }]',
|
|
'max-lines-per-function: ["error", { "skipComments": true, "max": 150 }]',
|
|
'max-params: ["error", 4]',
|
|
'max-depth: ["error", 4]',
|
|
];
|
|
const GOVERNED_ROOTS = ["src/", "config/"];
|
|
const EXCLUDE_DIR = /(^|\/)(mocks|test|__mocks__|node_modules|dist|coverage|e2e)\//;
|
|
const EXCLUDE_NAME = /\.(mock|test|spec)\.(ts|tsx)$|\.d\.ts$/;
|
|
|
|
function isGoverned(relativePath) {
|
|
return (
|
|
GOVERNED_ROOTS.some((root) => relativePath.startsWith(root)) &&
|
|
/\.(ts|tsx)$/.test(relativePath) &&
|
|
!EXCLUDE_DIR.test(relativePath) &&
|
|
!EXCLUDE_NAME.test(relativePath)
|
|
);
|
|
}
|
|
|
|
function gitText(args) {
|
|
return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim();
|
|
}
|
|
|
|
function gitLines(args) {
|
|
return gitText(args).split("\n").filter(Boolean);
|
|
}
|
|
|
|
function governedFiles() {
|
|
const tracked = gitLines(["ls-files"]);
|
|
const untracked = gitLines(["ls-files", "--others", "--exclude-standard"]);
|
|
return [...new Set([...tracked, ...untracked])].filter(isGoverned);
|
|
}
|
|
|
|
function lineCount(relativePath) {
|
|
const content = readFileSync(path.join(ROOT, relativePath), "utf8");
|
|
if (content.length === 0) return 0;
|
|
return content.endsWith("\n") ? content.split("\n").length - 1 : content.split("\n").length;
|
|
}
|
|
|
|
function readBaseline() {
|
|
return JSON.parse(readFileSync(BASELINE_PATH, "utf8"));
|
|
}
|
|
|
|
function readBaselineAtRef(ref) {
|
|
try {
|
|
const content = execFileSync("git", ["show", `${ref}:scripts/governance-baseline.json`], {
|
|
cwd: ROOT,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
return JSON.parse(content);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function godfileRatchet(baseRef) {
|
|
const baseline = readBaseline();
|
|
const cap = baseline.maxFileLines ?? MAX_FILE_LINES;
|
|
const debtEntries = new Map(
|
|
(baseline.godfileDebt ?? []).map((entry) => [entry.path, entry.maxLines]),
|
|
);
|
|
const files = governedFiles();
|
|
|
|
const newDebt = [];
|
|
const grownDebt = [];
|
|
for (const file of files) {
|
|
const lines = lineCount(file);
|
|
const debtCap = debtEntries.get(file);
|
|
if (lines > cap && debtCap === undefined) {
|
|
newDebt.push({ path: file, lines });
|
|
} else if (debtCap !== undefined && lines > debtCap) {
|
|
grownDebt.push({ path: file, lines, maxLines: debtCap });
|
|
}
|
|
}
|
|
|
|
const stale = [];
|
|
const remaining = [];
|
|
for (const [debtPath, maxLines] of debtEntries) {
|
|
const lines = files.includes(debtPath) ? lineCount(debtPath) : -1;
|
|
if (lines === -1 || lines <= cap) {
|
|
stale.push({ path: debtPath, lines });
|
|
} else {
|
|
remaining.push({ path: debtPath, lines, maxLines });
|
|
}
|
|
}
|
|
|
|
const baselineLoosening = [];
|
|
const baseBaseline = baseRef ? readBaselineAtRef(baseRef) : null;
|
|
if (baseBaseline) {
|
|
const baseCap = baseBaseline.maxFileLines ?? MAX_FILE_LINES;
|
|
if (cap > baseCap) {
|
|
baselineLoosening.push(`global cap increased from ${baseCap} to ${cap}`);
|
|
}
|
|
const baseEntries = new Map(
|
|
(baseBaseline.godfileDebt ?? []).map((entry) => [entry.path, entry.maxLines]),
|
|
);
|
|
for (const [debtPath, maxLines] of debtEntries) {
|
|
const priorMax = baseEntries.get(debtPath);
|
|
if (priorMax === undefined) {
|
|
baselineLoosening.push(`new debt entry: ${debtPath}`);
|
|
} else if (maxLines > priorMax) {
|
|
baselineLoosening.push(`cap increased for ${debtPath}: ${priorMax} -> ${maxLines}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
cap,
|
|
newDebt,
|
|
grownDebt,
|
|
stale,
|
|
remaining,
|
|
baselineLoosening,
|
|
comparedBaseline: Boolean(baseBaseline),
|
|
};
|
|
}
|
|
|
|
function resolveBaseRef() {
|
|
if (process.env.GOVERNANCE_BASE) return process.env.GOVERNANCE_BASE;
|
|
if (process.env.GITHUB_BASE_REF) return `origin/${process.env.GITHUB_BASE_REF}`;
|
|
for (const candidate of ["origin/dev", "origin/main"]) {
|
|
try {
|
|
execFileSync("git", ["rev-parse", "--verify", candidate], {
|
|
cwd: ROOT,
|
|
encoding: "utf8",
|
|
stdio: "ignore",
|
|
});
|
|
return candidate;
|
|
} catch {
|
|
// candidate ref not present locally; try the next
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function changedGovernedFiles(baseRef) {
|
|
let mergeBase;
|
|
try {
|
|
mergeBase = execFileSync("git", ["merge-base", baseRef, "HEAD"], {
|
|
cwd: ROOT,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
}).trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
const diffed = gitLines(["diff", "--name-only", "--diff-filter=AMR", mergeBase, "HEAD"]);
|
|
const untracked = gitLines(["ls-files", "--others", "--exclude-standard"]);
|
|
return [...new Set([...diffed, ...untracked])].filter(isGoverned);
|
|
}
|
|
|
|
function maintainabilityGate(files) {
|
|
if (files.length === 0) {
|
|
return { skipped: true, reason: "no changed governed TS/TSX files" };
|
|
}
|
|
const eslintJs = path.join(ROOT, "node_modules", "eslint", "bin", "eslint.js");
|
|
const ruleArgs = MAINTAINABILITY_RULES.flatMap((rule) => ["--rule", rule]);
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
eslintJs,
|
|
...files,
|
|
...ruleArgs,
|
|
"--max-warnings=0",
|
|
"--no-warn-ignored",
|
|
"--no-error-on-unmatched-pattern",
|
|
],
|
|
{ cwd: ROOT, encoding: "utf8" },
|
|
);
|
|
return {
|
|
skipped: false,
|
|
status: result.status,
|
|
stdout: result.stdout?.trim() ?? "",
|
|
stderr: result.stderr?.trim() ?? "",
|
|
files,
|
|
};
|
|
}
|
|
|
|
function plural(count, word) {
|
|
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
}
|
|
|
|
function main() {
|
|
const failures = [];
|
|
const baseRef = resolveBaseRef();
|
|
if (!baseRef) {
|
|
failures.push(
|
|
"base ref is required but was not found. Set GOVERNANCE_BASE to a valid commit or fetch origin/dev.",
|
|
);
|
|
} else {
|
|
try {
|
|
gitText(["merge-base", baseRef, "HEAD"]);
|
|
} catch {
|
|
failures.push(
|
|
`base ref '${baseRef}' cannot be resolved against HEAD. Fetch it or set GOVERNANCE_BASE correctly.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log("─".repeat(64));
|
|
console.log("godfile ratchet: legacy caps may only shrink");
|
|
const god = godfileRatchet(baseRef);
|
|
console.log(
|
|
` cap: ${god.cap} lines | grandfathered debt: ${plural(god.remaining.length, "file")} | new violations: ${god.newDebt.length}`,
|
|
);
|
|
for (const entry of god.remaining) {
|
|
console.log(` debt ${String(entry.lines).padStart(4)}/${entry.maxLines} ${entry.path}`);
|
|
}
|
|
for (const entry of god.newDebt) {
|
|
console.log(` NEW ${String(entry.lines).padStart(4)} ${entry.path}`);
|
|
}
|
|
for (const entry of god.grownDebt) {
|
|
console.log(` GREW ${String(entry.lines).padStart(4)}/${entry.maxLines} ${entry.path}`);
|
|
}
|
|
if (god.newDebt.length > 0) {
|
|
failures.push(
|
|
`godfile ratchet: ${plural(god.newDebt.length, "file")} exceed ${god.cap} lines. Refactor them under the cap; new baseline debt is forbidden.`,
|
|
);
|
|
}
|
|
if (god.grownDebt.length > 0) {
|
|
failures.push(
|
|
`godfile ratchet: ${plural(god.grownDebt.length, "grandfathered file")} exceeded its frozen line cap.`,
|
|
);
|
|
}
|
|
if (god.baselineLoosening.length > 0) {
|
|
failures.push(
|
|
`governance baseline was loosened: ${god.baselineLoosening.join("; ")}. Only cap reductions and entry removals are allowed.`,
|
|
);
|
|
}
|
|
if (!god.comparedBaseline) {
|
|
console.log(" baseline comparison unavailable (initial adoption or missing base file)");
|
|
}
|
|
if (god.stale.length > 0) {
|
|
console.log(` stale baseline entries (now compliant — remove to ratchet tighter):`);
|
|
for (const entry of god.stale) {
|
|
console.log(` stale ${entry.path}`);
|
|
}
|
|
}
|
|
|
|
console.log("─".repeat(64));
|
|
if (!baseRef) {
|
|
console.log("changed-file maintainability gate: FAIL (no valid base ref)");
|
|
} else {
|
|
const files = changedGovernedFiles(baseRef);
|
|
console.log(
|
|
`changed-file maintainability gate (base: ${baseRef}): ${files === null ? "unresolvable" : plural(files.length, "changed governed file")}`,
|
|
);
|
|
if (files === null) {
|
|
console.log(" failed — base ref could not be resolved against HEAD");
|
|
} else {
|
|
const gate = maintainabilityGate(files);
|
|
if (gate.skipped) {
|
|
console.log(` skipped — ${gate.reason}`);
|
|
} else {
|
|
const clean = gate.status === 0;
|
|
console.log(
|
|
` result: ${clean ? "PASS" : "FAIL"} (complexity<=20, function<=150 lines, params<=4, depth<=4)`,
|
|
);
|
|
if (!clean) {
|
|
if (gate.stdout) console.log(gate.stdout);
|
|
if (gate.stderr) console.log(gate.stderr);
|
|
failures.push(
|
|
"changed-file maintainability gate: see ESLint output above. Extract functions/components to meet the thresholds; do not relax the thresholds.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log("─".repeat(64));
|
|
if (failures.length > 0) {
|
|
console.log(`RESULT: FAIL (${plural(failures.length, "gate")})`);
|
|
for (const failure of failures) console.log(` - ${failure}`);
|
|
process.exit(1);
|
|
}
|
|
console.log("RESULT: PASS — all governance gates green");
|
|
}
|
|
|
|
main();
|