Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<basename>
# where <basename> has been migrated to memory/persona/<persona>/conversations/.
Comment thread
AceHack marked this conversation as resolved.
#
# 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
Expand Down
19 changes: 15 additions & 4 deletions tools/hygiene/audit-section-33-migration-xrefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 {
Expand All @@ -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]!;
Expand All @@ -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/
Expand Down Expand Up @@ -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;
}

Expand Down
Loading