Skip to content

feat(groom): durable dedup/rejection ledger backed by GitHub issue state (BE-3874)#43

Merged
mattmillerai merged 3 commits into
mainfrom
matt/be-3874-groom-rejection-ledger
Jul 21, 2026
Merged

feat(groom): durable dedup/rejection ledger backed by GitHub issue state (BE-3874)#43
mattmillerai merged 3 commits into
mainfrom
matt/be-3874-groom-rejection-ledger

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

The "groom" janitor bot files GitHub issues for code cleanups it spots. In CI it
has no memory — every scheduled run it wakes up with a blank brain. So it
would file the same cleanup again next week, and — worse — re-file things a
human already looked at and said "no thanks" to. That's how you get everyone to
mute and disable the bot.

This PR gives it a memory that survives across runs, without any new database
or secret
: it just uses the GitHub issues themselves as the memory. Before
filing, it lists the cleanup issues it already opened and skips any it's seen —
including ones a human closed as "won't fix" (those stay suppressed forever).

What

.github/groom/ledger.py — a durable dedup/rejection ledger keyed on
(repo, finding_signature) -> {filed | rejected | superseded}, built by reading
live GitHub issue state.

Live GitHub state for a signature Ledger status Re-file?
Open groom issue filed no
Closed as completed filed no
Closed as not planned (GitHub "close as wontfix") rejected no — durable
Has the groom-rejected label (open or closed) rejected no — durable
Has the groom-superseded label superseded no
No groom issue carries it unknown yes

Filing embeds the verifier's opaque signature as an invisible HTML-comment
marker (<!-- groom-signature: … -->) in the issue body; the next run recovers
it. Pure logic (marker round-trip, classification, ledger build, candidate
partition) is separated from the one thin gh api I/O shell, so it's fully
unit-tested with no network (.github/groom/tests/, 29 cases, wired into CI via
test-groom-scripts.yml).

CLI the groom workflow calls right before filing:

python3 .github/groom/ledger.py --repo owner/name --candidates findings.json --out decision.json
# -> {to_file, suppressed, invalid, ledger_size}; open issues only for to_file

Why this store (design decision)

The ticket asked to prefer the GitHub-native option that needs no net-new
secret. Issue state wins on all three acceptance points at once: it needs no
separate store, the run's built-in GITHUB_TOKEN already reads issues (no new
secret), and it's inherently auditable (the record is the issues). It also
captures human rejection for free — a maintainer closing an issue as "not
planned" is the rejection signal, no extra tracking needed. I considered a
committed .ledger.json but rejected it: it duplicates state that already lives
in the issues and adds bot-commit noise/merge churn on every run.

Acceptance criteria

  • Filed/rejected in run N not re-filed in run N+1 (same signature) — any
    existing groom issue for a signature makes it known; should_file is true
    only for unknown. (AcceptanceScenarioTest, LedgerDecisionTest.)
  • Human rejection durably suppresses — close-as-not_planned or the
    groom-rejected label → rejected; read live every run, so it never expires.
  • Auditable, no net-new org secret — GitHub issues are the store; read via
    gh api with the run's GITHUB_TOKEN.

Self-review notes / judgment calls

  • Load-bearing filing contract (dependency on the not-yet-written groom
    workflow).
    The memory only works if the step that opens an issue applies
    the groom label and appends signature_marker(sig) to the body. This is
    the correct seam — a ledger can't remember what the filer didn't record — and
    it's documented in the module docstring + README. The filing step is Phase 1/2
    of BE-3870; this PR is the standalone hard piece it will call.
  • Closed-as-completed → suppressed (won't re-file a fixed-then-regressed
    debt).
    Deliberate anti-annoyance default matching the acceptance wording
    ("filed in run N not re-filed in N+1"). If re-detecting regressions is wanted
    later, treat completed as unknown — it's a one-constant change.
  • superseded is a defined hook with no producer in this PR — the
    groom-superseded label is classified if present, part of the ticket's
    {filed|rejected|superseded} model; nothing here sets it yet.
  • Fail-loud, not fail-empty. If gh api errors, fetch_groom_issues raises
    rather than returning an empty ledger — an empty ledger would re-file
    everything (the exact spam this prevents).
  • Negative-claim check: the diff adds no capability-denying dead-end / "not
    supported" path; the invalid bucket is defensive routing of malformed
    (signature-less) input, surfaced in output — not a user-facing denial. Trigger
    does not fire.
  • Purely additive: 5 new files, +8 doc lines in AGENTS.md; no existing behavior
    changed (Chesterton's Fence clean). AGENTS.md integrity checker still passes.

…ate (BE-3874)

The stateless groom CI run starts fresh every time, so without a durable memory
it would re-file already-filed OR already human-rejected findings on every
scheduled run. Add a dedup ledger keyed on (repo, finding_signature) ->
{filed | rejected | superseded} that uses GitHub issue state itself as the
store — the GitHub-native option that needs no net-new secret and is fully
auditable.

- .github/groom/ledger.py: lists groom-labeled issues (state=all), recovers each
  embedded signature marker, and classifies it. Human rejection (close-as-not-
  planned or the groom-rejected label) maps to REJECTED and durably suppresses
  that signature. Only genuinely-new (unknown) signatures are filed.
- Pure logic (marker round-trip, classification, ledger build, partition) is
  split from the one thin gh I/O shell so it is fully unit-tested with no network.
- README documents the load-bearing filing contract (embed the marker + apply
  the groom label) and CI test workflow mirrors the sibling test-*.yml.
@mattmillerai mattmillerai added the agent-coded Authored by the agent-work loop label Jul 21, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 21, 2026 02:58
@mattmillerai mattmillerai added the cursor-review Multi-model cursor review label Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

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: 54 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: fedf6592-79fb-48e8-8062-1f55e7151e7e

📥 Commits

Reviewing files that changed from the base of the PR and between 5c24e7a and e612423.

📒 Files selected for processing (4)
  • .github/groom/README.md
  • .github/groom/ledger.py
  • .github/groom/tests/test_ledger.py
  • .github/workflows/test-groom-scripts.yml
📝 Walkthrough

Walkthrough

Changes

Groom ledger

Layer / File(s) Summary
Ledger contracts and status construction
.github/groom/ledger.py, .github/groom/README.md, .github/groom/tests/test_ledger.py
Adds signature markers, issue classification, duplicate-status precedence, and ledger construction rules.
Ledger loading and CLI decisions
.github/groom/ledger.py, .github/groom/tests/test_ledger.py
Fetches paginated groom issues, partitions findings, and supports batch and single-signature CLI decisions.
Automated workflow and repository guidance
.github/workflows/test-groom-scripts.yml, AGENTS.md, .github/groom/tests/test_ledger.py
Adds unittest coverage, CI execution, and repository documentation for the groom ledger.

Sequence Diagram(s)

sequenceDiagram
  participant GroomCLI
  participant GitHubIssues
  participant Ledger
  GroomCLI->>GitHubIssues: Fetch groom-labeled issues
  GitHubIssues-->>GroomCLI: Return paginated issue JSON
  GroomCLI->>Ledger: Build statuses and partition findings
  Ledger-->>GroomCLI: Return filing and suppression decisions
Loading
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-3874-groom-rejection-ledger
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-3874-groom-rejection-ledger

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/groom/ledger.py:
- Around line 92-99: Update signature_marker and its paired extraction logic in
.github/groom/ledger.py:92-99 to encode normalized opaque signatures with a
delimiter-safe format such as URL-safe base64 before placing them in the HTML
comment, then decode the payload during extraction while preserving round-trip
behavior. Add a test in .github/groom/tests/test_ledger.py:38-58 covering
signatures containing "-->" and preferably newlines to verify extraction returns
the original signature.
- Around line 218-228: The candidate classification loop in ledger.py around
status() must deduplicate signatures within one batch: track signatures already
accepted into to_file, keep the first as a filing candidate, and route later
duplicates to a documented non-filing result without marking them filed. Update
.github/groom/tests/test_ledger.py lines 162-175 with two same-signature
candidates and assert exactly one is returned in to_file.

In @.github/workflows/test-groom-scripts.yml:
- Around line 28-36: Update the Checkout and Set up Python steps to pin
actions/checkout and actions/setup-python to their full commit SHA references,
retaining the required trailing version comments (for example, # v6) after each
SHA instead of using mutable tags.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 86d339ee-6e98-4e26-bf7c-2e10b9f136b0

📥 Commits

Reviewing files that changed from the base of the PR and between b47ac21 and 5c24e7a.

📒 Files selected for processing (5)
  • .github/groom/README.md
  • .github/groom/ledger.py
  • .github/groom/tests/test_ledger.py
  • .github/workflows/test-groom-scripts.yml
  • AGENTS.md

Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py
Comment thread .github/workflows/test-groom-scripts.yml

@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 10 finding(s).

Severity Count
🟠 High 2
🟡 Medium 4
🟢 Low 4

Panel: 6/8 reviewers contributed findings.

Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)

Comment thread .github/groom/ledger.py Outdated
Comment thread .github/workflows/test-groom-scripts.yml Outdated
Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py
Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py
Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py Outdated
Comment thread .github/groom/ledger.py Outdated
mattmillerai and others added 2 commits July 20, 2026 23:12
Address CodeRabbit + cursor-review panel findings on the dedup/rejection
ledger:

- Encode the opaque signature as URL-safe base64 in the HTML-comment marker
  so a signature containing `-->`, newlines, or markdown can no longer close
  the comment early (which truncated the recovered key and re-filed the
  finding every run) or inject into the public issue body. Bounded, possessive
  regex removes the O(n^2) ReDoS the old lazy `(.+?)` DOTALL pattern carried.
- Recover the LAST marker, not the first, so a marker-shaped comment planted
  in an attacker-controlled snippet cannot shadow the authoritative marker the
  filing step appends last.
- Deduplicate repeated signatures within a single candidate batch: the first
  new signature files, later duplicates are suppressed as `pending` (not
  falsely labeled `filed`), preventing duplicate issues in one run.
- Make `should_file`/`--check` reject blank signatures (route to invalid),
  matching `partition` instead of reporting `unknown` and filing an
  un-dedupable issue.
- Bound the `gh api` call with a timeout and validate `--repo` against
  `owner/name` before interpolating it into the URL.
- Document that the (not-yet-written) caller workflow must serialize runs with
  a `concurrency:` group to close the read-then-file TOCTOU.
- Pin actions/checkout and actions/setup-python to full commit SHAs per the
  repo's pin-by-SHA standard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…jection-ledger

# Conflicts:
#	.github/groom/README.md
@mattmillerai
mattmillerai merged commit 7978a59 into main Jul 21, 2026
3 of 4 checks passed
@mattmillerai
mattmillerai deleted the matt/be-3874-groom-rejection-ledger branch July 21, 2026 17:49
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