Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/api/audit/full.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1797,7 +1797,7 @@ pub async fn full_audit(
crate::db::audit_runs::update_td_counts(
&tx, &run_id,
counts.critical, counts.high, counts.medium, counts.low,
carried,
resolved, new, carried,
)?;
}
tx.commit()?;
Expand Down
67 changes: 66 additions & 1 deletion backend/src/api/audit/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,25 @@ pub fn check_detector_disposition(
/// Liquid / Mermaid syntax that uses `{{` for its own purposes (e.g.
/// `{{ asset('foo') }}` in a Twig snippet inside coding-rules.md, or
/// `{{ DECISION_1 }}` is matched, but `{{ asset(...) }}` is not).
/// Fenced blocks and inline code spans are invisible to the counter —
/// documenting a placeholder is not the same as leaving one unfilled
/// (benchmark run 1 false positive: the TD index mentioning
/// `{{DO_NOT_1}}` in inline code made a 43-TD file read as "still the
/// template"). Reuses the anti-hallu strippers: inline spans pair
/// N-backtick runs (an UNCLOSED run keeps its text — a stray backtick can
/// never hide a real slot, bounded pairing on hostile lines); fences are
/// lexical, and an UNCLOSED fence restores everything it withheld,
/// opening line included.
pub(crate) fn count_raw_placeholders(content: &str) -> usize {
let stripped = crate::core::anti_halluc::strip_inline_code(
&crate::core::anti_halluc::strip_fenced_code(content),
);
// Match `{{IDENT}}` and `{{ IDENT }}` where IDENT is
// UPPERCASE_SNAKE (with optional digits + _). The trailing
// boundary is a literal `}}`, not just `}`, to avoid hits on
// Twig double-brace blocks that contain spaces / parens.
let mut count = 0usize;
let mut rest = content;
let mut rest = stripped.as_str();
while let Some(start) = rest.find("{{") {
let after_open = &rest[start + 2..];
// Find closing `}}`
Expand Down Expand Up @@ -786,6 +798,59 @@ mod tests {
assert_eq!(count_raw_placeholders(body), 2);
}

#[test]
fn count_raw_placeholders_ignores_code_spans_and_fences() {
// Benchmark run 1 (2026-07-21) false positive: the TD index
// DOCUMENTED `{{DO_NOT_1}}`/`{{DO_NOT_2}}` in inline code while
// describing a redirector stub — a 43-TD file read as "still the
// template" and failed step 8. Documenting a placeholder in code
// is content, not an unfilled slot.
assert_eq!(count_raw_placeholders("mentions `{{DO_NOT_1}}` in prose"), 0);
assert_eq!(
count_raw_placeholders("it carries `{{DO_NOT_1}}`/`{{DO_NOT_2}}` placeholders"),
0
);
assert_eq!(
count_raw_placeholders("```\n| {{ID}} |\n```\nafter the fence"),
0
);
// CommonMark double-backtick spans strip too (a naive char toggle
// would leave the inner token counted).
assert_eq!(count_raw_placeholders("``{{ID}}``"), 0);
// An UNCLOSED fence (truncated markdown) must never hide a real
// slot sitting after it — fail-closed, like the stray backtick.
assert_eq!(
count_raw_placeholders("```text\nexemple tronqué\n| {{ID}} |"),
1
);
// The OPENING line of an unclosed fence must not swallow a slot
// sitting on it either.
assert_eq!(count_raw_placeholders("```text {{ID}}\ntruncated"), 1);
// A ```suffix line is NOT a valid closer: no closer before EOF ⇒
// everything restored ⇒ the slot stays visible.
assert_eq!(
count_raw_placeholders("```text\n| {{ID}} |\n```still-code"),
1
);
// A backtick inside the info string is NOT a valid opener — the
// slot after it stays visible.
assert_eq!(
count_raw_placeholders("```lang`oops\n| {{ID}} |\n```"),
1
);
// A REAL unfilled slot outside code still counts — including next
// to documented ones.
assert_eq!(count_raw_placeholders("| {{ID}} |"), 1);
assert_eq!(
count_raw_placeholders("mentions `{{DOC}}` but leaves | {{ID}} | unfilled"),
1
);
// An UNCLOSED backtick run keeps its text: a stray backtick (typo)
// must never hide a real unfilled slot behind it.
assert_eq!(count_raw_placeholders("stray ` then | {{ID}} |"), 1);
assert_eq!(count_raw_placeholders("| {{ID}} | then stray `"), 1);
}

#[test]
fn normalized_hash_ignores_cosmetic_touches_only() {
// Matrix v2 — a cosmetic touch (CRLF, trailing ws) must not read as
Expand Down
132 changes: 122 additions & 10 deletions backend/src/core/anti_halluc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,19 +628,73 @@ fn is_heading_or_imperative(s: &str) -> bool {

/// Strip fenced ```code blocks``` — agents emit lots of code and linting it
/// would be pure noise. Returns the text with fenced regions removed.
fn strip_fenced_code(text: &str) -> String {
///
/// Fail-closed semantics (benchmark hotfix, 2026-07-21):
/// - a fence OPENS on a run of ≥3 backticks indented ≤3 spaces (info
/// string allowed); anything more indented or shorter is content;
/// - it CLOSES only on a run of the same character, at least as long,
/// indented ≤3 spaces, with a whitespace-only suffix — a ```suffix line
/// is CONTENT, never a closer;
/// - everything withheld (opening line included) is RESTORED verbatim if
/// the document ends before a valid closer: a truncated markdown must
/// never hide real content from a fail-closed consumer (the step-8
/// placeholder validator feeds on this).
pub(crate) fn strip_fenced_code(text: &str) -> String {
/// `Some((run_len, suffix))` when the line is a fence marker candidate:
/// ≤3 leading spaces then a run of ≥3 backticks. Tabs disqualify.
fn fence_marker(line: &str) -> Option<(usize, &str)> {
let stripped = line.trim_start_matches(' ');
if line.len() - stripped.len() > 3 {
return None; // 4+ spaces = indented code, not a fence
}
let run = stripped.chars().take_while(|&c| c == '`').count();
if run < 3 {
return None;
}
Some((run, &stripped[run..]))
}

let mut out = String::with_capacity(text.len());
let mut in_fence = false;
// (opening run length, withheld lines — restored verbatim on EOF)
let mut fence: Option<(usize, String)> = None;
for line in text.lines() {
if line.trim_start().starts_with("```") {
in_fence = !in_fence;
continue;
}
if !in_fence {
out.push_str(line);
out.push('\n');
match fence.as_mut() {
None => {
// A backtick fence's info string may not contain a backtick
// (CommonMark): ```lang`oops is CONTENT, not an opener — a
// false opener must never swallow a real slot.
match fence_marker(line) {
Some((run, info)) if !info.contains('`') => {
let mut buf = String::new();
buf.push_str(line);
buf.push('\n');
fence = Some((run, buf));
}
_ => {
out.push_str(line);
out.push('\n');
}
}
}
Some((open_run, buf)) => match fence_marker(line) {
// Closer suffix: spaces/tabs ONLY — Unicode whitespace must
// not promote a content line into a closer.
Some((run, suffix))
if run >= *open_run
&& suffix.chars().all(|c| c == ' ' || c == '\t') =>
{
fence = None; // valid closer: the withheld block is code — drop it
}
_ => {
buf.push_str(line);
buf.push('\n');
}
},
}
}
if let Some((_, buf)) = fence {
out.push_str(&buf);
}
out
}

Expand All @@ -657,7 +711,7 @@ const MAX_BACKTICK_RUNS_PER_LINE: usize = 64;
/// kept verbatim — backticks in plain prose never eat the rest of the line.
/// Runs are pre-scanned once per line and their count is capped, so a long
/// hostile line can't turn the pairing into a CPU hotspot (Copilot review).
fn strip_inline_code(text: &str) -> String {
pub(crate) fn strip_inline_code(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for line in text.lines() {
// Fast path — the overwhelmingly common case.
Expand Down Expand Up @@ -1585,6 +1639,64 @@ pub fn finalize_lint_report(
mod tests {
use super::*;

#[test]
fn strip_fenced_code_drops_closed_blocks_but_restores_unclosed_ones() {
// Closed fence: content is code, dropped.
assert_eq!(
strip_fenced_code("before\n```\ncode\n```\nafter"),
"before\nafter\n"
);
// UNCLOSED fence (truncated document): the withheld content is
// restored — a fail-closed consumer (step-8 placeholder validator)
// must never lose sight of real content behind a stray ```.
assert_eq!(
strip_fenced_code("```text\ntruncated example\n| {{ID}} |"),
"```text\ntruncated example\n| {{ID}} |\n"
);
// Two blocks, second unclosed: first dropped, second restored
// (including ITS opening line).
assert_eq!(
strip_fenced_code("```\na\n```\nkeep\n```\nb"),
"keep\n```\nb\n"
);
// The opening line itself is withheld-then-restored: an info-string
// carrying real content must survive an unclosed fence.
assert_eq!(
strip_fenced_code("```text {{ID}}\ntruncated"),
"```text {{ID}}\ntruncated\n"
);
// A ```suffix line is CONTENT, not a closer — without a valid
// closer the whole block is restored.
assert_eq!(
strip_fenced_code("```text\n| {{ID}} |\n```still-code"),
"```text\n| {{ID}} |\n```still-code\n"
);
// A closer run SHORTER than the opening run does not close.
assert_eq!(
strip_fenced_code("````\n| {{ID}} |\n```\ntail"),
"````\n| {{ID}} |\n```\ntail\n"
);
// 4-space indentation = indented code, not a fence at all.
assert_eq!(
strip_fenced_code(" ```\n| {{ID}} |"),
" ```\n| {{ID}} |\n"
);
// Trailing whitespace on a closer is fine (CommonMark).
assert_eq!(strip_fenced_code("```\ncode\n``` "), "");
// A backtick in the info string invalidates the OPENER: the line is
// content and nothing after it is withheld.
assert_eq!(
strip_fenced_code("```lang`oops\n| {{ID}} |\n```"),
"```lang`oops\n| {{ID}} |\n```\n"
);
// Unicode whitespace in a closer suffix does NOT close (spaces and
// tabs only) — the block stays unclosed and is restored.
assert_eq!(
strip_fenced_code("```\n| {{ID}} |\n```\u{00A0}"),
"```\n| {{ID}} |\n```\u{00A0}\n"
);
}

// ── Mode ──────────────────────────────────────────────────────────

#[test]
Expand Down
38 changes: 32 additions & 6 deletions backend/src/db/audit_runs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,39 @@ pub fn update_last_completed_step(conn: &Connection, id: &str, step: u32) -> Res
/// Stamp the TD counters computed at end-of-stream onto a row regardless of
/// its terminal status. Interrupted runs used to keep td_* at 0 while 14 TD
/// files sat on disk — the history chip strip under-reported real findings.
#[allow(clippy::too_many_arguments)]
pub fn update_td_counts(
conn: &Connection,
id: &str,
td_critical: u32,
td_high: u32,
td_medium: u32,
td_low: u32,
td_resolved_since_last: u32,
td_new_since_last: u32,
td_carried_over: u32,
) -> Result<()> {
// resolved/new are persisted too (benchmark run 1, 2026-07-21): the
// Interrupted path computed them but left the row at its 0 defaults,
// and the UI presented "0 new" as a KNOWN value while a genuinely new
// TD existed on disk.
conn.execute(
"UPDATE audit_runs SET
td_critical = ?2, td_high = ?3, td_medium = ?4, td_low = ?5,
td_total = ?6, td_carried_over = ?7
td_total = ?6, td_resolved_since_last = ?7,
td_new_since_last = ?8, td_carried_over = ?9
WHERE id = ?1",
params![id, td_critical, td_high, td_medium, td_low,
td_critical + td_high + td_medium + td_low, td_carried_over],
params![
id,
td_critical,
td_high,
td_medium,
td_low,
td_critical + td_high + td_medium + td_low,
td_resolved_since_last,
td_new_since_last,
td_carried_over
],
)?;
Ok(())
}
Expand Down Expand Up @@ -752,17 +769,26 @@ mod tests {
#[test]
fn update_td_counts_stamps_interrupted_runs() {
// The TD files exist on disk even when the run ends Interrupted —
// the history row must not report 0 findings.
// the history row must not report 0 findings. Benchmark run 1
// (2026-07-21): resolved/new were computed but never persisted on
// this path, so a genuinely new TD read as "0 new" — the exact
// reconciliation partition (13 carried + 1 new, 2 resolved) must
// land in the row.
let conn = fresh_conn();
let start = Utc::now();
insert_running(&conn, "r1", "p1", "Full", "ClaudeCode", start).unwrap();
mark_interrupted(&conn, "r1", "warned steps: [1]").unwrap();
update_td_counts(&conn, "r1", 0, 2, 8, 4, 14).unwrap();
update_td_counts(&conn, "r1", 0, 2, 8, 4, 2, 1, 13).unwrap();
let row = &list_recent(&conn, "p1", 1).unwrap()[0];
assert_eq!(row.status, "Interrupted");
assert_eq!(row.td_total, 14);
assert_eq!(row.td_high, 2);
assert_eq!(row.td_carried_over, 14);
assert_eq!(row.td_resolved_since_last, 2);
assert_eq!(
row.td_new_since_last, 1,
"a new TD on an interrupted run must not read as 0"
);
assert_eq!(row.td_carried_over, 13);
}

#[test]
Expand Down
Loading