config-audit/tests/lib/finding-codes.test.mjs
Kjell Tore Guttormsen dbb6a6a3cf feat(scanners): a path written in prose is now resolved, not assumed (C3)
`import-resolver` follows @import targets; a path written in ordinary prose
was checked by nothing. CA-CML-013 resolves those too — one finding per file,
severity low, against both the CLAUDE.md's own directory and the scan root,
because a nested file may legitimately write repo-root-relative paths.

The design work here is the SILENCE list, and every entry on it was measured
against 407 real CLAUDE.md files rather than argued for:

- Bare filenames excluded: admitting them tripled the output (2350 vs 810),
  led by name-drops of tools that exist elsewhere on the machine.
- Org/repo slugs, npm packages, pytest node ids and prose enumerations
  excluded: 111 fires, inspected, all false positives.
- Bare folder names excluded on the same reasoning one level up: 183 of the
  remaining 699 fires (26%), led by `open/` — a Forgejo remote namespace
  prefix, not a directory. This one overturned a premise the fasit had
  asserted without measuring; the deviation is recorded rather than the
  prediction quietly edited.
- Containment is checked against the scan root, not the file's own dir: a
  base a `..` chain can escape is not a base. Measured — without it,
  `../../../../etc/passwd` resolved to the real file and silenced its own
  finding, while a legitimate `../docs/x.md` still resolves.

Rule ORDER is the reported reason (first match wins), so `npm test` is
silenced as a command rather than as a bare token, and two silences with
different causes keep their own fixtures. Twelve classes, pinned by name.

Both load-bearing rules were seen RED against their own defect: deleting
containment fails 1 test, deleting the slug rule fails 6.

Dogfooded through the argv the command template itself constructs, which
found a true positive in our own CLAUDE.md — `lib/humanizer.mjs` where the
file is `scanners/lib/humanizer.mjs`. Fixed here.

Suite 1662 -> 1701, 0 failing. Frozen v5.0.0 and default-output baselines:
0 changed files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJbfM3N8zWQ1wA2voTrZxz
2026-08-12 20:11:12 +02:00

116 lines
4.7 KiB
JavaScript

/**
* Registry invariants for the finding-code scheme (M-BUG-28).
*
* These are blanket assertions over the whole registry, never a relation between
* two chosen entries: a per-entry check goes green on a partial conversion, which
* is the failure mode #51/#57/#58 kept reproducing.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { FINDING_CODES, RETIRED_CODES, codeNumber, findingId, allFindingIds } from '../../scanners/lib/finding-codes.mjs';
import { GAP_CHECKS, LEVERS } from '../../scanners/feature-gap-scanner.mjs';
describe('finding-code registry', () => {
it('gives every check a distinct number within its scanner', () => {
const collisions = [];
for (const [scanner, table] of Object.entries(FINDING_CODES)) {
const seen = new Map();
for (const [key, n] of Object.entries(table)) {
if (seen.has(n)) collisions.push(`${scanner}: ${seen.get(n)} and ${key} both claim ${n}`);
seen.set(n, key);
}
}
assert.deepEqual(collisions, []);
});
it('uses positive integers only', () => {
const bad = [];
for (const [scanner, table] of Object.entries(FINDING_CODES)) {
for (const [key, n] of Object.entries(table)) {
if (!Number.isInteger(n) || n < 1) bad.push(`${scanner}.${key} = ${n}`);
}
}
assert.deepEqual(bad, []);
});
it('never lets a retired key stay active', () => {
const resurrected = [];
for (const [scanner, keys] of Object.entries(RETIRED_CODES)) {
for (const key of keys) {
if (FINDING_CODES[scanner] && key in FINDING_CODES[scanner]) {
resurrected.push(`${scanner}.${key}`);
}
}
}
assert.deepEqual(resurrected, []);
});
it('never reissues a retired number', () => {
// The whole point of the tombstone: D1 retired GAP t3_8 and shifted three
// live IDs down by one. Reusing a retired number would repeat that silently.
assert.ok(RETIRED_CODES.GAP.includes('t3_8'), 'D1 tombstone missing');
});
it('declares exactly the GAP dimensions the scanner ships, plus its levers', () => {
const declared = new Set(Object.keys(FINDING_CODES.GAP));
const shipped = GAP_CHECKS.map((g) => g.id);
const missing = shipped.filter((id) => !declared.has(id));
assert.deepEqual(missing, [], 'a GAP dimension has no declared code');
// Derived from the scanner, not listed again here: a hand-written copy of
// this list is the drift class the registry exists to prevent.
const levers = Object.values(LEVERS).map((l) => l.code);
const orphans = [...declared].filter((k) => !shipped.includes(k) && !levers.includes(k));
assert.deepEqual(orphans, [], 'a declared GAP code matches no shipped dimension');
});
it('throws on an undeclared code instead of inventing an ID', () => {
assert.throws(() => codeNumber('GAP', 'nope'), /undeclared check/);
assert.throws(() => codeNumber('NOSUCH', 't1_1'), /unknown scanner/);
assert.throws(() => codeNumber('GAP', undefined), /missing "code"/);
});
it('names retirement explicitly when a retired key is used', () => {
assert.throws(() => codeNumber('GAP', 't3_8'), /RETIRED/);
});
it('renders the published ID format', () => {
assert.equal(findingId('GAP', 't1_1'), 'CA-GAP-001');
assert.equal(findingId('PLH', 'skills-array-entry'), 'CA-PLH-016');
assert.ok(allFindingIds().has('CA-SKL-003'));
});
});
describe('published finding IDs (pinned exhaustively — README is a contract)', () => {
// Every number that shipped in README, CLAUDE.md or command copy before the
// registry existed. Spot-checking these would let a renumber through.
const PUBLISHED = [
['SKL', 'description-over-cap', 'CA-SKL-001'],
['SKL', 'aggregate-listing-budget', 'CA-SKL-002'],
['SKL', 'oversized-body', 'CA-SKL-003'],
['OST', 'strips-coding-instructions', 'CA-OST-001'],
['OST', 'plugin-forces-style', 'CA-OST-002'],
['OST', 'style-not-found', 'CA-OST-003'],
['TOK', 'volatile-top', 'CA-TOK-001'],
['TOK', 'redundant-permissions', 'CA-TOK-002'],
['TOK', 'deep-import-chain', 'CA-TOK-003'],
['TOK', 'mcp-schema-budget', 'CA-TOK-005'],
['TOK', 'mcp-schema-deferral', 'CA-TOK-006'],
['PLH', 'plugin-json-shadows-default', 'CA-PLH-015'],
['PLH', 'skills-array-entry', 'CA-PLH-016'],
['OPT', 'procedure-should-be-skill', 'CA-OPT-001'],
['AGT', 'description-bloat', 'CA-AGT-001'],
['AGT', 'aggregate-listing-budget', 'CA-AGT-002'],
['CPS', 'volatile-in-prefix', 'CA-CPS-001'],
['COL', 'skill-user-vs-plugin', 'CA-COL-001'],
['CML', 'dead-prose-reference', 'CA-CML-013'],
];
for (const [scanner, key, expected] of PUBLISHED) {
it(`${expected} still names ${key}`, () => {
assert.equal(findingId(scanner, key), expected);
});
}
});