toga-ai 1.0.668 → 1.0.669

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.
@@ -21,6 +21,7 @@ related:
21
21
  - ../workflows/environment-configuration-and-provisioning.md
22
22
  - ../workflows/codepipeline-codeconnections-deploy.md
23
23
  - ../../_underscore/features/togaiq-gateway-client.md
24
+ - ../../dbchanges2/workflows/framework-branch-ahead-of-schema.md
24
25
  ---
25
26
 
26
27
  ## Summary
@@ -154,6 +155,26 @@ Related detail: **the slug V2 uses to pick the override class is `Clients.client
154
155
  (`V2.php:1521`), taken from the JWT. `Core.Clients` has **no `slug` column** — do not go looking for
155
156
  one.
156
157
 
158
+ ## ⚠ Moving a tier onto its correct branch is a SCHEMA event, not just a code event
159
+
160
+ The gotchas below are all about a tier running **stale** code. The inverse hurts too: when a deploy
161
+ moves an environment **forward** onto a newer `_underscore` branch — including a deploy that *fixes*
162
+ a wrong branch pin — it fast-forwards across every model change made since the pin, and
163
+ `_Model::initialize()` SELECTs **every declared property**. Any of those columns without a merged
164
+ `dbchanges2` migration becomes an **environment-wide MySQL 1054 → HTTP 500 / `EO-1`**, for every
165
+ tenant, immediately.
166
+
167
+ **Concrete (2026-08-26).** The 2026-08-26 deploy moved `sandbox-client` off the `_production`
168
+ framework — which it had been running for weeks (see the 2026-08-25 entry below) — onto
169
+ `_sandbox-client`, carrying ~2 weeks of model changes whose migrations were stranded on an unmerged
170
+ `dbchanges2` branch. Table-meta requests 500'd on `RecordFields.maxLength`, then (once that was
171
+ patched) on `TableViewFields.isGroupable`.
172
+
173
+ **Rule: pair a branch-pin correction with a migration sweep** — enumerate what the new branch expects
174
+ (`git log origin/_production..origin/<env-branch> --oneline -- Model/`) **before** redeploying, or
175
+ the tier trades a stale-code bug for a total outage. Full procedure:
176
+ [framework branch ahead of schema](../../dbchanges2/workflows/framework-branch-ahead-of-schema.md).
177
+
157
178
  ## Gotchas / known issues
158
179
 
159
180
  - **⚠ Merging to the branch named after the EB environment can be a no-op.** Merge to
@@ -220,6 +241,13 @@ one.
220
241
 
221
242
  ## Change history
222
243
 
244
+ - 2026-08-27 — Recorded the **inverse hazard**: moving a tier **forward** onto its correct
245
+ `_underscore` branch is a **schema event**. The 2026-08-26 deploy moved `sandbox-client` off the
246
+ `_production` framework (which the 2026-08-25 entry below explains it had been running for weeks)
247
+ onto `_sandbox-client`, fast-forwarding it across ~2 weeks of model changes whose `dbchanges2`
248
+ migrations were stranded on an unmerged branch — producing environment-wide MySQL 1054 → 500/`EO-1`
249
+ on `RecordFields.maxLength` and then `TableViewFields.isGroupable`. A branch-pin correction must
250
+ ship with a migration sweep of `origin/_production..origin/<env-branch> -- Model/`. (apeterson)
223
251
  - 2026-08-25 — Recorded the **second delivery mechanism**: besides `.ebextensions/git.php`, the
224
252
  **CodePipeline `_underscore` source action has its own branch trigger configured in AWS**, and it
225
253
  can serve the wrong branch while `git.<env>.json` is perfectly correct. Live case: `ApiSandboxClient`
@@ -43,9 +43,20 @@ it.
43
43
  - **`Client_*.TableViewFields`** — one row per **column** of a view. Key columns: `recordFieldId`
44
44
  (which field this column shows), `tableViewJoinId` (which join in the view the field is reached
45
45
  through), `slug` (the frontend/tanstack column id), `isVisible` (0 = projected-but-hidden),
46
- and the per-view **presentation flags** `isCopyable` / `isSortable` / `isFilterable`. These
47
- four flags **are** per-view — the same underlying field can be copyable in one view and not in
48
- another.
46
+ and the per-view **presentation flags** `isCopyable` / `isSortable` / `isFilterable` /
47
+ `isGroupable`. These flags **are** per-view — the same underlying field can be copyable in one
48
+ view and not in another.
49
+
50
+ > **⚠ `isGroupable` is declared in the framework but has NO migration (as of 2026-08-27).**
51
+ > `_underscore` `9f296a71` (2026-08-26) added `public $isGroupable = self::FIELD_BOOLEAN;` to
52
+ > `_Model_Client_TableViewField` **and** named the column in `_Model_Client_TableView::meta()`'s
53
+ > hand-written `SELECT` — but no `dbchanges2` file adds it, in any branch or in the blank client
54
+ > template. Any environment whose framework branch carries that commit **500s (`EO-1`, MySQL 1054)
55
+ > on every table-meta request, for every tenant**, until the column is added to each `Client_*` DB.
56
+ > It was hand-applied to `sandbox-client` as
57
+ > `TINYINT UNSIGNED NULL … AFTER isFilterable` (matching its sibling flags); the real fan-out
58
+ > migration is still outstanding. Background:
59
+ > [framework branch ahead of schema](../../dbchanges2/workflows/framework-branch-ahead-of-schema.md).
49
60
 
50
61
  ### A column's `type` / `precision` are NOT per-view — they come from `Core.RecordFields`
51
62
 
@@ -207,9 +218,20 @@ SalesOrderListingSortByDateOrderDesc.sql`).
207
218
  is suppressed.
208
219
  - **`type`/`precision` are canonical (Core-wide), not per-view.** Editing `Core.RecordFields`
209
220
  changes every view/client that shows the field; there is no per-view type override. `isCopyable`,
210
- `isSortable`, `isFilterable`, `isVisible` are the per-view knobs (on `TableViewFields`).
221
+ `isSortable`, `isFilterable`, `isGroupable`, `isVisible` are the per-view knobs (on
222
+ `TableViewFields`).
223
+ - **`TableView::meta()` contains a hand-written `SELECT` that names `TableViewFields` columns
224
+ explicitly.** So a new column on that table is required by **two** mechanisms — the model property
225
+ *and* that query — and a grep for the property alone under-reports what a missing column breaks.
211
226
 
212
227
  ## Change history
228
+ - 2026-08-27 - Added **`isGroupable`** as a fourth per-view presentation flag on `TableViewFields`,
229
+ with the warning that it is declared in `_underscore` (`9f296a71`, 2026-08-26 — model property
230
+ **and** `TableView::meta()`'s hand-written SELECT) while **no `dbchanges2` migration adds it on any
231
+ branch**, so any environment on that framework branch 500s (`EO-1`/1054) on every table-meta
232
+ request for every tenant. Hand-applied to `sandbox-client` as `TINYINT UNSIGNED NULL AFTER
233
+ isFilterable`; fan-out migration still outstanding. Also recorded that `meta()`'s explicit SELECT
234
+ makes a property grep an under-count of a missing column's blast radius. (apeterson)
213
235
  - 2026-08-26 - Added the **per-client type override**: a column can point at
214
236
  `TableViewFields.customRecordFieldId` instead of `recordFieldId`, so registering a
215
237
  `Client_<X>.CustomRecordFields` row with the **same field name** as the Core RecordField and the
@@ -7,6 +7,7 @@
7
7
  | [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/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, 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, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql |
8
8
  | [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/ |
9
9
  | [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 |
10
+ | [Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards — **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
10
11
  | [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**. | |
11
12
  | [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 |
12
13
  | [Verifying whether a dbchanges2 migration actually ran in an environment](workflows/verifying-a-migration-ran.md) | dbchanges2 has no execution ledger you can query — "did this file run here?" has to be answered from the **rows the file would have produced**. | Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql |
@@ -7,7 +7,7 @@ client: shared
7
7
  type: workflow
8
8
  status: active
9
9
  updated: 2026-08-11
10
- owners: ["bala", "mhammontree"]
10
+ owners: ["bala", "mhammontree", "apeterson"]
11
11
  files:
12
12
  - dbchanges2/Client_Growrk/2026-05-28.sql
13
13
  - dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql
@@ -22,6 +22,7 @@ related:
22
22
  - ../../_underscore/features/acl-permission-chain.md
23
23
  - ../../api2/features/v2-api-error-codes.md
24
24
  - ../../../../clients/growrk/features/units-netsuite-custom-fields.md
25
+ - ./framework-branch-ahead-of-schema.md
25
26
  ---
26
27
 
27
28
  ## Summary
@@ -36,6 +37,12 @@ touches the record.
36
37
  > (works in prod, broken in beta). Here **production is the outlier** and beta/dev-sandbox is
37
38
  > correct — so "it works on beta" is not evidence the schema is fine. Check *which* side is behind
38
39
  > before assuming.
40
+ >
41
+ > **Third possibility — no database is behind, the CODE is ahead.** If the 1054 started right after
42
+ > a **deploy**, is **not** client-specific, and the column exists in **no** environment, the
43
+ > framework branch is running ahead of schema (its migration was never merged). Use
44
+ > [framework branch ahead of schema](./framework-branch-ahead-of-schema.md) — the repair is a branch
45
+ > merge, not a per-client catch-up file.
39
46
 
40
47
  Written from repairing `Client_Growrk.Units` (missing `c_lastModified`, 6 of 7 columns and 1 of 6
41
48
  metadata rows applied) — see
@@ -155,6 +162,11 @@ part, and it is what makes a missing column fatal. Standard, non-`c_` fields use
155
162
 
156
163
  ## Change history
157
164
 
165
+ - 2026-08-27 — Added a pointer to the **third drift direction**: when the 1054 starts right after a
166
+ deploy, is not client-specific, and the column exists in no environment, the **framework branch is
167
+ ahead of schema** and the repair is a branch merge, not a catch-up file — see
168
+ [framework branch ahead of schema](./framework-branch-ahead-of-schema.md). No change to this
169
+ procedure. (apeterson)
158
170
  - 2026-08-11 — TRUE-80824: **corrected the premise.** The `Client_Growrk` drift was **not** a partially
159
171
  applied module migration — `Client_Growrk/2026-05-28.sql` is committed on `_main` (`af30e63`) and was
160
172
  **never executed** against production, which explains both the 2026-08-10 `Units` 500s and the
@@ -0,0 +1,256 @@
1
+ ---
2
+ title: "Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy"
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-27
10
+ owners: [apeterson]
11
+ files:
12
+ - _underscore/Model/Core/RecordField.php
13
+ - _underscore/Model/Client/TableViewField.php
14
+ - _underscore/Model/Client/TableView.php
15
+ - _underscore/Model/Core/Surface.php
16
+ - _underscore/Model/Client/Urgency.php
17
+ - api2/Component/Api/V2/V2.php
18
+ - dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql
19
+ - dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql
20
+ - dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql
21
+ - dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql
22
+ related:
23
+ - ./client-schema-drift-audit.md
24
+ - ./nonprod-metadata-drift-repair.md
25
+ - ./verifying-a-migration-ran.md
26
+ - ../architecture.md
27
+ - ../../api2/features/environment-variable-drives-underscore-branch.md
28
+ - ../../api2/features/tableview-field-metadata.md
29
+ - ../../_underscore/features/field-storage-row-hydration.md
30
+ ---
31
+
32
+ ## Summary
33
+
34
+ A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database
35
+ went backwards — **the code went forwards**. A deploy moved an environment onto a newer
36
+ `_underscore` branch that carries model changes whose `dbchanges2` migrations were **never merged**.
37
+ Every request touching an affected model then dies with MySQL **1054 "Unknown column"** →
38
+ **HTTP 500 / `EO-1`**, for **every tenant at once**, tenant-agnostic.
39
+
40
+ > **Which of the three drift docs do you want?**
41
+ >
42
+ > | The outlier is… | Doc |
43
+ > |---|---|
44
+ > | one **client's** DB, behind its models (migration partially/never applied) | [client schema-drift audit](./client-schema-drift-audit.md) |
45
+ > | a **non-prod environment's** metadata, behind production | [non-prod metadata drift repair](./nonprod-metadata-drift-repair.md) |
46
+ > | **the code** — the framework branch is ahead of every schema, including production's | **this doc** |
47
+ >
48
+ > The tell for *this* doc: the failure started **immediately after a deploy**, it is **not**
49
+ > client-specific, and the missing column exists in **no** environment because the migration
50
+ > exists in **no merged branch**.
51
+
52
+ Written from the 2026-08-26 `sandbox-client` outage: table-meta requests returned 500/EO-1 on two
53
+ successive columns after a deploy moved that environment from the `_production` framework onto
54
+ `_sandbox-client`, which carried ~2 weeks of model changes.
55
+
56
+ ## Why one model property takes down a whole environment
57
+
58
+ `_Model::initialize()` builds its SELECT from **every property declared on the class**, ignoring the
59
+ caller's `fields=` (see
60
+ [client schema-drift audit](./client-schema-drift-audit.md) → *Why it 500s*).
61
+ So this one line, shipped without a migration, breaks **every construction of that model,
62
+ everywhere**:
63
+
64
+ ```php
65
+ public $isGroupable = self::FIELD_BOOLEAN; // _Model_Client_TableViewField
66
+ ```
67
+
68
+ **Second variant, same failure, no model property involved:** hand-written SQL inside a model that
69
+ names the new column explicitly. `_Model_Client_TableView::meta()` lists `isGroupable` in its own
70
+ `SELECT`, so the column is required **twice** by two different mechanisms — a grep for the property
71
+ alone under-reports the blast radius.
72
+
73
+ **Contrast — `FIELD_STORAGE` is the exception**: those columns are excluded from the main SELECT and
74
+ hydrated lazily per row, so they 500 only once a query **returns** rows. See
75
+ [FIELD_STORAGE row hydration](../../_underscore/features/field-storage-row-hydration.md).
76
+
77
+ **Why a deploy can cause this without anyone changing the framework:** api2 clones `_underscore` at
78
+ **build time from a moving `_<ENVIRONMENT>` branch**, and the CodePipeline source action has its own
79
+ branch setting. A redeploy — including one that *fixes* a stale box — can silently move the
80
+ environment onto a newer framework branch. See
81
+ [ENVIRONMENT decides the `_underscore` branch](../../api2/features/environment-variable-drives-underscore-branch.md).
82
+
83
+ > **The corollary that surprises people: "unstaling" a box is a schema event.** Correcting a wrong
84
+ > branch pin fast-forwards the environment across every model change since the pin was made. Before
85
+ > re-pointing a tier at its correct branch, run the sweep below — the branch fix and the migrations
86
+ > ship together, or the tier trades a stale-code bug for an environment-wide 500.
87
+
88
+ ## Steps
89
+
90
+ 1. **Get the real exception.** You need the **column name**, the **database** (Core vs Client), and
91
+ the **model file + line** from the stack. Both 2026-08-26 failures pointed at the same entry
92
+ point, `V2.php:4516` → `_Model_Client_TableView::meta()`, but at different columns in different
93
+ databases — so the entry point tells you nothing; the column does.
94
+
95
+ 2. **Prove the code is ahead, not the DB behind** — in `_underscore`:
96
+
97
+ ```
98
+ git log --all -S"<columnName>" -- Model/ # which commit introduced the property
99
+ git branch -a --contains <sha> # WHICH environment branches carry it
100
+ ```
101
+
102
+ If the commit is on `_sandbox-client` but not `_production`, the environments are running
103
+ **different schema expectations** from the same repo. That is the finding.
104
+
105
+ 3. **Find out whether a migration exists at all** — in `dbchanges2`:
106
+
107
+ ```
108
+ git log --all -S"<columnName>" # any branch, ever
109
+ git ls-tree --name-only origin/_main <folder>/ # is it merged?
110
+ git merge-base --is-ancestor <sha> origin/_main
111
+ ```
112
+
113
+ Three possible answers, and they need different fixes: **merged** (an executor problem — use the
114
+ other two drift docs), **written but stranded on an unmerged branch** (merge it), or **never
115
+ written** (author it).
116
+
117
+ 4. **⚠ Do the full sweep BEFORE fixing anything.** This is the step that turns the incident from
118
+ whack-a-mole into one piece of work:
119
+
120
+ ```
121
+ git log origin/_production..origin/<env-branch> --oneline -- Model/
122
+ git show <sha> -- Model/ | grep -E "^\+\s*public \$"
123
+ ```
124
+
125
+ Enumerate **every** new column the framework branch expects, then cross-check each token against
126
+ `dbchanges2`. Also grep the same commits for hand-written `SELECT`s (the `meta()` variant above),
127
+ which the `public $` grep misses.
128
+
129
+ 5. **Merge the stranded branch — do not hand-patch column by column.** *(Decision, 2026-08-27.)*
130
+ The failures arrive **one at a time**: each next column only surfaces once the previous is fixed,
131
+ so column-by-column patching guarantees a repeat incident per column. And a column applied by
132
+ hand exists in **no migration file**, so that environment drifts permanently from every other one.
133
+ Merge the branch, then verify with [verifying a migration ran](./verifying-a-migration-ran.md).
134
+
135
+ 6. **If you must unblock by hand, write down exactly what you ran** (see the section below) and
136
+ treat the eventual migration as **idempotent by requirement**, not by preference — it will run
137
+ against at least one database that already has the column.
138
+
139
+ 7. **Remember `Client/` is a fan-out.** A Core column is one `ALTER`; a Client column is one `ALTER`
140
+ **per tenant database**. Getting this wrong fixes the tenant you are testing and leaves the rest
141
+ 500ing.
142
+
143
+ ## The 2026-08-26 `sandbox-client` incident (worked example)
144
+
145
+ Two successive 500s, both `V2.php:4516` → `_Model_Client_TableView::meta()`:
146
+
147
+ | # | Error | DB | Failing path | Mechanism |
148
+ |---|---|---|---|---|
149
+ | 1 | `Unknown column 'maxLength' in 'field list'` (`SELECT … FROM RecordFields WHERE id = 41`) | **Core** | `Model/Client/TableView.php:84` → `_Model::initialize()` → `Query.php:405` | model **property** (`_Model_Core_RecordField::$maxLength`) |
150
+ | 2 | `Unknown column 'TableViewFields.isGroupable' in 'field list'` | **Client** | `Model/Client/TableView.php:188` → `_Query::fetchRow()` → `Query.php:186` | **hand-written SELECT** in `meta()` |
151
+
152
+ The second only appeared **after** the first was fixed — the canonical reason step 4 exists.
153
+
154
+ ## Current drift inventory — `_underscore` `_sandbox-client` vs. schema (as of 2026-08-27)
155
+
156
+ Framework commits ahead of `_production` on `_sandbox-client`: `af90e987` (08-13), `a5e79448`
157
+ (08-14), `9f296a71` (08-26), `78ad65a5` (08-26), `64233162` (08-26 — logic only, no new columns).
158
+
159
+ **Category A — migration WRITTEN but stranded on `dbchanges2` branch `origin/toga25-desk`**, never
160
+ merged to `origin/_main`, so it has run in **no** environment fed by `_main`:
161
+
162
+ | Object | File (on `origin/toga25-desk`) |
163
+ |---|---|
164
+ | `Core.RecordFields.maxLength` + `validationType` | `Core/2026-08-14c - RecordFieldsValidationMetadata.sql` (commit `84a022c`, jcardinal, 2026-08-17) |
165
+ | `Client.RecordFieldSettings` (new table) | `Client/2026-08-14a - RecordFieldSettingsClientTable.sql` |
166
+ | `Client.UserNavPreferences` (new table) | `Client/2026-08-13a`, `b`, `d`, `e`, `g`, `h` (commit `9523c04`) |
167
+ | `Client.PersonaNavVisibility` (new table) | `Client/2026-08-13c`, `f` (same commit) |
168
+ | Core surface/ACL seeds | `Core/2026-08-14a` SettingsNavSurfaceSeed, `2026-08-14b` SettingsProfileAndAppearance, `2026-08-14d` ProfileSensitiveFieldAclRevoke, `2026-08-14e` SettingsProfileFieldElements, `2026-08-14f` LogoutConfirmSurfaceSeed |
169
+
170
+ `Core/2026-08-14c` adds `maxLength INT UNSIGNED NULL` + `validationType VARCHAR(32) NULL` and then
171
+ seeds **13 platform defaults by literal `RecordFields` id** (113, 115–118, 1324–1327, 1329, 1863).
172
+ `Client.RecordFieldSettings.recordFieldId` is a deliberate **soft (non-FK) cross-cluster reference**
173
+ to `Core.RecordFields` — correct per the [database-isolation rule](../architecture.md), not an
174
+ oversight.
175
+
176
+ **Category B — framework shipped with NO migration written ANYWHERE** (no `dbchanges2` branch, not in
177
+ `Client/2026-06-03- BLANK_CLIENT_DATABASE.sql`). Both landed **2026-08-26**, the day of the deploy:
178
+
179
+ - **`Client.TableViewFields.isGroupable`** — `_underscore` `9f296a71` "Add groupable table view
180
+ fields". Adds the model property **and** the column to `meta()`'s hand-written SELECT. Verified
181
+ absent from every `dbchanges2` branch (`git log --all -S"isGroupable"` returns nothing).
182
+ - **`Client.Urgencies.slug`** — `_underscore` `78ad65a5` "Add slug to urgency model". The blank
183
+ template's `Urgencies` has only `id`/`uuid`/`weight`/`name`. **Has not surfaced yet** because
184
+ Supply never constructs an Urgency — it is the Desk/Tickets path, so this one is a **pre-detonated
185
+ mine**, not a live outage.
186
+
187
+ **Verified NOT a problem** (already on `_main`): the `SurfaceElements` columns
188
+ `showRequiredIndicator`, `isSearchable`, `characterLimit`, `trueMessageId`, `falseMessageId`,
189
+ `inputType`, `validationType` — `Core/2026-07-17a - Migrate - AlterSurfaceElements.sql`.
190
+
191
+ > **Note on the stranded branch's file names:** `origin/toga25-desk` carries **three**
192
+ > `Core/2026-08-14a` files and **three** `Client/2026-08-14a` files. That violates the
193
+ > one-letter-per-day contract in [architecture.md](../architecture.md) and is the same shape as the
194
+ > unproven duplicate-letter executor lead in
195
+ > [non-prod metadata drift](./nonprod-metadata-drift-repair.md). Fix the letters as part of the merge.
196
+
197
+ ## Hand-applied to `sandbox-client` on 2026-08-27 (NOT in any migration file)
198
+
199
+ Recorded so the real migrations are written **idempotently** and nobody is surprised by a column that
200
+ exists here and in no `.sql` file.
201
+
202
+ **Core (single DB)** — lifted verbatim from the stranded `Core/2026-08-14c`; the 13 seed `UPDATE`s
203
+ were **not** run, because `NULL` correctly means "no platform default":
204
+
205
+ ```sql
206
+ ALTER TABLE RecordFields
207
+ ADD COLUMN maxLength INT UNSIGNED NULL COMMENT 'Platform-default maximum character length for the field',
208
+ ADD COLUMN validationType VARCHAR(32) NULL COMMENT 'Named front-end validator, e.g. email/phone';
209
+ ```
210
+
211
+ **Client_&lt;Tenant&gt; (FAN-OUT — every tenant DB)** — no migration file exists for this yet; the
212
+ type/nullability/position were chosen to match its siblings `isSortable`/`isFilterable`/
213
+ `isCopyable`/`isEditable` in the blank template:
214
+
215
+ ```sql
216
+ ALTER TABLE TableViewFields
217
+ ADD COLUMN isGroupable TINYINT UNSIGNED NULL COMMENT 'Whether or not the field is groupable' AFTER isFilterable;
218
+ ```
219
+
220
+ **Still outstanding in `sandbox-client` at session end:** the 13 Core seed `UPDATE`s,
221
+ `Client.RecordFieldSettings`, `Client.Urgencies.slug`, `Client.PersonaNavVisibility`,
222
+ `Client.UserNavPreferences`, and the five Core `2026-08-14` surface/ACL seeds.
223
+
224
+ ## Gotchas / known issues
225
+
226
+ - **The failures are serialized — one fix reveals the next.** Never treat "the 500 is gone" as
227
+ "the environment is healthy" until the step-4 sweep comes back empty.
228
+ - **`git grep` for the property under-counts.** A column can be required by a hand-written `SELECT`
229
+ inside a model with no property declared at all (`TableView::meta()`), and by both at once.
230
+ - **A column that has not failed yet is not safe** — it is waiting for the first request that
231
+ constructs that model. `Urgencies.slug` is live in `sandbox-client` right now and simply
232
+ unreachable from Supply.
233
+ - **Fixing a wrong branch pin is a schema change in disguise.** Pair the branch correction with the
234
+ migration sweep; do not ship it as a lone "deploy fix".
235
+ - **A migration on an unmerged branch has run nowhere** — including production. "It's written" and
236
+ "it's applied" are different claims, and so are "it's on a branch" and "it's on `_main`".
237
+ - **Core is one `ALTER`; `Client` is one per tenant.** Fixing only the tenant in front of you leaves
238
+ every other tenant 500ing on the same request.
239
+
240
+ ## Change history
241
+
242
+ - 2026-08-27 — Created from the `sandbox-client` outage that followed the 2026-08-26 production
243
+ `_underscore`/`api2`/`_production`-DB deploy. Recorded the **third drift direction** (the code
244
+ moved forward, not the DB backward): a deploy moved the environment off the `_production`
245
+ framework onto `_sandbox-client`, which carried ~2 weeks of model changes whose migrations were
246
+ never merged, producing environment-wide MySQL 1054 → 500/`EO-1`. Documented the two mechanisms
247
+ (a `public $x = self::FIELD_*` property, which `_Model::initialize()` puts in every SELECT; and a
248
+ hand-written `SELECT` inside a model such as `TableView::meta()`), the reusable triage
249
+ (`git log --all -S"<column>"` + `git branch -a --contains` in `_underscore`, the same in
250
+ `dbchanges2`, then the `git log origin/_production..origin/<env-branch> -- Model/` sweep), and the
251
+ **decision to merge the stranded branch wholesale rather than hand-patch column by column** —
252
+ because failures serialize and hand-applied columns drift permanently. Captured the full
253
+ 2026-08-27 inventory (Category A stranded on `origin/toga25-desk`: `RecordFields.maxLength`/
254
+ `validationType`, `RecordFieldSettings`, `UserNavPreferences`, `PersonaNavVisibility`, five Core
255
+ surface/ACL seeds; Category B never written anywhere: `TableViewFields.isGroupable`,
256
+ `Urgencies.slug`) and the two `ALTER`s hand-applied to `sandbox-client`. (apeterson)
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 69 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
- - **dbchanges2** (Database Changes) _(framework core)_ — 12 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
+ - **dbchanges2** (Database Changes) _(framework core)_ — 13 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
27
27
  - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.668",
3
+ "version": "1.0.669",
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",