Repository navigation
Description
Problem
The live source's two Project-pool consumers assume the fleet feed's
projects section is an array without ever checking:
(feed?.projects ?? []).filter(isProjectPoolItem)
(pm-stack/aggregator/src/sources/live.ts:703 in items() and
:742 in projects()). isFleetFeed
(pm-stack/aggregator/src/feed.ts:207-219) never checks projects
at all, so a feed whose projects is a string, object or number
passes the gate — and .filter does not exist on those values, so
both consumers throw a TypeError instead of serving an empty pool.
The entry-level filter works fine (a [null] pool is filtered to
empty); it is the container shape that is unguarded.
Evidence (verified this run, main 5a34fa70)
feed.ts:61-66:FleetFeed.projects?: ProjectPoolItem[]— absent
when the collector had no Projects token or the read failed.feed.ts:207-219:isFleetFeedcontains noprojectscheck.sources/live.ts:703:items()computes the pool with
(feed?.projects ?? []).filter(isProjectPoolItem)before any
other work;:740-743:projects()does the same.- Most
projects()call sites are defended
(server.ts:267,558,574,586,609,620,745use
.catch(() => []), and the provenance primer at:95-112uses
Promise.allSettled) — butitems()is not:boardContext()
(server.ts:222-227) androadmapContext()(:229-234) await
source.items(refs)with no catch, and they back the board page
(boardRender,:263-265),/today(todayContext,:306-307),
/roadmap(:479),/api/board.json(:644) and
/api/roadmap.json(:648). A throw initems()500s all of
them — the aggregator's main board surfaces, not just a pool facet.
Laptop Bun probe (~/git/fleet/probe-2026-10-29-laneA-feedpayg/
probe3.ts, the REAL liveSource built exactly as
test/projects.test.ts:158 builds it — real config.ts +
sources/live.ts from main 5a34fa70, md5-identical feed.ts,
Bun 1.4.2; feed files written to a temp dir):
projects in feed |
src.projects() |
src.items([]) |
|---|---|---|
| valid pool (control) | OK — 1 served | OK |
[null] (entry-level) |
OK — 0 served (filtered) | OK |
"oops" (non-array string) |
throws TypeError: (feed?.projects ?? []).filter is not a function |
throws same |
{...} (non-array object) |
throws same | throws same |
42 (number) |
throws same | throws same |
Why it matters
The collector writes projects from a GraphQL read that can fail or
drift independently of the rest of the feed; the section is documented
as absent on failure, but nothing enforces array-ness when it is
present-but-wrong. One bad container value takes down the board,
today and roadmap pages plus their JSON twins — a much larger blast
radius than the pool facet itself, and a different failure mode from
the entry-level gaps: here even the defensive .filter(...) call is
the crash site. Related but distinct: #2418 (entry-level —
isProjectPoolItem never checks labels inside an entry that
passes the filter) and the feed-gate siblings #2427/#2429/#2439/
#2445/#2446/#2450/#2451 (isFleetFeed never checks those sections at
all; their consumers dereference entries rather than calling
.filter on the container).
Acceptance criteria
- A feed passing
isFleetFeedwith a non-arrayprojects(string,
object, number) can no longer throw initems()orprojects():
the pool degrades to empty (the documented absent-section
behaviour), and the board / today / roadmap pages and their JSON
twins render normally. - The guard lives at a layer consistent with the feed-gate sibling
rulings (aggregator(feed): isFleetFeed never checks attention entries — one null entry crashes /attention + /today, entries without a string sessionId silently collapse into one #2427/aggregator(feed): isFleetFeed checks only that quota.pools is an array — burn/choices, pool entries and windows unvalidated, one malformed pool crashes /quota #2429/aggregator(feed): isFleetFeed never checks chargesAttention — one malformed entry crashes usage() and takes down /quota + the quota JSON twins #2439/aggregator(feed): isFleetFeed never checks charges — one malformed row crashes /quota, a non-array object silently empties the charges strip #2445/aggregator(feed): isFleetFeed never checks subscriptions — one null row crashes /quota #2446/aggregator(feed): isFleetFeed never checks payg — one malformed line crashes usage() and /quota #2450/aggregator(feed): isFleetFeed never checks saas — one malformed service crashes usage() and /quota, a non-array silently switches the whole usage view to fallback #2451): gate-level
array check inisFleetFeed, or anArray.isArrayguard at both
live-source consumers — pinned by triage, applied to both call
sites (live.ts:703and:742), not just one. - Regression tests cover:
projectsas a string, as an object, and
as a number —projects()returns[],items()resolves, and a
well-formed pool still serves its entries unchanged (the existing
test/projects.test.tspool test keeps passing).
Dedup
gh searches (open + closed) for projects filter aggregator,
payg, isFleetFeed and isFleetFeed never checks returned no
covering issue. #2418 is the entry-level labels gap inside
isProjectPoolItem — the filter this issue's consumers call — not
the container shape those consumers assume.
Activity
developing-today commentedon Oct 29, 2026
CODE-1253
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 1, 2026
Triage findings — task 3669
Already fixed on current main
2841b8de0; no follow-up fix task is needed.pm-stack/aggregator/src/feed.ts:259-264— sharedpoolFromFeedreturns[]for absent or non-arrayprojects, and filters array entries.pm-stack/aggregator/src/sources/live.ts:742-751(items()) and:795-801(projects()) both use this shared helper, so the optional feed section degrades to an empty pool without rejecting the rest of the feed.111a31806, part of impl(aggregator+collector) part 1 of 2: Project pool graph edges contract (split of #1122) #2399); that merge is an ancestor of current main. This pins the layer at the shared feed-pool reader, not whole-feed rejection inisFleetFeedor duplicated guards in each consumer.bun test test/feed.test.ts test/projects.test.ts(34 passed, 0 failed) and a realliveSourceprobe:projectsas string, object, or number returnsprojects() = []anditems()resolves.This issue was filed before #2744 landed; closing as already implemented.