test(repo): guard critical ownership surfaces

This commit is contained in:
2026-04-01 09:04:29 +02:00
parent b2568a3cb4
commit 01e116ce99
2 changed files with 133 additions and 12 deletions
+59 -7
View File
@@ -1,11 +1,12 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import { resolveRealWorkspaceRoot } from "./load-env.mjs";
const rootDir = resolveRealWorkspaceRoot();
const rules = [
export const rules = [
{
file: "apps/web/src/server/auth.ts",
required: [
@@ -60,6 +61,7 @@ const rules = [
},
{
file: "packages/api/src/sse/subscription-policy.ts",
maxLines: 80,
required: [
{
pattern: /\bderiveUserSseSubscription\b/,
@@ -83,6 +85,21 @@ const rules = [
{ pattern: /\baudience\b/, message: "timeline SSE route must not parse raw audience values from the client" },
],
},
{
file: "apps/web/src/hooks/useTimelineSSE.ts",
maxLines: 120,
required: [
{
pattern: /\bgetTimelineSseInvalidationKeys\s*\(/,
message: "timeline SSE hook must keep invalidation policy delegated to the extracted policy helper",
},
{
pattern: /\bparseTimelineSseEvent\s*\(/,
message: "timeline SSE hook must keep event parsing delegated to the extracted policy helper",
},
],
forbidden: [],
},
{
file: "docker-compose.prod.yml",
required: [
@@ -184,11 +201,12 @@ const rules = [
},
];
const violations = [];
export function countLines(source) {
return source.split("\n").length;
}
for (const rule of rules) {
const absolutePath = path.join(rootDir, rule.file);
const source = await readFile(absolutePath, "utf8");
export function evaluateRule(rule, source) {
const violations = [];
for (const requirement of rule.required) {
if (!requirement.pattern.test(source)) {
@@ -201,14 +219,48 @@ for (const rule of rules) {
violations.push(`${rule.file}: forbidden pattern matched: ${forbidden.message}`);
}
}
if (typeof rule.maxLines === "number") {
const lines = countLines(source);
if (lines > rule.maxLines) {
violations.push(
`${rule.file}: file grew to ${lines} lines and exceeds maxLines=${rule.maxLines}; split the ownership surface before expanding it further`,
);
}
}
return violations;
}
if (violations.length > 0) {
export async function collectArchitectureGuardrailViolations(
architectureRules = rules,
{ readSource = readFile, workspaceRoot = rootDir } = {},
) {
const violations = [];
for (const rule of architectureRules) {
const absolutePath = path.join(workspaceRoot, rule.file);
const source = await readSource(absolutePath, "utf8");
violations.push(...evaluateRule(rule, source));
}
return violations;
}
async function main() {
const violations = await collectArchitectureGuardrailViolations();
if (violations.length > 0) {
console.error("Architecture guardrail check failed:");
for (const violation of violations) {
console.error(`- ${violation}`);
}
process.exit(1);
}
console.log("Architecture guardrails passed.");
}
console.log("Architecture guardrails passed.");
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
await main();
}
@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
collectArchitectureGuardrailViolations,
countLines,
evaluateRule,
} from "./check-architecture-guardrails.mjs";
describe("architecture guardrails", () => {
it("counts lines consistently for maxLines checks", () => {
assert.equal(countLines("a\nb\nc"), 3);
});
it("reports required, forbidden, and maxLines violations together", () => {
const violations = evaluateRule(
{
file: "apps/web/src/example.ts",
maxLines: 2,
required: [{ pattern: /\bexpectedHelper\b/, message: "must call the extracted helper" }],
forbidden: [{ pattern: /\binlineLogic\b/, message: "must not re-inline complex logic" }],
},
"inlineLogic();\nconst a = 1;\nconst b = 2;\n",
);
assert.deepEqual(violations, [
"apps/web/src/example.ts: missing guardrail anchor: must call the extracted helper",
"apps/web/src/example.ts: forbidden pattern matched: must not re-inline complex logic",
"apps/web/src/example.ts: file grew to 4 lines and exceeds maxLines=2; split the ownership surface before expanding it further",
]);
});
it("returns no violations when a rule is satisfied", () => {
const violations = evaluateRule(
{
file: "packages/api/src/example.ts",
maxLines: 4,
required: [{ pattern: /\bderiveThing\b/, message: "must keep derivation centralized" }],
forbidden: [{ pattern: /\bunsafeThing\b/, message: "must not use unsafe helper" }],
},
"export function deriveThing() {\n return true;\n}\n",
);
assert.deepEqual(violations, []);
});
it("reads sources through the injected reader when collecting violations", async () => {
const violations = await collectArchitectureGuardrailViolations(
[
{
file: "apps/web/src/example.ts",
maxLines: 2,
required: [],
forbidden: [],
},
],
{
workspaceRoot: "/virtual/repo",
readSource: async (filePath) => {
assert.equal(filePath, "/virtual/repo/apps/web/src/example.ts");
return "line1\nline2\nline3\n";
},
},
);
assert.deepEqual(violations, [
"apps/web/src/example.ts: file grew to 4 lines and exceeds maxLines=2; split the ownership surface before expanding it further",
]);
});
});