chore(scripts): remove dead references to a retired repository

Two comments in build-rollup-register.{mjs,test.mjs} used a since-deleted
repository as the example of a nested same-name git repo. Replaced with the
invented `example-repo/example-repo`; the test fixture (`outer/outer`) and
what it proves are unchanged.

Adds scripts/check-retired-refs.mjs (+ test): fails when a tracked text
file names a retired term in its content or path. The term list is
local-only (scripts/retired-terms.local.md, ignored via *.local.md) and
never tracked; a missing or empty list reports NOT CHECKED (exit 2), and
the repo-level test skips with that reason instead of passing silently.
Named exemption list, empty. Chosen *.local.md because the existing ignore
rule already covers it, so .gitignore is untouched.

Red on 69fb250 (112 files, 2 hits, exit 1) -> green (114 files, 0 hits).
Suite 217 -> 227 tests, 211 -> 221 pass; the same 6 check-okf-parity
failures before and after, caused by the worktree path breaking its
sibling-repo lookup (9/9 green in the main checkout). check-versions:
12/12 OK, 0 ERROR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-23 12:28:11 +02:00
commit 4ecc6e2b6a
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 260 additions and 2 deletions

View file

@ -206,7 +206,7 @@ export function buildRegister({ repos }) {
// state-bearing directories — a fact, not a guarantee, hence the collision gate below.
//
// Not descending into a git repo also makes the known nested-name trap
// (claude-code-100x/claude-code-100x, both git repos) structurally unreachable rather than
// (e.g. example-repo/example-repo, both git repos) structurally unreachable rather than
// merely absent.
//
// "Is a repo" tests `.git` for EXISTENCE, not directory-ness: a worktree or submodule has

View file

@ -321,7 +321,7 @@ test('D1: repos under a polyrepo container (depth 2) are discovered', () => {
});
test('D1: a git repo is NOT descended into (the nested same-name trap stays unreachable)', () => {
// claude-code-100x/claude-code-100x: the outer IS a git repo, so board.sh's rule never
// example-repo/example-repo: the outer IS a git repo, so board.sh's rule never
// reaches the inner one. Matching that rule makes this collision structurally impossible
// rather than merely absent today.
const root = tree({ 'outer': MARKER, 'outer/outer': MARKER });

View file

@ -0,0 +1,112 @@
#!/usr/bin/env node
// Retired-reference gate: fails when a tracked text file names, in its content or in its
// path, a repository or subject that has been removed from the org. A reference to a
// deleted repository is a dead link the day it lands, and a public catalog must not point
// readers at something that no longer exists.
//
// Usage: node scripts/check-retired-refs.mjs [repo-root] [--terms <file>]
// exit 0 = checked, 0 hits · exit 1 = hit(s), or 0 files checked · exit 2 = NOT CHECKED
//
// The term list is local-only (default: scripts/retired-terms.local.md, gitignored via
// *.local.md) and never tracked: one term per line, blank lines and lines starting with #
// ignored, matched case-insensitively as plain substrings. A missing or empty list is
// reported as NOT CHECKED — never as a pass.
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
export const DEFAULT_TERMS_FILE = join(HERE, 'retired-terms.local.md');
// Named exceptions: tracked paths allowed to match. Empty by design — add a path here only
// with a comment saying why the reference must stay.
export const EXEMPT_PATHS = [];
// Returns the list of terms, or null when the file does not exist.
export function loadTerms(file = DEFAULT_TERMS_FILE) {
if (!existsSync(file)) return null;
return readFileSync(file, 'utf8')
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('#'));
}
function escapeRegExp(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function buildPattern(terms) {
if (!terms || terms.length === 0) throw new Error('buildPattern: empty term list');
return new RegExp(terms.map(escapeRegExp).join('|'), 'i');
}
// entries: [{ path, content }] where content is a string, or null for a binary file.
// Returns one finding per matching path and per matching content line.
export function scanEntries(entries, { pattern, exempt = EXEMPT_PATHS }) {
const findings = [];
for (const { path, content } of entries) {
if (exempt.includes(path)) continue;
if (pattern.test(path)) findings.push({ path, kind: 'path', line: 0, text: path });
if (content === null) continue;
content.split('\n').forEach((text, i) => {
if (pattern.test(text)) findings.push({ path, kind: 'content', line: i + 1, text: text.trim() });
});
}
return findings;
}
export function listTracked(root) {
const out = execFileSync('git', ['-C', root, 'ls-files', '-z'], { encoding: 'utf8' });
return out.split('\0').filter(Boolean);
}
export function readEntries(root, paths) {
const entries = [];
for (const path of paths) {
const abs = join(root, path);
if (!existsSync(abs)) continue; // tracked but deleted in the working tree
const buf = readFileSync(abs);
entries.push({ path, content: buf.includes(0) ? null : buf.toString('utf8') });
}
return entries;
}
export function runCheck(root, terms) {
const entries = readEntries(root, listTracked(root));
const findings = scanEntries(entries, { pattern: buildPattern(terms) });
return { checked: entries.length, findings };
}
function parseArgs(argv) {
const args = { root: join(HERE, '..'), termsFile: DEFAULT_TERMS_FILE };
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--terms') args.termsFile = argv[++i];
else args.root = argv[i];
}
return args;
}
function main(argv) {
const { root, termsFile } = parseArgs(argv);
const terms = loadTerms(termsFile);
if (!terms || terms.length === 0) {
console.log(`check-retired-refs: NOT CHECKED — term list ${terms ? 'empty' : 'missing'}: ${termsFile}`);
return 2;
}
const { checked, findings } = runCheck(root, terms);
for (const f of findings) {
const where = f.kind === 'path' ? `${f.path} (path)` : `${f.path}:${f.line}`;
console.log(`[ERROR] retired reference — ${where}: ${f.text}`);
}
console.log(`check-retired-refs: checked ${checked} tracked files against ${terms.length} terms — ${findings.length} hit(s)`);
if (checked === 0) {
console.log('check-retired-refs: 0 files checked — verified nothing');
return 1;
}
return findings.length > 0 ? 1 : 0;
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
process.exit(main(process.argv.slice(2)));
}

View file

@ -0,0 +1,146 @@
// Tests for the retired-reference gate. The unit tests use synthetic terms; only the last
// test reads the real, local-only term list, and it SKIPS loudly when that file is absent.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
DEFAULT_TERMS_FILE,
EXEMPT_PATHS,
loadTerms,
buildPattern,
scanEntries,
runCheck,
} from './check-retired-refs.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = join(HERE, '..');
const CLI = join(HERE, 'check-retired-refs.mjs');
const TERMS = ['retired-alpha', 'Beta.Tool', 'gamma'];
const PATTERN = buildPattern(TERMS);
function tmpDir(prefix) {
return mkdtempSync(join(tmpdir(), prefix));
}
function termsFile(lines) {
const file = join(tmpDir('retired-terms-'), 'terms.local.md');
writeFileSync(file, lines.join('\n'));
return file;
}
test('loadTerms: one term per line, blanks and # lines ignored, missing file -> null', () => {
const file = termsFile(['# comment', '', ' retired-alpha ', 'gamma', '']);
assert.deepEqual(loadTerms(file), ['retired-alpha', 'gamma']);
assert.equal(loadTerms(join(tmpDir('retired-none-'), 'absent.md')), null);
});
test('buildPattern: case-insensitive plain substrings, regex metacharacters escaped', () => {
assert.match('uses RETIRED-ALPHA here', PATTERN);
assert.match('beta.tool', PATTERN);
assert.doesNotMatch('betaXtool', PATTERN, '"." is literal, not a wildcard');
assert.doesNotMatch('repo-mailbox, check-versions', PATTERN);
assert.throws(() => buildPattern([]), /empty term list/);
});
test('scanEntries reports content hits with line numbers and path hits', () => {
const findings = scanEntries(
[
{ path: 'clean.md', content: 'nothing here\n' },
{ path: 'a.md', content: 'line one\nuses retired-alpha/retired-alpha\n' },
{ path: 'docs/gamma.md', content: 'clean body\n' },
],
{ pattern: PATTERN },
);
assert.deepEqual(
findings.map((f) => [f.path, f.kind, f.line]),
[
['a.md', 'content', 2],
['docs/gamma.md', 'path', 0],
],
);
});
test('scanEntries skips binary content but still checks its path', () => {
const findings = scanEntries(
[
{ path: 'img.png', content: null },
{ path: 'gamma.bin', content: null },
],
{ pattern: PATTERN },
);
assert.deepEqual(findings.map((f) => f.path), ['gamma.bin']);
});
test('scanEntries honours the named exemption list', () => {
const entries = [{ path: 'keep.md', content: 'gamma' }];
assert.equal(scanEntries(entries, { pattern: PATTERN }).length, 1);
assert.equal(scanEntries(entries, { pattern: PATTERN, exempt: ['keep.md'] }).length, 0);
assert.deepEqual(EXEMPT_PATHS, [], 'the shipped exemption list is empty');
});
function tmpRepo(files) {
const root = tmpDir('retired-refs-');
execFileSync('git', ['init', '-q', root]);
for (const [path, body] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true });
writeFileSync(join(root, path), body);
}
execFileSync('git', ['-C', root, 'add', '-A']);
return root;
}
test('runCheck reads the git index: a staged hit is found, an untracked one is not', () => {
const root = tmpRepo({ 'README.md': 'points at gamma\n' });
writeFileSync(join(root, 'untracked.md'), 'retired-alpha');
const { checked, findings } = runCheck(root, TERMS);
assert.equal(checked, 1);
assert.deepEqual(findings.map((f) => `${f.path}:${f.line}`), ['README.md:1']);
});
test('CLI exits 1 on a hit and 0 on a clean tree, always printing the denominator', () => {
const terms = termsFile(TERMS);
const run = (root) => spawnSync(process.execPath, [CLI, root, '--terms', terms], { encoding: 'utf8' });
const dirty = run(tmpRepo({ 'x.md': 'retired-alpha' }));
assert.equal(dirty.status, 1);
assert.match(dirty.stdout, /\[ERROR\] retired reference — x\.md:1/);
assert.match(dirty.stdout, /checked 1 tracked files against 3 terms — 1 hit/);
const clean = run(tmpRepo({ 'x.md': 'clean\n' }));
assert.equal(clean.status, 0);
assert.match(clean.stdout, /checked 1 tracked files against 3 terms — 0 hit/);
});
test('CLI fails on an empty index: 0 files checked verified nothing', () => {
const empty = tmpDir('retired-refs-empty-');
execFileSync('git', ['init', '-q', empty]);
const r = spawnSync(process.execPath, [CLI, empty, '--terms', termsFile(TERMS)], { encoding: 'utf8' });
assert.equal(r.status, 1);
assert.match(r.stdout, /verified nothing/);
});
test('CLI reports NOT CHECKED (exit 2) for a missing or empty term list, never a pass', () => {
const root = tmpRepo({ 'x.md': 'retired-alpha' });
const missing = spawnSync(process.execPath, [CLI, root, '--terms', join(root, 'absent.md')], { encoding: 'utf8' });
assert.equal(missing.status, 2);
assert.match(missing.stdout, /NOT CHECKED — term list missing/);
const empty = spawnSync(process.execPath, [CLI, root, '--terms', termsFile(['# only a comment'])], { encoding: 'utf8' });
assert.equal(empty.status, 2);
assert.match(empty.stdout, /NOT CHECKED — term list empty/);
});
// The FERDIG criterion: this repository's own tracked files carry zero retired references.
// The real term list is local-only by design; without it this test cannot measure anything,
// so it skips with the reason instead of passing silently.
test('this repository: 0 retired references across its tracked files', (t) => {
const terms = loadTerms();
if (!terms || terms.length === 0) {
t.skip(`NOT CHECKED — local term list missing or empty: ${DEFAULT_TERMS_FILE}`);
return;
}
const { checked, findings } = runCheck(REPO, terms);
assert.ok(checked > 0, 'checked 0 files — verified nothing');
assert.deepEqual(findings.map((f) => `${f.path}:${f.line}`), []);
});