toga-ai 1.0.1070 → 1.0.1071

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.
@@ -6,11 +6,13 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-18
10
- owners: [apeterson]
9
+ updated: 2026-10-17
10
+ owners: [apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
13
13
  - _underscore/Model/Compass/SalesOrder.php
14
+ - _underscore/Model/Client/Certification.php
15
+ - _underscore/Model/Client/UserCertification.php
14
16
  - _underscore/Model.php
15
17
  related:
16
18
  - sales-order-status-filter-surface.md
@@ -34,7 +36,10 @@ Before this, `filterOptions` in table meta was filled in **only** for `TYPE__STA
34
36
  column whose options are **live data** (e.g. the list of active Admin users): seeding a surface
35
37
  element per user would go stale constantly.
36
38
 
37
- First and only consumer today: the Compass **Assigned Admin** column on the sales-orders table.
39
+ Consumers: Compass **Assigned Admin** (live data, below); `_Model_Client_Certification::_complianceStateFilterOptions()`
40
+ and `_Model_Client_UserCertification::_statusFilterOptions()` (fixed enum list: loop the model's
41
+ state constant, `slug` = `name` = the value; the front end labels it from language data). Use the
42
+ enum shape for any status-like calc column.
38
43
 
39
44
  ## How it works
40
45
 
@@ -56,7 +61,7 @@ an empty dropdown. Same contract as `getStatusFilterOptions()`.
56
61
  throws when a `FIELD_SQL` field has no static of the same name — that is why `_assignedAdmin()`
57
62
  exists. The seam just adds a `FilterOptions` suffix to the same name-derived rule.
58
63
 
59
- It is a **no-op for every existing column**: nothing else declares a `<field>FilterOptions` static.
64
+ It is a **no-op** for any column whose model declares no `<field>FilterOptions` static.
60
65
 
61
66
  ### The Compass example
62
67
 
@@ -161,10 +166,11 @@ generic version at roughly **4–5** such columns, driven by real examples.
161
166
  the same pattern `_assignedManager()` already uses.
162
167
  - **Matching a role by its human-readable `name` is fragile** (flagged by `sql-reviewer`): a client
163
168
  renaming the `Admin` role silently empties the list. A stable role id would be sturdier.
164
- - **⚠ Filtering a calculated column lands in HAVING, not WHERE.**
165
- `api2/Component/Api/V2/V2.php` `splitWhereExpressionsIntoWhereHaving` routes a where on a
166
- calculated field into `HAVING`, so the `_assignedAdmin` correlated subquery is evaluated for every
167
- row surviving the WHERE, **before LIMIT** — and again for the pagination count query. On a large
169
+ - **⚠ Filter with the BARE calc name** (`_complianceState:eq:AT_RISK`) — it is inlined into
170
+ `WHERE`. The table-prefixed form (`Table._x`) goes to `HAVING` and returns 500 / `EO-1`. Mechanism:
171
+ [V2 REST query contract](../../api2/features/v2-rest-query-contract.md). Either way the expression
172
+ (here the `_assignedAdmin` correlated subquery) is evaluated **per row, before LIMIT** — and again
173
+ for the pagination count query. On a large
168
174
  Compass `SalesOrders` table this can be slow, and it will not fail loudly (api2's Apache gateway
169
175
  timeout is 1800s). This is pre-existing behaviour of filtering **any** calculated column, not new
170
176
  here, and it was **not** measured with `EXPLAIN` at production size. The cheaper target would be
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-15
9
+ updated: 2026-10-17
10
10
  owners: [apeterson, bala, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
@@ -140,6 +140,7 @@ So the visible failure is a broken SQL query while the actual defect is one miss
140
140
  - **Flag columns are NULLable** — guard flag migrations with `COALESCE(col, 0) = 0`, never `col = 0`, or the UPDATE matches 0 rows and looks like caching.
141
141
  - **`type`/`precision` are canonical (Core-wide), not per-view.** Editing `Core.RecordFields` changes every view/client that shows the field; there is no per-view type override. `isCopyable`, `isSortable`, `isFilterable`, `isGroupable`, `isVisible` are the per-view knobs (on `TableViewFields`).
142
142
  - **`TableView::meta()` contains a hand-written `SELECT` that names `TableViewFields` columns explicitly.** So a new column on that table is required by **two** mechanisms — the model property *and* that query — and a grep for the property alone under-reports what a missing column breaks.
143
+ - **⚠ Type `SELECT` only on a JOINED FK column.** `meta()`'s `TYPE__SELECT` branch reads `$tableViewJoins[$tableAlias]->record` (`TableView.php` ~L265-272); a main-table column has no join alias, so it breaks at runtime. For an enum column on the main table, expose a calc label field (e.g. `_status`) with a `<field>FilterOptions` static — see [calculated-column filter options](../../_underscore/features/calculated-column-filter-options.md).
143
144
  - **`isGroupable` has no migration** — omit it from new TableView migrations (fails on prod/beta) until the fan-out migration lands. Composite unique keys read as unconstrained in `information_schema.COLUMNS`; use `information_schema.STATISTICS` / `SHOW INDEX`.
144
145
 
145
146
  ## Related
@@ -151,4 +152,5 @@ So the visible failure is a broken SQL query while the actual defect is one miss
151
152
  - [TableView joins](../../_underscore/features/tableview-joins.md)
152
153
 
153
154
  ## Change history
155
+ - 2026-10-17 — `SELECT` type is safe only on a joined FK column; main-table enum → calc label + FilterOptions. (jcardinal)
154
156
  - 2026-09-23 — Recorded that `Core.RecordFields.type` drives display/filter only and never sorting; that `NUMBER` on a text column blanks non-numeric rows (Compass `kit 205-207` bundles) while `BOOLEAN` fixes genuinely blank flag columns; and that the flag columns are NULLable so a `= 0` migration guard matches nothing. (apeterson)
@@ -56,6 +56,10 @@ controller.
56
56
  (`V2.php` ~L3673–3682). **Prefer bare for ascending:** the query string is parsed *without*
57
57
  urldecoding (see Encoding rules), so a client that percent-encodes the `+` as `%2B` sends a
58
58
  field literally named `%2Bfield` and the sort is silently dropped. Bare has no such failure mode.
59
+ - **Multi-sort = comma list** (`sort=-dateIssued,uuid`; `parseOptionsSort` ~L10888 explodes on `,`;
60
+ a blox `sort: [...]` array is sent comma-joined). **Offset paging (`page`) is only stable with a
61
+ unique last key** — add `uuid` as the tie-breaker, or rows with equal sort values shuffle between
62
+ pages.
59
63
  - **A LIST select is `SELECT SQL_CALC_FOUND_ROWS DISTINCT <table>.<primaryKey>`** (`V2.php:3884`;
60
64
  the aggregate-fields path is the one exception). Two consequences: (a) join fan-out collapses to
61
65
  one row per record, so **duplicate child rows are invisible in the response** — a duplicate join
@@ -382,9 +386,10 @@ Mechanism, read from `api2/Component/Api/V2/V2.php`:
382
386
  1. `buildWhereExpressionsFromOptions` (~L6953) **deliberately does not prepend the default table**
383
387
  to a field starting with `_` — its own comment says *"add the default table if not specified and
384
388
  is not a calculated field."*
385
- 2. `buildWhereClauseFromWhereExpressions` (~L7069) sees the leading `_` and **inlines the field's own
386
- SQL expression** straight into the `WHERE` clause. That is the working path.
387
- 3. `splitWhereExpressionsIntoWhereHaving` (~L8622) routes to `HAVING` **only when the field contains
389
+ 2. `validateWhereFieldReference` (~L8704-8741) accepts a bare calc name on the primary table.
390
+ 3. `buildWhereClauseFromWhereExpressions` (~L9225-9234) sees the leading `_` and **inlines the field's
391
+ own SQL expression** straight into the `WHERE` clause. That is the working path.
392
+ 4. `splitWhereExpressionsIntoWhereHaving` (~L11286) routes to `HAVING` **only when the field contains
388
393
  `._`** — i.e. only the table-**prefixed** form. That form targets a joined-table select alias
389
394
  (`PurchaseOrderItems__qtyAvailable`, built ~L3984) which **does not exist for the main table**,
390
395
  and ~L4116 also appends the `HAVING` to the `$sqlProhibited` COUNT query, which has no such alias
@@ -411,9 +416,11 @@ production — `toga2-supply/src/api/toga.ts` L906-919 (`_status` OR group) and
411
416
  **Zero** occurrences of the table-prefixed form exist in any repo — nobody has ever used it
412
417
  successfully.
413
418
 
414
- > ⚠ **Scope this to `where` only.** It says nothing about `group` or `sort` on a calculated field;
415
- > `group=_status` is separately known to fail. And `where` still never prunes a nested child array
416
- > (see below).
419
+ > **`sort` on a calculated field also works** (bare name, calc branch of `ORDER BY` above).
420
+ > **`group=_status` fails.** `where` still never prunes a nested child array (see below).
421
+ > A TableView column over a calc field can be `isFilterable` — a code comment saying "calc fields
422
+ > are not filterable" records a choice, not an engine limit. Dropdown options:
423
+ > [calculated-column filter options](../../_underscore/features/calculated-column-filter-options.md).
417
424
 
418
425
  #### ⚠ `in` instead of an OR-chain of `contains` shortens the SQL - it does NOT make the filter fast
419
426
 
@@ -618,6 +625,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
618
625
  [V2 request logging](request-logging.md).
619
626
 
620
627
  ## Change history
628
+ - 2026-10-17 — Multi-sort comma list + `uuid` tie-breaker for stable paging; sort on calc works; refreshed where-path line refs. (jcardinal)
621
629
  - 2026-10-17 — POST create = 201, PUT/DELETE = 200 (TRUE-82889). (jcardinal)
622
630
  - 2026-10-15 — Security: `where` on an unreadable field still filters under default `identifier_acl_mode=log` (hash guessing); dotted FK path in `where` = `EV-8`, use a join alias (TRUE-82836). Folded older history. (jcardinal)
623
631
  - 2026-09-24 — `depth`/`calcDepth` ignored inside the base64 `_` blob; field three levels under an inherent child dropped (TRUE-82138). (apeterson)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1070",
3
+ "version": "1.0.1071",
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",