config-audit/tests/lib/prompting-model-scope.test.mjs
Kjell Tore Guttormsen 7df8e0d65b feat(scanners): a redundancy claim that belongs to one model is scoped to it
Anthropic documents that Claude Opus 5 verifies its own work, and that telling
it to double-check or to delegate verification to a subagent causes
over-verification -- token cost with no quality gain. The general subtraction
detector (BP-SUB-001) already surfaces those blocks for every user, with no
model-awareness at all.

`optimize --subtract --for-model <name>` adds the missing half. It ANNOTATES a
subset of the candidates --subtract already produced; it is not a second
detector and can never widen the candidate set. A second SUBTRACT_DETECTORS
entry would have collided with BP-SUB-001 on de-dup, and a prose-only signal in
the agent prompt would have been untestable.

There is no auto-detection, by measurement rather than omission: a CLAUDE.md has
no frontmatter and no resolvable target model, and this operator's own `route`
skill deliberately runs a different model per session -- the same file is read
by whichever model comes next. So the model is named, and the citation is
reported as conditional everywhere a human sees it (agent report copy, and the
Step 7a listing that is the last surface before an approval file).

Precision comes from the TARGET, not the verb list. Measured across the
409-file corpus: 392 BP-SUB-001 candidates, 31 (7.9%) carry a verify verb, and
0 also carry a reflexive or delegated target. Two independent raw-text greps
found 0 as well, so the zero is the corpus rather than an over-narrow regex.
Those 31 verb-only blocks -- "sjekk relevante config-filer", "Type-sjekk:
pyright", "To verify plugin functionality" -- are exactly the false positives a
verb-only version would have produced, which is BP-JUDG-001's 7/7 failure
arriving one lens over. The numbers live in the register entry's note and are
pinned by a test, because a session that cannot see the measurement reads the
zero as a broken detector and loosens it.

`recognized` is reported separately from `matchedCount`: a typo'd model name and
a genuinely clean config both yield zero, and without the distinction the CLI
would report a silent no-op as good news. Dogfooded on the real machine --
`opus-5` gives recognized:true/matchedCount:0, `oppus5` gives recognized:false.

source.published is absent because the guide carries no visible publish date;
its absence is asserted so a later session does not invent one to match the
other entries' shape. Both quoted sentences were verified verbatim 2026-08-12.

The payload stays additive -- forModel and per-candidate modelScope appear only
under the flag, so a plain --subtract run is byte-identical to before (asserted
on the serialized bytes, since a key set to undefined passes a shallow check).

Suite 1724 -> 1752 (+28). The one remaining failure is the pre-existing
drift-cli --output-file crash, untouched by this work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BRuXt6tZyowi8QYNKLSHQm
2026-08-12 23:16:23 +02:00

115 lines
4.3 KiB
JavaScript

/**
* prompting-model-scope tests — the model-scoped ANNOTATION layer on top of the
* subtraction lens's single `compensatory-instruction` detector (BP-SUB-001).
*
* This module does NOT generate new subtraction candidates. It answers a
* narrower question over text that already passed the general detector: does
* this block specifically claim a model no longer needs — self-verification, or
* delegated-to-a-subagent verification? Precision comes from requiring a
* reflexive/delegate TARGET alongside the verify verb, not from narrowing the
* verb list — a bare "check"/"verify" also matches EXTERNAL verification
* ("check the CI status"), which must stay a true negative here even though it
* is (correctly) still a BP-SUB-001 candidate upstream.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
isModelContradictedVerification,
matchModelScope,
normalizeModel,
} from '../../scanners/lib/prompting-model-scope.mjs';
describe('isModelContradictedVerification — true positives', () => {
const positives = [
'Always double-check your own work before responding.',
'Re-verify before responding to the user.',
'Verify complex changes with a subagent.',
'Use a subagent to verify the diff before finishing.',
'Dobbeltsjekk ditt eget arbeid før du svarer.',
'Verifiser med en subagent før du er ferdig.',
];
for (const text of positives) {
it(`matches: "${text}"`, () => {
assert.equal(isModelContradictedVerification(text), true);
});
}
});
describe('isModelContradictedVerification — true negatives (external verification)', () => {
const negatives = [
'Check the CI status before merging.',
'Verify the deployment succeeded.',
'Sjekk build-status i CI før du merger.',
'Review the pull request for style issues.',
'Confirm the ticket is assigned to you.',
'Read the file before editing it.',
];
for (const text of negatives) {
it(`does not match: "${text}"`, () => {
assert.equal(isModelContradictedVerification(text), false);
});
}
});
describe('normalizeModel', () => {
it('lowercases and strips non-alphanumerics', () => {
for (const s of ['opus-5', 'Opus 5', 'OPUS5', 'opus_5']) {
assert.equal(normalizeModel(s), 'opus5');
}
});
it('handles null/undefined/empty', () => {
assert.equal(normalizeModel(null), '');
assert.equal(normalizeModel(undefined), '');
assert.equal(normalizeModel(''), '');
});
});
describe('matchModelScope', () => {
const entries = [
{ id: 'BP-PROMPT-001', claim: 'test claim', modelScope: ['opus-5'] },
];
const positiveText = 'Always double-check your own work.';
it('returns null when targetModel is absent', () => {
assert.equal(matchModelScope(positiveText, null, entries), null);
assert.equal(matchModelScope(positiveText, undefined, entries), null);
assert.equal(matchModelScope(positiveText, '', entries), null);
});
it('returns null when the text is not a model-contradicted verification claim', () => {
assert.equal(matchModelScope('Check the CI status.', 'opus-5', entries), null);
});
it('returns null when no entry matches the requested model', () => {
assert.equal(matchModelScope(positiveText, 'sonnet-5', entries), null);
});
it('returns null when entries is empty', () => {
assert.equal(matchModelScope(positiveText, 'opus-5', []), null);
});
it('matches on an exact normalized model name', () => {
const m = matchModelScope(positiveText, 'opus-5', entries);
assert.ok(m);
assert.equal(m.registerId, 'BP-PROMPT-001');
assert.equal(m.claim, 'test claim');
assert.equal(m.requestedModel, 'opus-5');
});
it('matches across normalization variants of the same model name', () => {
for (const variant of ['Opus 5', 'OPUS-5', 'opus5']) {
const m = matchModelScope(positiveText, variant, entries);
assert.ok(m, `expected a match for "${variant}"`);
assert.equal(m.registerId, 'BP-PROMPT-001');
}
});
it('picks the entry whose modelScope contains the requested model, ignoring others', () => {
const multi = [
{ id: 'BP-PROMPT-OTHER', claim: 'unrelated', modelScope: ['haiku-5'] },
{ id: 'BP-PROMPT-001', claim: 'test claim', modelScope: ['opus-5'] },
];
const m = matchModelScope(positiveText, 'opus-5', multi);
assert.equal(m.registerId, 'BP-PROMPT-001');
});
});