Repository navigation
Description
Summary
matchService and isSaasLooking in pm-stack/aggregator/src/charges-match.ts match on squashed substrings with no token boundary, so ordinary non-SaaS merchants whose names merely contain a marker or service name are classified as SaaS/AI charges — and, when a hub service carries that name, are relabelled as that service in the charges feed.
isSaasLooking(charges-match.ts:158-160):SAAS_MARKERS.some((marker) => flat.includes(marker))over the space-stripped merchant, with markers as short as 3–4 chars (XAI,META,GROK,CLINE).matchService(charges-match.ts:164-172): a service name/alias whose squashed form is ≥4 chars matches anywhere inside the squashed merchant (flat.includes(flatNorm)), so the serviceMetamatches any merchant containing the letters META.
Evidence (verified this run, main 0f84fe08; laptop probe ~/git/fleet/probe-2026-10-31-laneB-charges/probe.ts importing the shipped file — charges-match.ts md5 bec11791e5b91a32acf78529d4960087 identical in the sandbox and laptop clones)
normalizeMerchantName("METAMUCIL")→"METAMUCIL";isSaasLooking→ true;matchService(..., [{name:"Meta"}, ...])→ Meta. A fullmatchChargesrun on a single PlaidMETAMUCIL $12.50charge produces one row labelled "Meta" — a pharmacy product reported as a Meta AI charge.normalizeMerchantName("METAL ROOFING LLC")→"METAL ROOFING";isSaasLooking→ true;matchService→ Meta. A roofing contractor is likewise classified as a Meta charge.- Control (existing behaviour that must keep working):
OPENAI *CHATGPT SUBmatches the OpenAI service via its ChatGPT alias, andCURSOR.COMnormalizes toCURSOR(covered bytest/charges-match.test.ts).
Why it matters
The charges strip is financial evidence (see #1057/#2130): a mislabelled row attributes one vendor's charge to a hub service, and a false SaaS-looking flag pushes household merchants (pharmacy, trades) into the unmatched-SaaS attention strip. The failure is silent — the row looks authoritative, carries the service's name, and rerun determinism makes the wrong answer stable.
Distinct from #2130 (same module): that issue is the dedupe key collapsing distinct charges; this one is the classification match itself. #2130's acceptance criteria do not touch matching boundaries.
Acceptance criteria
- Service names/aliases and SaaS markers match only at token boundaries (a whole normalized token, or a whole multi-token sequence), never as a substring inside a longer token:
METAMUCILandMETAL ROOFINGmatch neither theMetaservice nor theMETASaaS marker. - Existing matches keep working and stay tested:
CHATGPT→OpenAI alias,CURSOR.COM→Cursor, multi-word service names, and the token-set path inmatchService. - New tests in
pm-stack/aggregator/test/charges-match.test.tscover the boundary cases above (prefix-containing merchants for at leastMETAand one more short marker), and all existing charges-match tests pass.
Related: #1057 (feature), #2130 (dedupe defect in the same module), #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 2 (Admission & classification) 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 rules this phase lands under: admission requires payment evidence (declined/failed and merely-due amounts produce no candidate; the amount is the amount paid — explicit paid/charged labels outrank a bare total, and a bare total must be a standalone label, never a substring of "Subtotal"); classification matches names/aliases/markers at token boundaries only, against the hub's whole catalog (SaaS services plus quota pools and their aliases, from their existing extractors). Phase 2 siblings: #3120, #3121, #3048, #3119. Phase 2 is independent of Phase 1. Details on #3161 and in files/designs/code-charges-pipeline-contract.md (goal workspace).
51 remaining items
dezren39 commentedon Nov 2, 2026
agent-note:v1 — holder: repo-worker-code (OpenChamber scheduled lane), at 2026-11-02T14:53:00Z. event: picked-up; task: 5620; queue: repo-health-code-pilot. Fresh metadata confirms #3048 OPEN and canonical PR #4306 OPEN, non-draft, authored/assigned to Drew. No PR contents/checks/reviews/branch/source inspected.
dezren39 commentedon Nov 2, 2026
agent-note:v1 — holder: repo-worker-code (OpenChamber scheduled lane), at 2026-11-02T14:54:00Z. event: parked; task: 5620; queue: repo-health-code-pilot. Canonical PR #4306 remains OPEN, non-draft, Drew-authored/assigned. Hard stop: this lane did not inspect or touch PR contents, checks, reviews, branch or source and did not create a competing PR. Resume after ownership release or PR disposition, then revalidate fresh.
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done