toga-ai 1.0.727 → 1.0.728

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,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-15
9
+ updated: 2026-09-01
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -14,6 +14,7 @@ files:
14
14
  - toga2-supply/src/api/toga.ts
15
15
  related:
16
16
  - ../../../clients/compass-usa/features/item-fulfillment-tracking-tableview.md
17
+ - ../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md
17
18
  - tableview-field-metadata.md
18
19
  ---
19
20
 
@@ -36,8 +37,9 @@ view with a migration and no application code.
36
37
  - **Consumption** — `toga2-supply/src/api/toga.ts` (~line 943-951): pushes the `apiWhereClause`
37
38
  literal string into `options.where.and`, and (~line 631-641) builds `options.join` / `ojoin`
38
39
  from the view's `TableViewJoins`.
39
- - **Parsing** — `api2/Component/Api/V2/V2.php::parseOptionsWhere` (~line 7574-7773): turns the
40
- clause string into SQL.
40
+ - **Parsing** — `api2/Component/Api/V2/V2.php::parseOptionsWhere` (~line 8200-8390, re-located
41
+ 2026-09-01; it was ~7574 when this doc was first written, so search for the function name rather
42
+ than trusting the line number): turns the clause string into SQL.
41
43
 
42
44
  ## How it works
43
45
 
@@ -68,6 +70,22 @@ The field slot accepts a **raw SQL expression**, not just a plain column — e.g
68
70
  exclusion on the Compass fulfillment view): `(IFNULL(Items.assetTypeId,0):ne:<FEE id>)` — the
69
71
  `IFNULL(...,0)` keeps rows with a NULL FK from being silently dropped.
70
72
 
73
+ ### A single-condition clause is wrapped in parens too
74
+
75
+ `parseOptionsWhere` **strips one leading `(` and one trailing `)` from the whole clause before it
76
+ scans for top-depth commas** (V2.php ~line 8222-8229). Those outer parens are the group wrapper,
77
+ not part of the first condition, so the house form for a one-condition filter is:
78
+
79
+ ```
80
+ (Items.catalogId:eq:1)
81
+ ```
82
+
83
+ The wrapper comes off first, leaving `Items.catalogId:eq:1`, which is then read as a plain
84
+ condition — it is never mistaken for a nested group needing recursion. Write the parens; they match
85
+ the existing Compass convention and the stored clauses stay uniform whether they hold one condition
86
+ or five. (The "expression field must not start with `(`" gotcha below is about the *field slot* of a
87
+ condition **after** the wrapper is removed — the two are not in conflict.)
88
+
71
89
  ### Join alias rule (which table-qualified name to use)
72
90
 
73
91
  In `_underscore/Model/Client/TableView.php` (~line 89-96), the **first** occurrence of a joined
@@ -77,6 +95,19 @@ referencing `Items.<field>` only binds correctly when the view joins `Items` **e
77
95
  Before writing a clause, confirm the target table is joined once, or qualify against the correct
78
96
  suffixed alias.
79
97
 
98
+ **A view's base record is not a join, so it never gets a suffix.** Worked example — the shared
99
+ `items` view (`TableViews.slug = 'items'`, `id 9`, `recordId 21` = `_Model_Client_Item`, route
100
+ `items`; the same shape in `Client_Compass`, `Client_CompassCanada` and `Client_Quad`). Its four
101
+ `TableViewJoins` are `joinRecordId` **20** Manufacturers, **33** ItemTypes, **32** AssetTypes and
102
+ **34** ItemCategories, all INNER. `Items` appears only as the base record and is never re-joined,
103
+ so the bare `Items.` qualifier is correct and there is no `_B`/`_C` to worry about. Check this the
104
+ same way on any view before writing a clause: list the view's `TableViewJoins` and count how many
105
+ resolve to the table you want to filter.
106
+
107
+ Two schema notes for anyone writing the migration: `TableViews` has **no `name` or `tableName`
108
+ column** in these client schemas — identify a view by its `slug` (UNIQUE) plus `recordId`, which is
109
+ what makes `WHERE slug = 'items'` a safe, id-agnostic target across tenants.
110
+
80
111
  ## Data model
81
112
 
82
113
  - `TableViews.apiWhereClause` (TEXT, nullable) — the view-level row filter.
@@ -91,6 +122,8 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
91
122
 
92
123
  - **Comma inside a function is safe.** The parser only splits conditions on top-depth commas, so
93
124
  a comma inside `IFNULL(x,0)` or `IN(...)` does not break the clause.
125
+ - **A one-condition clause still needs its outer parens** — they are stripped before parsing, so
126
+ `(Field:eq:value)` and `Field:eq:value` behave identically; use the wrapped form for consistency.
94
127
  - **An expression field must NOT start with `(`.** A condition that starts with `(` is treated by
95
128
  the parser as a nested group and recursed into — so wrap an expression field in a function that
96
129
  starts with a letter (e.g. `IFNULL(...)`), never a bare parenthesized expression.
@@ -102,6 +135,15 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
102
135
 
103
136
  ## Change history
104
137
 
138
+ - 2026-09-01 — Recorded that the parser **strips one leading `(` and one trailing `)` from the whole
139
+ clause before splitting**, so the wrapped single-condition form `(Items.catalogId:eq:1)` is safe
140
+ and is the house convention (this was previously only documented for the multi-condition form).
141
+ Added a worked alias-verification example on the shared `items` view (id 9, recordId 21, four
142
+ INNER `TableViewJoins` 20/33/32/34, `Items` only as base record → bare `Items.` qualifier), noted
143
+ that `TableViews` carries no `name`/`tableName` column so `slug` is the id-agnostic target, and
144
+ re-pointed `parseOptionsWhere` at its current location (~8200, was ~7574). No code change —
145
+ verified while writing the catalog-1 filter migration for the `items` view in `Client_Compass`,
146
+ `Client_CompassCanada` and `Client_Quad`. (bala)
105
147
  - 2026-07-15 — Documented the end-to-end `apiWhereClause` row-filter mechanism (emission →
106
148
  consumption → `parseOptionsWhere` grammar), the operator→SQL table, the raw-expression field
107
149
  slot, the leading-paren / top-depth-comma parser behavior, and the `_underscore` join-alias
@@ -196,4 +196,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
196
196
  numbers — because it does **not** use NetSuite, so it never hit the inventory-adjustment
197
197
  dupe-item bug that fed the merge. The catalog-fold damage and reversal were Compass USA only.
198
198
  See [Compass USA catalog-fold reversal](../compass-usa/workflows/catalog-fold-reversal.md).
199
+ - **Items listing being filtered to catalog 1 (written 2026-09-01, NOT yet applied).** A migration
200
+ sets `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` on `Client_CompassCanada`'s shared
201
+ `items` view (`slug = 'items'`, id 9, recordId 21), alongside the same change in `Client_Compass`
202
+ and `Client_Quad`. **Canada is the harmless case today** — Canada has a **single catalog**
203
+ ("compass", see the bullet above), so every `Client_CompassCanada.Items` row is already catalog 1 and so the filter changes nothing visible now; it only pins the listing so a
204
+ future non-catalog-1 item (e.g. anything a NetSuite-style sync creates) would silently not appear.
205
+ Mechanism: [TableView apiWhereClause row-filtering](../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md);
206
+ catalog background: [Compass item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md).
199
207
  - Related: [Compass USA](../compass-usa/profile.md).
@@ -17,6 +17,7 @@ files:
17
17
  - worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
18
18
  related:
19
19
  - odp-edi-850-item-resolution.md
20
+ - ../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md
20
21
  - mits-po-to-so-item-linking.md
21
22
  - mits-sales-order-transmission-alerting.md
22
23
  - isfulfillable-data-quality-and-type-rule.md
@@ -132,6 +133,32 @@ Plus `-1` / `-2` suffixed copies: items **2431, 2432, 2443, 2653**.
132
133
  (e.g. the `#B1H` / `-S` variant forms above) or genuinely-shared ones before treating table-wide
133
134
  `partNumber` uniqueness as settled.
134
135
 
136
+ ## Filtering by catalog: fine for *display*, never for *resolution*
137
+
138
+ The blanket rule below ("never scope a Compass item lookup to one catalog") is about **resolving a
139
+ part number to a record** — order import, PO→SO linking, vendor-item matching. It does **not** mean
140
+ a catalog filter is banned everywhere. A **listing** is the legitimate exception: showing a user
141
+ only the customer-facing Compass catalog is a product decision, and nothing downstream re-resolves
142
+ off that list.
143
+
144
+ **In flight as of 2026-09-01 (SQL written, NOT yet applied):** the shared TOGa Supply `items` table
145
+ view (`TableViews.slug = 'items'`, `id 9`, `recordId 21`) is being restricted to catalog 1 by
146
+ setting `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` — one `UPDATE … WHERE slug = 'items'`
147
+ per schema, in `Client_Compass`, `Client_CompassCanada` **and** `Client_Quad` (the same view exists
148
+ with the same id/recordId in all three). No application code changes; the mechanism is
149
+ [TableView row-filtering via apiWhereClause](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md).
150
+ The migration was written as a loose `items_tableview_catalog_filter_2026-09-01.sql` in the
151
+ workspace root and handed to a developer to run (the DB tooling is read-only) — it still needs to
152
+ land in `dbchanges2/Client_<schema>/` under the mandatory `YYYY-MM-DD<letter> - Name.sql` filename
153
+ before it is a real change-set.
154
+
155
+ **Open decision on the clause, and it is the NULL question again.** `Items.catalogId` is nullable in
156
+ all three schemas, so a bare `:eq:1` silently drops the NULL-catalog items (a real, non-trivial
157
+ population here — see the gotcha below, and it is proportionally much larger in `Client_Quad`). If
158
+ those items must stay visible in the listing, the clause has to be
159
+ `(IFNULL(Items.catalogId,1):eq:1)` instead. Decide before applying; the two forms are not
160
+ interchangeable.
161
+
135
162
  ## "Who changed this part number?" — the audit trail says nobody
136
163
 
137
164
  The reflex answer to a part number that looks edited is to hunt for the person who edited it. For
@@ -151,9 +178,10 @@ children" rule:
151
178
 
152
179
  ## Gotchas
153
180
 
154
- - **⚠ Never scope a Compass item lookup to one catalog** unless you have proved the item can only
155
- come from that source. The correct default is `Items.partNumber IN (...)` with **no** catalog
156
- condition, and then reference the resolved record **by `uuid`**. **The catalog filter never buys
181
+ - **⚠ Never scope a Compass item *lookup* to one catalog** unless you have proved the item can only
182
+ come from that source. (A read-only **listing** filter is the one sanctioned exception — see
183
+ [Filtering by catalog](#filtering-by-catalog-fine-for-display-never-for-resolution) above.) The
184
+ correct default is `Items.partNumber IN (...)` with **no** catalog condition, and then reference the resolved record **by `uuid`**. **The catalog filter never buys
157
185
  uniqueness on Compass** — `Items.partNumber` is unique across the whole table (re-verified prod
158
186
  2026-09-01) — so every catalog condition is pure downside.
159
187
  - **⚠ The 2.0 side hardcodes the OTHER catalog, and it broke MR purchase orders.**
@@ -193,6 +221,14 @@ children" rule:
193
221
  **unresolved tension**: that reversal folded/restored 199 catalog-1 items by `UPPER(partNumber)`,
194
222
  which implies shared cross-catalog part numbers pre-merge and does not square with the same-day
195
223
  "partNumber is unique table-wide" verification — needs a prod re-check. (jcardinal)
224
+ - 2026-09-01 — Split the catalog-filter rule into **display vs. resolution**: the "never scope to one
225
+ catalog" ban covers part-number *resolution*, while a read-only *listing* filter is sanctioned.
226
+ Recorded the in-flight (written, **not yet applied**) change setting
227
+ `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'` on the shared `items` view
228
+ (`slug = 'items'`, id 9, recordId 21) in `Client_Compass`, `Client_CompassCanada` and
229
+ `Client_Quad`, and the open decision it carries: `Items.catalogId` is nullable, so bare `:eq:1`
230
+ hides NULL-catalog items and `IFNULL(Items.catalogId,1):eq:1` is the alternative. Migration still
231
+ needs moving into `dbchanges2/Client_<schema>/`. (bala)
196
232
  - 2026-09-01 — Recorded a **second, independent victim of the same catalog-scoping trap, on the 2.0
197
233
  side**: `_Model_Compass_PurchaseOrder::handleMrOrder` references MR sales-order items with a
198
234
  hardcoded `COMPASS_CATALOG_UUID` (`961dbcbc…` = catalog **1**), which is the mirror image of the
@@ -142,4 +142,16 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
142
142
  Quad's **storefront only** (not orders/transmit), and Quad was the **only other client besides
143
143
  Compass** that needed a reversal. See
144
144
  [Quad catalog-fold reversal](./workflows/catalog-fold-reversal.md).
145
+ - **Items listing being filtered to catalog 1 (written 2026-09-01, NOT yet applied) — verify the
146
+ intent for Quad first.** A migration sets `TableViews.apiWhereClause = '(Items.catalogId:eq:1)'`
147
+ on `Client_Quad`'s shared `items` view (`slug = 'items'`, id 9, recordId 21); the same one-line
148
+ change is going to `Client_Compass` and `Client_CompassCanada`. Read it against the bullet above:
149
+ for Quad, catalog 1 is the **storefront (Etilize)** catalog and catalog 2 is the **procurement
150
+ (Agilant/NetSuite)** catalog that Quad's storefront orders actually sit on, so `:eq:1` hides the
151
+ procurement half of `Client_Quad.Items` from the Supply items listing — a much bigger cut than the
152
+ equivalent change on Compass, and a majority of Quad's items. `Items.catalogId` is also nullable
153
+ here and a bare `:eq:1` drops those rows too; `IFNULL(Items.catalogId,1):eq:1` recovers the NULLs
154
+ but not catalog 2. Mechanism:
155
+ [TableView apiWhereClause row-filtering](../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md);
156
+ catalog background: [Compass item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md).
145
157
  - This profile is a starting point; expand as more Quad-specific behavior is captured.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.727",
3
+ "version": "1.0.728",
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",