fix(acr): CPS ignores fenced/inline code + CC-stable path vars (M-BUG-7)
CPS flagged ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} (CC-provided stable
paths) and {date}/timestamp tokens shown in documentation as cache-busters.
Fix: skip fenced code blocks, strip inline-code spans, and whitelist CC-stable
vars before pattern-matching. Suppress-only — frozen v5.0.0 snapshots untouched
(CPS yields findings:[] there), no re-seed. Suite 1316/0 (+6). Dogfood ~/.claude
5->2 (3 doc false-positives suppressed; 2 remaining = own volatile test fixtures).
This commit is contained in:
parent
bfd577aeee
commit
dd9db60fc9
8 changed files with 403 additions and 11 deletions
|
|
@ -30,7 +30,28 @@ const SCANNER = 'CPS';
|
|||
// hits per turn, not to chase every inline date in a long backlog file.
|
||||
const CACHED_PREFIX_LINES = 150;
|
||||
|
||||
// Volatile-pattern set (extends token-hotspots.mjs Pattern A).
|
||||
// CC-provided substitution variables that resolve to a stable per-install or
|
||||
// per-project path (e.g. "${CLAUDE_PLUGIN_ROOT}/hooks/x.mjs"). CC expands them
|
||||
// to the same value every turn, so they never break the prompt cache — unlike a
|
||||
// runtime ${TIMESTAMP}. Excluded from the ${VAR} volatile flag (M-BUG-7).
|
||||
const STABLE_CC_VARS = new Set(['CLAUDE_PLUGIN_ROOT', 'CLAUDE_PROJECT_DIR']);
|
||||
|
||||
// Matches every ${VAR} occurrence on a line so a line carrying only stable CC
|
||||
// vars is not mistaken for a runtime cache-buster.
|
||||
const VAR_RX = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
||||
|
||||
/** True when a line contains at least one non-CC-stable ${VAR} substitution. */
|
||||
function hasVolatileVar(line) {
|
||||
VAR_RX.lastIndex = 0;
|
||||
let m;
|
||||
while ((m = VAR_RX.exec(line)) !== null) {
|
||||
if (!STABLE_CC_VARS.has(m[1])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Volatile-pattern set (extends token-hotspots.mjs Pattern A). The ${VAR} entry
|
||||
// is `varAware` — flagged via hasVolatileVar() so CC-stable vars are excluded.
|
||||
const VOLATILE_PATTERNS = [
|
||||
{ rx: /\{timestamp\}/i, label: '{timestamp} placeholder' },
|
||||
{ rx: /\{uuid\}/i, label: '{uuid} placeholder' },
|
||||
|
|
@ -41,7 +62,7 @@ const VOLATILE_PATTERNS = [
|
|||
{ rx: /^\s*\[\d{4}-\d{2}-\d{2}/, label: 'dated log line [YYYY-MM-DD ...]' },
|
||||
// v5 N3 extensions:
|
||||
{ rx: /^\s*!/, label: 'shell-exec line (! prefix)' },
|
||||
{ rx: /\$\{[A-Z_][A-Z0-9_]*\}/, label: '${VAR} substitution' },
|
||||
{ rx: /\$\{[A-Z_][A-Z0-9_]*\}/, label: '${VAR} substitution', varAware: true },
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -68,16 +89,33 @@ function findVolatileLines(content) {
|
|||
const out = [];
|
||||
if (!content) return out;
|
||||
const lines = content.split('\n').slice(0, CACHED_PREFIX_LINES);
|
||||
let inFence = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
for (const { rx, label } of VOLATILE_PATTERNS) {
|
||||
if (rx.test(lines[i])) {
|
||||
out.push({
|
||||
line: i + 1,
|
||||
label,
|
||||
snippet: lines[i].length > 120 ? lines[i].slice(0, 117) + '...' : lines[i],
|
||||
});
|
||||
break;
|
||||
}
|
||||
const line = lines[i];
|
||||
// Fenced code blocks (``` or ~~~) hold illustrative, byte-stable literal
|
||||
// text — a ${VAR} or timestamp shown inside one is documentation, not a
|
||||
// runtime cache-buster — so the fence delimiters and their content are
|
||||
// skipped (M-BUG-7).
|
||||
if (/^\s*(```|~~~)/.test(line)) {
|
||||
inFence = !inFence;
|
||||
continue;
|
||||
}
|
||||
if (inFence) continue;
|
||||
// Strip `inline code` spans before pattern-testing: a {date} or ${VAR}
|
||||
// shown inside backticks is literal documentation text, byte-stable, not a
|
||||
// runtime cache-buster (M-BUG-7). The original line is still reported as the
|
||||
// snippet so context is preserved.
|
||||
const probe = line.replace(/`[^`]*`/g, '');
|
||||
for (const { rx, label, varAware } of VOLATILE_PATTERNS) {
|
||||
// The ${VAR} pattern flags only non-CC-stable substitutions; every other
|
||||
// pattern keeps its plain line test.
|
||||
if (varAware ? !hasVolatileVar(probe) : !rx.test(probe)) continue;
|
||||
out.push({
|
||||
line: i + 1,
|
||||
label,
|
||||
snippet: line.length > 120 ? line.slice(0, 117) + '...' : line,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue