mattrguerra/meridian-outreach-vaultmainPrivate on GitHub, published here in full

DECISIONS.md

Language Markdown23.4 KB125 lines

DECISIONS.md

  1. ---
  2. type: decisions
  3. scope: project-root
  4. tags: [claude, outreach, decisions]
  5. created: 2026-06-16
  6. updated: 2026-08-12
  7. ---
  8. # DECISIONS
  9. ## 2026-08-12: Directly-partnered accounts are never cold-outreached (the never-contact domain rule)
  10. **Decision (Matt).** Clients on an `@harlowridge.example` address are a category of their own. **Harlow Ridge Homes is Rhodium regardless of per-mailbox spend, and receives no outreach email of any kind.** The relationship is already handled directly (Hazel speaks with them every other day), so a check-in or winback email would be reaching out to people we are already reaching out to, and would read as though nobody looked.
  11. **Why it needed a rule and not a one-off fix.** Six Harlow Ridge mailboxes existed in the vault, **about $19.9K combined lifetime value**, and none were suppressed. Every one was mis-tiered (one Palladium, one Platinum, four below the partner line with a blank computed tier), because the driver tiers each *mailbox* on that mailbox's own spend, so one Rhodium relationship looked like a scatter of small accounts, and the four below the line had no chunk in the XML census at all.
  12. **The mechanism: a `domains` list in `07-Automation/data/suppression_list.json`.** Name-shaped screening (`ROLE_NAME_RE`) catches a "Purchasing Dept" or a test account but is blind to a *domain* that should never be contacted. That blindness had already produced one near-miss: **a staff member's own `@meridiandisplay.example` account ranked #6 by LTV among the dormant partner clients** and was caught by hand, not by the screen. Both domains are now on the list, and `route` excludes any client whose email domain matches, with an explicit reason string so the exclusion is auditable rather than silent.
  13. **Tiering precedent, narrow on purpose.** This resolves the org-vs-mailbox question *for Harlow Ridge only*. It does not settle the general tiering policy still open in [[HEARTBEAT]]: whether an org tier attaches to the organisation record or to every mailbox on the domain, which changes how many clients the program counts. Harlow Ridge is decided because Matt decided it, not because the general rule was worked out.
  14. **Harlow Ridge is the only member, confirmed.** A sweep of every non-free email domain by combined lifetime value was run the same day and reviewed by Matt: at about $19.9K combined, Harlow Ridge is nowhere near the top (single-client domains run past $110K), which is exactly why size cannot find these accounts, and **Matt confirmed no other account gets this treatment**. The `domains` list is therefore `harlowridge.example` (partner) and `meridiandisplay.example` (internal), and nothing else. Candidates that were looked at and deliberately **not** added: a regional credit union, an insurance agency, a youth sports league and a church, plus a robotics company and an agency whose mailboxes were all already in pipelines.
  15. **Do not infer new members from data.** Size cannot tell which accounts are handled relationally; that is knowledge Matt, Hazel and Ines hold. A domain joins this list only when Matt says so.
  16. ---
  17. ## 2026-08-11: One enrichment skill for both operators (`client-enrich` is canonical)
  18. **Decision (Matt).** `client-enrich` is the single enrichment skill for this vault. `client-enrich-hazel` is retired: the skill will be shared to Hazel rather than forked, now that skill sharing exists.
  19. **Why there were two.** A diff showed the fork carried no behavior of its own: all five helper scripts byte-identical, and the SKILL.md differing in exactly three places (the `name:` field, the H1, and "Use when *the user* says" in place of "Use when *Matt* says"). Even the LEDGER attribution step was identical, and it already resolves both operators from the session email. The fork looks like it existed to distribute the skill before sharing was available, not to do anything different.
  20. **Why `client-enrich` is the survivor.** It is the maintained copy (four-funnel scope, suppression check, flat `client-stubs/` paths); the fork was frozen 2026-07-15 and still pointed at `03-Clients/Check-In/`, deleted in the 07-23 backfill. Its name is also operator-neutral: three people work this vault ([[HEARTBEAT]], `08-Planning/`), and a shared skill named for one of them mislabels itself. And the provenance already names it: every enriched stub carries `<!-- Enriched via client-enrich -->`, and the [[LEDGER]]'s enrichment entries reference it.
  21. **The forking cost, for next time.** A copy freezes on the day it is made and drifts silently. Hazel's went two and a half weeks stale against a vault reorganization and, more seriously, predated the suppression list: a batch run from that copy could have spent Apollo credits researching opted-out clients. Prefer sharing one skill; fork only when the behavior genuinely differs.
  22. **Follow-through owned by Matt:** share `client-enrich` to Hazel, have her enable it and check whether her project's `CLAUDE.md` names `client-enrich-hazel`, then delete the fork.
  23. ---
  24. ## 2026-08-10: The outreach audit joins the weekly run, and what counts as "contact"
  25. **Scope corrected same day:** Matt confirmed the audit covers FOUR funnels: 24 Partner Tier Upgrade, 26 Winback, 36 Quarterly Check-In, 16 Abandoned Carts. The first pass also swept 28 (7 Days Delivery Up-Keep) and 30 (Cold Outreach), because the originating question was "look in all of the pipelines": a fair reading for a one-off audit, and the wrong basis for a standing weekly job. Lesson: a scope that came from an ad-hoc question must be re-confirmed before it becomes recurring. The cat-28 findings below are still true; they are just no longer tracked weekly.
  26. **Decision.** Fold the per-pipeline outreach audit into the weekly automation as Stage F5, so the run emits two spreadsheets: the Drive-swap partner roster and `Bitrix_Pipeline_Outreach_Audit_<date>.xlsx`. Implemented as two new driver subcommands (`outreach-pull-list`, `outreach-audit`); Claude still does the Bitrix reads and hands the script JSON, the same contract as `ingest` and `reconcile-deals`.
  27. **Why a stage-history sweep at all.** Bitrix exposes no last-modified filter through the tools we have, and the XML census only records each deal's *current*-stage entry date: it reported zero activity for the week of 2026-07-27, a week in which clients had actually been emailed. Per-deal stage history is the only reliable source for "when was this person last contacted".
  28. **Contact is defined by token, never by stage name** (`SEND_TOKENS`). Only entries with a real outbound email, call or LinkedIn DM behind them count; system reactions (No - Reply, No Reply Nth Email (Auto), Email Defunct, Changed Companies, Not Interested, Completed) do not. Two traps forced this: **cat 28 `NEW` "Pre-Call Email" is the funnel's creation stage and sends nothing**, while a later cat-28 stage is the actual send (verified against outbound emails to the minute); and **cat 26 `FINAL_INVOICE` is also called "Pre-Call Email" and IS a send**. Identical labels, opposite meanings, different funnels.
  29. **Six retired stages are named in code on purpose.** They were deleted from their funnel configs, so Bitrix returns a bare token and a naive audit drops them silently. Identified by matching each stage-entry timestamp against the deal's outbound email timeline: cat 24 `PREPARATION` (Email Sent), `UC_CEAGAV` (2nd reachout, call + email) and `UC_V6JARB` (3rd reachout follow-up) are sends; `UC_N4JCMW` is a bulk move with no email behind it. Cat 26 `UC_PMLDJW` / `UC_GDHDJR` are the February 2026 feedback-survey send and its reminder: for some Winback clients that survey was their only contact ever. Without `RETIRED_STAGE_NAMES` everyone reached only through these stages reads as never contacted.
  30. **Incremental by terminal stage.** A full sweep is one `get_stage_history` call per in-scope deal; repeating that weekly is wasteful. A deal is served from `07-Automation/data/outreach_touch_cache.json` only when it is cached, in a terminal stage, and that stage has not changed since the cache was written. The cache is seeded with every verified deal, so an automated run pulls only the deals that can have changed (in the sample, the second run pulls 18 of 54). Accepted residual risk: a deal that moved out of and back into the same terminal stage between runs would be missed; it self-corrects the next time its stage changes.
  31. **Touch count is best-effort; the date is not.** Bitrix workflows re-enter a stage several times in one minute, so same-stage entries within `SAME_SEND_GAP_MIN` collapse to one send. A bulk re-stage days apart still counts as a separate touch. The workbook says so, and "Last reached out" is the column to trust.
  32. **Reading Bitrix is not routing it.** F5 reads stage history and deal records and writes nothing back. The standing hold in [[pipeline-routing-rules]] is on *creating deals and moving stages*; that stays human-gated, and the audit must never "correct" a stage it thinks is wrong.
  33. **Also corrected here:** the 2026-08-06 cat-36 rename was logged against the wrong tokens. Live `pipeline_summary` shows `C36:NEW` = "Active Check-In List" and `C36:UC_LXCMAV` = "Dormant"; there is no "Partnership Flags" stage. The driver had `NEW` = "Dormant", so the 08-06 reconcile mislabelled the census blocks of clients sitting in Active Check-In List. Fixed in `STAGE_NAMES`; the next `reconcile-deals` overwrites them.
  34. ---
  35. ## 2026-08-06: Routing goes live (execution model)
  36. - **Winback loads now, in bulk, into the cat 26 Client bucket** (Matt). Safe because the bucket is non-automated; Nora's stage move is the send and the throttle. List refreshed weekly.
  37. - **Cat 24 refresh becomes a weekly-automation duty**: the driver will gain a routing stage (Matt authorized revisions).
  38. - **New "Dormant" first stage in the Quarterly Check-In flow** (Matt creates it in Bitrix): all check-in-eligible clients park there; Matt feeds N at a time, longest-since-last-outreach first; N set with Ines. Winback clients never cross into it until they place a new order.
  39. - **"Not Interested" = a full email opt-out** (Matt, same day, correcting the v1 list): a client whose funnel history closed at a Not Interested stage asked to stop receiving emails, so they are suppressed from EVERY pipeline permanently, not merely flagged.
  40. - **First candidate list, v2** (the 08-06 routing-candidates workbook, since removed): Winback by far the largest list, then the cat 24 first touches and a short Dormant queue; excluded were the opt-outs, the open deals (including the bulk tier-change cohort) and winback-done-this-spell. The sample's first run has the same shape: Winback 50, cat 24 29, Dormant queue 4, excluded 45. Winback sorted by lifetime value desc; Dormant queue longest-since-outreach first. (v1 was superseded within the hour.)
  41. ## 2026-08-06: Census to Bitrix reconcile rules (pipeline blocks + bitrix_id)
  42. - **Closed deals belong in the census.** Pipeline blocks are written for won AND lost deals, not just open ones: a lost Winback deal is the "one touch per dormancy spell" suppressor, and any deal is "previously contacted" evidence. Status vocabulary: `active` / `completed` (won) / `lost` (lose-class stages, incl. C36 "Not Interested").
  43. - **One block per (client, funnel), recording the newest deal.** Multiple deals in the same funnel over time keep the latest; older deals stay in Bitrix and in that day's raw deal pull (since pruned).
  44. - **Deal to client matching is by exact normalized name** (deal title = client name per the naming convention). Deals matching no chunk or multiple chunks (almost all duplicate-account pairs from the 2026-07-29 duplicate-account review) were NOT attached; they were parked in a review note in `04-Working/` (since removed). The ambiguous ones resolve when the merge policy lands.
  45. - **`bitrix_id` = the contact the client's live deal is attached to.** Email search alone is unreliable (duplicate contacts per email are the norm, sometimes several for one address); the deal linkage is authoritative and corroborated both of the ids hand-verified on 07-24. Every unverified value (they all held Plinth or tracker ids) was blanked: a blank now means "not verified against the CRM yet", never a wrong id.
  46. ## 2026-08-06: Pipeline routing rules ratified (+ cat 24 rename)
  47. - **Source:** the operations team's SOP for pulling the quarterly check-in and winback lists (a Google Doc from Ines and operations, 06-2026, not included), with Matt's rulings resolving its ambiguities. Canonical rule set: [[pipeline-routing-rules]] (07-Automation). Nothing routes automatically yet.
  48. - **Winback threshold = 12+ months without an order, on the lifetime-partner base.** The doc contradicts itself (its summary table says 12 months; every operational step computes 180 days); Matt ruled 12 months. The $2,500 floor is the Partner Program definition (lifetime), not the trailing-window spend the manual spreadsheet actually computed. Consequence accepted: decade-dormant partner clients DO qualify for one winback touch, which also answers the recency question from the 2026-07-29 duplicate-account review (dormant clients get a winback, not ongoing partner cadence; their TIER is a separate, still-open question).
  49. - **One winback touch per dormancy spell** (Matt): once reached out to, a still-dormant client is not contacted again, by any pipeline, until they place an order. This forces Check-In to require an order within 12 months, otherwise winbacked clients would leak into the quarterly cadence.
  50. - **Precedence: Winback > Check-In; one open outreach pipeline per client at a time.** Opt-outs never route (first entry: one client's explicit removal request, 2026-07-30).
  51. - **Cat 24 renamed "Partner Tier Upgrade"** (formerly "Partner Outreach"): a Bitrix-side rename, same categoryId 24; the `partner_outreach` XML/stub key and the skill wiring stay. Scope widened per Matt: **first touch AND tier crossings** (including crossings produced by approved merges).
  52. - ~~Claude's encoding, flagged for Matt's confirm: never-contacted + dormant → cat 24.~~ **Overruled by Matt same day: those clients fall under Winback.** Dormancy trumps first touch; cat 24 takes only *active* never-contacted clients (and tier crossings). Consequence: Winback is the dominant wave (most of the partner base is 12+ months dormant; in the sample, 75 of the 128 partner clients at the 2026-09-14 run). **Winback outreach is automated inside Bitrix (Matt, same day), but not from the first stage:** routing drops the client into the non-automated first-stage bucket, and the automation fires only when Nora advances the deal. So the copy lives in Bitrix (the old missing-template gap is moot), deal creation is safe, and **Nora's stage move is the send and the throttle**.
  53. - **Execution shape:** weekly evaluation (per-client cadence, not batch-quarterly), candidate list in the run artifacts, human approval, push via `partner-pipeline-push`, enrichment on the approved cohort. Bitrix stays human-gated per SOP guardrail 5. Prerequisites in the rules note.
  54. ## 2026-08-06: Approved-merge registry (duplicate Plinth accounts)
  55. - **Mechanism: a merge registry, not a wider dedup key** (Matt's explicit go-ahead, this session). `07-Automation/data/merge_registry.json` maps each absorbed Plinth account to its surviving account; the weekly driver folds absorbed accounts into survivors after Stage B (metrics), so tiers see combined lifetime value, absorbed accounts never classify or get stubs/chunks, and `refresh-xml` removes an absorbed `pl-` chunk if one reappears. Policy lives in **data, not code**: each merge is explicit, attributable, reversible, and survives future changes to the matching rules. Extending the driver's `(email, name)` dedup key was rejected: it hard-codes today's heuristics and still misses the different-email case ([[Imogen Radcliffe]], two accounts either side of her organisation's domain rename) that caused the weekly revert.
  56. - **Registry entries are human-approved only.** The pipeline never infers a merge; every entry carries approver, date and evidence, and gets its own LEDGER entry. Seeded with Imogen Radcliffe (40374 → 40373, approved by Matt). A second candidate (two accounts on the identical email address) is the obvious next entry but is held until the tiering policy is written, because that merge changes a tier (Palladium → Platinum).
  57. - **Fail-open on the registry, fail-closed on data:** a missing or malformed registry warns and runs unmerged (a bad JSON file must not kill the 06:00 scheduled run); the no-shrink verify guardrail is unaffected because absorbed accounts drop out of the snapshot entirely while survivors only grow.
  58. - **Scope note:** this is the *mechanism* decision only. The tiering-unit rulings Matt relayed on 2026-08-06 (tier follows the person across employers; one company's accounts combine as one organisation) get their own DECISIONS entry once the open points are settled (org-record vs per-mailbox attachment, recency, proof standard, exclusion list), after the routing conversation.
  59. ## 2026-07-23: XML map = complete partner census; `pl-<plinth_id>` keys
  60. - **Every partner client gets a chunk** (Matt's call), reversing the prior "add a chunk only when they enter a Bitrix pipeline" rule. The map is now a machine-readable account of the whole partner base; pipeline placement is populated later. Clients not in a pipeline carry an empty `<pipelines count="0" />`.
  61. - **Key = `pl-<plinth_id>` for map-only clients.** Bitrix contact/tracker ids key the pipeline-mapped chunks, and Plinth account ids overlap that integer range, so the `pl-` namespace prevents id collisions. Never renumber; reconcile to the real Bitrix id if and when the client gets a contact and enters a pipeline.
  62. - **Dedup by identity, not raw email.** Two Plinth accounts that are the same person (same name + email) share one chunk; two different people on one company mailbox (a drilling contractor's site lead and marketing coordinator, say, or two coordinators at one tour operator) each get their own chunk. Email-only dedup would have hidden the colleague.
  63. - **refresh-xml stays idempotent and stub-derived.** Identity is pulled from the client's stub (name-first resolution, so shared-mailbox colleagues point to the right stub), the `<summary>` is auto-generated, and the whole file is re-indented on write. Re-running adds nothing.
  64. - **Flag (pre-existing, left as-is):** some pipeline-mapped chunks carry an empty `id` attribute (created before this convention). Untouched this pass: they are all in live pipelines with `client_ref`s that would desync if renumbered. Worth a separate cleanup if we want every chunk stably keyed.
  65. ## 2026-07-22: Plinth truncation bug → incremental pipeline architecture
  66. - **Root cause of the lifetime-value discrepancy raised on the 2026-07-22 Matt / Hazel / Ines call:** Plinth `orders.fetch` silently truncates large result sets at ~70 orders per response with **no** "has more" indicator, and `offset` does not skip by order rows (pages overlap). Reproduced exactly: an all-time query on one long-standing account (Plinth customer 40115 in the sample's numbering) returned 70 of its 74 orders, and the total read off on the call was short by every order it dropped. The dropped orders were the **newest** ones, so truncation corrupts both LTV and last-order date (an active client would have looked dormant and routed to Winback). Ruled out: email churn (every order under one email) and cross-account summing (a similarly named account in another state is an unrelated client).
  67. - **Only long-history clients are affected**: under ~70 orders a single call is complete (which is why a shorter-history comparison client matched perfectly). The bug selectively hits the Iridium/Rhodium segment the Partner Program exists for.
  68. - **Decision: never query the API for an all-time total.** The pipeline (v2 driver, rebuilt today) keeps a local append-only order history (`07-Automation/data/order_history.json`): seeded once from Ines's partner roster workbook (per-customer baselines, `as_of` = sheet cutoff), then grown by weekly day-windowed delta pulls, paged offset + dedupe-by-`order_ref` until a page adds zero new ids. Lifetime LTV = baseline + post-baseline history, computed locally.
  69. - **No-shrink guardrail:** lifetime totals can only grow; `verify` exits 2 and halts the run if any customer's total or count decreases. No tier demotion or Winback routing off a shrinking run: that signature means data loss, not client behavior.
  70. - **Counting rules deferred to Nora** (leadership call): shipping in/out (`grand_total` vs `subtotal`) and whether cancelled/refunded count. Defaults stay roster-compatible (`grand_total`, all statuses) so the first reconciliation is apples-to-apples.
  71. - **Arming switch kept:** `min_orders_to_qualify: TBD` is the one remaining sentinel; Matt sets it tomorrow after seeding. Schedule stays uncreated until he activates.
  72. ## 2026-07-22: 03-Clients restructure + 08-Planning scope
  73. - **Stubs into `client-stubs/`, flat** (Matt's call): separates the stub files from the folder's control files (CLAUDE-clients, XML map), so the `03-Clients/` root reads as an interface (rules + retrieval map) and RAG chunking/globbing targets one clean directory.
  74. - **Repointed links to bare stub names** (`[[Imogen Radcliffe]]`), not new full paths. Path-form links have now broken twice (the Check-In/Winback flattening, and this move); bare names survive future moves. Rule codified in [[CLAUDE-clients]].
  75. - **XML `<summary>` paths rewritten in place** rather than fully regenerated: the only changed fact was the stub path, so a targeted rewrite preserves the auto-generated text byte-for-byte otherwise.
  76. - **07-Automation left untouched** per Matt: contents are templates, not armed. Flagged: `weekly_partner_pipeline.py` (`CLIENTS_DIR`) and the SOP still target `03-Clients/*.md` and must be repointed to `client-stubs/` before arming.
  77. - **08-Planning is human-only** (Matt, Hazel, Ines). Claude reads for context, never writes; a placeholder README anchors the folder for Obsidian sync.
  78. - **Historical prose not rewritten** (CHANGELOG/LEDGER, dated intake notes); the one live mention of the dead `partner-outreach/` stub folder (since removed) got a parenthetical pointer instead.
  79. ## 2026-06-16: Collaboration ledger + attribution
  80. - **Single shared [[LEDGER]] file**, append-only, newest-first, not per-user logs, so both Matt and Hazel read one timeline of who touched what.
  81. - **Attribution by active session user email**, because `CLAUDE.md` syncs to both vaults and can't hardcode a single identity. `hello@meridiandisplay.example` = Matt; Hazel's email = `[TBD]` (resolved the same day: `accounts@meridiandisplay.example`); unknown emails are logged literally and flagged rather than guessed.
  82. - **Trigger is any vault change**, not just CHANGELOG/HEARTBEAT updates (Matt's revised scope). CHANGELOG/HEARTBEAT edits are a subset that still always log.
  83. - **One entry per change-set**, not per file, to keep the timeline readable.
  84. ## 2026-06-16: Architecture choices
  85. - **Stubs organized by outreach play** (`Check-In/`, `Winback/`; since flattened into `03-Clients/client-stubs/`), not by partner tier. Tier is captured in frontmatter and surfaced via Dataview, so it stays filterable without duplicating files.
  86. - **Tiers modeled as the Partner Program** (Palladium / Platinum / Iridium / Rhodium, with Retail as the baseline), per Matt, not an abstract numbered-tier scheme. Facts pulled live from the Partner Program page rather than invented.
  87. - **All founding stubs generated up front** so scraping appends to existing files instead of creating them.
  88. - **Email/call templates kept verbatim** from the source design doc (not included) to preserve the exact approved copy, including personalization placeholders.
  89. - **Winback email copy left as [TBD]** because the source doc contains none; not fabricated.