diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f07f273..01ba13c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,11 +38,6 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Maintainability guardrails (fail-fast) - run: | - bun run check:main-fanin:strict - bun run check:runtime-cycles:strict - - name: Build (TypeScript check) # Keep explicit typecheck for fast fail before full build/bundle. run: bun run tsc --noEmit diff --git a/backlog/tasks/task-111 - Remove-Maintainability-Guardrails-docs-section-and-related-guardrail-code.md b/backlog/tasks/task-111 - Remove-Maintainability-Guardrails-docs-section-and-related-guardrail-code.md new file mode 100644 index 0000000..90060f3 --- /dev/null +++ b/backlog/tasks/task-111 - Remove-Maintainability-Guardrails-docs-section-and-related-guardrail-code.md @@ -0,0 +1,47 @@ +--- +id: TASK-111 +title: Remove Maintainability Guardrails docs section and related guardrail code +status: Done +assignee: [] +created_date: '2026-02-23 03:37' +updated_date: '2026-02-23 03:40' +labels: + - docs + - cleanup + - guardrails +dependencies: [] +priority: medium +--- + +## Description + + +User requested removing the docs section labeled "Maintainability Guardrails" and deleting associated project code/commands for those guardrails so docs and scripts stay consistent. + + +## Acceptance Criteria + +- [x] #1 Docs no longer contain the Maintainability Guardrails section shown in the request. +- [x] #2 Commands and code associated specifically with that removed guardrails section are removed from scripts/config where applicable. +- [x] #3 Project references remain consistent (no stale mentions of removed guardrails commands). + + +## Implementation Notes + + +Removed the `## Maintainability Guardrails` section from `docs/development.md` including strict command examples and troubleshooting bullets. + +Removed guardrail scripts from `package.json`: `check:main-fanin`, `check:main-fanin:strict`, `check:runtime-cycles`, `check:runtime-cycles:strict`. + +Deleted associated script code and fixtures: `scripts/check-main-runtime-fanin.ts`, `scripts/check-runtime-cycles.ts`, `scripts/check-runtime-cycles.test.ts`, and `scripts/fixtures/runtime-cycles/**`. + +Removed CI fail-fast guardrail step from `.github/workflows/ci.yml` that invoked strict fan-in/runtime-cycle checks. + +Validation: `bun run tsc --noEmit` and `bun run docs:build` passed. + + +## Final Summary + + +Removed the Maintainability Guardrails docs section and fully removed the related fan-in/runtime-cycle guardrail implementation from scripts, package commands, CI wiring, and fixtures. The repository now has no active references to those guardrail commands in development docs, package scripts, or CI workflow. + diff --git a/docs/development.md b/docs/development.md index 196fe7b..ab937f9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -83,25 +83,6 @@ bun run test:core:dist # optional full dist core suite bun run test:subtitle:dist # optional smoke lane for subtitle dist regressions ``` -## Maintainability Guardrails - -Run guardrails locally before opening a PR: - -```bash -bun run check:main-fanin:strict -bun run check:runtime-cycles:strict -``` - -Expected success output includes: - -- `[OK] main runtime fan-in (strict) — ...` -- `[OK] runtime cycle check (strict) - ... no cycles detected` - -Troubleshooting guardrail failures: - -- Main fan-in failure: move runtime imports behind `src/main/runtime/domains/*` or composer barrels. -- Runtime cycle failure: break bidirectional imports by extracting shared helpers into leaf modules. - ## Config Generation ```bash diff --git a/package.json b/package.json index 2a72db9..5b35b43 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,6 @@ "docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "format": "prettier --write .", "format:check": "prettier --check .", - "check:main-fanin": "bun run scripts/check-main-runtime-fanin.ts", - "check:main-fanin:strict": "bun run scripts/check-main-runtime-fanin.ts --strict", - "check:runtime-cycles": "bun run scripts/check-runtime-cycles.ts", - "check:runtime-cycles:strict": "bun run scripts/check-runtime-cycles.ts --strict", "test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts", "test:config:dist": "node --test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js", "test:config:smoke:dist": "node --test dist/config/path-resolution.test.js", diff --git a/scripts/check-main-runtime-fanin.ts b/scripts/check-main-runtime-fanin.ts deleted file mode 100644 index e223a52..0000000 --- a/scripts/check-main-runtime-fanin.ts +++ /dev/null @@ -1,103 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -type ParsedArgs = { - strict: boolean; - uniquePathLimit: number; - importLineLimit: number; -}; - -const DEFAULT_UNIQUE_PATH_LIMIT = 11; -const DEFAULT_IMPORT_LINE_LIMIT = 110; -const MAIN_PATH = path.join(process.cwd(), 'src', 'main.ts'); - -function parseArgs(argv: string[]): ParsedArgs { - let strict = false; - let uniquePathLimit = DEFAULT_UNIQUE_PATH_LIMIT; - let importLineLimit = DEFAULT_IMPORT_LINE_LIMIT; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; - if (arg === '--strict') { - strict = true; - continue; - } - if (arg === '--unique-path-limit') { - const raw = argv[i + 1]; - const parsed = Number.parseInt(raw ?? '', 10); - if (!Number.isFinite(parsed) || parsed <= 0) { - throw new Error(`Invalid --unique-path-limit value: ${raw ?? ''}`); - } - uniquePathLimit = parsed; - i += 1; - continue; - } - if (arg === '--import-line-limit') { - const raw = argv[i + 1]; - const parsed = Number.parseInt(raw ?? '', 10); - if (!Number.isFinite(parsed) || parsed <= 0) { - throw new Error(`Invalid --import-line-limit value: ${raw ?? ''}`); - } - importLineLimit = parsed; - i += 1; - continue; - } - } - - return { strict, uniquePathLimit, importLineLimit }; -} - -function collectRuntimeImportStats(source: string): { - importLines: number; - uniquePaths: string[]; -} { - const pathMatches = Array.from(source.matchAll(/from '(\.\/main\/runtime[^']*)';/g)).map( - (match) => match[1], - ); - const uniquePaths = new Set(); - - for (const runtimeImportPath of pathMatches) { - uniquePaths.add(runtimeImportPath); - } - - return { - importLines: pathMatches.length, - uniquePaths: Array.from(uniquePaths).sort(), - }; -} - -function main(): void { - const { strict, uniquePathLimit, importLineLimit } = parseArgs(process.argv.slice(2)); - const source = fs.readFileSync(MAIN_PATH, 'utf8'); - const { importLines, uniquePaths } = collectRuntimeImportStats(source); - const overUniquePathLimit = uniquePaths.length > uniquePathLimit; - const overImportLineLimit = importLines > importLineLimit; - const hasFailure = overUniquePathLimit || overImportLineLimit; - const mode = strict ? 'strict' : 'warning'; - - if (!hasFailure) { - console.log( - `[OK] main runtime fan-in (${mode}) — ${importLines} import lines, ${uniquePaths.length} unique runtime paths`, - ); - return; - } - - const heading = strict ? '[FAIL]' : '[WARN]'; - console.log( - `${heading} main runtime fan-in (${mode}) — ${importLines} import lines, ${uniquePaths.length} unique runtime paths`, - ); - console.log(` limits: import lines <= ${importLineLimit}, unique paths <= ${uniquePathLimit}`); - console.log(' runtime import paths:'); - for (const runtimeImportPath of uniquePaths) { - console.log(` - ${runtimeImportPath}`); - } - console.log( - ' Hint: keep main.ts focused on boot wiring; move domain imports behind domain barrels/registries.', - ); - - if (strict) { - process.exitCode = 1; - } -} - -main(); diff --git a/scripts/check-runtime-cycles.test.ts b/scripts/check-runtime-cycles.test.ts deleted file mode 100644 index 663e5b7..0000000 --- a/scripts/check-runtime-cycles.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import path from 'node:path'; -import { spawnSync } from 'node:child_process'; -import { runRuntimeCycleCheck } from './check-runtime-cycles'; - -type CliResult = { - status: number | null; - stdout: string; - stderr: string; -}; - -const CHECKER_PATH = path.join(process.cwd(), 'scripts', 'check-runtime-cycles.ts'); -const FIXTURE_ROOT = path.join('scripts', 'fixtures', 'runtime-cycles'); - -function runCheckerCli(args: string[]): CliResult { - const result = spawnSync('bun', ['run', CHECKER_PATH, ...args], { - cwd: process.cwd(), - encoding: 'utf8', - }); - - return { - status: result.status, - stdout: result.stdout || '', - stderr: result.stderr || '', - }; -} - -test('acyclic fixture passes runtime cycle check', () => { - const result = runRuntimeCycleCheck(path.join(FIXTURE_ROOT, 'acyclic'), true); - assert.equal(result.hasCycle, false); - assert.equal(result.fileCount >= 3, true); -}); - -test('cyclic fixture fails via CLI and prints readable cycle path', () => { - const result = runCheckerCli(['--strict', '--root', path.join(FIXTURE_ROOT, 'cyclic')]); - - assert.equal(result.status, 1); - assert.match(result.stdout, /^\[FAIL\] runtime cycle check \(strict\)/m); - assert.match(result.stdout, /module-a\.ts/); - assert.match(result.stdout, /module-b\.ts|nested\/index\.ts/); -}); - -test('missing root returns actionable error', () => { - const missingRoot = path.join(FIXTURE_ROOT, 'does-not-exist'); - const result = runCheckerCli(['--strict', '--root', missingRoot]); - - assert.equal(result.status, 1); - assert.match(result.stdout, /Runtime root not found:/); - assert.match(result.stdout, /--root /); -}); diff --git a/scripts/check-runtime-cycles.ts b/scripts/check-runtime-cycles.ts deleted file mode 100644 index a3ad179..0000000 --- a/scripts/check-runtime-cycles.ts +++ /dev/null @@ -1,214 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -export type ParsedArgs = { - strict: boolean; - root: string; -}; - -export type RuntimeCycleCheckResult = { - root: string; - mode: 'warning' | 'strict'; - fileCount: number; - hasCycle: boolean; - cyclePath: string[]; -}; - -const DEFAULT_ROOT = path.join('src', 'main', 'runtime'); - -export function parseArgs(argv: string[]): ParsedArgs { - let strict = false; - let root = DEFAULT_ROOT; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; - if (arg === '--strict') { - strict = true; - continue; - } - if (arg === '--root') { - const raw = argv[i + 1]; - if (!raw || raw.startsWith('--')) { - throw new Error('Missing value for --root. Usage: --root '); - } - root = raw; - i += 1; - continue; - } - throw new Error(`Unknown argument: ${arg}`); - } - - return { strict, root }; -} - -function walkTsFiles(root: string): string[] { - const entries = fs.readdirSync(root, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - const fullPath = path.join(root, entry.name); - if (entry.isDirectory()) { - files.push(...walkTsFiles(fullPath)); - continue; - } - if (entry.isFile() && path.extname(entry.name) === '.ts') { - files.push(fullPath); - } - } - return files; -} - -function extractRelativeSpecifiers(source: string): string[] { - const specifiers = new Set(); - const pattern = /(?:import|export)\s+(?:[^'";]+\s+from\s+)?['"]([^'"]+)['"]/g; - for (const match of source.matchAll(pattern)) { - const specifier = match[1]; - if (specifier.startsWith('.')) { - specifiers.add(specifier); - } - } - return Array.from(specifiers); -} - -function resolveRelativeTarget(importerFile: string, specifier: string): string | null { - const importerDir = path.dirname(importerFile); - const baseTarget = path.resolve(importerDir, specifier); - const extension = path.extname(baseTarget); - - const candidates = extension - ? [baseTarget] - : [`${baseTarget}.ts`, path.join(baseTarget, 'index.ts')]; - - for (const candidate of candidates) { - if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { - return path.normalize(candidate); - } - } - - return null; -} - -function toRelativePosix(root: string, filePath: string): string { - return path.relative(root, filePath).split(path.sep).join('/'); -} - -function detectCycle(graph: Map): string[] { - const state = new Map(); - const stack: string[] = []; - - const visit = (node: string): string[] => { - state.set(node, 1); - stack.push(node); - - const neighbors = graph.get(node) ?? []; - for (const next of neighbors) { - const nextState = state.get(next) ?? 0; - if (nextState === 0) { - const cycle = visit(next); - if (cycle.length > 0) { - return cycle; - } - } else if (nextState === 1) { - const startIndex = stack.indexOf(next); - if (startIndex >= 0) { - return [...stack.slice(startIndex), next]; - } - } - } - - stack.pop(); - state.set(node, 2); - return []; - }; - - const nodes = Array.from(graph.keys()).sort(); - for (const node of nodes) { - if ((state.get(node) ?? 0) !== 0) { - continue; - } - const cycle = visit(node); - if (cycle.length > 0) { - return cycle; - } - } - - return []; -} - -export function runRuntimeCycleCheck(rootArg: string, strict: boolean): RuntimeCycleCheckResult { - const root = path.resolve(process.cwd(), rootArg); - if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) { - throw new Error( - `Runtime root not found: ${rootArg}. Use --root to point at a directory.`, - ); - } - - const files = walkTsFiles(root) - .map((file) => path.normalize(file)) - .sort(); - const fileSet = new Set(files); - const graph = new Map(); - - for (const file of files) { - const source = fs.readFileSync(file, 'utf8'); - const specifiers = extractRelativeSpecifiers(source); - const edges = new Set(); - for (const specifier of specifiers) { - const target = resolveRelativeTarget(file, specifier); - if (!target) { - continue; - } - if (fileSet.has(target)) { - edges.add(target); - } - } - graph.set(file, Array.from(edges).sort()); - } - - const cycle = detectCycle(graph); - return { - root: rootArg, - mode: strict ? 'strict' : 'warning', - fileCount: files.length, - hasCycle: cycle.length > 0, - cyclePath: cycle.map((file) => toRelativePosix(root, file)), - }; -} - -export function formatSummary(result: RuntimeCycleCheckResult): string[] { - const lines: string[] = []; - if (!result.hasCycle) { - lines.push( - `[OK] runtime cycle check (${result.mode}) - ${result.fileCount} files scanned, no cycles detected`, - ); - return lines; - } - - const heading = result.mode === 'strict' ? '[FAIL]' : '[WARN]'; - lines.push(`${heading} runtime cycle check (${result.mode}) - cycle detected`); - lines.push(` root: ${result.root}`); - lines.push(` cycle: ${result.cyclePath.join(' -> ')}`); - lines.push(' Hint: break bidirectional imports by moving shared logic to leaf modules.'); - return lines; -} - -export function main(argv: string[] = process.argv.slice(2)): void { - try { - const { strict, root } = parseArgs(argv); - const result = runRuntimeCycleCheck(root, strict); - for (const line of formatSummary(result)) { - console.log(line); - } - - if (strict && result.hasCycle) { - process.exitCode = 1; - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.log(`[FAIL] runtime cycle check (strict) - ${message}`); - process.exitCode = 1; - } -} - -if (import.meta.main) { - main(); -} diff --git a/scripts/fixtures/runtime-cycles/acyclic/entry.ts b/scripts/fixtures/runtime-cycles/acyclic/entry.ts deleted file mode 100644 index 113a1d3..0000000 --- a/scripts/fixtures/runtime-cycles/acyclic/entry.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './utils'; -export { value } from './feature'; diff --git a/scripts/fixtures/runtime-cycles/acyclic/feature.ts b/scripts/fixtures/runtime-cycles/acyclic/feature.ts deleted file mode 100644 index fcd5ff9..0000000 --- a/scripts/fixtures/runtime-cycles/acyclic/feature.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { utilValue } from './utils'; - -export const value = utilValue + 1; diff --git a/scripts/fixtures/runtime-cycles/acyclic/utils/index.ts b/scripts/fixtures/runtime-cycles/acyclic/utils/index.ts deleted file mode 100644 index a3c93d7..0000000 --- a/scripts/fixtures/runtime-cycles/acyclic/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const utilValue = 1; diff --git a/scripts/fixtures/runtime-cycles/cyclic/module-a.ts b/scripts/fixtures/runtime-cycles/cyclic/module-a.ts deleted file mode 100644 index 0d36b99..0000000 --- a/scripts/fixtures/runtime-cycles/cyclic/module-a.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { b } from './module-b'; - -export const a = b + 1; diff --git a/scripts/fixtures/runtime-cycles/cyclic/module-b.ts b/scripts/fixtures/runtime-cycles/cyclic/module-b.ts deleted file mode 100644 index 1df25f7..0000000 --- a/scripts/fixtures/runtime-cycles/cyclic/module-b.ts +++ /dev/null @@ -1 +0,0 @@ -export { c as b } from './nested'; diff --git a/scripts/fixtures/runtime-cycles/cyclic/nested/index.ts b/scripts/fixtures/runtime-cycles/cyclic/nested/index.ts deleted file mode 100644 index 7ee8c7a..0000000 --- a/scripts/fixtures/runtime-cycles/cyclic/nested/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { a } from '../module-a.ts'; - -export const c = a + 1;