toga-ai 1.0.331 → 1.0.333

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.
@@ -3,8 +3,9 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [AI-BDR Architecture](architecture.md) | **AI-BDR** is TOGA's outbound AI sales-development representative. | ai-bdr/README.md, ai-bdr/requirements.txt, ai-bdr/.env.example, ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/vapi/templates/assistant.template.json, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py |
6
- | [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Status:** Draft 2 (needs one more review). | bdr/PLAN.md, bdr/mockup/app.jsx, bdr/mockup/screens.jsx, bdr/mockup/components.jsx |
6
+ | [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Execution status (2026-07-14):** PLAN **Phases 0–5 are BUILT and QA'd** — the web > app engine, full mockup port, and real backend are done. | bdr/PLAN.md, bdr/mockup/app.jsx, bdr/mockup/screens.jsx, bdr/mockup/components.jsx |
7
7
  | [Call Orchestration — PHP Worker ↔ Vapi (the integration seam)](features/call-orchestration.md) | The **PHP worker** is the orchestrator; **Vapi** is the actor. | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/scripts/update_structured_output.py |
8
8
  | [Vapi Integration — Assistants, Tools, Structured Output](features/vapi-integration.md) | Everything inside Vapi: the three assistants, the three shared tools, the shared structured-output schema, the Liquid-templated system prompt, and the Python sc | ai-bdr/vapi/templates/assistant.template.json, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md |
9
- | [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | The **public-facing web funnel** is the new UI / lead-capture front door of the **same AI-BDR product** documented in `../architecture.md` it is the front doo | bdr/src/content/schema.ts, bdr/src/content/hubspotProvider.ts, bdr/src/content/useCampaign.ts, bdr/src/content/default.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/app/api/call-now/route.ts, bdr/src/app/api/call-later/route.ts |
9
+ | [Web Funnel — Built Next.js App (structure, stack, how to run)](features/web-funnel-app.md) | The **built** state of the BDR web funnel the Next.js UI / lead-capture front door of the existing AI-BDR product (see `../architecture.md`). | bdr/PLAN.md, bdr/.gitignore, bdr/eslint.config.mjs, bdr/amplify.yml, bdr/src/app, bdr/src/lib/resolveCampaignId.ts, bdr/src/content, bdr/src/components, bdr/src/flow, bdr/src/server, bdr/src/app/api, bdr/test, bdr/public, bdr/mockup/styles.css |
10
+ | [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | > **Status note:** the content **contract** below (schema, provider seam, adapters) > is now BUILT and QA'd (PLAN Phases 0–5). | bdr/src/content/schema.ts, bdr/src/content/hubspotProvider.ts, bdr/src/content/useCampaign.ts, bdr/src/content/default.ts, bdr/src/lib/resolveCampaignId.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/app/api/call-now/route.ts, bdr/src/app/api/call-later/route.ts, bdr/src/app/api/contact/route.ts |
10
11
  | [New-Campaign Onboarding (6-phase runbook)](workflows/new-campaign-onboarding.md) | The end-to-end procedure for launching a new outbound BDR campaign (industry, offer, target persona, geography). | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/calcom-new-campaign-event-guide.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py |
@@ -5,7 +5,7 @@
5
5
  | [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
6
6
  | [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, api2/_.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql |
7
7
  | [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
8
- | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
8
+ | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
9
9
  | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php |
10
10
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
11
11
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-09
9
+ updated: 2026-07-14
10
10
  owners: ["jcardinal", "bala"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -15,10 +15,16 @@ files:
15
15
  - _underscore/Model/Core/RecordField.php
16
16
  - _underscore/Model/Core/DefaultGlobalSetting.php
17
17
  - _underscore/Model/Client/ItemTranslation.php
18
+ - _underscore/Model/Client/FeatureTranslation.php
19
+ - _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php
20
+ - _underscore/Model/Client/ItemFeatureTranslation.php
18
21
  - dbchanges2/Client/2026-06-23a - ItemTranslations.sql
19
22
  - dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql
23
+ - dbchanges2/Client/2026-07-13a - FeatureTranslations.sql
24
+ - dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql
20
25
  - dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql
21
26
  - dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql
27
+ - dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql
22
28
  related:
23
29
  - ./acl-permission-chain.md
24
30
  - ../../_underscore/features/model-magic-field-access.md
@@ -26,8 +32,9 @@ related:
26
32
 
27
33
  ## Summary
28
34
 
29
- Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple
30
- languages without forking the schema or breaking English consumers. English stays in the source
35
+ Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text —
36
+ `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures.value`/`presentationValue`) in
37
+ multiple languages without forking the schema or breaking English consumers. English stays in the source
31
38
  table; per-language overlays live in per-table **sidecar** tables (`ItemTranslations` first).
32
39
  The caller's language is resolved once at authentication, surfaced on every response's
33
40
  `audience.language`, and embedded in the JWT; reads/writes are redirected to the sidecar in the
@@ -126,8 +133,19 @@ endpoint for those.
126
133
  - `Core.RecordFields.translationRecordFieldId` — new nullable self-FK (after `recordId`).
127
134
  - `Core.Records` 331 = `item-translations` (`aclDatabase='CLIENT'`); `Core.RecordFields` 2233–2239
128
135
  (its fields), 2240 (the self-describing `translationRecordFieldId` field).
129
- - `item-translations` ACL chain lives in each client DB targeting the Base role — see
130
- [acl-permission-chain.md](./acl-permission-chain.md).
136
+ - **Feature-text sidecars** (same pattern as `ItemTranslations`, ship via `dbchanges2/Client/`):
137
+ - `Client.FeatureTranslations` — overlay for `Features.name`; `id, uuid, dtCreated, dtUpdated,
138
+ featureId (FK), languageId (FK), name`; `UNIQUE(featureId, languageId)`.
139
+ - `Client.ItemCategoryFeatureGroupTranslations` — overlay for `ItemCategoryFeatureGroups.name`;
140
+ `…, itemCategoryFeatureGroupId (FK), languageId, name`; `UNIQUE(fk, languageId)`.
141
+ - `Client.ItemFeatureTranslations` — overlay for `ItemFeatures.value` + `presentationValue`;
142
+ `…, itemFeatureId (FK), languageId, value, presentationValue`; `UNIQUE(fk, languageId)`.
143
+ - `Core.Records` 343 / 344 / 345 = the three above; `Core.RecordFields` 2442–2457. The
144
+ `translationRecordFieldId` column on the source fields (`features.name`,
145
+ `item-category-feature-groups.name`, `item-features.value`/`presentationValue`) is set by an
146
+ `UPDATE…JOIN` so `getTranslatedFieldValue()`/`shouldTranslate()` substitute the sidecar value on read.
147
+ - `item-translations` (and the feature sidecars') ACL chain lives in each client DB targeting the
148
+ Base role — see [acl-permission-chain.md](./acl-permission-chain.md).
131
149
 
132
150
  ## Client variations
133
151
 
@@ -157,6 +175,22 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
157
175
  `$this->response->status` to `200` **before** finalization. An `is_null` guard there suppresses the
158
176
  `201` + tokens and yields a broken `200`/`data:null` login. Branch on the explicit
159
177
  `$invalidLanguageSetting` flag instead.
178
+ - **The all-fields (expand-everything) serializer branch must also translate.** `V2.php` has two
179
+ nested-child serialization paths: when the caller requests **specific** fields it already ran each
180
+ value through `getTranslatedFieldValue()`, but when the caller requested **no** specific fields
181
+ (empty `$varOptions`, i.e. "return everything I have access to") the code copied raw child values
182
+ straight into `$outData` and never translated — so any field returned via the all-fields expansion
183
+ stayed in base English even for a non-base-language user. Both branches must route through
184
+ `getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` (preserving the
185
+ `renamedFields` alias). Symptom that found it: on the Compass Canada French storefront item-detail
186
+ page (loaded via `/bundles`), feature-spec **values** were French but the feature-group **headers**
187
+ (`itemCategoryFeatureGroup.name`, e.g. "Processeur"/"Mémoire") stayed English, because the group
188
+ object serialized through the all-fields branch. **Not a query-depth problem** — a commerce-side
189
+ `depth:6` theory was wrong and reverted; the fix returns French headers at the default `meta.depth 3`.
190
+ - **Sidecar/source collation mismatch.** The translation sidecar tables use `utf8mb4_unicode_ci`
191
+ (matching `ItemTranslations`) while the source fields are `utf8mb4_0900_ai_ci`. Ad-hoc queries that
192
+ compare a sidecar string against a source string must add `COLLATE utf8mb4_bin` or MySQL throws a
193
+ collation-mismatch error.
160
194
  - Reading a possibly-absent magic field off a generic `_Model` needs `array_key_exists(...)` +
161
195
  `__get` — `isset()`/`?? null` always read false/null. See
162
196
  [_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the
@@ -168,6 +202,14 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
168
202
 
169
203
  ## Change history
170
204
 
205
+ - 2026-07-13 — Extended the translation layer to item **feature** text: three new sidecar tables
206
+ (`FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`, `ItemFeatureTranslations`; Records
207
+ 343/344/345, RecordFields 2442–2457) with `translationRecordFieldId` wired to the source fields,
208
+ so feature names, group headers, and feature values translate on read exactly like Item text. Fixed
209
+ the all-fields (expand-everything) nested-child serializer branch in `V2.php` (~L5958) to route
210
+ through `getTranslatedFieldValue()` — previously it copied raw base-English child values (Compass
211
+ Canada French group headers stayed English while values were French); the `depth:6` theory was wrong
212
+ and reverted. Noted the sidecar/source collation-mismatch gotcha. (bala)
171
213
  - 2026-06-23 — Initial build: audience.language + JWT embedding, ItemTranslations sidecar + metadata +
172
214
  full ACL chain, and translation-aware read/write at the API layer. Also fixed a latent autoload bug
173
215
  by renaming `DefaultFlobalSetting.php` → `DefaultGlobalSetting.php`. (jcardinal)
@@ -6,13 +6,15 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-26
9
+ updated: 2026-07-14
10
10
  owners: [bala]
11
11
  files:
12
12
  - worker2/Worker/Etilize/ItemTranslations.php
13
13
  related:
14
14
  - ./creating-worker-actions.md
15
15
  - ./etilize-catalog-item-import.md
16
+ - ../../api2/features/language-translation-layer.md
17
+ - ../../../clients/compass-canada/features/french-item-feature-translations.md
16
18
  ---
17
19
 
18
20
  ## Summary
@@ -35,6 +37,28 @@ Routes are `Etilize/ItemTranslations/<Method>` (see
35
37
  — translates **all** items that have a `c_etilizeProductId`.
36
38
  2. `ImportItemsLanguageByPartNumbers(string $client, string $languageCode, array $partNumbers, ?string $languageName = null, ?string $catalog = null)`
37
39
  — translates only the given part numbers (uses a `partNumber IN (...)` filter).
40
+ 3. `BackfillEtilizeAttributeIds(...)` — populates `c_etilizeAttributeId` (on `Features`) and
41
+ `c_etilizeAttributeGroupId` (on `ItemCategoryFeatureGroups`) from Etilize `en_us`, so features and
42
+ groups can be matched to their Etilize attribute id. Must run **before** feature-translation import
43
+ so each French value lands on the right feature/group.
44
+
45
+ ## Feature-text translation (item features, not just Item title/desc)
46
+
47
+ Beyond `ItemTranslations` (Item title/description/longDescription), `importItemLanguage` now also
48
+ writes **feature** translations via `importItemFeatureTranslations` → `translateAttributeGroup` /
49
+ `translateAttribute`, into the `FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`, and
50
+ `ItemFeatureTranslations` sidecar tables (see the shared
51
+ [Language Translation Layer](../../api2/features/language-translation-layer.md) for the read-side
52
+ metadata wiring). Matching is by Etilize **attribute id** (`c_etilizeAttributeId`) and **attribute
53
+ group id** (`c_etilizeAttributeGroupId`), which is why the backfill action above must run first.
54
+
55
+ - **Group translation is scoped per-item, not catalog-wide.** `findGroupIdsForItemByEtilizeAttributeGroupId`
56
+ joins through the item's own `ItemFeatures → ItemCategoryFeatures → groups` so only the groups that
57
+ item actually uses are translated. Doing it catalog-wide would write thousands of unrelated group rows.
58
+ - Constants: `ETILIZE_LOCALE_ENGLISH = 'en_us'`, `ETILIZE_GENERAL_ATTRIBUTE_GROUP_ID = '35'`,
59
+ `FEATURE_VALUE_MAX_LENGTH = 254`.
60
+ - `RefreshItemsFromEtilize` is idempotent — it reuses existing features/groups by name (and by
61
+ item+feature), so re-running does not duplicate.
38
62
 
39
63
  ## How it works
40
64
 
@@ -61,8 +85,21 @@ Items **not published in the requested locale are skipped**.
61
85
  - Only items with a `c_etilizeProductId` are translatable — items without an Etilize id
62
86
  are silently skipped.
63
87
  - English is intentionally never written here; it lives in the `Items` table.
88
+ - **Use `_Query::getAffectedRows()`, not `rowsAffected()`.** There is no `rowsAffected()` method
89
+ (the CLAUDE.md doc was wrong); calling it throws "Call to undefined method".
90
+ - **OOM on a full (~200-item) run.** `_Database` caches every SELECT in `$_queryCache`, so a large
91
+ batch grows unbounded. Call `_Database::useQueryCache(false)` and `gc_collect_cycles()` **per item**
92
+ inside the batch loops.
93
+ - **`clampFeatureValue` must use `mb_strlen` / `mb_substr` with `'UTF-8'`**, never byte-based
94
+ `strlen`/`substr`, or a French accent gets cut mid-character (`FEATURE_VALUE_MAX_LENGTH = 254`).
64
95
 
65
96
  ## Change history
97
+ - 2026-07-13 — Added `BackfillEtilizeAttributeIds` action and extended `importItemLanguage` to import
98
+ item **feature** translations (`importItemFeatureTranslations` → `translateAttributeGroup` /
99
+ `translateAttribute`) into the Feature/group/item-feature sidecar tables; groups scoped per-item via
100
+ `findGroupIdsForItemByEtilizeAttributeGroupId` to avoid catalog-wide writes. Durable gotchas:
101
+ `getAffectedRows()` (no `rowsAffected()`); disable `_Database` query cache + `gc_collect_cycles()`
102
+ per item to avoid OOM on full runs; `mb_`-based value clamp for accents. (bala)
66
103
  - 2026-06-26 — Documented `_Worker_Etilize_ItemTranslations`: per-language Etilize
67
104
  `getProduct` import into `ItemTranslations` (title=desc type 2, longDescription=type 1),
68
105
  locale derived from language code, COALESCE upsert that never overwrites with blanks,
@@ -27,7 +27,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
27
27
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
28
28
  - **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
29
29
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
30
- - **ai-bdr** (AI-BDR) — 6 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
30
+ - **ai-bdr** (AI-BDR) — 7 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
31
31
  - **toga2-commerce** (TOGa Commerce) — 9 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
32
32
  - **toga25-supply** (TOGa 2.5 Supply) — 8 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
33
33
  - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
@@ -2,5 +2,6 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
+ | [French (fr-CA) Item Feature Translations (Compass Canada)](features/french-item-feature-translations.md) | 2.0 | Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group headers (e.g. | _underscore/Model/Compass/Canada/Feature.php, _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, worker2/Worker/Etilize/ItemTranslations.php, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql, dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql, dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql, dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql |
5
6
  | [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
6
7
  | [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. | |
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: French (fr-CA) Item Feature Translations (Compass Canada)
3
+ framework: "2.0"
4
+ project: _Underscore
5
+ client: compass-canada
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-07-14
9
+ owners: ["bala"]
10
+ files:
11
+ - _underscore/Model/Compass/Canada/Feature.php
12
+ - _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php
13
+ - _underscore/Model/Client/FeatureTranslation.php
14
+ - _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php
15
+ - _underscore/Model/Client/ItemFeatureTranslation.php
16
+ - worker2/Worker/Etilize/ItemTranslations.php
17
+ - dbchanges2/Client/2026-07-13a - FeatureTranslations.sql
18
+ - dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql
19
+ - dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql
20
+ - dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql
21
+ - dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql
22
+ - dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql
23
+ related:
24
+ - ../profile.md
25
+ - ../../../2.0/apps/api2/features/language-translation-layer.md
26
+ - ../../../2.0/apps/worker2/features/etilize-item-translation-import.md
27
+ ---
28
+
29
+ ## Summary
30
+ Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group
31
+ headers (e.g. "Processeur", "Mémoire", "Chipset"), and feature values — in fr-CA. It layers the shared
32
+ metadata-driven sidecar translation infrastructure ([Language Translation Layer](../../../2.0/apps/api2/features/language-translation-layer.md))
33
+ on top of a French **import** from Etilize ([Etilize Item Translation Import](../../../2.0/apps/worker2/features/etilize-item-translation-import.md)),
34
+ matched to the right feature/group by Etilize **attribute id**. fr-CA is `languageId 2`.
35
+
36
+ This replaces the old approach where the `worker/crons/toga2/compasscanada/Imports2.0/Import_items.php`
37
+ cron wrote French straight into the **base** fields and minted new group UUIDs every run — corrupting
38
+ English and producing thousands of duplicate features/groups.
39
+
40
+ ## Compass-Canada-specific pieces
41
+ - **Custom match fields** (Compass Canada only, shipped via `dbchanges2/Client_CompassCanada/`, models
42
+ under `_underscore/Model/Compass/Canada/`):
43
+ - `c_etilizeAttributeId` on `Feature` (`Model/Compass/Canada/Feature.php`)
44
+ - `c_etilizeAttributeGroupId` on `ItemCategoryFeatureGroup` (`Model/Compass/Canada/ItemCategoryFeatureGroup.php`)
45
+ These let the importer match an English feature/group to its Etilize attribute id so the correct
46
+ French value lands on the right feature. Populate them first with the worker2
47
+ `BackfillEtilizeAttributeIds` action.
48
+ - **fr-CA = `languageId 2`** (see also profile: `UserGlobalSettings.settingId = 2`, `en` / `fr-CA`).
49
+ - The shared sidecar tables (`FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`,
50
+ `ItemFeatureTranslations`) and Core Records 343/344/345 + RecordFields 2442–2457 are documented in the
51
+ shared layer doc; only the custom fields and the seed/dedup data are Compass-Canada-specific.
52
+
53
+ ## Rollout order (dev/beta done, prod pending)
54
+ 1. Deploy the translation schema (sidecars + ACL + Core RecordFields) with the correct **env-specific**
55
+ Core Record/RecordField ids.
56
+ 2. **Dedup** existing features/groups (see below).
57
+ 3. `BackfillEtilizeAttributeIds` — populate the custom match fields from Etilize `en_us`.
58
+ 4. Import French translations (worker2 Etilize feature-translation import).
59
+ 5. Seed any remaining French feature translations.
60
+ 6. **Retire** the old `worker/crons/toga2/compasscanada/Imports2.0/Import_items.php` cron.
61
+
62
+ Prod rollout is deferred until explicitly requested; the dev/beta build (beta DB) is complete.
63
+
64
+ ## Dedup of duplicate features/groups
65
+ - **Root cause:** the old `Import_items.php` cron did unconditional POSTs and minted new group UUIDs on
66
+ every run → ~7700 duplicate features and ~2100 duplicate groups on dev; prod counts ~12665→4932
67
+ features and ~2390→263 groups.
68
+ - **Migration technique (team practice):** the dedup migration must **not** use `CREATE TEMPORARY
69
+ TABLE` — use inline **derived-table subqueries**. MySQL 8 materializes them and avoids error 1093
70
+ (can't reopen the table being updated).
71
+ - **Keep-which-row differs by env:** the **dev** variant prefers the already-translated duplicate —
72
+ `COALESCE(MIN(CASE WHEN translated THEN id END), MIN(id))`; the **prod** variant keeps the **lowest
73
+ id**.
74
+
75
+ ## Gotchas / known issues
76
+ - **Sidecar/source collation mismatch.** Sidecar tables are `utf8mb4_unicode_ci` (matching
77
+ `ItemTranslations`); the source fields are `utf8mb4_0900_ai_ci`. Any ad-hoc query comparing across the
78
+ two needs `COLLATE utf8mb4_bin` or MySQL throws a collation-mismatch error.
79
+ - **Etilize attribute ids of the form `34xxxxxx` are NOT corrupted** (false alarm resolved). A manual
80
+ XML showing `32xxxxxx` came from a different Etilize view; the worker's Etilize endpoint returns
81
+ `34xxxxxx` consistently for both backfill and import, so translations match. Do not "fix" them.
82
+ - **A shared `c_etilizeProductId` across two part numbers can be a data error.** On dev, partNumber
83
+ `BT6L3UC#ABA` had the wrong shared `c_etilizeProductId 1083084905` (shared with `B4TM0UC#ABA`); prod's
84
+ correct value is `1086408433`. Fixed by a single `UPDATE` — this was a data error, not a code bug.
85
+
86
+ ## Change history
87
+ - 2026-07-13 — Built the fr-CA item-feature translation path end-to-end on dev/beta: shared feature
88
+ sidecar tables + Core wiring, Compass-only `c_etilizeAttributeId` / `c_etilizeAttributeGroupId` custom
89
+ fields, worker2 backfill + feature-translation import, dedup of ~7700 dup features / ~2100 dup groups,
90
+ French seed. Corrected the `34xxxxxx` attribute-id false alarm and fixed the `BT6L3UC#ABA` shared
91
+ `c_etilizeProductId` data error. Prod rollout + retiring the old `Import_items.php` cron deferred. (bala)
92
+
93
+ ## Related docs
94
+ - [Compass Canada profile](../profile.md)
95
+ - [Language Translation Layer](../../../2.0/apps/api2/features/language-translation-layer.md)
96
+ - [Etilize Item Translation Import](../../../2.0/apps/worker2/features/etilize-item-translation-import.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.331",
3
+ "version": "1.0.333",
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",