auth-yes/scripts/lint_arch.ts
Tyler Gillispie 06cad3d8fe feat(arch): enforce 400-line ceiling and subdivide vertical slices
- Update AGENTS.md with 400-line hard ceiling and sub-feature slicing rules
- Update scripts/lint_arch.ts to enforce 400 lines max with anti-formatting heuristics
- Subdivide src/shared/ui/ into layout, navbar, and admin layout fragments
- Subdivide src/features/auth/ into modular login, register, and recovery routes/fragments
- Subdivide src/features/admin/ into users, user details, apps, and audit fragments
- Maintain backwards compatibility via fragment index re-exports
2026-08-27 19:55:01 -07:00

94 lines
2.9 KiB
TypeScript

import { walk } from "jsr:@std/fs";
const TARGET_DIR = "./src";
const MAX_FILE_LINES = 400;
// Auditable allowlist for exceptional files that legitimately exceed MAX_FILE_LINES.
// Every entry must be explicitly documented and approved.
const ALLOWLISTED_LARGE_FILES = new Set<string>([
// Currently empty: all modules in src/ must strictly conform to <= 400 lines.
]);
let hasErrors = false;
async function checkFile(path: string) {
const content = await Deno.readTextFile(path);
const lines = content.split("\n");
// 1. Enforce strict 400-line ceiling
if (lines.length > MAX_FILE_LINES && !ALLOWLISTED_LARGE_FILES.has(path)) {
console.error(
`[Arch Lint] ❌ File size ceiling exceeded in ${path}: ${lines.length} lines (Hard Ceiling: ${MAX_FILE_LINES} lines).`,
);
console.error(
` -> Subdivide this module into focused, SRP-aligned component files within the feature directory (e.g. login_fragments.tsx, register_fragments.tsx).`,
);
hasErrors = true;
}
lines.forEach((line, index) => {
// 2. Block anti-formatting minification hacks (lines over 300 chars without SVG/CSS/raw string reasons)
if (
line.length > 300 &&
!line.includes("<svg") &&
!line.includes("data:image") &&
!line.includes("style=") &&
!line.includes("`") &&
!line.includes("/*")
) {
console.error(
`[Arch Lint] ❌ Anti-formatting detected (abnormally long line: ${line.length} chars) in ${path}:${
index + 1
}`,
);
console.error(
` -> Format code with standard 'deno fmt'. Do not minify to bypass line limits.`,
);
hasErrors = true;
}
// 3. Block banned DOM APIs in src/
if (
line.includes("document.getElementById") ||
line.includes("document.querySelector") ||
line.includes("document.createElement")
) {
console.error(
`[Arch Lint] ❌ Banned DOM API used in ${path}:${index + 1}`,
);
console.error(` ${line.trim()}`);
console.error(
` -> Use Datastar reactive attributes or SSE morphs instead.`,
);
hasErrors = true;
}
// 4. Block unescaped HTML in raw strings (basic heuristic for dangerouslySetInnerHTML)
if (
line.includes("dangerouslySetInnerHTML") &&
!path.includes("error_fragments.tsx")
) {
console.error(
`[Arch Lint] ❌ dangerouslySetInnerHTML used in ${path}:${index + 1}`,
);
console.error(` ${line.trim()}`);
console.error(
` -> Native JSX HTML escaping should be used unless in explicit core fragments.`,
);
hasErrors = true;
}
});
}
for await (const entry of walk(TARGET_DIR, { exts: [".ts", ".tsx"] })) {
if (entry.isFile) {
await checkFile(entry.path);
}
}
if (hasErrors) {
Deno.exit(1);
} else {
console.log("✅ Architecture lint passed (All files <= 400 lines & clean).");
}