Repository navigation
Description
Summary
A Project-pool item whose blockedBy / blocks edge carries a ref parseRef rejects breaks three aggregator surfaces at render/derive time. The chain has two mismatched layers:
- The shared pool gate admits it.
isProjectPoolItem(pm-stack/aggregator/src/projects.ts:280-283) checks the edge fields only by shape:parentRef/epicRefare any string,blockedBy/blocksare any array of strings. There is noisValidRefcheck — exactly the disagreement model.ts:463-466 documents for the shared rule (pm-stack(aggregator): validateNewComment/validateNewAction accept ref keys parseRef rejects (GH_REF number unbounded) #4952): a boundary validator must route throughisValidRefinstead of a local shape check whose valuesparseRef(andrefUrl) then throw on. Sodeveloping-today/code#99999999999999999999(unsafe integer) and812(not repo-qualified) both pass the gate. - The feed path trusts it.
poolFromFeed(pm-stack/aggregator/src/feed.ts:466-470) keeps any item passing that gate, so a feed-published pool section carrying such an edge reaches every consumer verbatim. - Three consumers call
refUrlunguarded on those edges:- Per-item brief:
buildItemBrief(pm-stack/aggregator/src/brief.ts:92-96) maps eachblockedByref totarget?.issueUrl ?? refUrl(ref, "issue")— an out-of-pool bad blocker throws, so the brief (and any page/API building briefs for the pool) fails. - Project page, pool graph section:
blockedSummary(graph.ts) followsblockedByedges and reports the bad ref inlongestChain;poolGraphHtml(render.ts:6479) renders the chain with unguardedrefUrl(r)— one bad edge 500s the whole project page. - Project page, per-epic graph: the ranked list (render.ts:6590) and the dependency list (render.ts:6645
blocked by:, :6648blocks:) renderblockedBy/blocksrefs with unguardedrefUrl(b)— a member item with a bad edge 500s that epic's project page.
- Per-item brief:
Evidence (verified this run, laptop, aggregator sources at origin/main 04f7869)
Probe ~/git/fleet/probe-pool-edges/probe.ts (bun, real aggregator sources, run synchronously):
isValidRef BAD_UNSAFE: false | BAD_PLAIN: false | GOOD: true
gate blockedBy=[BAD_UNSAFE]: true
gate blockedBy=[BAD_PLAIN]: true
gate blocks=[BAD_UNSAFE], parentRef=BAD_PLAIN: true
poolFromFeed keeps bad item: 1
brief BAD_UNSAFE blocker (out of pool): THREW: ref number is not a safe integer: developing-today/code#99999999999999999999
brief GOOD blocker control: OK len=49
blockedSummary chain with BAD: ["developing-today/code#1","developing-today/code#99999999999999999999"]
projectPage graph BAD_UNSAFE chain: THREW: ref number is not a safe integer: developing-today/code#99999999999999999999
projectPage graph GOOD chain control: OK len=34510
projectPage epic BAD_PLAIN blocker: THREW: not a repo-qualified ref: 812
projectPage epic GOOD blocker control: OK len=38578
BAD_UNSAFE is developing-today/code#99999999999999999999; BAD_PLAIN is 812; controls use developing-today/code#3 in the same positions and render/derive cleanly.
Why it matters
The pool's edge fields are the native dependency graph the dashboard is built on (#2399 contract): the collector writes them from GitHub's native relations, but the feed is a published artifact other producers can write, and the gate is explicitly the shared contract both sides must not drift from (feed.ts:440-446). One malformed edge in one feed item — a hand-edited feed, a buggy producer, a non-GitHub dependency written as a bare number — permanently breaks the brief for that item and 500s the affected project pages until the feed is fixed by hand. Siblings already filed cover other producers of the same throw (projector registry links #6211 — which covers render.ts:6741 roadmap rows and /search, not these sites; lined-up titles #6210/#6195; objectives #6174; overview #6205), and #5202 covers a different field of this same gate (updatedAt). None covers the pool edge fields or the brief / pool-graph / epic-graph consumers. The session-record path (render.ts:1912) already shows the house pattern for untrusted refs: try/catch and omit only the bad link.
Acceptance criteria
- A pool item whose
blockedBy/blocks(and, for consistency,parentRef/epicRef) edge failsisValidRefnever reaches a renderer: eitherisProjectPoolItemvalidates edge refs throughisValidRef(bad item drops individually inpoolFromFeed, per the gate's existing fail-soft contract), or the three consumer sites guardrefUrlthe waysessionRecordLine(render.ts:1912) does. The gate layer is preferred — it is the shared contract — but stored/in-memory pools that bypass the feed must not stay lethal, so a consumer-side guard in addition is welcome. - The brief, the project page's pool-graph section, and the per-epic graph all survive a pool item carrying an unsafe-integer or non-repo-qualified edge: the item renders, the bad edge is omitted or shown unlinked, valid edges still render as links.
- Regression tests alongside the existing aggregator suites (graph / epic-graph / brief / projects gate tests) pinning: unsafe-integer
blockedBy, non-repo-qualifiedblockedBy, and unsafeblocks, each with a valid-ref control. - Existing behavior unchanged for valid refs (controls above).
Related: #6211 (projector registry links; render.ts:6741 + /search), #6210, #6195, #6174, #6205, #5202 (same gate, updatedAt field), #4952 (the shared ref-validity rule), #2399 (pool edge contract).
Activity
developing-today commentedon Nov 5, 2026
CODE-1821
dezren39 commentedon Nov 5, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 5, 2026
🤖 Agent note · adopted as a design phase
files/designs/code-aggregator-ref-contract.md(keep-the-four-repos-healthy goal workspace).20 remaining items
dezren39 commentedon Nov 5, 2026
🤖 Agent note · handed off
developing-today commentedon Nov 5, 2026
🤖 Agent note · handed off
opencodeMerge pull request #6365 from developing-today/queue/10537-6224-pool-…
dezren39 commentedon Nov 5, 2026
🤖 Agent note · done