mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 11:33:12 +00:00
* feat(vendors): support company roster workflows * fix(vendors): address roster review feedback * test(vendors): preserve draft on roster load failure * fix(vendors): preserve current roster selection
293 lines
9.3 KiB
JavaScript
293 lines
9.3 KiB
JavaScript
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { existsSync, 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(
|
|
(relativePath) => isGoverned(relativePath) && existsSync(path.join(ROOT, relativePath)),
|
|
);
|
|
}
|
|
|
|
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" };
|
|
}
|
|
// Invoke eslint via node so Windows (no shebang exec) and Unix both work.
|
|
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();
|