toga-ai 1.0.834 → 1.0.835

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: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-15
9
+ updated: 2026-09-17
10
10
  owners: [rgirish]
11
11
  files:
12
12
  - library/app/api/toga2.php
@@ -23,6 +23,7 @@ related:
23
23
  - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
24
24
  - ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
25
25
  - ../../../clients/elite/features/netsuite-togasupply-sync.md
26
+ - ../../../clients/elite/features/supply2-tableview-config-drift.md
26
27
  ---
27
28
 
28
29
  ## Summary
@@ -138,7 +139,20 @@ without `AclRecordPermissions` the API role cannot `POST /item-classes` at all.
138
139
  sync re-walks transactions that reference them. That needs a
139
140
  [cursor rewind](../../worker/features/netsuite-togasupply-per-client-sync.md).
140
141
 
142
+ - **The tree depth is not capped — consumers that walk it with fixed joins can break silently.**
143
+ `Client_Elite` is exactly **5 levels** deep today (4 roots: 1 Lifecycle Services, 4 Technology
144
+ Sales, 22 Advisory and Modernization, 40 True Partner Services; verified 2026-09-15 that every
145
+ 5-level chain ends at a true root). Nothing stops NetSuite adding a 6th level. Anything that
146
+ resolves an item to its root class with a fixed number of `LEFT JOIN`s — such as Elite's
147
+ [Inventory service-item filter](../../../clients/elite/features/supply2-tableview-config-drift.md) —
148
+ will then silently mis-classify the deeper items with no error. Re-check depth when the NetSuite
149
+ tree changes.
150
+
141
151
  ## Change history
152
+ - 2026-09-17 — Added the **unbounded tree depth** caveat: `Client_Elite` is exactly 5 levels with 4
153
+ roots (1 / 4 / 22 / 40) and every chain verified to end at a true root, but nothing caps depth, so
154
+ any consumer walking to the root with a fixed join count breaks silently if NetSuite adds a level.
155
+ No code change. (rgirish)
142
156
  - 2026-09-15 — Documented the feature from an audit of the deployed code (library `37b55595`, worker
143
157
  `6637ae75`). **Found and fixed a field-name mismatch that made the feature a complete no-op since
144
158
  deploy:** the PHP sent `c_netsuiteInternalItemClassId` in 5 places while the live column and
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
10
- owners: ["bala"]
9
+ updated: 2026-09-17
10
+ owners: ["bala", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - _underscore/Model/Client/TableView.php
@@ -15,6 +15,7 @@ files:
15
15
  related:
16
16
  - ../../../clients/compass-usa/features/item-fulfillment-tracking-tableview.md
17
17
  - ../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md
18
+ - ../../../clients/elite/features/supply2-tableview-config-drift.md
18
19
  - tableview-field-metadata.md
19
20
  ---
20
21
 
@@ -86,6 +87,34 @@ the existing Compass convention and the stored clauses stay uniform whether they
86
87
  or five. (The "expression field must not start with `(`" gotcha below is about the *field slot* of a
87
88
  condition **after** the wrapper is removed — the two are not in conflict.)
88
89
 
90
+ ### Subqueries ARE possible — use `/**/` in place of every space
91
+
92
+ The parser splits the stored clause on **literal spaces**, so a normal SQL subquery (which needs
93
+ spaces) cannot be stored in `apiWhereClause`. The way around it: write `/**/` instead of every
94
+ space. MySQL treats `/**/` as whitespace, and the parser sees no space at all. That makes an
95
+ arbitrary **correlated subquery** usable as the *field* side of a condition.
96
+
97
+ ```
98
+ (SELECT/**/c5.id/**/FROM/**/ItemClasses/**/c1/**/WHERE/**/c1.id=Items.itemClassId):notin:1:22:40
99
+ ```
100
+
101
+ Confirmed by reading `parseOptionsWhere` and simulating it in PHP (2026-09-17):
102
+
103
+ - Conditions split on `,` **only at matching paren depth** — commas nested deeper (e.g. inside a
104
+ `COALESCE(a,b,c)` within the subquery) are safe.
105
+ - Each condition then splits on `:` into field / operator / value.
106
+ - **The field side passes through to SQL unescaped**, so it can be any expression that has no
107
+ literal space, `:` or `,` at top depth.
108
+ - `notin` splits its value on `:`, so `:notin:1:22:40` becomes the IN-list `(1,22,40)`.
109
+
110
+ **Precedent:** Elite's prod clause on slug `item-fulfillments-for-sales-orders` already uses this
111
+ same `/**/` trick with a `CONCAT((SELECT ...))` subquery, so it is an established pattern, not a
112
+ one-off.
113
+
114
+ **Caveat — leave a comment.** This is powerful but it makes the stored clause hard to read for
115
+ anyone looking straight at `TableViews`. Always add a `--` comment in the migration explaining
116
+ what the subquery does.
117
+
89
118
  ### Join alias rule (which table-qualified name to use)
90
119
 
91
120
  In `_underscore/Model/Client/TableView.php` (~line 89-96), the **first** occurrence of a joined
@@ -129,12 +158,27 @@ None — engine behavior. A specific client's view may carry its own `apiWhereCl
129
158
  starts with a letter (e.g. `IFNULL(...)`), never a bare parenthesized expression.
130
159
  - **NULL FKs drop silently.** A plain `field:ne:x` excludes rows where `field IS NULL` too. Wrap
131
160
  nullable columns in `IFNULL(col,<sentinel>)` when the intent is "exclude only these values."
161
+ - **A literal space ends the clause element** — that is why a subquery needs `/**/` for every
162
+ space (see above). The same applies to any expression field: no spaces, anywhere.
163
+ - **Filter on ids, not names.** Names in lookup tables are free-text imported from NetSuite and
164
+ can be renamed without the id changing, so a `name LIKE` match is the fragile choice. Proven on
165
+ Elite: filtering root classes by `name LIKE '%Services%'` left 94 items visible instead of the
166
+ correct 82, because "Advisory and Modernization" contains no "Services".
132
167
  - **Operator allowlist.** `parseOptionsWhere` whitelists the operators above and rejects unknown
133
168
  ones (e.g. `regexp` returns a 500) — see the Compass cost-centers doc. Numeric/regex filtering
134
169
  that the grammar can't express must be done client-side or by adding an operator to api2.
135
170
 
136
171
  ## Change history
137
172
 
173
+ - 2026-09-17 — **Recorded that subqueries ARE possible in a stored `apiWhereClause`**: the parser
174
+ splits on literal spaces, so writing `/**/` in place of every space (MySQL reads it as
175
+ whitespace, the parser sees no space) lets an arbitrary correlated subquery act as the field
176
+ side of a condition. Confirmed the parser mechanics by reading `parseOptionsWhere` and
177
+ simulating it in PHP — commas split only at matching paren depth, the field side passes to SQL
178
+ unescaped, and `notin` splits its value on `:` so `:notin:1:22:40` yields the IN-list (1,22,40).
179
+ Noted the existing prod precedent (Elite's `item-fulfillments-for-sales-orders` clause already
180
+ uses `CONCAT((SELECT ...))` this way), and added the "filter on ids, not names" gotcha —
181
+ name-matching left 94 Elite items visible instead of 82. No code change. (rgirish)
138
182
  - 2026-09-01 — Recorded that the parser **strips one leading `(` and one trailing `)` from the whole
139
183
  clause before splitting**, so the wrapped single-condition form `(Items.catalogId:eq:1)` is safe
140
184
  and is the house convention (this was previously only documented for the multi-condition form).
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-04
9
+ updated: 2026-09-17
10
10
  owners: [tcox, bala, rgirish]
11
11
  files:
12
12
  - dbchanges2/Client_Elite/
@@ -14,6 +14,7 @@ files:
14
14
  - dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql
15
15
  - dbchanges2/Client_Elite/2026-08-13a - InventoryGroupingsSurfaceOverrides.sql
16
16
  - dbchanges2/Client_Elite/2026-09-04b - EliteInventoryHideServiceItems.sql
17
+ - dbchanges2/Client_Elite/2026-09-15a - InventoryHideServiceClassItems.sql
17
18
  - dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql
18
19
  - dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
19
20
  - dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql
@@ -25,6 +26,8 @@ related:
25
26
  - ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
26
27
  - ../../../2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md
27
28
  - ../../../2.0/apps/_underscore/features/page-meta-context-field-settings.md
29
+ - ../../../1.0/apps/library/features/netsuite-item-class-sync.md
30
+ - ../../../2.0/apps/_underscore/features/item-classification-itemclasses.md
28
31
  ---
29
32
 
30
33
  ## Summary
@@ -276,33 +279,106 @@ the live Units view is `inventory_units` (`TableViews` id 21), not
276
279
  This pairs with the existing team rule that **a committed migration records intent, not deployed
277
280
  state** — the frontend repo records the *default*, not this client's state.
278
281
 
279
- ## Hiding service lines from Inventory — `2026-09-04b`
282
+ ## Hiding service lines from Inventory — `2026-09-04b`, replaced by `2026-09-15a`
280
283
 
281
- `2026-09-04b - EliteInventoryHideServiceItems.sql` sets `TableViews.apiWhereClause`:
284
+ > **⚠ Current rule (2026-09-15): service items are hidden by their ROOT ItemClass, not by the
285
+ > `SVC-` partNumber prefix.** `2026-09-15a - InventoryHideServiceClassItems.sql` rewrites the
286
+ > `inventory_items` clause. The 2026-09-04b history below is kept because it explains the
287
+ > `IFNULL(...)` NULL-drop rule, which still applies.
288
+
289
+ `2026-09-15a - InventoryHideServiceClassItems.sql` rewrites `TableViews.apiWhereClause` for slug
290
+ `inventory_items` in `Client_Elite`. It walks the `ItemClasses` parent chain and excludes any item
291
+ whose **root** ItemClass id is **1** (Lifecycle Services), **22** (Advisory and Modernization) or
292
+ **40** (True Partner Services), using a correlated subquery written with `/**/` as whitespace —
293
+ see [apiWhereClause row filtering](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md#subqueries-are-possible--use--in-place-of-every-space)
294
+ for the technique and the parser mechanics.
295
+
296
+ **No `_underscore` deploy is needed.** The whole check lives in the stored clause. An earlier draft
297
+ required a `_isServiceClass` model field on `_Model_Elite_Item`; **that field does not exist and is
298
+ not needed** — do not add one.
299
+
300
+ ### Why the class check replaced the `SVC-` prefix
301
+
302
+ The `SVC-` prefix was an unreliable signal. Verified on **prod `Client_Elite`** (2026-09-15):
303
+
304
+ | Measure | Count |
305
+ |---|---|
306
+ | Items total | 122 |
307
+ | Service items by root ItemClass | 40 |
308
+ | Service items by `SVC-` prefix | 27 |
309
+ | Visible after the class filter | 82 |
310
+
311
+ The class check catches **18 services the prefix missed** — Cisco `CON-SNT-*` / `CON-ROB-*`
312
+ contracts, `LIC-ENT-*` licenses, `CarePacks - Fixed`, `PROJECT - CABLING`, `CONFIG/INSTALL` — with
313
+ **zero false hides**.
314
+
315
+ ### Match on ids, never on class NAMES
316
+
317
+ Hardcoded ids 1 / 22 / 40 are the stable reference. Names are free-text imported from NetSuite and
318
+ can be renamed without the id changing. Proven wrong in practice here: filtering on
319
+ `name LIKE '%Services%'` returned **94** visible instead of the correct **82**, because "Advisory
320
+ and Modernization" contains no "Services".
321
+
322
+ ### `Client_Elite` ItemClasses hierarchy (prod, verified 2026-09-15)
323
+
324
+ The table is **`ItemClasses`**, not `ItemClassifications`. Exactly **4 root rows**
325
+ (`parentItemClassId IS NULL`):
326
+
327
+ | id | Name | Kind |
328
+ |---|---|---|
329
+ | 1 | Lifecycle Services | service |
330
+ | 4 | Technology Sales | product |
331
+ | 22 | Advisory and Modernization | service |
332
+ | 40 | True Partner Services | service |
333
+
334
+ Tree depth is exactly **5 levels**, and every 5-level chain terminates at a true root (a query for
335
+ level-5 classes with a non-null parent returned 0 rows). So the migration's fixed 5-alias
336
+ `LEFT JOIN` walk resolves every item to its true root **today**. See
337
+ [the hierarchy import](../../../1.0/apps/library/features/netsuite-item-class-sync.md).
338
+
339
+ ### Unclassified items stay visible — and 5 real services leak
340
+
341
+ **29 Items have `itemClassId` NULL.** The clause is a blocklist, so unclassified items stay
342
+ **visible** — the safer default, since hiding stock people expect to see is worse than showing a
343
+ few extra lines. But 5 of those 29 are real services by name and will newly appear in Inventory:
344
+
345
+ - `SVC-FS-SHIPPING-RECLAIM-LAPTOP`
346
+ - `SVC-FS-SHIPPING-SYSTEM SWAP-LAPTOP`
347
+ - `SVC-FS-DEPLOY-GW`
348
+ - `SVC-FS-SHIPPING-2DAY`
349
+ - `SVC-FS-ELITE-PERIPHERAL`
350
+
351
+ **Open decision left with the developer:** either set an ItemClass on those 5 in NetSuite, or AND
352
+ the old `SVC-` prefix check back into the clause.
353
+
354
+ ### The previous rule — `2026-09-04b` (superseded for `inventory_items`)
355
+
356
+ `2026-09-04b - EliteInventoryHideServiceItems.sql` set:
282
357
 
283
358
  | View | Clause |
284
359
  |---|---|
285
- | `inventory_units` | `(IFNULL(Items.assetTypeId,0):ne:2)` |
286
- | `inventory_items` | `(Items.partNumber:excludes:SVC-,IFNULL(Items.assetTypeId,0):ne:2)` |
360
+ | `inventory_units` | `(IFNULL(Items.assetTypeId,0):ne:2)` — **still in force** |
361
+ | `inventory_items` | `(Items.partNumber:excludes:SVC-,IFNULL(Items.assetTypeId,0):ne:2)` — replaced |
287
362
 
288
- `assetTypeId 2` = Elite's `Services` row. Notes for anyone porting this:
363
+ `assetTypeId 2` = Elite's `Services` row. Notes that still apply to anyone porting this:
289
364
 
290
365
  - **The filter belongs in the database table view, not the frontend** — an explicit scope decision.
291
366
  - **`IFNULL(...,0):ne:2` is mandatory, not cosmetic.** A bare `:ne:2` drops every `NULL` row and
292
- would hide **all unstamped items** — and 110 of Elite's 113 items are unstamped until the cursor
293
- backfill runs. The shape is copied from Elite's existing `item-fulfillments-for-sales-orders`
294
- view. Grammar and the NULL-drop rule:
367
+ would hide **all unstamped items**. The shape is copied from Elite's existing
368
+ `item-fulfillments-for-sales-orders` view. Grammar and the NULL-drop rule:
295
369
  [apiWhereClause row filtering](../../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md).
296
- - **The `SVC-` prefix rule is kept on `inventory_items`** because it still catches the three items
297
- NetSuite mis-types as `InvtPart` (`SVC-TS-ELITE-HDONBOARDING`, `CONFIG/INSTALL`,
298
- `CONF-RM-INSTALL-SUPP`) — see
299
- [netsuite-togasupply-sync](./netsuite-togasupply-sync.md).
300
370
  - **Both `UPDATE`s are guarded on the current value**, so a re-run is a no-op.
301
371
  - `inventory_units` was targeted **because of the `SurfaceOverrides` finding above** — filtering
302
372
  view 17 would have had no visible effect for Elite.
303
373
 
304
374
  ## Gotchas / known issues
305
375
 
376
+ - **A 6th NetSuite class level would silently leak services back into Inventory.** The
377
+ `2026-09-15a` clause walks a **fixed 5-alias** chain because Elite's tree is exactly 5 deep
378
+ today — but nothing in the schema caps the depth. If NetSuite adds a level under a service root,
379
+ those items reappear in Inventory with no error. Re-check the depth whenever the classification
380
+ tree changes.
381
+
306
382
  - **A `Client_*` DB that predates a platform migration fails silently until a user opens the
307
383
  view.** Nothing validates `TableViewJoins.parentRecordFieldId` against live `Core.RecordFields`,
308
384
  so a deleted id sits there until it 500s. Worth checking for **any** client onboarded before
@@ -312,6 +388,18 @@ state** — the frontend repo records the *default*, not this client's state.
312
388
  [error-reporting-issue-event](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md).
313
389
 
314
390
  ## Change history
391
+ - 2026-09-17 — **Replaced the `SVC-` partNumber prefix filter with an ItemClass-hierarchy check**
392
+ on the `inventory_items` view (`2026-09-15a - InventoryHideServiceClassItems.sql`): the clause
393
+ now walks the `ItemClasses` parent chain and excludes items whose ROOT class id is 1, 22 or 40,
394
+ via a `/**/`-whitespace correlated subquery. Prod numbers: 122 items, 40 service by class vs 27
395
+ by prefix — the class check catches 18 the prefix missed (Cisco CON-SNT-*/CON-ROB-*, LIC-ENT-*,
396
+ CarePacks, PROJECT - CABLING, CONFIG/INSTALL) with zero false hides; 82 visible after filtering.
397
+ **No `_underscore` deploy** — an earlier draft's `_isServiceClass` model field does not exist and
398
+ is not needed. Recorded the verified `Client_Elite` hierarchy (4 roots, exactly 5 levels, every
399
+ chain terminating at a true root) and the decision to match on **ids, not names** (a
400
+ `name LIKE '%Services%'` filter left 94 visible, not 82, because "Advisory and Modernization"
401
+ has no "Services" in it). Open item: 29 items have a NULL `itemClassId` and stay visible by
402
+ design, but 5 of them are real services and will newly appear. (rgirish)
315
403
  - 2026-09-04 — **Corrected which table view Elite's Inventory page actually reads, then filtered
316
404
  it.** `toga25-supply/src` shows the DEFAULT groupings using `inventory_items` +
317
405
  `units-for-items-for-purchase-orders`, and `inventory_units` appears nowhere in `src` — reading as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.834",
3
+ "version": "1.0.835",
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",