Every command so far asked an addition question — what to add, what to move, what it costs. Nothing asked what is no longer earning its always-loaded rent. This adds that axis as a fourth lensCheck on the existing hybrid motor rather than a new scanner or a 22nd command: the measured payoff (~18% of one file) justifies a mode, not machinery. It is the only lens that proposes REMOVING config, so it carries a guarantee the others don't need: a load-bearing block is never a candidate. Precision is asymmetric — a missed dead line costs a few tokens per turn, a deleted one costs a wrong remote or a broken script — so the floor is decided in code (lib/floor-exclusion.mjs) before the opus judge sees anything, never in prose. Granularity is the leaf block, with two structural exceptions: a paragraph ending in ':' merges with the list it introduces, and an ordered list is a contract whose steps inherit floor from any sibling. Unordered lists deliberately do not inherit — a load-bearing bullet and a disposable one routinely share a list, and container-reasoning is the error the hand-built ground truth exists to catch. Verified against that ground truth (built before any classifier existed), with the comparison machine-checked rather than read by eye: zero load-bearing blocks proposed, 11/18 deletable groups surfaced, ~756 tok ~ 18% of a ~4300 token file — inside the pre-registered band. The first run found five floor violations the synthesized fixture missed; each got a structural rule and a fixture shape so it cannot regress. Three real bugs the dogfood run exposed, all now covered: - JS \b is ASCII-only, so /\bunngå\b/ never matches — every Norwegian keyword ending in æ/ø/å was silently dead. - A bare word/word is not a path; "pros/cons" vetoed the largest deletable block until PATH_RE was tightened to rooted paths and globs. - "Mid-sentence" must key on a preceding lowercase letter; the loose version read **bold labels:** and quoted openers as entities, costing 4 of 11 groups. BP-SUB-001 is grounded entirely in the Anthropic steering blog already cited by BP-MECH-001..004 and asserts nothing from the talk that motivated the feature — no "80%", no ablation figure. Suite 1365 -> 1382/0. Frozen v5.0.0 snapshots untouched; plain optimize output byte-identical on identical input (--subtract adds keys only when passed). knowledge-refresh-cli's reference date moved to 2026-08-01: its premise that every seed entry was verified 2026-06-20 expired when BP-SUB-001 got a genuine verification date, and backdating the entry to fit the test would have been a lie about when its source was checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW2haJXbxZpKivKHseSXNh
228 lines
12 KiB
JavaScript
228 lines
12 KiB
JavaScript
/**
|
|
* subtraction-prefilter tests — the deterministic half of the v5.13 subtraction
|
|
* lens (`/config-audit optimize --subtract`, CA-OPT / BP-SUB-001).
|
|
*
|
|
* The subtraction axis asks the inverse of every other command: *what is no
|
|
* longer earning its always-loaded rent?* Precision here is ASYMMETRIC — a
|
|
* missed dead line costs a few tokens per turn, a deleted load-bearing line
|
|
* costs a wrong remote or a broken script (brief §6.0). So unlike
|
|
* `lens-prefilter` (recall-first, agent as the gate), this module is
|
|
* precision-first and carries a BLOCKING deterministic guarantee:
|
|
*
|
|
* **a load-bearing block never reaches the judge at all.**
|
|
*
|
|
* That guarantee is delivered by two independent mechanisms, and the contract is
|
|
* therefore asserted on the CANDIDATE LIST rather than on either mechanism:
|
|
* 1. the compensatory detector does not fire on declarative local facts
|
|
* ("Language: Norwegian for dialogue") — they are never generated; and
|
|
* 2. floor-exclusion vetoes any leaf block carrying an underivable local
|
|
* literal (code span, path, domain, version pin) or a policy invariant.
|
|
* A test written against `isLoadBearing()` alone would miss group 1 entirely.
|
|
*
|
|
* GRANULARITY (the one design choice the fasit left open, §5): leaf-block — one
|
|
* markdown list item including its wrapped continuation lines, or one paragraph.
|
|
* It must SPLIT a mixed numbered list and three consecutive bullets, and must
|
|
* KEEP WHOLE a bullet whose load-bearing literal sits on a continuation line.
|
|
*
|
|
* FIXTURE, not the real subject file. The fasit was built against the operator's
|
|
* private `~/.claude/CLAUDE.md`; that file is machine-dependent, edited between
|
|
* sessions, and this repo's only remote is a PUBLIC mirror. So the committed
|
|
* suite runs against a synthesized fixture that reproduces the discriminating
|
|
* *shapes*, and the run against the real file is the manual dogfood gate (brief
|
|
* §8), recorded in the worklist — not a suite test.
|
|
*/
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
splitLeafBlocks,
|
|
subtractionCandidates,
|
|
floorExcluded,
|
|
SUBTRACT_DETECTORS,
|
|
} from '../../scanners/lib/subtraction-prefilter.mjs';
|
|
import { isLoadBearing } from '../../scanners/lib/floor-exclusion.mjs';
|
|
|
|
const FIXTURE = readFileSync(
|
|
fileURLToPath(new URL('../fixtures/subtraction/shapes-claude-md.md', import.meta.url)),
|
|
'utf8',
|
|
);
|
|
|
|
const candidateLines = (text) => subtractionCandidates(text).map((c) => c.line);
|
|
const covers = (cands, line) => cands.some((c) => line >= c.startLine && line <= c.endLine);
|
|
|
|
/**
|
|
* Every line of the fixture that the fasit labels FLOOR or POLICY-FLOOR. If any
|
|
* of these is covered by a candidate block, the gate fails outright — whatever
|
|
* the recall. (brief §8, "zero load-bearing blocks proposed for deletion")
|
|
*/
|
|
const FLOOR_LINES = [
|
|
13, 14, // declarative preference facts (B-04 / B-05 shape) — must never fire
|
|
22, // format contract in a code span (B-30c) — adjacent to two other calls
|
|
26, 27, 28, 29, // ordered list = contract (B-16 / B-17): siblings 27-28 floor the whole run
|
|
91, 92, 93, // list stem + its floor items (B-09) — the stem carries no literal of its own
|
|
97, // "bruk Explore for søk" — an entity the mechanism cannot resolve, so it keeps it
|
|
40, 41, // generic-sounding advice wrapped around a local incident (B-27 — THE trap)
|
|
47, 48, 49, 50, // floor bullet inside a compensatory list (B-32b), literal on a continuation line
|
|
55, 56, // §8 named anchor: the remote (B-25) + the push workflow (B-23)
|
|
60, 61, // §8 named anchor: system bash version (B-26)
|
|
65, 66, // stale-in-content but load-bearing (B-29) — staleness is NOT a deletion signal
|
|
70, // policy invariant, floor by decision not classification (B-34)
|
|
72, // absolute policy prohibition as a heading (B-37)
|
|
80, // local fact about how skills behave (B-38b)
|
|
85, 86, // inside a fenced code block — never a candidate
|
|
];
|
|
|
|
/** Lines the fasit labels DELETABLE that the mechanism must actually surface. */
|
|
const MUST_SURFACE = [
|
|
15, // "avoid unnecessary text" — the B-06 half of the B-05/B-06 adjacent pair
|
|
16, // "Never say 'as an AI I cannot'" — textbook tier-3 compensatory
|
|
33, // "Commit often with descriptive messages" (B-19b) — the named pure-behaviour case
|
|
36, // "Aldri gjett på rotårsak" — Norwegian imperative
|
|
46, // "Never declare done without running the tests"
|
|
76, // the workflow paragraph (B-38a) — split from its floor line at 80
|
|
98, // "Skriv alltid kortest mulig kode" — survives next to an entity-vetoed sibling
|
|
];
|
|
|
|
describe('subtraction-prefilter — detector table', () => {
|
|
it('registers the subtraction class against BP-SUB-001, separately from LENS_DETECTORS', async () => {
|
|
const map = new Map(SUBTRACT_DETECTORS.map((d) => [d.lensCheck, d]));
|
|
assert.ok(map.has('compensatory-instruction'), 'subtraction lensCheck must exist');
|
|
assert.equal(map.get('compensatory-instruction').registerId, 'BP-SUB-001');
|
|
|
|
// Byte-stability: the three mechanism-fit detectors drive the plain
|
|
// `optimize` payload's `register` block. Adding the subtraction class there
|
|
// would change plain-optimize output, which the brief forbids.
|
|
const { LENS_DETECTORS } = await import('../../scanners/lib/lens-prefilter.mjs');
|
|
assert.equal(LENS_DETECTORS.length, 3, 'plain optimize must still expose exactly 3 detectors');
|
|
assert.ok(
|
|
!LENS_DETECTORS.some((d) => d.lensCheck === 'compensatory-instruction'),
|
|
'the subtraction class must not leak into the plain-optimize detector table',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('subtraction-prefilter — leaf-block granularity (the declared contract)', () => {
|
|
it('keeps a wrapped bullet whole, so a continuation-line literal still protects it', () => {
|
|
const blocks = splitLeafBlocks(FIXTURE);
|
|
const b32b = blocks.find((b) => b.startLine === 47);
|
|
assert.ok(b32b, 'the wrapped bullet must start its own block at line 47');
|
|
assert.equal(b32b.endLine, 50, 'its continuation lines belong to the same block');
|
|
});
|
|
|
|
it('splits three consecutive bullets into three blocks (three different calls)', () => {
|
|
const blocks = splitLeafBlocks(FIXTURE);
|
|
for (const line of [20, 21, 22]) {
|
|
assert.ok(
|
|
blocks.some((b) => b.startLine === line && b.endLine === line),
|
|
`line ${line} must be its own leaf block`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('treats an ordered list as a contract: a floor sibling floors the whole run', () => {
|
|
// Steps 2-3 name the file to read and the check to run; steps 1 and 4 are
|
|
// filler. Splitting them would propose deleting step 4 of a five-step
|
|
// protocol — so the run inherits floor. (Structural exception 2.)
|
|
const cands = subtractionCandidates(FIXTURE);
|
|
for (const line of [26, 27, 28, 29]) {
|
|
assert.ok(!covers(cands, line), `ordered step at line ${line} must inherit floor`);
|
|
}
|
|
const reasons = floorExcluded(FIXTURE);
|
|
assert.ok(
|
|
reasons.some((r) => r.startLine === 29 && r.marker === 'ordered-contract'),
|
|
'step 4 must be recorded as floored by contract inheritance, not by its own literal',
|
|
);
|
|
});
|
|
|
|
it('merges a list stem with the items it introduces (structural exception 1)', () => {
|
|
const blocks = splitLeafBlocks(FIXTURE);
|
|
const stem = blocks.find((b) => b.startLine === 91);
|
|
assert.ok(stem, 'the stem paragraph must open a block at line 91');
|
|
assert.equal(stem.endLine, 93, 'the stem absorbs the list it introduces');
|
|
// The stem itself carries no literal — it is protected only by the merge.
|
|
assert.equal(isLoadBearing('Konteksten mellom sesjoner hviler på tre lag med hver sin ene jobb:'), false);
|
|
});
|
|
|
|
it('never emits a heading as a candidate block', () => {
|
|
const cands = subtractionCandidates(FIXTURE);
|
|
assert.ok(!covers(cands, 72), 'the Iron Law heading must not be a deletion candidate');
|
|
});
|
|
});
|
|
|
|
describe('subtraction-prefilter — THE GATE (brief §8, blocking)', () => {
|
|
it('proposes ZERO load-bearing lines for deletion', () => {
|
|
const cands = subtractionCandidates(FIXTURE);
|
|
const violations = FLOOR_LINES.filter((line) => covers(cands, line));
|
|
assert.deepEqual(
|
|
violations,
|
|
[],
|
|
`load-bearing lines proposed for deletion: ${violations.join(', ')} — gate fails outright`,
|
|
);
|
|
});
|
|
|
|
it('still surfaces genuinely compensatory blocks (recall is secondary, not zero)', () => {
|
|
const cands = subtractionCandidates(FIXTURE);
|
|
const missed = MUST_SURFACE.filter((line) => !covers(cands, line));
|
|
assert.deepEqual(missed, [], `expected these to be candidates: ${missed.join(', ')}`);
|
|
});
|
|
|
|
it('separates the adjacent near-identical preference bullets (B-05 vs B-06)', () => {
|
|
const lines = candidateLines(FIXTURE);
|
|
assert.ok(!lines.includes(14), 'the tone preference is a fact about the human — keep');
|
|
assert.ok(lines.includes(15), 'the "avoid unnecessary text" nag is compensatory — surface');
|
|
});
|
|
|
|
it('does not fire inside fenced code blocks', () => {
|
|
const cands = subtractionCandidates(FIXTURE);
|
|
assert.ok(!covers(cands, 85) && !covers(cands, 86), 'fenced code is not config prose');
|
|
});
|
|
});
|
|
|
|
describe('floor-exclusion — the deterministic veto', () => {
|
|
it('vetoes underivable local literals', () => {
|
|
assert.ok(isLoadBearing('Aldri GitHub. Kun Forgejo (`git.example.test`).'), 'code span + domain');
|
|
assert.ok(isLoadBearing('System bash er **3.2**: ingen declare -A.'), 'version pin');
|
|
assert.ok(isLoadBearing('Read the nearest STATE.md before starting.'), 'concrete filename');
|
|
assert.ok(isLoadBearing('Tokens live in ~/.zshenv on this machine.'), 'home path');
|
|
});
|
|
|
|
it('vetoes policy invariants regardless of phrasing (§6.0 carve-out)', () => {
|
|
assert.ok(isLoadBearing('ALDRI commit secrets.'), 'secrets are floor by decision');
|
|
});
|
|
|
|
it('does not veto generic behaviour correction carrying no local fact', () => {
|
|
assert.equal(isLoadBearing('Commit often with descriptive messages.'), false);
|
|
assert.equal(isLoadBearing('Think before you code and avoid unnecessary text.'), false);
|
|
});
|
|
|
|
it('vetoes an unresolvable mid-sentence entity, but not a bold label or a quoted opener', () => {
|
|
// The conservative default: a product/tool name is local vocabulary the
|
|
// mechanism cannot resolve without a dictionary, so it keeps the block.
|
|
assert.ok(isLoadBearing('push til deres egne Forgejo-remotes også'), 'mid-sentence entity');
|
|
// …but these are sentence openings dressed in punctuation. Reading them as
|
|
// entities cost 4 of 11 deletable groups in the dogfood run.
|
|
assert.equal(isLoadBearing('- **Format:** Konkret og handlingsorientert'), false, 'bold label');
|
|
assert.equal(isLoadBearing('- **Aldri:** "Som AI kan jeg ikke..."'), false, 'quoted opener + acronym');
|
|
});
|
|
|
|
it('does not treat a bare word/word as a path (it is not one)', () => {
|
|
// "pros/cons" vetoed the largest deletable block until PATH_RE was tightened.
|
|
assert.equal(isLoadBearing('Presenter alternativene med pros/cons og foreslå ett valg.'), false);
|
|
assert.ok(isLoadBearing('Store baselines under ~/.config-audit/baselines/.'), 'a real path still vetoes');
|
|
});
|
|
});
|
|
|
|
describe('subtraction-prefilter — Unicode word boundaries', () => {
|
|
// JS `\b` is ASCII-only, so /\bunngå\b/ never matches: the trailing "å" is
|
|
// not a word character. Every Norwegian keyword ending in æ/ø/å was silently
|
|
// dead until the dogfood run surfaced it.
|
|
it('matches Norwegian keywords ending in æ/ø/å', () => {
|
|
const c = subtractionCandidates('- Vær konkret og handlingsorientert, unngå overflødig tekst her.');
|
|
assert.equal(c.length, 1, '"unngå" must be detected despite the trailing å');
|
|
});
|
|
|
|
it('still requires a whole-word match', () => {
|
|
assert.deepEqual(subtractionCandidates('- Systemet er uunngåelig komplekst og stort.'), []);
|
|
});
|
|
});
|