toga-ai 1.0.294 → 1.0.295

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.
@@ -6,12 +6,13 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-07
10
- owners: ["bala"]
9
+ updated: 2026-07-09
10
+ owners: ["bala", "mhammontree"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  related:
14
14
  - ../architecture.md
15
+ - ../../../clients/aig/features/entitlement-intake.md
15
16
  ---
16
17
 
17
18
  ## Summary
@@ -57,6 +58,37 @@ rows on every write.
57
58
  If you need to link by a business key (like an employee XID), **resolve that key to a uuid in your
58
59
  caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
59
60
 
61
+ ## Per-API overrides (`Apis_RecordFields`)
62
+
63
+ The base identifier flags above (`Core.RecordFields.isIdentifier`) can be **overridden per API**
64
+ via rows in a tenant's `Apis_RecordFields` table, read by the V2 resolver's override block
65
+ (`V2.php` ~6926–6936; child policy in `getChildPolicy()` ~8019–8041; loader ~8526–8538). Two
66
+ columns matter:
67
+
68
+ - **`overrideIsIdentifier`** — overrides whether the field is a searchable identifier for that API.
69
+ - **`overrideChildPolicy`** — overrides link-vs-create for the nested record (e.g. `MATCH_CREATE`
70
+ = match an existing child by identifier, else create it).
71
+
72
+ > **Trap: a `NULL` `overrideIsIdentifier` is read as "not an identifier", not "no opinion".**
73
+ > The override block evaluates `!$row->overrideIsIdentifier`, so a `NULL` **unsets** the field
74
+ > from the API's searchable identifiers even when the base `Core.RecordFields` row has
75
+ > `isIdentifier=1`. Any `Apis_RecordFields` row created **only** to set a child policy
76
+ > (leaving `overrideIsIdentifier` NULL) will silently strip that field as an identifier. The
77
+ > nested object then has no searchable identifier and the write fails **EV-12 VALIDATION**
78
+ > (`V2.php:7094`) — the `MATCH_CREATE` that should have saved it never runs on that path.
79
+ > Correct the data with an explicit `overrideIsIdentifier = 1`, or (systemic fix, not yet done)
80
+ > change the code so only an explicit `0` removes an identifier and `NULL` means "no override".
81
+
82
+ **Build divergence warning.** The `Apis_RecordFields` override feature (reading
83
+ `overrideIsIdentifier`/`overrideChildPolicy`) was introduced 2026-03-20 (api2 commit `624146d`
84
+ "Cache custom fields and API override lookups"). Environments on **older** builds ignore the
85
+ override and keep the base identifier, so a tenant with identical Core metadata + override rows +
86
+ data can behave differently across environments purely by build version. A NULL-`overrideIsIdentifier`
87
+ override row that works on prod (older build) will start throwing EV-12 once prod gets the newer
88
+ build — audit such rows before/with the deploy. First seen in AIG (TRUE-79978): prod resolved,
89
+ beta/QA threw EV-12 on the injected `entitlementFulfillmentType` — see
90
+ [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md).
91
+
60
92
  ## Gotcha
61
93
 
62
94
  - **Nested write with only a non-identifier field silently creates duplicates.** This is a
@@ -68,6 +100,11 @@ caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
68
100
 
69
101
  ## Change history
70
102
 
103
+ - 2026-07-09 — Documented the per-API `Apis_RecordFields` override layer (`overrideIsIdentifier`,
104
+ `overrideChildPolicy`) and the trap that a `NULL` `overrideIsIdentifier` is read as "not an
105
+ identifier" (`!$row->overrideIsIdentifier`) — an override row set only for a child policy
106
+ silently strips the field as an identifier → EV-12 at `V2.php:7094`. Added the build-divergence
107
+ warning (override feature added 2026-03-20, commit `624146d`). Surfaced by AIG TRUE-79978. (mhammontree)
71
108
  - 2026-07-07 — Documented that api2 nested-relationship writes match an existing child **only** by
72
109
  identifier (`uuid`); a non-unique custom field is not a match key and forces a new-record insert.
73
110
  Correct pattern is `contact: {uuid}` (resolve business keys to uuid in the caller). Surfaced by
@@ -78,3 +115,5 @@ caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
78
115
  - [api2 architecture](../architecture.md) — the V2 CRUD engine (`processRoutePairs`).
79
116
  - [Prudential device import + contact linking](../../../clients/prudential/features/device-information-import-and-contact-linking.md)
80
117
  — the production incident that exposed this behavior.
118
+ - [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md) — the TRUE-79978
119
+ EV-12 that exposed the `Apis_RecordFields` NULL-`overrideIsIdentifier` trap + build divergence.
@@ -5,12 +5,13 @@ project: API
5
5
  client: aig
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-06-19
8
+ updated: 2026-07-09
9
9
  owners: ["mhammontree"]
10
10
  files:
11
11
  - _underscore/Model/Aig/Entitlement.php
12
12
  - dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
13
13
  related:
14
+ - 2.0/apps/api2/features/nested-relationship-writes.md
14
15
  - 2.0/apps/api2/architecture.md
15
16
  - 2.0/apps/dbchanges2/architecture.md
16
17
  - 2.0/apps/_underscore/architecture.md
@@ -42,7 +43,9 @@ the FK can't resolve and intake fails with **"Missing AIG item ID."** Keeping
42
43
  - reads `partNumber = payload->saleItem->partNumber` and runs
43
44
  `SELECT id FROM Items WHERE partNumber = '{partNumber}' LIMIT 1` to get `$itemId`;
44
45
  - uses `$itemId` to load fulfillment **types** (`Items_EntitlementFulfillmentTypes`) and
45
- **methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload.
46
+ **methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload as
47
+ **name-only** related objects (e.g. `entitlementFulfillmentType: {name: "Repair"}`). AIG
48
+ never sends `entitlementFulfillmentType`; it is populated entirely by this interceptor.
46
49
  3. The V2 engine resolves `Entitlement.saleItemId` (FK → `_Model_Client_Item`) from that same
47
50
  `saleItem.partNumber`. **No matching `Items` row → unresolvable FK → "Missing AIG item ID".**
48
51
  4. `postPost` then emails the contact a Staples Protection Plan registration link
@@ -118,9 +121,28 @@ this interceptor or use this dual-purpose Items pattern.
118
121
  codes are **not** in the SaleItemID spreadsheet and must be sourced separately if missing.
119
122
  - **No unique key on `Items.partNumber`** — use an anti-join for idempotent loads, not
120
123
  `INSERT IGNORE`.
124
+ - **An `entitlementFulfillmentType` EV-12 VALIDATION error is NOT a bad client payload —
125
+ it's this interceptor's injected field failing to resolve.** AIG never sends that field;
126
+ the interceptor injects it as `{name: "Repair"}`. The error means the V2 resolver could not
127
+ find a searchable identifier for the injected object. Root cause (TRUE-79978) was an
128
+ `Apis_RecordFields` override row that stripped `name` as an identifier — see
129
+ [api2 nested-relationship writes → Per-API overrides](../../../2.0/apps/api2/features/nested-relationship-writes.md).
130
+ - **Empty `saleItem.partNumber` silently resolves to the wrong item.** The lookup is
131
+ `WHERE partNumber = '{$partNumber}' LIMIT 1`; when `partNumber` is `""`, it matches item
132
+ `id=10` ("2YR Tablet") — the first row with an empty part number — which carries a "Repair"
133
+ fulfillment type. So a blank part number does not fail loudly; it injects a spurious
134
+ name-only fulfillment type. The interceptor should guard an empty/failed lookup before
135
+ injecting (not yet fixed).
121
136
 
122
137
  ## Change history
123
138
 
139
+ - 2026-07-09 — TRUE-79978: root-caused an `entitlementFulfillmentType` EV-12 on beta/QA. The
140
+ field is injected (name-only) by `prePost`; the beta `Client_Aig.Apis_RecordFields` override
141
+ row (apiId=2, recordFieldId=1202) had `overrideIsIdentifier=NULL` + `overrideChildPolicy=MATCH_CREATE`,
142
+ which the newer V2 build reads as "not an identifier", stripping `name` → no searchable id →
143
+ EV-12. Unblocked with a manual one-row beta data fix (`SET overrideIsIdentifier=1`); prod runs
144
+ an older build without the override feature so it still resolves. See the api2 feature doc for
145
+ the shared resolver behavior. (mhammontree)
124
146
  - 2026-06-19 — Documented intake flow; loaded 2,200 new SaleItem codes (1,700 STS `ASI-*`/`SM-*`
125
147
  + 500 unique numeric `SA`) into `Client_Aig.Items` via TRUE-79534, fixing "Missing AIG item ID"
126
148
  on the new `ASI-*` scheme. Follow-up to TRUE-79441. (mhammontree)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.294",
3
+ "version": "1.0.295",
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",