toga-ai 1.0.588 → 1.0.590

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.
@@ -32,7 +32,7 @@
32
32
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
33
33
  | [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | `_Trait_Netsuite_SalesOrder` is the **shared** sales-order importer composed into **22 client models** (every client on the dbchanges2 `netsuite` module). | _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Model.php, dbchanges2/_modules/netsuite/2026-08-10a - AddressPhoneNumberApiRoleAcl.sql |
34
34
  | [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | `_Model_Core_Page::meta()` is the **legacy** page-meta resolver behind `GET /pages/meta?slug=<slug>` — still the live path for `toga2-supply` and other pre-Surf | _underscore/Model/Core/Page.php, _underscore/Model/Client/TableView.php |
35
- | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
35
+ | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Model.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
36
36
  | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
37
37
  | [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
38
38
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
@@ -6,10 +6,11 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-30
9
+ updated: 2026-08-17
10
10
  owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi"]
11
11
  files:
12
12
  - _underscore/Database.php
13
+ - _underscore/Model.php
13
14
  - _underscore/Query.php
14
15
  - _underscore/ApiRequest.php
15
16
  - _underscore/Model/Client/Logs/Api.php
@@ -177,8 +178,48 @@ here — they live in `Config/*.ini`.)
177
178
  (`db_logs`) — the laptop trap there is documented separately in the worker NetSuite bootstrap
178
179
  notes.
179
180
 
181
+ ## ⚠ OPEN QUESTION — does the alias-keyed `$_modelCache` leak across tenants? (UNPROVEN)
182
+
183
+ **Do not repeat this as an established leak, and do not dismiss it either.** It needs a focused
184
+ security review. Observed 2026-08-17 (TRUE-80562):
185
+
186
+ An **AIG** `POST /v2/entitlements` carrying nested `entitlementUnits` emitted an
187
+ `INSERT INTO Units` containing `c_itemDescription`, `c_lastModified`, `c_chargingBrick`,
188
+ `c_deviceStatus`, `c_chargingCable`, `c_grade` → **MySQL 1054**, surfaced as EV-12 with a
189
+ `modelSaveError`.
190
+
191
+ **Verified facts:**
192
+
193
+ - `Client_Aig.Units` has **only** `c_netsuiteInternalInventoryAssignmentId`, and
194
+ `Client_Aig.CustomRecordFields` declares only that one field for `Units` (recordId 31) — AIG's
195
+ **schema and metadata agree**.
196
+ - Those six columns exist **only** in `Client_Adyen`, `Client_Elite`, `Client_Growrk`,
197
+ `Client_Prudential`. Five are declared **only** on `_Model_Growrk_Unit`; `_Model_Aig_Unit` extends
198
+ `_Model_Client_Unit` and declares none. `c_lastModified` is declared in **no** model anywhere in
199
+ `_underscore`.
200
+ - So the field list is **neither** AIG's schema, **nor** AIG's metadata, **nor** any model class AIG
201
+ uses — it is exactly **four other tenants'** column set.
202
+
203
+ **Hypothesis (not proven):** `_underscore/Model.php` (~676–687) caches query results as
204
+ `_Database::$_modelCache[$this::DATABASE][$sql]`, and `DATABASE` is the shared **alias** `'Client'`
205
+ — *not* the tenant schema name — so two tenants collide on the same cache key (the same alias-keying
206
+ already documented in the gotcha above). **Unknown: whether that static survives across requests
207
+ under PHP-FPM.** That single unknown is the difference between a harmless within-request artifact and
208
+ a **cross-tenant data-leak vector**, which is why it must be answered rather than assumed.
209
+
210
+ **Where to look:** `_underscore/Model.php` ~676–687, `api2/Component/Api/V2/V2.php` ~8918–8931,
211
+ `_underscore/Model/Growrk/Unit.php`, `_underscore/Model/Aig/Unit.php`. Context:
212
+ [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md).
213
+
180
214
  ## Change history
181
215
 
216
+ - 2026-08-17 — Recorded an **unproven open question** (TRUE-80562): an AIG nested `Units` write
217
+ emitted four *other* tenants' `c_` columns (1054 → EV-12) although AIG's schema, its
218
+ `CustomRecordFields` metadata, and its model class all declare exactly one custom field. Suspected
219
+ mechanism is the **alias-keyed `_Database::$_modelCache['Client'][$sql]`** collision; whether the
220
+ static survives across PHP-FPM requests is unknown, and that determines whether this is a
221
+ within-request artifact or a cross-tenant leak. Flagged for security review; no code changed.
222
+ (mhammontree)
182
223
  - 2026-07-30 — Added four framework-level gotchas surfaced while building the all-client email
183
224
  queue monitor: the log-DB name has the same never-string-build rule as the client DB;
184
225
  `registerClientDatabases()` is unsafe for an all-client loop (INNER JOINs on client+archive
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-03
9
+ updated: 2026-08-17
10
10
  owners: ["bala", "mhammontree", "tcox"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -115,6 +115,38 @@ columns matter:
115
115
  > Correct the data with an explicit `overrideIsIdentifier = 1`, or (systemic fix, not yet done)
116
116
  > change the code so only an explicit `0` removes an identifier and `NULL` means "no override".
117
117
 
118
+ ### Fix it with a migration — a manual one-row data patch on non-prod DOES NOT SURVIVE
119
+
120
+ The TRUE-79978 beta fix was applied by hand as a single `UPDATE` on 2026-07-09. The **identical
121
+ EV-12 recurred on beta on 2026-07-21** (3 requests, `Logs_Aig.Api` ids 93 / 95 / 98) with the same
122
+ signature. Evidence the tenant was **reseeded** underneath the patch: those log rows report
123
+ `apiId 2` as *"AIG Integrations"*, while `Client_Aig.Apis` on beta now names id 2 *"Agilant"* — the
124
+ tenant data was rewritten after 2026-07-21, which is also where an incidental
125
+ `overrideIsIdentifier = 1` on one row came from (not a deliberate re-patch).
126
+
127
+ > **Rule: any `Apis_RecordFields` identifier correction must ship as a `dbchanges2/Client_<Tenant>/`
128
+ > migration.** A manual patch on beta / dev-sandbox evaporates on the next tenant reseed and the
129
+ > bug returns looking brand new. (TRUE-80562.)
130
+
131
+ **Scope rule when correcting these rows — only touch fields whose BASE flag is already 1.** Fix a
132
+ `NULL` `overrideIsIdentifier` **only** where `Core.RecordFields.isIdentifier = 1`. A `NULL` override
133
+ on a base-`0` field is a harmless no-op; setting it to `1` **adds** an identifier the platform never
134
+ had and changes nested match-vs-create behavior for that API. On beta `Client_Aig` there were 15
135
+ override rows (all `apiId 2`) but only **5** qualified: `recordFieldId` **1202**
136
+ (*Entitlement fulfillment types.Name*), **1216** (*Entitlement fulfillment methods.Name*), **1259**
137
+ (*Entitlement units.Unitid*), **1386** (*Entitlement coverage types.Name*), **1651**
138
+ (*Contacts.Customerid*).
139
+
140
+ **Expect a next landmine — fix the whole set, not the one field in the error.** Correcting only
141
+ 1202 moved the same EV-12 onto `entitlementFulfillmentMethod` (1216), because
142
+ `_Model_Aig_Entitlement::prePost` injects **both** name-only objects. Audit every qualifying row in
143
+ one migration.
144
+
145
+ **Diagnostic fingerprint:** the EV-12's `searchableIdentifierFields` list is the record's identifier
146
+ set **minus the stripped field** — e.g. `["id","uuid","code"]` on a record whose identifiers are
147
+ Id/Uuid/Code/**Name** means `name` was stripped by an override row. Read that list first; it names
148
+ the culprit without any DB query.
149
+
118
150
  **Build divergence warning.** The `Apis_RecordFields` override feature (reading
119
151
  `overrideIsIdentifier`/`overrideChildPolicy`) was introduced 2026-03-20 (api2 commit `624146d`
120
152
  "Cache custom fields and API override lookups"). Environments on **older** builds ignore the
@@ -174,6 +206,16 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
174
206
 
175
207
  ## Change history
176
208
 
209
+ - 2026-08-17 — TRUE-80562: the TRUE-79978 **manual one-row beta patch did not survive** — the same
210
+ EV-12 recurred on beta 2026-07-21 (`Logs_Aig.Api` ids 93/95/98) after a tenant reseed (log rows
211
+ call `apiId 2` "AIG Integrations"; beta `Client_Aig.Apis` now names it "Agilant"). Identifier
212
+ corrections must ship as a `dbchanges2/Client_<Tenant>/` migration. Added the **scope rule** (only
213
+ fix `NULL` overrides where `Core.RecordFields.isIdentifier = 1` — 5 of 15 AIG rows qualified:
214
+ 1202/1216/1259/1386/1651), the **next-landmine** warning (fixing 1202 alone moves the EV-12 to
215
+ 1216 because `prePost` injects both objects), and the **`searchableIdentifierFields` fingerprint**
216
+ (base identifier set minus the stripped field names the culprit without a query). Verified
217
+ 2026-08-17: `POST /v2/entitlements` on beta returned 201 with both injected fields resolved.
218
+ (mhammontree)
177
219
  - 2026-08-03 (later pass) — **Correction, supersedes the entry below on one point.** The bare-string
178
220
  collection shape does **not** make the addresses "vanish"/blank the consumer's field: the empty
179
221
  `array_column()` result makes the consumer's primary-email fallback fire, so the stored value is
@@ -7,4 +7,4 @@
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
8
8
  | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
9
9
  | [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
10
- | [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini |
10
+ | [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql |
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-14
9
+ updated: 2026-08-17
10
10
  owners: ["mhammontree", "bala"]
11
11
  files:
12
12
  - dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
@@ -14,6 +14,9 @@ files:
14
14
  - dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql
15
15
  - dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql
16
16
  - api2/Config/beta.ini
17
+ - api2/Config/sandbox-dev.ini
18
+ - dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql
19
+ - dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql
17
20
  related:
18
21
  - ../architecture.md
19
22
  - ./client-schema-drift-audit.md
@@ -41,8 +44,12 @@ exercise TOGa Supply Fulfill & Ship (TRUE-80494).
41
44
  ## Steps
42
45
 
43
46
  1. **Identify which cluster the environment actually reads.** `api.beta.togahub.com` reads the
44
- **dev-sandbox** cluster (`api2/Config/beta.ini` `[database] hostname =
45
- dev.sandbox.database.togahub.com`) — "beta" is not its own database. Fix the right DB.
47
+ **dev-sandbox** cluster — "beta" is not its own database. Fix the right DB. Confirmed again
48
+ 2026-08-17 from the other side: **`api2/Config/sandbox-dev.ini`** carries
49
+ `[api] _ = "https://api.beta.togahub.com/v2"` and `hostname = dev.sandbox.database.togahub.com`.
50
+ So the toga-db alias to target is **`dev-sandbox`**, *not* `client-beta` — e.g. `Client_Aig`
51
+ exists only in **prod and dev-sandbox** and is absent from `client-beta` entirely. Picking the
52
+ wrong alias wastes a full diagnostic cycle.
46
53
  2. **Full-table compare the small metadata tables, prod vs. target.** `ApiPayloadInterceptors` in
47
54
  particular is small enough to diff wholesale, and the drift is typically **exactly one row**
48
55
  (dev-sandbox was missing `(recordId 28, PRE, POST)`, so `prePost` never defaulted
@@ -78,6 +85,48 @@ exercise TOGa Supply Fulfill & Ship (TRUE-80494).
78
85
  6. **Re-test the feature end-to-end in that environment**, then record what is still missing so the
79
86
  next person doesn't rediscover it.
80
87
 
88
+ ## The `Client/` fan-out is skipped independently of `Core/` — and the gap is NON-CONTIGUOUS
89
+
90
+ Measured on dev-sandbox 2026-08-17 (TRUE-80562): **31 of 33 tenant DBs lack
91
+ `Entitlements.serviceAddressId`** (only `Client_Rate` and `Client_Elite` have it), while
92
+ **Core does have the RecordField (id 2763)**. So `Core/2026-07-22a` ran and the
93
+ **`Client/` fan-out of the same change did not.** Symptom: **EV-10 / MySQL 1054 "Unknown column
94
+ 'serviceAddressId'" on every entitlement write** — for 31 tenants, not just the one you are
95
+ debugging. That migration's own header states the column must exist for all tenants *before* the
96
+ `_underscore` code deploys, so the code is live against schemas that never got the column.
97
+
98
+ > **Do not conclude "beta is behind as of date X."** The gap is **partial and non-contiguous** —
99
+ > verified against each file's actual `ALTER`:
100
+ >
101
+ > | State | `Client/` file |
102
+ > |---|---|
103
+ > | **MISSING** | `2026-07-10a` ItemFulfillments.returnAddressId |
104
+ > | **MISSING** | `2026-07-16a` TrackingNumbers.signatureType |
105
+ > | **MISSING** | `2026-07-22a` Entitlements.serviceAddressId |
106
+ > | **MISSING** | `2026-07-28a` TicketStages.ticketTypeId |
107
+ > | PRESENT | `2026-07-17` Items.isFulfillable |
108
+ > | PRESENT | `2026-07-22b` TrackingNumbers.length / weightMeasureId |
109
+ >
110
+ > A skipped `07-22a` sits **beside a `07-22b` that ran**. Check file-by-file; a date cutoff will
111
+ > mislead you.
112
+
113
+ **Fix direction: run the existing pending `Client/` files — do NOT author a new migration**, which
114
+ would duplicate a file already on `_main`. A scoped manual `ALTER` on one tenant unblocks your own
115
+ testing but leaves the other 30 broken; say so when you do it.
116
+
117
+ **LEAD (unproven) — duplicate letter suffixes may be why the executor skipped it.** `dbchanges2/Client/`
118
+ contains **three** files all named `2026-07-22a` (`EntitlementServiceAddressId`, `MeasureUomColumns`,
119
+ `Notification&PersonaTranslations`), violating the one-letter-per-day naming contract in
120
+ [architecture.md](../architecture.md). The skipped file is one of that trio. **Worth checking whether
121
+ the executor dedupes on date+letter** — if it does, the naming violation is the root cause and the
122
+ same trap is live wherever letters collide.
123
+
124
+ **Rule violation worth fixing separately:** `Client/2026-07-23a -
125
+ EntitlementServiceAddressIdFieldPermission.sql` references **`Core.RecordFields` and `Core.Records`
126
+ from a `Client/` fan-out file**, breaking the hard one-database-per-file cluster-isolation rule. Its
127
+ own comment says *"same server"* — which is exactly the trap the rule exists for: it works on
128
+ dev-sandbox (shared cluster) and would fail in production, where `Core` is a separate cluster.
129
+
81
130
  ## Environment facts worth knowing before you test here
82
131
 
83
132
  - **NO carrier can be tested on ANY non-production environment.** Beta runs
@@ -156,6 +205,17 @@ blocked by the id-divergence hazard in item 2 above, which is why both go to jca
156
205
 
157
206
  ## Change history
158
207
 
208
+ - 2026-08-17 — TRUE-80562: documented that the **`Client/` fan-out is skipped independently of
209
+ `Core/`** on dev-sandbox — 31 of 33 tenant DBs lack `Entitlements.serviceAddressId` (only
210
+ `Client_Rate`/`Client_Elite` have it) while Core carries the RecordField (2763), so **entitlement
211
+ writes EV-10/1054 for 31 tenants**. The gap is **partial and non-contiguous** (missing 07-10a,
212
+ 07-16a, 07-22a, 07-28a; present 07-17, 07-22b), so a date cutoff misleads. Fix = run the existing
213
+ pending files, never author a duplicate. Recorded the unproven **duplicate-letter lead** (three
214
+ `Client/2026-07-22a` files; the skipped one sits beside a `07-22b` that ran — check whether the
215
+ executor dedupes on date+letter) and a **cluster-isolation violation** in `Client/2026-07-23a`
216
+ (reads `Core.RecordFields`/`Core.Records` from a `Client/` file, justified as "same server").
217
+ Step 1 now cites `api2/Config/sandbox-dev.ini` and the `dev-sandbox` vs `client-beta` alias trap.
218
+ (mhammontree)
159
219
  - 2026-08-14 — Recorded that metadata drift runs in **both** directions: a cleanup migration that
160
220
  *removed* a row in production is as easily skipped in non-prod as an additive one. Evidence —
161
221
  `Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql` had never run on
@@ -5,7 +5,7 @@
5
5
  | [TOGa Blox (toga-blox) Architecture](architecture.md) | `toga-blox` (npm package **`@agilant/toga-blox`**) is the **shared React + TypeScript component / template / utility library** reused across the 2.0 frontends — | toga-blox/package.json, toga-blox/src/components/index.ts, toga-blox/src/api/index.ts, toga-blox/src/templates/index.ts |
6
6
  | [AddNotes template — badge style/label decoupling & Badge style passthrough](features/add-notes-template.md) | The `@agilant/toga-blox` `AddNotesTemplate` renders a list of notes, each with a per-note contact **badge** (e.g. | toga-blox-npm/src/templates/AddNotes/types.ts, toga-blox-npm/src/templates/AddNotes/AddNotesTemplate.tsx, toga-blox-npm/src/components/Badge/Badge.types.tsx, toga-blox-npm/src/components/Badge/Badge.tsx |
7
7
  | [AdvancedSelect (virtualized single/multi select)](features/advanced-select.md) | `AdvancedSelect<T>` is a **fully controlled, virtualized** single/multi select built from scratch (not react-select) on **`@tanstack/react-virtual`**, for large | toga-blox/src/components/AdvancedSelect/AdvancedSelect.tsx, toga-blox/src/components/AdvancedSelect/AdvancedSelect.types.ts, toga-blox/src/components/AdvancedSelect/AdvancedSelect.module.css |
8
- | [API client (axios wrapper, auth, table-data fetchers)](features/api-client.md) | `src/api/` is a thin **axios** wrapper that standardizes the **2.0 API envelope**, manages auth (Bearer + refresh), serializes complex query options, and provid | toga-blox/src/api/index.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/api/apiFunctions.ts, toga-blox/src/api/auth.ts, toga-blox/src/api/genericApi.ts, toga-blox/src/api/types.ts, toga-blox/src/api/tableData |
8
+ | [API client (axios wrapper, auth, table-data fetchers)](features/api-client.md) | `src/api/` is a thin **axios** wrapper that standardizes the **2.0 API envelope**, manages auth (Bearer + refresh), serializes complex query options, and provid | toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/api/index.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/api/apiFunctions.ts, toga-blox/src/api/auth.ts, toga-blox/src/api/genericApi.ts, toga-blox/src/api/types.ts, toga-blox/src/api/tableData |
9
9
  | [BaseInput (react-hook-form field factory)](features/base-input.md) | `BaseInput` is a **form-field factory** driven by react-hook-form. | toga-blox/src/components/BaseInput/BaseInput.tsx, toga-blox/src/components/BaseInput/BaseInput.types.ts, toga-blox/src/components/BaseInput/BaseInput.module.css, toga-blox/src/components/BaseInput/components |
10
10
  | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
11
11
  | [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
@@ -6,9 +6,10 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-13
9
+ updated: 2026-08-17
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
+ - toga-blox/src/reactQuery/queryHelpers.ts
12
13
  - toga-blox/src/api/index.ts
13
14
  - toga-blox/src/api/axiosInstance.ts
14
15
  - toga-blox/src/api/apiFunctions.ts
@@ -105,8 +106,25 @@ Filterable`, `hyperlinkField`, `imageUrlField`, `sticky`, `precision`, …), `Da
105
106
  `Core.Domains` row, not frontend code. See
106
107
  [encrypted user-uuid auth handoff](../../api2/features/encrypted-user-uuid-auth-handoff.md) for
107
108
  the same gating on the SSO handoff route.
109
+ - **⚠ `assembleOptionsWhere` (`reactQuery/queryHelpers.ts`) expects every `where.and`/`where.or`
110
+ entry to be a STRUCTURED OBJECT** (`{field:{op:value}}`). A **raw API-syntax string** clause —
111
+ e.g. a metadata `apiWhereClause` like `(IFNULL(Items.assetTypeId,0):ne:3)` that
112
+ `getDataTableData` pushes into `apiOptions.where.and` — is iterated with `for (const field in
113
+ entry)`, which on a string walks **character indices**, emitting garbage `index:0:char`
114
+ conditions (`0:0:(`, `1:0:I`, …). api2 v2 then rejects the request with **`Invalid operator '0'`
115
+ → EO-1 500**. Symptom: opening a modal/table whose `tableViewMeta` carries an `apiWhereClause`
116
+ (seen in the Compass USA Item Fulfillment modal) 500s. Fix (in place since 2026-08-17): the
117
+ serializer now passes any `typeof === "string"` entry through **verbatim**, stripping one outer
118
+ paren pair so the group re-wrap doesn't double-wrap. Any raw pre-formatted where clause was
119
+ 100% broken before this. If you add a new raw-string clause path, route it through this string
120
+ branch — do **not** feed a raw clause into an object-expecting serializer.
108
121
 
109
122
  ## Change history
123
+ - 2026-08-17 — Fixed `assembleOptionsWhere` spreading a raw-string `apiWhereClause` character-by-
124
+ character into invalid `index:0:char` conditions (api2 `Invalid operator '0'` / EO-1 500 opening
125
+ the Compass USA Item Fulfillment modal). String entries now pass through verbatim (single outer
126
+ paren pair stripped). Library-wide fix in `reactQuery/queryHelpers.ts`, not client-specific.
127
+ (apeterson)
110
128
  - 2026-08-13 — TRUE-80091: recorded that `fetchPublicToken()`/`POST /auth/public` is **origin-gated
111
129
  against `Core.Domains` (EN-6 on an unregistered origin)** and that each host variant needs its own
112
130
  row — `www.togatech.com` is rejected while `togatech.com` and `localhost` are allowed, which
@@ -3,5 +3,5 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [AIG Contract Reconciliation & Dealer Programs (STS_001 / SA_001)](features/contract-reconciliation.md) | 2.0 | How to reconcile an **AIG contract sales sheet** against the `Client_Aig` tenant, and the durable finding that the **Staples Advantage.com (`SA_001`) program do | _underscore/Model/Client/Entitlement.php, _underscore/Model/Aig/Entitlement.php, _underscore/Model/Client/Contact.php, _underscore/Model/Aig/Contact.php |
6
- | [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path * | _underscore/Model/Aig/Entitlement.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql |
6
+ | [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path * | _underscore/Model/Aig/Entitlement.php, _underscore/Model/Aig/Unit.php, api2/Component/Api/V2/V2.php, api2/Config/sandbox-dev.ini, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql, dbchanges2/Client_Aig/2026-08-17a - TRUE-80562 AIG API identifier overrides.sql |
7
7
  | [AIG (Staples Protection Plan)](profile.md) | 2.0 | AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. | |
@@ -5,12 +5,15 @@ project: API
5
5
  client: aig
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-08-10
8
+ updated: 2026-08-17
9
9
  owners: ["mhammontree"]
10
10
  files:
11
11
  - _underscore/Model/Aig/Entitlement.php
12
+ - _underscore/Model/Aig/Unit.php
12
13
  - api2/Component/Api/V2/V2.php
14
+ - api2/Config/sandbox-dev.ini
13
15
  - dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
16
+ - dbchanges2/Client_Aig/2026-08-17a - TRUE-80562 AIG API identifier overrides.sql
14
17
  related:
15
18
  - clients/aig/features/contract-reconciliation.md
16
19
  - 1.0/apps/library/features/toga2-api-client-and-bridge.md
@@ -18,7 +21,9 @@ related:
18
21
  - 2.0/apps/api2/features/nested-relationship-writes.md
19
22
  - 2.0/apps/api2/architecture.md
20
23
  - 2.0/apps/dbchanges2/architecture.md
24
+ - 2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md
21
25
  - 2.0/apps/_underscore/architecture.md
26
+ - 2.0/apps/_underscore/features/per-client-database-connections.md
22
27
  ---
23
28
 
24
29
  ## Summary
@@ -149,6 +154,93 @@ recovered on 07-29 exactly as the resolution above describes. But the creating p
149
154
  rows (through 2026-08-04) was **not positively identified** — confirm it in `Logs_Aig.Api` before
150
155
  asserting either "intake is entirely down" or "intake is healthy."
151
156
 
157
+ > **Partly answered 2026-08-17 (TRUE-80562):** `Logs_Aig.Api` **does** carry non-2xx entitlement
158
+ > writes on beta (ids 93 / 95 / 98, all 400/EV-12 on 2026-07-21), so the "failures only ever land in
159
+ > core `Logs`" reading is too strong — `Logs_Aig` is a usable first stop for beta 4xx. Only 5xx-class
160
+ > failures reliably escape to the core/writer `Logs`.
161
+
162
+ ## Which environment is "beta"? (`api.beta.togahub.com` = **dev-sandbox**)
163
+
164
+ `api2/Config/sandbox-dev.ini` carries `[api] _ = "https://api.beta.togahub.com/v2"` and a database
165
+ `hostname` of `dev.sandbox.database.togahub.com`. So for AIG/API work, **"beta" resolves to the
166
+ dev-sandbox cluster** (toga-db alias `dev-sandbox`), **not** the `client-beta` alias — which has no
167
+ `Client_Aig` schema at all. **`Client_Aig` exists only in prod and dev-sandbox.** Targeting the wrong
168
+ alias costs a whole diagnostic cycle. See
169
+ [non-prod metadata drift repair](../../../2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md).
170
+
171
+ ## EV-12 on the injected fulfillment fields — fix it with a MIGRATION (TRUE-80562)
172
+
173
+ The TRUE-79978 EV-12 (an `Apis_RecordFields` override row with `overrideIsIdentifier = NULL`
174
+ stripping `name` as an identifier) **came back on beta**. The 2026-07-09 manual one-row `UPDATE`
175
+ **did not survive**: the identical error hit `entitlementFulfillmentType` again on **2026-07-21**
176
+ (`Logs_Aig.Api` ids 93 / 95 / 98). The tenant had been **reseeded** — those log rows call `apiId 2`
177
+ *"AIG Integrations"* while beta `Client_Aig.Apis` now names id 2 *"Agilant"*.
178
+
179
+ > **Manual data patches on beta evaporate. The fix must be a `dbchanges2/Client_Aig/` migration** —
180
+ > shipped as `2026-08-17a - TRUE-80562 AIG API identifier overrides.sql`.
181
+
182
+ - **Scope rule:** correct a `NULL` `overrideIsIdentifier` **only** where the BASE
183
+ `Core.RecordFields.isIdentifier = 1`. On a base-`0` field the NULL is a harmless no-op and setting
184
+ it to `1` would **add** an identifier and change nested match-vs-create behavior. Beta
185
+ `Client_Aig` had **15** override rows (all `apiId 2`); only **5** qualified — `recordFieldId`
186
+ **1202** (Entitlement fulfillment types.Name), **1216** (Entitlement fulfillment methods.Name),
187
+ **1259** (Entitlement units.Unitid), **1386** (Entitlement coverage types.Name), **1651**
188
+ (Contacts.Customerid).
189
+ - **Fix the whole set — there is a next landmine.** Fixing only 1202 moved the same EV-12 onto
190
+ `entitlementFulfillmentMethod` (1216), because `prePost` injects **both** name-only objects.
191
+ - **Fingerprint:** the error's `searchableIdentifierFields` is the record's identifier set *minus*
192
+ the stripped field — `["id","uuid","code"]` on record 186 (identifiers Id/Uuid/Code/**Name**)
193
+ points straight at `name`.
194
+ - **Verified 2026-08-17:** `POST /v2/entitlements` on `api.beta.togahub.com` returned **201** with
195
+ both injected fields resolved, 3 coverage types and 3 contact email addresses written.
196
+
197
+ ## ⚠ `Client_Aig.Contacts.c_aigCustomerId` integer overflow — destroying identifiers IN PRODUCTION
198
+
199
+ **Needs its own ticket; higher priority than TRUE-80562.**
200
+
201
+ `Client_Aig.Contacts.c_aigCustomerId` is `int unsigned` (max **4,294,967,295**) on **both beta and
202
+ prod**, but AIG customer ids are **13 digits** (e.g. `1000045957545`). MySQL **silently clamps** every
203
+ one of them to the maximum.
204
+
205
+ - **Measured on prod 2026-08-17:** 353 contacts — **314 (89%) clamped to 4294967295**, 14 NULL, only
206
+ **26 distinct values** left.
207
+ - **Why it is severe, not cosmetic:** `c_aigCustomerId` is `isIdentifier = 1` in
208
+ `Client_Aig.CustomRecordFields`, i.e. a **searchable identifier used for nested-object matching**.
209
+ 314 contacts now share one identifier value, so a match on it is ambiguous and can attach an
210
+ entitlement / email / claim to the **wrong customer**.
211
+ - **Fix direction:** a `dbchanges2/Client_Aig/` migration to `BIGINT UNSIGNED` **stops the bleeding
212
+ but recovers nothing** — the original values are destroyed in the column. **Possible recovery
213
+ source:** the true ids are in the logged `requestPayload` of inbound entitlement POSTs
214
+ (`Logs_Aig.Api` and the core/writer `Logs`), keyed by contract number. **Confirm that before
215
+ assuming the loss is permanent.**
216
+
217
+ ## Open question — cross-tenant field list on nested `Units` writes (mechanism UNPROVEN)
218
+
219
+ An AIG POST carrying `entitlementUnits` produced an `INSERT INTO Units` that included
220
+ `c_itemDescription`, `c_lastModified`, `c_chargingBrick`, `c_deviceStatus`, `c_chargingCable`,
221
+ `c_grade` → **MySQL 1054**, surfaced as EV-12 with a `modelSaveError`.
222
+
223
+ **Verified facts:**
224
+
225
+ - `Client_Aig.Units` has **only** `c_netsuiteInternalInventoryAssignmentId`, and
226
+ `Client_Aig.CustomRecordFields` declares only that same one field for `Units` (recordId 31) —
227
+ AIG's schema and metadata **agree**.
228
+ - Those six columns exist only in `Client_Adyen`, `Client_Elite`, `Client_Growrk`,
229
+ `Client_Prudential`. Five are declared **only** on `_Model_Growrk_Unit`; `_Model_Aig_Unit` extends
230
+ `_Model_Client_Unit` and declares none. `c_lastModified` is declared in **no** model anywhere in
231
+ `_underscore`.
232
+ - So the field list is neither AIG's schema, nor AIG's metadata, nor any model class AIG uses — it is
233
+ **exactly four other tenants' column set**.
234
+
235
+ **Hypothesis — NOT PROVEN, do not repeat as fact.** `_underscore/Model.php` (~676–687) caches query
236
+ results as `_Database::$_modelCache[$this::DATABASE][$sql]`, where `DATABASE` is the shared **alias**
237
+ (`'Client'`), not the tenant schema name. It is **unknown** whether that static survives across
238
+ requests under PHP-FPM. That distinction is the difference between a harmless within-request artifact
239
+ and a **cross-tenant data-leak vector**, so it needs a focused security review before anyone dismisses
240
+ it. Entry points: `_underscore/Model.php` ~676–687, `api2/Component/Api/V2/V2.php` ~8918–8931,
241
+ `_underscore/Model/Growrk/Unit.php`, `_underscore/Model/Aig/Unit.php`. See
242
+ [per-client database connections](../../../2.0/apps/_underscore/features/per-client-database-connections.md).
243
+
152
244
  ## Business Unit (`UserDefined2`) — answered and DROPPED (nothing to build)
153
245
 
154
246
  AIG (Alissa, 2026-05-04) confirmed the **Business Unit / Value** field needs **nothing returned to
@@ -320,6 +412,20 @@ this interceptor or use this dual-purpose Items pattern.
320
412
  find a searchable identifier for the injected object. Root cause (TRUE-79978) was an
321
413
  `Apis_RecordFields` override row that stripped `name` as an identifier — see
322
414
  [api2 nested-relationship writes → Per-API overrides](../../../2.0/apps/api2/features/nested-relationship-writes.md).
415
+ - **The empty-`partNumber` variant is CONFIRMED LIVE (observed 2026-08-17, not inferred).** A
416
+ payload with `saleItem.partNumber = ""` resolved to `saleItemId = 10` — on beta a row with
417
+ `partNumber = ''` and description *"2YR Tablet: ($300-$399.99)"*, an item **unrelated to the
418
+ submitted contract**. The same payload with a real code (`SP-3DAD-LTP2`) correctly resolved to
419
+ id 1. The doc predicted this; it now has a live sighting. The guard clause is still not written.
420
+ - **`prePost` OVERRIDES the client-submitted fulfillment method and nothing signals it.** A payload
421
+ sending `entitlementFulfillmentMethod {name: "Credit"}` produced a stored record of **"Gift Card"**
422
+ plus an injected type **"Reimbursement"** — the interceptor populates both from
423
+ `Items_EntitlementFulfillmentMethods`/`Types` for the matched sale item and **discards the
424
+ client-supplied value**. This may be intended (catalog is the authority), but the **201 response
425
+ does not signal the override**, so a partner cannot tell their value was dropped. **Open question
426
+ for the integration owner** — confirm intent, and consider echoing the resolved values.
427
+ - **Beta 4xx entitlement writes DO appear in `Logs_Aig.Api`** (ids 93/95/98, 2026-07-21). Check the
428
+ client log first for validation failures; reserve the core/writer `Logs` hunt for 5xx.
323
429
  - **A lookup miss yields an empty `$itemId` that crashes the next query (confirmed in prod,
324
430
  TRUE-79534).** The unguarded line is **`_underscore/Model/Aig/Entitlement.php:48`** in `prePost`,
325
431
  reached from **`api2/Component/Api/V2/V2.php:2601`**. The malformed statement it emits is:
@@ -357,6 +463,21 @@ this interceptor or use this dual-purpose Items pattern.
357
463
 
358
464
  ## Change history
359
465
 
466
+ - 2026-08-17 — TRUE-80562 (beta EV-12 on entitlement intake). The TRUE-79978 **manual beta patch did
467
+ not survive a tenant reseed** — same EV-12 recurred 2026-07-21 (`Logs_Aig.Api` 93/95/98); the fix
468
+ now ships as `dbchanges2/Client_Aig/2026-08-17a`. Recorded the **scope rule** (only correct NULL
469
+ `overrideIsIdentifier` where base `Core.RecordFields.isIdentifier = 1` — 5 of 15 rows:
470
+ 1202/1216/1259/1386/1651), the **next landmine** (fixing 1202 alone moves the error to 1216 since
471
+ `prePost` injects both objects), and the `searchableIdentifierFields` fingerprint. **Verified: 201
472
+ on `api.beta.togahub.com`.** Added: `api.beta.togahub.com` = **dev-sandbox** per
473
+ `api2/Config/sandbox-dev.ini` (`Client_Aig` exists only in prod + dev-sandbox, never
474
+ `client-beta`); beta **4xx** writes DO land in `Logs_Aig.Api`. New **⚠ prod data defect**:
475
+ `Contacts.c_aigCustomerId` is `int unsigned` while AIG ids are 13 digits — **314 of 353 prod
476
+ contacts clamped to 4294967295** on an `isIdentifier` field (ambiguous nested matching); needs its
477
+ own ticket, BIGINT migration does not recover the values. New **open question**: a nested `Units`
478
+ write emitted four other tenants' `c_` columns (1054) — alias-keyed `_modelCache` hypothesis,
479
+ unproven, needs security review. Confirmed the empty-`partNumber` trap live (`saleItemId = 10`)
480
+ and that `prePost` silently overrides a submitted `entitlementFulfillmentMethod`. (mhammontree)
360
481
  - 2026-08-10 — Read-only prod investigation (no code/SQL changed). Added **Verified prod state**:
361
482
  the TRUE-79534 catalog **is** loaded (`Client_Aig.Items` = 2,212 rows — 1,110 `ASI-*` / 558
362
483
  `SM-*` / 37 `SP-*`; `ASI-2YG2` present), so a future 1064 here is the **empty-input** variant,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.588",
3
+ "version": "1.0.590",
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",