Repository navigation
Description
Summary
The projector's reconcile poller can never learn a PR's draft / merged / merged_at — and for a PR it discovers first, it stores the wrong values permanently.
Both item streams in pm-stack/projector/src/reconcile.ts read the issues endpoint:
reconcile.ts:47-49— theissuesandprsstreams share one path:`repos/${repo}/issues?${q}&filter=all`; theprsstream only filters the same list to objects with apull_requestlink (:55-57).
Objects from that endpoint are issue-shaped for PRs: a pull_request link (whose nested object carries merged_at) but no top-level draft / merged / merged_at — the same shape the repo's own test documents for webhook issue objects (pm-stack/projector/test/projector.test.ts, the #4158 describe block: "it has a pull_request link but NO merged / merged_at / draft").
Since the #4158 fix (merged today as #4516), fromIssueOrPr (projector.ts:52-58) emits null for those three fields on issue-shaped payloads, and upsertItem (store.ts) preserves stored values for null. That is correct for webhooks. For reconcile it means:
- First sight materializes false defaults. A brand-new row INSERTs with
COALESCE(?,0)(store.ts, upsertItem insert step), so a draft PR reconcile discovers is storeddraft=0(ready for review) and a merged PR is storedmerged=0, merged_at=null(closed-unmerged). - Reconcile can never heal it. Every later poll re-reads the same issue-shaped objects, emits
nullagain, and the store preserves the wrong stored values. Only a laterpull_requestwebhook with a newerupdated_atcan correct the row — exactly the delivery reconcile exists to not depend on ("poll GitHub … so the projection self-heals beyond webhook delivery",reconcile.ts:1-4). - The truth is already in the same run, and is discarded. The
reviewsstream fetchesrepos/${repo}/pulls?per_page=100&state=all(reconcile.ts:128) — pulls-shaped objects that DO carrydraft/merged/merged_at— and uses them only to enumerate PR numbers for review fetching. No item row is upserted from them.
Evidence (verified this run, laptop probe)
Probe: ~/git/fleet/probe-2026-11-02-laneB-reconcileprs/ on the laptop — verbatim copies of pm-stack/projector/src/{reconcile,projector,store,github}.ts + schema.sql at main 4c42670d0 (md5-matched against the primary clone), driven through the real reconcileRepos with a fake GitHub fetch:
/issues?…returns two issue-shaped PR objects: Bump build/buck/preludes/buck2-prelude from3a02ea3toc77fc97#7 an open draft PR, Bump build/buck/preludes/buck2-prelude from3a02ea3to3e4d218#8 a merged PR (pull_request.merged_atnested in the link object only)./pulls?…(reviews stream) returns the pulls-shaped truth: Bump build/buck/preludes/buck2-prelude from3a02ea3toc77fc97#7draft:true, Bump build/buck/preludes/buck2-prelude from3a02ea3to3e4d218#8merged:true, merged_at:"2026-10-03T00:00:00Z".
Stored rows after reconcile run 1 and run 2 (identical):
PROBE run1 #7 draft/merged/merged_at: 0 0 null
PROBE run1 #8 draft/merged/merged_at: 0 0 null
PROBE run2 #7 draft/merged/merged_at: 0 0 null
PROBE run2 #8 draft/merged/merged_at: 0 0 null
Control: the repo's own reconcile.test.ts + projector.test.ts pass in the same probe directory (0 fail).
Why it matters
pmp_items.draft/merged/merged_at is what the aggregator/dashboard reads as fact about PR state. Any PR whose pull_request webhook was missed, predates the projector, or lands while webhooks are down is projected by reconcile with merge fields that are silently wrong — a merged PR reads as closed-unmerged and a draft reads as ready — and no amount of re-polling fixes it. This is the reconcile-side counterpart of #4158: that issue fixed issue-shaped payloads resetting known values; this one is reconcile never being able to learn them, because its item source is an endpoint that does not carry them.
Distinct from: #4158 (webhook reset, fixed), #4302/#4303 (the application contract and its writer-inventory audit — this is a concrete writer defect that inventory should disposition), #4210 (item deletion path).
Acceptance criteria
- Reconcile sources PR-shaped data for the
prsstream (e.g. upsert items from the/pullslist response it already fetches, or fetch pulls-shaped objects for the prs stream), so a reconcile-discovered PR is stored with its realdraft/merged/merged_at. - A PR first projected by reconcile as a draft reads
draft=1; a merged PR readsmerged=1with itsmerged_at; a second reconcile pass over unchanged data changes nothing. - A row previously materialized with the false defaults is healed by a later reconcile pass (the pulls-shaped upsert carries a non-null carrier signal, so the 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 preservation logic does not pin the wrong values).
- Regression tests in
pm-stack/projector/test/reconcile.test.ts(fake-GitHub style, like the existing streams tests) cover draft-first-sight, merged-first-sight, and heal-after-webhook-gap; the existing projector suite stays green. - Issues stream behavior is unchanged (plain issues still project from
/issues).
Activity
dezren39 commentedon Nov 2, 2026
🤖 Agent note · filed
3a02ea3toc77fc97#7 draft=0, Bump build/buck/preludes/buck2-prelude from3a02ea3to3e4d218#8 merged=0/merged_at=null); related 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, webhook case), Design: a projection writes only what the event proves — one application contract for the PM projector #4302, Phase 4 (audit) of #4302 — closing writer inventory + contract statement for the PM projector #4303, projector(items): an issues deleted event is upserted, not removed — deleted GitHub issues stay in pmp_items forever (no delete path exists, and reconcile only upserts) #4210dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked-up
43 remaining items
dezren39 commentedon Nov 3, 2026
agent-note:v1 — state: parked (task 6483, OpenChamber scheduled lane)
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
fix(projector): derive merged from merged_at for pulls-list payloads (#…
Merge pull request #4597 from developing-today/queue/6483-4528-reconc…
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done (task 6483, lane code-d)
mergedfrommerged_atfor pulls-LIST payloads in projector.ts (a09c6cd) — that fixed the Codex P1 on fix(projector): source prs stream from pulls endpoint so draft/merged heal (#4528) #4597 (real pulls-list objects carry nomergedboolean, so merged PRs would have stored merged=0 and could have overwritten webhook truth). Codex re-review of a09c6cd: no findings.