toga-ai 1.0.601 → 1.0.603

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: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-17
9
+ updated: 2026-08-18
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -27,6 +27,7 @@ related:
27
27
  - ../../library/features/toga2-api-client-and-bridge.md
28
28
  - ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
29
29
  - ../../../clients/elite/features/netsuite-togasupply-sync.md
30
+ - ../../../clients/compass-usa/features/sales-order-line-renumbering.md
30
31
  ---
31
32
 
32
33
  ## Summary
@@ -402,9 +403,46 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
402
403
  `itemFulfillmentItems` or `invoiceItems` reverse-hasMany collections (only the PO-link bridges), so
403
404
  the fix keys off the recorded "could-not-delete" set rather than trusting those collections — see
404
405
  [App_Api_Toga2 client & bridge](../../library/features/toga2-api-client-and-bridge.md).
406
+ - **⚠ A second, distinct poison-order variant froze SALES_ORDERS again on SO 14106 (fixed
407
+ 2026-08-18) — this one was on the api2/2.0 side, not in `library`.** Fixing the 2026-08-17
408
+ renumber-DELETE bug let Compass USA's sync advance past SO 7447 (`06:13:26`) to SO **14106**
409
+ (`06:30:41`), which then threw for a completely different reason: the api2 `PUT /sales-orders`
410
+ fired `_Model_Compass_SalesOrder::postPut()` → `_Model_Compass_SalesOrderItem::renumberLineNumbers()`,
411
+ whose **single-UPDATE renumber raised MySQL 1062 duplicate-key** on any order whose `id`-order ≠
412
+ `lineNumber`-order (SO 14106: `id 86674` line 19→6 collided with `id 86675` still on 6). The 1062
413
+ fataled the api2 request, `App_Api_Toga2::send()` threw, and — same as before — the
414
+ `try/catch`-less SALES_ORDERS loop aborted the whole section:
415
+ `NETSUITE_EXECUTION_MODE_SALES_ORDERS='1-RUNNING'`, watermark frozen at `2026-08-12 06:30:41`
416
+ (~6 days), other five sections healthy at `432000-IDLE`. **Root-cause fix is in `_underscore`
417
+ (2.0), not this worker** — the renumber is now two-phase (park negative, flip positive); full
418
+ detail in [Compass sales-order line-number renumbering](../../../clients/compass-usa/features/sales-order-line-renumbering.md).
419
+ The worker/1.0 side is only the trigger path (unchanged). **Lesson: the missing SALES_ORDERS
420
+ per-record `try/catch` means every future api2-side write error on a single order re-freezes the
421
+ section** — the poison-order class is not closed by fixing individual orders.
422
+ - **⚠ Diagnostic trap: a fatal api2 PUT leaves NO row in `Logs_<Client>.Api` — check `Logs.Issue`
423
+ instead.** When the api2 request fatals *before* the request-logging middleware (as the 1062
424
+ renumber collision does), it writes **nothing** to `Logs_Compass.Api`. Do **not** conclude "no
425
+ failing api2 call happened" from an empty per-client Api log. The error surfaces only in the shared
426
+ `Logs.Issue` tracker (here **Issue 246**, first seen 2026-08-17 22:16), with the full trace
427
+ pointing at `renumberLineNumbers ← postPut ← V2.php processRoutePairs`. Localize the stalled
428
+ section from `Client_<Client>.Parameters` (`NETSUITE_EXECUTION_MODE_*` RUNNING-vs-IDLE +
429
+ `NETSUITE_LAST_SYNC_DATETIME_*` frozen watermark), then read `Logs.Issue` — not `Logs_<Client>.Api`
430
+ — for the throwing order.
405
431
 
406
432
  ## Change history
407
433
 
434
+ - 2026-08-18 — Recorded the **next poison-order variant** that re-froze Compass USA's SALES_ORDERS
435
+ section on SO **14106** right after the 2026-08-17 fix let the sync advance past SO 7447. This one
436
+ is 2.0-side: the api2 `PUT /sales-orders` → `postPut` → `renumberLineNumbers()` **single-UPDATE**
437
+ renumber threw MySQL **1062** on any order whose `id`-order ≠ `lineNumber`-order, fataling the api2
438
+ request; the `try/catch`-less SALES_ORDERS loop aborted the section (mode `1-RUNNING`, watermark
439
+ stuck `2026-08-12 06:30:41`, ~6 days). Root-cause fix is in **`_underscore`** (two-phase renumber —
440
+ see the [renumbering doc](../../../clients/compass-usa/features/sales-order-line-renumbering.md));
441
+ this worker is only the trigger and is unchanged. Also recorded the **diagnostic trap**: the fatal
442
+ PUT wrote **no** `Logs_Compass.Api` row (it died before the logging middleware), so the failure was
443
+ visible only in shared `Logs.Issue` (Issue 246) — do not read an empty per-client Api log as "no
444
+ failing call." Reinforced that the missing SALES_ORDERS per-record `try/catch` keeps the
445
+ poison-order class open. Diagnosis only on this repo (read-only). (jcardinal)
408
446
  - 2026-08-17 — Fixed a Compass USA ~5-day SALES_ORDERS freeze: an **invoiced** line that NetSuite
409
447
  renumbered could not be DELETEd (FK `InvoiceItems.salesOrderItemId` `RESTRICT` → MySQL 1451 →
410
448
  api2 EV-11), and because the SALES_ORDERS loop has **no per-record `try/catch`** the throw aborted
@@ -35,7 +35,7 @@
35
35
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Model.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
36
36
  | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
37
37
  | [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
38
- | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
38
+ | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
39
39
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
40
40
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
41
41
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-02
9
+ updated: 2026-08-18
10
10
  owners: [jcardinal, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/ItemFulfillment.php
@@ -17,6 +17,7 @@ files:
17
17
  - dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql
18
18
  - dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql
19
19
  - dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
20
+ - dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql
20
21
  related:
21
22
  - ../architecture.md
22
23
  - ../../api2/architecture.md
@@ -55,8 +56,10 @@ fire those records' interceptors) no-op. A `while` loop walks UP one level at a
55
56
  1. Load downstream IF; no `salesOrderId` (transfer order) → return null (out of scope).
56
57
  2. Resolve upstream SO via the **header walk** (bridge tables); none → null (top of chain).
57
58
  3. Build desired upstream items: map each downstream IFI's SOI up via the **item walk**,
58
- GROUP BY upstream SOI, sum fulfilled qty, then **scale into upstream order units** for
59
- bundle decomposition (see Gotchas).
59
+ GROUP BY upstream SOI, sum fulfilled qty, then **scale into upstream order units** — but
60
+ only for genuine multi-component bundles, keyed on **distinct downstream items** (see
61
+ Gotchas → Bundle scaling). A single distinct downstream item (incl. accepted over-ship)
62
+ mirrors unscaled.
60
63
  4. Resolve-or-create the upstream IF (**eager** — header-only POST still builds the chain):
61
64
  load existing via `upstreamItemFulfillmentId` and PUT changed header fields, or POST a
62
65
  new IF with mirrored header + upstream SO then PUT the downstream IF's link.
@@ -95,9 +98,36 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
95
98
  `…Put.sql` for POST/PUT) against that env's Core DB — the PHP won't fire on PUT otherwise.
96
99
  - **`depth: -1` always** on orchestration calls. `depth => 0` means *unlimited* FK
97
100
  traversal → OOM. Never use 0.
98
- - **Bundle scaling:** `upstreamQty = totalDownstreamFulfilled × upstreamOrderedQty /
99
- totalDownstreamOrderedQty`, **capped at the raw sum** so the factor can only reduce
100
- (roll-up), never inflate. A qty-1 bundle of 6 fully-shipped components → upstream qty 1.
101
+ - **Bundle scaling is gated on DISTINCT DOWNSTREAM ITEMS, not downstream SOI lines
102
+ (corrected 2026-08-18).** A "Recipe" query, per upstream SOI, takes **one ordered-qty slot
103
+ per distinct downstream `itemId`** (MAX ordered qty across duplicate same-item sourcing
104
+ lines) and counts the downstream lines. Scaling
105
+ (`upstreamQty = totalDownstreamFulfilled × upstreamOrderedQty / Σ distinctDownstreamOrderedQty`,
106
+ **capped at the raw sum** so it can only reduce, never inflate) applies **only when
107
+ `distinctDownstreamItemCount > 1`** — a genuine multi-component bundle. A qty-1 bundle of 6
108
+ fully-shipped distinct components still → upstream qty 1 (6×1/6); 3×2/6=1 preserved. When a
109
+ single distinct downstream item is sourced (incl. an accepted over-shipment split across
110
+ several downstream SOs/SOIs), the raw fulfilled qty mirrors up **unscaled** (round() applied
111
+ symmetrically on this branch too).
112
+ - **Same-item over-shipment used to mis-scale into fractions (root cause, fixed 2026-08-18).**
113
+ Pre-fix the denominator summed ordered qty over *distinct downstream SOI lines*. A vendor
114
+ (e.g. Office Depot / ODP) can create **multiple downstream SalesOrders for the SAME item off
115
+ ONE upstream PO line** (accepted over-ship / re-send); each duplicate is another distinct
116
+ downstream SOI, inflating the denominator and shrinking the factor. Compass SA135471's hero
117
+ SOI (item 2682, qty 1) mapped to two ODP SOIs (same item, each qty 1) via one POI, so factor
118
+ = 1/2 and each of two customer IFs showed **qty 0.50** ("Quantity Fulfilled" 0.5 + 0.5). Two
119
+ physically distinct serials had shipped — a real over-ship, not a phantom duplicate. Fix:
120
+ distinct-item denominator (above) mirrors the raw qty (→ 2). Data repaired by
121
+ `dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql`.
122
+ - **Mixed topology (a real bundle whose component is ALSO sourced across multiple downstream
123
+ lines) is logged, not silently averaged.** When the Recipe's `lineCount > distinctItemCount`,
124
+ the engine `error_log()`s a diagnostic instead of averaging — same log-don't-swallow rule as
125
+ the broken-bridge guard.
126
+ - **Diagnosing over-ship vs. phantom duplicate:** traverse the bridge — upstream SOI →
127
+ `SalesOrderItems_PurchaseOrderItems` → POI → `PurchaseOrders_SalesOrders` → downstream ODP
128
+ SOs (`customerId=1`); compare downstream vs. upstream `itemId` (**same itemId ⇒ over-ship /
129
+ duplicate sourcing, NOT a bundle**); then check `ItemFulfillmentItemUnits` serials — distinct
130
+ serials ⇒ genuine over-shipment, identical ⇒ phantom duplicate.
101
131
  - **DELETE propagation is NOT implemented.** The engine sets `$outData = null` on DELETE so
102
132
  post-delete hooks never fire; a standalone downstream DELETE doesn't immediately remove its
103
133
  upstream mirror. A later reconciling PUT cleans up stale records via set comparison. Full
@@ -131,6 +161,19 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
131
161
  upstream record is written at most once — idempotent). Fine for normal fulfillment sizes.
132
162
 
133
163
  ## Change history
164
+ - 2026-08-18 — **Bundle scaling now discriminates on distinct downstream items, not SOI lines
165
+ (CTO-approved "Option B").** `reconcileUpstreamLevel`'s Recipe query takes one ordered-qty
166
+ slot per distinct downstream `itemId` and scales only when `distinctDownstreamItemCount > 1`;
167
+ a single distinct item (incl. accepted over-ship split across multiple downstream SOs)
168
+ mirrors the raw fulfilled qty unscaled. Mixed topology (recipe lineCount > distinctItemCount)
169
+ is `error_log`'d, not averaged. Added `(int)` casts on the method's trusted-PK SQL
170
+ interpolations and round() symmetry on the unscaled branch. Root-caused on Compass prod
171
+ SA135471: the hero item (2682, qty 1) sourced via two ODP SalesOrders off one PO line showed
172
+ "Quantity Fulfilled" 0.5 + 0.5 across two customer IFs (old denominator summed distinct SOI
173
+ lines → factor 1/2); two distinct serials confirmed a genuine over-ship. Data repaired by the
174
+ idempotent retro migration `dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql`
175
+ (IFI 302121 / 302126 quantity 0.50 → 1.00). Deploy order: `_underscore` code first, then the
176
+ SQL against the Compass DB. Neither deployed at capture time. (jcardinal)
134
177
  - 2026-07-02 — Added a **broken-bridge guard** to `reconcileUpstreamLevel`: when `$desiredItems`
135
178
  is empty but the downstream IF has items (broken SOI↔POI bridge), it logs and returns null
136
179
  instead of creating an empty header-only duplicate upstream IF. Root-caused on Compass SA133377
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-11
10
- owners: ["bala"]
9
+ updated: 2026-08-18
10
+ owners: ["bala", "jcardinal"]
11
11
  files:
12
12
  - _underscore/Model/Compass/SalesOrderItem.php
13
13
  - _underscore/Model/Compass/SalesOrder.php
@@ -43,6 +43,29 @@ Live production wiring (`ApiPayloadInterceptors`, Compass):
43
43
  Add/edit renumbering rides the **parent** (`sales-orders`) hooks; delete renumbering rides the
44
44
  **child**'s `preDelete`.
45
45
 
46
+ ### How the renumber SQL must run — two phases, never one UPDATE (2026-08-18)
47
+
48
+ Lines are numbered `1..N` by `ROW_NUMBER() OVER (ORDER BY id)`, but the write **cannot** be a
49
+ single `UPDATE … JOIN (SELECT id, ROW_NUMBER() … AS rowNumber) SET lineNumber = rowNumber`.
50
+ `SalesOrderItems` carries `UNIQUE(salesOrderId, lineNumber)`, which MySQL enforces **per row as
51
+ each row updates**, not deferred to statement end. Whenever `id`-order ≠ current `lineNumber`-order,
52
+ a row that takes a number another **not-yet-updated** row still holds raises **MySQL 1062
53
+ duplicate-key** mid-statement and the whole UPDATE (and its request) fatals.
54
+
55
+ > Observed: SO **14106**, `id 86674` moving line 19→**6** collided with `id 86675` still sitting on
56
+ > line 6 → `Duplicate entry '14106-6' for key 'salesOrderId'`.
57
+
58
+ The collision-free form (same numbering outcome, `ROW_NUMBER() OVER (ORDER BY id)` unchanged) is a
59
+ **two-phase** renumber:
60
+
61
+ 1. **Park negative** — `SET lineNumber = -Temporary.rowNumber` for every line. Negatives never
62
+ collide with the positive originals or finals, and each `-rowNumber` is distinct.
63
+ 2. **Flip positive** — `SET lineNumber = -lineNumber WHERE … AND lineNumber < 0`.
64
+
65
+ Values only ever range `-N..N`, safe for the `SMALLINT` `lineNumber` column. Both UPDATEs run in
66
+ the **same api2 request → same `DB_CLIENT` transaction**, so the intermediate negative state is
67
+ never externally visible and the pair rolls back together on failure. Signature unchanged (PHP 8.1).
68
+
46
69
  ## Why delete-time renumbering CANNOT be a `postDelete`
47
70
 
48
71
  A `postDelete` interceptor can never fire at all. api2's post-processing interceptor block runs
@@ -87,9 +110,42 @@ fire reliably as the request's **first-resolved record**. Verified healthy after
87
110
  subclasses inherit it. Do not add per-region copies.
88
111
  - **These interceptor rows have no route and no audit trail.** They are direct SQL only; nothing
89
112
  records who added or removed one. Snapshot Compass's row set into the ticket before changing it.
113
+ - **⚠ The renumber must never be a single collision-prone UPDATE.** `UNIQUE(salesOrderId,
114
+ lineNumber)` is enforced per-row mid-statement, so a one-shot `SET lineNumber = rowNumber` throws
115
+ MySQL 1062 the moment any row takes a number a not-yet-updated row still holds. Renumber in two
116
+ phases (park negative, then flip positive) — see *How it works*.
117
+ - **Both Compass regions are affected.** `renumberLineNumbers()` lives on the shared
118
+ `_Model_Compass_SalesOrderItem` base, so **Compass Canada** (`compass-canada`) inherits the same
119
+ bug and the same fix; there is no per-region copy to touch.
120
+ - **⚠ The `preDelete` (delete-a-middle-line) path is a pre-existing latent collision — unchanged by
121
+ this fix.** When a middle line is deleted, the row being removed keeps its positive `lineNumber`
122
+ during the renumber, so `preDelete` can still hit the same 1062 as the old add/edit code did. It
123
+ is healthy in practice only because deletes are almost always the **last** line. If middle-line
124
+ deletes become common, `preDelete` needs the same two-phase treatment.
125
+ - **This was not a one-order anomaly.** As of 2026-08-18, **38,947 of 105,169** Compass orders (37%)
126
+ have `lineNumber`-order ≠ `id`-order (`ROW_NUMBER() OVER (PARTITION BY salesOrderId ORDER BY id)`)
127
+ — any of them ran the buggy renumber on its next PUT/edit and could collide. No mass data retrofix
128
+ is needed for the freeze: the two-phase renumber normalizes each order as the sync re-touches it.
129
+ A post-deploy sweep of that set could confirm whether any were left with gap'd numbering (which
130
+ MITS/PO linking depends on — see [MITS PO → SO item linking](./mits-po-to-so-item-linking.md))
131
+ from silently-rolled-back renumbers.
90
132
 
91
133
  ## Change history
92
134
 
135
+ - 2026-08-18 — **Fixed** the `renumberLineNumbers()` single-UPDATE **1062 duplicate-key collision**.
136
+ The renumber ran as one `UPDATE … JOIN (SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rowNumber)
137
+ SET lineNumber = rowNumber`; because `UNIQUE(salesOrderId, lineNumber)` is enforced per-row
138
+ mid-statement, any order whose `id`-order ≠ `lineNumber`-order fataled with `Duplicate entry`
139
+ (observed SO 14106, `id 86674` line 19→6 vs `id 86675` still on line 6). Replaced with a
140
+ **two-phase** renumber (Phase 1 parks each line at `-rowNumber`, Phase 2 flips positive) that keeps
141
+ the exact `ROW_NUMBER() OVER (ORDER BY id)` outcome, never collides, stays within the `SMALLINT`
142
+ range, and runs both UPDATEs in the same api2 `DB_CLIENT` transaction (atomic, no visible negative
143
+ state). Shared base class → **Compass Canada inherits the fix**. Systemic scope: 38,947/105,169
144
+ (37%) Compass orders currently have out-of-order numbering and were all exposed. Noted the
145
+ pre-existing `preDelete` middle-line-delete path can still collide identically (unchanged, latent).
146
+ This collision was the root cause of the Compass USA SALES_ORDERS sync freeze on SO 14106 — cascade
147
+ + diagnosis recorded in the [NetSuite → TOGa Supply per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
148
+ (Fix confirmed live in prod; local working tree still shows the pre-fix single UPDATE.) (jcardinal)
93
149
  - 2026-08-11 — Documented the as-built wiring and the reasoning behind it after a production
94
150
  post-mortem (investigation only, no code change): add/edit renumbering hangs off
95
151
  `_Model_Compass_SalesOrder::postPost`/`postPut` (record **14** `sales-orders`), delete-time
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.601",
3
+ "version": "1.0.603",
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",