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>
This commit is contained in:
parent
c1409ae9b9
commit
7b3b487d26
9 changed files with 687 additions and 13 deletions
114
scanners/lib/lens-prefilter.mjs
Normal file
114
scanners/lib/lens-prefilter.mjs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
160
scanners/optimize-lens-cli.mjs
Normal file
160
scanners/optimize-lens-cli.mjs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* optimize-lens CLI — feeds the v5.7 optimization lens (CA-OPT) `/config-audit
|
||||
* optimize` command. It produces the two halves of the hybrid motor as one JSON
|
||||
* payload:
|
||||
*
|
||||
* 1. `deterministic` — the OPT scanner's high-precision findings (CA-OPT-001:
|
||||
* a long numbered procedure in CLAUDE.md → skill). Already part of the
|
||||
* orchestrated audit; surfaced here so /optimize is a complete view.
|
||||
* 2. `candidates` — recall-oriented prose-judgment candidates from the
|
||||
* lens-prefilter (lifecycle → hook, unscoped path-specific → rule, "never"
|
||||
* → permission), each stamped with the CONFIRMED register entry it might fit
|
||||
* (claim / recommendation / source / severity). The opus
|
||||
* optimization-lens-agent is the precision gate over these.
|
||||
*
|
||||
* Only CONFIRMED register entries are attached (Verifiseringsplikt); a candidate
|
||||
* whose register rule is missing or unconfirmed is dropped, so the agent never
|
||||
* sees an unverifiable recommendation.
|
||||
*
|
||||
* Usage:
|
||||
* node optimize-lens-cli.mjs [path] [--output-file <path>] [--global]
|
||||
*
|
||||
* Exit codes: 0=ok, 3=unrecoverable error. Zero external dependencies.
|
||||
*/
|
||||
|
||||
import { resolve } from 'node:path';
|
||||
import { writeFile, readFile, stat } from 'node:fs/promises';
|
||||
import { discoverConfigFiles } from './lib/file-discovery.mjs';
|
||||
import { resetCounter } from './lib/output.mjs';
|
||||
import { parseFrontmatter } from './lib/yaml-parser.mjs';
|
||||
import { loadRegister, getEntry } from './lib/best-practices-register.mjs';
|
||||
import { prefilterClaudeMd, LENS_DETECTORS } from './lib/lens-prefilter.mjs';
|
||||
import { scan as optScan } from './optimization-lens-scanner.mjs';
|
||||
|
||||
/** Confirmed register entry for `id`, or null. */
|
||||
function confirmedEntry(register, id) {
|
||||
const e = getEntry(register, id);
|
||||
return e && e.confidence === 'confirmed' ? e : null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let targetPath = '.';
|
||||
let outputFile = null;
|
||||
let includeGlobal = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--global') includeGlobal = true;
|
||||
else if (args[i] === '--output-file' && args[i + 1]) outputFile = args[++i];
|
||||
else if (!args[i].startsWith('-')) targetPath = args[i];
|
||||
}
|
||||
|
||||
const absPath = resolve(targetPath);
|
||||
try {
|
||||
const s = await stat(absPath);
|
||||
if (!s.isDirectory()) {
|
||||
process.stderr.write(`Error: ${absPath} is not a directory\n`);
|
||||
process.exit(3);
|
||||
}
|
||||
} catch {
|
||||
process.stderr.write(`Error: path does not exist: ${absPath}\n`);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
// Load the register once; tolerate its absence (deterministic half still runs).
|
||||
let register = null;
|
||||
try {
|
||||
register = loadRegister();
|
||||
} catch {
|
||||
register = null;
|
||||
}
|
||||
|
||||
resetCounter();
|
||||
const discovery = await discoverConfigFiles(absPath, { includeGlobal });
|
||||
|
||||
// ── Deterministic half: the OPT scanner (CA-OPT-001) ──
|
||||
const opt = await optScan(absPath, discovery);
|
||||
|
||||
// ── Recall half: prose-judgment candidates from the pre-filter ──
|
||||
const claudeMdFiles = (discovery.files || []).filter((f) => f.type === 'claude-md');
|
||||
const candidates = [];
|
||||
for (const file of claudeMdFiles) {
|
||||
let content;
|
||||
try {
|
||||
content = await readFile(file.absPath, 'utf-8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseFrontmatter(content);
|
||||
const body = parsed.body || content;
|
||||
const bodyStartLine = parsed.bodyStartLine || 1;
|
||||
for (const cand of prefilterClaudeMd(body)) {
|
||||
const entry = register ? confirmedEntry(register, cand.registerId) : null;
|
||||
if (!entry) continue; // never surface an unverifiable recommendation
|
||||
candidates.push({
|
||||
file: file.relPath || file.absPath,
|
||||
line: bodyStartLine - 1 + cand.line,
|
||||
lensCheck: cand.lensCheck,
|
||||
mechanism: cand.mechanism,
|
||||
signalText: cand.text,
|
||||
register: {
|
||||
id: entry.id,
|
||||
claim: entry.claim,
|
||||
recommendation: entry.recommendation || null,
|
||||
severity: entry.severity || 'low',
|
||||
source: entry.source,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The CONFIRMED prose-judgment entries, so the agent has full provenance even
|
||||
// for a detector class that produced no candidates this run.
|
||||
const registerEntries = register
|
||||
? LENS_DETECTORS.map((d) => confirmedEntry(register, d.registerId))
|
||||
.filter(Boolean)
|
||||
.map((e) => ({
|
||||
id: e.id,
|
||||
lensCheck: e.lensCheck,
|
||||
claim: e.claim,
|
||||
recommendation: e.recommendation || null,
|
||||
mechanism: e.mechanism || null,
|
||||
severity: e.severity || 'low',
|
||||
source: e.source,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const payload = {
|
||||
status: 'ok',
|
||||
target: absPath,
|
||||
deterministic: opt.findings || [],
|
||||
candidates,
|
||||
register: registerEntries,
|
||||
counts: {
|
||||
deterministic: (opt.findings || []).length,
|
||||
candidates: candidates.length,
|
||||
byLensCheck: candidates.reduce((acc, c) => {
|
||||
acc[c.lensCheck] = (acc[c.lensCheck] || 0) + 1;
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
};
|
||||
|
||||
const json = JSON.stringify(payload, null, 2);
|
||||
if (outputFile) {
|
||||
await writeFile(outputFile, json, 'utf-8');
|
||||
}
|
||||
if (!outputFile) {
|
||||
process.stdout.write(json + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
||||
if (isDirectRun) {
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`Fatal: ${err.message}\n`);
|
||||
process.exit(3);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue