Skip to content

feat(review): report a superseded pull request as superseded, not unlinked - #10205

Merged
JSONbored merged 4 commits into
mainfrom
fix/superseded-linked-issue-10168
Jul 31, 2026
Merged

feat(review): report a superseded pull request as superseded, not unlinked#10205
JSONbored merged 4 commits into
mainfrom
fix/superseded-linked-issue-10168

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

A contributor whose linked issue is closed by a rival PR that merged first is currently told:

No linked issue detected — The PR cites an issue number, but it could not be verified as a currently open issue.
If this PR is intended to solve an issue, link it explicitly in the PR body.

They did link one, correctly. The advice cannot work — re-linking a closed issue changes nothing. This splits that verdict in two.

confirmedNoOpenLinkedIssue collapses two different situations:

  • Gaming — the PR cited an already-dead issue to clear linkedIssueGateMode: block. Real, and unchanged by this PR.
  • Superseded — the PR linked a genuinely open issue that a rival then closed. Not a linking failure at all.

The superseded case now gets its own code (linked_issue_superseded), its own message naming the rival, and an action a contributor can act on.

The two facts are proven before the split fires, both from data already in hand:

  1. the issue's closed_at postdates the PR's created_at — it was open when the PR was written;
  2. a merged sibling citing the same issue landed in the window ending at that close.

resolveLinkedIssueHasOpenReference already fetched every linked issue and discarded all but a boolean, so the closure facts now ride out of that same pass — no extra GitHub call. The rival comes from a bounded pull_requests read; the duplicate/overlap machinery cannot see it, because that keys on open siblings and the rival stopped being one the moment it merged.

Reproduced from the Orb (JSONbored/metagraphed): PR 8886 opened 09:22:36 citing issue 8829; PR 8881 merged 09:30:24 citing the same issue; issue 8829 closed 09:30:25. 8886 then produced 56 identical hold | missing_linked_issue verdicts on one head SHA.

Notable decisions

  • Reads pull_requests, not recent_merged_pull_requests. That purpose-shaped table stopped being written — its newest row on the Orb is three weeks old — so a rival that merged minutes ago is absent from it and the check would silently never fire. Filed separately.
  • Rides the same linkedIssueGateMode knob it was split out of. A repo that opted into block already asked for this PR to be acted on; splitting the message must not quietly change whether it is acted on, only what the contributor is told and which code the ledger records.
  • Every uncertain case resolves to "not superseded". A supersession verdict closes a pull request, so a missing timestamp, an unparseable date, a still-open issue, or an absent rival all leave the existing disposition alone.
  • Ordering is pinned on both axes (ascending issue number, then ascending PR number within an issue) because a result that closes a PR must not depend on database row order.
  • Applied to both advisory twinssrc/rules/advisory.ts and the engine's gate-advisory.ts — each with its own suite, since a fix to only one side is exactly the drift that pair is kept apart to expose.
  • Gated OFF by LOOPOVER_SUPERSEDED_CLOSE. Recognising supersession closes the PR, so it ships dark until shadow-checked against the held backlog.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked a currently open issue this PR resolves (e.g. Closes #123) — a linked open issue is required for every contributor PR.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally; codecov/patch requires ≥99% coverage of the lines AND branches you changed
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

Full npx vitest run: 1357 files, 26465 tests, 0 failures. @loopover/engine: 942 pass. test:engine-parity: 15 pass. Also green: dead-source-files, dead-exports, cf-typegen, import-specifiers, coverage-boltons, engine-parity:drift, docs:drift, db:migrations, typecheck-coverage.

src/review/linked-issue-superseded.ts is at 100% statements, branches, functions and lines, and every guard in it was mutation-tested — 12 mutants applied (each guard inverted or removed, both sort keys dropped), 12 killed. The first sweep left 3 survivors; fixing them exposed a real nondeterminism, where two rivals merged in the same second resolved by whichever row the database returned first.

If any required check was skipped, explain why:

  • actionlint, test:workers, test:mcp-pack, ui:lint, ui:build were not run locally — this PR touches no workflow, worker-pool, MCP-pack, or UI file. CI runs them all.
  • npm audit reports 13 pre-existing advisories on main; this PR changes no dependency (no package.json / lockfile diff).

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests.
  • API/OpenAPI/MCP behavior is updated and tested where needed.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks.
  • Visible UI changes include a UI Evidence section below.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs.

The one new contributor-visible string names a public PR number and a public issue number, and nothing else.

UI Evidence

Not applicable — no UI, frontend, docs, or extension files are touched.

Notes

The new finding reads:

Superseded by a merged pull request
Issue #8829 was closed by #8881, which merged after this pull request opened. The work this pull request targets is already on the default branch.
Nothing is wrong with the issue link. If part of this pull request is still unaddressed by #8881, open a new issue describing what remains.

Per the disposition decision on the issue, this treats a proven supersession as a close, not a hold. The countervailing risk — a superseded PR that also carries work the rival did not cover — is why the action text points the contributor at opening a fresh issue for the remainder, and why the whole thing ships behind a flag that is off.

Closes #10168

A contributor whose linked issue is closed by a rival PR that merged first is
currently told "No linked issue detected — link it explicitly in the PR body".
They did link one, correctly, and the advice cannot work: re-linking a closed
issue changes nothing. metagraphed#8886 linked issue #8829 at 09:22:36; rival
#8881 merged at 09:30:24 and the issue closed one second later, and from then on
every evaluation produced the same unactionable hold.

confirmedNoOpenLinkedIssue collapses gaming (citing an already-dead issue to
clear linkedIssueGateMode: block) with supersession (linking a genuinely open
issue that a rival then closed). Only the first is a linking failure.

The two separate on facts already in hand, with no new GitHub call: the issue's
closed_at postdates the PR's created_at, and a MERGED sibling citing the same
issue landed in the window ending at that close. GitHub's issue payload already
carries closed_at (#4528) and the linked-issue pass already fetches every linked
issue -- it discarded everything but a boolean. The rival comes from our own
pull_requests rows; the duplicate machinery cannot see it, because that keys on
OPEN siblings and the rival stopped being one when it merged.

Every uncertain case resolves to "not superseded": a supersession verdict closes
a contributor's PR, so a missing timestamp, an unparseable date, a still-open
issue, or an absent rival all leave the existing disposition alone. Ordering is
pinned on both axes (ascending issue number, then ascending PR number within one
issue) because a result that closes a PR must not depend on database row order.

Refs #10168
… checks

The supersession resolver (#10168) needs the rivals that merged between a PR's
creation and its linked issue's close. Selects only number/merged_at/linked
issues, bounded by the window and capped, ordered by ascending merge time so the
cap keeps the earliest merges -- the resolver elects the earliest qualifying
rival, so dropping those would mis-name the cause.

Reads pull_requests rather than the purpose-shaped recent_merged_pull_requests
table: that table is no longer written -- its newest row on the Orb is three
weeks old -- so a rival that merged minutes ago is absent from it entirely, and a
check reading it would silently never fire.

Refs #10168
…inked

Splits the confirmedNoOpenLinkedIssue verdict in two. A PR that cited an
already-dead issue keeps reading as missing_linked_issue -- that is the gaming
case the guardrail exists for. A PR whose genuinely-open issue a rival closed
after it opened now gets its own code, its own message naming the rival, and an
action a contributor can actually take.

The two facts are proven before the split fires: the issue outlived this PR's
creation, and a merged sibling citing it landed in the window ending at its
close. resolveLinkedIssueHasOpenReference already fetched every linked issue and
discarded all but a boolean, so the closure facts now ride out of that same pass
-- no extra GitHub call. The rival comes from a bounded pull_requests read.

Wired through all four gate-evaluating call sites via the shared
resolveLinkedIssueAdvisoryContext, so the sweep, the webhook path, the heavy
re-review and authorized PR actions cannot disagree about it. Applied to BOTH
advisory twins (src/rules/advisory.ts and the engine's gate-advisory.ts), each
with its own suite, since a fix to only one side is exactly the drift that pair
is kept apart to expose.

linked_issue_superseded rides the same linkedIssueGateMode knob it was split out
of: a repo that opted into block already asked for this PR to be acted on, and
splitting the message must not quietly change WHETHER it is acted on -- only what
the contributor is told and which code the ledger records. It joins
CONCRETE_EVIDENCE_BLOCKER_CODES on the same footing as its sibling (two recorded
timestamps and a merged PR's own linked-issue set; no AI judgment) and
CONFIGURED_GATE_BLOCKER_SIGNAL_CODES so its reversals record under their own id.

Gated OFF by LOOPOVER_SUPERSEDED_CLOSE: recognising supersession CLOSES the pull
request, so it ships dark until shadow-checked against the held backlog.

Closes #10168
Drives the split through reReviewStoredPullRequest rather than against
buildPullRequestAdvisory directly, so the whole seam is proven: the linked-issue
verification pass carrying closure facts out, the bounded merged-rival read, the
flag, and the finding the contributor actually sees. Reproduces the Orb's own
collision -- PR 8886 citing issue 8829, rival 8881 merging behind it, the issue
closing one second later -- and pins that the feature is byte-identical with the
flag off.

The candidate-window derivation moves into the pure module beside the resolver
it serves, so 'can this even be superseded' is one tested unit instead of
branches stranded in processors.ts that only an integration test could reach.

Refs #10168
@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 67ae721 Commit Preview URL

Branch Preview URL
Jul 31 2026, 12:33 PM

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.70330% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.21%. Comparing base (3884485) to head (67ae721).
⚠️ Report is 8 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/queue/processors.ts 83.33% 1 Missing and 1 partial ⚠️
src/rules/advisory.ts 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10205      +/-   ##
==========================================
- Coverage   92.21%   92.21%   -0.01%     
==========================================
  Files         934      936       +2     
  Lines      114178   114249      +71     
  Branches    27593    27618      +25     
==========================================
+ Hits       105294   105359      +65     
  Misses       7582     7582              
- Partials     1302     1308       +6     
Flag Coverage Δ
backend 95.67% <92.18%> (-0.02%) ⬇️
engine 73.82% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ages/loopover-engine/src/advisory/gate-advisory.ts 81.67% <100.00%> (+0.61%) ⬆️
src/db/repositories.ts 96.79% <100.00%> (+<0.01%) ⬆️
src/review/linked-issue-hard-rules.ts 98.55% <100.00%> (+0.06%) ⬆️
src/review/linked-issue-superseded.ts 100.00% <100.00%> (ø)
src/settings/agent-actions.ts 98.31% <ø> (ø)
src/settings/superseded-close-mode.ts 100.00% <100.00%> (ø)
src/rules/advisory.ts 97.84% <80.00%> (-0.26%) ⬇️
src/queue/processors.ts 94.45% <83.33%> (-0.05%) ⬇️

... and 1 file with indirect coverage changes

loopover-orb Bot pushed a commit that referenced this pull request Jul 31, 2026
… of a 12-argument tail (#10214)

addPullRequestFindings exists twice, in the two deliberately-divergent advisory
twins, and both took the same POSITIONAL tail. Every new signal had to be
threaded in identical ORDER through two files -- a shape where transposing two
same-typed arguments compiles cleanly and silently changes a verdict. #10205
added the twelfth argument and paid that tax in both signatures and both call
sites; the next one would too.

Both copies now take one named object. Each declares its own local type rather
than sharing one: keeping these files free of a common import is precisely what
the divergence exists for (#4518, keep-divergent recorded for #4881), so the
engine still never reaches into the host's graph.

Pure refactor, and the evidence is that NOTHING ELSE MOVED: no finding added or
removed, no message text touched, no test edited. The diff contains zero changed
lines matching findings.push or a finding code literal. The existing advisory
suites on both sides, test:engine-parity and engine-parity:drift-check all pass
unmodified, which is the whole regression proof -- if an assertion had needed
editing, that would have meant behaviour changed.

Closes #10210
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.

gate: a PR superseded by a merged rival is reported as "No linked issue detected"

1 participant