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>
This commit is contained in:
parent
55f83a3c99
commit
e7833b65fc
14 changed files with 350 additions and 38 deletions
112
tests/scanners/optimization-lens-scanner.test.mjs
Normal file
112
tests/scanners/optimization-lens-scanner.test.mjs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -103,7 +103,7 @@ describe('scan-orchestrator humanizer wiring (Step 5)', () => {
|
|||
assert.ok(actual.meta, 'meta present');
|
||||
assert.ok(Array.isArray(actual.scanners), 'scanners array present');
|
||||
assert.ok(actual.aggregate, 'aggregate present');
|
||||
assert.equal(actual.scanners.length, 14, 'all 14 scanners present');
|
||||
assert.equal(actual.scanners.length, 15, 'all 15 scanners present');
|
||||
});
|
||||
|
||||
it('preserves scanner shape (scanner/status/findings/counts)', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue