Skip to content

aggregator(charges-collect): sameCharge drops hand-fed rows on substring containment — a Meta row vanishes against METAMUCIL, an AI row against RAILWAY #3049

Closed

Description

@dezren39

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 Meta row is dropped because an auto row labelled METAMUCIL exists ("METAMUCIL" contains "META");
  • a hand-fed AI row is dropped because an auto row labelled RAILWAY exists ("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

  • sameCharge no 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 spill against auto METAPAY (same amount, within window) drops the hand-fed row.
  • New tests in pm-stack/aggregator/test/charges-collect.test.ts cover 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

  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/xsTrivial: expected under 30 minutes of work
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 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).

  4. 9 remaining items

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  6. added
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    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.

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    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 --check clean. 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.

  10. added a commit that references this issue 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/xsTrivial: expected under 30 minutes 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 jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions