fix(signal): kill phantom findings and un-hide real ones across patterns, breaking, perf, semgrep and coverage - #21
fix(signal): kill phantom findings and un-hide real ones across patterns, breaking, perf, semgrep and coverage#21m-szymanska wants to merge 69 commits into
Conversation
Plain-word pattern needles (TODO, FIXME, HACK, XXX) matched as raw substrings, so `mktemp fooXXXXXX` was flagged as a TODO and any identifier containing "TODO" (e.g. TODOS, todos_list) false-positived. Add contains_word_bounded(), applied only to needles that are plain words/identifiers (via is_plain_word()) — needles already bounded by punctuation (todo!(, .unwrap(), etc.) are untouched.
Same-file remove+re-add pairing in the breaking-change scanner covered `pub fn` only. A struct/enum/trait/type/const/static whose declaration line was re-emitted unchanged by the diff (fields or body changed below it) therefore produced a phantom RemovedSymbol, and MERGE_GATE escalated a breaking removal that never happened. Track every kind in PUB_SYMBOL_TYPES on both the removed and added side, then pair on (file, kind, name): an identical declaration drops the removal, a changed one becomes a single ChangedSignature instead of a removal plus a silent re-addition. Genuine removals with no re-add stay breaking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
A non-zero semgrep exit was classified Failed unconditionally, conflating a real --error exit (actual findings in the code) with a config/tool error (exit 2, no findings payload at all). The latter now classifies Skipped with a reason carrying the exit code and a stderr excerpt, mirroring the missing-tool pattern already used for ruff/mypy in checks/python.rs. An exit with a genuine, non-empty findings payload still classifies Failed. Fixes verify-ledger claim #8 (PRV-TOOL-VS-CODE-FAILURE family).
PerfSuspect resolved inline Rust test context per hunk: a single `#[cfg(test)]` / `mod tests` / `#[test]` marker anywhere in the hunk marked every hit in it as test context. A production hot path sharing a hunk with a trailing test module became `test_context_only`, which drops it from `perf_regression_suspected` and from the risk score — a silent false negative on a real production signal. Test context is now resolved per added line: it opens at its marker and closes once the braces opened after it balance out, so code before (and after) an inline test module stays production. Commented-out markers no longer open it, and any ambiguity — non-Rust files, unknown context — resolves toward production. Regression coverage: mixed hunk (prod hit + test hit), production hit after a closed test module, commented marker, same reason in both contexts, pure test hunk, and hit in a test file by path.
A diff with zero changed source files produced a 0/0 ratio that every consumer rendered as 100%: AI_INDEX.md, coverage-delta.txt, the dashboard chip/card/section, and report.json's quality.coverage.heuristic_ratio (1.0). "Nothing was measured" was indistinguishable from "everything is covered", inverting the SKIP semantics the merge gate already applies. CoverageSignal::coverage_pct and CoverageDelta::pct become Option<u32>, None when total_source == 0, so the compiler forces every consumer to decide. Text artifacts render "not measured" via format_coverage_pct; the dashboard omits the coverage surface entirely rather than inventing a number (this also closes the non-code-only diff leak, where a pure docs/config change still emitted a "Coverage: 100%" chip). report.json keeps every existing field: heuristic_ratio is now nullable and is joined by measured: bool and an optional not_measured_reason. The bundled PR-comment generator handles null; history.rs already treats an absent ratio as "no baseline". A real 0/N (N > 0) is untouched — that IS a measurement and stays 0%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
…d results
The merge gate and heuristics_loctree.result.json already treat a loctree
run with total_files == 0 as SKIP, but report.json did not: it emitted
available: true alongside dead_exports/cycles/twins/unused_symbols = 0,
making a scan that measured nothing indistinguishable from a clean scan.
quality.heuristics now carries an explicit status ("measured" / "skipped"),
an optional skip_reason, and total_files. When the scan is not a measurement
the count fields are omitted rather than serialized as zero, so a consumer
cannot read absence of data as absence of findings.
Additive only — no existing field was removed or renamed, and the counts
keep their skip_serializing_if semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
# Conflicts: # CHANGELOG.md
There was a problem hiding this comment.
An organization admin can view or raise the cap at claude.ai/admin-settings/claude-code. The cap resets at the start of the next billing period.
Once the cap resets or is raised, reopen this pull request to trigger a review.
There was a problem hiding this comment.
Pull request overview
This PR improves the accuracy and trustworthiness of prview’s reviewer-facing signals by eliminating known sources of phantom findings and by ensuring real findings aren’t silently suppressed. It tightens pattern matching, corrects breaking-change pairing semantics across more symbol kinds, refines perf test-context classification, distinguishes semgrep tool/config errors from code regressions, and prevents “0/0” coverage/heuristics from being rendered as a perfect/clean result.
Changes:
- Make multiple signals “truth-preserving”: fix substring false positives in pattern scan, fix phantom breaking removals for non-
fnpublic items, and classify perf hits’ inline Rust test context per-hit (not per-hunk). - Treat semgrep non-zero exits without a findings payload as tool/config errors (Skipped with an actionable reason) rather than code failures.
- Make coverage/heuristics “skip vs measured” explicit end-to-end (Option-based coverage pct, report/dashboard rendering changes, and contract/test updates).
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/regression/perf.rs | Refines perf regression detection to classify inline Rust test context per hit line and track mixed/prod/test proximity hits. |
| src/checks/semgrep.rs | Distinguishes semgrep tool/config failures from genuine code findings; formats skip reasons for policy plumbing. |
| src/artifacts/verdict.rs | Updates coverage caveat logic to handle optional coverage percent (unmeasured vs low coverage). |
| src/artifacts/tests.rs | Adjusts artifact tests for coverage pct becoming optional and adds regression guards for “0/0 not measured”. |
| src/artifacts/signal/risk.rs | Updates risk-signal test scaffolding for optional coverage pct. |
| src/artifacts/signal/patterns.rs | Adds word-boundary matching for plain-word needles to prevent substring false positives; adds regression tests. |
| src/artifacts/signal/coverage.rs | Makes coverage percent optional to prevent 0/0 rendering as 100%; adds shared formatter and tests. |
| src/artifacts/signal/breaking.rs | Expands remove+re-add pairing to all tracked public symbol kinds; adds regression tests and docs alignment. |
| src/artifacts/report.rs | Makes report.json reflect “measured vs skipped” for heuristics and “measured vs not measured” for coverage, avoiding skip-as-zero. |
| src/artifacts/pr_review.rs | Updates PR review warnings to handle optional coverage percent correctly. |
| src/artifacts/merge_gate.rs | Updates merge-gate test scaffolding for optional coverage percent. |
| src/artifacts/dashboard/trends_tests.rs | Adjusts dashboard trend tests for optional coverage percent. |
| src/artifacts/dashboard/tests.rs | Adjusts dashboard rendering tests for optional coverage percent. |
| src/artifacts/dashboard/sections.rs | Omits coverage chips/sections when coverage is unmeasured (no pct), preventing fabricated 100% UI. |
| src/artifacts/dashboard/mod.rs | Updates dashboard summary/nav rendering for optional coverage percent. |
| src/artifacts/dashboard/assets.rs | Updates PR-comment generator JS to render null coverage ratios as “not measured”. |
| src/artifacts/ai_index.rs | Uses shared optional-coverage formatter so AI index never reports 0/0 as 100%. |
| docs/architecture.md | Documents new breaking/coverage/heuristics semantics (pairing, relocated symbols, and “not measured” coverage). |
| CHANGELOG.md | Records signal-truth fixes and the additive report.json schema update (nullable heuristic_ratio). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1f0af7243
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/artifacts/signal/patterns.rs:86
contains_word_boundedadvancessearch_from = start + 1and then sliceshaystack[search_from..]. Becausestartis a byte offset,start + 1can land in the middle of a UTF-8 codepoint, which will panic at runtime when slicing. Consider iterating withhaystack.match_indices(needle)(or doing the search onhaystack.as_bytes()), so all offsets remain valid UTF-8 boundaries.
let hbytes = haystack.as_bytes();
let mut search_from = 0;
while let Some(rel) = haystack[search_from..].find(needle) {
let start = search_from + rel;
let end = start + needle.len();
let left_ok = start == 0 || !is_word_byte(hbytes[start - 1]);
let right_ok = !needs_right_boundary || end == hbytes.len() || !is_word_byte(hbytes[end]);
if left_ok && right_ok {
return true;
}
search_from = start + 1;
}
src/regression/perf.rs:403
added_line_test_contextonly treats//...as a comment. A block comment like/* #[cfg(test)] */still matchesINLINE_RUST_TEST_CONTEXT_PATTERN(it searches substrings), which can incorrectly open test context and potentially mute/shift subsequent production hits. If the intent is “commented-out markers do not open it”, block comments likely need to be excluded (e.g., trackin_block_commentand skip marker detection while inside it, or at least ignore lines starting with/*).
// Comments (including doc comments) never open test context — a doc
// comment mentioning `#[cfg(test)]` must not mute a production hit.
if trimmed.starts_with("//") {
if is_added {
flags.push(in_test);
}
continue;
}
// Only the outermost marker opens the context, so nested `#[test]`
// attributes do not reset the enclosing `mod tests` brace tracking.
if !in_test && INLINE_RUST_TEST_CONTEXT_PATTERN.is_match(trimmed) {
in_test = true;
depth = 0;
seen_open = false;
}
A baseline-signal check reporting `Warnings` (cargo-audit advisory, rustfmt,
eslint, ruff, prettier, stylelint, semgrep) is admitted to the quality summary
so the pre-existing downgrade can be computed for it. When it produced no
locatable finding it classified as `Unclassified`, which counted as a new
failure: `quality_pass` flipped to false, `analysis_status` was degraded, the
dashboard hero read HOLD, and the gate text claimed "N quality checks failed"
for output that contained no failure at all.
Quality-summary entries now carry their origin. Only `Failed`/`Error` entries
can fail the gate, whatever they classify as; warning entries keep taking part
in the pre-existing downgrade and keep their review weight through the policy
engine (Warnings -> Advisory -> ReviewRequired), so the verdict is unchanged --
only the label becomes true. The reason text gets a separate honest sentence
("2 warning signals: 1 pre-existing, 1 introduced").
Consequence for `--ci`: a warnings-only run now exits 0 instead of 1. The new
`--fail-on-warnings` flag (requires `--ci`) restores the old exit for teams
that want a warnings-clean trunk. `prview gate` exit codes are untouched:
CONDITIONAL still exits 2 under --strict, pre-existing-only PASS still exits 0.
The cargo-audit warnings test injected `CheckStatus::Passed` alongside a
warnings payload, which kept the check out of the summary entirely and masked
this bug; it now uses the real `Warnings` status. The R5-21 control test is
updated deliberately: it protects the classification suppression (still
asserted), never the claim that a formatter warning is a failed check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
`build_cli_json_summary` fell back to `fallback_merge_gate_summary` whenever `00_summary/MERGE_GATE.json` was missing or unparsable, re-deriving the decision from the in-memory policy engine with `allow_merge = recommendation != block`. That was the only place in the codebase where `allow_merge: true` could coexist with a `CONDITIONAL` verdict, contradicting the `allow_merge == (verdict == "PASS")` invariant in docs/contracts/merge_gate.md. The fallback is removed. An unreadable gate artifact is an execution error: the CLI prints it and exits 3, the same code `prview gate` already used, including on `--update` runs that re-read an earlier pack. Human stdout stops printing "All checks passed!" on that path — a raw check tally is not a verdict. Readers also stop guessing at what they cannot decode: * `schema_version` is checked against the known MAJORs (1, 2). An unknown or unparsable MAJOR fails loud; a newer MINOR is read with a `schema_forward_compat:` caveat; an absent field stays accepted as the documented pre-2.1 surface, alongside the `ALLOW`/`HOLD` verdict tolerance. * An unrecognized verdict still collapses to BLOCK on the CLI, but through a new additive `caveats` array on the `--json` summary, so a normalization is never mistaken for a reading. The MCP adapter reports `unknown_verdict` / `unknown_merge_recommendation` and sets `normalized: true` instead of dropping the field into `flatten()`. The emitter now stamps `crate::gate::MERGE_GATE_SCHEMA_VERSION` so the written and accepted schema versions cannot drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
# Conflicts: # CHANGELOG.md
`added_line_test_context` tracked inline Rust test scope over every hunk
line, removed ones included. Two consequences, both silencing production
signal:
- a `#[cfg(test)]` DELETED by the patch opened test context over the
added production code below it;
- a renamed test fn contributed two opening braces (`-fn old() {` and
`+fn new() {`) against one shared closing brace, so the scope never
closed and muted every production hit later in the hunk.
Removed lines describe the state being replaced, so they are now skipped
wholesale — markers and brace tracking alike.
Separately, proximity pairing ignored the loop's own context: a
production statement sitting above a trailing test module borrowed the
loop from a test that happened to be within the window (and the reverse
manufactured test-context suspects). A hit now pairs only with a nearby
loop in the SAME context. Unknown context still resolves to production on
both sides, so ambiguity keeps pairing and keeps erring toward prod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
Three defects in the non-fn remove+re-add pairing introduced with the
phantom-removal fix:
1. Pairing on (file, kind, name) alone ignored module scope: deleting
`a::Config` while adding `b::Config` in the same file cancelled a real
removal. A per-side `ModScope` now tracks inline `mod X {` nesting —
context lines feed both sides, `-`/`+` lines only their own, so a
rename cannot unbalance it. State is hunk-local and an unseen opener
leaves the scope unknown, which still pairs as before; only two KNOWN
and different module paths block the pairing.
2. Only the opening declaration line was compared, so a change confined
to a continuation line (`pub struct Config<` with a changed bound
below) vanished behind an identical opener. Continuation lines are now
accumulated on BOTH sides for every symbol kind — the `pub fn`-only
accumulator generalized — bounded at 8 lines so a `Lazy::new(|| {..})`
static cannot swallow its whole body into a table cell.
3. `format_breaking_changes` grouped changed signatures by (file, name).
Now that non-fn declarations reach that table, `pub struct Limit` and
`pub const Limit` share an identifier across namespaces and collapsed
into one row with a bogus "feature-gated variant" note. The grouping
key carries the symbol kind.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
The warnings-scoping tests from the signal cuts predate the hardened merge-gate reader, which turned build_cli_json_summary into a Result and refuses to invent a verdict when no MERGE_GATE.json exists. Plant a real gate artifact in a temp dir and unwrap the Result explicitly so the tests assert against the same read path production uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
Addresses all 6 review threads on PR #21: removed-line handling in the perf test-context tracker, kind-aware signature grouping, module-scope pairing and multi-line declaration accumulation in the breaking tracker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d94ebdc143
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`raw_string_start` accepted the `r` and `br` prefixes but not `cr` (raw C strings, Rust 1.77). The `c` was therefore left as code, the `r` that followed it was rejected as an opener because its preceding byte was alphanumeric, and the literal's first interior `"` opened a phantom ordinary string -- so every brace in the body was counted as syntax. That is the same failure mode as an untracked multi-line literal: a `mod` scope popped early, a removal left with an unknown scope, and an unknown scope pairs with anything. The construct is real outside this tree: a 2025-crate crates.io sample carries 38 `cr#"…"#` sites across 11 crates, including `syn` and `proc-macro2`, which sit in most Rust dependency graphs. `b"…"` and `c"…"` are deliberately NOT touched. They are not raw -- they escape exactly like an ordinary string, so the existing `"` arm already blanks them correctly. Their prefix letter survives into the code text, as it always has; a bare `b` or `c` is not a delimiter and cannot form a keyword, so no consumer reads it, and consuming it would change `b` handling for no measured defect. A test pins that neighbour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
The doc on `select_decision_object` claimed the rule separating the two shapes is "the presence of `schema_version`, not the presence of `decision`". The code has never done that: a stated `decision` object wins first, and only an absent one makes the schema check decide. The doc, not the code, was wrong. Preferring the root for every schema-less pack would REGRESS the readable case: a schema-less pack that carries a `decision` object would be read from its root instead, every signal would come back absent, and absent signals normalize to `BLOCK` -- a fabricated block for an artifact that stated an approval. The new test fails with exactly that (`verdict` reads as `None`) when the precedence is flipped, so it guards the choice rather than describing it. The ambiguous shape -- schema-less AND carrying decision fields at both levels -- is now named as undefined rather than silently resolved. No writer generation has ever produced it: every writer back to the first public release emits `schema_version` and `decision` together, and all 1796 packs on this machine carry both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
`mod_opening_name` requires the opening brace on the declaration's own
line, so `mod name` with `{` on the next line records no scope and the
lines under it read as `None`. That is real, and `None` pairs with
anything, which is the direction that can hide a removal.
It is left as-is on measurement rather than fixed on principle: the style
is 20 sites in ONE crate out of 2025 sampled from crates.io -- 0.12% of the
16,968 module declarations in 27.1M lines, and zero in this tree. Carrying
a pending name needs cross-line state that would itself be heuristic at
hunk boundaries, and it produces the same `None` a hunk that omits the
context line already produces. Naming the boundary in the doc keeps it a
known limit instead of silent drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
…hing `char::is_alphanumeric` is Unicode Alphabetic + Numeric, which excludes most combining marks, while every scanned language admits them as identifier continuations. `TODO` followed by a bare combining mark therefore still reads as a standalone marker and inflates `prod_hits`. Left as-is on measurement: a 33.4M-line sample across three ecosystems (crates.io 27.1M, npm 5.9M, site-packages 0.4M) holds 181,402 combining marks and NOT ONE adjacent to a `TODO`/`FIXME`/`HACK`/`XXX` occurrence. Closing it needs an `XID_Continue` table -- a new dependency or a hand-rolled range set -- for a case with no observed instance, and the residual error points at reporting a false marker rather than hiding a real one. The doc now states the boundary so it stays a known limit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
…inned, measured deferrals) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tools/validate_merge_gate.py:39
schema_at_least()usesstr.isdigit(), which accepts non-ASCII Unicode digits (e.g. "٢.٢"). That would maketools/validate_merge_gate.pytreat such a schema_version as canonical and >= 2.2, while the Rust reader rejects it (gate::canonical_u32requires ASCII digits viais_ascii_digit()), reintroducing validator/reader drift. Consider enforcing ASCII digits explicitly (e.g.p.isascii() and p.isdigit()), or use a[0-9]+check for each component.
There was a problem hiding this comment.
💡 Codex Review
prview-rs/src/regression/perf.rs
Line 69 in f8f77bb
This predicate matches any cfg(...) containing the word test, including production-only guards such as #[cfg(not(test))] and mixed guards such as #[cfg(any(test, feature = "bench"))]. Added loops and queries under those guards are therefore classified as test-only and omitted from perf_regression_suspected and the downstream risk score even though they compile in non-test builds. Recognize cfg semantics rather than the presence of the token alone, and add a negated-cfg regression test.
AGENTS.md reference: AGENTS.md:L47-L47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The CLI matched a stored verdict case-sensitively while the MCP adapter ranked it through an ASCII-uppercase fold. A pack stating `verdict: "pass"` was therefore a clean `PASS` to MCP automation and an unknown verdict normalized to `BLOCK` on the CLI: the same artifact approved by one reader and rejected by the other, which is exactly the divergence the shared reconciliation exists to prevent. `APPROVE` diverged the same way, case aside -- it ranked as a pass but was not in the CLI's fold. A third surface was worse. `prview gate` compared the FOLDED summary verdict against the pack's raw string, so a legacy `ALLOW`/`HOLD` pack, or any non-canonical spelling, failed loud as a "gate verdict mismatch" on an artifact both other readers read fine. Rather than patch the CLI match for a fourth time, the vocabulary itself moves into `gate::canonical_verdict` and all three surfaces fold through it. `rank_from_verdict` is now derived from that fold, so the ranking and the folding cannot drift apart again. The mismatch check in `prview gate` compares canonical to canonical, which keeps it a guard against the summary and the pack stating DIFFERENT decisions while no longer firing on a spelling difference. `GateVerdict` stays a strict parser of the canonical spellings and is fed the folded value, never the raw one. Case is not meaning, and neither is a retired synonym: reading `"pass"` as a block would fabricate a verdict the artifact never gave. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
A predicate wrapped across lines recorded only its opener. `#[cfg(any(` became the whole guard, and the very next line -- `feature = "a",` -- matched nothing and was read as a new item, which cleared the guard outright. The declaration below it was therefore UNGUARDED on both sides of the diff, so a `pub` item that really disappeared for one configuration paired with its re-add under a different one and left no finding at all. That is the precise false negative the guard exists to prevent, and it is not a rare shape: 3,668 multiline `cfg` attributes sit directly on a public item across 243 of the 2,025 crates measured in the local crates.io registry. Attributes are now accumulated until their delimiters balance, and only the finished text becomes a guard. Because whitespace was already dropped, a wrapped predicate now compares EQUAL to its single-line spelling -- a `rustfmt` rewrap is formatting, not a different gate. Any other wrapped attribute is carried the same way, so a multiline `#[derive(…)]` between the `cfg` and its item no longer takes the guard down with it. A diff shows attributes partially like everything else, so an opener whose close never arrives gives up after `MAX_ATTRIBUTE_CONTINUATION_LINES` and falls back to the tolerant `None`: a stale guard would fabricate removals, and unknown pairs with anything. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
Accumulation stopped after eight continuation lines. Two long declarations agreeing on their opener and those eight lines therefore finalized to the SAME truncated text: the exact-match pass paired them as an unchanged re-add, consumed the addition and dropped the removal, so a parameter, bound or return type changed on the ninth line or later produced no finding at all -- the tool stayed silent about exactly the edit it exists to name. The bound was documented as serving two purposes, and only one of them is real. It is a runaway valve, not a display width: nothing downstream truncates a declaration, and an equally long SINGLE-line declaration was never cut at all. What the cap actually truncates is the text the pairing COMPARES. Eight cut inside the distribution. Measured over 2,970,120 `pub` declarations in the local crates.io registry (59,974 files): 94.76% wrap over no continuation line, 4.96% over one to eight, 0.27% over more. A bound of 32 covers 87% of that remainder; what is left beyond it is dominated by generated data tables, where "the rest of the declaration" is data rather than signature. That residual is now stated in the constant's doc rather than left implied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
The tracker closed a test context only when the brace it opened balanced again. Not every test item opens one: `#[cfg(test)] mod tests;` and `#[cfg(test)] use crate::helper;` annotate an item that ends at its `;`, so `seen_open` stayed false and the close could never fire. The context remained active for the rest of the hunk, and every production loop and query added below such a declaration was recorded as test-only -- muted out of the performance signal entirely, which is the direction that hides a real regression rather than inventing one. A context opened over an item that ends at its `;` now closes there. The item is found by skipping the attributes stacked above it, so the one-line form `#[cfg(test)] mod tests;` is read the same as the wrapped one, and an item that DOES open a body is left to the brace tracker exactly as before: closing at the first `;` inside a test module would report genuine test-only work as production. Measured in the local crates.io registry (59,974 files): 1,464 test markers stand over a body-less item, across 457 of 2,025 crates, against 110,465 that open a body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
…cfg attribute reads, declaration cap widened, bodiless test-context close) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
…edge
Bounded matching was reached only by needles made entirely of identifier
characters, so `todo!(`, `dbg!(`, `println!(`, `console.log(`, `unsafe {`
and `as any` kept raw substring matching. `mytodo!(…)` was reported as a
TODO marker and every `has any` in a doc comment as a type cast -- the
exact substring false positive that bounded matching was added to
exclude. The helper itself was already correct for a punctuated needle:
its own test asserted `mytodo!(` does not match `todo!(`, while the
scanner never called it for that needle. A green unit test on a helper is
not evidence about the pipeline.
Each side is now bounded where the NEEDLE has an identifier edge, which
is the only side a longer identifier can swallow it from. `todo!(` is
already right-bounded by its `(` and gains the left check; `.unwrap()`
starts with `.` and must NOT gain one, or `value.unwrap()` stops matching
-- deriving the rule per edge is what makes both true at once.
`eslint-disable` keeps matching `eslint-disable-next-line`, because `-`
is not an identifier character.
The whole needle table was reviewed, not the three reported macros. It
turned up one coverage loss to repair: `eprintln!(` CONTAINS `println!(`,
so the eprint family was being caught by accident, and bounding the
needle would have dropped it silently. It is now listed explicitly.
Measured in the local crates.io registry (60,201 files), identifier-
prefixed occurrences that stop being reported: `as any` 192 in 100
crates (almost all prose -- "has any"), `print!(` 163 in 36, `dbg!(` 35
in 2, `todo!(` 17 in 7, `unsafe {` 17 in 15; and the eprint family, 2,308
hits in 349 crates, which is preserved by name instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
…mily pinned) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4dc55cfa05
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !attribute.starts_with("#[cfg(") { | ||
| return; |
There was a problem hiding this comment.
Preserve cfg_attr-based configuration guards
When an unchanged non-function declaration is re-emitted while its guard changes from #[cfg_attr(feature = "a", cfg(unix))] to the Windows equivalent, this filter discards both attributes because it recognizes only literal #[cfg(. Both declarations therefore receive cfg_guard: None, exact pairing consumes the addition, and the removal finding disappears even though Config is no longer available on Unix when feature a is enabled. Include cfg_attr forms capable of applying cfg in the guard identity and add a regression test for this pairing case.
AGENTS.md reference: AGENTS.md:L47-L47
Useful? React with 👍 / 👎.
| pub(crate) struct QualityFailureDetail { | ||
| pub name: String, | ||
| pub classification: QualityFailureClass, | ||
| pub origin: QualityFailureOrigin, |
There was a problem hiding this comment.
Serialize quality-failure origins in report.json
This new field distinguishes advisory Warnings entries from actual failed checks, but build_report still maps each detail into GateQualityFailureDetail using only name and classification (src/artifacts/report.rs:692-698). For a warning such as an introduced Rustfmt delta, report.json therefore contains introduced_quality_failures alongside quality_pass: true without the origin needed to explain that state, even though the parallel MERGE_GATE.json entry is now unambiguous. Propagate origin into the report payload and document/test that contract addition.
AGENTS.md reference: AGENTS.md:L47-L49
Useful? React with 👍 / 👎.
| if !decl.text.ends_with('(') && !trimmed.is_empty() { | ||
| decl.text.push(' '); | ||
| } | ||
| decl.text.push_str(trimmed); |
There was a problem hiding this comment.
Keep line comments from swallowing declaration tails
Fresh evidence after the literal/comment scanner fix is that joining physical declaration lines with spaces changes // semantics. If a moved or re-emitted multiline public declaration has a parameter line ending in // comment, declaration_complete treats every subsequently appended line as part of that comment, never sees the closing ) or {, and absorbs body lines until finalization; a body-only rewrite can then be emitted as a phantom ChangedSignature. Preserve physical line-comment boundaries or scan each continuation line with state, and cover a multiline signature containing a trailing comment.
AGENTS.md reference: AGENTS.md:L47-L47
Useful? React with 👍 / 👎.
…trate) Merge, not rebase: the 44 resolved review threads on this PR are anchored to their commits. One conflict, CHANGELOG.md, and it was a both-added `[Unreleased]`: the two campaigns wrote the section independently from the same empty base. Resolved as a union, every bullet from both sides kept verbatim and re-sectioned as Added / Changed / Fixed / Security -- 3 Added (1 from the verdict line, 2 from substrate), 11 Changed, 37 Fixed (24 + 13), 1 Security. The ammonia entry is main's single canonical one, not a second copy. The loctree-bump bullet, inherited by both sides from the merge base, is deduplicated back to one. Main's copy also carried a committed conflict marker inside `[Unreleased]` -- a literal `||||||| 5177ace` line followed by a duplicate `### Changed` block. Resolving this file into a state that still contained a stray marker was not an option, so the marker and its duplicate block are gone. The released history below `[Unreleased]` is byte-identical to main, including #19's expanded 0.6.0 loctree entry. Everything else auto-merged. Cargo.lock and Cargo.toml come out byte-identical to main (ammonia 4.1.4): this branch changes no dependency. The shared code and docs carry both behaviours -- the substrate/provenance work and the verdict-vocabulary, needle-edge, declaration-cap and test-context work sit in different functions and different sections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a6848e820e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| match find_pairable_addition(&added_syms, &added_used, removed, true) { | ||
| Some(index) => { | ||
| added_used[index] = true; | ||
| drop_removal_finding(&mut findings, removed); |
There was a problem hiding this comment.
Compare public bodies before canceling non-function removals
When a body-bearing declaration is fully removed and re-added with the same opener, this exact-match pass drops the removal even though SymbolDecl.text ends at the opening { and excludes the body. For example, moving or re-emitting pub enum Mode while replacing variant Old with New produces identical declaration texts, and no other scanner records the removed variant, so a real downstream API break disappears entirely; public struct-field and trait-item changes have the same failure mode. Compare the relevant public body or avoid exact-canceling body-bearing declarations when their bodies differ, with a regression test for an enum variant removal.
AGENTS.md reference: AGENTS.md:L47-L47
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr # Conflicts: # CHANGELOG.md
Verdict & signal truth: stop fabricating findings, stop hiding real ones
Part 2 of the 2-PR campaign (part 1: #20, substrate truth). Five surgical cuts, each independently falsified before merge, all attacking the same failure family: prview's signals lied in both directions — phantom findings that erode trust, and real signals silently suppressed.
The cuts
fix/pattern-word-boundaries):TODO/FIXME/HACK/XXXmatched inside identifiers and words (mastodon→TODO). Plain-word needles now require word boundaries; 9 regression tests.fix/breaking-phantom-nonfn): remove+re-add pairing only worked forpub fn(three independent fn-only gates, plus a drifted duplicate ofPUB_SYMBOL_TYPES). An unchangedpub structmoved within a file was reported as a breaking removal. Pairing now covers all tracked symbol kinds; identical re-adds are silenced, changed declarations becomeChangedSignature. Genuine removals stay reported (guard test).fix/perf-test-context-per-hit): one#[cfg(test)]anywhere in a hunk classified all hits as test-only, dropping production hot-path signals fromperf_regression_suspectedand the risk score (the historical argon2 shape). Context now opens at its marker and closes on brace balance, per hit line; every ambiguity resolves toward production.fix/semgrep-exit2-tool-error): exit ≠ 0 without a findings payload (config error, exit 2) wasFailed— indistinguishable from real findings. NowSkippedwith an explicit reason (exit code + stderr excerpt). Fail-closed is preserved: atBlockseverity the policy engine treats it exactly like a missing tool.fix/skip-as-zero-residual): a0/0coverage scan reported 100%; skipped heuristics serialized as zeros indistinguishable from a clean scan. Coverage is nowOptionend-to-end (not measured, card/chip omitted),report.jsoncarriesmeasured/not_measured_reasonand heuristicsstatus/skip_reason/total_files, with counters omitted on skip. A real0/Nstays a genuine 0%. Verified E2E on sandbox repos with a release build (non-code diff vs. code-without-tests diff).Verification
-D warningsclean; fmt clean.json_contract18/18,mcp_contract24/24 — no consumer contract broken (report.jsonchange is additive;heuristic_ratiomay now benull, called out in the CHANGELOG).Scope notes
pub usere-export detection deliberately not added — it was never a tracked symbol type, so there was no phantom to fix; adding detection is a new (noisy) finding class. A contract test pins this so it can't sneak back in as a phantom.Follow-up cuts (pushed after the initial review round)
fix/warning-not-failure): the P0 advisory→failure conflation from the PR #46 audit.QualityFailureOrigin { Failure, Warning }separates the two classes;has_new_failurescounts only failure-origin entries, so aWarnings-status baseline check without located findings can no longer flipquality_pass=falseand fabricate "N quality checks failed".--cigets an explicit--fail-on-warningsescape hatch (requires--ci; outside CI the flag never hardens a local run). Pre-existing-findings degradation is untouched — both contract tests pass unchanged. Runtime proof on a real repo: semgrepPartialParsingwarnings →quality_pass=true, exit 0, zero "failed" wording in the pack.fix/read-hardening): killed the fourth parallel verdict derivation (fallback_merge_gate_summaryre-derivingallow_mergefrom the recommendation).build_cli_json_summaryis now fail-loud: an unreadable/absentMERGE_GATE.jsonis an execution error (exit 3), not an invented verdict; unknown verdict/recommendation values surface as explicit caveats instead of being silently dropped; schema mismatches follow the MAJOR/MINOR convention documented indocs/contracts/merge_gate.md.fix/review-followup-signal): all 6 bot review threads addressed with verify-first regression tests (each new test fails on the pre-fix code) — removed-line handling in the perf test-context tracker (2 threads), context-matched proximity pairing, kind-aware signature grouping, module-scope pairing, and multi-line non-fn declaration accumulation (2× P1). Details in the thread replies.Integrated gates after all follow-ups: 1353 tests, 0 failures; clippy
-D warningsclean; fmt clean.fix/review-followup-signal-2): strict merge-gate schema parsing aligned withtools/validate_merge_gate.py(exact known set, fail-loud on malformed versions); trailing//comments no longer open test context or shift brace depth in the perf tracker; forward-schema MCP reads markednormalized; non-stringschema_versionvalues rejected instead of silently read as legacy; cross-hunk module scope deferred with a measured basis (173-commit corpus: the proposed rule would resurrect 4 phantom removals and lose every genuine signature change — recorded in thescopes_may_pairdoc comment).MERGE_GATE.jsonschema 2.1 → 2.2 (additive MINOR):quality_failure_details[]entries carry"origin": "failure" | "warning"; onlyorigin: failuremay fail the quality gate. Allowlisted in the validator, documented indocs/contracts/merge_gate.md.report.jsonschema 1.0 → 2.0 (MAJOR):heuristic_ratiobecame nullable and the loctree counters became omittable — a 1.0 decoder stops parsing some packs, and this repo defines MINOR as "unknown fields ignored". Stamping this MINOR would repeat the "0/0 is 100%" lie at the schema level. No in-repo reader consumes the stamp; the bump is declarative for external consumers.Final integrated gates: 1363 tests, 0 failures; clippy
-D warningsclean; fmt clean. Live pack verified:validate_merge_gate.py→ OK, schema 2.2, a warning-origin semgrep entry rendered alongsidequality_pass: true.Review followup round 3 (
fix/review-followup-signal-3): duplicate public declarations pair one-to-one (additions are consumed; two-pass — identical matches first, then changed signatures); braces inside string/char literals no longer close the perf tracker's test scope (raw/byte strings and'{'covered, per-line best-effort named); schema versions must be canonically spelled (02.02/+2.2no longer read as 2.2), matching the validator's exact strings; the validator itself requires a usableoriginon everyquality_failure_detailsentry at schema ≥ 2.2 (5-shape mutation test in the harness); wrongly typed MCP decision signals (includingallow_merge) are named in caveats instead of silently dropped.Versioned packs must carry a decision object (
fix/review-followup-signal-4):read_merge_gate_summary's root-as-decision fallback is narrowed to unversioned legacy packs only — a pack that declaresschema_versionwithout adecisionobject is a corrupt artifact (exit 3), matching the validator and the MCP reader. Contract tests pin both directions;docs/contracts/merge_gate.mdstates the rule.Final integrated gates: 1374 tests, 0 failures; clippy
-D warningsclean; fmt clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr