Skip to content

Fix #6224: pm-stack(aggregator): a pool item with an invalid blockedBy/blocks edge  #6226

Closed

Description

@developing-today

Implement GitHub issue #6224 in developing-today/code: 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

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).

Rules from the repo owner: (1) Deliver the finished work as a pull request in this run; put "Fixes #6224" in the PR description so the issue closes on merge. (2) Cover the whole set in the one PR: if the issue names a set (files, subcommands, cases), fix every member, and batch any directly related dependency updates into the same PR — never one PR per item or per dependency. (3) Run the relevant tests and checks before opening the PR. (4) Merging is permitted but never required: merge only when checks pass and you are reasonably certain the PR is correct and low-risk — otherwise leave it open, and state your confidence and what a human should check. (5) If you hit a genuine blocker (missing credentials, a decision only the owner can make), stop and say exactly what is needed in your final summary instead of guessing.


Launched by pm-dispatch for developing-today/code. Queue task and OpenCode session ids follow in a comment below.

Activity

  1. developing-today commented on Nov 5, 2026

    @developing-today
    ContributorAuthor

    Dispatch launch confirmed.

    • Queue task: #10537 (repo-health-code-pilot, status: claimed, worker: dispatch-opencode)
    • OpenCode session: ses_e5e5424c12dffeJo6eWfbDGMMP
    • Launched by: bearer-token caller
  2. dezren39 commented on Nov 5, 2026

    @dezren39
    Member

    🤖 Agent note · phase pin for the implementation of #6224

  3. developing-today commented on Nov 5, 2026

    @developing-today
    ContributorAuthor
  4. 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
  5. developing-today commented on Nov 5, 2026

    @developing-today
    ContributorAuthor

    🤖 Agent note · parked

  6. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    on Nov 5, 2026
  7. 1 remaining item

  8. developing-today commented on Nov 5, 2026

    @developing-today
    ContributorAuthor

    🤖 Agent note · conflict resolved, parked on CI

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

    @dezren39
    Member

    🤖 Agent note · done — PR #6365 merged

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions