config-audit/tests/scanners/optimization-lens-scanner.test.mjs
Kjell Tore Guttormsen e7833b65fc feat(opt): optimization lens CA-OPT-001 (procedure→skill) — v5.7 Fase 1 Chunk 2a
First detector of the 'is the config OPTIMAL?' axis (vs the existing 'correct?' scanners). New orchestrated scanner family CA-OPT (count 14->15), the deterministic half of the hybrid optimization lens.

CA-OPT-001 (low, Missed opportunity): a multi-step procedure in CLAUDE.md (>=6 consecutive numbered steps) that belongs in a skill. Reads recommendation + provenance from the best-practices register (BP-MECH-003). Conservative by design; the negative corpus proves null false-positives. Prose-judgment cases (lifecycle->hook, 'never'->permission) are deferred to the Chunk 2b opus analyzer.

Wiring mirrors OST: orchestrator entry, humanizer (OPT->'Missed opportunity' + family), scoring (OPT->'CLAUDE.md', existing area -> no new posture row -> byte-stable), strip-helper (OST,OPT), SC-5 regenerated under hermetic HOME (additive OPT entry only). 10 new tests; suite 1045->1055, self-audit A/A, readmeCheck passed (all verified with a clean HOME).

Note: the pre-existing TOK test reads the real ~/.claude (non-hermetic) -> run the suite with a clean HOME for deterministic results. Tracked as a separate follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:10:03 +02:00

112 lines
4.5 KiB
JavaScript

/**
* OPT scanner tests — optimization lens / mechanism-fit (v5.7 Fase 1 Chunk 2a).
*
* Chunk 2a ships ONE deterministic, high-precision detector: a multi-step
* procedure in CLAUDE.md should be a skill (BP-MECH-003). Each case has a
* positive fixture (fires) and negatives (silent) — the negative corpus proves
* the null-false-positive requirement. Prose-judgment heuristics (lifecycle→hook,
* "never"→permission) are deliberately deferred to the Chunk 2b opus analyzer.
*
* The scanner reads discovery's claude-md files directly and pulls its
* recommendation/provenance from the best-practices register (BP-MECH-003).
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { resetCounter } from '../../scanners/lib/output.mjs';
import { scan } from '../../scanners/optimization-lens-scanner.mjs';
const tmp = () => mkdtemp(join(tmpdir(), 'ca-opt-'));
const PROCEDURE_TITLE = 'A multi-step procedure in CLAUDE.md belongs in a skill';
/** numbered list of n imperative steps */
const steps = (n, sep = '\n') =>
Array.from({ length: n }, (_, i) => `${i + 1}. Run deploy step ${i + 1} and verify it.`).join(sep);
async function runOpt(claudeMd, relPath = 'CLAUDE.md') {
const repo = await tmp();
const abs = join(repo, relPath.replace(/\//g, '-'));
await writeFile(abs, claudeMd, 'utf-8');
resetCounter();
try {
return await scan(repo, {
files: [{ absPath: abs, relPath, type: 'claude-md', scope: 'project', size: claudeMd.length }],
});
} finally {
await rm(repo, { recursive: true, force: true });
}
}
const hasProc = (result) => (result.findings || []).find((f) => f.title === PROCEDURE_TITLE);
describe('OPT scanner — result shape', () => {
it('returns an ok OPT scanner result', async () => {
const result = await runOpt('# Project\n\nJust facts here.\n');
assert.equal(result.scanner, 'OPT');
assert.equal(result.status, 'ok');
});
it('emits nothing and stays ok when no claude-md files are discovered', async () => {
const repo = await tmp();
resetCounter();
try {
const result = await scan(repo, { files: [] });
assert.equal(result.status, 'ok');
assert.deepEqual(result.findings, []);
} finally {
await rm(repo, { recursive: true, force: true });
}
});
});
describe('OPT CA-OPT-001 — procedure in CLAUDE.md → skill (positive)', () => {
it('fires on a 6-step numbered procedure', async () => {
const f = hasProc(await runOpt(`# Release\n\n${steps(6)}\n`));
assert.ok(f, 'expected the procedure finding');
assert.equal(f.scanner, 'OPT');
assert.equal(f.severity, 'low');
assert.match(f.id, /^CA-OPT-\d{3}$/);
});
it('fires when steps are separated by blank lines', async () => {
const f = hasProc(await runOpt(`# Release checklist\n\n${steps(7, '\n\n')}\n`));
assert.ok(f);
assert.equal(f.details.steps >= 6, true);
});
it('reads its recommendation + provenance from register BP-MECH-003', async () => {
const f = hasProc(await runOpt(`# Steps\n\n${steps(6)}\n`));
assert.equal(f.details.register, 'BP-MECH-003');
assert.equal(f.details.mechanism, 'skill');
assert.ok(f.recommendation && f.recommendation.length > 0);
assert.ok(f.description && f.description.length > 0);
});
it('emits one finding per file, not one per step', async () => {
const result = await runOpt(`# Steps\n\n${steps(12)}\n`);
const procs = (result.findings || []).filter((x) => x.title === PROCEDURE_TITLE);
assert.equal(procs.length, 1);
});
});
describe('OPT CA-OPT-001 — negative corpus (must be silent)', () => {
it('does NOT fire on a short (5-step) list', async () => {
assert.equal(hasProc(await runOpt(`# Steps\n\n${steps(5)}\n`)), undefined);
});
it('does NOT fire on a bulleted (non-numbered) list of 8 items', async () => {
const bullets = Array.from({ length: 8 }, (_, i) => `- point ${i + 1}`).join('\n');
assert.equal(hasProc(await runOpt(`# Notes\n\n${bullets}\n`)), undefined);
});
it('does NOT fire on prose with no list', async () => {
assert.equal(hasProc(await runOpt('# Project\n\nWe use TypeScript. Build with npm.\n')), undefined);
});
it('does NOT fire on two separate short numbered lists', async () => {
const md = `# A\n\n${steps(3)}\n\n## B\n\nsome prose breaks the run\n\n${steps(3)}\n`;
assert.equal(hasProc(await runOpt(md)), undefined);
});
});