Skip to content

fix(groom): anchor the dedup signature to the finding's file path (BE-4460)#74

Open
mattmillerai wants to merge 3 commits into
mainfrom
matt/be-4460-groom-path-signature
Open

fix(groom): anchor the dedup signature to the finding's file path (BE-4460)#74
mattmillerai wants to merge 3 commits into
mainfrom
matt/be-4460-groom-path-signature

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

The groom robot files a GitHub issue for each code-cleanup finding, and remembers what it already filed by a "signature" string. That signature used to be built from the finding's title — but the model writes a fresh title every run, so re-wording "split tools.ts into modules" into "tools.ts is a monolith" produced a different signature, the ledger thought it was a brand-new finding, and the same finding got filed a second time. This anchors the signature to the finding's file path instead, because paths don't change when the wording does, and adds a backstop in the ledger that suppresses a candidate whose path is already covered by an issue we filed before.

What changed

  • .github/groom/verifier.md — the signature is now <repo-basename>:<scope-label>:<path-slug> where <path-slug> is the finding's PRIMARY file or directory path (lowercased, every run of non-alphanumeric characters collapsed to a single hyphen: src/tools.tssrc-tools-ts). Multi-file finding → the most representative path (the one the body's first evidence cites). Only a repo-wide pattern with no single anchor falls back to a normalized subject noun-phrase (not the full title). The brief states explicitly WHY: titles are re-generated every run, paths are stable across rewordings.
  • .github/groom/ledger.py — new path_token() (everything after the second :; "" when there is no third segment) plus a path-token backstop in partition(): a candidate whose exact signature is unknown but whose path segment is already covered — by a known signature, or by a candidate already routed to to_file in the same batch — is reported under suppressed with the distinct status path-collision. Ledger.should_file() and the --check CLI agree with partition (they report path-collision and exit 1). The signature stays an opaque string for classification — no format change is required to classify, precedence is untouched, and the marker round-trip is unchanged.
  • TestsPathTokenTest + PathCollisionTest (path-collision suppression, exact-signature status not shadowed, no false suppression across different paths incl. same-basename-different-directory, intra-batch collision, rejection surviving a rekey, should_file/partition agreement) and two acceptance-scenario tests (reworded-next-run files nothing; marker round-trip with a path-format signature). 74 tests pass.
  • Docs.github/groom/README.md: the contract-table signature row now documents the path derivation and why, a path-collision row in the ledger-status table, and a new "path-token backstop" section covering the exact-match rule and the transition consequence.

Acceptance criteria

  1. Two consecutive runs describing the same file with different wording produce ONE issue — met. The signature no longer depends on the title, so run N+1 emits the identical signature and partition suppresses it with ledger_status: filed, visible in decision.json. Covered by test_same_file_reworded_next_run_is_not_refiled. (The derivation lives in an LLM prompt, so it is as reliable as the brief is followed — hence the backstop below, and the explicit "keep it to the path ALONE" instruction.)
  2. Existing open groom issues with legacy signatures are not re-filed when the new-format candidate shares their path tokenmet only for an exact path-segment match; flagged as partially unmet. The ticket asked for exact matching ("no fuzzy matching"), and exact matching cannot recognize a legacy TITLE-derived slug such as split-tools-ts-into-focused-modules or tools-ts-monolith as covering the new src-tools-ts — the strings genuinely differ. I deliberately did not add substring/shared-token matching: src-index-ts vs lib-index-ts would then collide and a real finding would be silently dropped forever, which is a worse failure than one duplicate. What the backstop does catch: the same path under a different <scope-label>, and a legacy slug that coincides with the path slug. The transition cost is therefore bounded at one extra issue per pre-existing legacy finding, after which it is stable forever; retire the legacy issue with groom-superseded (or close as not planned). Pinned by test_legacy_title_slug_embedding_the_path_is_not_matched and documented in the README.
  3. ci-groom.yml / test-groom-scripts.yml passtest-groom-scripts.yml runs the unit suite on this PR (green locally: 74 tests). ci-groom.yml never fires on a PR (schedule + workflow_dispatch only) and is untouched by this diff — groom.yml's interface is unchanged.

Judgment calls

  • Intra-batch path collisions are suppressed too, not just collisions against the live ledger. The ticket specified "each candidate and each known signature"; within one run GitHub state is not refreshed, so two candidates anchored to the same path would otherwise open two issues in a single run — the same failure mode, and the existing pending status exists for exactly that reason.
  • Path tokens are lowercased for comparison only. The full signature stays case-sensitive and opaque everywhere else; the verifier already emits lowercase slugs, so this only absorbs a stray capital.
  • Inherent tradeoff of path anchoring: two genuinely distinct findings about the same file now share one signature and collapse to one issue. That is a direct consequence of the ticket's design decision, not an artifact of the backstop; the repo-wide noun-phrase fallback is the escape hatch.
  • groom.yml untouched. The job-summary line counts suppressed without breaking it out by status, so path-collision entries are counted there and fully itemized in decision.json — no workflow change was needed, and keeping the diff inside .github/groom/** avoids spurious consumer SHA bumps.

Follow-up (operator, post-merge)

ci-groom.yml pins workflows_ref to a merged-main SHA (07154fb), and the briefs + ledger.py are loaded from that pinned ref at run time — so this fix takes effect for this repo's own groom (and for external consumers) only once that pin is bumped and the floating v1 tag is moved, per the repo's versioning policy.

Verification

python3 -m unittest discover -s .github/groom/tests -p 'test_*.py'   # 74 tests, OK
python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py'   # 40 tests, OK
python3 -m unittest discover -s .github/agents-md-integrity/tests -p 'test_*.py'   # 18 tests, OK
python3 .github/agents-md-integrity/check_agents_md.py --root .   # passed (pre-existing CODEOWNERS warning)

The --check CLI path was exercised directly against a stubbed ledger: a colliding signature prints path-collision / exit 1, a new one unknown / exit 0, an exact match filed / exit 1, a blank one invalid / exit 1.

Self-review note on the negative-claim rule: this diff's "denial" is a dedup suppression, not a product-capability dead-end — it adds no unsupported/unavailable path. Its risk is over-suppression, which is bounded by exact string equality and pinned by explicit no-false-suppression tests.

…-4460)

The verifier derived its "stable" dedup signature from a slug of the finding
TITLE, but titles are re-generated by the model on every run — a rewording
produced a new slug, the ledger's exact-string match saw a brand-new finding,
and the SAME finding was filed as a second issue on the next run.

- verifier.md: `<path-slug>` is now the finding's primary file/directory path
  (lowercased, non-alphanumeric runs collapsed to hyphens), with the most
  representative path for a multi-file finding and a subject noun-phrase only
  for a repo-wide pattern with no anchor. The brief states WHY.
- ledger.py: path-token backstop in `partition` — a candidate whose signature
  is unknown but whose path segment is already covered (by a known signature or
  by an earlier candidate in the same batch) is suppressed as `path-collision`.
  Exact string equality on the path segment; no fuzzy matching. Classification
  and the marker round-trip are unchanged.
- tests + README: path-collision suppression, no false suppression across
  different paths, marker round-trip, and the contract-table signature row.
@mattmillerai mattmillerai added the agent-coded Authored by the agent-work loop label Jul 24, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 24, 2026 22:58
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1f56e22e-67c8-4a91-82f3-2ba4795bad65

📥 Commits

Reviewing files that changed from the base of the PR and between 29a81ca and e914096.

📒 Files selected for processing (4)
  • .github/groom/README.md
  • .github/groom/ledger.py
  • .github/groom/tests/test_ledger.py
  • .github/groom/verifier.md
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-4460-groom-path-signature
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-4460-groom-path-signature

Comment @coderabbitai help to get the list of available commands.

@mattmillerai mattmillerai added the cursor-review Multi-model cursor review label Jul 24, 2026

@github-actions github-actions 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.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 6 finding(s).

Severity Count
🟠 High 1
🟡 Medium 3
🟢 Low 2

Panel: 8/8 reviewers contributed findings.

Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py
Comment thread .github/groom/verifier.md Outdated
Comment thread .github/groom/verifier.md Outdated
Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py Outdated
…ng (BE-4460)

Review-panel follow-ups on the path-anchored dedup signature. A path anchors a
LOCATION, not a finding, so the path-collision backstop — which suppresses a
candidate whose own signature is new — needed narrowing:

- `security: true` candidates are never suppressed by the backstop, and the
  verifier now prefixes their slug `sec-`. Without both, one already-filed
  routine finding on `src/tools.ts` silently buried a later security finding on
  the same file, breaking the "security findings always surface as
  investigations" guarantee the rest of the pipeline enforces. Exact-signature
  dedup still applies, so the exemption costs at most one issue per finding.
- `superseded` records no longer seed the path index. `groom-superseded` is the
  documented "retire this issue so its finding can be re-filed" signal; keeping
  it indexed let the retired issue go on suppressing the replacement by path.
- `path_token` splits from the LAST colon, not the second: the slug and repo
  basename are colon-free by construction, but `scope_label` is a free-form
  input, and `pkg:api` sheared the scope's tail onto the token.
- `path_token` trims leading/trailing hyphens, and verifier.md now says to —
  its own rule read literally made `services/ingest/` -> `services-ingest-`
  while its worked example showed `services-ingest`, two tokens for one dir.
- verifier.md picks the multi-file anchor mechanically (alphabetically first
  cited path) instead of "the most representative"/evidence order, both of
  which are judgments the LLM re-makes differently next run; the repo-wide
  noun-phrase fallback is tightened and de-prioritized.
- `--check` gains `--check-security` so the probe still mirrors `partition`.

Tests: 90 pass (was 84).
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Jul 24, 2026

@github-actions github-actions 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.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 7 finding(s).

Severity Count
🟠 High 1
🟡 Medium 3
🟢 Low 3

Panel: 8/8 reviewers contributed findings.

Comment thread .github/groom/verifier.md Outdated
Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py
Comment thread .github/groom/verifier.md
Comment thread .github/groom/ledger.py
Comment thread .github/groom/ledger.py
Comment thread .github/groom/ledger.py
…g (BE-4460)

Second review-panel round, on the previous commit's own fix:

- The `sec-` slug prefix was NOT domain-separated: a routine finding about
  `sec/auth.ts` slugifies to `sec-auth-ts` and collided with the security lane
  for `auth.ts`, silently suppressing one of the two. The prefix is now `sec_`
  — an underscore can never come out of slugification (every `_` in a path
  collapses to a hyphen), so the two keys are provably disjoint.
- `is_security_finding` now fails CLOSED (`!= "false"`, the build gate's
  reading) instead of requiring an explicit `true`. It decides a security
  guarantee from an LLM-authored field, so an omitted or mangled flag must not
  be what subjects a genuine security finding to the backstop. The verifier's
  schema requires the field, so well-formed batches are unaffected; malformed
  output costs at most one extra issue.
- README: the path-collision suppression is PERMANENT, not a one-time
  transition cost — say so plainly instead of "rare in practice", and state
  that the `sec_` lane does not make individual security findings addressable
  (two vulns in one file still collapse, as two routine findings do).
- Note in `Ledger.__init__` that `_PRECEDENCE` collapses a signature holding
  both a superseded and a live record to SUPERSEDED, dropping it from the path
  index — bounded to one duplicate, since exact-signature dedup still applies.

Tests: 92 pass (was 90); suppression cases now pin `security: False`
explicitly, which is what a well-formed verifier emits.
@mattmillerai

Copy link
Copy Markdown
Contributor Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

  • BE-4486 — Make groom dedup deterministic for repo-wide findings with no anchoring path

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant