config-audit/tests/lib/finding-codes.test.mjs
Kjell Tore Guttormsen 9ae4be26d2 feat(scanners): model/effort routing becomes a lever, not a 25th dimension (C4)
New GAP finding CA-GAP-028: authored subagents exist and not one of them names
`model:` or `effort:`, so every delegated task runs on the main conversation's
model (`model` defaults to `inherit`). Cites BP-MODEL-001/002, landed in C1.
`whats-active` and `manifest` now carry `model`/`effort` per agent.

Shipped as a conditional LEVER rather than a 25th dimension, and the choice was
made by measurement: as a t3 dimension the agent-less marketplace-medium fixture
would count it vacuously-present, moving the denominators 41->42 and utilization
44->45 — which flips `segment` "Developing"->"Competent" in the frozen v5.0.0
posture baseline, a field strip-retired-gap.mjs does not mask. A lever never
enters those denominators. The general rule is now an invariant in CLAUDE.md.

One check across both axes, not one per axis: it fires only when neither is used
anywhere, so a deliberate everything-on-one-model policy stays silent. Cost is
recall, chosen for precision.

Found by dogfooding, fixed red-first: `model: inherit` is the documented default
spelled out, so it must not count as routing — otherwise a config opts out of the
opportunity without changing anything real.

Two pre-existing defects surfaced and closed on the way:
- The humanizer guard asserted TRANSLATIONS.GAP.static EQUALS the dimension
  titles, which forbade humanizing any lever — all three existing levers fell
  through to the generic "feature opportunity" default, wrong for a budget lever.
  Guard now requires coverage of every emittable title, seen red against those
  three before the entries were written.
- Two hand-written copies of the lever list (finding-codes guard, humanizer
  guard) merged into one exported LEVERS registry carrying code AND title.
- suppression-validation pinned CA-GAP-028 as an unoccupied number; C4 claimed
  it. Fixed structurally with a derived first-free id, not by picking a new
  literal — same class as #60's "bump this again".

Suite 1596/0. Frozen v5.0.0 snapshots untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq3nye21RVYk4pZLeT8pGz
2026-08-10 05:07:23 +02:00

115 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'],
];
for (const [scanner, key, expected] of PUBLISHED) {
it(`${expected} still names ${key}`, () => {
assert.equal(findingId(scanner, key), expected);
});
}
});