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
115 lines
4.7 KiB
JavaScript
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);
|
|
});
|
|
}
|
|
});
|