Skip to content

fix(signal): kill phantom findings and un-hide real ones across patterns, breaking, perf, semgrep and coverage - #21

Open
m-szymanska wants to merge 69 commits into
mainfrom
fix/verdict-signal-truth
Open

fix(signal): kill phantom findings and un-hide real ones across patterns, breaking, perf, semgrep and coverage#21
m-szymanska wants to merge 69 commits into
mainfrom
fix/verdict-signal-truth

Conversation

@m-szymanska

@m-szymanska m-szymanska commented Aug 22, 2026

Copy link
Copy Markdown
Member

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

  1. Pattern scan word boundaries (fix/pattern-word-boundaries): TODO/FIXME/HACK/XXX matched inside identifiers and words (mastodonTODO). Plain-word needles now require word boundaries; 9 regression tests.
  2. Phantom removed symbols for non-fn types (fix/breaking-phantom-nonfn): remove+re-add pairing only worked for pub fn (three independent fn-only gates, plus a drifted duplicate of PUB_SYMBOL_TYPES). An unchanged pub struct moved within a file was reported as a breaking removal. Pairing now covers all tracked symbol kinds; identical re-adds are silenced, changed declarations become ChangedSignature. Genuine removals stay reported (guard test).
  3. Per-hit test context in perf detection (fix/perf-test-context-per-hit): one #[cfg(test)] anywhere in a hunk classified all hits as test-only, dropping production hot-path signals from perf_regression_suspected and 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.
  4. Semgrep tool errors are not code regressions (fix/semgrep-exit2-tool-error): exit ≠ 0 without a findings payload (config error, exit 2) was Failed — indistinguishable from real findings. Now Skipped with an explicit reason (exit code + stderr excerpt). Fail-closed is preserved: at Block severity the policy engine treats it exactly like a missing tool.
  5. Skip-as-zero residuals (fix/skip-as-zero-residual): a 0/0 coverage scan reported 100%; skipped heuristics serialized as zeros indistinguishable from a clean scan. Coverage is now Option end-to-end (not measured, card/chip omitted), report.json carries measured/not_measured_reason and heuristics status/skip_reason/total_files, with counters omitted on skip. A real 0/N stays a genuine 0%. Verified E2E on sandbox repos with a release build (non-code diff vs. code-without-tests diff).

Verification

  • Full gates on the integrated branch: 1239 lib + 81 integration tests, 0 failures; clippy -D warnings clean; fmt clean. json_contract 18/18, mcp_contract 24/24 — no consumer contract broken (report.json change is additive; heuristic_ratio may now be null, called out in the CHANGELOG).
  • Each branch report documents its falsification run (test fails on reverted logic / old semantics scaffold).

Scope notes

  • pub use re-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.
  • The deeper verdict-pipeline redesign (advisory→failure conflation, semantic invariants, completeness/confidence from the PR #46 audit) is a separate decision-core line landing next — this PR is the signal-level groundwork.

Follow-up cuts (pushed after the initial review round)

  1. Warnings are not failures (fix/warning-not-failure): the P0 advisory→failure conflation from the PR #46 audit. QualityFailureOrigin { Failure, Warning } separates the two classes; has_new_failures counts only failure-origin entries, so a Warnings-status baseline check without located findings can no longer flip quality_pass=false and fabricate "N quality checks failed". --ci gets an explicit --fail-on-warnings escape 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: semgrep PartialParsing warnings → quality_pass=true, exit 0, zero "failed" wording in the pack.
  2. Readers stop guessing (fix/read-hardening): killed the fourth parallel verdict derivation (fallback_merge_gate_summary re-deriving allow_merge from the recommendation). build_cli_json_summary is now fail-loud: an unreadable/absent MERGE_GATE.json is 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 in docs/contracts/merge_gate.md.
  3. Review followup (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 warnings clean; fmt clean.

  1. Review followup round 2 (fix/review-followup-signal-2): strict merge-gate schema parsing aligned with tools/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 marked normalized; non-string schema_version values 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 the scopes_may_pair doc comment).

⚠️ Contract changes (consumer-visible, deliberate)

  • MERGE_GATE.json schema 2.1 → 2.2 (additive MINOR): quality_failure_details[] entries carry "origin": "failure" | "warning"; only origin: failure may fail the quality gate. Allowlisted in the validator, documented in docs/contracts/merge_gate.md.
  • report.json schema 1.0 → 2.0 (MAJOR): heuristic_ratio became 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 warnings clean; fmt clean. Live pack verified: validate_merge_gate.py → OK, schema 2.2, a warning-origin semgrep entry rendered alongside quality_pass: true.

  1. 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.2 no longer read as 2.2), matching the validator's exact strings; the validator itself requires a usable origin on every quality_failure_details entry at schema ≥ 2.2 (5-shape mutation test in the harness); wrongly typed MCP decision signals (including allow_merge) are named in caveats instead of silently dropped.

  2. 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 declares schema_version without a decision object is a corrupt artifact (exit 3), matching the validator and the MCP reader. Contract tests pin both directions; docs/contracts/merge_gate.md states the rule.

Final integrated gates: 1374 tests, 0 failures; clippy -D warnings clean; fmt clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nqgpf1YwXw2m3vqC8Ur8nr

vetcoders-agents and others added 11 commits August 22, 2026 13:55
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
Copilot AI lite review requested due to automatic review settings August 22, 2026 12:41

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has reached its monthly code review spending cap.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-fn public 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.

Comment thread src/regression/perf.rs
Comment thread src/artifacts/signal/breaking.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/artifacts/signal/breaking.rs Outdated
Comment thread src/regression/perf.rs
Comment thread src/regression/perf.rs Outdated
Comment thread src/artifacts/signal/breaking.rs Outdated
Copilot AI review requested due to automatic review settings August 22, 2026 13:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_bounded advances search_from = start + 1 and then slices haystack[search_from..]. Because start is a byte offset, start + 1 can land in the middle of a UTF-8 codepoint, which will panic at runtime when slicing. Consider iterating with haystack.match_indices(needle) (or doing the search on haystack.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_context only treats //... as a comment. A block comment like /* #[cfg(test)] */ still matches INLINE_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., track in_block_comment and 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;
        }

vetcoders-agents and others added 9 commits August 22, 2026 15:17
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
`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
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
Copilot AI review requested due to automatic review settings August 22, 2026 13:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/gate.rs
Comment thread src/artifacts/signal/patterns.rs
Comment thread src/artifacts/signal/breaking.rs
Comment thread src/rust_source.rs
vetcoders-agents and others added 5 commits August 22, 2026 20:27
`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
Copilot AI review requested due to automatic review settings August 22, 2026 18:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() uses str.isdigit(), which accepts non-ASCII Unicode digits (e.g. "٢.٢"). That would make tools/validate_merge_gate.py treat such a schema_version as canonical and >= 2.2, while the Rust reader rejects it (gate::canonical_u32 requires ASCII digits via is_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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

r"#\[\s*(?:cfg\s*\([^]]*\btest\b[^]]*\)|(?:[\w:]+::)*test|rstest)\s*\]|\bmod\s+tests\b",

P2 Badge Do not treat negated test cfgs as test scope

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".

Comment thread src/artifacts/signal/breaking.rs Outdated
Comment thread src/artifacts/signal/breaking.rs
Comment thread src/regression/perf.rs
Comment thread src/gate.rs Outdated
vetcoders-agents and others added 5 commits August 22, 2026 21:11
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
Copilot AI review requested due to automatic review settings August 22, 2026 19:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.

Comment thread src/artifacts/signal/patterns.rs Outdated
vetcoders-agents and others added 2 commits August 22, 2026 22:25
…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
Copilot AI review requested due to automatic review settings August 22, 2026 20:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +818 to +819
if !attribute.starts_with("#[cfg(") {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/artifacts/verdict.rs
pub(crate) struct QualityFailureDetail {
pub name: String,
pub classification: QualityFailureClass,
pub origin: QualityFailureOrigin,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +890 to +893
if !decl.text.ends_with('(') && !trimmed.is_empty() {
decl.text.push(' ');
}
decl.text.push_str(trimmed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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
Copilot AI review requested due to automatic review settings August 22, 2026 21:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +624 to +627
match find_pairable_addition(&added_syms, &added_used, removed, true) {
Some(index) => {
added_used[index] = true;
drop_removal_finding(&mut findings, removed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants