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
|
|
@ -17,7 +17,7 @@
|
|||
* return stripAddedScanners(stripHotspotLoadPattern(out));
|
||||
*/
|
||||
|
||||
const ADDED_SCANNERS = new Set(['OST']);
|
||||
const ADDED_SCANNERS = new Set(['OST', 'OPT']);
|
||||
|
||||
function stripFromEnvelope(env) {
|
||||
if (!env || typeof env !== 'object' || !Array.isArray(env.scanners)) return;
|
||||
|
|
@ -58,5 +58,5 @@ export function stripAddedScanners(payload) {
|
|||
*/
|
||||
export function stripAddedScannerStderr(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
return text.replace(/^[ \t]*\[OST\][^\n]*\n/gm, '');
|
||||
return text.replace(/^[ \t]*\[(OST|OPT)\][^\n]*\n/gm, '');
|
||||
}
|
||||
|
|
|
|||
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 () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"kind": "text",
|
||||
"payload": "`[CML] CLAUDE.md Linter`: 1 finding(s) (0ms)\n `[SET] Settings Validator`: 0 finding(s) (0ms)\n `[HKV] Hook Validator`: 0 finding(s) (0ms)\n `[RUL] Rules Validator`: 0 finding(s) (0ms)\n `[MCP] MCP Config Validator`: 0 finding(s) (0ms)\n `[IMP] Import Resolver`: 0 finding(s) (0ms)\n `[CNF] Conflict Detector`: 0 finding(s) (0ms)\n `[GAP] Feature Gap Scanner`: 17 finding(s) (0ms)\n `[TOK] Token Hotspots`: 1 finding(s) (0ms)\n `[CPS] Cache-Prefix Stability`: 0 finding(s) (0ms)\n `[DIS] Disabled-In-Schema`: 0 finding(s) (0ms)\n `[COL] Plugin Skill Collision`: 0 finding(s) (0ms)\n `[SKL] Skill-Listing Budget`: 0 finding(s) (0ms)\n `[OST] Output-Style Validation`: 0 finding(s) (0ms)\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n Configuration health\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n Health: A (98/100) — Healthy setup, only minor polish needed\n 9 areas reviewed\n\n Area scores\n ───────────\n `CLAUDE.md` ........... A (90) `Settings` ............ A (100)\n `Hooks` ............... A (100) `Rules` ............... A (100)\n `MCP` ................. A (100) `Imports` ............. A (100)\n `Conflicts` ........... A (100) `Token Efficiency` .... A (90)\n `Plugin Hygiene` ...... A (100)\n\n 17 ways you could get more out of Claude Code — see /config-audit feature-gap\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
"payload": "`[CML] CLAUDE.md Linter`: 1 finding(s) (0ms)\n `[SET] Settings Validator`: 0 finding(s) (0ms)\n `[HKV] Hook Validator`: 0 finding(s) (0ms)\n `[RUL] Rules Validator`: 0 finding(s) (0ms)\n `[MCP] MCP Config Validator`: 0 finding(s) (0ms)\n `[IMP] Import Resolver`: 0 finding(s) (0ms)\n `[CNF] Conflict Detector`: 0 finding(s) (0ms)\n `[GAP] Feature Gap Scanner`: 17 finding(s) (0ms)\n `[TOK] Token Hotspots`: 1 finding(s) (0ms)\n `[CPS] Cache-Prefix Stability`: 0 finding(s) (0ms)\n `[DIS] Disabled-In-Schema`: 0 finding(s) (0ms)\n `[COL] Plugin Skill Collision`: 0 finding(s) (0ms)\n `[SKL] Skill-Listing Budget`: 0 finding(s) (0ms)\n `[OST] Output-Style Validation`: 0 finding(s) (0ms)\n `[OPT] Optimization Lens`: 0 finding(s) (0ms)\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n Configuration health\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n Health: A (98/100) — Healthy setup, only minor polish needed\n 9 areas reviewed\n\n Area scores\n ───────────\n `CLAUDE.md` ........... A (90) `Settings` ............ A (100)\n `Hooks` ............... A (100) `Rules` ............... A (100)\n `MCP` ................. A (100) `Imports` ............. A (100)\n `Conflicts` ........... A (100) `Token Efficiency` .... A (90)\n `Plugin Hygiene` ...... A (100)\n\n 17 ways you could get more out of Claude Code — see /config-audit feature-gap\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -582,6 +582,20 @@
|
|||
"low": 0,
|
||||
"info": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"scanner": "OPT",
|
||||
"status": "ok",
|
||||
"files_scanned": 1,
|
||||
"duration_ms": 0,
|
||||
"findings": [],
|
||||
"counts": {
|
||||
"critical": 0,
|
||||
"high": 0,
|
||||
"medium": 0,
|
||||
"low": 0,
|
||||
"info": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"aggregate": {
|
||||
|
|
@ -596,7 +610,7 @@
|
|||
"risk_score": 11,
|
||||
"risk_band": "Medium",
|
||||
"verdict": "PASS",
|
||||
"scanners_ok": 13,
|
||||
"scanners_ok": 14,
|
||||
"scanners_error": 0,
|
||||
"scanners_skipped": 1
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue