toga-ai 1.0.855 → 1.0.856

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.
@@ -38,6 +38,7 @@
38
38
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | Shared 2.0 `_underscore` NetSuite REST client (`_Component_Api_Netsuite`) — record GET/write, SuiteQL, auth, escaping, logging; open before any 2.0 NetSuite cal |
39
39
  | [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | Where the shared NetSuite SO importer (`_Trait_Netsuite_SalesOrder`) sources SO#, PO#, and ship-to phone, and the traps in changing it; open before editing this |
40
40
  | [Note records have NO author — every `*Notes` model's `contactId` is unwritten](features/note-record-authorship.md) | Every `*Notes` model declares a `contactId` FK but no code writes it — note authorship is unrecoverable from 2.0 data; open before building any note author disp |
41
+ | ["Order For" delegation (Users_DelegatorUsers, the permission gate, originalUserId)](features/order-for-delegation.md) | Server side of "Order For" (TRUE-81848): who may order for whom, where that is enforced, and how the audit column records it. |
41
42
  | [Outbound third-party API rate budget (_Component_Api_RateBudget) — designed + staged, NOT merged](features/outbound-api-rate-budget.md) | Design record for a shared outbound API rate budget (one component + one Core table, keyed by `bucketKey`); open when adding call-rate metering/limiting to a 2. |
42
43
  | [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | Legacy `Page::meta()` page-meta resolver + context-scoped `ClientRecordFieldSettings`; open when a grid column header is wrong/camelCase, or a `/pages/meta` pag |
43
44
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | `_underscore` opens three per-client DB connections (data/logs/archive) per request; open when a local or prod request 500s with `Unknown database`, when resolv |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-18
9
+ updated: 2026-09-21
10
10
  owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -53,7 +53,11 @@ AclRecordPermissions → AclLogicGroups → AclLogicGroupExpressions →
53
53
  > rejected with the exact message **`No ACL Logic Groups defined for ACL Record Permissions`**
54
54
  > (~L4437 / ~L5845); a missing expression binding fails on the second flag the same way. So an
55
55
  > `AclRecordPermissions` row with no `AclLogicGroups` row returns UNAUTHORIZED carrying that string
56
- > — which means the **chain**, not the grant, is the problem.
56
+ > — which means the **chain**, not the grant, is the problem. The mechanism is
57
+ > `_Model_Core_AclRecordPermission::buildSqlExpression`, which returns **`'(0)'`** when the
58
+ > permission has no logic group — so `allowRead = 1` on its own **denies everything**. Second
59
+ > symptom shape (2026-09-18): a 403 that lists `aclRecordPermissionIds` with the nested child array
60
+ > coming back as `[null, null, null]`.
57
61
 
58
62
  **Field visibility — `AclFieldPermissions`.** Record-level access alone does **not** expose
59
63
  fields. Each field needs a row: `recordFieldId`, `roleId`, `isWritable` (0 = read-only, 1 =
@@ -503,6 +507,15 @@ path on the strength of them.
503
507
  > [V2 REST query contract → ACL behavior](../../api2/features/v2-rest-query-contract.md). Confirm
504
508
  > the request shape first; it eliminates the entire ACL chain in one step.
505
509
 
510
+ **⚠ A `CLIENT` record needs the grant in EVERY `Client_*` DB — a Core row for it is dead metadata.**
511
+ Every ACL lookup in `api2/Component/Api/V2/V2.php` (L3236, L4057, L6502, L7307) switches on the
512
+ record's `aclDatabase` and reads only the matching bucket, so `AclRecordPermissions` /
513
+ `AclRecordExpressions` / `AclFieldPermissions` written into `Core` for a `CLIENT` record are **never
514
+ evaluated**. Found 2026-09-18 on record **362** (`user-delegator-users`): role 3 (API) has a Core
515
+ grant and none in any client DB, so no integration can reach the route. For every other client
516
+ record the role-3 grant is mirrored in **both** places (268 rows in `Core`, 275 in
517
+ `Client_Compass`) — mirror it, or the API role silently has nothing.
518
+
506
519
  **Role ids differ per client DB**, so never hardcode a `roleId` in a `CLIENT` ACL migration —
507
520
  resolve it with a subselect, e.g. the Base role:
508
521
 
@@ -510,6 +523,21 @@ resolve it with a subselect, e.g. the Base role:
510
523
  roleId = (SELECT id FROM Roles WHERE `name` = 'Base')
511
524
  ```
512
525
 
526
+ ### ⚠ On a real top-level route, `sqlExpression = '1'` leaks every tenant row
527
+
528
+ `'1'` (slug `all`) is right for a record only ever read nested under its parent. If the record also
529
+ has its own route, `'1'` lets any user `GET /v2/<route>` and read everyone else's rows. Scope the
530
+ expression to the caller instead — `AclUser` is joined by `_Model_True::isPermitted` on the
531
+ logged-in user:
532
+
533
+ ```sql
534
+ CASE WHEN Users_DelegatorUsers.userId = AclUser.id THEN 1 ELSE 0 END
535
+ ```
536
+
537
+ Caught in security review on `user-delegator-users` (2026-09-18), where a `'1'` grant would have
538
+ listed who every colleague may order for. See
539
+ [order-for delegation](./order-for-delegation.md).
540
+
513
541
  ## Canonical example (grant the Base role full CRUD on a record)
514
542
 
515
543
  ```sql
@@ -893,6 +921,14 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
893
921
  and every repo is on the **same branch** so the generated model matches the DB.
894
922
 
895
923
  ## Change history
924
+ - 2026-09-21 — Named the mechanism behind the half-built chain: `_Model_Core_AclRecordPermission::buildSqlExpression`
925
+ returns **`'(0)'`** with no logic group, so `allowRead = 1` alone denies everything (second symptom: 403 listing
926
+ `aclRecordPermissionIds` with `[null, null, null]`). Added that a `CLIENT` record's grant must exist in every
927
+ `Client_*` DB — the `aclDatabase` switch at `V2.php` L3236/L4057/L6502/L7307 makes a Core row dead metadata;
928
+ record 362 (`user-delegator-users`) role 3 is Core-only and therefore unreachable, while every other client record
929
+ mirrors role 3 in both. Also recorded the caller-scoped row expression pattern
930
+ (`CASE WHEN <Table>.userId = AclUser.id THEN 1 ELSE 0 END`) required when a record is a real top-level route.
931
+ (bala)
896
932
  - 2026-09-18 — **The `2026-09-17a` fleet migration has a gap: it granted only the PO-first bridges
897
933
  325/329, leaving the TO-first siblings 326 (`transfer-orders-purchase-orders`) and 330
898
934
  (`transfer-order-items-purchase-order-items`) with ZERO `AclRecordPermissions` rows.** Found on
@@ -0,0 +1,162 @@
1
+ ---
2
+ title: "Order For" delegation (Users_DelegatorUsers, the permission gate, originalUserId)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-21
10
+ owners: [bala]
11
+ files:
12
+ - _underscore/Model/Client/Users/DelegatorUser.php
13
+ - _underscore/Model/Client/User.php
14
+ - _underscore/Model/Client/Logs/Api.php
15
+ - api2/Component/Api/V2/V2.php
16
+ - dbchanges2/Client/2026-09-18a - UsersDelegatorUsersTable.sql
17
+ - dbchanges2/Core/2026-09-18a - UserDelegatorUserRecords.sql
18
+ related:
19
+ - ../../api2/features/request-logging.md
20
+ - ./acl-permission-chain.md
21
+ - ../../toga2-commerce/features/header-order-for-dropdown.md
22
+ - ../../toga2-commerce/features/view-as-session-tokens.md
23
+ - ../../api2/features/encrypted-user-uuid-auth-handoff.md
24
+ ---
25
+
26
+ Server side of "Order For" (TRUE-81848): who may order for whom, where that is enforced, and how the audit column records it. Open before touching `/auth/delegator`, `Users_DelegatorUsers`, or `Logs_<Client>.Api.originalUserId`. Team skill: `/order-for`.
27
+
28
+ ## Summary
29
+
30
+ A user may place an order **as** another user only when a `Users_DelegatorUsers` row says so.
31
+ Three pieces: the junction table (who may order for whom), the gate
32
+ (`_Model_Client_User::canUserOrderForOtherUser`, checked at both mint points), and the
33
+ `originalUserId` audit column (who actually signed in).
34
+
35
+ **The table ships EMPTY** — no admin screen, no import. Until rows exist every delegation is
36
+ refused and every delegation dropdown is empty. That is intended, not a bug.
37
+
38
+ ## How it works
39
+
40
+ ### 1. `Users_DelegatorUsers` (every `Client_*` DB)
41
+
42
+ `id`, `uuid`, `userId`, `delegatorUserId`, UNIQUE on the pair. Read as **"`userId` may order for
43
+ `delegatorUserId`"**. Model `_Model_Client_Users_DelegatorUser`.
44
+
45
+ Core metadata (`Core/2026-09-18a`): record **362**, route `user-delegator-users`,
46
+ `aclDatabase = CLIENT`, RecordFields **2604** id / **2605** uuid / **2606** userId /
47
+ **2607** delegatorUserId, registered as an **inherent child of record 2 (Users)** so
48
+ `userDelegatorUsers` is readable nested under `/users/{uuid}`.
49
+
50
+ ### 2. The gate — checked TWICE
51
+
52
+ `_Model_Client_User::canUserOrderForOtherUser($orderingUserId, $orderedForUserId)` (L354), used by
53
+ `getRequestingUser()` / `encryptedUser` (L398) and again by `/auth/delegator` in
54
+ `api2/Component/Api/V2/V2.php`.
55
+
56
+ - Check 1 = when the encrypted `{client,user}` pair is minted.
57
+ - Check 2 = when `/auth/delegator` exchanges that pair for a token. **This second check is the real
58
+ control**: the encrypted pair has no expiry and no MAC (`_String::encryptWithKey`, AES-256-CBC
59
+ keyed on `Core.Parameters.API_SECRET_REFRESH_TOKEN`), so a captured pair could otherwise be
60
+ replayed forever.
61
+ - `/auth/refresh` re-checks an active delegation, so deleting the row or deactivating either
62
+ account **ends the live session**, not just new switches.
63
+
64
+ `V2.php` sets `$isDelegatorAuthRequest = true` as the **first statement** of `case 'delegator'`,
65
+ which then falls through into `case 'encrypted'`; the flag is declared `false` (L609) so a forged or
66
+ expired bearer cannot mint. **Fail-closed is deliberate** — a fail-open default was caught in
67
+ security review before it shipped.
68
+
69
+ ### 3. `originalUserId` audit column
70
+
71
+ `Logs_<Client>.Api.originalUserId` — nullable int, directly after `userId`
72
+ (`Logs_Client/2026-09-18a`), on `_Model_Client_Logs_Api`. `V2.php` fills it at all three client
73
+ log-write sites plus the CloudWatch entry.
74
+
75
+ - `userId` = who the token is for · `originalUserId` = who actually signed in · **NULL = no
76
+ delegation**. No row should ever have `originalUserId = userId`.
77
+ - Verified on local order SA136321: `SalesOrders.createdByUserId` = 124256 (Dawn Anderson), log row
78
+ `userId 124256 / originalUserId 127046` (Dev Team). Last 200 rows: 126 own-token (NULL) + 70
79
+ delegated, zero self-referencing.
80
+
81
+ ### 4. ACL — read-only, and scoped to the CALLER
82
+
83
+ Per-tenant files `Client_Compass/2026-09-18a` and `Client_CompassCanada/2026-09-18a`, `appId 5`
84
+ (TOGa Commerce). **Read only, on purpose:** a role that can write its own row here self-grants the
85
+ right to order for anyone in the tenant.
86
+
87
+ The row expression is scoped to the caller, never `'1'`:
88
+
89
+ ```sql
90
+ CASE WHEN Users_DelegatorUsers.userId = AclUser.id THEN 1 ELSE 0 END
91
+ ```
92
+
93
+ `user-delegator-users` is a real top-level route, so `'1'` would let any user
94
+ `GET /v2/user-delegator-users` and list who every colleague may order for. `AclUser` is joined by
95
+ `_Model_True::isPermitted` on the logged-in user. Chain rules: [ACL permission chain](./acl-permission-chain.md).
96
+
97
+ **Role ids differ per tenant** — US Compass **7** = Compass Base, **8** = Manager; Compass Canada
98
+ **8** = Compass Base, **9** = Manager. Canada also needed `Core.RecordScripts` **16**
99
+ (`encryptedUser`) granted to its roles 8 and 9 — it shipped with role 1 only, so every normal Canada
100
+ user got a 403 on a pick. US already had 16 for roles 3/7/8.
101
+
102
+ **Dev-sandbox needs its own files** (`2026-09-21a … DevSandbox.sql`) because the pinned field ids
103
+ collide there — field ids **2767/2768/2769/2770**, record 362 unchanged. See
104
+ [non-prod metadata drift](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
105
+
106
+ ### 5. The front-end call that exists
107
+
108
+ `toga2-commerce/src/pages/Cart/api/CartApi.ts` → `fetchDelegationList(ownUuid)` returns
109
+ `DelegationOption[]` (`{uuid, name, email}`) — everyone that user may order for, one call, no
110
+ paging. Two hard rules in
111
+ [header order-for dropdown](../../toga2-commerce/features/header-order-for-dropdown.md).
112
+
113
+ ## Gotchas
114
+
115
+ - **The table is empty in every environment.** "Delegation refused / dropdown empty" is the
116
+ expected state until someone inserts rows; there is no UI or import to do it.
117
+ - **Never let the `isDelegatorAuthRequest` flag default to true** — the encrypted pair is
118
+ unauthenticated, replayable input.
119
+ - **Outbound logs never carry `userId` or `originalUserId`.** `_underscore/ApiRequest.php` and
120
+ `SoapRequest.php` write `direction = OUT` rows for calls TOGa makes to NetSuite/UPS — there is no
121
+ signed-in user on those. A CodeRabbit review asking for the stamp there was wrong and rejected.
122
+ - **Role 3 (API) cannot reach this record.** Its grant for record 362 was written into `Core`, but
123
+ `aclDatabase = CLIENT` means Core rows are never consulted — so no integration can read or write
124
+ `user-delegator-users` through api2 until the grant is mirrored into each `Client_*` DB.
125
+
126
+ ## Still open (next person: do not rediscover)
127
+
128
+ 1. **The front end does not switch the token, so the feature is not real yet.** The header dropdown
129
+ only saves the pick to localStorage and passes it as a cart form value; it never calls
130
+ `/auth/delegator` (only admin View As does, in `NavIconList.tsx`). With that, `$api->delegator` is
131
+ never set, `originalUserId` stays NULL, the gate never fires, and the buyer browses their **own**
132
+ catalog and prices while "ordering for" someone else. Required switch: `GET
133
+ /users/encryptedUser?uuid=<target>` → `POST /auth/delegator` → `beginDelegation(tokens,
134
+ originalUserUuid)` (`src/api/sessionTokens.ts`); `restoreOriginalTokens()` returns with no new
135
+ login. **One parked-token slot, shared with View As** — the two cannot nest.
136
+ 2. **`c_orderSubmittedById` must be stamped server side.** The column exists on `SalesOrders` in
137
+ `Client_Compass`, `Client_CompassCanada`, `Client_Quad`, with `CustomRecordFields` id 40 on record
138
+ 14 and `_Model_Compass_SalesOrder` L51 as `FIELD_INTEGER`. It is a plain integer, not an FK, so
139
+ api2 will not resolve a `{uuid}` into it and the client cannot set it. Belongs in
140
+ `_Model_Compass_SalesOrder::prePost()` (L445; `ApiPayloadInterceptor` 24 is already PRE on POST
141
+ for record 14), read from `$api->delegator->user`, NULL when there is no delegation — matching
142
+ 1.0 `store/common/compass/convertcarttoorder.php` L28-34. **Reading old data:** of 112,096
143
+ Compass orders 8,118 have the field set and 7,174 of those equal `createdByUserId`, because the
144
+ 1.0 guard compared a manager id against a contact id.
145
+ 3. **`/auth/encrypted` still accepts a delegation pair with no bearer and records no delegator.**
146
+ Narrowed because Commerce no longer calls it; `toga2-desk` still does for client switching.
147
+
148
+ ## Change history
149
+
150
+ - 2026-09-21 — Created (TRUE-81848). `Users_DelegatorUsers` + record 362, the two-point
151
+ `canUserOrderForOtherUser` gate, `Logs_<Client>.Api.originalUserId`, caller-scoped read-only ACL
152
+ for Compass US/Canada (+ dev-sandbox variants), and `fetchDelegationList`. Table ships empty; the
153
+ token switch, `c_orderSubmittedById` and the `/auth/encrypted` hole are still open. Team skill
154
+ `/order-for` (commit d4751825) loads this context. (bala)
155
+
156
+ ## Related
157
+
158
+ - [request logging](../../api2/features/request-logging.md)
159
+ - [ACL permission chain](./acl-permission-chain.md)
160
+ - [header order-for dropdown](../../toga2-commerce/features/header-order-for-dropdown.md)
161
+ - [view-as session tokens](../../toga2-commerce/features/view-as-session-tokens.md)
162
+ - [encrypted-user auth handoff](../../api2/features/encrypted-user-uuid-auth-handoff.md)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-09-21
10
10
  owners: ["mhammontree", "dfranks", "bala", "jcardinal"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -21,6 +21,7 @@ related:
21
21
  - ./v2-deadlock-retry.md
22
22
  - ./environment-variable-drives-underscore-branch.md
23
23
  - ../../_underscore/features/error-reporting-issue-event.md
24
+ - ../../_underscore/features/order-for-delegation.md
24
25
  - ../../../../clients/aig/features/entitlement-intake.md
25
26
  - ../../../../clients/prudential/features/service-request-auth-rejection-alert.md
26
27
  - ../../../../clients/compass-usa/features/mits-po-to-so-item-linking.md
@@ -39,6 +40,7 @@ In `api2/Component/Api/V2/V2.php` (~L2168) every request is logged and routed:
39
40
  - **A successful, client-scoped read (GET)** → the **CLIENT log DB** via `_Model_Client_Logs_Api` → table `Logs_<Client>.Api` (e.g. `Logs_Aig.Api`).
40
41
  - **A request fails *before* client scoping** (bad/expired auth, unknown client — e.g. `EN-1` / `EN-4`) → the **CORE log DB** via `_Model_Core_Logs_Api` → the core `Logs` DB (older docs call this `Logs_2`). Pre-scope failures land in **Core**, **not** the client log.
41
42
  - **A FAILED write attempt (non-2xx on a write) lands in the CORE/writer `Logs`, even when the request authenticated to a client.** The subtle one: the request can carry a valid client JWT (fully scoped to, say, `Aig`) and still have its non-2xx `POST`/`PUT` logged to the production **core `Logs`** on the **writer cluster** (host `api-writer.togahub.com`), **not** to `Logs_<Client>`. So `Logs_<Client>.Api` effectively shows the client's **reads**; **failed writes are in core `Logs`.** (A *successful* write is logged to the client log as normal.)
43
+ - **`Logs_<Client>.Api.originalUserId`** (added 2026-09-18) records **who actually signed in** when the token was minted for someone else: `userId` = the token's user, `originalUserId` = the real person, **NULL = no delegation** (no row should ever have the two equal). `V2.php` fills it at all three client log-write sites plus the CloudWatch entry. **Outbound rows (`direction = OUT`, written by `_underscore/ApiRequest.php` / `SoapRequest.php`) never set either column** — there is no signed-in user on a call TOGa makes out to NetSuite or UPS. See [order-for delegation](../../_underscore/features/order-for-delegation.md).
42
44
  - There is also a **config-gated branch** (~L2166): when `_Config::api('log_filepath')` is set, the entry is written to a **file** on the host instead of the DB.
43
45
 
44
46
  **401 lands in base `Logs`; 403 lands in `Logs_<Client>`.** The pre-scope/post-scope split has a sharp edge on the two auth codes:
@@ -207,6 +209,10 @@ Both a **fallback recovery path** and a **scope diagnostic**. In the TRUE-80179
207
209
  - **Query prod `Logs.Api` by narrowing on `dtStamp` (indexed) first**, collect ids, then fetch `mediumtext` payloads by id with `LEFT()`/`SUBSTRING()`. Never start from a `LIKE` over `requestPayload`/`responsePayload`.
208
210
  - **Never copy logged rows into committed artifacts** — they carry live Bearer tokens and PII.
209
211
 
212
+ ## Change history
213
+
214
+ - 2026-09-21 — Added `Logs_<Client>.Api.originalUserId` (delegation audit: NULL = none, never equal to `userId`) and the rule that outbound `direction = OUT` rows stamp neither user column. (bala)
215
+
210
216
  ## Related
211
217
 
212
218
  - [api2 architecture](../architecture.md)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-17
9
+ updated: 2026-09-21
10
10
  owners: [bala, kyalamarthi, apeterson, ajean, jcardinal, rgirish]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
@@ -87,7 +87,16 @@ DELETE FROM AclFieldPermissions WHERE uuid IN ('<literal-1>', …, '<literal-8>'
87
87
 
88
88
  It can only ever match rows this file inserted. **Reusing the same literal across tenants is valid**
89
89
  — `uuid` is unique *per table per database*, and each tenant is a separate database, so one literal
90
- per (row × file) is enough for the whole fan-out. Keep the `NOT EXISTS` guard on the real unique key
90
+ per (row × file) is enough for the whole fan-out.
91
+
92
+ **⚠ But do NOT reuse a pinned uuid across two files whose rows have DIFFERENT natural keys**
93
+ (e.g. a prod file and its `…DevSandbox.sql` twin, 2026-09-21). `AclFieldPermissions` rows are
94
+ guarded on `recordFieldId`, which differs per environment, so the `NOT EXISTS` guard lets the
95
+ insert through and the UNIQUE `uuid` key then fails: *"Duplicate entry … for key
96
+ `aclfieldpermissions.uuid`"*. Rule of thumb: **same natural key across environments → keep the same
97
+ uuid** (a row then means the same thing everywhere — `RecordFields` keyed on `recordId+field`,
98
+ record permissions keyed on `recordId+roleId`); **environment-specific natural key → its own
99
+ uuid.** Keep the `NOT EXISTS` guard on the real unique key
91
100
  (here `(recordFieldId, roleId)`) so re-running is still a no-op on tenants that already have the
92
101
  grant; the literal governs only deletion.
93
102
 
@@ -278,6 +287,10 @@ it. Two cheap local checks catch almost everything, and neither needs a sandbox:
278
287
  2. **Prove re-runnability by concatenating the file to itself** and running *both copies inside one
279
288
  transaction*. If the second pass errors — or changes anything the first pass did not — the file is
280
289
  not idempotent, and the executor will eventually run it twice.
290
+ **First DELETE the rows to reproduce the TARGET environment's starting state**, inside the same
291
+ transaction, then run the file twice, `SELECT` the resulting counts, and `ROLLBACK`. Rehearsing
292
+ against your own already-seeded local DB proves nothing — every guard short-circuits. This is
293
+ what caught the duplicate-uuid failure above, which eyeballing missed.
281
294
 
282
295
  3. **When the migration must not change behaviour, simulate the OLD rules and the NEW rules inside
283
296
  ONE database** and diff the results per population. **Never diff prod against a sandbox clone** —
@@ -353,6 +366,10 @@ concluding a migration is production-safe.
353
366
  [FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
354
367
 
355
368
  ## Change history
369
+ - 2026-09-21 — Narrowed the "reuse one uuid literal across tenants" rule: it holds only when the rows share a
370
+ natural key. An env-specific twin file (guarded on `recordFieldId`, which differs per environment) reusing the
371
+ literal dies on `Duplicate entry … aclfieldpermissions.uuid`. Also added the DELETE-first transaction rehearsal
372
+ (reproduce the target env's starting state, run twice, count, rollback), which is what found it. (bala)
356
373
  - 2026-09-17 — Three additions from the Compass persona refactor: a **subtractive** phase cannot
357
374
  `NOT EXISTS` against the table it deletes from (**MySQL 1093**) — use a multi-table `DELETE` with a
358
375
  self `LEFT JOIN … IS NULL`; comparing a varchar `number` column to a string user-variable can raise
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-09-21
10
10
  owners: ["bala", "mhammontree", "apeterson", "rgirish"]
11
11
  files:
12
12
  - dbchanges2/Client_Growrk/2026-05-28.sql
@@ -164,12 +164,19 @@ This is the write-side twin of the read-side rule above (*"the field named in th
164
164
 
165
165
  ## Gotchas
166
166
 
167
+ - **A LOCAL checkout that is many `dbchanges` behind looks exactly like a schema gap — it is not.** Every error in a 2026-09-21 local Compass session was an **existing file never applied** (`Client/2026-09-11a` → `SalesOrderItems.fulfillmentType`, the real cause of an order-submit 400 *"Unknown column 'fulfillmentType'"*; the `Logs_Client` blank-DB `Email`/`EmailAttachment` tables, cause of a 500 *"Table 'logs_compass.email' doesn't exist"*; the `Logs` blank-DB `Issue`/`Event` tables; `Client/2026-08-28b`, `2026-08-28f`, `2026-09-01b`, `2026-09-01c`, `2026-09-14a`, `_modules/netsuite/2026-09-11a`). **Grep `dbchanges2` for the column or table name before writing any SQL.**
168
+ - **Diff a whole environment against prod in ONE pass** — missing tables *and* missing columns together — by comparing `SELECT CONCAT(TABLE_NAME, ':', COUNT(*)) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '<db>' GROUP BY TABLE_NAME` on both sides and `comm`-ing the sorted lists. Step 3's per-table diff is for one known traversal path; this is the catch-up sweep.
169
+
167
170
  - **Partial means arbitrary — never assume all-or-nothing.** The observed run landed **6 of 7** columns and **1 of 6** metadata rows from one file. "The migration ran" is not a fact about any individual statement in it.
168
171
  - **A `CustomRecordFields` row with no ACL grant is not automatically a defect.** Compare with the healthy client before "fixing" it — `Client_Growrk` recordId 312 (`c_netsuiteInternalSalesOrderId`, `TransferOrders`) has no grant, and `Client_Aig` is identical, so that is the normal cross-client state. Repairing it would be a change, not a fix.
169
172
  - **A client can be fine for months and then break on onboarding.** Before a client joins the module, its records resolve to the plain `_Model_Client_*` class with none of the `c_` fields, so the missing columns are unreachable. The 500s begin the day the client is switched onto the module-specific model — which is why the "it has always worked" evidence is misleading. Prove it with `Logs.Api`: successful 200s for the exact same request shape right up to the onboarding timestamp, 500s immediately after.
170
173
  - **`ALTER TABLE … ADD COLUMN` was INSTANT here** (MySQL 8.0.39, ~5.4k rows) — a small tenant table needs no maintenance window, but confirm the server version and row count rather than assuming.
171
174
  - **Re-running the whole module change-set is the wrong repair** when only one environment drifted: the other statements are already applied and some are not idempotent. Write a scoped catch-up file instead.
172
175
 
176
+ ## Change history
177
+
178
+ - 2026-09-21 — Added the local-checkout case (behind on `dbchanges` reads as a schema gap; the fix is always an unapplied existing file — grep first) and the one-pass whole-schema `information_schema` column-count diff. (bala)
179
+
173
180
  ## Related
174
181
 
175
182
  - [dbchanges2 architecture](../architecture.md)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-09-21
10
10
  owners: ["mhammontree", "bala", "kyalamarthi", "rgirish"]
11
11
  files:
12
12
  - dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
@@ -133,6 +133,23 @@ Verified 2026-08-17 on beta `Client_Aig`: **`_modules/netsuite/2026-07-10a - Uni
133
133
 
134
134
  **5. Related observation, same escalation.** `Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql` and `Client/2026-07-16b - TrackingNumberSignatureTypeFieldPermission.sql` resolve their `recordFieldId` via `SET @fieldId = (SELECT … Core.RecordFields …)` before each INSERT block, with later blocks' `SET`s positioned **after** earlier INSERTs. Two factual consequences observed: (a) reading `Core.RecordFields` from a `Client/` file is itself a **Database-isolation violation** per [architecture.md](../architecture.md); (b) when the file is run in chunks / per client selection, the later block's variables are unset, `WHERE sibling.recordFieldId = NULL` matches nothing, and **0 rows insert with no error** — this silently no-op'd the `measures.measureType` read grant **twice** (first in production, then again in dev-sandbox), each time presenting as "empty unit dropdowns". The architecture-consistent remedy is to **hardcode the `recordFieldId` literal**, which removes the `Core` lookup entirely and with it the half-execution failure mode — but that is exactly the practice blocked by the id-divergence hazard in item 2 above, which is why both go to jcardinal together.
135
135
 
136
+ **6. Confirmed again 2026-09-21 — and the failure is WORSE than a duplicate-key error: it truncates
137
+ the file.** `Core/2026-09-18a - UserDelegatorUserRecords.sql` pins `RecordFields` **2604-2607**; on
138
+ dev-sandbox those four ids already belong to record 344 (`ItemCategoryFeatureGroupTranslations`).
139
+ The file dies on its **first** `RecordFields` insert and **every statement after that point never
140
+ runs** — dev-sandbox was left with `Records` 362 present, **zero** `RecordFields`, no
141
+ `InherentRecordChildren` row and no ACL, i.e. a half-registered record that looks provisioned.
142
+ Only the field ids clashed; `Records.id` 362 was free in both.
143
+
144
+ **Working practice until the escalation is answered:** read `MAX(id)` on the **target** environment
145
+ before pinning, and ship an environment-specific twin file when they differ — here
146
+ `Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql` with ids **2767-2770**, plus matching
147
+ `Client_Compass` / `Client_CompassCanada` ACL twins. Give the twin its **own** uuid literals for any
148
+ row whose natural key is environment-specific (see
149
+ [re-runnable additive migrations](../features/rerunnable-additive-inserts.md)). After running a
150
+ pinned-id `Core/` file anywhere, **verify the tail of the file landed**, not just its first
151
+ statement.
152
+
136
153
  ### Known remaining drift (dev-sandbox, as of 2026-08-03)
137
154
 
138
155
  - `item-fulfillments.returnAddressId` — the **column exists** but there is **no Core RecordField**. Only needed for the return-label flow.
@@ -154,10 +171,16 @@ All three missing columns are merged in `dbchanges2` `_main`. Note the shape dif
154
171
 
155
172
  ## Gotchas
156
173
 
174
+ - **A pinned-id `Core/` metadata file that hits a duplicate key STOPS THERE** — the record row can exist while its `RecordFields`, `InherentRecordChildren` and ACL silently never ran. Check the last statement's rows, not the first.
175
+
157
176
  - **NO carrier can be tested on ANY non-production environment.** Beta runs `[_underscore] debug_mode = 1`, which routes UPS to `wwwcie.ups.com` (CIE/test), and its `[fedex] oauth` points at `https://apis-sandbox.fedex.com` (production uses `https://apis.fedex.com`). But the carrier **account numbers** in non-prod data are copied from production, and neither test environment accepts a production account — UPS returns **"Missing or invalid shipper number" (120100)** and FedEx returns **HTTP 400**. **Label generation can only be verified in production** until CIE/sandbox-valid test accounts are seeded into non-prod `ShippingCarrierAccountNumbers`. (Remedy = obtain those test accounts; documented as the fix, not done.)
158
177
  - **⚠ Beta and production point at the SAME NetSuite account (1095849).** Any beta fulfillment writes to **production** NetSuite. Treat a beta fulfill run as a production side effect.
159
178
  - **In dev-sandbox, `Core` and `Client_*` share one cluster**, so a `Core.`-qualified reference in a `Client/` migration resolves there and looks fine. **Production splits them** (prod-core / prod-client) — a clean dev-sandbox run is never proof a migration is cluster-safe (see the architecture doc's hard isolation rule).
160
179
 
180
+ ## Change history
181
+
182
+ - 2026-09-21 — Second confirmed case of the pinned `Core.RecordFields` id divergence (2604-2607 belong to record 344 on dev-sandbox) and the worse consequence: the duplicate key **aborts the rest of the file**, leaving a half-registered record. Added the environment-specific twin-file practice (`…DevSandbox.sql`, ids 2767-2770) used for `user-delegator-users`. (bala)
183
+
161
184
  ## Related
162
185
 
163
186
  - [dbchanges2 architecture](../architecture.md)
@@ -6,8 +6,8 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-17
10
- owners: [tcox]
9
+ updated: 2026-09-21
10
+ owners: [tcox, bala]
11
11
  files:
12
12
  - src/components/OrderForDropdown/OrderForDropdown.tsx
13
13
  - src/components/OrderForDropdown/useOrderForDropdownViewModel.ts
@@ -18,6 +18,7 @@ files:
18
18
  - src/pages/Cart/api/CartApi.ts
19
19
  - src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/ADMIN/HEADERFIELDS.json
20
20
  - src/pages/Cart/viewModel/FIELDS/COMPASS/ENGLISH/ADMIN/CARTPAGE.ts
21
+ - src/pages/Cart/types.ts
21
22
  - cypress/e2e/homePage/orderForDropdown.cy.ts
22
23
  related:
23
24
  - 2.0/apps/toga2-commerce/architecture.md
@@ -25,6 +26,7 @@ related:
25
26
  - 2.0/apps/toga2-commerce/features/cart-notification-emails.md
26
27
  - 2.0/apps/toga2-commerce/features/cart-page-config-architecture.md
27
28
  - 2.0/apps/toga2-commerce/features/view-as-session-tokens.md
29
+ - 2.0/apps/_underscore/features/order-for-delegation.md
28
30
  - 2.0/apps/toga2-commerce/workflows/cypress-testing.md
29
31
  ---
30
32
 
@@ -49,11 +51,17 @@ MVVM split: `OrderForDropdown.tsx` (view), `useOrderForDropdownViewModel.ts` (st
49
51
  - Menu: search box → yourself first as **"<Name> (Myself)"** → the user list → **Load more** past 100. Check mark on the current pick. `aria` listbox/option roles, Escape closes and returns focus to the trigger, outside click closes.
50
52
  - Copy keys under `orderFor`: `isEnabled`, `label`, `searchPlaceholder`, `myselfLabel`, `loadingMessage`, `noOptionsFoundMessage`, `loadMoreLabel` — in all four COMPASS/ENGLISH role files.
51
53
 
52
- ### 2. There is no "delegation" API — it is the cart's own user list
54
+ ### 2. The delegation list — `fetchDelegationList`, plus the legacy `GET /users` list
53
55
 
54
- Verified 2026-09-17 across `toga2-commerce`, `api2`, `_underscore`, `dbchanges2` and the client-sandbox + prod databases: **no delegation endpoint or table exists.** api2's `/auth/delegator` is the view-as token, unrelated (see [view-as-session-tokens](view-as-session-tokens.md)).
56
+ **Corrected 2026-09-21: a delegation table and API now exist** (TRUE-81848). The 2026-09-17 note here — "no delegation endpoint or table exists" — was true then and is **false now**: `Users_DelegatorUsers` + route `user-delegator-users` ship in every `Client_*` DB, and `/auth/delegator` now enforces the delegation, so it is no longer only the view-as token. Server side: [order-for delegation](../../_underscore/features/order-for-delegation.md).
55
57
 
56
- The list is the same `GET /users` the cart uses, via `getUserOptions()` / `getSearchValuesFromAdvancedInput()` in `src/pages/Cart/api/CartApi.ts`:
58
+ `fetchDelegationList(ownUuid)` in `src/pages/Cart/api/CartApi.ts` returns `DelegationOption[]` (`{uuid, name, email}`, `src/pages/Cart/types.ts`) — everyone that user may order for, one call, no paging. Three rules:
59
+
60
+ - **Call it BEFORE any token switch.** Once the token belongs to the other person, the signed-in user's own record reads as a **403**.
61
+ - **Never request `id` in the fields** — asking for `id` makes the whole GET 403 for these roles. Use `uuid`.
62
+ - **It returns an empty list today** — `Users_DelegatorUsers` ships empty, with no admin screen and no import.
63
+
64
+ The list the pill still renders is the same `GET /users` the cart uses, via `getUserOptions()` / `getSearchValuesFromAdvancedInput()` in `src/pages/Cart/api/CartApi.ts`:
57
65
 
58
66
  - **Admins** get all active users; server-side search when `CARTPAGE settings.isSearchAnyoneEnabled`.
59
67
  - **Supervisors** get their direct reports through the `Users@supervisor` join, filtered locally.
@@ -88,6 +96,7 @@ Test ids: `order-for-dropdown`, `order-for-trigger`, `order-for-current-name`, `
88
96
 
89
97
  ## Gotchas
90
98
 
99
+ - **⚠ The pick alone does NOT order for anyone — the front end must switch the token.** Today the pill only stores the pick and passes it as a cart form value; nothing calls `/auth/delegator`. So `$api->delegator` is never set, `originalUserId` stays NULL, the server-side delegation gate never fires, and the buyer browses their **own** catalog and prices while "ordering for" someone else. Required switch: `GET /users/encryptedUser?uuid=<target>` → `POST /auth/delegator` → `beginDelegation(tokens, originalUserUuid)` from `src/api/sessionTokens.ts`; `restoreOriginalTokens()` returns with no new login. **One parked-token slot, shared with View As** — Order For and View As cannot nest. Owned by another developer; not in TRUE-81848.
91
100
  - **The `ownerUuid` guard is what makes persistence safe — never drop it.** A persisted pick with no owner check is inherited by the next login or by a view-as target and silently orders for the wrong person. Read the store through `getOrderForUserFor(pick, ownerUuid)`, never raw.
92
101
  - **Do not set `useSelectedUserZu` from the header.** The cascade reads the *previous* selected user out of the store (see [cart-notification-emails](cart-notification-emails.md)); writing it early makes the previous-user email cleanup a no-op and strands the old person's notification emails on the order.
93
102
  - **A missing `orderFor` key renders nothing, by design.** `useAssignClientFields` has no fallback for a missing key, so Compass Canada and Quad get `undefined` and the component returns `null` — safe default-off. Adding a tenant means adding the key to every role file for that tenant.
@@ -97,6 +106,7 @@ Test ids: `order-for-dropdown`, `order-for-trigger`, `order-for-current-name`, `
97
106
 
98
107
  ## Change history
99
108
 
109
+ - 2026-09-21 — Corrected the "no delegation API exists" finding: `Users_DelegatorUsers`, route `user-delegator-users` and the enforced `/auth/delegator` now exist (TRUE-81848). Added `fetchDelegationList` + `DelegationOption` (call before any token switch; never request `id`), and recorded that without the token switch the pick changes nothing server side. (bala)
100
110
  - 2026-09-17 — Pick is now persisted to `localStorage["order-for-zu"]` with an `ownerUuid` guard (TRUE-81852); the submit / edit-order-exit / view-as-return / reload clears were removed, and the cart writes the loaded user back to the store. Spec now 4/4 green. (tcox)
101
111
  - 2026-09-17 — Header order-for pill added (TRUE-81851), in-memory hand-off store, cart clears on failed user load. (tcox)
102
112
 
@@ -107,4 +117,5 @@ Test ids: `order-for-dropdown`, `order-for-trigger`, `order-for-current-name`, `
107
117
  - [cart-notification-emails](cart-notification-emails.md)
108
118
  - [cart-page-config-architecture](cart-page-config-architecture.md)
109
119
  - [view-as-session-tokens](view-as-session-tokens.md)
120
+ - [order-for delegation (server side)](../../_underscore/features/order-for-delegation.md)
110
121
  - [cypress-testing](../workflows/cypress-testing.md)
@@ -6,8 +6,8 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-17
10
- owners: [tcox]
9
+ updated: 2026-09-21
10
+ owners: [tcox, bala]
11
11
  files:
12
12
  - src/api/sessionTokens.ts
13
13
  - src/api/apiBaseUrl.ts
@@ -21,6 +21,7 @@ related:
21
21
  - 2.0/apps/toga2-commerce/features/header-order-for-dropdown.md
22
22
  - 2.0/apps/toga2-commerce/workflows/cypress-testing.md
23
23
  - 2.0/apps/api2/architecture.md
24
+ - 2.0/apps/_underscore/features/order-for-delegation.md
24
25
  ---
25
26
 
26
27
  How an admin impersonates another user and gets back: the delegated token pair, the parked admin pair in localStorage, and the return path. Open before touching auth tokens, the axios refresh, or the view-as button.
@@ -76,10 +77,12 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
76
77
  ## Open items (out of ticket, not fixed)
77
78
 
78
79
  - `SENTRY_AUTH_TOKEN` is committed in 25 tracked `.env*` files — treat it as compromised and rotate it. (Never store the value in this KB.)
79
- - Confirm api2 authorizes `/auth/delegator` **server-side** as admin-only; the client-side gate is not a control.
80
+ - ~~Confirm api2 authorizes `/auth/delegator` **server-side**~~ — **answered 2026-09-21:** it now re-checks `_Model_Client_User::canUserOrderForOtherUser` at mint time (and `/auth/refresh` re-checks it), so a captured encrypted pair cannot be replayed. See [order-for delegation](../../_underscore/features/order-for-delegation.md).
81
+ - **The parked-token slot is shared with "Order For".** There is one `originalAuthTokens` key and `beginDelegation` refuses to overwrite it, so once Order For switches tokens the two features cannot nest — decide the precedence before wiring the Order For switch.
80
82
 
81
83
  ## Change history
82
84
 
85
+ - 2026-09-21 — `/auth/delegator` is now gated server side by the Order For delegation check; noted that Order For will share the single parked-token slot. (bala)
83
86
  - 2026-09-17 — Created. Admin pair parked in `originalAuthTokens` on delegation and restored on return; `/auth/encrypted` re-mint removed; single-flight identity-bound refresh (TRUE-81852, commit a8c3a537). (tcox)
84
87
 
85
88
  ## Related
@@ -88,3 +91,4 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
88
91
  - [header-order-for-dropdown](header-order-for-dropdown.md)
89
92
  - [cypress-testing](../workflows/cypress-testing.md)
90
93
  - [api2 architecture](../../api2/architecture.md)
94
+ - [order-for delegation](../../_underscore/features/order-for-delegation.md)
@@ -6,8 +6,8 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: ["jcardinal", "apeterson", "tcox"]
9
+ updated: 2026-09-21
10
+ owners: ["jcardinal", "apeterson", "tcox", "bala"]
11
11
  files:
12
12
  - toga2-commerce/amplify.yml
13
13
  - toga2-commerce/.gitattributes
@@ -117,6 +117,8 @@ AWS Amplify (build + hosting); GitHub Actions (`sync-stage-environments.yml` + t
117
117
  3. Restart the dev server.
118
118
 
119
119
  A **full `node_modules` wipe is not needed**. Because blox is installed per channel, every branch switch can move the required blox version — treat this class of error as "reinstall blox and clear the Vite cache" before touching any import statement.
120
+
121
+ **Second shape of the SAME drift (2026-09-21): the app crashes on any toast.** `package.json` + lock pinned `1.1.1-production.139` while `node_modules` again held `1.0.317`. The **old** blox `Toaster` takes `message` as an **array** and calls `message.map()`; `ToasterList.tsx` passes a string or `{title, content}` matching the **new** one — so `Uncaught TypeError: message.map is not a function` plus a React error boundary on `<Toaster>`, which reads as an app bug. Same fix, plus **restart Vite with `--force`** so it drops the pre-bundled dep cache. Any blox runtime type error is a version check first.
120
122
  - **`resolve.dedupe` was missing from commerce's `vite.config.ts`** (a `frontend.md` §11 MUST), which made consuming blox via a local symlink hazardous: `toga-blox-npm/node_modules/` contains its own `react@18.3.1`/`react-dom@18.3.1`, and because blox ships **unbundled**, imports resolve against the nearest `node_modules` — under a symlink that is blox's own, giving a second React copy. This is precisely why supply's symlink workflow works (it has always deduped `react`/`react-dom`/`@tanstack/react-table`) and commerce's did not. Commerce now dedupes `react`, `react-dom`, `react-hook-form`, `react-router-dom`, `framer-motion`, `@tanstack/react-query`, `@tanstack/react-table`.
121
123
  - **`package.json` pins blox with a CARET** (`"@agilant/toga-blox": "^1.0.322-sandbox-client.117"`), violating `frontend.md` §13(a) — some published versions ship an empty `dist`, and a caret can float you onto one. Pin exact. Related: the `TRUE-80707` branch pin is **11 versions behind** the `_sandbox-client` branch pin (`1.0.333-sandbox-client.134`); reconcile before merging.
122
124
  - **Secret hygiene re-confirmed unremediated 2026-08-26** — the live tokens are still in the tracked `.npmrc` in **both** `toga2-commerce` and `toga25-supply`.
@@ -125,6 +127,10 @@ AWS Amplify (build + hosting); GitHub Actions (`sync-stage-environments.yml` + t
125
127
  - **A caret on a PRERELEASE is inert, not floating.** `^1.0.322-sandbox-client.117` resolves to **exactly** `1.0.322-sandbox-client.117`: npm only matches prereleases sharing the same `major.minor.patch`. Verified with `semver.maxSatisfying`. It is still a [frontend standard](../../../standards/frontend.md) §13(a) violation and confusing, but it is **not** a drift risk — don't chase it as one.
126
128
  - **Commerce's dependency health is better than supply's**, verified 2026-08-27: all 19 blox peers satisfied (none missing) and `resolve.dedupe` is committed on `TRUE-80707`.
127
129
 
130
+ ## Change history
131
+
132
+ - 2026-09-21 — Added the second symptom of stale-`node_modules` blox drift: the old `Toaster` `message.map()` crash on any toast (installed 1.0.317 vs pinned 1.1.1-production.139); fix is installing the locked version and restarting Vite with `--force`. (bala)
133
+
128
134
  ## Related
129
135
 
130
136
  - [toga2-supply Amplify Build & Deploy](../../toga2-supply/workflows/amplify-build-and-deploy.md) — the base pattern and shared gotchas.
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
 
20
20
  ## 2.0 framework
21
21
 
22
- - **_underscore** (_Underscore) _(framework core)_ — 87 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
+ - **_underscore** (_Underscore) _(framework core)_ — 88 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
23
23
  - **worker2** (Worker) — 73 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
24
24
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
25
25
  - **dbchanges2** (Database Changes) _(framework core)_ — 19 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.855",
3
+ "version": "1.0.856",
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",