Problem
linkedIssueClaimedAt is a single PR-level timestamp, and resolveLinkedIssueClaimedAt preserves it
whenever the new linked-issue set overlaps the old one (src/db/repositories.ts:535-562):
if (existing.linkedIssuesJson === linkedIssuesJson ||
linkedIssueSetsOverlap(parseLinkedIssuesJson(existing.linkedIssuesJson), linkedIssues))
return existing.linkedIssueClaimedAt ?? observedLinkedIssueClaimedAt;
So a PR that claimed #1 on day 1 and later edits its body to Fixes #1, Fixes #7 keeps the day-1
timestamp for #7 as well.
packages/loopover-engine/src/duplicate-winner.ts:14-18 documents that creation time is deliberately
excluded from the election, and why:
contributors can edit an old placeholder PR to add a linked issue later, so creation time would let
backdated claims steal duplicate-winner credit
The #linked-issue-claim-overlap-preserve fix reintroduced exactly that backdating by a different route.
Its own reasoning is sound for the case it addresses (a pure addition shouldn't reset issue #1's clock) —
the flaw is that one timestamp cannot serve two issues.
Trigger
An attacker keeps a long-lived PR linking a throwaway issue. A victim opens a PR claiming valuable issue
#7. The attacker edits their body to add Fixes #7 — and can copy the victim's diff. The attacker's
claim time is now weeks earlier, so isDuplicateClusterWinnerByClaim
(duplicate-winner.ts:55-63) elects them; the victim gets duplicate of open PR #<attacker> and is
auto-closed. A follow-up edit dropping #1 still overlaps {1,7}, so the backdate persists.
Impact
Reward theft plus one-shot destruction of the genuine claimant's PR — the copycat-farming economics of
#9033, reached through the claim ledger rather than content similarity. Pairs directly with #9129 (the
duplicate rule as an offensive close primitive): that issue is how you fire the weapon, this is how you
make sure you win the exchange.
Impact is gated on LOOPOVER_DUPLICATE_WINNER, which defaults off — confirm the live setting before
prioritising.
Requirements
- Store the claim time per (PR, issue) rather than per PR, so adding a new issue starts that issue's
clock fresh while existing claims keep theirs. This resolves the original
#linked-issue-claim-overlap-preserve bug and this one simultaneously; the current single-column shape
cannot satisfy both.
- Contiguous
migrations/NNNN_*.sql for the new table/column.
- Add an adversarial regression test: adding an issue to an old PR must not backdate the new claim.
Test Coverage Requirements
99%+ patch coverage, branch-counted; the overlap-preserve case and the new-issue-added case both asserted.
Links & Resources
maintainer-only — reward-integrity and close authority.
Problem
linkedIssueClaimedAtis a single PR-level timestamp, andresolveLinkedIssueClaimedAtpreserves itwhenever the new linked-issue set overlaps the old one (
src/db/repositories.ts:535-562):So a PR that claimed
#1on day 1 and later edits its body toFixes #1, Fixes #7keeps the day-1timestamp for
#7as well.packages/loopover-engine/src/duplicate-winner.ts:14-18documents that creation time is deliberatelyexcluded from the election, and why:
The
#linked-issue-claim-overlap-preservefix reintroduced exactly that backdating by a different route.Its own reasoning is sound for the case it addresses (a pure addition shouldn't reset issue #1's clock) —
the flaw is that one timestamp cannot serve two issues.
Trigger
An attacker keeps a long-lived PR linking a throwaway issue. A victim opens a PR claiming valuable issue
#7. The attacker edits their body to addFixes #7— and can copy the victim's diff. The attacker'sclaim time is now weeks earlier, so
isDuplicateClusterWinnerByClaim(
duplicate-winner.ts:55-63) elects them; the victim getsduplicate of open PR #<attacker>and isauto-closed. A follow-up edit dropping
#1still overlaps{1,7}, so the backdate persists.Impact
Reward theft plus one-shot destruction of the genuine claimant's PR — the copycat-farming economics of
#9033, reached through the claim ledger rather than content similarity. Pairs directly with #9129 (the
duplicate rule as an offensive close primitive): that issue is how you fire the weapon, this is how you
make sure you win the exchange.
Impact is gated on
LOOPOVER_DUPLICATE_WINNER, which defaults off — confirm the live setting beforeprioritising.
Requirements
clock fresh while existing claims keep theirs. This resolves the original
#linked-issue-claim-overlap-preservebug and this one simultaneously; the current single-column shapecannot satisfy both.
migrations/NNNN_*.sqlfor the new table/column.Test Coverage Requirements
99%+ patch coverage, branch-counted; the overlap-preserve case and the new-issue-added case both asserted.
Links & Resources
src/db/repositories.ts~535-562;packages/loopover-engine/src/duplicate-winner.ts~14-18, ~55-63maintainer-only — reward-integrity and close authority.