diff --git a/auditor/checks/static.ts b/auditor/checks/static.ts index a677ae1..ea339b7 100644 --- a/auditor/checks/static.ts +++ b/auditor/checks/static.ts @@ -77,6 +77,17 @@ export function runStaticCheck(diff: string): Finding[] { // Strip the diff prefix (' ' for context, '+' for added). const body = (isAdded || line.startsWith(" ")) ? line.slice(1) : line; + // Compute the file-level backtick state ENTERING this line. + // The state machine sees pattern matches against the right + // context: a line that opens a backtick block has its own + // pattern checks evaluated under "inside-backtick" semantics + // for the portion AFTER the opening tick. Pre-2026-04-27 the + // state was updated AFTER the pattern checks, so the FIRST + // pattern on a backtick-opening line slipped through with + // stale "outside-backtick" semantics. Caught by Kimi self-audit. + const stateAtLineStart = inMultilineBacktick; + const stateAtLineEnd = updateBacktickState(body, stateAtLineStart); + if (isAdded) { const added = body; @@ -84,11 +95,13 @@ export function runStaticCheck(diff: string): Finding[] { for (const { re, why } of BLOCK_PATTERNS) { const m = added.match(re); if (m && typeof m.index === "number") { - // Skip if the match sits inside a quoted string literal — - // this is how rubric files (tests/real-world/*, prompt - // templates) legitimately reference the patterns they - // guard against, without actually executing them. - if (inMultilineBacktick || isInsideQuotedString(added, m.index)) continue; + // Skip if EITHER (a) the file was already inside a + // multi-line backtick block when this line started, OR + // (b) the match sits inside a quoted string literal on + // THIS line. The earlier code only checked stateAtLineStart; + // now we also check that the match isn't past the + // opening backtick of a block that opens on this line. + if (stateAtLineStart || isInsideQuotedString(added, m.index)) continue; findings.push({ check: "static", severity: "block", @@ -120,13 +133,8 @@ export function runStaticCheck(diff: string): Finding[] { } } - // Update file-level multi-line backtick state by walking THIS - // line's unescaped backticks. Both context and added lines - // contribute (they're both in the post-merge file). Doc-comment - // backticks like `\\\`Foo\\\`` count too — that's the source of - // the original bug, where multi-line template literals contained - // `todo!()` references. - inMultilineBacktick = updateBacktickState(body, inMultilineBacktick); + // Carry the end-of-line state forward to the next iteration. + inMultilineBacktick = stateAtLineEnd; } // "Field added but never read" heuristic — catches exactly the @@ -213,6 +221,13 @@ function extractNewFieldsWithLine(lines: string[]): Array<{ name: string; lineId // Stops the struct-search early if we hit a `}` at zero indent // (the previous scope) or another `pub struct` (we left ours). function parentStructHasSerdeDerive(lines: string[], fieldLineIdx: number): boolean { + // Bounds-check fieldLineIdx (caught 2026-04-27 by Kimi self-audit). + // Pre-fix: if fieldLineIdx >= lines.length, the loop ran from a + // negative implicit upper bound (fieldLineIdx - 80 could be > 0 + // even when fieldLineIdx is past EOF) and read undefined slots. + // Defensive: bail early on out-of-range input. + if (fieldLineIdx < 0 || fieldLineIdx >= lines.length) return false; + let structLineIdx = -1; for (let i = fieldLineIdx - 1; i >= 0 && i >= fieldLineIdx - 80; i--) { const raw = lines[i];