/** * 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; }