config-audit/scanners/lib/lens-prefilter.mjs
Kjell Tore Guttormsen 7b3b487d26 feat(opt): optimization lens Chunk 2b — opus prose-judgment analyzer + /config-audit optimize
The recall+precision halves of the CA-OPT hybrid motor for the three
mechanism-fit cases the deterministic OPT scanner (2a) deliberately skips:
lifecycle→hook (BP-MECH-001), unscoped path-specific→rule (BP-MECH-002),
absolute "never"→permission (BP-MECH-004).

New:
- scanners/lib/lens-prefilter.mjs — cheap, recall-oriented line scan of the
  CLAUDE.md body; detector names mirror the register lensCheck fields; skips
  fenced code, gates the path class on an instruction verb. Pure + 13 tests.
- scanners/optimize-lens-cli.mjs — discovery + OPT scanner + pre-filter; attaches
  only the CONFIRMED register entry to each candidate (unverifiable → dropped,
  Verifiseringsplikt); emits {deterministic, candidates, register, counts}.
- agents/optimization-lens-agent.md — opus precision gate (7th agent, orange):
  reads the real CLAUDE.md, drops low-confidence candidates, keeps only genuine
  opportunities, cites register id + source.
- commands/optimize.md — /config-audit optimize orchestrates pre-filter→agent→report.

Agent-driven → deliberately NOT byte-stable (own command, outside the snapshot
suite). No new orchestrated scanner → scanner count stays 15. Counts: agents
6→7, commands 18→19, suite 1055→1068. Self-audit A/A unchanged, readmeCheck
passed (clean HOME). Plan: docs/v5.7-optimization-lens-plan.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:01:18 +02:00

114 lines
4.7 KiB
JavaScript

/**
* lens-prefilter — deterministic, recall-oriented candidate generator for the
* v5.7 optimization lens (CA-OPT) hybrid motor, Chunk 2b.
*
* The OPT *scanner* (Chunk 2a) handles the one mechanism-fit case it can decide
* deterministically with high precision (a long numbered procedure → skill). The
* other three cases in the register are PROSE-JUDGMENT calls — whether a line is
* really lifecycle automation, a path-specific constraint, or an absolute
* prohibition depends on reading intent, which a regex cannot settle. So the
* hybrid motor splits the work:
*
* pre-filter (this module, CHEAP, recall-oriented)
* → surfaces candidate lines tagged with the register rule they might fit
* opus optimization-lens-agent (PRECISION gate)
* → reads each candidate in context, keeps only genuine mechanism-fit
* opportunities, cites the register rule + source
*
* Therefore this pre-filter deliberately errs toward recall: a false candidate
* costs the agent a moment's judgement, a missed line is never recoverable. It
* does, however, avoid the two obvious noise sources — fenced code blocks and
* (when the caller passes the parsed body) YAML frontmatter — and it requires an
* imperative-looking line for the path-specific class so plain "see docs/x.md"
* references don't flood the candidate list.
*
* The detector names mirror the `lensCheck` fields of the register entries
* (knowledge/best-practices.json), so the agent can map each candidate straight
* back to its provenance.
*
* Zero external dependencies. Pure: input text → candidate array.
*/
/**
* The three prose-judgment detectors, keyed to their register entries.
* `mechanism` is the better-fit mechanism the register recommends.
*/
export const LENS_DETECTORS = Object.freeze([
{ lensCheck: 'claude-md-lifecycle-phrasing', registerId: 'BP-MECH-001', mechanism: 'hook' },
{ lensCheck: 'unscoped-path-specific-instruction', registerId: 'BP-MECH-002', mechanism: 'rule' },
{ lensCheck: 'never-instruction', registerId: 'BP-MECH-004', mechanism: 'permission' },
]);
// Lifecycle automation phrased as an instruction: "after every commit", "before
// each push", "every time you …", "whenever you …", "always run". Recall-first.
const LIFECYCLE_RE =
/\b(?:after (?:every|each)|before (?:every|each)|on (?:every|each)|every time|each time|always run|whenever)\b/i;
// Absolute prohibition: a standalone "never" followed by an action word. Kept
// permissive (recall); the agent decides whether it is a real hard rule.
const NEVER_RE = /\bnever\s+[a-z]/i;
// A concrete path / glob / known-extension filename anywhere in the line.
const PATH_RE =
/(?:(?:\.{0,2}\/)?[\w.-]+\/[\w.*/-]+|\*\*?\/[\w.*-]+|\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|md|json|ya?ml|toml|go|rs|java|rb|php|c|cpp|h|hpp|sh|sql|css|scss|html|env)\b)/;
// An imperative / modal verb that marks a line as an instruction rather than a
// bare cross-reference. Gates the path-specific class to cut "see foo/bar.md".
const INSTRUCTION_RE =
/\b(?:use|edit|run|always|must|should|put|place|write|add|modify|update|format|lint|test|name|store|keep|never|generate|build|deploy|commit)\b/i;
const getDetector = (lensCheck) => LENS_DETECTORS.find((d) => d.lensCheck === lensCheck);
function candidate(lensCheck, lineNo, lineText) {
const d = getDetector(lensCheck);
return {
lensCheck,
registerId: d.registerId,
mechanism: d.mechanism,
line: lineNo,
text: lineText.trim(),
};
}
/**
* Scan CLAUDE.md text for prose-judgment mechanism-fit candidates.
*
* Pass the file body (frontmatter stripped) for clean line numbers; the caller
* is then responsible for offsetting `line` by the body's start line. Raw text
* also works — fenced code is skipped either way.
*
* @param {string} text
* @returns {Array<{lensCheck:string, registerId:string, mechanism:string, line:number, text:string}>}
*/
export function prefilterClaudeMd(text) {
const lines = String(text == null ? '' : text).split('\n');
const out = [];
let inFence = false;
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const lineNo = i + 1;
// Toggle fenced code blocks (``` or ~~~). Fence lines themselves are skipped.
if (/^\s*(?:```|~~~)/.test(raw)) {
inFence = !inFence;
continue;
}
if (inFence) continue;
const trimmed = raw.trim();
if (trimmed === '') continue;
if (LIFECYCLE_RE.test(raw)) {
out.push(candidate('claude-md-lifecycle-phrasing', lineNo, raw));
}
if (NEVER_RE.test(raw)) {
out.push(candidate('never-instruction', lineNo, raw));
}
if (PATH_RE.test(raw) && INSTRUCTION_RE.test(raw)) {
out.push(candidate('unscoped-path-specific-instruction', lineNo, raw));
}
}
return out;
}