toga-ai 1.0.753 → 1.0.754

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-01
9
+ updated: 2026-09-02
10
10
  owners: [dfranks, jcardinal, kyalamarthi]
11
11
  files:
12
12
  - library/app/api/netsuite/rest.php
@@ -98,6 +98,21 @@ Forecast2 tables already store.
98
98
  - **TO_DATE rejects impossible dates** (e.g. `2026-06-31`) with an opaque HTTP 400 — validate
99
99
  calendar dates before building SuiteQL.
100
100
 
101
+ - **⚠ SuiteQL returns date/datetime columns in the account DISPLAY format (here `M/D/YYYY`,
102
+ date-only) unless you wrap them in `TO_CHAR` — losing the time is silent (2026-09-02).** A bare
103
+ `SELECT lastmodifieddate` came back `9/2/2026` (no time, locale-dependent order), while
104
+ `TO_CHAR(lastmodifieddate,'YYYY-MM-DD HH24:MI:SS')` gave `2026-09-02 17:39:21`. Any shim that
105
+ **stores or compares a NetSuite date STRING** must `TO_CHAR(col,'YYYY-MM-DD HH24:MI:SS')` it, or it
106
+ drops the time and inherits the account's locale format. This defeated the per-client sync's
107
+ per-record resume cursor: it stored `9/2/2026|<id>`, sat at midnight, and re-scanned the whole day
108
+ every run — fixed by `TO_CHAR`-ing `lastmodifieddate AS lastmodifieddate` in the 8 cursor-feeding
109
+ SELECTs (`listSalesOrders`, `fetchSalesOrderByIdFull`, `listSales`, `listPurchaseOrders`,
110
+ `fetchPurchaseOrderById`, `listItemReceipts`, `listItemFulfillments`, `listInventoryAdjustments`).
111
+ `WHERE`/`ORDER BY` are unaffected by the alias — bound via `TO_TIMESTAMP` — so they still use the base
112
+ column. **Still latent:** `listOpportunities` and `listItems` return raw date-only `lastmodifieddate`
113
+ into their shims; harmless only because neither feeds a resume cursor yet. Engine-side detail on the
114
+ [per-client-sync doc](../../worker/features/netsuite-togasupply-per-client-sync.md).
115
+
101
116
  - **The line SuiteQL must SELECT the line location or it is always NULL — the shim reads it but
102
117
  didn't select it.** `listSalesOrders()`'s line query now selects `tl.location` and
103
118
  `BUILTIN.DF(tl.location) AS location_name`. The line-shim assembly code already *read* those two
@@ -275,6 +290,12 @@ None — uniform across clients (NetSuite is a single shared account).
275
290
 
276
291
  ## Change history
277
292
 
293
+ - 2026-09-02 — Recorded that **SuiteQL returns date columns in the account DISPLAY format (date-only
294
+ `M/D/YYYY` here) unless wrapped in `TO_CHAR`** — a bare `lastmodifieddate` silently drops the time and
295
+ is locale-dependent. This defeated the per-client sync's per-record resume cursor (stored `9/2/2026|id`,
296
+ sat at midnight, re-scanned the whole day); fixed by `TO_CHAR(...,'YYYY-MM-DD HH24:MI:SS') AS
297
+ lastmodifieddate` in the 8 cursor-feeding SELECTs. `listOpportunities`/`listItems` still return raw
298
+ date-only but are latent (no cursor). (jcardinal)
278
299
  - 2026-09-01 — Recorded that **`listPurchaseOrders()` scoped POs by the VENDOR header `entity`** (via
279
300
  the reused `buildEntityInFilter()`), silently dropping every drop-ship / special-order PO for all
280
301
  togasupply clients since the SOAP→REST migration. A PO header `entity` is the vendor, not the
@@ -122,6 +122,35 @@ rename migration run on every client DB first (see the deploy-order gotcha below
122
122
  the same guarded `UPDATE ... WHERE key LIKE 'NETSUITE_LAST_SYNC_DATETIME_%'` so future new clients land on
123
123
  CURSOR keys. Still uncommitted in the working tree.
124
124
 
125
+ ### ⚠ The cursor was storing a date-ONLY, US-format value — the same-second tiebreak was dead (FIXED + DEPLOYED 2026-09-02)
126
+
127
+ The per-record cursor "advanced" but its datetime half was `M/D/YYYY` with **no time** (e.g.
128
+ `9/2/2026|6302323`), so it defeated its own same-second id tiebreak: the cursor sat at **midnight**,
129
+ which no record's real timestamp equals, so every run re-fetched the **whole day** — the resume was
130
+ defeated even while it looked healthy. Root cause: the SuiteQL SELECTs in
131
+ `library/app/api/netsuite/rest.php` returned `lastmodifieddate` **raw**, and NetSuite hands that column
132
+ back in the account **display** format (here `M/D/YYYY`, date-only). Proven: `SELECT lastmodifieddate`
133
+ → `9/2/2026`, but `TO_CHAR(lastmodifieddate,'YYYY-MM-DD HH24:MI:SS')` → `2026-09-02 17:39:21`. See the
134
+ [SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md) for the general rule.
135
+
136
+ - **Fix part 1 (`rest.php`):** wrapped the select as
137
+ `TO_CHAR(lastmodifieddate, 'YYYY-MM-DD HH24:MI:SS') AS lastmodifieddate` in the **8** SuiteQL SELECTs
138
+ that feed a cursor shim — `listSalesOrders`, `fetchSalesOrderByIdFull`, `listSales`,
139
+ `listPurchaseOrders`, `fetchPurchaseOrderById`, `listItemReceipts`, `listItemFulfillments`,
140
+ `listInventoryAdjustments`. `WHERE`/`ORDER BY` still use the **base** `lastmodifieddate` column (the
141
+ alias doesn't affect them) — the query bounds were already correct ISO via
142
+ `TO_TIMESTAMP('YYYY-MM-DD HH24:MI:SS')`.
143
+ - **Fix part 2 (`common_sync_togasupply.php`):** normalized the datetime half at the **6** per-record
144
+ cursor PUTs to canonical `Y-m-d H:i:s` via `strtotime()` + `date()` (falls back to the raw string if
145
+ `strtotime` fails). The empty-window `"<windowTop>|0"` PUT was already canonical
146
+ (`App_Date::convertToSQLDate`).
147
+ - **Old date-only cursors self-heal.** `strtotime('9/2/2026')` parses to midnight (one-time re-scan of
148
+ that day), then the next successful record overwrites with the full timestamp.
149
+ - **LATENT (noted, not fixed):** `listOpportunities` and `listItems` in `rest.php` also return raw
150
+ date-only `lastmodifieddate` into their shims — harmless today because neither feeds a resume cursor,
151
+ but the same trap if either ever becomes one.
152
+ - Deployed 2026-09-02 (worker + library re-deploy; both files had changed).
153
+
125
154
  ### ⚠ Per-record isolation is SECTION-SPECIFIC — and SALES_ORDERS has NONE (corrected 2026-08-17)
126
155
 
127
156
  Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
@@ -1095,6 +1124,14 @@ the code deploy).
1095
1124
 
1096
1125
  ## Change history
1097
1126
 
1127
+ - 2026-09-02 — **Per-record cursor was storing a date-ONLY, US-format value — FIXED + DEPLOYED (shared:
1128
+ `worker` + `library`).** The datetime half was `M/D/YYYY` with no time (e.g. `9/2/2026|6302323`), so the
1129
+ cursor sat at midnight and the same-second id tiebreak never matched — every run re-fetched the whole day.
1130
+ Root cause: `rest.php` SELECTed `lastmodifieddate` raw and NetSuite returns it in the account display format
1131
+ (date-only here). Fixed by `TO_CHAR(lastmodifieddate,'YYYY-MM-DD HH24:MI:SS') AS lastmodifieddate` in the 8
1132
+ cursor-feeding SuiteQL SELECTs, plus normalizing the 6 per-record cursor PUTs to canonical `Y-m-d H:i:s`
1133
+ (`strtotime`+`date`, raw fallback). Old cursors self-heal (midnight → one re-scan → full timestamp).
1134
+ `listOpportunities`/`listItems` share the raw-date trap but are latent (no cursor). (jcardinal)
1098
1135
  - 2026-09-02 — **Per-record resume cursor DEPLOYED to prod (shared: `worker` + `library` + `dbchanges2`).**
1099
1136
  Now live: worker commit `06e8f9ef` + library commit `3a70b59c` (tips of `_production`); rename migration
1100
1137
  `Client/2026-09-02b - NetsuiteSyncCursorRename.sql` run on all client DBs **before** the code. Recorded the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.753",
3
+ "version": "1.0.754",
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",