Skip to content

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

Description

@dezren39

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_request events carry the full PR object (merged, merged_at,
    draft).
  • issues events — and payload.issue in issue_comment events — carry the
    issue view of a PR, which has no merged, merged_at, or draft
    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:

  1. pull_request event, action closed, 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".
  2. issues event, action labeled, with the issue-shaped view of the same
    PR (pull_request key present, no merged/merged_at/draft keys,
    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 fires issues / issue_comment events. 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 an issues (e.g. labeled) event and an
    issue_comment event for the same PR, and the stored row keeps
    merged = 1 and its original merged_at after both.
  • Fields a payload shape does not carry (merged, merged_at,
    draft in 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 test in 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    effort/sSmall: expected under 2 hours of work
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Filed this run by the new-issue-creator (Lane B) after a verified laptop probe: a merged pull_request event stores merged=1/merged_at, then an issues labeled 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

  3. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    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 obj etc.), producing null for absent, and upsertItem's conflict clause applies COALESCE(excluded.x, pmp_items.x) for those three columns only — an absent field writes nothing, while a PR-shaped payload's genuine merged = 0 still updates the row. No other column changes update semantics. Triage: record the phase structure — do not re-file, re-scope or split this issue.

  4. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  5. 3 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage)

    Verification (fresh, 2026-11-02)

    Code, read from origin/main this run:

    • pm-stack/projector/src/projector.ts — fromIssueOrPr builds 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 (the issue object in issues / issue_comment events) never carry merged, draft, or merged_at, so a PR seen through one gets merged=0, draft=0, merged_at=null.
    • Same file — applyIssue and applyComment both upsert that row unconditionally; applyComment takes payload.issue ?? payload.pull_request, and for issue_comment on a PR that is the issue-shaped object (classified pr via its pull_request key, but with no merge fields).
    • pm-stack/projector/src/store.ts — the upsert's only guard is monotonicity on source_updated_at (WHERE excluded.source_updated_at >= pmp_items.source_updated_at). A post-merge comment/label event carries a newer updated_at, so the guard passes and the default-filled row overwrites the merged row. reconcile.ts is not in this path.

    Runtime reproduction (laptop, bun, existing fakeStore harness from pm-stack/projector/test/projector.test.ts, temp test deleted after the run): seed with a pull_request closed event (merged: true, merged_at: 2026-10-25T03:00:00Z) → row is merged=1. Then one issue_comment created event whose issue object omits the merge fields → row becomes merged=0, merged_at=null. The repro test failed exactly on the reset (expect(after.merged).toBe(1) received 0).

    Coverage gap confirmed: test/projector.test.ts has "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).

  8. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Agent note — task 6325 picked up (lane code-l, direct execution)

  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Agent note — task 6325 done (lane code-l)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions