Skip to content

pm-stack(aggregator): a pool item with an invalid blockedBy/blocks edge 500s the project page and throws the brief — isProjectPoolItem validates edges by shape only, brief + pool-graph + epic-graph call refUrl unguarded #6224

Closed

Description

@dezren39

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:

  1. The shared pool gate admits it. isProjectPoolItem (pm-stack/aggregator/src/projects.ts:280-283) checks the edge fields only by shape: parentRef/epicRef are any string, blockedBy/blocks are any array of strings. There is no isValidRef check — 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 through isValidRef instead of a local shape check whose values parseRef (and refUrl) then throw on. So developing-today/code#99999999999999999999 (unsafe integer) and 812 (not repo-qualified) both pass the gate.
  2. 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.
  3. Three consumers call refUrl unguarded on those edges:
    • Per-item brief: buildItemBrief (pm-stack/aggregator/src/brief.ts:92-96) maps each blockedBy ref to target?.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) follows blockedBy edges and reports the bad ref in longestChain; poolGraphHtml (render.ts:6479) renders the chain with unguarded refUrl(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:, :6648 blocks:) render blockedBy/blocks refs with unguarded refUrl(b) — a member item with a bad edge 500s that epic's project page.

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 fails isValidRef never reaches a renderer: either isProjectPoolItem validates edge refs through isValidRef (bad item drops individually in poolFromFeed, per the gate's existing fail-soft contract), or the three consumer sites guard refUrl the way sessionRecordLine (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-qualified blockedBy, and unsafe blocks, 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    on Nov 5, 2026
  2. developing-today commented on Nov 5, 2026

    @developing-today
    Contributor
  3. added
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Nov 5, 2026
  4. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  5. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · adopted as a design phase

  6. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  7. 20 remaining items

  8. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  9. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 5, 2026
  10. developing-today commented on Nov 5, 2026

    @developing-today
    Contributor

    🤖 Agent note · handed off

  11. added a commit that references this issue on Nov 5, 2026
  12. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 5, 2026
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/codekind/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 jobstatus:donetask/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