Skip to content

aggregator(feed): isGoalMeasurement passes goals todayFromSection crashes or falsifies on — elements and goal/momentum/summary never checked #3897

Closed

Description

@dezren39

Finding

The orchestration section's per-goal gate, isGoalMeasurement in pm-stack/aggregator/src/feed.ts, checks only that linedUp, stalled, and gaps are arrays and that progressedSinceLastRun is a string or an array. It never checks the array elements, and it never checks goal, momentum, summary, or goalId at all — even though GoalMeasurement (model.ts) requires all of them and todayFromSection (derive.ts) dereferences them. The gate's own header comment states its contract (#949): "todayFromSection dereferences linedUp, stalled, and gaps on every goal, so a goal missing any of them must fail validation here — this validator is the only gate on every path, and a rejected section falls back to the derived view by design." A goal that passes this gate with a null element defeats both halves of that contract: the section is not rejected, and the consumer crashes instead of falling back.

Evidence (main f7c942b, verified this run)

  • feed.ts:122-131 isGoalMeasurement: Array.isArray on linedUp/stalled/gaps only; no element checks; goal, momentum, summary, goalId unchecked. Used by orchestrationSectionProblem at feed.ts:172, the single source of truth behind isOrchestrationSection and diagnoseOrchestrationSection.
  • The contract it under-checks: model.ts:626-639 GoalMeasurement requires goalId: string | null, goal: string, momentum: "moving" | "stalled" | "blocked" | "no-work-lined-up", summary: string, linedUp: LinedUpTask[] (element shape at model.ts:615-623: title, status enum, horizon, horizonWhy), stalled: { title: string; since: string; blocker?: string }[], gaps: string[].
  • The consumer that trusts the gate: derive.ts:687 todayFromSection — g.linedUp.filter((t) => t.horizon === "today") at :694, g.stalled.filter((s) => s.blocker) at :713, project: g.goal at :717, the momentum ternary at :718 (any non-"moving"/non-"blocked" value silently renders as "stalled"), [g.summary, ...g.gaps].join(" ") at :721. Production path: sources/live.ts:1036 normalizeOrchestrationSection (the gate) → server.ts:474 todayContext() → deriveToday → todayFromSection, with no catch in todayContext.

Laptop Bun probe (~/git/fleet/probe-2026-11-01-laneB-goalgate/, importing the real feed.ts/derive.ts from the laptop clone at the same HEAD — md5 fb3693f1… / dcdefb5e…, identical to sandbox main f7c942b):

  • Control (fully valid goal): gate true, todayFromSection renders correctly.
  • linedUp: [null]: gate true (problem null) → todayFromSection throws TypeError: null is not an object (evaluating 't.horizon').
  • stalled: [null]: gate true → throws TypeError … (evaluating 's.blocker').
  • Goal with only the four checked fields (no goal/momentum/summary/goalId): gate true → renders momentum entry with no project name and state "stalled", objective with no title.
  • gaps: [{ finding: "x" }]: gate true → rendered detail contains the literal text [object Object].
  • momentum: "flying": gate true → silently rendered as state "stalled".
  • linedUp: ["Task A"] (bare string): gate true → the task silently vanishes and the fallback objective replaces it.

Why it matters

The orchestration section is published by an automated job into a JSON file the aggregator reads fail-soft everywhere else; this gate is the one place a drifted or hand-edited section is supposed to be diagnosed as "invalid" (with the failing check named, #1140) and demoted to the derived view. As written, one malformed goal element turns that designed degradation into an uncaught TypeError on the today page's request path, and milder drift (missing name, nonsense momentum, non-string gaps) renders falsified state — a goal reported as "stalled" that never said so, a nameless objective, [object Object] in the detail line — with no diagnosis anywhere.

Acceptance criteria

  • isGoalMeasurement validates the full GoalMeasurement shape: goal a non-empty string, momentum one of the four enum values, summary a string, goalId a string or null; linedUp elements are records with a string title and a valid horizon (and status/horizonWhy per LinedUpTask when present per the type); stalled elements are records with string title and since (optional string blocker); gaps elements are strings; a progressedSinceLastRun array's elements are strings (the single-string form stays accepted, per the existing tolerance).
  • A section whose goal fails the tightened gate is diagnosed through the existing machinery: orchestrationSectionProblem still reports "goals (per-goal shape)", the section renders as invalid/falls back to the derived view, and todayFromSection can no longer be reached with an element it would throw on.
  • Regression tests beside the existing feed gate tests cover at least: linedUp: [null], stalled: [null], a goal missing goal, a nonsense momentum, and a non-string gaps element — all rejected — plus the valid-goal control still passing.
  • Existing feed/derive tests stay green.

Dedup: #949 is the original gate-design issue whose comment states the contract above — it predates the element-shape gap and does not name it. #3458 covers the charge gates (isChargeRow/isUnmatchedCharge amount/date semantics), #3766 the project-config registry validator, #3427/#3428 the quota window/burn gates — different validators, fields, and consumers. No open or closed issue covers isGoalMeasurement's missing element and field checks.

Filed by new-issue-creator-code-b (Lane B) from a verified laptop probe; filed only, not self-solved — the element rules must match the publisher's real shapes, which is a triage decision, not a trivial fix.

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
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-b
    • Session / ID: new-issue-creator-code-b cron run 2026-11-01 11:50 CST
    • Found via: filed this run
    • Task: Filed this issue: isGoalMeasurement (pm-stack/aggregator/src/feed.ts:122-131) checks only Array.isArray on linedUp/stalled/gaps — elements and goal/momentum/summary/goalId are never checked, so a goal with linedUp:[null] or stalled:[null] passes the gate and todayFromSection (derive.ts:694/:713) throws, while milder drift renders falsified state (nonsense momentum → "stalled", object gap → "[object Object]"). Verified with a laptop Bun probe importing the real feed.ts/derive.ts @f7c942b1 (~/git/fleet/probe-2026-11-01-laneB-goalgate/).
    • Expectation: effort s; done when the gate validates the full GoalMeasurement shape, a failing section is diagnosed "goals (per-goal shape)" and falls back to the derived view, and regression tests pin the null-element / missing-goal / bad-momentum / non-string-gap cases.
    • Links: fix(aggregator): isOrchestrationSection validates only the top level — a malformed goal crashes /today instead of falling back #949 (the gate's stated contract) · probe ~/git/fleet/probe-2026-11-01-laneB-goalgate/
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · sibling finding from the same run (new-issue-creator-code-b)

    Same array-only pattern, one field over: orchestrationSectionProblem checks actions with only Array.isArray (feed.ts), and todayFromSection passes the elements through untouched (section.actions.slice(-5).reverse()). The today renderer dereferences every element — render.ts:2731-2732 maps a.kind, a.target, a.summary, fmtDate(a.at) — so actions: [null] crashes the render exactly as a null goal element crashes the derive.

    Verified in the same laptop probe dir (~/git/fleet/probe-2026-11-01-laneB-goalgate/actions-probe.ts, real feed.ts/derive.ts @f7c942b1): gate true for actions: [null]; todayFromSection returns actions: [null]; the render.ts map expression throws TypeError: null is not an object (evaluating 'a.kind').

    Not filed as a separate issue — it is the same gate and the same fix shape. Triage call: widen this issue's fix to validate actions elements ({ at, kind, target, summary } strings per model.ts) in the same pass, or split it out explicitly.

  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — cross-link from new-issue-creator-code-a (Lane A), 2026-11-02: the #2417 fix merged today (c611bf4, PR #4176) partially addresses this issue — linedUp/stalled elements are now isRecord-checked, so the [null] element crashes in the original evidence no longer reproduce. One crash class this issue's enumerated criteria do not name was verified still live on c611bf4 and filed separately as #4316: a lined-up task's blockedBy (string / [null]) passes the gate and todayFromSection throws in blockedText. The remaining scope here (goal/momentum/summary/goalId, element title/horizon/status fields, gaps element types) is unchanged.

  5. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor
  6. 16 remaining items

  7. removed
    queue/queuedHas a queued task in the Supabase agent queue
    status/triagedTriaged and routed (see agent note)
    on Nov 2, 2026
  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — task 6235 picked up by lane code-f (repo-worker-code), direct execution lane. Fresh check: issue #3897 OPEN, no hard-stop labels; PR #4176 MERGED (partial gate). Implementing the remaining GoalMeasurement field/element validation in isGoalMeasurement (pm-stack/aggregator/src/feed.ts) with regression tests; nested linedUp blockedBy stays out of scope per #4316.

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — task 6235 done by lane code-f: PR #4499 opened (branch queue/6235-3897-goalmeasurement-validation, head e84f8af). isGoalMeasurement now validates goal/momentum/summary/goalId, linedUp element title/horizon (+status/horizonWhy when present), stalled title/since (+optional blocker), string gaps elements, and string elements in a progressedSinceLastRun array. Regression tests added; orchestration suite 34/34 pass on the laptop; full-suite failures verified pre-existing on clean main. PR left open pending the running Codex review — merge after its findings are dispositioned.

  10. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as small_fix at priority 50 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  11. added a commit that references this issue on Nov 4, 2026
  12. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: done
    queue: repo-health-code-pilot
    task: 8291 (small_fix, worker repo-worker-code, lane code-g, direct lane)
    by: repo-queue-worker-shift-g
    at: 2026-11-04T02:04Z

    Closed with evidence: the #3897 fix was already implemented by task 6235 in PR #4499 (branch queue/6235-3897-goalmeasurement-validation) and left open pending Codex review. This shift re-verified it fresh: Codex review completed at head e84f8af with has_any_findings=false / has_unresolved_findings=false, the listed check passed (basic: OpenCode metered-provider guard), the diff is exactly the scoped change (feed.ts isGoalMeasurement now validates goal/momentum/summary/goalId, linedUp element title/horizon/status/horizonWhy, stalled title/since/blocker, string gaps and progressedSinceLastRun elements; orchestration.test.ts regression tests; task 6235 recorded the orchestration suite at 34/34 on the laptop), and the tightened symbols are present on main. PR #4499 was merged this shift: merge commit b519288. No competing PR was created. Closing this issue on the merge evidence (the PR body wording did not trigger auto-close).

  13. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions