toga-ai 1.0.1076 → 1.0.1077

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.
@@ -35,6 +35,7 @@
35
35
  | [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` columns are hydrated lazily per returned row, so a missing column 500s only when a query returns rows; open when auditing storage-field schema d |
36
36
  | [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time webhook importer that writes NetSuite sale records (invoice/cashSale/creditMemo/cashRefund/journalEntry) into `Forecast.Sales`; open before touching s |
37
37
  | [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | api2 interceptor that propagates `Items.isFulfillable` UP the SO↔PO chain onto client-facing item rows; open when the storefront Qty-Fulfilled gate is wrong or |
38
+ | [Global search (GET /records/search — tiered, per-client, data-configured fields)](features/global-search.md) | Platform (all 2.0 apps) header search across record types; open when making a field searchable, tuning ranking, or a search is slow or misses results. |
38
39
  | [ItemClasses / _Model_Client_ItemClass — who actually uses it (and why it is not free)](features/item-classification-itemclasses.md) | `ItemClasses` is a near-unused per-client lookup that 2 of 36 clients already repurpose (Northwell service categories, Prudential device tiers); open before reu |
39
40
  | [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Item-fulfillment stage lifecycle (picked→packed→shipped) driving the calculated order `_status`, with per-client status policies; open when working order status |
40
41
  | [DB-free unit testing for _underscore model interceptors](features/model-interceptor-unit-testing.md) | DB-free unit-test scaffold for `_underscore` model interceptor/validation logic (stub the persistence base, reflect into privates); open when testing a `prePost |
@@ -44,6 +45,7 @@
44
45
  | [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 |
45
46
  | [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 |
46
47
  | [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 |
48
+ | [Notification engine (rule-driven in-app notifications from record change logs)](features/notification-engine.md) | Platform (all 2.0 apps) in-app notifications: data rules on Core turn a record's change log into Client.Notifications rows; open when adding a notification kind |
47
49
  | ["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. |
48
50
  | [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. |
49
51
  | [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 |
@@ -10,6 +10,7 @@ updated: 2026-10-18
10
10
  owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
+ - _underscore/Component/Acl/ReadExpression/ReadExpression.php
13
14
  - dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql
14
15
  - _underscore/Model/Core/Page.php
15
16
  - _underscore/Model/Core/Surface.php
@@ -578,7 +579,9 @@ INSERT INTO AclLogicGroupExpressions SET
578
579
  `_Component_Api_V2::buildLookups()` bulk-loads these tables (from the Core and/or Client ACL
579
580
  database per `Records.aclDatabase`) into in-memory lookups once per request; `processRoutePairs()`
580
581
  then checks the caller's roles against `AclRecordPermissions` (→ `EZ-1` if no grant) and computes
581
- readable/writable fields from `AclFieldPermissions`. `_Model_Core_Page::meta()` reads the same
582
+ readable/writable fields from `AclFieldPermissions`. The row-level READ expression is built by
583
+ `_Component_Acl_ReadExpression` (`_underscore`, moved out of V2 2026-10-18) — shared by V2 and the
584
+ [notification engine](./notification-engine.md); change it in one place. `_Model_Core_Page::meta()` reads the same
582
585
  tables to return per-page ACL to the frontend.
583
586
 
584
587
  > **⚠ There is NO ACL/metadata caching layer — the load is per-request and there is nothing to bust.**
@@ -937,6 +940,7 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
937
940
  and every repo is on the **same branch** so the generated model matches the DB.
938
941
 
939
942
  ## Change history
943
+ - 2026-10-18 — Row-level READ expression moved to shared `_Component_Acl_ReadExpression` (V2 + notification engine). TRUE-82907 (jcardinal)
940
944
  - 2026-10-18 — Client ACL rows now carry `capabilityId`; linked the new Capabilities doc (TRUE-82880). (jcardinal)
941
945
  - 2026-09-21 — Named the mechanism behind the half-built chain: `_Model_Core_AclRecordPermission::buildSqlExpression`
942
946
  returns **`'(0)'`** with no logic group, so `allowRead = 1` alone denies everything (second symptom: 403 listing
@@ -58,7 +58,8 @@ Removed: `FIELDOPT_AUTOINCREMENT_PREFIX` / `_FIRSTNUMBER` / `_SUFFIX`, the `MAX(
58
58
 
59
59
  ## Gotchas
60
60
 
61
- - **New numbered model needs a `RecordNumbers` row in every client DB** (or Core for `DB_CORE` models), else creates throw. Scopes: no `Core.Records` row yet; `Model/Client/Scope.php` exists only on Desk branch TRUE-82615 — on merge make it plain `FIELD_AUTOINCREMENT` + seed a row.
61
+ - **New numbered model needs a `RecordNumbers` row in every client DB** (or Core for `DB_CORE` models), else creates throw. Example: Scopes (record 356) = prefix `SC`, `nextNumber` 100020 (`Client/2026-10-18e`); `Model/Client/Scope.php` and `Model/Core/Client.php` are plain `FIELD_AUTOINCREMENT`.
62
+ - **Prefix = letters only.** `SC-` is invalid (no hyphen) — the old `SC-100001` style is gone.
62
63
  - **New client DB** must get `RecordNumbers` rows from the `Client/` blank seed.
63
64
  - **Gaps only come from failed saves.** A gap = a failed create → check `Logs.Api` / `Logs.Issue`.
64
65
  - **SA / TR numbers are placeholders** — NetSuite `tranId` overwrites them on write-back (accepted). Never key a match on `number`; pushes key `externalId` on uuid.
@@ -79,6 +80,7 @@ Removed: `FIELDOPT_AUTOINCREMENT_PREFIX` / `_FIRSTNUMBER` / `_SUFFIX`, the `MAX(
79
80
  - [NetSuite SalesOrder outbound push](../../worker2/features/netsuite-salesorder-outbound-push.md)
80
81
 
81
82
  ## Change history
83
+ - 2026-10-18 — `_production` merged into `toga25`; Scope + Core Client models on plain `FIELD_AUTOINCREMENT`; Scopes seeded `SC` 100020. TRUE-82907 (jcardinal)
82
84
  - 2026-10-18 — Final design: per-client `RecordNumbers` table (client DBs + Core), counting in order, old series continued under new prefixes, Compass Canada SAC; dropped the platform-wide scrambled `Core.Records` counter. TRUE-82715. (jcardinal)
83
85
  - 2026-10-13 — Platform-wide scrambled counter in `Core.Records` (dropped before release). (jcardinal)
84
86
  - 2026-10-02 — Per-tenant Parameters counter on `_beta` (superseded, PR 756 not merged). (rgirish)
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: Global search (GET /records/search — tiered, per-client, data-configured fields)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-18
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - _underscore/Component/GlobalSearch/GlobalSearch.php
13
+ - _underscore/Model/Core/Record.php
14
+ - _underscore/Model/Core/SearchField.php
15
+ - _underscore/Model/Cache/SearchRequest.php
16
+ - _underscore/Component/RecordActivity/Reader/Reader.php
17
+ related:
18
+ - ../../api2/features/record-scripts.md
19
+ - ./autoincrement-record-numbering.md
20
+ - ./notification-engine.md
21
+ - ../../toga25-blox/features/shell-search-and-notifications.md
22
+ ---
23
+ Platform (all 2.0 apps) header search across record types; open when making a field searchable, tuning ranking, or a search is slow or misses results.
24
+
25
+ ## Summary
26
+
27
+ `GET /records/search` is a Record Script on record 6 (`Records`), `phpMethod` `globalSearch`. Searchable fields are rows in `Core.SearchFields` — no code per record type. The browser calls each client it can reach, with that client's own token. Built TRUE-82907.
28
+
29
+ ## How it works
30
+
31
+ **Tiers** (one call = one tier):
32
+ 1. tier 1 = EXACT on tier-1 fields,
33
+ 2. tier 2 = STARTS WITH on fields with tier <= 2,
34
+ 3. tier 3 = CONTAINS on fields with tier <= 3.
35
+
36
+ A field is skipped when its `maxTier` < the requested tier.
37
+
38
+ **`Core.SearchFields`:** `recordFieldId`, `tier` (TINYINT UNSIGNED), `maxTier`, `minChars`, `parentRecordFieldId`, `sortOrder`.
39
+
40
+ **Number hint.** A term matching `^[A-Z]{1,8}\d+$` whose prefix exists in that client's `RecordNumbers` searches only that record type's number field ([record numbering](./autoincrement-record-numbering.md)).
41
+
42
+ **Reads** go through api2 `Reader::listAsReader` — the caller's record + field ACL apply. Not `internalApiRequest` (that writes a `Logs.Api` row and commits per call).
43
+
44
+ **Cancel stale searches.** `Cache.SearchRequests` keeps one row per client + user + search box with the latest sequence number. The search stops between record types when a newer sequence number exists.
45
+
46
+ **Rank:** exact match, all words, whole word, fewer results of that type, newest.
47
+
48
+ ## Gotchas
49
+
50
+ - **A field may be tier 1 or 2 only if it has its own index (as the first column).** Otherwise exact/starts-with scans the table. Indexes added: `Customers.name`, `Locations.name`, `Units.serialNumber`, `Users.firstName`, `Users.lastName` (`Client/2026-10-18g`).
51
+ - `phpMethod` is `globalSearch`, not `search` — see [record scripts](../../api2/features/record-scripts.md) gotcha on `_Model` method names.
52
+
53
+ ## Change history
54
+ - 2026-10-18 — Built tiered global search, `Core.SearchFields`, `Cache.SearchRequests`, search indexes. TRUE-82907 (jcardinal)
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Notification engine (rule-driven in-app notifications from record change logs)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-18
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - _underscore/Notification.php
13
+ - _underscore/Component/Notification/Dispatcher/Dispatcher.php
14
+ - _underscore/Component/Acl/ReadExpression/ReadExpression.php
15
+ - _underscore/Component/SettingResolver/SettingResolver.php
16
+ - _underscore/Model/Core/NotificationType.php
17
+ - _underscore/Model/Core/NotificationRule.php
18
+ - _underscore/Model/Client/Notification.php
19
+ - api2/Component/Api/V2/V2.php
20
+ - worker2/Worker/Notification/Dispatch.php
21
+ related:
22
+ - ./record-change-audit-log.md
23
+ - ./acl-permission-chain.md
24
+ - ./global-search.md
25
+ - ../../api2/features/language-translation-layer.md
26
+ - ../../toga25-blox/features/shell-search-and-notifications.md
27
+ - ../../worker2/features/creating-worker-actions.md
28
+ ---
29
+ Platform (all 2.0 apps) in-app notifications: data rules on Core turn a record's change log into Client.Notifications rows; open when adding a notification kind/rule, a non-V2 writer must notify, or a user did/didn't get a notification.
30
+
31
+ ## Summary
32
+
33
+ Setup is data, not code: one `Core.NotificationTypes` row (what it looks like) + one `Core.NotificationRules` row (when it fires, who gets it). Any record written through api2 V2 then notifies automatically. Other writers call `_Notification::dispatchForLogRecords($clientId, $logRecordIds)` after their own commit. Built TRUE-82907.
34
+
35
+ ## How it works
36
+
37
+ 1. **Trigger (api2).** After a successful V2 POST/PUT/PATCH, `flushPendingRecordChangeLogs()` commits the change logs ([audit log](./record-change-audit-log.md)). After that commit it keeps only `Logs.Record` ids whose `recordId` has an active `NotificationRules` row (`_Notification::selectLogRecordIdsWithActiveRules`) and sends ONE `_Worker::runTask('Notification/Dispatch/Run', ['clientId'=>…, 'logRecordIds'=>[…]])` per request, chunks of 500. A dispatch failure is logged and never fails the user's request.
38
+ 2. **Worker.** worker2 `_Worker_Notification_Dispatch::Run` → `_Component_Notification_Dispatcher`. Reads `Logs_<client>.Record` + `RecordField` (old/new values).
39
+ 3. **Match rules:** `action` CREATE / UPDATE; optional `changedRecordFieldId` (fires only when that field changed).
40
+ 4. **Recipients:**
41
+ - `FIELD` — a person field on the record. A `Contacts` FK is resolved to `Users.contactId`.
42
+ - `RELATED` — a user field on a related table via `viaRecordFieldId` (e.g. `TicketWatchers`).
43
+ - Optional `parentRecordFieldId` — child record (e.g. `TicketNotes`) notifies about and links to the parent (the ticket).
44
+ 5. **Filter recipients:** drop the actor; drop users without READ on the record — row-level ACL through `_Component_Acl_ReadExpression` (the same builder V2 uses; see [ACL chain](./acl-permission-chain.md)); drop users whose setting is off.
45
+ 6. **User setting.** `NotificationTypes.settingId` NULL = always on. Otherwise resolved through the `*Settings` cascade by `_Component_SettingResolver` (same resolver as language/currency). Values `'0'`, `'false'`, `'off'`, `'no'` = off; unset = on.
46
+ 7. **Insert** `Client.Notifications`: `notificationTypeSlug`, `messageParams` JSON snapshot (number / title / subtitle), `actorUserId`, `logRecordId`, `primaryKeyUuid`, `dtCreated`, `dtUpdated`, `dtCleared`. `UNIQUE(logRecordId, userId, notificationTypeSlug)` blocks duplicates from SQS re-delivery.
47
+
48
+ **Data model (dbchanges2 `Core/2026-10-18h..l`, `Client/2026-10-18e..g`):**
49
+ - `Core.NotificationTypes` — `slug`, `iconToken`, `colorToken`, title/body `Core.Messages` keys on the app-less Surface `notifications`, `settingId`.
50
+ - `Core.NotificationRules` — record, action, optional changed field, recipient kind + field, `viaRecordFieldId`, `parentRecordFieldId`, active flag.
51
+ - `Client.Notifications` references the type by **slug** — `NotificationTypes` ids are auto-increment and differ per environment.
52
+ - Clear = set `dtCleared` (archive). Never delete.
53
+ - ACL is owner-only: `Notifications.userId = AclUser.id` + `AclApi.id IS NULL` (same pattern as `TicketWatchers`).
54
+ - `_Model_Client_Notification` has calc field `_isMine`.
55
+
56
+ ## Gotchas
57
+
58
+ - **Only V2 writes notify automatically.** A worker, cron, or model writing records outside V2 must call `_Notification::dispatchForLogRecords()` itself, after its own commit — or nobody is notified.
59
+ - **Fail closed on per-app-only READ grants.** A user whose READ comes only from an app-scoped grant is never notified (the worker has no app context).
60
+ - **Local dispatch fails (known gap).** In debug mode `_Worker::runTask` POSTs to `_Config::api('worker')`; dev configs have no worker URL and worker2 has no dev config. Locally the user request still succeeds; no notification rows appear.
61
+ - **Existing writers keep working.** Compass's own `Notifications` writer and the toga2-commerce bell are unchanged; they do not use rules.
62
+ - `_Component_Acl_ReadExpression` and `_Component_SettingResolver` were moved out of V2 unchanged (READ expression parity proven on 440 user/record pairs). Change them in one place — V2 and the dispatcher both depend on them.
63
+
64
+ ## Change history
65
+ - 2026-10-18 — Built the rule-driven engine, Notifications archive columns, owner-only ACL; moved READ expression + setting resolver into `_underscore`. TRUE-82907 (jcardinal)
@@ -20,6 +20,7 @@ related:
20
20
  - ../../../../clients/compass-usa/features/stranded-approval-reassignment.md
21
21
  - ../../api2/features/api-payload-interceptors.md
22
22
  - ../../worker2/features/creating-worker-actions.md
23
+ - ./notification-engine.md
23
24
  ---
24
25
 
25
26
  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.
@@ -30,6 +31,8 @@ Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. `Log
30
31
 
31
32
  Key property: **a change made outside the API leaves a `Record` row's field children empty — or no row at all.** That absence distinguishes a direct SQL edit from an in-app change — usually the first fork in any "how did this record get like this?" investigation.
32
33
 
34
+ These rows also drive in-app notifications: after api2 commits them, matching rows go to the [notification engine](./notification-engine.md).
35
+
33
36
  This is the **model-write** audit log — a different thing from the **request** log (`Logs_<Client>.Api` / core `Logs.Api`, see [V2 Request Logging](../../api2/features/request-logging.md), which records HTTP traffic; this one records the resulting field mutations).
34
37
 
35
38
  ## How it works
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-18
10
10
  owners: [jcardinal, apeterson, bala]
11
11
  files:
12
12
  - _underscore/Model/Client/Language.php
@@ -323,8 +323,8 @@ Same discipline as the reserved-id blocks: **read the id out of the target envir
323
323
 
324
324
  Developer decision (jcardinal, 2026-10-08), platform apps (Desk, IQ, template). Which layer owns what:
325
325
 
326
- 1. **Surface owns what users SEE.** `Core.Surfaces` / `SurfaceElements` / `Messages` + Client `MessageTranslations` + Client `SurfaceOverrides`: copy, labels, order, visibility, required, icons, colours. Client changes = `SurfaceOverrides` (client / persona / role); translations = `MessageTranslations`.
327
- 2. **`*Settings` keeps ONLY preferences:** `language`, `date-format`, `time-format`, `time-zone`, `currency`, `persona`. One shared preferences resolver (to move from api2 `V2::resolveSettingValue` into `_underscore`), honouring `isOverridable`. UI keys are FROZEN, new apps never read them: `label`, `label-singular`, `icon`, `tooltip`, `placeholder`, `sort-order`, `is-required`, `default-value`, `min-required-siblings`, `max-required-siblings`.
326
+ 1. **Surface owns what users SEE.** `Core.Surfaces` / `SurfaceElements` / `Messages` + Client `MessageTranslations` + Client `SurfaceOverrides`: copy, labels, order, visibility, required, icons, colors. Client changes = `SurfaceOverrides` (client / persona / role); translations = `MessageTranslations`.
327
+ 2. **`*Settings` keeps ONLY preferences:** `language`, `date-format`, `time-format`, `time-zone`, `currency`, `persona`. One shared preferences resolver, `_Component_SettingResolver` in `_underscore` (moved out of api2 V2 2026-10-18), honoring `isOverridable`. UI keys are FROZEN, new apps never read them: `label`, `label-singular`, `icon`, `tooltip`, `placeholder`, `sort-order`, `is-required`, `default-value`, `min-required-siblings`, `max-required-siblings`.
328
328
  3. **Record + field names move to `Core.Messages`** (translatable) via message-id link columns on `Core.Records` / `Core.RecordFields`, first seeded from `DefaultRecordSettings` / `DefaultRecordFieldSettings` labels; until seeded, fall back to the settings label.
329
329
  4. **No per-user text overrides.** User choices = preferences, saved views, column order. Add a Surface lock only when a real need appears.
330
330
  5. **`SurfaceOverrides` keep numeric Core ids** in client DBs (no uuid re-key) - see [surface-overrides-uuid-rekey](surface-overrides-uuid-rekey.md).
@@ -344,5 +344,6 @@ Precedence is per layer and never mixed on one value: Surface = base < client <
344
344
  - [side-navigation-and-default-route](../../toga25-supply/features/side-navigation-and-default-route.md)
345
345
 
346
346
  ## Change history
347
+ - 2026-10-18 — Shared preferences resolver now lives in `_underscore` as `_Component_SettingResolver`. TRUE-82907 (jcardinal)
347
348
  - 2026-10-17 — `IS_REQUIRED` override loses to `requiredRule`; use static required + `visibilityRule` (TRUE-82889). (jcardinal)
348
349
  - 2026-10-08 — Added "Layer boundary": Surface owns what users see, `*Settings` = preferences only (jcardinal decision; see Desk ADR 0140) (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-18
10
10
  owners: ["jcardinal", "bala"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -37,8 +37,9 @@ Uniform across all clients (no client variations). The sidecar table + ACL ship
37
37
  ## How it works
38
38
 
39
39
  **Key files / entry points:**
40
- - `_underscore/Model/Core/Setting.php` — `SLUG_LANGUAGE` const + `valueIsSet()` helper (the cascade resolver itself lives in V2.php, since it is a language/API concern).
41
- - `api2/Component/Api/V2/V2.php`: `resolveSettingValue($slug, $context)` (the 8-layer Settings-cascade resolver, private); `resolveValidLanguageCode()` (validates against `Languages.code`, default `en`); `getTranslatedFieldValue()` / `loadTranslationSidecar()` / `shouldTranslate()` / `addTranslationFallbackWarning()` (read path); `extractTranslationWrites()` / `saveTranslationWrites()` (write path); `lookupTranslatableFieldByRecordFieldId` built in the RecordFields load (~line 200).
40
+ - `_underscore/Model/Core/Setting.php` — `SLUG_LANGUAGE` const + `valueIsSet()` helper.
41
+ - `_underscore/Component/SettingResolver/SettingResolver.php` — `_Component_SettingResolver`, the 8-layer Settings-cascade resolver (moved out of V2 unchanged 2026-10-18; V2 `resolveSettingValue()` delegates to it; also used by the [notification engine](../../_underscore/features/notification-engine.md)).
42
+ - `api2/Component/Api/V2/V2.php`: `resolveValidLanguageCode()` (validates against `Languages.code`, default `en`); `getTranslatedFieldValue()` / `loadTranslationSidecar()` / `shouldTranslate()` / `addTranslationFallbackWarning()` (read path); `extractTranslationWrites()` / `saveTranslationWrites()` (write path); `lookupTranslatableFieldByRecordFieldId` built in the RecordFields load (~line 200).
42
43
  - `_underscore/Model/Client/ItemTranslation.php` — the first sidecar model.
43
44
 
44
45
  **Language resolution (auth time, user auth only).** `resolveSettingValue('language', …)` walks the Settings Matrix in order — DefaultGlobal, DefaultApp, ClientGlobal, ClientApp, PersonaGlobal, PersonaApp, UserGlobal, UserApp — each overriding the previous when set. A Client/Persona layer with `isOverridable = 0` **locks** the value against all later layers. For the persona layers, the **first persona** (in the user's persona order) that sets it wins. The resolved code is validated against the client `Languages.code` (see *Invalid saved language* below), embedded in the JWT `id.language` claim, and echoed on every response as `audience.language`. API-credential auth gets no language.
@@ -113,4 +114,5 @@ Placement is load-bearing: the `id.language` mutation happens **after** client-D
113
114
  - [_Model magic-field access](../../_underscore/features/model-magic-field-access.md)
114
115
 
115
116
  ## Change history
117
+ - 2026-10-18 — Settings-cascade resolver moved to `_Component_SettingResolver` (`_underscore`), unchanged. TRUE-82907 (jcardinal)
116
118
  - 2026-10-09 — Login no longer fails on an unknown saved language (EV-16 retired): client-language fallback + WZ-4; language row made overridable on all clients; user-global-settings write guard linked. (jcardinal)
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
10
- owners: ["kyalamarthi", "mhammontree", "tcox"]
9
+ updated: 2026-10-18
10
+ owners: ["kyalamarthi", "mhammontree", "tcox", "jcardinal"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - _underscore/Model/Team/Sprint.php
@@ -111,11 +111,13 @@ So for a scripted call, **authorization comes solely from `AclCustomRecordScript
111
111
  - **No bound params in `_Query`.** Sanitize with `\_Database::escape()` (strings) and `(int)` casts (numerics) for every value placed into the query text — query-string args are user input.
112
112
  - **Restore the query-cache flag.** `useQueryCache(false)` is a global toggle; capture the prior value and restore it, or you disable caching for the rest of the request.
113
113
  - **`aclDatabase` has no `TEAM`.** A `Team`-schema record registers as `'CORE'`; picking `'CLIENT'` sends the engine looking in the wrong cluster.
114
- - **⚠ Never hardcode a `Core.RecordScripts` id in a grant — it WILL be renumbered on apply.** Concrete case (verified on production 2026-08-13): `Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql` wrote the `campaigns`/`jobs` script as **id 25** ("next free; prod max was 24"), and its sibling `Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql` granted `recordScriptId = 25` — but the row landed as **id 30** in production, so the grants pointed at nothing and a later file (`Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql`) had to re-grant against 30. A grant that references a stale id inserts a row happily and fails **only** at request time as a missing-grant error. Resolve the id by `recordId` + `method` + `route` where the databases allow it, and always verify the actual id in the target environment.
114
+ - **⚠ Never hardcode a `Core.RecordScripts` id in a grant — it WILL be renumbered on apply.** Concrete case (verified on production 2026-08-13): `Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql` wrote the `campaigns`/`jobs` script as **id 25** ("next free; prod max was 24"), and its sibling `Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql` granted `recordScriptId = 25` — but the row landed as **id 30** in production, so the grants pointed at nothing and a later file (`Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql`) had to re-grant against 30. A grant that references a stale id inserts a row happily and fails **only** at request time as a missing-grant error. Resolve the id by `recordId` + `method` + `route` where the databases allow it, and always verify the actual id in the target environment. **In a `Client/` grant file the Core lookup is not allowed (other cluster)**, so the file types the id as a literal with header SELECTs to run on Core; the operator replaces the literal per environment before running (approved manual step, jcardinal 2026-10-18).
115
+ - **⚠ `phpMethod` must not reuse a `_Model` method name.** `search` on `_Model_Core_Record` fatals ("Cannot make non static method … static") on every request that loads Records — `_Model` already has non-static `search()`. Global search uses `globalSearch` ([global search](../../_underscore/features/global-search.md)).
115
116
  - **Registration is per-environment and split.** A missing `Core.RecordScripts` row → the route isn't a script (falls through to normal CRUD / 404-shaped behavior); a missing `AclRecordScripts` grant → the caller's roles can't run it. Both rows must exist in every environment the API serves.
116
117
  - **The committed-tile definition is intentionally not the sprint-scoring definition.** `committedTile()` uses the **Power BI / current-state** definition of done/committed (`dtDone IS NOT NULL` + `workTypeNow`), which is *deliberately different* from TOGa Talos's canonical sprint scoring (`statusNow IN (STATUS_IN__DONE)` + `workTypeAtLock`). See [Team Sprint Management](../../worker2/features/team-sprint-management.md) before assuming a tile agrees with sprint scores.
117
118
 
118
119
  ## Change history
120
+ - 2026-10-18 — `phpMethod` vs `_Model` method-name collision; manual per-environment `RecordScripts` id in `Client/` grant files. TRUE-82907 (jcardinal)
119
121
  - 2026-09-25 — renamed product references from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
120
122
 
121
123
  ## Related
@@ -411,9 +411,11 @@ The extra `SELECT … FROM ( … ) existing` layer both dodges error 1093 and pi
411
411
  - **A `Client/` fan-out file that hardcodes a `customRecordFieldId` wires the wrong field for 29 of 30 tenants with no error** (rule #10). Custom-field ids are per-client `AUTO_INCREMENT`; keep custom-field work in the tenant's own folder.
412
412
  - **An unguarded registration/seed insert can double-register** on a fresh environment (rule #7) — guard on the semantic identity, not the description.
413
413
  - **The `dbchanges2` folder name IS the target database.** Running a `Core/` file against a client DB fails as `Table 'Client_X.<Table>' doesn't exist` (files are unqualified by design) — which names a missing table, not the real mistake. See [targeting the right database](workflows/targeting-the-right-database.md).
414
+ - **Core legacy tables are `utf8mb4_unicode_ci`; new tables are `utf8mb4_0900_ai_ci`.** Legacy = `Settings`, `DefaultGlobalSettings`, `Messages`, `Records`, `RecordFields`, `Surfaces`, `SurfaceElements`. Any SQL that compares strings between a legacy and a new table needs `COLLATE utf8mb4_unicode_ci` on the comparison, or it fails with error 1267 "Illegal mix of collations". Temp tables: see *Staging tables MUST pin the collation* above. (TRUE-82907, jcardinal)
414
415
 
415
416
  ## Change history
416
417
 
418
+ - 2026-10-18 — Core legacy-vs-new table collation gotcha (1267). TRUE-82907 (jcardinal)
417
419
  - 2026-10-18 — Core.Records.name required + human naming; record icons in Core.Records (TRUE-82880). (jcardinal)
418
420
  - 2026-10-17 — /finish reports central ids used + next free (TRUE-82889). (jcardinal)
419
421
  - 2026-10-17 — Second same-folder file also allowed for interceptor rows that must run after the code deploy (TRUE-82889). (jcardinal)
@@ -6,5 +6,6 @@
6
6
  | [Contact primary email/phone save (updateContactChannels) — table calls only](features/contact-channels-save.md) | How a contact edit form saves the contact's primary email and phone without nested writes; use it (do not rebuild it) in any app that edits a contact's email/ph |
7
7
  | [Money formatting (useSessionCurrency / formatMoney / useFormatMoney)](features/money-formatting.md) | The one money formatter for every platform app (session currency + session language via Intl); open before showing any money amount or adding a money column. |
8
8
  | [Old blox (toga-blox) straight port inside toga25-blox](features/old-blox-port.md) | Old `@agilant/toga-blox` code copied unchanged into toga25-blox (blox-v1 layer); open before touching any old component, its checks/config, or merging Storybook |
9
+ | [Shell GlobalSearch, NotificationBell, NotificationsInbox (@toga/blox/shell)](features/shell-search-and-notifications.md) | Shared app-shell header search + notifications for every platform app; open when wiring search or the bell into an app, or changing search timing/keys. |
9
10
  | [GitHub Actions → AWS role via OIDC (trust policy + sub claim)](workflows/github-actions-aws-oidc-role.md) | How a GitHub Actions job assumes an AWS IAM role with OIDC, and the `sub` claim trap; open before creating or debugging any GitHub-to-AWS role ("Not authorized |
10
11
  | [Moving an old app from @agilant/toga-blox onto toga25-blox](workflows/moving-an-old-app-onto-toga25-blox.md) | Planned steps to switch an old app off the `@agilant/toga-blox` npm package onto toga25-blox source with zero import edits; open before moving any old app. |
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Shell GlobalSearch, NotificationBell, NotificationsInbox (@toga/blox/shell)
3
+ framework: "2.0"
4
+ repo: toga25-blox
5
+ project: TOGa Blox 2.5
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-18
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - toga25-blox/src/shell/search/GlobalSearch.tsx
13
+ - toga25-blox/src/shell/search/searchController.ts
14
+ - toga25-blox/src/shell/search/searchBoxStorage.ts
15
+ - toga25-blox/src/shell/notifications/NotificationBell.tsx
16
+ - toga25-blox/src/shell/notifications/NotificationsInbox.tsx
17
+ related:
18
+ - ../../_underscore/features/global-search.md
19
+ - ../../_underscore/features/notification-engine.md
20
+ - ../architecture.md
21
+ ---
22
+ Shared app-shell header search + notifications for every platform app; open when wiring search or the bell into an app, or changing search timing/keys.
23
+
24
+ ## Summary
25
+
26
+ Every platform app uses these shell pieces — never its own copy. Each app passes a `provider` prop that supplies the data (which clients, which tokens). Backend: [global search](../../_underscore/features/global-search.md), [notification engine](../../_underscore/features/notification-engine.md). Built TRUE-82907.
27
+
28
+ ## How it works
29
+
30
+ **GlobalSearch:**
31
+ - Opens with Ctrl/Cmd-K.
32
+ - `GLOBAL_SEARCH_MIN_CHARS = 3`.
33
+ - **Enter** = tier 1 only; a single exact match jumps straight to the record.
34
+ - **Pause** `GLOBAL_SEARCH_PAUSE_MS = 2000` (same as Vision) = tiers 1 → 2 → 3 chained; results stream in; shows "Searching deeper…".
35
+ - A new keystroke aborts in-flight calls (`AbortController`).
36
+ - `searchBoxUuid` is stable per tenant + user in localStorage — the backend uses it to drop stale searches.
37
+ - Arrow keys + Enter move through results.
38
+
39
+ **NotificationBell / NotificationsInbox:** infinite scroll; counts come from server aggregates, not from loaded rows.
40
+
41
+ **Design:** for shell pieces the design system wins over an app's designer (developer decision 2026-10-18).
42
+
43
+ ## Change history
44
+ - 2026-10-18 — Added shell GlobalSearch, NotificationBell, NotificationsInbox. TRUE-82907 (jcardinal)
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-10-13
8
+ updated: 2026-10-18
9
9
  owners: [jcardinal, mhammontree, dfranks, bala, ajean]
10
10
  files: []
11
11
  related:
@@ -317,6 +317,8 @@ WHERE
317
317
 
318
318
  ### **SQL Naming Conventions**
319
319
 
320
+ - **American spelling everywhere** — code, table/column names, copy, docs (`color`, not `colour`; `honor`, not `honour`). Team rule 2026-10-18 (jcardinal).
321
+
320
322
  #### **Table Names — plural, except the `Logs` cluster**
321
323
 
322
324
  * Table names are **plural**: `Orders`, `Customers`, `PurchaseOrderItems`. This is the default
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-10-17
8
+ updated: 2026-10-18
9
9
  owners: [jcardinal, apeterson, tcox]
10
10
  files: []
11
11
  related:
@@ -50,6 +50,8 @@ Anything about env files, build mode, Amplify, the per-environment blox channel,
50
50
 
51
51
  ## 1. Project structure & module organization — **MUST / SHOULD**
52
52
 
53
+ - **American spelling** in identifiers, CSS tokens, and copy (`color`, not `colour`) — see [backend-php.md → SQL Naming Conventions](backend-php.md).
54
+
53
55
  **MUST: organize feature-first.** A page folder colocates the page's view, its data hook, its
54
56
  sub-components, and its CSS. Do not spread one screen's files across parallel `components/`,
55
57
  `hooks/`, `styles/` trees by type — group by feature.
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
 
22
22
  ## 2.0 framework
23
23
 
24
- - **_underscore** (_Underscore) _(framework core)_ — 105 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
24
+ - **_underscore** (_Underscore) _(framework core)_ — 107 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
25
25
  - **worker2** (Worker) — 90 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
26
26
  - **api2** (API) — 27 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
27
27
  - **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -34,7 +34,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
34
34
  - **toga25-supply** (TOGa 2.5 Supply) — 26 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
35
35
  - **toga25-iq** (TOGa IQ) — 1 doc(s) → [2.0/apps/toga25-iq/INDEX.md](2.0/apps/toga25-iq/INDEX.md)
36
36
  - **toga25-desk** (TOGa Desk 2.5) — 1 doc(s) → [2.0/apps/toga25-desk/INDEX.md](2.0/apps/toga25-desk/INDEX.md)
37
- - **toga25-blox** (TOGa Blox 2.5) — 6 doc(s) → [2.0/apps/toga25-blox/INDEX.md](2.0/apps/toga25-blox/INDEX.md)
37
+ - **toga25-blox** (TOGa Blox 2.5) — 7 doc(s) → [2.0/apps/toga25-blox/INDEX.md](2.0/apps/toga25-blox/INDEX.md)
38
38
  - **toga-blox** (TOGa Blox) — 15 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
39
39
 
40
40
  ## standalone framework
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1076",
3
+ "version": "1.0.1077",
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",