/** * 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.'), []); }); });