diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 102a56ebf..7ea8a24ad 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -736,6 +736,34 @@ jobs: - name: Run check-archive-header-section33 run: bun tools/hygiene/check-archive-header-section33.ts + lint-section-33-migration-xrefs: + # Fail if any live-nav surface (.claude/, memory/*.md top-level, + # docs/backlog/, repo-root *.md) still references docs/research/ + # where has been migrated to memory/persona//conversations/. + # + # Sibling of lint-archive-header-section33 (B-0036): same shape, different + # failure-class. The §33 migration pattern moves files but does not + # auto-update backlinks; without this lint, dead xrefs accumulate silently + # (empirical baseline 2026-05-15 before cleanup: 10 dead xrefs across 6 files; + # full discovery: PR #3513 → Codex P2 → narrow fix PR #3529 → scanner + # PR #3548 → baseline cleanup PR #3552). + # + # B-0533 Slice B.3 + B.4 — wire the scanner to gate.yml in --enforce mode + # after PR #3552 backfilled all pre-existing violations to 0. + name: lint (§33 migration xrefs) + timeout-minutes: 2 + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install toolchain via three-way-parity script (GOVERNANCE §24) + run: ./tools/setup/install.sh + + - name: Run audit-section-33-migration-xrefs (--enforce) + run: bun tools/hygiene/audit-section-33-migration-xrefs.ts --enforce + lint-no-empty-dirs: # Fail if a committed directory has no files — almost always a # forgotten artefact (an agent-created skill folder without a diff --git a/tools/hygiene/audit-section-33-migration-xrefs.ts b/tools/hygiene/audit-section-33-migration-xrefs.ts index b9c58205f..fa7f13bfe 100644 --- a/tools/hygiene/audit-section-33-migration-xrefs.ts +++ b/tools/hygiene/audit-section-33-migration-xrefs.ts @@ -32,10 +32,12 @@ // // bun tools/hygiene/audit-section-33-migration-xrefs.ts # detect-only // bun tools/hygiene/audit-section-33-migration-xrefs.ts --report PATH # write markdown report +// bun tools/hygiene/audit-section-33-migration-xrefs.ts --enforce # exit non-zero on findings // // Exit codes: // -// 0 always (detect-only; humans triage candidates before fixing) +// 0 no findings, OR detect-only mode (default) +// 1 findings present AND --enforce flag set (CI gate) // 64 argument error // // Composes with: audit-rule-cross-refs.ts (template), B-0532 (sibling lint pattern), @@ -46,12 +48,13 @@ import { join, relative } from "node:path"; const PERSONA_BASE = "memory/persona"; const LIVE_NAV_SURFACES = [".claude/rules", ".claude/agents", ".claude/commands", ".claude/skills", "memory", "docs/backlog"]; -const ROOT_MD = ["CLAUDE.md", "AGENTS.md", "README.md", "GOVERNANCE.md"]; +const ROOT_MD = readdirSync(".").filter(f => f.endsWith(".md")); -type AuditExitCode = 0 | 64; +type AuditExitCode = 0 | 1 | 64; interface Args { readonly report: string | null; + readonly enforce: boolean; } interface DeadXref { @@ -71,6 +74,7 @@ interface AuditResult { function parseArgs(argv: string[]): { kind: "args"; args: Args } | { kind: "error"; message: string } { let report: string | null = null; + let enforce = false; let i = 0; while (i < argv.length) { const a = argv[i]!; @@ -79,11 +83,14 @@ function parseArgs(argv: string[]): { kind: "args"; args: Args } | { kind: "erro if (!next) return { kind: "error", message: "--report requires a path" }; report = next; i += 2; + } else if (a === "--enforce") { + enforce = true; + i += 1; } else { return { kind: "error", message: `Unknown argument: ${a}` }; } } - return { kind: "args", args: { report } }; + return { kind: "args", args: { report, enforce } }; } // Build basename to persona index by walking memory/persona/*/conversations/ @@ -274,6 +281,10 @@ function main(argv: string[]): AuditExitCode { console.log(report); } + if (parsed.args.enforce && result.deadXrefs.length > 0) { + console.error(`error: ${result.deadXrefs.length} dead xref(s) found; --enforce flag set`); + return 1; + } return 0; }