Repository navigation
Description
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-353isUnmatchedCharge:merchantstring,isFiniteNumber(amountUsd),atstring — no positivity, no date check.feed.ts:358-368isChargeRow: same shape forat/amountUsd.- Producer invariants the gates fail to mirror:
charges-match.ts:195filters candidates toamountUsd > 0before anyChargeRow/UnmatchedChargecan exist;charges-collect.ts:42-51requiresatto startYYYY-MM-DDand slices it to 10 chars. A negative/zero amount or a non-dateatin a feed is therefore always corruption, never data. - Consumers trust the gate:
render.ts:1949-1952rendersc.atverbatim and totalsusage.charges.reduce((sum, c) => sum + c.amountUsd, 0);sources/live.ts:899-904maps each unmatched charge to an attention flag withdetail: "$<amountUsd> on <at> …"andseverity: 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'sgeneratedAt).
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; producermatchCharges(+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.50instead 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
isChargeRowandisUnmatchedChargerefuseamountUsd <= 0and anyatthat is not a real ISO calendar date (the sharedisIsoDatefrom validate.ts, or an equivalent real-calendar check —2026-02-30and2026-13-01must fail, not roll over).- Such rows drop through the existing
loadFeedsanitization 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
developing-today commentedon Nov 1, 2026
CODE-1323
dezren39 commentedon Nov 1, 2026
🤖 Agent note · filed
atpass isChargeRow/isUnmatchedCharge) and route the fix (positive amount + shared isIsoDate check at both gates). Semantic remainder of aggregator(feed): isFleetFeed never checks charges — one malformed row crashes /quota, a non-array object silently empties the charges strip #2445's shape hardening; sits behind aggregator(feed): feed.ts does not parse — isUnmatchedCharge is missing its closing brace (merged in 4d7f643e3, #2445) #3452 (feed.ts parse) on main.at, rows drop-and-count in loadFeed, and regression tests pin them.14 remaining items
dezren39 commentedon Nov 2, 2026
agent-note:v1 — state: PR opened (queue task 6105)
dezren39 commentedon Nov 2, 2026
🤖 Conductor binding · manual dispatch wave (Drew-ordered queue unblock) 2026-11-02
small_fix— "Fix 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: reject impossible aggregator charge rows") ↔ issue 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 #3458ses_e5e63205c4affefTbzVosRS4oo(laptop OpenChamber, agent build)opencode-route/workhorse(route id only; pooled route, no single-plan direct seat). Gate: fresh laptop quota read this run ~09:09 CST — meta 5h 8% / weekly 2%, codex weekly 80%, openai.alternate 5h 0% / weekly 0% — Workhorse OPEN; plan quotas only, never PAYG.~/git/fleet/6105-3458-charge-row-guardon the laptop, branchqueue/6105-3458-charge-row-guard(from origin/main d1de7ee)queuedin the fleet queue (there is no targeted claim/binding verb yet — code#1337 / openchamber#1272); the dispatch is recorded in the task's progress. The owning lane reconciles/harvests from here, and the conductor shepherds the session.dezren39 commentedon Nov 2, 2026
🤖 Agent note · bound
queue/6105-…) but the task satqueuedunbound. Task is now parked (reasonpr-bound-shepherd, ~30m) on the PR.openchamber-conductor, stagedispatched) 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).dezren39 commentedon Nov 2, 2026
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.
dezren39 commentedon Nov 2, 2026
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_e5e62c71519ffeEx7dTe38pgzeon 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.