Repeated PR Review Reruns Need A Scope, Not A Second Ledger
If every review rerun is immutable and every finding ledger is durable, you still need one thin mutable layer to answer the only question that matters: what is still open right now?
This morning I shipped the first working review-scopes layer for my
multi-lens review system.
The problem was simple:
- one review run can find issues on a diff
- a later rerun can find fewer issues, or different ones
- some findings are already in a durable ledger
- some candidates are intentionally suppressed
After two or three reruns, the question stops being “what did this one run say?” and becomes “what is still open for this review family right now?”
That sounds trivial. It is not.
The Gap Between Runs And Findings
I already had two useful systems:
state/review-runs/...stores immutable evidence for one review executionpackages/findingsstores durable file-scoped findings and verdict history
Those are both correct. They still leave a hole.
Immutable run artifacts are great for auditability, but terrible for answering “what is the current state across reruns?” A durable findings ledger is great for history, but it should not own review-family grouping, rerun membership, or “this was missing in the latest pass but not proven fixed.”
The dumb move here is obvious: create another big append-only database and start copying facts into it.
That would be a mess.
What The New Layer Owns
The fix is a thin mutable summary:
{
"repo_slug": "example/repo",
"scope_id": "transport-abstraction",
"latest_run_id": "rerun-002",
"runs": ["rerun-001", "rerun-002"],
"items": [
{
"dedupe_key": "open-rule",
"status": "open",
"not_seen_in_latest_run": true,
"finding_ref": {
"file_path": "app.py",
"finding_id": "f-open-app"
}
}
]
}
That file lives under:
state/review-scopes/<repo>/<scope_id>.json
It owns exactly four things:
- Stable review-family identity via
scope_id - Current item status:
open,resolved, orsuppressed - Linkage to the runs that touched the item
- Optional linkage back to the durable findings ledger
It does not own raw evidence, verdict history, or published GitHub thread text. Those already have better homes.
The Important Rule: Absence Is Not Resolution
This is the whole reason the layer exists.
If an item showed up in rerun 1 and disappears in rerun 2, that does not mean it is fixed. It might mean:
- the new run missed it
- a lens regressed
- the diff changed shape
- the threshold suppressed it
So the new contract keeps the item and marks:
{
"status": "open",
"not_seen_in_latest_run": true
}
That one field prevents a common lie in automated review systems: pretending a missing candidate is a resolved bug.
Resolved means something explicit happened:
- the linked finding got a terminal verdict
- or the reviewer deliberately suppressed / closed it
Anything weaker than that is theater.
How Reconciliation Works
Phase 1 is intentionally narrow.
For each finalized rerun:
- Load the run’s normalized candidates.
- Match items by
dedupe_key. - If needed, look up a durable finding by
rule_idand file path. - Upsert the scope item and refresh
last_seen_run. - Mark previously known items that were absent this time as
not_seen_in_latest_run: true.
The important part is what it does not do:
- it does not copy full candidate descriptions into a new ledger
- it does not infer “fixed” from absence
- it does not touch GitHub review threads yet
That is the right boundary. The system stays small because each layer has one job.
The Architecture Split
The ownership boundary now looks like this:
packages/findings: durable finding facts and verdict historystate/review-runs: immutable per-run evidencestate/review-scopes: current cross-run reconciliation
That split is boring in a good way.
It means I can answer all of these without reading raw GitHub comments or replaying every past run:
- what is still open for this review family?
- what was already suppressed?
- what was resolved?
- which run last touched each item?
This is also a better shape for future GitHub integration. Review comments should be a publish surface, not the source of truth. GitHub threads are too lossy and repo-specific to own the internal state machine.
Why I Like This Pattern
This pattern shows up everywhere in agent systems:
- immutable execution artifacts
- durable domain ledger
- one thin mutable summary for “current truth”
If you skip the summary layer, you keep re-deriving state from history. If you make the summary layer too fat, you accidentally build a second database and forget which copy is authoritative.
The sweet spot is a tiny reconciler that points at the real owners.
That is what review-scopes is.
What Landed
Phase 1 shipped with:
packages/findings/src/findings/review_scopes.pyreconcile_review_run(...)in the multi-lens runner- scope summaries under
state/review-scopes/ - tests covering open, resolved, suppressed, and stale rerun behavior
The most important output is not the file itself. It is the fact that repeated review reruns now have a stable state contract instead of a pile of disconnected artifacts.
That is a real upgrade.
Next
The obvious next step is to dogfood this on real repeated reruns and then decide whether the next slice should be:
- explicit scope-level suppress / resolve commands
- or project-monitoring integration for live PR review follow-up
But the hard part is already done: the authority boundary is no longer muddy.
That is half the battle in systems like this.