docs(#3182): ADR-3180 — planning semantic model single owner (#3196)

* docs(#3182): ADR-3180 — planning semantic model single owner

Phase 0 design lock for epic #3180. Names one canonical owner per
semantic derivation, specifies the frozen-enum scope contract that
distinguishes a genuinely-empty computation from a truncated or
unscoped one, and locks the drift-guard contract.

Ships no production code. Phases 1-5 execute against this ADR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma

* test(#3182): prune real issue 3182 from phantom-ref guard, fix empty-list regex

The guard's own header documents that its list rots: entries are phantom
only until the repo's shared issue/PR counter reaches them, and once the
counter passes an entry it must be deleted. Creating the Phase-0 sub-issue
advanced the counter past 3182, so the guard began rejecting a legitimate
citation of a real issue - the failure its header already records happening
twice, with PRs 2551 and 2361.

3182 was the last entry, and removing it exposed a latent bug: the regex
builder interpolated the list unconditionally, so an empty list yields
(?:#(?:)\b)|(?:issues/(?:)\b), whose empty alternation matches every issue
reference in the repo. Following the file's own maintenance instruction
would have turned a green guard into one failing on nearly every file.

buildRefRe() now returns null for an empty list and is exported, with
boundary coverage at 0/1/2 entries plus word-boundary and bare-digit
negative cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-08-07 20:25:12 -04:00
committed by GitHub
parent 3f349e551d
commit 664d49e513
3 changed files with 291 additions and 10 deletions

View File

@@ -31,13 +31,26 @@ const ROOT = path.resolve(__dirname, '..');
// digit-bearing strings like an `id_ed25519` SSH key fingerprint are NOT
// false-positives.
//
// Re-verified against the live repo on 2026-07-25, when the shared issue/PR
// counter stood at 2654: 3182 is still a 404; 2551 and 2361 now resolve to
// merged PRs and were removed (#2653).
const PHANTOM = ['3182'];
const REF_RE = new RegExp(
'(?:#(?:' + PHANTOM.join('|') + ')\\b)|(?:issues/(?:' + PHANTOM.join('|') + ')\\b)',
);
// Re-verified against the live repo on 2026-08-07: the shared issue/PR
// counter has now passed 3182 — it is the real Phase-0 sub-issue of epic
// #3180 — so it was pruned per the maintenance rule above, leaving the list
// empty.
const PHANTOM = [];
// Builds the phantom-ref regex from a list of numbers. Returns null when the
// list is empty — that null is load-bearing, not defensive: interpolating an
// empty list into the alternation produces `(?:#(?:)\b)|(?:issues/(?:)\b)`,
// whose empty alternative matches EVERY issue reference (`#1073`,
// `issues/2653`, etc.), turning the guard into a false-positive machine.
// Callers must treat null as "no phantom numbers defined, nothing to scan".
function buildRefRe(phantom) {
if (!phantom.length) return null;
return new RegExp(
'(?:#(?:' + phantom.join('|') + ')\\b)|(?:issues/(?:' + phantom.join('|') + ')\\b)',
);
}
const REF_RE = buildRefRe(PHANTOM);
const SCAN_EXT = new Set(['.md', '.cjs', '.js', '.cts', '.ts']);
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.changeset']);
@@ -58,7 +71,11 @@ function walk(dir, acc) {
return acc;
}
test('no phantom pre-migration issue references remain in repo text (#1073)', () => {
test('no phantom pre-migration issue references remain in repo text (#1073)', (t) => {
if (!REF_RE) {
t.skip('PHANTOM list is empty — no phantom numbers to guard against');
return;
}
const offenders = [];
for (const file of walk(ROOT, [])) {
const rel = path.relative(ROOT, file);
@@ -113,3 +130,19 @@ test('walk() skips broken symlinks and does not throw ENOENT (#1545)', (t) => {
fs.rmSync(fixture, { recursive: true, force: true });
}
});
test('buildRefRe() boundaries — empty list is inert, single/double entries match exactly', () => {
assert.strictEqual(buildRefRe([]), null, 'empty list must build no regex');
const single = buildRefRe(['9999']);
assert.ok(single.test('#9999'), 'single-entry list must match #<n>');
assert.ok(single.test('issues/9999'), 'single-entry list must match issues/<n>');
assert.ok(!single.test('#99990'), 'word boundary must reject a longer number sharing the prefix');
assert.ok(!single.test('9999'), 'bare digits with no # or issues/ prefix must not match');
const double = buildRefRe(['9999', '8888']);
assert.ok(double.test('#9999'), 'two-entry list must match the first entry');
assert.ok(double.test('#8888'), 'two-entry list must match the second entry');
});
module.exports = { buildRefRe };