toga-ai 1.0.589 → 1.0.591

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,54 @@ 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
+ ## WITHDRAWN — the "alias-keyed `$_modelCache` cross-tenant leak" hypothesis (2026-08-17)
182
+
183
+ **There is no known cross-tenant leak. Do not cite this as a security concern.** An earlier entry in
184
+ this doc (same day, TRUE-80562) raised an *unproven* hypothesis that `_Database::$_modelCache`,
185
+ being keyed by the shared alias `'Client'`, was emitting one tenant's `c_` columns on another
186
+ tenant's `INSERT`. **That is not what happened.** The trigger was an AIG `Units` write failing MySQL
187
+ 1054 on six `c_` columns; the real cause was a **missing migration on beta**, not a cache collision:
188
+
189
+ - `dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql` adds all six columns
190
+ (`c_itemDescription`, `c_lastModified`, `c_chargingBrick`, `c_deviceStatus`, `c_chargingCable`,
191
+ `c_grade`) **and** their `CustomRecordFields` rows to every client whose `_modules.txt` lists
192
+ `netsuite`. **`Client_Aig/_modules.txt` contains exactly `netsuite`** — AIG is opted in, so those
193
+ are **AIG's own fields**, not another tenant's.
194
+ - **Prod `Client_Aig.Units` has all 7 custom columns.** Beta simply never ran that module file — the
195
+ same execution gap as `Entitlements.serviceAddressId`. After the module migration was run manually
196
+ on beta `Client_Aig`, `Units` gained all 7 columns and the nested unit write succeeded.
197
+
198
+ **Methodology lesson — why the wrong conclusion was reached.** The model-class comparison that
199
+ produced the leak hypothesis was made against the **local `_underscore` checkout, which was on branch
200
+ `TRUE-80282`, while beta deploys `_sandbox-dev`.** Comparing beta *runtime* behavior against a local
201
+ checkout on a different branch is **not evidence**. api2's architecture doc already warns that
202
+ `_underscore` is cloned at build time from a moving branch
203
+ ([environment-variable-drives-underscore-branch](../../api2/features/environment-variable-drives-underscore-branch.md));
204
+ this is that hazard biting in practice. Before attributing a runtime field list to a model class,
205
+ confirm which branch the environment actually built from.
206
+
207
+ **The real generalizable fact** — the beta migration fan-out gap is **not limited to `Client/`; it
208
+ also hits `_modules/<module>/`** — is recorded in
209
+ [non-prod metadata drift repair](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
210
+
180
211
  ## Change history
181
212
 
213
+ - 2026-08-17 (later pass) — **WITHDRAWN, supersedes the entry below.** The alias-keyed
214
+ `$_modelCache` cross-tenant-leak hypothesis is **not supported** and is no longer a live security
215
+ concern. The six `c_` columns are **AIG's own**: `_modules/netsuite/2026-07-10a -
216
+ UnitInventoryFields.sql` adds them to every client whose `_modules.txt` lists `netsuite`, and
217
+ `Client_Aig/_modules.txt` contains exactly that; prod `Client_Aig.Units` has all 7 custom columns
218
+ and beta had simply never run the module file. Running it on beta fixed the write. The wrong
219
+ conclusion came from comparing beta runtime behavior against a **local `_underscore` checkout on
220
+ branch `TRUE-80282`** while beta deploys `_sandbox-dev` — the moving-branch hazard the api2
221
+ architecture doc already warns about. (mhammontree)
222
+ - 2026-08-17 — Recorded an **unproven open question** (TRUE-80562): an AIG nested `Units` write
223
+ emitted four *other* tenants' `c_` columns (1054 → EV-12) although AIG's schema, its
224
+ `CustomRecordFields` metadata, and its model class all declare exactly one custom field. Suspected
225
+ mechanism is the **alias-keyed `_Database::$_modelCache['Client'][$sql]`** collision; whether the
226
+ static survives across PHP-FPM requests is unknown, and that determines whether this is a
227
+ within-request artifact or a cross-tenant leak. Flagged for security review; no code changed.
228
+ (mhammontree)
182
229
  - 2026-07-30 — Added four framework-level gotchas surfaced while building the all-client email
183
230
  queue monitor: the log-DB name has the same never-string-build rule as the client DB;
184
231
  `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
@@ -94,6 +94,60 @@ fixing. Until then the caller-side workaround is a follow-up identifier-only `PU
94
94
  self-heal that detects the null/mismatched echo (shipped on the BDR funnel side — see
95
95
  web-funnel-app.md).
96
96
 
97
+ ## ⚠ CREATE path: a nested singular FK is matched TENANT-WIDE by VALUE and RE-PARENTED
98
+
99
+ The forward-singular-FK back-reference machinery has a second, worse manifestation — on the
100
+ **CREATE** path, and the damage is a **stolen child row** rather than a null pointer.
101
+
102
+ A nested `primaryContactEmailAddress` / `primaryContactPhoneNumber` object is matched **across the
103
+ whole tenant by its VALUE** (`emailAddress` / `phoneNumber`) — **not** by `uuid` — and the matched
104
+ child's back-reference `contactId` is then **flipped to the newest writer**, silently detaching it
105
+ from its previous owner.
106
+
107
+ **Measured on beta `Client_Aig` 2026-08-17 (TRUE-80562).** Two sequential POSTs each created a new
108
+ Contact (**854** at 10:19, **856** at 10:41). Afterwards:
109
+
110
+ - Contacts **854 AND 856** both have `primaryContactEmailAddressId = 250` and
111
+ `primaryContactPhoneNumberId = 18`.
112
+ - `ContactEmailAddresses` row **250** has `contactId = 856`.
113
+ - So contact **854 still points at a child row that now belongs to contact 856** — 854's primary
114
+ email is effectively **detached**.
115
+
116
+ **Singular and collection paths DIVERGE — only the forward singular FK re-parents.** The collection
117
+ members were **not** stolen: the nested `contactEmailAddresses` entries produced rows **251/252**
118
+ owned by 854 and **separate new rows 255/256** owned by 856. Same payload, same request — different
119
+ behavior per path.
120
+
121
+ > **Production implication.** Any two contacts sharing an email address or phone number — spouses, a
122
+ > shared household line, a corporate address — **collapse onto one child row whose owner flips to
123
+ > whoever wrote last**. For AIG this compounds with the `c_aigCustomerId` integer overflow (314 of
124
+ > 353 prod contacts clamped to a single identifier value), so **AIG contact identity on prod is not
125
+ > reliably distinct**. See
126
+ > [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md).
127
+
128
+ Same back-reference machinery as the
129
+ [UPDATE-path back-reference injection](#update-path-back-reference-injection-defeats-the-uuid-forced-match-primary-pointer-bug)
130
+ above; treat them as one defect with two symptoms when a platform fix is scoped.
131
+
132
+ ## Matching is "link-AND-UPDATE", not "link" — a matched row is mutated from the payload
133
+
134
+ A nested related object that **matches** an existing record does not merely link to it — the engine
135
+ also **writes the payload's fields onto that existing row**. So partner-supplied data can **mutate
136
+ shared catalog rows**.
137
+
138
+ **Measured (beta `Client_Aig`, 2026-08-17):** the nested `unit.item` matched `Client_Aig.Items`
139
+ id **1** by `partNumber`, and the payload's `modelNumber` was then written onto it — `Items` id 1
140
+ `modelNumber` went from `NULL` to a test value, `dtUpdated 2026-08-17 10:41:17`.
141
+
142
+ > **"Link vs. create" is really "link-and-update vs. create". Matching is not read-only.**
143
+
144
+ **Caveat on this example — do not over-read it.** The test payload put the **sale item** part number
145
+ in `unit.item`, whereas AIG's real payload sends the **device** part number there. Per the AIG data
146
+ model those are two different row kinds in the dual-purpose `Items` table (sale item =
147
+ `itemCategoryId` NULL; unit/device item = `itemCategoryId` set), so **real traffic would not mutate
148
+ this particular row** — but it would do exactly the same thing to the **device item's** row. The
149
+ behavior is general; the specific row in the measurement is an artifact of the test payload.
150
+
97
151
  ## Per-API overrides (`Apis_RecordFields`)
98
152
 
99
153
  The base identifier flags above (`Core.RecordFields.isIdentifier`) can be **overridden per API**
@@ -115,6 +169,38 @@ columns matter:
115
169
  > Correct the data with an explicit `overrideIsIdentifier = 1`, or (systemic fix, not yet done)
116
170
  > change the code so only an explicit `0` removes an identifier and `NULL` means "no override".
117
171
 
172
+ ### Fix it with a migration — a manual one-row data patch on non-prod DOES NOT SURVIVE
173
+
174
+ The TRUE-79978 beta fix was applied by hand as a single `UPDATE` on 2026-07-09. The **identical
175
+ EV-12 recurred on beta on 2026-07-21** (3 requests, `Logs_Aig.Api` ids 93 / 95 / 98) with the same
176
+ signature. Evidence the tenant was **reseeded** underneath the patch: those log rows report
177
+ `apiId 2` as *"AIG Integrations"*, while `Client_Aig.Apis` on beta now names id 2 *"Agilant"* — the
178
+ tenant data was rewritten after 2026-07-21, which is also where an incidental
179
+ `overrideIsIdentifier = 1` on one row came from (not a deliberate re-patch).
180
+
181
+ > **Rule: any `Apis_RecordFields` identifier correction must ship as a `dbchanges2/Client_<Tenant>/`
182
+ > migration.** A manual patch on beta / dev-sandbox evaporates on the next tenant reseed and the
183
+ > bug returns looking brand new. (TRUE-80562.)
184
+
185
+ **Scope rule when correcting these rows — only touch fields whose BASE flag is already 1.** Fix a
186
+ `NULL` `overrideIsIdentifier` **only** where `Core.RecordFields.isIdentifier = 1`. A `NULL` override
187
+ on a base-`0` field is a harmless no-op; setting it to `1` **adds** an identifier the platform never
188
+ had and changes nested match-vs-create behavior for that API. On beta `Client_Aig` there were 15
189
+ override rows (all `apiId 2`) but only **5** qualified: `recordFieldId` **1202**
190
+ (*Entitlement fulfillment types.Name*), **1216** (*Entitlement fulfillment methods.Name*), **1259**
191
+ (*Entitlement units.Unitid*), **1386** (*Entitlement coverage types.Name*), **1651**
192
+ (*Contacts.Customerid*).
193
+
194
+ **Expect a next landmine — fix the whole set, not the one field in the error.** Correcting only
195
+ 1202 moved the same EV-12 onto `entitlementFulfillmentMethod` (1216), because
196
+ `_Model_Aig_Entitlement::prePost` injects **both** name-only objects. Audit every qualifying row in
197
+ one migration.
198
+
199
+ **Diagnostic fingerprint:** the EV-12's `searchableIdentifierFields` list is the record's identifier
200
+ set **minus the stripped field** — e.g. `["id","uuid","code"]` on a record whose identifiers are
201
+ Id/Uuid/Code/**Name** means `name` was stripped by an override row. Read that list first; it names
202
+ the culprit without any DB query.
203
+
118
204
  **Build divergence warning.** The `Apis_RecordFields` override feature (reading
119
205
  `overrideIsIdentifier`/`overrideChildPolicy`) was introduced 2026-03-20 (api2 commit `624146d`
120
206
  "Cache custom fields and API override lookups"). Environments on **older** builds ignore the
@@ -174,6 +260,29 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
174
260
 
175
261
  ## Change history
176
262
 
263
+ - 2026-08-17 (later pass) — Two new measured behaviors from the TRUE-80562 verification run on beta
264
+ `Client_Aig`. (1) **CREATE-path re-parenting:** a nested forward singular FK
265
+ (`primaryContactEmailAddress` / `primaryContactPhoneNumber`) is matched **tenant-wide by VALUE**,
266
+ not by uuid, and the matched child's back-reference `contactId` is **flipped to the newest writer**
267
+ — Contacts 854 and 856 both point at `ContactEmailAddresses` 250, which now belongs to 856, so
268
+ 854's primary email is detached. **Collection members are NOT stolen** (rows 251/252 stayed with
269
+ 854; 856 got new rows 255/256), so the singular and collection paths diverge. Prod implication: any
270
+ two contacts sharing an email/phone collapse onto one child row, compounding the AIG
271
+ `c_aigCustomerId` overflow. Same back-reference machinery as the 2026-07-20 UPDATE-path bug, but on
272
+ CREATE and with a stolen child instead of a null pointer. (2) **Matching is link-AND-UPDATE:** a
273
+ matched nested object is **mutated from the payload** — `unit.item` matched `Items` id 1 by
274
+ `partNumber` and wrote the payload's `modelNumber` onto it, so partner data can mutate shared
275
+ catalog rows. (mhammontree)
276
+ - 2026-08-17 — TRUE-80562: the TRUE-79978 **manual one-row beta patch did not survive** — the same
277
+ EV-12 recurred on beta 2026-07-21 (`Logs_Aig.Api` ids 93/95/98) after a tenant reseed (log rows
278
+ call `apiId 2` "AIG Integrations"; beta `Client_Aig.Apis` now names it "Agilant"). Identifier
279
+ corrections must ship as a `dbchanges2/Client_<Tenant>/` migration. Added the **scope rule** (only
280
+ fix `NULL` overrides where `Core.RecordFields.isIdentifier = 1` — 5 of 15 AIG rows qualified:
281
+ 1202/1216/1259/1386/1651), the **next-landmine** warning (fixing 1202 alone moves the EV-12 to
282
+ 1216 because `prePost` injects both objects), and the **`searchableIdentifierFields` fingerprint**
283
+ (base identifier set minus the stripped field names the culprit without a query). Verified
284
+ 2026-08-17: `POST /v2/entitlements` on beta returned 201 with both injected fields resolved.
285
+ (mhammontree)
177
286
  - 2026-08-03 (later pass) — **Correction, supersedes the entry below on one point.** The bare-string
178
287
  collection shape does **not** make the addresses "vanish"/blank the consumer's field: the empty
179
288
  `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/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Aig/_modules.txt, 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,14 +6,19 @@ 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
13
13
  - dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql
14
14
  - dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql
15
+ - dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql
16
+ - dbchanges2/Client_Aig/_modules.txt
15
17
  - dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql
16
18
  - api2/Config/beta.ini
19
+ - api2/Config/sandbox-dev.ini
20
+ - dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql
21
+ - dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql
17
22
  related:
18
23
  - ../architecture.md
19
24
  - ./client-schema-drift-audit.md
@@ -41,8 +46,12 @@ exercise TOGa Supply Fulfill & Ship (TRUE-80494).
41
46
  ## Steps
42
47
 
43
48
  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.
49
+ **dev-sandbox** cluster — "beta" is not its own database. Fix the right DB. Confirmed again
50
+ 2026-08-17 from the other side: **`api2/Config/sandbox-dev.ini`** carries
51
+ `[api] _ = "https://api.beta.togahub.com/v2"` and `hostname = dev.sandbox.database.togahub.com`.
52
+ So the toga-db alias to target is **`dev-sandbox`**, *not* `client-beta` — e.g. `Client_Aig`
53
+ exists only in **prod and dev-sandbox** and is absent from `client-beta` entirely. Picking the
54
+ wrong alias wastes a full diagnostic cycle.
46
55
  2. **Full-table compare the small metadata tables, prod vs. target.** `ApiPayloadInterceptors` in
47
56
  particular is small enough to diff wholesale, and the drift is typically **exactly one row**
48
57
  (dev-sandbox was missing `(recordId 28, PRE, POST)`, so `prePost` never defaulted
@@ -78,6 +87,76 @@ exercise TOGa Supply Fulfill & Ship (TRUE-80494).
78
87
  6. **Re-test the feature end-to-end in that environment**, then record what is still missing so the
79
88
  next person doesn't rediscover it.
80
89
 
90
+ ## The `Client/` fan-out is skipped independently of `Core/` — and the gap is NON-CONTIGUOUS
91
+
92
+ Measured on dev-sandbox 2026-08-17 (TRUE-80562): **31 of 33 tenant DBs lack
93
+ `Entitlements.serviceAddressId`** (only `Client_Rate` and `Client_Elite` have it), while
94
+ **Core does have the RecordField (id 2763)**. So `Core/2026-07-22a` ran and the
95
+ **`Client/` fan-out of the same change did not.** Symptom: **EV-10 / MySQL 1054 "Unknown column
96
+ 'serviceAddressId'" on every entitlement write** — for 31 tenants, not just the one you are
97
+ debugging. That migration's own header states the column must exist for all tenants *before* the
98
+ `_underscore` code deploys, so the code is live against schemas that never got the column.
99
+
100
+ > **Do not conclude "beta is behind as of date X."** The gap is **partial and non-contiguous** —
101
+ > verified against each file's actual `ALTER`:
102
+ >
103
+ > | State | `Client/` file |
104
+ > |---|---|
105
+ > | **MISSING** | `2026-07-10a` ItemFulfillments.returnAddressId |
106
+ > | **MISSING** | `2026-07-16a` TrackingNumbers.signatureType |
107
+ > | **MISSING** | `2026-07-22a` Entitlements.serviceAddressId |
108
+ > | **MISSING** | `2026-07-28a` TicketStages.ticketTypeId |
109
+ > | PRESENT | `2026-07-17` Items.isFulfillable |
110
+ > | PRESENT | `2026-07-22b` TrackingNumbers.length / weightMeasureId |
111
+ >
112
+ > A skipped `07-22a` sits **beside a `07-22b` that ran**. Check file-by-file; a date cutoff will
113
+ > mislead you.
114
+
115
+ ### The gap is NOT limited to `Client/` — `_modules/<module>/` is skipped too
116
+
117
+ Verified 2026-08-17 on beta `Client_Aig`: **`_modules/netsuite/2026-07-10a - UnitInventoryFields.sql`
118
+ had never run** either, so `Client_Aig.Units` was missing six `c_` columns that **production has**.
119
+ `Client_Aig/_modules.txt` contains `netsuite`, so the client *is* opted in — the file was simply not
120
+ applied. Symptom is identical: **MySQL 1054 on a nested write**, surfaced as EV-12 with a
121
+ `modelSaveError`.
122
+
123
+ > **Audit the module folders as well as `Client/`.** For each tenant, read its `_modules.txt` and
124
+ > diff every `_modules/<module>/` file against the tenant's actual schema. A `Client/`-only audit
125
+ > reports a clean bill of health on a tenant that is still broken.
126
+
127
+ **This strengthens the "specific dates were skipped" reading.** The missing module file is dated
128
+ **`2026-07-10a`** — the **same date** as the already-recorded missing
129
+ `Client/2026-07-10a` (`ItemFulfillments.returnAddressId`). Two different fan-out folders both missing
130
+ the same date is far better explained by *that date's run failing* than by "beta is behind as of
131
+ date X."
132
+
133
+ **⚠ Diagnostic trap: do not attribute an unexpected runtime field list to a model class you read
134
+ locally.** During this session the missing-module 1054 was initially mis-diagnosed as a *cross-tenant
135
+ `$_modelCache` leak*, because the model-class comparison was made against a **local `_underscore`
136
+ checkout on branch `TRUE-80282`** while beta deploys **`_sandbox-dev`**. Beta runtime behavior
137
+ compared to a local checkout on a different branch is **not evidence** —
138
+ `_underscore` is cloned at build time from a moving branch
139
+ ([environment-variable-drives-underscore-branch](../../api2/features/environment-variable-drives-underscore-branch.md)).
140
+ Check the environment's built branch **before** concluding anything about model classes; the boring
141
+ "a migration never ran" explanation is the one that keeps being true.
142
+
143
+ **Fix direction: run the existing pending `Client/` files — do NOT author a new migration**, which
144
+ would duplicate a file already on `_main`. A scoped manual `ALTER` on one tenant unblocks your own
145
+ testing but leaves the other 30 broken; say so when you do it.
146
+
147
+ **LEAD (unproven) — duplicate letter suffixes may be why the executor skipped it.** `dbchanges2/Client/`
148
+ contains **three** files all named `2026-07-22a` (`EntitlementServiceAddressId`, `MeasureUomColumns`,
149
+ `Notification&PersonaTranslations`), violating the one-letter-per-day naming contract in
150
+ [architecture.md](../architecture.md). The skipped file is one of that trio. **Worth checking whether
151
+ the executor dedupes on date+letter** — if it does, the naming violation is the root cause and the
152
+ same trap is live wherever letters collide.
153
+
154
+ **Rule violation worth fixing separately:** `Client/2026-07-23a -
155
+ EntitlementServiceAddressIdFieldPermission.sql` references **`Core.RecordFields` and `Core.Records`
156
+ from a `Client/` fan-out file**, breaking the hard one-database-per-file cluster-isolation rule. Its
157
+ own comment says *"same server"* — which is exactly the trap the rule exists for: it works on
158
+ dev-sandbox (shared cluster) and would fail in production, where `Core` is a separate cluster.
159
+
81
160
  ## Environment facts worth knowing before you test here
82
161
 
83
162
  - **NO carrier can be tested on ANY non-production environment.** Beta runs
@@ -156,6 +235,25 @@ blocked by the id-divergence hazard in item 2 above, which is why both go to jca
156
235
 
157
236
  ## Change history
158
237
 
238
+ - 2026-08-17 (later pass) — Extended the fan-out gap: it **also hits `_modules/<module>/`**, not just
239
+ `Client/`. Beta `Client_Aig` had never run `_modules/netsuite/2026-07-10a - UnitInventoryFields.sql`
240
+ despite `_modules.txt` listing `netsuite`, so `Units` lacked six `c_` columns prod has → 1054/EV-12
241
+ on nested unit writes. Note it is the **same date** as the missing `Client/2026-07-10a`, which
242
+ favors "specific dates were skipped" over "beta is behind as of date X". Added the **diagnostic
243
+ trap**: that 1054 was first mis-diagnosed as a cross-tenant `$_modelCache` leak because model
244
+ classes were read from a **local `_underscore` checkout on branch `TRUE-80282`** while beta deploys
245
+ `_sandbox-dev` — verify the environment's built branch before blaming code. (mhammontree)
246
+ - 2026-08-17 — TRUE-80562: documented that the **`Client/` fan-out is skipped independently of
247
+ `Core/`** on dev-sandbox — 31 of 33 tenant DBs lack `Entitlements.serviceAddressId` (only
248
+ `Client_Rate`/`Client_Elite` have it) while Core carries the RecordField (2763), so **entitlement
249
+ writes EV-10/1054 for 31 tenants**. The gap is **partial and non-contiguous** (missing 07-10a,
250
+ 07-16a, 07-22a, 07-28a; present 07-17, 07-22b), so a date cutoff misleads. Fix = run the existing
251
+ pending files, never author a duplicate. Recorded the unproven **duplicate-letter lead** (three
252
+ `Client/2026-07-22a` files; the skipped one sits beside a `07-22b` that ran — check whether the
253
+ executor dedupes on date+letter) and a **cluster-isolation violation** in `Client/2026-07-23a`
254
+ (reads `Core.RecordFields`/`Core.Records` from a `Client/` file, justified as "same server").
255
+ Step 1 now cites `api2/Config/sandbox-dev.ini` and the `dev-sandbox` vs `client-beta` alias trap.
256
+ (mhammontree)
159
257
  - 2026-08-14 — Recorded that metadata drift runs in **both** directions: a cleanup migration that
160
258
  *removed* a row in production is as easily skipped in non-prod as an additive one. Evidence —
161
259
  `Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql` had never run on
@@ -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,116 @@ 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
+ ## RESOLVED — the `Units` 1054 was a missing module migration, NOT a cross-tenant leak
218
+
219
+ **Supersedes the earlier "cross-tenant field list" open question. There is no leak — do not repeat
220
+ that hypothesis.** An AIG POST carrying `entitlementUnits` produced an `INSERT INTO Units` including
221
+ `c_itemDescription`, `c_lastModified`, `c_chargingBrick`, `c_deviceStatus`, `c_chargingCable`,
222
+ `c_grade` → **MySQL 1054**. Those are **AIG's own fields**:
223
+
224
+ - `dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql` adds all six columns **and**
225
+ their `CustomRecordFields` rows to every client whose `_modules.txt` lists `netsuite`.
226
+ **`Client_Aig/_modules.txt` contains exactly `netsuite`** — AIG is opted in.
227
+ - **Prod `Client_Aig.Units` has all 7 custom columns.** Beta had never run the module file — the same
228
+ execution gap as `Entitlements.serviceAddressId`. After running it manually on beta `Client_Aig`,
229
+ `Units` gained all 7 columns and the nested unit write succeeded with those fields present.
230
+ - The wrong conclusion came from comparing beta runtime behavior against a **local `_underscore`
231
+ checkout on branch `TRUE-80282`** while beta deploys `_sandbox-dev`. See
232
+ [per-client database connections](../../../2.0/apps/_underscore/features/per-client-database-connections.md)
233
+ and [non-prod metadata drift repair](../../../2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md).
234
+
235
+ ## Verified end-to-end with the full real-shaped payload (TRUE-80562, 2026-08-17)
236
+
237
+ `POST /v2/entitlements` on `api.beta.togahub.com` returned **201 with `entitlementUnits`
238
+ INCLUDED** — 1 unit, 3 coverage types (`C001`/`C002`/`C039`), 1 fulfillment method, 3 contact email
239
+ addresses.
240
+
241
+ > **This supersedes the earlier partial verification.** Removing `entitlementUnits` was **only a
242
+ > diagnostic isolation step**. **AIG must keep sending it — the unit is the covered device.** Their
243
+ > payload was never wrong.
244
+
245
+ ## `Entitlements.number` carries a UNIQUE key — a replay 400s, it is not idempotent
246
+
247
+ Re-POSTing an already-created contract number returns **EV-10 / MySQL 1062** *"Duplicate entry … for
248
+ key `Entitlements.number`"* as an **HTTP 400**. This matters against AIG's documented
249
+ [scheduled re-send model](#feed-is-a-scheduled-re-send-batch-not-one-shot--self-healing): the feed is
250
+ safe **only while it re-sends unacknowledged contracts**. A bulk replay or back-fill over
251
+ already-created contracts will 400 per row — plan for that before running one.
252
+
253
+ ## Beta test data left behind by TRUE-80562 (clean-up list)
254
+
255
+ For whoever cleans up beta `Client_Aig` (row ids only — do not copy the QA contact PII anywhere):
256
+
257
+ - `Entitlements` id **838** (number `1000045957545`) and the 10:41 entitlement (number
258
+ `1000045957546`).
259
+ - `Contacts` **854** and **856**, with the cross-linked primary email row **250** / phone row **18**
260
+ described in
261
+ [nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md).
262
+ - `Items` id **1** — `modelNumber` polluted with a test value; **revert to `NULL`**.
263
+ - **Beta `Client_Aig` had two migrations applied MANUALLY this session** (`Client/2026-07-22a`
264
+ `serviceAddressId` ALTERs, and the `_modules/netsuite/2026-07-10a` module file), so this tenant is
265
+ now **ahead of whatever the beta executor believes it has applied**.
266
+
152
267
  ## Business Unit (`UserDefined2`) — answered and DROPPED (nothing to build)
153
268
 
154
269
  AIG (Alissa, 2026-05-04) confirmed the **Business Unit / Value** field needs **nothing returned to
@@ -320,6 +435,20 @@ this interceptor or use this dual-purpose Items pattern.
320
435
  find a searchable identifier for the injected object. Root cause (TRUE-79978) was an
321
436
  `Apis_RecordFields` override row that stripped `name` as an identifier — see
322
437
  [api2 nested-relationship writes → Per-API overrides](../../../2.0/apps/api2/features/nested-relationship-writes.md).
438
+ - **The empty-`partNumber` variant is CONFIRMED LIVE (observed 2026-08-17, not inferred).** A
439
+ payload with `saleItem.partNumber = ""` resolved to `saleItemId = 10` — on beta a row with
440
+ `partNumber = ''` and description *"2YR Tablet: ($300-$399.99)"*, an item **unrelated to the
441
+ submitted contract**. The same payload with a real code (`SP-3DAD-LTP2`) correctly resolved to
442
+ id 1. The doc predicted this; it now has a live sighting. The guard clause is still not written.
443
+ - **`prePost` OVERRIDES the client-submitted fulfillment method and nothing signals it.** A payload
444
+ sending `entitlementFulfillmentMethod {name: "Credit"}` produced a stored record of **"Gift Card"**
445
+ plus an injected type **"Reimbursement"** — the interceptor populates both from
446
+ `Items_EntitlementFulfillmentMethods`/`Types` for the matched sale item and **discards the
447
+ client-supplied value**. This may be intended (catalog is the authority), but the **201 response
448
+ does not signal the override**, so a partner cannot tell their value was dropped. **Open question
449
+ for the integration owner** — confirm intent, and consider echoing the resolved values.
450
+ - **Beta 4xx entitlement writes DO appear in `Logs_Aig.Api`** (ids 93/95/98, 2026-07-21). Check the
451
+ client log first for validation failures; reserve the core/writer `Logs` hunt for 5xx.
323
452
  - **A lookup miss yields an empty `$itemId` that crashes the next query (confirmed in prod,
324
453
  TRUE-79534).** The unguarded line is **`_underscore/Model/Aig/Entitlement.php:48`** in `prePost`,
325
454
  reached from **`api2/Component/Api/V2/V2.php:2601`**. The malformed statement it emits is:
@@ -357,6 +486,33 @@ this interceptor or use this dual-purpose Items pattern.
357
486
 
358
487
  ## Change history
359
488
 
489
+ - 2026-08-17 (later pass) — **Correction + final verification, supersedes two points in the entry
490
+ below.** (1) The nested-`Units` "cross-tenant field list" open question is **WITHDRAWN** — the six
491
+ `c_` columns are AIG's **own**, added by `_modules/netsuite/2026-07-10a` to every client whose
492
+ `_modules.txt` lists `netsuite` (AIG's does); prod has all 7 columns, beta had never run the module
493
+ file. Running it on beta fixed the write. The wrong conclusion came from comparing beta runtime
494
+ against a **local `_underscore` checkout on branch `TRUE-80282`** vs beta's `_sandbox-dev`. (2)
495
+ Verified end-to-end with the **full real-shaped payload including `entitlementUnits`** — 201 with
496
+ 1 unit / 3 coverage types / 1 fulfillment method / 3 emails; removing `entitlementUnits` earlier was
497
+ only an isolation step and **AIG must keep sending it**. Also recorded: `Entitlements.number` has a
498
+ **UNIQUE key**, so a replay of an already-created contract returns EV-10/1062 as HTTP 400 (matters
499
+ for any bulk back-fill); and a **beta clean-up list** of the rows/manual migrations this session
500
+ left behind. (mhammontree)
501
+ - 2026-08-17 — TRUE-80562 (beta EV-12 on entitlement intake). The TRUE-79978 **manual beta patch did
502
+ not survive a tenant reseed** — same EV-12 recurred 2026-07-21 (`Logs_Aig.Api` 93/95/98); the fix
503
+ now ships as `dbchanges2/Client_Aig/2026-08-17a`. Recorded the **scope rule** (only correct NULL
504
+ `overrideIsIdentifier` where base `Core.RecordFields.isIdentifier = 1` — 5 of 15 rows:
505
+ 1202/1216/1259/1386/1651), the **next landmine** (fixing 1202 alone moves the error to 1216 since
506
+ `prePost` injects both objects), and the `searchableIdentifierFields` fingerprint. **Verified: 201
507
+ on `api.beta.togahub.com`.** Added: `api.beta.togahub.com` = **dev-sandbox** per
508
+ `api2/Config/sandbox-dev.ini` (`Client_Aig` exists only in prod + dev-sandbox, never
509
+ `client-beta`); beta **4xx** writes DO land in `Logs_Aig.Api`. New **⚠ prod data defect**:
510
+ `Contacts.c_aigCustomerId` is `int unsigned` while AIG ids are 13 digits — **314 of 353 prod
511
+ contacts clamped to 4294967295** on an `isIdentifier` field (ambiguous nested matching); needs its
512
+ own ticket, BIGINT migration does not recover the values. New **open question**: a nested `Units`
513
+ write emitted four other tenants' `c_` columns (1054) — alias-keyed `_modelCache` hypothesis,
514
+ unproven, needs security review. Confirmed the empty-`partNumber` trap live (`saleItemId = 10`)
515
+ and that `prePost` silently overrides a submitted `entitlementFulfillmentMethod`. (mhammontree)
360
516
  - 2026-08-10 — Read-only prod investigation (no code/SQL changed). Added **Verified prod state**:
361
517
  the TRUE-79534 catalog **is** loaded (`Client_Aig.Items` = 2,212 rows — 1,110 `ASI-*` / 558
362
518
  `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.589",
3
+ "version": "1.0.591",
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",