toga-ai 1.0.912 → 1.0.914

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.
@@ -44,6 +44,7 @@
44
44
  | [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 |
45
45
  | [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 |
46
46
  | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona names in multiple languages via a `PersonaTranslations` sidecar (copy of the assortment-translation pattern); open when adding/running persona tr |
47
+ | [Persona nav-visibility gate + user nav pins (keyed by Core.SurfaceElements uuid)](features/persona-nav-visibility-gate.md) | Sidebar-nav persona gate in `_Model_Core_Surface` (`Client.PersonaNavVisibility`) and user nav pins (`Client.UserNavPreferences`), both keyed by Core `SurfaceEl |
47
48
  | [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Field-level change audit trail per tenant; open to answer "who changed this field, to what, when" or to tell a direct SQL edit from an in-app change. |
48
49
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | Engine that mirrors a downstream ItemFulfillment up the SO→PO→SO chain onto every customer-facing SO; open when touching IF creation, bundle scaling, or upstrea |
49
50
  | [Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode](features/sales-order-denial-reason.md) | Map of the SO approval denial reason (`ApprovalDecisions.note`) — where it is written, read, and displayed; open when building any denial-reason display, banner |
@@ -53,6 +54,7 @@
53
54
  | [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | Per-client `ShippingCarriers` / `ShippingCarrierAccountNumbers` model, how the syncs match/create carrier rows, and the duplication + blank-carrier + pagination |
54
55
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` static string utilities: use `encodeToAsciiHtmlEntities` for any dynamic text going into a 2.0 email; open when escaping email text or parsing substri |
55
56
  | [Surface FIELD ↔ RecordField binding (maxLength / validationType / isWritable fold-in)](features/surface-field-recordfield-binding.md) | Resolver capability that folds `Core.RecordFields` input-contract metadata (maxLength/validationType/isWritable) onto a surface FIELD element; open when a 2.5 s |
57
+ | [DEFERRED — re-key Client.SurfaceOverrides from numeric Core ids to uuids](features/surface-overrides-uuid-rekey.md) | Deferred: `Client.SurfaceOverrides.surfaceId`/`surfaceElementId` still point at Core rows by numeric id; open before a client's first override rows on an env wh |
56
58
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | Runtime for the platform Surface UI layer (`_Model_Core_Surface::resolve`/`metaGroup`, replaces `Page::meta`); open for surface resolution, the override cascade |
57
59
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | How a 2.0 table-view column renders its value as a clickable link (`hyperlinkRecordFieldId` → ACL field-read → computed URL → render); open when a configured hy |
58
60
  | [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | How `TableViewJoins` rows become a view's join SQL — aliasing, chained multi-hop joins, ACL gating, and the three ways a view breaks; open before adding/rebasin |
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: "Persona nav-visibility gate + user nav pins (keyed by Core.SurfaceElements uuid)"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Core/Surface.php
13
+ - _underscore/Model/Client/PersonaNavVisibility.php
14
+ - _underscore/Model/Client/UserNavPreference.php
15
+ - dbchanges2/Client/2026-09-29a - NavItemSurfaceElementUuidColumns.sql
16
+ - dbchanges2/Client/2026-09-29b - DropNavItemSurfaceElementIdColumns.sql
17
+ - dbchanges2/Core/2026-09-29a - UserNavPreferencesNavItemUuidRecordField.sql
18
+ related:
19
+ - surface-resolver.md
20
+ - surface-overrides-uuid-rekey.md
21
+ - ../../dbchanges2/features/surface-layer-schema.md
22
+ ---
23
+
24
+ Sidebar-nav persona gate in `_Model_Core_Surface` (`Client.PersonaNavVisibility`) and user nav pins (`Client.UserNavPreferences`), both keyed by Core `SurfaceElements.uuid`; open before touching nav visibility, pins, or any Client row that points at a Core Surface element.
25
+
26
+ ## Summary
27
+
28
+ - Gate runs only when the surface `config.renderAs === 'nav'`. Default-deny: a nav element survives only if one of the caller's personas has a `PersonaNavVisibility` grant for it in the request app.
29
+ - Grants and pins store `navItemSurfaceElementUuid` (CHAR(36), lowercase) = `Core.SurfaceElements.uuid`. The numeric `navItemSurfaceElementId` column was dropped 2026-09-29.
30
+ - Rule: never key a Client row to a Core Surface/SurfaceElement by numeric id. Core ids shift when Core is rebuilt or copied to another env; seeded uuids are the same everywhere.
31
+
32
+ ## How it works
33
+
34
+ 1. `_buildBundle` step 5b: `$isNavGated` = `config.renderAs === 'nav'`. False → nothing runs, no element dropped (user menu uses `renderAs='menu'`). Do not widen this check.
35
+ 2. App id = `$api->app->id`, else `Surfaces.appId`.
36
+ 3. `_grantedNavItemSurfaceElementUuids($personaIds, $appId)`: one query on the client DB (`personaId IN (...) AND appId = ... AND navItemSurfaceElementUuid IS NOT NULL`) → set keyed by lowercase uuid. No personas or no app → empty set → all nav elements hidden.
37
+ 4. Per element: drop if `strtolower($element->uuid)` is not in the set.
38
+ 5. Cluster-safe: grant uuids are read on the client connection and matched in PHP against the Core elements. No cross-database SQL.
39
+
40
+ **Pins (`Client.UserNavPreferences`, Core.Records 349):** RecordField 2645 = `navItemSurfaceElementUuid` (2487 = old id field, retired). `prePost` interceptor stamps `userId` from the JWT and `appId` (`$api->app->id`, fallback 34), checks uuid format, lowercases it.
41
+
42
+ ## Gotchas
43
+
44
+ - Model field is `FIELD_CHAR`, not `FIELD_CHAR_UUID`. `FIELD_CHAR_UUID` auto-generates a value, which would overwrite the FK.
45
+ - Symptom of id drift: sidebar-nav returns only the theme (all elements dropped). Seen on qa-alpha 2026-09-29: Core rebuilt from prod, grants pointed at ids 114-134, local elements were 241-261 (+127).
46
+ - `Client/2026-09-29b` deletes old id-only grants only for a persona+app that already has uuid grants, then sets the uuid NOT NULL. A client never re-keyed fails loud (`Invalid use of NULL`) instead of losing its whole sidebar. Fix: write that client's uuid grant seed (pattern: `Client_True/2026-09-29a`), then re-run.
47
+ - Run order: `Client/2026-09-29a` + client grant seeds → code live → `Client/2026-09-29b` → `Core/2026-09-29b` (removes RecordField 2487).
48
+ - `Client.SurfaceOverrides` still uses numeric Core ids — same risk, deferred: [surface-overrides-uuid-rekey](surface-overrides-uuid-rekey.md).
49
+
50
+ ## Change history
51
+ - 2026-09-29 — Grants + pins re-keyed from numeric SurfaceElements id to uuid; `_grantedNavItemSurfaceElementIds` renamed `_grantedNavItemSurfaceElementUuids` (_underscore `8e299b1b`, dbchanges2 `66ba056`) (jcardinal)
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: "DEFERRED — re-key Client.SurfaceOverrides from numeric Core ids to uuids"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: deferred
9
+ updated: 2026-09-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Core/Surface.php
13
+ - _underscore/Model/Client/SurfaceOverride.php
14
+ related:
15
+ - persona-nav-visibility-gate.md
16
+ - surface-resolver.md
17
+ ---
18
+
19
+ Deferred: `Client.SurfaceOverrides.surfaceId`/`surfaceElementId` still point at Core rows by numeric id; open before a client's first override rows on an env whose Core can be rebuilt.
20
+
21
+ ## Summary
22
+
23
+ Not built. `_Model_Core_Surface::_loadOverrides` matches `SurfaceOverrides.surfaceId` / `surfaceElementId` to Core numeric ids. Same failure as the nav gate before 2026-09-29 ([persona-nav-visibility-gate](persona-nav-visibility-gate.md)): once a client has override rows and Core is rebuilt/copied with different AUTO_INCREMENT ids, overrides silently miss or land on the wrong element.
24
+
25
+ ## How it works
26
+
27
+ Planned fix (do before first client use on such an env):
28
+ 1. Add `surfaceUuid` / `surfaceElementUuid` CHAR(36) to `Client.SurfaceOverrides` (`FIELD_CHAR`, not `FIELD_CHAR_UUID`).
29
+ 2. `_loadOverrides`: match by uuid on the client connection, join to Core elements in PHP (no cross-database SQL).
30
+ 3. Forward-only re-key seeds per client, then drop the id columns with a fail-loud NOT NULL step (same shape as `Client/2026-09-29b`).
31
+
32
+ ## Gotchas
33
+
34
+ - Elements seeded with reserved id literals are stable across envs; the risk is elements that took AUTO_INCREMENT ids (e.g. the `navigation` surface's elements). The framework-rules "Bounded exemption — the Surface tables" stays as-is until this is built.
35
+
36
+ ## Change history
37
+ - 2026-09-29 — Recorded as deferred after the nav-gate uuid fix exposed the same numeric-id risk (jcardinal)
@@ -36,6 +36,8 @@ related:
36
36
  - sales-order-status-filter-surface.md
37
37
  - surface-field-recordfield-binding.md
38
38
  - ../../toga25-supply/features/side-navigation-and-default-route.md
39
+ - persona-nav-visibility-gate.md
40
+ - surface-overrides-uuid-rekey.md
39
41
  ---
40
42
 
41
43
  Runtime for the platform Surface UI layer (`_Model_Core_Surface::resolve`/`metaGroup`, replaces `Page::meta`); open for surface resolution, the override cascade, vocab/theme/message resolution, client seeding, caching, and 403/empty-bundle debugging.
@@ -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-29
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - api2/health
@@ -32,6 +32,7 @@ The EB/ALB liveness probe (`/health`) is served two ways, and the **load-bearing
32
32
  - **`api2/health`** — a one-line static file (`OK`). Apache serves it directly.
33
33
  - **`api2/.platform/httpd/conf.d/health_probe.conf`** — comment-only documentation of why the probe is static (no directives; explains the decision at the Apache layer).
34
34
  - **Why it works without a rewrite fight:** `api2/.htaccess` only rewrites to `index.php` when the request target is **not a real file** (`RewriteCond %{REQUEST_FILENAME} !-f`). Because `health` is a real file, the rewrite is skipped and Apache serves it straight.
35
+ - **`api2/.ebextensions/health-check.config`** (2026-09-29) — pins the EB default process health check: `aws:elasticbeanstalk:environment:process:default` → `HealthCheckPath: /health`, `MatcherHTTPCode: 200`. Without it EB checks `/`, which returns 405 → env Red/Severe, `Target.ResponseCodeMismatch`. Applies on the next deploy of each env.
35
36
  - **Why `enforce_https` doesn't redirect it:** EB health checks arrive on **HTTP:80 with no `X-Forwarded-Proto`**, and `enforce_https.conf` only redirects requests carrying the forwarded-proto marker — so the probe is not bounced to HTTPS.
36
37
 
37
38
  **PHP short-circuit (for `/v2/health` and direct hits).** At the top of `Index.php::api()`, before host dispatch and before the Core/Logs DB bootstrap. Matches against a class constant:
@@ -56,7 +57,14 @@ So **all** these probe variants return 200 and never touch routing or the DB: `/
56
57
  - **Do not** move `/health` back into a PHP-served route or rename/remove `api2/health` — that re-couples EB liveness to PHP-FPM worker availability, the exact failure this fixed (see [api2 architecture — EB "Degraded" health](../architecture.md)).
57
58
  - Health probes must degrade to a 200 short-circuit, not enter routing. Add new probe paths to `HEALTH_CHECK_ROUTES` in normalized form, never as a new exact-string branch.
58
59
 
60
+ - Env Red/Severe with `Target.ResponseCodeMismatch` → check the target group health-check path is `/health`, not `/`. The `.ebextensions` file sets it; an env not yet redeployed still has the old path.
61
+ - Git Bash path-converts AWS CLI args (`/health` → `C:/Program Files/Git/health`). Prefix the command with `MSYS_NO_PATHCONV=1`.
62
+
59
63
  ## Related
60
64
 
61
65
  - [api2 architecture — EB "Degraded" health](../architecture.md)
62
66
  - [V2 API error codes](v2-api-error-codes.md)
67
+
68
+ ## Change history
69
+ - 2026-09-29 — Added `.ebextensions/health-check.config` (HealthCheckPath `/health`, matcher 200); api-qa-alpha was Red on `/` 405 (api2 `885d114`) (jcardinal)
70
+ - 2026-08-28 — `/health` moved to a static Apache file, off PHP-FPM (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-17
9
+ updated: 2026-09-29
10
10
  owners: ["bala", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -17,6 +17,7 @@ related:
17
17
  - ../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md
18
18
  - ../../../clients/elite/features/supply2-tableview-config-drift.md
19
19
  - tableview-field-metadata.md
20
+ - ../../_underscore/features/calculated-sql-fields.md
20
21
  ---
21
22
 
22
23
  ## Summary
@@ -167,8 +168,40 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
167
168
  - **Operator allowlist.** `parseOptionsWhere` whitelists the operators above and rejects unknown
168
169
  ones (e.g. `regexp` returns a 500) — see the Compass cost-centers doc. Numeric/regex filtering
169
170
  that the grammar can't express must be done client-side or by adding an operator to api2.
171
+ - **⚠ The FIELD name is never validated — an unknown one is a bare 500 with an EMPTY body.**
172
+ `parseOptionsWhere` maps only the operator token; the field slot passes straight into the SQL.
173
+ A name no model declares reaches MySQL as `Unknown column '<x>' in 'where clause'`, nothing
174
+ catches it, and the request dies as a 500 with **no message at all** (the 1.0 monitor then reports
175
+ its fallback literal *"No message found in response payload"* — see
176
+ [v2-api-error-codes](v2-api-error-codes.md) note 13). Live case: a `_isServiceClass` condition
177
+ stored on Elite's `inventory_items` clause 500'd `GET /v2/items` **91 times in 5 minutes across
178
+ 7 prod api2 instances** (2026-09-17). **Open gap:** api2 has no guard that rejects an unknown
179
+ `_`-prefixed field with a 400 before it reaches MySQL.
180
+ - **⚠ Deploy order: the `_underscore` model field ships BEFORE the migration that references it.**
181
+ An `_`-prefixed name in a clause only resolves if the model declares it `FIELD_SQL` with a
182
+ matching same-name static (e.g. `_qtyOnHand` on `_Model_Client_Item`) — see
183
+ [calculated SQL fields](../../_underscore/features/calculated-sql-fields.md). A clause is **data**,
184
+ so it goes live the instant the `dbchanges2` file runs, with no deploy gate; applying it first
185
+ takes the listing endpoint down for that tenant. Check the client's own model too — a
186
+ `_Model_<Client>_Item` inherits from `_Model_Client_Item` and may declare neither.
187
+ - **An unexplained field in a `where=` lives in the DB, not the frontend.** Filters reaching api2
188
+ are stored in `Client_<Tenant>.TableViews.apiWhereClause`, not written in app code. When a field
189
+ name appears in **no** repo and **no** git history, query `TableViews` across the client DBs —
190
+ that is where it is.
170
191
 
171
192
  ## Change history
193
+ - 2026-09-29 — **Recorded that an unvalidated field name in a stored clause is a production
194
+ outage class, not a typo.** `parseOptionsWhere` maps only the operator and passes the field slot
195
+ straight to MySQL, so an undeclared `_`-prefixed name becomes `Unknown column` → an uncaught
196
+ 500 with an **empty body**. Live case: a draft `_isServiceClass` condition on Elite's
197
+ `inventory_items` view 500'd `GET /v2/items` 91 times in 5 minutes across 7 prod api2 instances
198
+ (2026-09-17); `_isServiceClass` was declared on no model and existed in no repo or git history.
199
+ Added the deploy-ordering rule (the `_underscore` `FIELD_SQL` field must be deployed **before**
200
+ the `dbchanges2` file that references it, because a clause is data and needs no deploy), the
201
+ debugging path (an unexplained `where=` field lives in `TableViews.apiWhereClause`, not the
202
+ frontend repo), and the open gap that api2 rejects unknown operators but not unknown fields.
203
+ Diagnosis only, no code changed. Prod re-verified 2026-09-29: zero `TableViews` rows across all
204
+ 37 `Client_*` databases reference the field. (bala)
172
205
 
173
206
  - 2026-09-17 — **Recorded that subqueries ARE possible in a stored `apiWhereClause`**: the parser
174
207
  splits on literal spaces, so writing `/**/` in place of every space (MySQL reads it as
@@ -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)_ — 90 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
+ - **_underscore** (_Underscore) _(framework core)_ — 92 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
23
23
  - **worker2** (Worker) — 80 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)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-17
9
+ updated: 2026-09-29
10
10
  owners: [tcox, bala, rgirish]
11
11
  files:
12
12
  - dbchanges2/Client_Elite/
@@ -297,6 +297,17 @@ for the technique and the parser mechanics.
297
297
  required a `_isServiceClass` model field on `_Model_Elite_Item`; **that field does not exist and is
298
298
  not needed** — do not add one.
299
299
 
300
+ **⚠ That earlier draft reached PRODUCTION and took `/v2/items` down.** Before the rewrite, the
301
+ stored clause was `(Items.partNumber:excludes:SVC-,_isServiceClass:eq:0)`. `_isServiceClass` is
302
+ declared on neither `_Model_Client_Item` nor `_Model_Elite_Item`, so the raw name went into the SQL
303
+ and MySQL threw `Unknown column '_isServiceClass' in 'where clause'` — **91 empty-body 500s on
304
+ `GET /v2/items` in 5 minutes across 7 prod api2 instances (2026-09-17)**. A stored clause needs no
305
+ deploy, so it went live the moment the migration ran. Closed: `2026-09-15a` reverted the clause and
306
+ the current one uses only `_qtyOnHand` / `_qtyAvailable`, which **are** declared `FIELD_SQL`.
307
+ Re-verified 2026-09-29 — zero `TableViews` rows in any of the 37 prod `Client_*` databases mention
308
+ `ServiceClass`. The general rule and the debugging path are in
309
+ [apiWhereClause row filtering](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md#gotchas--known-issues).
310
+
300
311
  ### Why the class check replaced the `SVC-` prefix
301
312
 
302
313
  The `SVC-` prefix was an unreliable signal. Verified on **prod `Client_Elite`** (2026-09-15):
@@ -388,6 +399,15 @@ the old `SVC-` prefix check back into the clause.
388
399
  [error-reporting-issue-event](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md).
389
400
 
390
401
  ## Change history
402
+ - 2026-09-29 — **Recorded that the abandoned `_isServiceClass` draft clause actually shipped to
403
+ production and caused an outage**, which this doc previously reported only as "does not exist and
404
+ is not needed". The stored clause `(Items.partNumber:excludes:SVC-,_isServiceClass:eq:0)` put an
405
+ undeclared field into the SQL, giving `Unknown column '_isServiceClass' in 'where clause'` and 91
406
+ empty-body 500s on `GET /v2/items` in 5 minutes across 7 prod api2 instances (2026-09-17).
407
+ Incident is closed — `2026-09-15a` reverted the clause, the live one uses only the declared
408
+ `FIELD_SQL` fields `_qtyOnHand` / `_qtyAvailable`, and a 2026-09-29 sweep of all 37 prod
409
+ `Client_*` databases found no `TableViews` row mentioning `ServiceClass`. Diagnosis only, no code
410
+ changed. (bala)
391
411
  - 2026-09-17 — **Replaced the `SVC-` partNumber prefix filter with an ItemClass-hierarchy check**
392
412
  on the `inventory_items` view (`2026-09-15a - InventoryHideServiceClassItems.sql`): the clause
393
413
  now walks the `ItemClasses` parent chain and excludes items whose ROOT class id is 1, 22 or 40,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.912",
3
+ "version": "1.0.914",
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",