toga-ai 1.0.843 → 1.0.844

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.
@@ -16,6 +16,8 @@ related:
16
16
  - sales-order-status-filter-surface.md
17
17
  - tableview-joins.md
18
18
  - ../../toga-blox/features/table.md
19
+ - ../../api2/features/environment-variable-drives-underscore-branch.md
20
+ - ../../toga25-supply/features/persisted-query-cache.md
19
21
  - ../../api2/features/tableview-field-metadata.md
20
22
  ---
21
23
 
@@ -72,23 +74,44 @@ It is a **no-op for every existing column**: nothing else declares a `<field>Fil
72
74
  - Verified locally: 13 options for `Client_Compass`, 5 for `Client_CompassCanada`. Compass Canada
73
75
  also has a role `Agilant - Administrators`, correctly excluded by the exact `name = 'Admin'` match.
74
76
 
75
- **Internal TOGA staff are excluded (2026-09-18).** Admin-role holders on `@togatech.com`
76
- (Dev Team, internal employees) were showing up as assignable to Compass users. Added
77
- `const INTERNAL_EMAIL_DOMAIN = 'togatech.com'` plus a WHERE condition. The condition is
78
- **NULL-safe on purpose**:
77
+ **Two exclusion rules, both applied (2026-09-18).** Admin-role holders were showing up as
78
+ assignable to Compass users when they should not be:
79
+
80
+ 1. **Internal TOGA staff, by domain** — `const INTERNAL_EMAIL_DOMAIN = 'togatech.com'`.
81
+ 2. **Named individuals on a CLIENT domain** — `const EXCLUDED_ADMIN_EMAILS = ['vanessa.burks@compass-usa.com'];`
82
+ built into a `NOT IN (...)`. The domain rule alone could never catch her — her email is a
83
+ `@compass-usa.com` address. Assume any hide list will eventually need both.
84
+
85
+ Both conditions are **NULL-safe on purpose**:
79
86
 
80
87
  ```sql
81
88
  (Users.email IS NULL OR Users.email NOT LIKE '%@togatech.com')
89
+ (Users.email IS NULL OR Users.email NOT IN ('...'))
82
90
  ```
83
91
 
84
- A bare `NOT LIKE` yields NULL for a NULL email and would silently drop those admins.
85
-
86
- Two alternatives were considered and rejected:
87
- 1. Removing the `Admin` role from those users in `Users_Roles` — the team will not strip admin
88
- roles from real accounts.
89
- 2. A `c_isHiddenFromAdminFilter` custom column on `Client_Compass.Users` (the `c_isVip`
90
- precedent), making a hide a plain UPDATE with no deploy. Rejected for the simpler hardcoded
91
- domain rule — but this is the natural next step if per-user control is ever needed.
92
+ A bare `NOT LIKE` / `NOT IN` yields NULL for a NULL email and would silently drop those admins.
93
+ Each list value is escaped with `_Database::escape()`, and the `NOT IN` is **skipped entirely when
94
+ the array is empty** — an empty list builds `NOT IN ()`, a MySQL syntax error.
95
+
96
+ **Why a constant and not a `c_isHiddenFromAdminFilter` column on `Client_Compass.Users`** (proposed
97
+ twice, rejected twice — do not re-propose): these same people **must still appear in other admin
98
+ lists** in the product. A `c_` flag reads as a property of the person ("hidden from admin lists"),
99
+ so the next developer reuses it for a different list and hides them there too. A constant inside
100
+ `_assignedAdminFilterOptions()` cannot leak — the exclusion stays scoped to this one dropdown.
101
+ Accepted trade: adding a name needs a deploy. Rationale is repeated in the method docblock.
102
+ The other rejected option: stripping the `Admin` role in `Users_Roles` — the team will not strip
103
+ admin roles from real accounts.
104
+
105
+ **ACL cannot do this**, for two independent reasons. Row-level ACL does exist in 2.0
106
+ (`Core.AclRecordExpressions.sqlExpression`, wired to a role via `AclLogicGroups` /
107
+ `AclLogicGroupExpressions`), so the instinct is reasonable, but:
108
+ 1. `_assignedAdminFilterOptions()` runs a raw `new _Query(...)` straight against `DB_CLIENT` and
109
+ never passes through the `_Model` / V2 ACL layer, so no ACL row changes its result;
110
+ 2. even if it did, an `AclRecordExpression` filters **every** read of that record for that role —
111
+ hiding the person from user lists and lookups everywhere, far wider than one dropdown.
112
+
113
+ **General rule: a filter-options static that builds its own `_Query` is outside ACL by
114
+ construction.** Whatever it must exclude, it excludes in its own SQL.
92
115
 
93
116
  ### Why a model static and not a config column
94
117
 
@@ -119,6 +142,15 @@ generic version at roughly **4–5** such columns, driven by real examples.
119
142
  - **The dropdown is backend-driven — do not look in toga25-supply.** The front end only renders
120
143
  what `_assignedAdminFilterOptions()` returns. Any change to which admins appear goes in
121
144
  `_underscore/Model/Compass/SalesOrder.php`.
145
+ - **⚠ "My change had no effect" is usually stale cache or an undeployed branch, not this code.**
146
+ Work the ladder cheapest-first: clear the `supply-chain-query-cache` localStorage key (stale table
147
+ meta is served with **nothing in the network tab**), then read the meta request host in DevTools to
148
+ learn which api2 tier answered, then check branch/deploy state — see
149
+ [ENVIRONMENT drives the _underscore branch](../../api2/features/environment-variable-drives-underscore-branch.md).
150
+ The client-model class chain is **not** a likely cause: `_Model_Client_TableView::meta()` swaps
151
+ `_Model_Client_` → `_Model_<clientIdentifier>_` (`TableView.php` ~L266), and
152
+ `_Model_Compass_Usa_SalesOrder` is an empty subclass of `_Model_Compass_SalesOrder`, so the
153
+ override is inherited. Rule it out in one minute.
122
154
  - **Key on the FIELD name, not the column slug.** The slug is `assigned-admin` (hyphen); the model
123
155
  field is `_assignedAdmin`. The seam uses the field name.
124
156
  - **Type `STRING` (not STATUS/SELECT) is what lets the branch fire** — the existing `switch` in
@@ -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-18
10
10
  owners: ["bala", "mhammontree", "apeterson", "jcardinal", "tcox"]
11
11
  files:
12
12
  - api2/.ebextensions/git.php
@@ -90,6 +90,12 @@ So on `api.beta.togahub.com` and `api.dev.sandbox.togahub.com` the code fataled
90
90
 
91
91
  ## Gotchas
92
92
 
93
+ - **⚠ Merged locally ≠ live. A `_underscore` change reaches a tier only when the branch is PUSHED *and* api2 has been REDEPLOYED since.** The clone happens in the api2 prebuild hook, so an unpushed (or un-redeployed) branch leaves the tier running the old framework while your local checkout and `git log` both look correct. Seen 2026-09-18: a model change was committed and merged locally but never pushed; `_sandbox-client` served the old code.
94
+ - Diagnosis ladder, cheapest first, when "my `_underscore` change did nothing":
95
+ 1. Delete the `supply-chain-query-cache` localStorage key. toga25-supply persists the React Query cache, so stale table meta is served with **nothing in the network tab** — which reads as a backend fault. See [persisted query cache](../../toga25-supply/features/persisted-query-cache.md).
96
+ 2. Read the meta request **host** in DevTools to learn which tier actually answered (`api.client.sandbox.togahub.com` = the `_sandbox-client` tier).
97
+ 3. Only then suspect branch/deploy state — `git ls-remote origin <branch>` (is it pushed?) then the pipeline source action.
98
+
93
99
  - **⚠ Cross-repo change ordering: land `_underscore` BEFORE deploying api2.** Because api2's EB deploy **clones `_underscore` (branch `_<ENVIRONMENT>`, e.g. `_production`) at BUILD time** (`.platform/hooks/prebuild/git.sh`), a change spanning both repos must have its `_underscore` side merged to `_<ENVIRONMENT>` **first** — otherwise the freshly-built api2 calls a framework method/class not on the box and fatals. Landing `_underscore` first is **safe on its own**: the old api2 controller doesn't use the new framework code yet. Order: merge `_underscore` → (verify it's on the branch) → deploy api2.
94
100
  - **🚨 Worked instance — `client-sandbox` was 100% DOWN on this exact violation (2026-08-31, since fixed).** Every request returned 500 `Call to undefined method _Model_Core_AclFieldPermission::resolveFieldPermissions()`. `api2@_sandbox-client` (`Component/Api/V2/V2.php:6198`, commit `0cc37fe` "Delegate ACL field perms to shared resolver", jcardinal 2026-08-14) delegates to a resolver whose `_underscore` half (`a5e79448` "Surface field binding and ACL field resolver", same author/date) exists **only on `_qa-alpha` and `toga25-desk`** — never merged to **`_underscore@_sandbox-client`**. Resolved the same day (branches matched + redeployed). **Why a half-shipped ACL refactor is a TOTAL outage:** field-ACL resolution runs on essentially every authenticated GET, so there is no degraded mode — the whole tier 500s and every unrelated feature under test looks broken. When a whole sandbox goes dark, check the two-repo pairing before debugging your own feature.
95
101
  - **⚠ A "Could not find required file for `_Model_...`" fatal on prod can be a STALE-BUILD artifact, not a missing file.** `Logs.Issue #476` ("Could not find required file for '_Model_Client_ItemFulfillments_InventoryAdjustment'") fired because that model file **was** committed to `_production` but `api-production-1` was running an **older build**; a **redeploy** (re-pulls `_production`) resolved it. Before assuming a model file is absent, check the **running build's `_underscore` against `_production`** (file presence on the box; `git merge-base --is-ancestor`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.843",
3
+ "version": "1.0.844",
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",