toga-ai 1.0.355 → 1.0.357

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,11 +19,15 @@ related:
19
19
  - web-funnel-app.md
20
20
  ---
21
21
 
22
- > **Execution status (2026-07-14):** PLAN **Phases 0–5 are BUILT and QA'd** — the web
23
- > app engine, full mockup port, and real backend are done. Remaining work is
24
- > decision-gated, not code (§9.10 HubSpot mechanism, leadership copy, Phase 7 polish,
25
- > Phase 8 deploy). The built codebase is documented in `web-funnel-app.md`; this doc
26
- > stays a plan mirror. This body still mirrors `BDR/PLAN.md`.
22
+ > **Execution status (2026-07-16):** PLAN **Phases 0–5 are BUILT and QA'd**, Phase 6
23
+ > was **mostly already built** (the `info` contract was implemented at commit `34a3910`),
24
+ > and the 2026-07-16 session added phone prefill + mask, `submitOk` on Success, and GA4.
25
+ > **§9.10 is RESOLVED by dissolution** (HubSpot holds no marketing content — content is
26
+ > in-repo; see `web-funnel-content-model.md`) and **§9.11 is RESOLVED** (consent links →
27
+ > togatech.com pages). Remaining work is decision-gated, not code (leadership copy, Phase 8
28
+ > deploy). The built codebase is documented in `web-funnel-app.md`; this doc stays a plan
29
+ > mirror. This body still mirrors `BDR/PLAN.md` (which got a §5.2 superseded banner + §9.2/
30
+ > §9.10 re-resolution on 2026-07-16).
27
31
 
28
32
  # BDR: Multi-Campaign "AI BDR / Agent Studio" Web App — Implementation Plan
29
33
 
@@ -238,6 +242,13 @@ Port approach:
238
242
  > `NEXT_PUBLIC_` env.
239
243
 
240
244
  ### 5.2 Content provider — HubSpot is the source of truth
245
+ > **SUPERSEDED (2026-07-16).** This section's premise — HubSpot as the content source —
246
+ > was **dissolved** by the HubSpot instance owner (Rohan): HubSpot holds no marketing
247
+ > content for this product. Content lives **in the repo** (`src/content/default.ts`); the
248
+ > static `src/content/provider.ts` is permanent; the `hubspotProvider.ts` stub was deleted.
249
+ > Read §9.2 / §9.10 (resolved) and `web-funnel-content-model.md` for the current model. The
250
+ > text below is kept for historical context only.
251
+
241
252
  **Decision:** the content store is **HubSpot** (chosen over Contentful). The site's content shape is
242
253
  stable / not frequently restructured, so HubSpot's flatter modeling is acceptable, and a dedicated
243
254
  headless CMS (Contentful) would only pay off if the content model were rich and frequently
@@ -490,9 +501,12 @@ so fidelity should be near-exact.
490
501
  1. **Backend relationship:** RESOLVED — `bdr` is the new UI / front door of TOGA's existing **AI-BDR**
491
502
  product (not standalone); it reuses that system's backend calls, and its knowledge is filed under
492
503
  `2.0/apps/ai-bdr/`. `CallbackService` uses that system's callback path.
493
- 2. **Content store:** RESOLVED — **HubSpot** (over Contentful): content shape is stable, so HubSpot is
494
- acceptable and the integration is trivial (§5.2). Content is HubSpot-owned from the start (no
495
- hardcoding); the remaining detail is the HubSpot mechanism (§9.10).
504
+ 2. **Content store:** RE-RESOLVED (2026-07-16) — **content lives in the repo**
505
+ (`src/content/default.ts`), **not HubSpot.** The earlier "HubSpot-owned content" decision
506
+ was based on a misreading of how the team uses HubSpot; the HubSpot owner (Rohan) confirmed
507
+ HubSpot holds no marketing content for this product. The static `provider.ts` is permanent
508
+ (the `CampaignContentProvider` seam is kept only for a hypothetical future remote source).
509
+ See §9.10 and `web-funnel-content-model.md`.
496
510
  3. **Agent choice:** Stay with a campaign-fixed agent (auto-build to a preset), or ship the manual
497
511
  Creator (currently parked / "coming soon")? Determines whether `Creator` + full slot art ship now.
498
512
  4. **Service picker:** Is the `Landing` service-selection screen (AI BDR + "Talos coming soon") in
@@ -506,21 +520,24 @@ so fidelity should be near-exact.
506
520
  9. **Backend hosting:** RESOLVED by the Next.js choice — the secret-bearing HubSpot/Toga calls run in
507
521
  BDR's own server components / route handlers; no separate backend to host. Only remaining item is
508
522
  confirming the Amplify **SSR** hosting target (not static) at deploy.
509
- 10. **HubSpot content mechanism (gates the content layer, Phase 1) — the big one.** *How* campaign
510
- content is stored in HubSpot so it is marketing-editable and fetchable by slug. `info` gives no
511
- precedent (it only reads contacts). Options: **HubDB** (CMS-Hub table, one row per campaign, `slug`
512
- column + content columns, API-fetchable — the likely fit) vs. a **custom object** vs. properties on
513
- the Campaigns object. This determines the fetch + the HubSpot->`CampaignBundle` mapping. Sub-parts to
514
- settle with the HubSpot owner: which mechanism; how **nested content** (Q&A pool + rotation sets, call
515
- summaries, accent `RichLine`s) is represented (flat columns can't hold arrays cleanly — child rows or
516
- a JSON field); who authors/owns the schema; and how the slug relates to the existing
517
- `hsCampaignId`/Toga campaign UUID used for attribution.
518
- 11. **Privacy Policy & Terms links (§9.11, from the first design review).** The consent
519
- footnotes on Call Now / Schedule link to **dead anchors** (`#privacy` / `#terms`,
520
- ported verbatim from the mockup; the original `info` app had none). They need real
521
- pages or links to `togatech` pages. Wiring is a **2-line content change** — the schema
522
- already supports `[label](href)`, and HubSpot campaigns can override the href per
523
- campaign.
523
+ 10. **HubSpot content mechanism — RESOLVED by dissolution (2026-07-16, decision by Rohan,
524
+ the HubSpot instance owner).** There is no HubSpot content mechanism because **HubSpot
525
+ holds no marketing content** for this product and never will — it is **contact-lookup
526
+ only** (the `?hsContactId=` fetch, exactly as the old `info` site). Campaign records are
527
+ added **manually to the Toga database** and reach the app only as the `?hsCampaignId=`
528
+ attribution UUID on entry URLs (same URL contract as `info`). Consequences: campaign
529
+ content lives **in-repo** (`src/content/default.ts` is the authoring surface; every slug
530
+ renders `DEFAULT`); the throwing `hubspotProvider.ts` was **deleted**; `provider.ts`'s
531
+ static provider is the **permanent** implementation; the `CampaignContentProvider` seam is
532
+ retained for a hypothetical future remote source. The prior "HubSpot-owned content"
533
+ assumption (HubDB / custom object / campaign properties) is **moot.**
534
+ 11. **Privacy Policy & Terms links (§9.11) — RESOLVED (2026-07-16).** The consent footnotes
535
+ on Call Now / Schedule now link to the existing **togatech.com** pages
536
+ (`https://togatech.com/privacy-policy`, `.../terms-and-conditions`) — decision was to link
537
+ the existing pages, not build in-app ones. `RichText.tsx` `MarkupText` renders real
538
+ `http(s)` hrefs as live anchors (`target=_blank`, `rel="noopener noreferrer"`, aria-label
539
+ "... (opens in new tab)"); bare `#hash` hrefs keep the mockup's inert `preventDefault`. A
540
+ regression test guards against dead anchors returning.
524
541
  12. **Share button is a mock (§9.12).** The Success summary-card Share popover validates
525
542
  the email and shows "Sent" but **sends nothing** (the Copy button IS a real clipboard
526
543
  write). A functional version needs an **email-send route** AND the **real call
@@ -573,6 +590,14 @@ so fidelity should be near-exact.
573
590
  - [ ] Pixel-perfect parity pass against the mockup screens + screenshots.
574
591
 
575
592
  ## Change history
593
+ - 2026-07-16 — §9 resolutions mirrored from `BDR/PLAN.md`. §9.10 RESOLVED by dissolution
594
+ (decision by Rohan, HubSpot instance owner): HubSpot holds no marketing content — content
595
+ is in-repo (`default.ts`), `hubspotProvider.ts` deleted, static `provider.ts` permanent;
596
+ §9.2 re-resolved accordingly (content store is the repo, not HubSpot) and §5.2 marked
597
+ SUPERSEDED. §9.11 RESOLVED: consent links → togatech.com privacy/terms pages via live
598
+ `RichText` anchors. Execution-status banner updated: Phase 6 was mostly already built
599
+ (commit `34a3910`); session added phone prefill + mask, `submitOk` on Success, GA4. Built
600
+ detail in `web-funnel-app.md`; content model in `web-funnel-content-model.md`. (tcox)
576
601
  - 2026-07-15 — First UI design review: appended three future items to §9 open questions,
577
602
  mirroring `BDR/PLAN.md` §9.11–§9.13. §9.11 real Privacy/Terms pages (consent footnotes
578
603
  currently link to dead `#privacy`/`#terms` anchors; 2-line content fix). §9.12 Share
@@ -6,7 +6,7 @@ project: AI-BDR
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-15
9
+ updated: 2026-07-16
10
10
  owners: [tcox]
11
11
  files:
12
12
  - bdr/PLAN.md
@@ -15,9 +15,14 @@ files:
15
15
  - bdr/amplify.yml
16
16
  - bdr/src/app
17
17
  - bdr/src/app/layout.tsx
18
+ - bdr/src/app/page.tsx
18
19
  - bdr/src/app/studio.css
19
20
  - bdr/src/lib/resolveCampaignId.ts
20
21
  - bdr/src/lib/scheduleSlots.ts
22
+ - bdr/src/lib/formatPhone.ts
23
+ - bdr/src/lib/analytics.ts
24
+ - bdr/src/components/GoogleAnalytics.tsx
25
+ - bdr/src/components/RichText.tsx
21
26
  - bdr/src/content
22
27
  - bdr/src/components
23
28
  - bdr/src/flow
@@ -41,11 +46,15 @@ plan lives in `bdr-web-funnel-plan.md`; the config-driven content contract lives
41
46
  stack, the directory map, what was ported vs. parked, the test suite, and the
42
47
  decisions that are load-bearing for anyone touching the repo.**
43
48
 
44
- As of this writing **PLAN Phases 0–5 are BUILT and QA'd** — the web app engine and
45
- full mockup port are complete, all screens/modals render config-driven from a
46
- bundle, and the real backend is wired behind adapters. Developer QA verdict: the UI
47
- is "very close, minimal adjustments to be near exact" vs. the mockup. Remaining work
48
- is **decision-gated, not code** (see "What is parked / not built").
49
+ As of this writing **PLAN Phases 0–5 are BUILT and QA'd**, and the Phase 6 backend
50
+ work turned out to be **mostly already built** (the `info` URL/backend contract was
51
+ fully implemented at commit `34a3910`: `page.tsx primeLead` reads `?hsContactId=` +
52
+ `?hsCampaignId=` → server-side HubSpot `getContact` → Toga `upsertContactByEmail` with
53
+ campaign attribution → only `togaUuid` crosses to the client). The 2026-07-16 session
54
+ added the deltas below (phone prefill + live input mask, `submitOk` on Success, GA4
55
+ wiring) and resolved §9.10 (content mechanism — see `web-funnel-content-model.md`) and
56
+ §9.11 (consent links). Developer QA verdict: the UI is "very close, minimal adjustments
57
+ to be near exact" vs. the mockup. All checks green: tsc, eslint, 79/79 Vitest, `next build`.
49
58
 
50
59
  - **Repo:** `agilantsolutions/BDR` on GitHub, working branch `BDR-Development`. The
51
60
  machine-local `.claude/` harness is **gitignored** — committing it would snapshot
@@ -66,7 +75,8 @@ is **decision-gated, not code** (see "What is parked / not built").
66
75
  - **ESLint flat config** carries a **no-em-dash rule** on shipped strings
67
76
  (`Literal` / `TemplateElement` / `JSXText` selectors) — enforces the mockup copy
68
77
  rule on in-repo strings (`eslint.config.mjs`).
69
- - **Vitest** test suite: **67 tests across 7 files, all green.** Every phase was
78
+ - **Vitest** test suite: **79 tests, all green** (13 added in the 2026-07-16 session:
79
+ phone format/prefill/mask, live-anchor consent links, `submitOk` race). Every phase was
70
80
  verified with `tsc` + lint + build + a live SSR `curl` before commit.
71
81
  - **Deploy target:** AWS Amplify (SSR), `amplify.yml` present (Phase 8 not done).
72
82
 
@@ -116,24 +126,70 @@ is **decision-gated, not code** (see "What is parked / not built").
116
126
  (`src/server/toga.ts`) converts the picked slot to `America/Chicago` for Toga. If
117
127
  `Intl` is unavailable the note simply is not rendered (graceful, no crash). A design
118
128
  request to "make tz calculated" was already satisfied — do not re-implement.
129
+ - **Phone prefill (known leads).** `page.tsx` primeLead now also carries the contact's
130
+ phone: `ClientLeadRef` gained a `phone` field — the **one deliberate PII exception**
131
+ that crosses to the client (the visitor's own number, shown back to its owner, never
132
+ logged). `prefillPhone()` (`src/lib/formatPhone.ts`) normalizes CRM values, drops a US
133
+ `1` prefix, and returns `""` for junk / over-long / non-`+1`-country values (a `+45`
134
+ number next to the fixed `+1` country-code field would dial the wrong country). CallNow
135
+ + Schedule init their number fields from it. (The old `info` site exposed name + email +
136
+ phone the same way; BDR reduces the client-crossing PII to **phone only**.)
137
+ - **Live phone input mask.** `formatPhoneInput()` (`src/lib/formatPhone.ts`) renders a
138
+ progressive `(555) 012-3456` as the user types: hard 10-digit cap (`maxLength=14` on the
139
+ inputs), strips non-digits on paste, idempotent, and backspace-safe by design (formatting
140
+ is always re-derived from the digits, with no trailing punctuation after the last digit,
141
+ so deleting a `)` or `-` never fights the formatter). Matches the bundle placeholder.
142
+ - **`submitOk` surfaced on Success (known leads only).** When a KNOWN lead's
143
+ fire-and-forget backend submit resolves `false`, a **pre-mounted** `role="status"` live
144
+ region (mounted empty so screen readers reliably announce the change) shows
145
+ `success.submitFailed.{calling,scheduled}`. Anonymous visits always pass `null` and never
146
+ see it. A submission-generation counter (a ref in `useAgentFlow`) prevents a stale,
147
+ late-resolving submit from an abandoned attempt overwriting the current result.
148
+ - **GA4 analytics (`react-ga4`).** `src/lib/analytics.ts` + `src/components/GoogleAnalytics.tsx`
149
+ (mounted in `layout.tsx`), env-driven via `NEXT_PUBLIC_GA_MEASUREMENT_ID` — a silent
150
+ no-op when unset (dev-only console warning). Typed event map: `screen_viewed`,
151
+ `build_started`, `early_access_opened`, `call_now_submitted{success}`,
152
+ `call_later_submitted{success}`. No PII in any param. See the page_location leak gotcha.
153
+ - **Consent footnote links are live (§9.11 resolved).** They point at
154
+ `https://togatech.com/privacy-policy` and `.../terms-and-conditions` (decision: link the
155
+ existing togatech pages, not in-app pages). `RichText.tsx` `MarkupText` renders real
156
+ `http(s)` hrefs as live anchors (`target=_blank`, `rel="noopener noreferrer"`, aria-label
157
+ "... (opens in new tab)"); bare `#hash` hrefs keep the mockup's inert `preventDefault`.
119
158
 
120
159
  ## What is parked / not built
121
160
 
122
161
  - **Creator screen + `TweaksPanel` — deliberately NOT ported** (parked per plan
123
162
  §9.3 / §9.7).
124
- - **`hubspotProvider` is a throwing stub**, gated on the §9.10 HubSpot content-
125
- modeling decision. Until HubSpot is seeded, `DEFAULT_BUNDLE` carries the mockup's
126
- copy verbatim as documented **interim dev content**; it slims to a bare
127
- engineering fallback once HubSpot is seeded.
163
+ - **Content is in-repo, not HubSpot (§9.10 RESOLVED by dissolution, 2026-07-16).** The
164
+ throwing `hubspotProvider.ts` was **DELETED**; `src/content/provider.ts`'s static
165
+ provider is the **permanent** implementation, and `DEFAULT_BUNDLE` carries the real
166
+ shipped copy (not interim). HubSpot is contact-lookup only; campaign records live in Toga.
167
+ Full detail in `web-funnel-content-model.md`. Do not reintroduce a remote content source.
128
168
  - **`info`'s dead 501 `visit` route was dropped** on purpose (attack surface, no
129
169
  caller).
130
- - **Remaining work is decision-gated, not code:** §9.10 HubSpot content mechanism
131
- (HubDB likely — gates the provider + campaign seeding, Phase 6); leadership's
132
- first-campaign copy; Phase 7 pixel/GA4/accessibility polish; Phase 8 Amplify SSR +
133
- `togatech` integration.
170
+ - **Remaining work is decision-gated, not code:** leadership's first-campaign copy;
171
+ Phase 8 Amplify SSR (incl. a **per-IP WAF rate rule** — see the enumeration gotcha) +
172
+ `togatech` integration. GA4 (Phase 7) is now wired; §9.10 and §9.11 are resolved.
134
173
 
135
174
  ## Gotchas
136
175
 
176
+ - **CRITICAL — GA4 leaks the entry identifiers unless you scrub `page_location`.** `gtag`
177
+ stamps `page_location = window.location.href` (which **includes** `?hsContactId=` &
178
+ `?hsCampaignId=`) on the auto `page_view` **and on every subsequent event** — so the
179
+ entry identifiers would ship to Google on every hit. Mitigation (in place): initialize
180
+ with `gtagOptions {send_page_view:false}` and pin a **query-scrubbed** `page_location`
181
+ via `ReactGA.set` before each pageview. Never send raw `location.href` to analytics here.
182
+ - **HIGH / OPEN (security) — `?hsContactId=` is an enumerable capability token.** The entry
183
+ page is public and HubSpot contact ids are small integers; a bot walking ids can (a)
184
+ scrape each contact's phone from the server-rendered prefill, and (b) drive the page-prime
185
+ Toga upsert, polluting contacts/attribution. **Inherited from `info`'s CRM-link URL
186
+ contract — cannot be changed unilaterally.** Mitigation path before public launch: a
187
+ **per-IP WAF rate rule** on the Amplify distribution (Phase 8), and raise **opaque/signed
188
+ link tokens** with the CRM owner. Tracked in `BDR/PLAN.md` §10.
189
+ - **Standing rule: never add more PII fields to `ClientLeadRef`.** Phone is the one
190
+ deliberate exception (visitor's own number, shown back to them, never logged). Every field
191
+ on `ClientLeadRef` crosses to the public client and is exposed to the enumeration risk
192
+ above — do not widen it.
137
193
  - **The mockup's `mockup/styles.css` contains literal `\n` and `\"` escape sequences**
138
194
  (JS-string generation artifacts, around lines ~759 / ~840) that break real CSS
139
195
  parsers. This was **fixed in the ported `src/app/studio.css`**, but
@@ -162,6 +218,22 @@ is **decision-gated, not code** (see "What is parked / not built").
162
218
  harness; pull it with `npx toga-ai`.
163
219
 
164
220
  ## Change history
221
+ - 2026-07-16 — Phase 6 delta + §9.10/§9.11 resolutions. DISCOVERED the `info` URL/backend
222
+ contract was already fully built at commit `34a3910` (primeLead: `?hsContactId=` +
223
+ `?hsCampaignId=` → HubSpot `getContact` → Toga upsert + attribution → only `togaUuid` to
224
+ client). BUILT: phone prefill (`ClientLeadRef.phone`, the one deliberate client-crossing
225
+ PII field; `prefillPhone()` normalizes CRM values, drops US `1`, rejects non-`+1`-country
226
+ numbers); live phone input mask (`formatPhoneInput()`, 10-digit cap, paste-safe,
227
+ backspace-safe); `submitOk` surfaced on Success via a pre-mounted `role="status"` live
228
+ region for KNOWN leads, guarded by a submission-generation counter against stale late
229
+ resolves; GA4 via `react-ga4` (env-driven, silent no-op when unset). RESOLVED §9.10 (content
230
+ is in-repo, `hubspotProvider.ts` DELETED, static `provider.ts` permanent — see
231
+ `web-funnel-content-model.md`) and §9.11 (consent links now live togatech.com anchors via
232
+ `RichText`). Added CRITICAL GA4 `page_location` leak gotcha (scrub `?hsContactId=`/
233
+ `?hsCampaignId=` before every hit), HIGH/OPEN `?hsContactId=` enumeration gotcha (WAF rate
234
+ rule + signed tokens before launch), and the "never widen `ClientLeadRef` PII" standing
235
+ rule. 13 new tests (79 total); tsc/eslint/build all green. Work uncommitted at capture
236
+ time (branch `BDR-Phase-2`). (tcox)
165
237
  - 2026-07-15 — First UI design review pass. FIXED: site-wide font silently fell back to
166
238
  `system-ui` (mockup's `<head>` Google-Fonts link was never ported) — now Plus Jakarta
167
239
  Sans via `next/font/google` in `layout.tsx`, exposed as `--font-jakarta`, resolved
@@ -5,12 +5,12 @@ repo: ai-bdr
5
5
  project: AI-BDR
6
6
  client: shared
7
7
  type: feature
8
- status: draft
9
- updated: 2026-07-14
8
+ status: active
9
+ updated: 2026-07-16
10
10
  owners: [tcox]
11
11
  files:
12
12
  - bdr/src/content/schema.ts
13
- - bdr/src/content/hubspotProvider.ts
13
+ - bdr/src/content/provider.ts
14
14
  - bdr/src/content/useCampaign.ts
15
15
  - bdr/src/content/default.ts
16
16
  - bdr/src/lib/resolveCampaignId.ts
@@ -25,11 +25,14 @@ related:
25
25
  - web-funnel-app.md
26
26
  ---
27
27
 
28
- > **Status note:** the content **contract** below (schema, provider seam, adapters)
29
- > is now BUILT and QA'd (PLAN Phases 0–5). The built codebase — structure, stack,
30
- > how to run — is documented in `web-funnel-app.md`. The one part still **not** built
31
- > is the HubSpot content *sourcing* (`hubspotProvider` is a throwing stub gated on the
32
- > §9.10 mechanism decision), which is why this doc stays `draft`.
28
+ > **Status note (RESOLVED 2026-07-16):** the content **contract** below (schema,
29
+ > provider seam, adapters) is BUILT and QA'd (PLAN Phases 0–5). The last open item —
30
+ > *how* campaign content is sourced (§9.10) — is now **resolved by dissolution**
31
+ > (decision by Rohan, the HubSpot instance owner): **HubSpot never holds marketing
32
+ > content for this product.** Campaign content lives **in the repo** (`src/content/default.ts`
33
+ > is the authoring surface); the static `src/content/provider.ts` is the **permanent**
34
+ > implementation and the throwing `hubspotProvider.ts` stub was **deleted**. HubSpot is
35
+ > contact-lookup only (see "How campaigns actually work" below). This doc is now `active`.
33
36
 
34
37
  ## What this is
35
38
 
@@ -55,41 +58,42 @@ routes through `var(--accent)` + derived `--accent-fill`; never hardcode an acce
55
58
 
56
59
  Full implementation plan: `BDR/PLAN.md`.
57
60
 
58
- ## Config-driven campaign content (HubSpot-owned, fetched by slug at runtime)
61
+ ## How campaigns actually work (content in-repo; HubSpot is contact-lookup only)
62
+
63
+ > **This section supersedes the earlier "HubSpot-owned content" model.** The prior plan
64
+ > assumption — that marketing would author campaign content in HubSpot and the app would
65
+ > fetch it by slug — was a **misreading** of how the team uses HubSpot. The HubSpot
66
+ > instance owner (Rohan) confirmed on 2026-07-16 that **HubSpot holds no marketing content
67
+ > and never will** for this product. §9.10 is resolved by dissolution.
59
68
 
60
69
  Every campaign is a **serializable data bundle** (`CampaignBundle`) keyed by a stable
61
- `slug`. **Campaign content is authored by the marketing team in HubSpot** and fetched **by
62
- slug at runtime, server-side**, then mapped to a `CampaignBundle`. Content is **not** stored
63
- in the repo — a new campaign or a copy edit ships with **no developer and no redeploy**,
64
- only a HubSpot edit. There is **no hardcoded/in-code interim**: content is HubSpot-owned and
65
- fetched by slug from the start (an in-code interim was considered and rejected).
66
-
67
- **Content store decided: HubSpot (over Contentful).** Rationale: (a) marketing already
68
- operates in HubSpot; (b) the campaign model already exists there — the old BDR site (`info`)
69
- uses HubSpot for lead sourcing, personalization, and campaign **attribution**
70
- (`hsContactId` / `hsCampaignId` → Toga), so content keys off that same campaign concept and
71
- content + attribution stay in one system; (c) the API integration is trivial — a ~15-line
72
- server-side fetch proxied by a Next route handler (proven in `info`; same pattern serves
73
- HubDB); (d) no new vendor to contract. **Trade-off accepted:** HubSpot is not a
74
- purpose-built content CMS, so richly nested content and non-dev editor UX are weaker than
75
- Contentful — acceptable here because the content shape is **stable / not frequently
76
- restructured** (a headless CMS would only pay off if the model were rich and churning).
70
+ `slug`. **Campaign content is authored in the repo** — `src/content/default.ts` is the
71
+ authoring surface, and **every slug renders `DEFAULT`.** There is no remote content
72
+ source. What HubSpot *does* provide is unchanged from the old `info` site: a **contact
73
+ lookup** (`?hsContactId=` → server-side `getContact`), nothing more.
74
+
75
+ **Campaign records are added MANUALLY to the Toga database** and reach the app only as the
76
+ `?hsCampaignId=` attribution UUID on entry URLs (the exact same URL contract as `info`).
77
+ The UUID is passed through to Toga (`addContactToCampaign`) for attribution; it does **not**
78
+ select or fetch content.
79
+
80
+ The `CampaignContentProvider` **seam is retained** (so a hypothetical future remote source
81
+ could slot in), but the static `src/content/provider.ts` is the **permanent** implementation
82
+ — not an interim. The throwing `hubspotProvider.ts` stub was **deleted**.
77
83
 
78
84
  Config-driven, per TOGA precedent (`toga25-supply`'s `useClientFields`,
79
85
  `toga2-commerce`'s Cart C1–C7): components are **pure renderers of a bundle**; there is
80
- **no `if (campaign === 'x')` branching** anywhere in the screens. The repo holds only the
81
- **schema** (`CampaignBundle` types), the **HubSpot→bundle mapping** (provider), and a
82
- minimal safety-net **`DEFAULT`** bundle (an engineering fallback, not authored copy).
83
-
84
- - **Provider seam.** `CampaignContentProvider.resolve(slug)` returns a `CampaignBundle` (or
85
- `DEFAULT` on an unknown/unreachable slug). `HubSpotContentProvider` runs **server-side**
86
- (page server component / route handler), maps HubSpot content to a `CampaignBundle`,
87
- caches it (Next revalidation), and applies the no-em-dash normalization pass. Client
88
- components receive the resolved bundle as props / via `useCampaign()` and never see
89
- HubSpot.
86
+ **no `if (campaign === 'x')` branching** anywhere in the screens. The repo holds the
87
+ **schema** (`CampaignBundle` types), the **static provider**, and the **`DEFAULT`** bundle
88
+ that carries the real shipped content.
89
+
90
+ - **Provider seam.** `CampaignContentProvider.resolve(slug)` returns a `CampaignBundle`.
91
+ The permanent `provider.ts` implementation returns `DEFAULT` for every slug (server-side,
92
+ during render). Client components receive the resolved bundle as props / via
93
+ `useCampaign()`.
90
94
  - **Serializable-only bundles.** Non-serializable behavior (a CTA action, an icon) is
91
95
  referenced by an **enum key** and hydrated at the view-model layer through a
92
- `Record<key, fn|Component>` registry — HubSpot only ever stores plain strings/values.
96
+ `Record<key, fn|Component>` registry — the bundle stays pure strings/values.
93
97
  - **Campaign selection is URL-driven, per-request.** `resolveCampaignId()`, first match
94
98
  wins: `?campaign=<slug>` → mapped `?hsCampaignId=<id>` (id→slug lookup so existing
95
99
  outreach/CRM links keep working) → `DEFAULT`. Query-param entry ships first (matches how
@@ -122,10 +126,10 @@ returns the `DEFAULT` bundle, so the funnel always renders. Entry resolution liv
122
126
  `src/lib/resolveCampaignId.ts` (`?campaign=<slug>` → mapped `?hsCampaignId=` →
123
127
  `DEFAULT`; slug regex `^[a-z0-9][a-z0-9-]{0,63}$`).
124
128
 
125
- **`hubspotProvider` is a throwing stub** explicitly gated on the §9.10 HubSpot-modeling
126
- decision — it is not wired yet. Until HubSpot is seeded, **`DEFAULT_BUNDLE` carries the
127
- mockup's copy verbatim as documented interim dev content**; once HubSpot is seeded it
128
- slims to a bare engineering safety-net fallback (not authored marketing copy).
129
+ **`DEFAULT_BUNDLE` (`src/content/default.ts`) is the shipped content**, not interim dev
130
+ copy — it carries the mockup's real copy and is what every slug renders. The throwing
131
+ `hubspotProvider.ts` stub was **deleted** (§9.10 dissolution); do not reintroduce a remote
132
+ provider without a new decision.
129
133
 
130
134
  ## Backend reuse (from the `info` repo; existing AI-BDR calls)
131
135
 
@@ -157,10 +161,10 @@ Vitest tests. See `web-funnel-app.md` for the full built structure.
157
161
 
158
162
  Note on "campaign": `info` uses HubSpot **only to fetch the contact**; the URL's
159
163
  `hsCampaignId` is passed to Toga as the attribution campaign UUID
160
- (`addContactToCampaign`) — attribution-only in `info`. **Fetching marketing content by
161
- slug from HubSpot is net-new** to this funnel (above). The content slug and the attribution
162
- UUID are related but distinct; the `CampaignBundle` carries the UUID so a slug maps to the
163
- right campaign for logging.
164
+ (`addContactToCampaign`) — attribution-only. **BDR keeps this exact contract** — HubSpot
165
+ is contact-lookup only, campaign records are added manually to Toga, and the app never
166
+ fetches marketing content from anywhere (see "How campaigns actually work" above). The
167
+ `CampaignBundle` still carries the UUID so a slug maps to the right campaign for logging.
164
168
 
165
169
  ### Environment (values live in env / Amplify config, never in the repo)
166
170
 
@@ -170,13 +174,11 @@ right campaign for logging.
170
174
 
171
175
  ## Gotchas
172
176
 
173
- - **HubSpot content mechanism is an OPEN question that gates the content-layer phase.**
174
- `info` gives no precedent (it only *reads contacts*). Candidate homes: **HubDB** (CMS-Hub
175
- table — one row per campaign, `slug` column + content columns, API-fetchable; likely fit)
176
- vs. a **custom object** vs. properties on the Campaigns object. The hard part is the
177
- **nested content** — Q&A pool + rotation sets, call summaries, accent `RichLine`s — which
178
- flat columns can't hold cleanly, so it needs child rows or a structured (JSON) field.
179
- Settle with the HubSpot owner before building the content layer.
177
+ - **Do NOT reintroduce a HubSpot (or any remote) content source.** §9.10 is resolved by
178
+ dissolution (2026-07-16, HubSpot owner): HubSpot holds no marketing content for this
179
+ product. Content lives in `src/content/default.ts`; the static `provider.ts` is permanent.
180
+ The `CampaignContentProvider` seam is kept only for a *hypothetical* future remote source
181
+ and needs a fresh decision before anyone wires one.
180
182
  - **Keep secrets server-side.** `TOGA_CLIENT_API_SECRET` / `HUBSPOT_ACCESS_TOKEN` and the
181
183
  token exchange must live in server components / route handlers, never in a `"use client"`
182
184
  component or a `NEXT_PUBLIC_` env var (those ship to the browser). The interactive Agent
@@ -184,17 +186,26 @@ right campaign for logging.
184
186
  - **Do NOT port `info`'s debug `console.log`s** — several log PII (email/phone). Log
185
187
  identifiers only.
186
188
  - **Do not port `info`'s hero/curiosity marketing UI** — it is a different, older funnel.
187
- - **`DEFAULT` must always render.** An unknown/absent slug or a brief HubSpot outage falls
188
- back to the built-in safety net — never a 404 funnel.
189
- - **Runtime fetch adds latency + an availability dependency.** Cache server-side with short
190
- revalidation; mind HubSpot API rate limits.
191
- - **No em dashes in any shipped copy** (mockup rule) — can't be linted on HubSpot-authored
192
- copy, so enforce via marketing author guidance + the normalization pass; in-repo strings
193
- get an ESLint no-em-dash check.
189
+ - **`DEFAULT` is what always renders.** Every slug resolves to the in-repo `DEFAULT`
190
+ bundle — an unknown/absent slug is not a special case, it's the norm; never a 404 funnel.
191
+ - **No em dashes in any shipped copy** (mockup rule). Because content is now in-repo, this
192
+ is fully enforced by the ESLint no-em-dash check on in-repo strings — no author-guidance
193
+ or normalization-pass gap.
194
194
  - **Single accent source** (mockup rule): every accent-tinted element routes through
195
195
  `var(--accent)` (+ derived `--accent-fill`); never hardcode an accent value.
196
196
 
197
197
  ## Change history
198
+ - 2026-07-16 — §9.10 RESOLVED by dissolution (decision by Rohan, HubSpot instance owner):
199
+ HubSpot holds NO marketing content for this product and never will — it is contact-lookup
200
+ only (`?hsContactId=`, exactly as `info`). Campaign content lives in-repo (`default.ts` is
201
+ the authoring surface; every slug renders `DEFAULT`); campaign records are added MANUALLY
202
+ to Toga and reach the app only as the `?hsCampaignId=` attribution UUID. Rewrote the
203
+ content section (was "HubSpot-owned, fetched by slug"), removed the HubSpot-over-Contentful
204
+ framing, the throwing `hubspotProvider.ts` was DELETED, and the static `provider.ts` is now
205
+ the permanent implementation (the `CampaignContentProvider` seam is kept only for a
206
+ hypothetical future remote source). Dropped the now-moot runtime-fetch latency / HubSpot
207
+ content-mechanism gotchas. Flipped `draft`→`active` (the reason it was draft is resolved).
208
+ (tcox)
198
209
  - 2026-07-14 — Content contract BUILT + QA'd (PLAN Phases 0–5): schema calibrated by
199
210
  building the real screens (`src/content/schema.ts` is now THE contract — chrome,
200
211
  capabilities eyebrows/qbank/howLink, `connectNote` {label,value}, earlyAccess/phone/
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 32 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 33 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 11 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -14,11 +14,12 @@ project: _Underscore
14
14
  client: compass-canada
15
15
  type: profile
16
16
  status: active
17
- updated: 2026-06-30
17
+ updated: 2026-07-16
18
18
  owners: [jcardinal, bala, tcox, apeterson]
19
19
  files: []
20
20
  related:
21
21
  - ../compass-usa/profile.md
22
+ - ../compass-usa/features/approval-decision-flow.md
22
23
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
23
24
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
24
25
  - ../../2.0/apps/_underscore/features/surface-resolver.md
@@ -59,6 +60,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
59
60
  Compass USA's 1/3/4. See [Surface Resolver](../../2.0/apps/_underscore/features/surface-resolver.md).
60
61
  - Customer language preference: `UserGlobalSettings.settingId = 2` (`en` / `fr-CA`); customer-
61
62
  facing emails are sent in EN or FR accordingly.
63
+ - **Approval decisions use the shared Compass parent.** `_Model_Compass_Canada_ApprovalDecision`
64
+ is an empty subclass of `_Model_Compass_ApprovalDecision`; the entire approval/notification/
65
+ manager-reassignment/VIP-auto-approve flow lives in the parent and branches at runtime on
66
+ `clientIdentifier === 'Compass_Canada'` (email-template UUID + EN/FR localization). Fixes to the
67
+ parent cover Canada automatically — no per-tenant change. See
68
+ [Approval-Decision Flow](../compass-usa/features/approval-decision-flow.md).
62
69
  - Assortment (product-grouping) names are served in fr-CA via the `AssortmentTranslations` sidecar
63
70
  (16 rows seeded). French was extracted from the old bilingual `"English/French"` `Assortments.name`
64
71
  values, which were then cleaned to English-only. See
@@ -2,6 +2,7 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
+ | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php |
5
6
  | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
6
7
  | [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
7
8
  | [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql, dbchanges2/Client/2026-07-15a - ExcludeFeeItemsFromItemFulfillmentsForSalesOrdersView.sql |
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: Compass Approval-Decision Flow (Notifications & Manager Reassignment)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-07-16
10
+ owners: ["apeterson"]
11
+ files:
12
+ - _underscore/Model/Compass/ApprovalDecision.php
13
+ - _underscore/Model/Compass/Usa/ApprovalDecision.php
14
+ - _underscore/Model/Compass/Canada/ApprovalDecision.php
15
+ related:
16
+ - mr-ma-order-approval-and-status.md
17
+ - ../../compass-canada/profile.md
18
+ - ../../../2.0/apps/_underscore/features/email-template-sending.md
19
+ ---
20
+
21
+ ## Summary
22
+ Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**,
23
+ VIP auto-approve, and EN/FR localization — lives **entirely in the shared parent
24
+ `_Model_Compass_ApprovalDecision`**. Both tenant subclasses,
25
+ `_Model_Compass_Usa_ApprovalDecision` and `_Model_Compass_Canada_ApprovalDecision`, are **empty**
26
+ (`extends _Model_Compass_ApprovalDecision {}`, no overrides). Client- and language-specific
27
+ behavior is **not** subclassed; it branches at runtime inside the parent on
28
+ `$api->client->clientIdentifier` (`Compass_Usa` vs `Compass_Canada`). Practical consequence: a fix
29
+ to a parent method applies to **both** Compass US and Compass Canada automatically — there is no
30
+ per-client code change to make.
31
+
32
+ This doc covers the **approval-decision / notification** half of Compass approvals. The separate
33
+ auto-approval + `_status` gating on the order itself is in
34
+ [Compass MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md).
35
+
36
+ ## Key files / entry points
37
+ - **`_underscore/Model/Compass/ApprovalDecision.php`** — the shared parent that owns the whole
38
+ flow (notification-list maintenance, `_swapManagerEmailAddress()`, VIP auto-approve, email-template
39
+ UUID resolution, localization).
40
+ - **`_underscore/Model/Compass/Usa/ApprovalDecision.php`** — empty subclass, no overrides.
41
+ - **`_underscore/Model/Compass/Canada/ApprovalDecision.php`** — empty subclass, no overrides.
42
+
43
+ ## How it works
44
+ ### One shared parent, runtime client branch (not subclass overrides)
45
+ Compass US and Compass Canada resolve to different model classes at request time
46
+ (`_Model_Compass_Usa_ApprovalDecision` / `_Model_Compass_Canada_ApprovalDecision`), but both are
47
+ empty shells extending `_Model_Compass_ApprovalDecision`. All behavior is inherited. Where the two
48
+ tenants differ, the **parent** branches on `$api->client->clientIdentifier === 'Compass_Canada'`:
49
+ - **Email-template resolution** — `resolveEmailTemplateUuid()` picks the tenant's template UUID.
50
+ - **EN/FR localization** — user language is read from `UserGlobalSettings` (`settingId = 2`;
51
+ `en` / `fr-CA`) so Canadian notifications go out in the recipient's language.
52
+
53
+ Because the divergence is a runtime branch and not an override, do **not** add per-client logic to
54
+ the empty subclasses — extend the shared parent and branch there if a genuine tenant difference is
55
+ needed.
56
+
57
+ ### Manager reassignment and the notification list
58
+ When a step-2 (Manager) approval decision is **reassigned** to a new manager,
59
+ `_swapManagerEmailAddress()` updates the order's CC/notification list in `SalesOrderEmailAddresses`:
60
+ it removes the outgoing manager's email and adds the incoming manager's. The one email that must
61
+ **never** be dropped is the **order requester's** — the person the order is for, resolved via
62
+ `SalesOrders.contactId → Users` (the requester's email is looked up with a parameterized
63
+ `SELECT Users.email FROM SalesOrders INNER JOIN Users ON Users.contactId = SalesOrders.contactId
64
+ WHERE SalesOrders.id = ?`). Before deleting the old-manager email, the method compares it against
65
+ both the new-manager email and the requester email and **skips the delete when it matches the
66
+ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), matching
67
+ `getFilteredCcEmails()`.
68
+
69
+ ## Gotchas / known issues
70
+ - **Never drop the order requester from notifications on manager reassignment.** If the outgoing
71
+ manager happens to also be the order requester (same email), deleting the old-manager address
72
+ from `SalesOrderEmailAddresses` would silently remove the requester from all order notifications.
73
+ `_swapManagerEmailAddress()` guards against this by comparing the old-manager email to the
74
+ requester's email (looked up via `SalesOrders.contactId → Users`) and skipping the delete when
75
+ they match. This was a live bug (fixed 2026-07-16).
76
+ - **Email comparisons must be case-insensitive.** Use `strcasecmp()`, not `!==` — the notification
77
+ list can hold the same address in different casing, and `getFilteredCcEmails()` already compares
78
+ case-insensitively. A strict `!==` comparison here previously let a casing mismatch defeat the
79
+ requester/new-manager guards.
80
+ - **Don't put tenant behavior in the empty subclasses.** `Usa`/`Canada` `ApprovalDecision` are
81
+ intentionally empty; the tenant branch lives in the parent on `clientIdentifier`.
82
+
83
+ ## Change history
84
+ - 2026-07-16 — Fixed `_swapManagerEmailAddress()` dropping the order requester from the order's
85
+ notification list (`SalesOrderEmailAddresses`) when a step-2 manager was reassigned and the
86
+ outgoing manager's email also belonged to the requester: now looks up the requester email via
87
+ `SalesOrders.contactId → Users` and skips the delete when they match; also switched the
88
+ old-vs-new and old-vs-requester comparisons to case-insensitive `strcasecmp()` (was `!==`) to
89
+ match `getFilteredCcEmails()`. Documented that the flow is the shared parent
90
+ `_Model_Compass_ApprovalDecision` (empty `Usa`/`Canada` subclasses, runtime `clientIdentifier`
91
+ branch), so the fix covers Compass Canada automatically. Shipped on branch TRUE-80244 (pushed;
92
+ not yet merged to `_beta`). (apeterson)
93
+
94
+ ## Related docs
95
+ - [Compass MR/MA Order Auto-Approval & Status Gate](mr-ma-order-approval-and-status.md) — the
96
+ auto-approval + `_status` gating half of Compass approvals (on `_Model_Compass_SalesOrder`).
97
+ - [Compass Canada](../../compass-canada/profile.md) — uses this identical flow; the runtime
98
+ `Compass_Canada` branch drives its EN/FR notification localization.
99
+ - [Email Template Sending](../../../2.0/apps/_underscore/features/email-template-sending.md) — the
100
+ shared engine the resolved approval-notification templates are sent through.
@@ -15,12 +15,13 @@ project: _Underscore
15
15
  client: compass-usa
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-07-15
18
+ updated: 2026-07-16
19
19
  owners: [jcardinal, bala, tcox, apeterson]
20
20
  files: []
21
21
  related:
22
22
  - features/asn-to-item-fulfillment.md
23
23
  - features/cost-centers.md
24
+ - features/approval-decision-flow.md
24
25
  - workflows/cross-kit-bundle-corruption.md
25
26
  - ../../2.0/apps/worker2/features/compass-vip-support-importer.md
26
27
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
@@ -75,6 +76,10 @@ separate, related client (see its own profile).
75
76
  - [VIP Support Importer (worker2)](../../2.0/apps/worker2/features/compass-vip-support-importer.md)
76
77
  — manual (Postman) worker2 action that reads Compass's quarterly VIP spreadsheet and sets
77
78
  `Users.c_supportedByUserId` (assigned support tech) per VIP.
79
+ - [Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) —
80
+ the shared `_Model_Compass_ApprovalDecision` parent (empty `Usa`/`Canada` subclasses; runtime
81
+ `clientIdentifier` branch) that owns approval notifications, manager reassignment, VIP
82
+ auto-approve, and EN/FR localization.
78
83
 
79
84
  ## Notes
80
85
  - **Order status is shipped-only (2026-06-30).** Compass imports all IF stages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.355",
3
+ "version": "1.0.357",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",