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:
parent
74c27d0e8e
commit
4ecc6e2b6a
4 changed files with 260 additions and 2 deletions
112
scripts/check-retired-refs.mjs
Normal file
112
scripts/check-retired-refs.mjs
Normal 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)));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue