Skip to content

aggregator(feed): charge gates accept negative/zero amounts and non-date 'at' — isChargeRow and isUnmatchedCharge pass rows no producer can emit, falsifying the charges total and attention-flag severity #3458

Closed

Description

@dezren39

Finding

The feed's two charge gates — isChargeRow and isUnmatchedCharge in pm-stack/aggregator/src/feed.ts — check only the shape of a charge (strings present, amountUsd a finite number). They accept values no producer can emit: a negative or zero amountUsd, and an at that is not a date at all ("banana", "", 2026-02-30, 2026-13-01). Those rows survive loadFeed's drop-and-count sanitization and are rendered / converted to attention flags verbatim, so one drifted or hand-edited feed row falsifies the charges strip total and publishes a nonsense attention flag instead of being dropped and counted like other malformed rows.

Evidence (main d61aec4, verified this run)

  • feed.ts:350-353 isUnmatchedCharge: merchant string, isFiniteNumber(amountUsd), at string — no positivity, no date check.
  • feed.ts:358-368 isChargeRow: same shape for at/amountUsd.
  • Producer invariants the gates fail to mirror: charges-match.ts:195 filters candidates to amountUsd > 0 before any ChargeRow/UnmatchedCharge can exist; charges-collect.ts:42-51 requires at to start YYYY-MM-DD and slices it to 10 chars. A negative/zero amount or a non-date at in a feed is therefore always corruption, never data.
  • Consumers trust the gate: render.ts:1949-1952 renders c.at verbatim and totals usage.charges.reduce((sum, c) => sum + c.amountUsd, 0); sources/live.ts:899-904 maps each unmatched charge to an attention flag with detail: "$<amountUsd> on <at> …" and severity: u.amountUsd (flags sort by severity, derive.ts:885).
  • The shared real-date helper already exists and is already imported by feed.ts: isIsoDate (validate.ts:113, used for the orchestration section's generatedAt).

Laptop Bun probe (~/git/fleet/probe-2026-11-01-laneA-chargegate/, real feed.ts/charges-match.ts from origin/main — feed.ts md5 3bae075e identical at d61aec4 and at laptop HEAD 4501d41; probe copy differs from main by exactly the one missing } documented in #3452, diff shown in the probe dir):

  • Control: valid ChargeRow → true; valid UnmatchedCharge → true; producer matchCharges(+12.50) → 1 row.
  • isChargeRow: amount -5 → true, 0 → true, at: "banana" → true, "2026-02-30" → true, "2026-13-01" → true, "" → true.
  • isUnmatchedCharge: amount -5 → true, 0 → true, at: "banana" → true, "2026-02-30" → true.
  • Renderer expression on [12.50, -5] rows: total $7.50 instead of $12.50.
  • Live flag for the negative/banana unmatched row: detail $-5.00 on banana, severity -5.
  • Producer control: matchCharges(-5 candidate) → 0 rows — the pipeline itself never emits what the gate accepts.

Why it matters

charges / chargesAttention are the hub's charge-evidence surfaces (§2.4). #2445 hardened these gates against shape damage (a malformed row crashing /quota); this is the semantic remainder: a row that is well-shaped but impossible passes sanitization silently — no drop count, no provenance note — understates the period total, and turns into an attention flag with a negative severity and a nonsense date. The fix belongs at the same gate, so the existing drop-and-count machinery in loadFeed surfaces the loss exactly as it does for shape damage.

Acceptance criteria

  • isChargeRow and isUnmatchedCharge refuse amountUsd <= 0 and any at that is not a real ISO calendar date (the shared isIsoDate from validate.ts, or an equivalent real-calendar check — 2026-02-30 and 2026-13-01 must fail, not roll over).
  • Such rows drop through the existing loadFeed sanitization with their counts surfaced (chargesDropped / chargesAttentionDropped), never render, and never become attention flags.
  • Regression tests beside the existing feed gate tests cover at least: negative amount, zero amount, at: "banana", at: "2026-02-30", for both gates; the valid-row controls still pass and existing feed tests stay green.
  • Note: on current main this work sits behind aggregator(feed): feed.ts does not parse — isUnmatchedCharge is missing its closing brace (merged in 4d7f643e3, #2445) #3452 (feed.ts does not parse); the gate change and its tests should land on a feed.ts that parses.

Dedup: #2445 (closed) covered shape/crash only — its acceptance never mentions amount sign or date validity; #3428 is isBurnSegment (quota burn rows, different gate/fields); #3224 is the producer-side verb tier in produce-charge-candidates.ts; #2194/#2448 are producer-side date guards in charges-collect.ts. No open or closed issue covers the feed charge gates' amount/date semantics.

Activity

  1. developing-today commented on Nov 1, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. added
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    on Nov 1, 2026
  5. 14 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — state: PR opened (queue task 6105)

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Conductor binding · manual dispatch wave (Drew-ordered queue unblock) 2026-11-02

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · bound

    • Agent: fleet monitor
    • Session / ID: task 6105 · repo-fleet-monitor run 2026-11-02 10:12 CST
    • Found via: monitor reconciliation (Duty 1B, PR↔queue)
    • Task: Bound task 6105 to PR aggregator(feed): reject impossible charge rows #4424 — the PR implements this issue (head branch queue/6105-…) but the task sat queued unbound. Task is now parked (reason pr-bound-shepherd, ~30m) on the PR.
    • Pickup record: the PR was opened by a dispatcher session (queue task progress origin: openchamber-conductor, stage dispatched) whose binding never landed in the queue. The dispatcher session applied no lifecycle labels and posted no agent note on this issue — recorded as a protocol observation under protocol §9 (GitHub-native binding layer skipped).
    • Expectation: the resuming lane shepherds PR aggregator(feed): reject impossible charge rows #4424 — review CI and review comments, merge when green; do NOT re-implement.
    • Links: PR aggregator(feed): reject impossible charge rows #4424 · task 6105
  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/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Shepherd check for task 6105 (PR #4424): Codex review completed with no findings; CodeRabbit rate-limited with no actionable defect; gh pr checks showed no CI beyond CodeRabbit (skipping). Verified locally in pm-stack/aggregator: bun test test/feed.test.ts — 52 pass, 0 fail; bun build src/feed.ts --no-bundle — succeeds. Acceptance met: isChargeRow/isUnmatchedCharge require amountUsd > 0 and isIsoDate(at); valid rows pass; loadFeed drop-counts via chargesDropped/chargesAttentionDropped. Merged #4424 (merge commit 5c49f48); issue now closed.

  11. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — conductor dispatch binding (2026-11-02 10:52 CST cycle)

    Queue task 6105 (claimed atomically by the OpenChamber conductor, att 2/2) is bound to shepherd session ses_e5e62c71519ffeEx7dTe38pgze on the Workhorse route (opencode-route/workhorse), working in ~/git/fleet/6105-3458-charge-row-guard.

    Per the packet's resume brief, this is a shepherd dispatch for the existing PR #4424 — the implementing fix is already there (commit 7805af7); the session reviews CI + comments, verifies locally, and merges when green. It does not re-implement. Verified engaged by content minutes after launch: local checks pass (52 tests, build OK), PR mergeable, no blocking findings.

  12. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    queue/claimedQueue worker lifecycle state
    on Nov 2, 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/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/fixhas a fix task queued

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions