feat(config-audit): cache-prefix stability scanner CPS (v5 N3) [skip-docs]
New CPS scanner walks CLAUDE.md cascade and flags volatile content
between lines 31 and 150 — the cache-prefix window beyond TOK Pattern
A's top-30 territory. Volatile content anywhere in the cached prefix
forces a fresh cache write from that line down on every turn.
Volatile-pattern set extends TOK Pattern A with:
- shell-exec lines (! prefix) — common in CLAUDE.md to inject git/date
- ${VAR} substitutions — vary per-shell, defeat cache reuse
Severity: medium per finding. Skips lines 1-30 to avoid duplicating
Pattern A's range; CPS' value is in the 31-150 zone.
Wired into scan-orchestrator + scoring SCANNER_AREA_MAP. CPS shares
the "Token Efficiency" area with TOK; scoreByArea now deduplicates by
area name and combines counts across scanners contributing to the
same area, so the 9-area scorecard contract holds.
Fixtures volatile-mid-section/{volatile-line-60, volatile-line-200}
verify both positive (line 60) and out-of-window (line 200) cases.
[skip-docs] reason: v5 plan fences off README/CLAUDE.md badge updates
to Session 5; Forgejo pre-commit-docs-gate hook requires this tag.
Tests: 604 → 611 (+7).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0420b8cc4a
commit
65087e624f
6 changed files with 517 additions and 9 deletions
|
|
@ -151,6 +151,7 @@ const SCANNER_AREA_MAP = {
|
|||
CNF: 'Conflicts',
|
||||
GAP: 'Feature Coverage',
|
||||
TOK: 'Token Efficiency',
|
||||
CPS: 'Token Efficiency',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -181,21 +182,35 @@ function severityPenalty(counts) {
|
|||
* @returns {{ areas: Array<{ id: string, name: string, grade: string, score: number, findingCount: number }>, overallGrade: string, scoringVersion: string }}
|
||||
*/
|
||||
export function scoreByArea(scannerResults) {
|
||||
const areas = [];
|
||||
|
||||
// Group scanner results by area name so multiple scanners that share an area
|
||||
// (e.g. TOK + CPS both → "Token Efficiency") produce one combined row.
|
||||
const grouped = new Map();
|
||||
for (const result of scannerResults) {
|
||||
const name = SCANNER_AREA_MAP[result.scanner] || result.scanner;
|
||||
const findingCount = result.findings.length;
|
||||
if (!grouped.has(name)) grouped.set(name, []);
|
||||
grouped.get(name).push(result);
|
||||
}
|
||||
|
||||
const areas = [];
|
||||
|
||||
for (const [name, results] of grouped) {
|
||||
const findings = results.flatMap(r => r.findings || []);
|
||||
const findingCount = findings.length;
|
||||
|
||||
let score;
|
||||
if (result.scanner === 'GAP') {
|
||||
const util = calculateUtilization(result.findings);
|
||||
if (results.some(r => r.scanner === 'GAP')) {
|
||||
// GAP scoring uses utilization, not severity penalty
|
||||
const util = calculateUtilization(findings);
|
||||
score = util.score;
|
||||
} else {
|
||||
// v5 severity-weighted: penalty proportional to a per-scanner budget.
|
||||
// maxBudget = max(10, findingCount * 4) — adding more lows doesn't crater the
|
||||
// grade, but a single high-severity finding consumes a large fraction of budget.
|
||||
const counts = result.counts || {};
|
||||
// v5 severity-weighted: penalty proportional to a per-area budget.
|
||||
// Combine counts across all scanners contributing to this area.
|
||||
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
||||
for (const r of results) {
|
||||
for (const k of Object.keys(counts)) {
|
||||
counts[k] += (r.counts && r.counts[k]) || 0;
|
||||
}
|
||||
}
|
||||
const penalty = severityPenalty(counts);
|
||||
const maxBudget = Math.max(10, findingCount * 4);
|
||||
const passRate = Math.max(0, 100 - (penalty / maxBudget) * 100);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue