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>
112 lines
4.5 KiB
JavaScript
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);
|
|
});
|
|
});
|