toga-ai 1.0.430 → 1.0.431

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.
@@ -7,6 +7,7 @@
7
7
  | [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
8
8
  | [Health-check endpoint (/health liveness short-circuit)](features/health-check-endpoint.md) | `_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200 **before** any routing, DB bootstrap, or V2 engine work runs. | api2/Controller/Index.php |
9
9
  | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
10
+ | [Nested FK object embedding is gated by the CHILD record's own ACL](features/nested-fk-acl-embedding.md) | When the V2 JSON engine serializes a foreign-key field into a **nested object** (in `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record | api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql |
10
11
  | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php |
11
12
  | [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php |
12
13
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: Nested FK object embedding is gated by the CHILD record's own ACL
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-23
10
+ owners: [apeterson]
11
+ files:
12
+ - api2/Component/Api/V2/V2.php
13
+ - dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql
14
+ related:
15
+ - cross-client-data-retrieval.md
16
+ - tableview-apiwhereclause-row-filtering.md
17
+ ---
18
+
19
+ ## What it is
20
+
21
+ When the V2 JSON engine serializes a foreign-key field into a **nested object** (in
22
+ `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record's own
23
+ `AclRecordPermissions` against the caller's roles before embedding it. If no grant exists, the
24
+ engine records a debug `"No ACL Record Permissions records exist"` and **silently drops the nested
25
+ object** — still HTTP 200, `isSuccess: true`, no error or warning `message`. This is why a linked
26
+ object can silently disappear from an API payload for one client but embed fine for another.
27
+
28
+ ## How it works
29
+
30
+ - The gate needs only an `AclRecordPermissions` **row** for the child record on one of the caller's
31
+ roles. Unlike a direct top-level GET of that record, the embedding path does **not** call
32
+ `buildSqlExpression` — an ACL row alone (no logic-group expression) is sufficient to embed.
33
+ - Which roles are checked depends on the child `Records.aclDatabase`. A **CLIENT**-acl record checks
34
+ only the caller's **client** roles; a CORE-acl record checks CORE roles (mirrors the dispatch/record
35
+ rules in [surface-meta-option](surface-meta-option.md)).
36
+ - The drop is **silent by design**: absence of a grant is treated as "not authorized to see this
37
+ linked object", collapsed to omission rather than an error. There is no `messages[]` entry, so the
38
+ symptom presents purely as a missing key in `data`.
39
+
40
+ ## The role-3-vs-role-1 mis-seed pattern (the durable gotcha)
41
+
42
+ The most common cause of a silently-missing nested object is an ACL **mis-seed on the wrong role**:
43
+ a child record granted to a service role no human carries (e.g. role **3 "API"**) instead of the
44
+ base role every user carries (role **1 "Base"**). Because a CLIENT-acl record checks only the
45
+ caller's client roles, a human user authenticated with role 1 gets no grant → the child is dropped;
46
+ a machine/API caller on role 3 sees it fine.
47
+
48
+ - **Confirmed instances:** the sales-order↔purchase-order join. On **Compass** the `purchase-orders`
49
+ record (Core Record 17, `_Model_Client_PurchaseOrder`, `aclDatabase=CLIENT`) was granted in
50
+ `Client_Compass` only to role 3, so the nested `purchaseOrder` dropped from the
51
+ sales-order-purchase-orders join response; **Quad** had it on role 1 (perm + logic group → `all`
52
+ expression) and embedded fine. The **same** mis-seed was previously found on **CompassCanada**
53
+ (record 275). It is **not** a single-client quirk — audit every client when a nested object is
54
+ reported missing for some tenants only.
55
+ - **The fix mirrors the working tenant:** add a role-1 `AclRecordPermissions` (full CRUD,
56
+ app-agnostic) + `AclLogicGroups` (AND) + `AclLogicGroupExpressions` reusing the existing `all`
57
+ record expression on the child record. Field-read perms on role 1 usually already exist (no
58
+ `AclFieldPermissions` insert needed). Author it **id-agnostically** (natural keys, not hardcoded
59
+ ids), `NOT EXISTS`-guarded, wrapping any self-referencing subquery in a derived table to avoid MySQL
60
+ error 1093. See `dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql`.
61
+
62
+ ## Diagnostic tell
63
+
64
+ A top-level GET of the child record **works** but the **nested embed of the same record under a
65
+ parent is missing**, with a 200/`isSuccess:true` response and no `messages[]`. Chase the child
66
+ record's `AclRecordPermissions` for the caller's role — not the parent's grant, and not field
67
+ permissions.
68
+
69
+ ## Change history
70
+ - 2026-07-23 — Documented that V2 nested-FK embedding (`getFullModelData`) re-checks the CHILD
71
+ record's own `AclRecordPermissions` and SILENTLY drops the nested object (200, isSuccess true, no
72
+ message) when no grant exists — needing only an ACL row, not a logic-group expression. Fixed the
73
+ Compass sales-order↔PO join: `purchase-orders` (record 17, CLIENT-acl) was granted only to role 3
74
+ ("API") not role 1 ("Base"), so humans lost the nested `purchaseOrder` while Quad (role-1 grant)
75
+ embedded fine; mirrored Quad's role-1 grant (`Client_Compass/2026-07-23b`). Confirmed this is the
76
+ SAME role-3-vs-role-1 mis-seed pattern already seen on CompassCanada (record 275) — not
77
+ CompassCanada-only. (apeterson)
78
+ </content>
79
+ </invoke>
@@ -3,5 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
- | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql |
6
+ | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql |
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/ |
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-21
9
+ updated: 2026-07-23
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
@@ -65,6 +65,7 @@ files:
65
65
  - dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql
66
66
  - dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql
67
67
  - dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql
68
+ - dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql
68
69
  related:
69
70
  - ../../_underscore/features/surface-resolver.md
70
71
  ---
@@ -499,6 +500,14 @@ rule resumes.
499
500
  override is added). **Open follow-up.**
500
501
 
501
502
  ## Change history
503
+ - 2026-07-23 — Repointed the Order Details **"PO Number"** detail field (all clients, Core seed).
504
+ `Core/2026-07-23a - PoNumberDetailFieldValueKey.sql` updates the `SurfaceElements` row (uuid
505
+ `d45c51d8-8211-11f1-bfa7-a30f63c3a801`, label `salesOrder.field.poNumber`) `config` from
506
+ `{"valueKey":"_purchaseOrders","isPersonaValue":true}` to
507
+ `{"valueKey":"purchaseOrderDetails.purchaseOrder.number"}`. The FE DetailSection resolves a field
508
+ value by plain dot-path against the order object, and the real PO number lives at
509
+ `purchaseOrderDetails.purchaseOrder.number`; `isPersonaValue` was dropped (it applies only to
510
+ persona arrays, not scalars). Keyed by stable uuid. (apeterson)
502
511
  - 2026-07-21 — Seeded the **per-client decision-summary overrides** off the shared
503
512
  `sales-order-decision-summary` surface (all `2026-07-21c`, client-level — `roleId`/`personaId`/
504
513
  `languageId` NULL): **Quad** hides three summary rows (`assignedTo._name`, `c_erpEntityId`,
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-21
9
+ updated: 2026-07-23
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga25-supply/src/surface/useFetchSurfaceMeta.ts
@@ -165,11 +165,37 @@ modal copy, visibility/enable) to Surface; **leave data-fetch wiring (fetch slug
165
165
  nested-table topology) in JSON/code.** Not every screen reduces to a generic `SurfaceSection` swap —
166
166
  see SalesOrders below.
167
167
 
168
- ## SalesOrders record-modal sections migrated via the registered-renderer seam (roster-gated)
169
-
170
- The SalesOrders detail sections are now migrated onto Surface for the **seeded roster clients only**,
171
- via the registered-renderer approach (not a generic `SurfaceSection` swap `SalesOrderSummaryGrid`'s
172
- three bespoke renderers `detailSection`/`locationCard`/`totalsCard` are untouched).
168
+ ## Surface on/off is 100% BACKEND-driven no FE roster (2026-07-23)
169
+
170
+ **A client turns Surface on purely by backend state never a hardcoded FE allowlist.** The
171
+ SalesOrders record modal previously gated surface resolution on an in-code roster
172
+ (`SALES_ORDER_SURFACE_MIGRATED_CLIENTS = ["COMPASS","COMPASSCANADA","QUAD"]`): it fetched the surface
173
+ group only for rostered clients, and everyone else fell back to JSON. That roster was the **last FE
174
+ gate** blocking BE-driven resolution — and it blocked NYCHH from ever receiving a corrected surface
175
+ field even though its API returned the data. **It has been removed entirely.**
176
+
177
+ - The SO modal now **ALWAYS** fetches the surface group and spreads
178
+ `surfaceBundleToTenantFields(surfaces)` **unconditionally** (view-model
179
+ `useSalesOrderRecordModalLayoutModel.tsx`; roster constant deleted from
180
+ `surfaceBundleToTenantFields.ts` and `helpers/index.ts`).
181
+ - Fetch is **failure-isolated** (`retry:false`, errors → `isError`, `surfaces` defaults to `{}`), and
182
+ `surfaceBundleToTenantFields` returns `{}` for an empty/unauthorized bundle — so a client the backend
183
+ does **not** serve stays JSON-driven with no roster. Merge remains **JSON-base + Surface-wins**.
184
+ - A client now "turns on" Surface by two backend facts only: (1) being granted the `surfaces`/`meta-group`
185
+ script ACL (`AclRecordScripts` — see [surface-meta-option](../../api2/features/surface-meta-option.md)),
186
+ **and** (2) having its surfaces seeded/overridden server-side. No FE change is ever needed to onboard a
187
+ client onto Surface.
188
+ - **Audit outcome:** every other `useFetchSurfaceMeta`/`useFetchSurfaceMetaGroup` call site (item-record
189
+ modal, approval-decision modal, Items/VendorItems/Inventory/Login) was already BE-driven — the SO modal
190
+ roster was the ONLY offending gate. The `clientSlug` conditionals in `src/fieldsConfig/` (`resolveRole`,
191
+ `FIELDS[clientSlug]`) are the **legacy JSON fallback layer, NOT surface gating**, and are correctly left
192
+ in place (they serve clients the backend doesn't yet drive via Surface).
193
+
194
+ ## SalesOrders record-modal sections — migrated via the registered-renderer seam
195
+
196
+ The SalesOrders detail sections are migrated onto Surface via the registered-renderer approach (not a
197
+ generic `SurfaceSection` swap — `SalesOrderSummaryGrid`'s three bespoke renderers
198
+ `detailSection`/`locationCard`/`totalsCard` are untouched).
173
199
 
174
200
  - **`surfaceBundleToTenantFields()`** (`SalesOrders/helpers/`) adapts the grouped Surface bundle into
175
201
  the existing **`TenantFields`** shape, so the existing `SalesOrderSummaryGrid` renderers and
@@ -178,8 +204,9 @@ three bespoke renderers `detailSection`/`locationCard`/`totalsCard` are untouche
178
204
  gating are **not** moved. (Registered-renderer seam: *config describes, code decides.*)
179
205
  - **Full adapter-seam data path (verified this session, no direct `<SurfaceSection>`):**
180
206
  `useFetchSurfaceMetaGroup(SALES_ORDER_SURFACE_SLUGS)` → `surfaceBundleToTenantFields(surfaces)` →
181
- merged into `tenantFields` (roster clients only; `clientSlug` is the **uppercased** `getHostname()`
182
- result matched against the roster) → **`buildPatchedTenantFields`** (which only overrides
207
+ merged into `tenantFields` (**unconditionally, for every client**; an empty/unauthorized bundle
208
+ adapts to `{}` so unseeded clients stay JSON-driven) → **`buildPatchedTenantFields`** (which only
209
+ overrides
183
210
  `recordActionFields`, so it **preserves all surface section keys**) → `SalesOrderView` →
184
211
  **`SalesOrderSummaryGrid`** (renders by `section.type` / `cardType`:
185
212
  `detailSection`/`locationCard`/`totalsCard`, where `cardType` comes from `surface.config.cardType`)
@@ -212,15 +239,14 @@ from "≥1 field visible", and bind an array-derived value via element `config`
212
239
  resolved on the FE. This is the presentation counterpart of the schema's field-driven pattern and the
213
240
  third section pattern alongside card sections (`cardType` → grid renderer) and marker toggles.
214
241
 
215
- - **Roster gate `SALES_ORDER_SURFACE_MIGRATED_CLIENTS = [COMPASS, COMPASSCANADA, QUAD]`** (an
216
- in-code roster in `surfaceBundleToTenantFields.ts`; the view-model
217
- `useSalesOrderRecordModalLayoutModel.tsx` consults it). The view-model resolves sections from
218
- Surface **only** for roster clients; every other client (incl. never-migrated
219
- NYCHH/Prudential/SPGlobal) skips the fetch and stays fully JSON-driven. Merge is **JSON-base +
220
- Surface-wins.** **Why a roster, not a flag:** the Core SECTION surfaces are **shared** and seeded
221
- from Compass's shape without the gate every client would inherit Compass's base and regress. A
222
- **code** roster (not an env flag) is deliberate: a client is removed from the roster only once its
223
- own seeds are verified.
242
+ - **No roster (as of 2026-07-23).** The former
243
+ `SALES_ORDER_SURFACE_MIGRATED_CLIENTS = [COMPASS, COMPASSCANADA, QUAD]` in-code gate was **removed**;
244
+ the view-model now resolves sections from Surface for every client, and an empty/unauthorized bundle
245
+ adapts to `{}` so unseeded clients stay JSON-driven. Merge is still **JSON-base + Surface-wins**. The
246
+ guardrail that used to justify the roster (shared Core SECTION surfaces seeded from Compass's shape)
247
+ is now enforced backend-side by the **Core-neutral-default per-client opt-in** seeding rule (see
248
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md))Core no longer ships
249
+ Compass's shape as a live default, so removing the roster does not regress unseeded clients.
224
250
  - **JSON cleanup:** the migrated section blocks (orderDetails/shipTo/billTo/orderSummary/
225
251
  orderRecurring + the 5 display sections) were **deleted** from the COMPASS / COMPASSCANADA / QUAD
226
252
  `orderViewFields.json` files. Data-wiring (`salesOrderDetailsConfig`/`itemFulfillmentClickRule`/
@@ -471,6 +497,19 @@ now carry it (2026-07-21):
471
497
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
472
498
 
473
499
  ## Change history
500
+ - 2026-07-23 — **DECISION + FIX: surface on/off per client is now 100% backend-driven — removed the FE
501
+ roster entirely.** Deleted `SALES_ORDER_SURFACE_MIGRATED_CLIENTS = [COMPASS,COMPASSCANADA,QUAD]` from
502
+ `surfaceBundleToTenantFields.ts` + `helpers/index.ts`; `useSalesOrderRecordModalLayoutModel.tsx` now
503
+ ALWAYS fetches the surface group and spreads `surfaceBundleToTenantFields(surfaces)` unconditionally.
504
+ The fetch is failure-isolated (`retry:false` → `surfaces` defaults to `{}`) and the adapter returns
505
+ `{}` for an empty/unauthorized bundle, so a client the BE doesn't serve stays JSON-driven with no
506
+ roster. A client "turns on" Surface purely via backend state: the `surfaces`/`meta-group`
507
+ `AclRecordScripts` grant + seeded/overridden surfaces. This unblocked NYCHH (was blocked by the roster
508
+ from receiving the corrected PO-number field). Swept every other `useFetchSurfaceMeta(Group)` call
509
+ site (item-record modal, approval-decision modal, Items/VendorItems/Inventory/Login) — all already
510
+ BE-driven; the SO modal roster was the only FE gate. Confirmed the `src/fieldsConfig/` `clientSlug`
511
+ conditionals are the legacy JSON fallback layer (not surface gating) and left them in place.
512
+ (apeterson)
474
513
  - 2026-07-21 — Extended `surfaceBundlesToDecisionFields.ts` to emit **`isConcatenated`** on
475
514
  decision-summary rows: new exported `DecisionSummaryConcat` type (string shorthand | descriptor
476
515
  `{valueKey, prefix?, suffix?, valueType?, layout?, label?}`, mirroring blox `BaseDetailField`); optional
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.430",
3
+ "version": "1.0.431",
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",