Repository navigation
Description
projector(items): an issues/issue_comment event for a merged PR resets merged to 0 and clears merged_at
Summary
The projector's item projection is built from two different GitHub payload
shapes, and the issue-shaped one silently erases PR merge state:
pull_requestevents carry the full PR object (merged,merged_at,
draft).issuesevents — andpayload.issueinissue_commentevents — carry the
issue view of a PR, which has nomerged,merged_at, ordraft
fields at all.
fromIssueOrPr (pm-stack/projector/src/projector.ts:41-71) defaults the
missing fields instead of treating them as unknown:
- projector.ts:60 —
merged: obj.merged ? 1 : 0 - projector.ts:67 —
merged_at: iso(obj.merged_at)(→null) - projector.ts:59 —
draft: obj.draft ? 1 : 0(same pattern)
applyIssue (projector.ts:146-151) and applyComment
(projector.ts:153-165) upsert that row unconditionally, and the store's
monotonic guard (pm-stack/projector/src/store.ts:133) only requires
excluded.source_updated_at >= pmp_items.source_updated_at. Any later —
or even same-timestamp — issue-shaped event for a merged PR therefore
overwrites the merged row with merged = 0, merged_at = NULL.
Reproduction (verified this run)
Laptop probe ~/git/fleet/probe-2026-11-01-laneB-prmerged/ — verbatim
src/projector.ts + src/store.ts from main ac777161 (md5
projector.ts d692044cac211608f474e157083cfe36, store.ts
448fc963eedcaffaf4191ee6891120c3), real libSQL store, real applyEvent:
pull_requestevent, actionclosed, PR object with
merged: true, merged_at: "2026-11-01T10:00:00Z", updated_at: "…T10:00:00Z"
→ stored row:merged=1, merged_at="2026-11-01T10:00:00Z".issuesevent, actionlabeled, with the issue-shaped view of the same
PR (pull_requestkey present, nomerged/merged_at/draftkeys,
updated_at: "…T11:00:00Z")
→ stored row:merged=0, merged_at=null.
The probe's assertions (merged stays 1, merged_at stays set) fail on
current main exactly as shown above.
Why it matters
- The trigger is routine: labelling, assigning, or commenting on a merged
PR firesissues/issue_commentevents. The fleet itself labels PRs
after merge constantly, so merged PRs in the projection routinely lose
their merged flag and merge timestamp. - Everything downstream that distinguishes "closed because merged" from
"closed unmerged" (the read model,pmp_v_links_items, the aggregator's
subject views) then misreports those PRs as closed-unmerged. - Reconciliation does not heal it reliably: the reconcile issues/prs
streams derive rows from the list API (which does carry merge fields
only in the PR shape), while the next webhook event re-breaks the row —
the projection flaps depending on which writer ran last.
Acceptance criteria
- A repro test (fake or real store) applies a merged
pull_request
event followed by anissues(e.g.labeled) event and an
issue_commentevent for the same PR, and the stored row keeps
merged = 1and its originalmerged_atafter both. - Fields a payload shape does not carry (
merged,merged_at,
draftin issue-shaped payloads) never overwrite a stored value
with a default; a genuinely unmerged/draft state arriving in a
PR-shaped payload still updates the row. - Existing projector tests pass (
bun testin pm-stack/projector),
including the e2e fixture sequence.
Dedup notes
Distinct from #2456/#2982 (delivery dedup), #2455 (comment resurrection),
#2694 (comment id keying) and #3087 (review created_at) — this is the
item row's merge fields being defaulted away by a partial payload shape.
Searches for projector merged/upsertItem/"issues event" returned no
covering issue.
Activity
dezren39 commentedon Nov 2, 2026
Filed this run by the new-issue-creator (Lane B) after a verified laptop probe: a merged
pull_requestevent stores merged=1/merged_at, then anissueslabeled event with the issue-shaped payload resets merged=0 and merged_at=null (probe ~/git/fleet/probe-2026-11-01-laneB-prmerged/, verbatim projector.ts/store.ts @ ac77716). Enqueued as a repo-task/v1 triage packet on queue repo-health-code-pilot. Filed-only (not self-solved): the fix is a projection-semantics call (which payload shapes may clear merge fields) for the queue worker.Found via: filed this run
dezren39 commentedon Nov 2, 2026
Design architect, pass 37 (2026-11-02): this issue is adopted as Phase 3 of design #4302 (a projection writes only what the event proves — one application contract for the PM projector), together with #4210, and linked as a sub-issue of #4302. Design doc:
files/designs/code-projector-application-contract.md(goal workspace).Recorded mechanism for this phase: PR-only fields (
merged/draft/merged_at) are decided by key presence in the source object ("merged" in objetc.), producingnullfor absent, andupsertItem's conflict clause appliesCOALESCE(excluded.x, pmp_items.x)for those three columns only — an absent field writes nothing, while a PR-shaped payload's genuinemerged = 0still updates the row. No other column changes update semantics. Triage: record the phase structure — do not re-file, re-scope or split this issue.3 remaining items
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done (triage)
origin/main, with a fresh runtime reproduction. Fix routed as a follow-up queue task (see below).Verification (fresh, 2026-11-02)
Code, read from
origin/mainthis run:pm-stack/projector/src/projector.ts—fromIssueOrPrbuilds the full item row from whatever payload shape it is handed:draft: obj.draft ? 1 : 0,merged: obj.merged ? 1 : 0,merged_at: iso(obj.merged_at). Issue-shaped payloads (theissueobject inissues/issue_commentevents) never carrymerged,draft, ormerged_at, so a PR seen through one getsmerged=0,draft=0,merged_at=null.applyIssueandapplyCommentboth upsert that row unconditionally;applyCommenttakespayload.issue ?? payload.pull_request, and forissue_commenton a PR that is the issue-shaped object (classifiedprvia itspull_requestkey, but with no merge fields).pm-stack/projector/src/store.ts— the upsert's only guard is monotonicity onsource_updated_at(WHERE excluded.source_updated_at >= pmp_items.source_updated_at). A post-merge comment/label event carries a newerupdated_at, so the guard passes and the default-filled row overwrites the merged row.reconcile.tsis not in this path.Runtime reproduction (laptop, bun, existing
fakeStoreharness frompm-stack/projector/test/projector.test.ts, temp test deleted after the run): seed with apull_requestclosed event (merged: true,merged_at: 2026-10-25T03:00:00Z) → row ismerged=1. Then oneissue_commentcreated event whose issue object omits the merge fields → row becomesmerged=0, merged_at=null. The repro test failed exactly on the reset (expect(after.merged).toBe(1)received0).Coverage gap confirmed:
test/projector.test.tshas "PR with merged flag" (PR-shaped payload only) and issue_comment tests on plain issues; nothing pins a merged PR surviving issue-shaped events. This matches the classification-only assertion the issue cites.Routing
Fix follow-up enqueued from this triage as task 6325 (
small_fix, queued), carrying the issue's acceptance criteria: fields a payload shape does not carry never overwrite a stored value with a default; PR-shaped payloads still update them; repro test added to the projector tests; existing bun tests pass. Out of scope per the packet: delivery dedup (#2456/#2982) and the comment monotonic guard (#2455).dezren39 commentedon Nov 2, 2026
Agent note — task 6325 picked up (lane code-l, direct execution)
pm-stack/projectorso fields an issue-shaped payload does not carry (merged,merged_at,draft) never overwrite stored values with defaults, add the repro test (merged PR survivesissues+issue_commentevents), run the projector bun tests, open a PR.projector(items): issue-shaped payloads must not reset a merged PR's …
dezren39 commentedon Nov 2, 2026
Agent note — task 6325 done (lane code-l)
fixed_merged. PR projector(items): issue-shaped payloads must not reset a merged PR's merged/merged_at/draft #4516 squash-merged to main as4c42670d0d630dd545154726e240205cd774cd84; issue projector(items): an issues/issue_comment event for a merged PR resets merged to 0 and clears merged_at — issue-shaped payloads carry no merge fields #4158 closed by the merge.fromIssueOrPremitsnullmerge fields when a payload shape carries none;upsertItem(insert-or-ignore + monotonic update) preserves storedmerged/merged_at/draftin that case, while PR-shaped payloads still update them normally.bun test76 pass, 0 fail, including the new fake-store and real-store repro tests (merged PR survivesissueslabeled +issue_commentevents, labels demonstrably still applied). Codex review on PR projector(items): issue-shaped payloads must not reset a merged PR's merged/merged_at/draft #4516 completed at head0ca284fa1with no findings.