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>
146 lines
5.3 KiB
JavaScript
146 lines
5.3 KiB
JavaScript
/**
|
|
* OPT Scanner — Optimization Lens / mechanism-fit (v5.7 Fase 1 Chunk 2a)
|
|
*
|
|
* The first detector of the "is the config OPTIMAL?" axis (vs. the existing
|
|
* "is it CORRECT?" scanners). It reads the machine-readable best-practices
|
|
* register (knowledge/best-practices.json) and flags config that works but uses
|
|
* a mechanism a better one would fit — the deterministic half of the hybrid
|
|
* motor (the opus analyzer for prose-judgment cases is Chunk 2b).
|
|
*
|
|
* CA-OPT-001 A multi-step procedure in CLAUDE.md should be a SKILL (BP-MECH-003).
|
|
* CLAUDE.md is for facts Claude holds every turn; a procedure there
|
|
* costs always-loaded tokens whether or not you run it, and a skill's
|
|
* body loads only on invoke. Detection is deliberately CONSERVATIVE
|
|
* (a run of >= 6 consecutive numbered steps) to keep precision high —
|
|
* the negative corpus in the tests proves null false-positives.
|
|
* Framed as a Missed opportunity (humanizer), severity LOW.
|
|
*
|
|
* Provenance: the recommendation + claim come from the register entry (only a
|
|
* `confirmed` entry is used user-facing — Verifiseringsplikt); an inline default
|
|
* is the graceful fallback if the register is unavailable. Fixture-gated: the
|
|
* marketplace-medium CLAUDE.md has no numbered lists, so it emits nothing (SC-5
|
|
* byte-stable; the additive OPT scanner entry is stripped from frozen baselines).
|
|
*
|
|
* Zero external dependencies.
|
|
*/
|
|
|
|
import { readFile } from 'node:fs/promises';
|
|
import { finding, scannerResult } from './lib/output.mjs';
|
|
import { SEVERITY } from './lib/severity.mjs';
|
|
import { parseFrontmatter } from './lib/yaml-parser.mjs';
|
|
import { loadRegister, getEntry } from './lib/best-practices-register.mjs';
|
|
|
|
const SCANNER = 'OPT';
|
|
const STEP_THRESHOLD = 6;
|
|
const STEP_RE = /^\s*\d+\.\s+\S/;
|
|
const PROCEDURE_TITLE = 'A multi-step procedure in CLAUDE.md belongs in a skill';
|
|
|
|
// Graceful fallback if the register is missing/unreadable (the register is the
|
|
// source of truth; this keeps the scanner working without it).
|
|
const DEFAULT_MECH_003 = {
|
|
claim:
|
|
'A multi-step procedure in CLAUDE.md should be a skill — CLAUDE.md is for facts Claude ' +
|
|
'should hold all the time; procedures belong in skills.',
|
|
recommendation:
|
|
'Extract the procedure into .claude/skills/; its body then loads only on invoke instead ' +
|
|
'of every turn.',
|
|
};
|
|
|
|
/** Return the confirmed register entry for `id`, or null (→ caller uses default). */
|
|
function confirmedEntry(id) {
|
|
try {
|
|
const e = getEntry(loadRegister(), id);
|
|
return e && e.confidence === 'confirmed' ? e : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Longest run of consecutive numbered-list items. Blank lines and indented
|
|
* continuation lines neither extend nor break a run; any other non-step line
|
|
* breaks it. Conservative by design (a wrapped, non-indented step line ends the
|
|
* run → undercount, never overcount).
|
|
* @param {string} text
|
|
* @returns {{count:number, startIndex:number, firstStep:string}}
|
|
*/
|
|
function longestNumberedRun(text) {
|
|
const lines = String(text).split('\n');
|
|
let maxRun = 0;
|
|
let maxStartIdx = 0;
|
|
let maxFirstStep = '';
|
|
let run = 0;
|
|
let runStartIdx = 0;
|
|
let runFirstStep = '';
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (STEP_RE.test(line)) {
|
|
if (run === 0) {
|
|
runStartIdx = i;
|
|
runFirstStep = line.trim();
|
|
}
|
|
run++;
|
|
if (run > maxRun) {
|
|
maxRun = run;
|
|
maxStartIdx = runStartIdx;
|
|
maxFirstStep = runFirstStep;
|
|
}
|
|
} else if (line.trim() === '' || /^\s+\S/.test(line)) {
|
|
// blank or indented continuation — part of the list, neither step nor break
|
|
} else {
|
|
run = 0;
|
|
}
|
|
}
|
|
return { count: maxRun, startIndex: maxStartIdx, firstStep: maxFirstStep };
|
|
}
|
|
|
|
export async function scan(targetPath, discovery) {
|
|
const start = Date.now();
|
|
const findings = [];
|
|
const claudeMdFiles = ((discovery && discovery.files) || []).filter((f) => f.type === 'claude-md');
|
|
|
|
const entry = confirmedEntry('BP-MECH-003');
|
|
const claim = (entry && entry.claim) || DEFAULT_MECH_003.claim;
|
|
const recommendation = (entry && entry.recommendation) || DEFAULT_MECH_003.recommendation;
|
|
|
|
let filesScanned = 0;
|
|
for (const file of claudeMdFiles) {
|
|
let content;
|
|
try {
|
|
content = await readFile(file.absPath, 'utf-8');
|
|
} catch {
|
|
continue;
|
|
}
|
|
filesScanned++;
|
|
|
|
const parsed = parseFrontmatter(content);
|
|
const body = parsed.body || content;
|
|
const bodyStartLine = parsed.bodyStartLine || 1;
|
|
const run = longestNumberedRun(body);
|
|
|
|
if (run.count >= STEP_THRESHOLD) {
|
|
findings.push(
|
|
finding({
|
|
scanner: SCANNER,
|
|
severity: SEVERITY.low,
|
|
title: PROCEDURE_TITLE,
|
|
description: claim,
|
|
file: file.relPath || file.absPath,
|
|
line: bodyStartLine + run.startIndex,
|
|
evidence: run.firstStep,
|
|
recommendation,
|
|
category: 'mechanism-fit',
|
|
details: {
|
|
mechanism: 'skill',
|
|
steps: run.count,
|
|
register: entry ? entry.id : null,
|
|
source: entry ? entry.source.url : null,
|
|
confidence: entry ? entry.confidence : null,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
return scannerResult(SCANNER, 'ok', findings, filesScanned, Date.now() - start);
|
|
}
|