Repository navigation
Description
Summary
sameCharge in pm-stack/aggregator/src/charges-collect.ts decides that a hand-fed charges row duplicates an auto-matched row when their squashed normalized labels merely contain one another — no token boundary and no minimum length (charges-collect.ts:74-80). mergeChargeRows (:89-92) then silently drops the hand-fed row. With equal amounts and dates within the 3-day dedupe window, unrelated charges eat hand-fed rows:
- a hand-fed
Metarow is dropped because an auto row labelledMETAMUCILexists ("METAMUCIL" contains "META"); - a hand-fed
AIrow is dropped because an auto row labelledRAILWAYexists ("RAILWAY" contains "AI").
Evidence (verified this run, main 0f84fe08; laptop probe ~/git/fleet/probe-2026-10-31-laneB-charges/probe2.ts importing the shipped file — charges-collect.ts md5 246eca2b01496bf280146a2e506785d7 identical in the sandbox and laptop clones)
- Case A: auto
[{2026-10-06, "METAMUCIL", $20}]+ hand-fed[{2026-10-05, "Meta", $20}]→ merged output is only the METAMUCIL row; the Meta row is gone. - Case B: auto
[{2026-10-05, "RAILWAY", $10}]+ hand-fed[{2026-10-05, "AI", $10}]→ merged output is only the RAILWAY row. - Control (documented intent, must keep working): auto
"METAPAY"+ hand-fed"Meta PAYG spill", same amount/day → one row, the auto row (charges-collect.ts:68-73 names this pair as the reason for squash containment).
Why it matters
The merge's stated rule is conservative — "a hand-fed row is only dropped when an auto row plausibly IS that charge" (charges-collect.ts:70-73) — but substring containment with no floor is not conservative: short hand-fed labels (Meta, AI, any 2–4 letter service shorthand) are substrings of unrelated merchant names, so real hand-fed charges vanish from the feed with no trace. The feed is financial evidence (#1057); a silently missing row is the same failure family as #2130's under-count, by the opposite door.
Distinct from #3048 (same run): that issue is classification in charges-match.ts (matchService/isSaasLooking). This one is the hand-fed/auto merge in charges-collect.ts. Also distinct from #2194, which reaches sameCharge only via impossible calendar dates (NaN window); here the dates are valid and the containment alone drops the row.
Acceptance criteria
-
sameChargeno longer treats substring containment inside a longer token as a match: the Meta/METAMUCIL and AI/RAILWAY pairs above both keep their hand-fed rows. - The documented intended pair still merges: hand-fed
Meta PAYG spillagainst autoMETAPAY(same amount, within window) drops the hand-fed row. - New tests in
pm-stack/aggregator/test/charges-collect.test.tscover both false-merge pairs and the intended pair; all existing charges-collect and charges-match tests pass.
Related: #1057 (feature), #3048 (classification substring matching in charges-match), #2130 (dedupe collapse), #2194 (impossible dates in this file), #1047 (epic).
Activity
dezren39 commentedon Oct 31, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 31, 2026
🤖 Design-architect pass 27 (2026-10-31) — agent note: this issue is adopted as part of Phase 1 (Identity) of design #3161 ("a charge is evidence"). Triage: do NOT re-file, re-scope or split it — record the phase structure instead. The design records the shared identity rule this phase lands under: same charge only when provenance differs (cross-source for candidate dedupe; hand-fed vs auto for the merge), amounts equal, dates within the existing 3-day window, merchant identity at token boundaries (exact normalized equality for dedupe; token-boundary containment for the hand-fed merge, preserving the documented METAPAY / "Meta PAYG spill" pair), an empty normalized merchant never matches, and conflicting known payment methods veto the merge. Phase 1 siblings: #2130, #3049, #3123. Details on #3161 and in files/designs/code-charges-pipeline-contract.md (goal workspace).
9 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
ses_e5e648356c9ffeNBdFMCAFchFL· queue task56243049returned no PR. Design Design: a charge is evidence — one identity and lifecycle contract for the charges pipeline #3161 remains OPEN; prior triage already confirmed this task as its Phase 1 sibling fix, with aggregator(charges-match): service and SaaS-marker matching is substring-only — METAMUCIL and METAL ROOFING classify as Meta charges #3048/aggregator(charges-collect): parseChargeCandidates accepts impossible calendar dates — dedupe breaks and hand-fed rows are silently dropped #2194 separate.dezren39 commentedon Nov 2, 2026
agent-note:v1 — resumed
Agent: repo-worker-code (openchamber scheduled lane)
Task: repo-health-code-pilot task 5624 (small_fix, priority 50, attempt 2)
Started: 2026-11-02T09:49Z
State found: #3049 remains open with queue/claimed, status/in-progress, and work/assigned labels; no open PR mentioning #3049 was found. Existing task progress records the implementation and targeted tests already passing in /home/user/git/fleet/5624.
Plan: inspect the saved branch/worktree, verify the implementation and tests fresh, then open the canonical PR and complete as soon as the result is verified.
fix(aggregator): compare charge labels at token boundaries (#3049)
dezren39 commentedon Nov 2, 2026
agent-note:v1 — done (handed off to PR)
Agent: repo-worker-code (openchamber scheduled lane)
Task: repo-health-code-pilot task 5624 (small_fix, priority 50, attempt 2)
Result: PR #4307 opened at 3ead49a; issue remains open until merge.
Verification:
bun test test/charges-collect.test.ts test/charges-match.test.ts— 27 pass, 0 fail;git diff --checkclean. PR checks were pending at handoff; no CI wait performed.Labels: removed status/in-progress, queue/claimed, and work/assigned. Sandbox backlog/sweep bookkeeping deferred to fleet reconciliation per scheduled-lane protocol.
Merge pull request #4307 from developing-today/queue/5624-3049-charge…