toga-ai 1.0.787 → 1.0.789

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-09-08
9
+ updated: 2026-09-09
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -257,6 +257,45 @@ again, the sections still could **not advance** — the value was in the **wrong
257
257
  This is the same SuiteQL-vs-REST clock trap as the date-only-format bug above — see the
258
258
  [SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md).
259
259
 
260
+ ### ⚠ …and the cursor was READ on the wrong clock too — `strtotime()` (Chicago) shifted the fetch window +1h → SILENT record skips across ALL 6 sections + ALL clients (found + fixed 2026-09-09, NYCHH prod)
261
+
262
+ The deepest of the cursor-clock bugs, and the one that actually **dropped records that never came
263
+ back**. The two clock bugs above are on the **write** side of the cursor; this one is on the
264
+ **read** side of the same value, and it was silent — no crash, no alert, no frozen section. It
265
+ hit **every section and every client**, not just the by-reference ones.
266
+
267
+ - **Root cause — read clock ≠ fetch clock.** The cursor value `"<lastmoddate>|<netsuiteId>"` stores
268
+ its datetime in the NetSuite account timezone = **America/New_York (Eastern)** — the same clock the
269
+ list-fetch `WHERE` bounds render in (`App_Api_Netsuite_Rest::listItemFulfillments` et al. call
270
+ `->setTimezone('America/New_York')`). But `startModeIteration()`
271
+ (`common_sync_togasupply.php` ~L622) read it back with **`strtotime()`**, which parses in the
272
+ **script timezone America/Chicago** (set in library `_.php`). Chicago is 1h behind Eastern, so the
273
+ same digits produced an epoch **1 hour later**; the list fetch then re-rendered that bound in
274
+ Eastern, shifting the window's **lower bound +1 hour**.
275
+ - **Effect — a silent fetch-level skip.** Any record whose Eastern lastmod fell in that **1-hour
276
+ shadow** right after the previous checkpoint was **excluded from the fetch**; the window read empty,
277
+ and the `|0` empty-window jump advanced the cursor **past** it. Nothing threw — a throw still
278
+ freezes the section loudly; this is the quiet path that loses data. Combined with the giant-record
279
+ 900s timeouts (below) that historically froze sections and were then manually stepped over, the
280
+ gaps compounded over time.
281
+ - **Proven on NYCHH prod** (item fulfillment `6138135`, lastmod **2025-09-04 17:26:58 Eastern**): the
282
+ +1h bound (`> 17:29:03`) returned **0** records; the correct bound (`> 16:29:03`) returned **1**
283
+ (that record). The cursor trace showed the `|0` empty-window jump right over its time slot.
284
+ - **Fix (worker only, deployed + verified):** parse the cursor as Eastern in `startModeIteration` —
285
+ `$epochLastIntegration = (new \DateTime($dtLastIntegration, new \DateTimeZone('America/New_York')))->getTimestamp();`
286
+ — and change all **6** empty-window `|0` jump writes (~L831/938/1024/1193/1281/1369) from
287
+ `App_Date::convertToSQLDate(...)` (renders Chicago) to
288
+ `App_Date::convertToTimezone($epoch, 'Y-m-d H:i:s', 'America/New_York')`, so the **whole** cursor
289
+ pipeline — read + per-record write + empty-window write — runs on **one** clock (Eastern). DST-safe
290
+ (named IANA zone). php-reviewer + cto both **AGREE**; cto flagged (and we applied) the
291
+ empty-window-write hardening to stop a permanent 1h backward bias building up on idle runs. Verified
292
+ after setting NYCHH's fulfillment cursor to `2025-09-04 00:00:00|0`: `6138135` imported.
293
+ - **The rule (now proven THREE times — this bug + the two write-clock bugs above):** every touch of
294
+ the cursor value — the **read**, the per-record **write**, and the empty-window **write** — must use
295
+ the NetSuite account timezone (**Eastern**), never the script's Chicago clock and never the REST
296
+ GET's UTC. `strtotime()` and `App_Date::convertToSQLDate()` both use the script tz (Chicago) and are
297
+ the trap. Any manual cursor rewind value you set by hand must also be **Eastern**.
298
+
260
299
  ### ⚠ Per-record isolation is SECTION-SPECIFIC — and SALES_ORDERS has NONE (corrected 2026-08-17)
261
300
 
262
301
  Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
@@ -454,6 +493,19 @@ See the [tracking-number bridge doc](../../../2.0/apps/_underscore/features/trac
454
493
  0 of their fulfillments resolve an upstream sales order). `/units` writes and all three
455
494
  tracking-bridge models have **no** interceptor, and **DELETE fires none**.
456
495
 
496
+ - **The two `GET /sales-orders` calls use a narrow `fields` projection, NOT `depth` (2026-09-09).**
497
+ Both the sales-order lookup and the missing-SO refetch in `syncItemFulfillmentFromNetsuite`
498
+ (`toga2.php` ~L4644-4706) request only `uuid`,
499
+ `salesOrderItems.{uuid,lineNumber,quantity,c_netsuiteLineUniqueKey}`, and
500
+ `salesOrderItems.item.{uuid,partNumber}` — the only fields the line-matcher reads. They previously
501
+ sent `depth=5`, whose huge nested read (units, tracking, PO links) made api2 exceed the worker's
502
+ **900s cURL** limit and threw **before** the `ItemFulfillments` row was written, freezing the
503
+ section (`Logs.Issue` 666, `apitransaction.php:418`). SO GETs dropped 100s+ → ~1s. Same class of fix
504
+ as the 2026-09-08 inventory-adjustments `depth=5→4` change; a `fields` projection is the proven
505
+ pattern here (already used by the `/item-fulfillments` GET in this same method) and dodges the api2
506
+ max-depth FK omission. Keep new fields explicit in the projection — a needed field left out of the
507
+ list silently reads `null`, not an error.
508
+
457
509
  ### Single-tracking-number fan-out to items (318) and units (319) — corrected 2026-08-26
458
510
 
459
511
  The earlier "maintains all three bridges" claim was **only true for 317 (header)**: in prod
@@ -1387,6 +1439,30 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
1387
1439
 
1388
1440
  ## Change history
1389
1441
 
1442
+ - 2026-09-09 — **The resume cursor was READ on the wrong clock — a silent +1h window skip dropped
1443
+ records across ALL 6 sections and ALL clients (worker, deployed + verified).** `startModeIteration`
1444
+ (`common_sync_togasupply.php` ~L622) parsed the Eastern-stored cursor with `strtotime()` (script tz
1445
+ America/Chicago), so the fetch's lower bound rendered **1 hour high** and any record in that 1-hour
1446
+ shadow was excluded; the empty window then `|0`-jumped past it — no crash, no alert. Fixed by parsing
1447
+ the cursor as `America/New_York` and rewriting the 6 empty-window `|0` jump writes with
1448
+ `App_Date::convertToTimezone(..., 'America/New_York')`, so read + per-record write + empty-window
1449
+ write all run on one clock (Eastern). Proven on NYCHH IF `6138135` (Eastern 2025-09-04 17:26:58): the
1450
+ +1h bound returned 0 records, the correct bound returned 1. php-reviewer + cto AGREE; cto's
1451
+ empty-window-write hardening applied. Now the THIRD cursor-clock bug — the standing rule is every
1452
+ touch of the cursor value uses Eastern, never Chicago (`strtotime`/`convertToSQLDate`) and never the
1453
+ REST GET's UTC. See the read-clock gotcha in *How it works*. (jcardinal)
1454
+ - 2026-09-09 — **Fulfillment sales-order GET at `depth=5` hit the worker's 900s cURL timeout and froze
1455
+ the section (library, deployed + verified).** Both `GET /sales-orders` calls in
1456
+ `App_Api_Toga2::syncItemFulfillmentFromNetsuite` (`toga2.php` ~L4644-4706 — the lookup + the
1457
+ missing-SO refetch) changed from `depth=5` to a narrow `fields` projection (uuid;
1458
+ `salesOrderItems.{uuid,lineNumber,quantity,c_netsuiteLineUniqueKey}`;
1459
+ `salesOrderItems.item.{uuid,partNumber}` — the only fields the matcher reads), dropping the huge
1460
+ nested read (units, tracking, PO links) that pushed api2 past 900s and threw before the
1461
+ `ItemFulfillments` row was written (`Logs.Issue` 666, `apitransaction.php:418`). Same class as the
1462
+ 2026-09-08 inventory-adjustments depth fix; the `fields` projection is the proven pattern (already
1463
+ used by the `/item-fulfillments` GET in this method). Fulfillment SO GETs dropped 100s+ → ~1s. Both
1464
+ fixes address the NYCHH missing-fulfillment investigation; library + worker still deploy together for
1465
+ this sync path. (jcardinal)
1390
1466
  - 2026-09-08 - **A `Closed` NetSuite order never reaches the transfer-order sync** - the
1391
1467
  `status === 'Closed'` `continue` at `common_sync_togasupply.php` ~L746 sits **before** the
1392
1468
  `isTransferOrder()` branch, so `syncTransferOrderFromNetsuite`'s `case 'Closed'` was dead code and
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-04
9
+ updated: 2026-09-09
10
10
  owners: [jcardinal, mhammontree, bala, ajean]
11
11
  files:
12
12
  - Core/
@@ -72,19 +72,21 @@ team-maintained `id`s** — ask the developer for the next value before insertin
72
72
  > trailing-space quirk like `2026-06-03- BLANK_CLIENT_DATABASE.sql`). The **only** hard rule
73
73
  > is the leading `YYYY-MM-DD<letter>` so ordering holds.
74
74
 
75
- ## One file per type per build (keep change files consolidated)
75
+ ## One file per realm per turn (keep change files consolidated)
76
76
 
77
- **Prefer a single `.sql` file per database type (folder) per build.** When a session produces
78
- several changes for the same target — e.g. three separate files under `Core/`, or four under
79
- `Client/` — **consolidate them into one dated file** for that folder instead. Multiple same-type
80
- files in one build are not forbidden, but they have become too common; keep it to one per type
81
- unless they genuinely must be separate (for example, an ordering gap where another folder's file
82
- must run between them).
77
+ **Prefer a single `.sql` file per realm (folder) per turn.** A *realm* is the target database a
78
+ folder maps to — `Core`, the shared `Client`, a single tenant like `Client_True`, `Logs`, a module,
79
+ and so on. When one response produces several changes for the **same** realm — e.g. three separate
80
+ files under `Core/`, or four under `Client/` — **consolidate them into one dated file** for that
81
+ folder instead of writing several. This is a **strong preference**, not an absolute rule: multiple
82
+ same-realm files in one turn are allowed when they genuinely must be separate (for example, an
83
+ ordering gap where another folder's file must run **between** them).
83
84
 
84
- - Combine same-folder statements into one `YYYY-MM-DD<letter> - <Description>.sql`.
85
- - Different folders (types) stay in their own files — that split is required (see isolation).
86
- - The `dbchanges2-file-sprawl` hook warns when a second file of the same type is created in a
87
- session, as a reminder to consolidate.
85
+ - Combine same-realm statements into one `YYYY-MM-DD<letter> - <Description>.sql`.
86
+ - Different realms (folders) stay in their own files — that split is **required** (see isolation);
87
+ never mix two realms in one file.
88
+ - The `dbchanges2-file-sprawl` hook warns when a second file for the same realm is created in a
89
+ turn, as a reminder to consolidate.
88
90
 
89
91
  ## Folder → database mapping
90
92
 
@@ -362,12 +364,23 @@ its own header.)
362
364
  the relevant clients list `<module>` in their `_modules.txt`.
363
365
  5. Never edit or re-date an already-applied file — add a new dated file instead. Never put new
364
366
  work in a `HISTORIC` folder.
365
- 6. **Never seed a `uuid` column with MySQL `UUID()`.** The 2.0 standard requires a fully-random
366
- **v4** UUID; `UUID()` is time/MAC-based (v1) and violates it. Since a migration can't call
367
- `_String::generateUuid()`, **pre-generate a v4 UUID literal** (e.g. a v4 generator) and
368
- hardcode the literal string in the `VALUES` clause. One earlier migration
369
- (`Core/2026-05-08 … CronJobs_WorkerCleanup_insert`) used `UUID()` — that is a known violation,
370
- not a precedent to copy. First applied correctly in
367
+ 6. **Never seed a `uuid` column with MySQL `UUID()` — not even in a bulk insert.** The 2.0 standard
368
+ requires a fully-random **v4** UUID; `UUID()` is time/MAC-based (v1) and violates it. Since a
369
+ migration can't call `_String::generateUuid()`:
370
+ - **Single-row insert:** **pre-generate a v4 UUID literal** (a real v4 generator) and hardcode
371
+ the literal string in the `VALUES` clause.
372
+ - **Multi-row / bulk `INSERT … SELECT`:** use the inline `RANDOM_BYTES()` v4 expression (see
373
+ `features/rerunnable-additive-inserts.md` → *Generating a real UUID4 inline*), never `UUID()`.
374
+
375
+ **Every UUID must be freshly, fully random — never derive one from another.** Do **not** take a
376
+ known uuid and bump a digit (`…-0001`, `…-0002`, …), copy-and-tweak, or otherwise pattern them.
377
+ When a file needs several literals, generate each one independently at random. (Patterned uuids
378
+ are guessable and defeat the uniqueness guarantee.) The one intended exception is a fan-out
379
+ `Client/` grant, which **reuses the same** literal per row across tenants on purpose for safe
380
+ rollback — that is deliberate reuse, not incrementing (see the feature doc).
381
+
382
+ One earlier migration (`Core/2026-05-08 … CronJobs_WorkerCleanup_insert`) used `UUID()` — that is
383
+ a known violation, not a precedent to copy. First applied correctly in
371
384
  `Core/2026-07-21b - Insert - SprintLockScheduled CronJob.sql`.
372
385
  7. **Guard every registration/seed insert with `NOT EXISTS` (or `INSERT IGNORE`).** Rows that
373
386
  register platform behavior — `ApiPayloadInterceptors`, `Core.RecordFields`/`CustomRecordFields`,
@@ -562,8 +575,9 @@ Wrap the existing-set subquery in a **derived table** (with `DISTINCT` or `LIMIT
562
575
 
563
576
  ```sql
564
577
  -- CORRECT — the derived table snapshots the "already present" set before any insert
578
+ -- (uuid: inline RANDOM_BYTES() v4 expression, never UUID() — see rule #6 / the feature doc)
565
579
  INSERT INTO TranscriptPromptTerms (uuid, category, term, isActive, c_trueAiModelId)
566
- SELECT UUID(), t.category, t.term, t.isActive, m.mid
580
+ SELECT LOWER(CONCAT(HEX(RANDOM_BYTES(4)), '-', …)), t.category, t.term, t.isActive, m.mid
567
581
  FROM <base rows> t
568
582
  CROSS JOIN <active model ids> m
569
583
  WHERE m.mid NOT IN (
@@ -591,6 +605,15 @@ tables that `_Model_*` classes map to.
591
605
 
592
606
  ## Change history
593
607
 
608
+ - 2026-09-09 — **Two team-wide authoring changes (jcardinal).** (1) **UUIDs must be freshly, fully
609
+ random — never derived from another.** Sharpened rule #6: do not bump a digit / copy-and-tweak /
610
+ pattern uuid literals; generate each independently at random. Reaffirmed **never `UUID()` — not
611
+ even in bulk** (bulk uses the `RANDOM_BYTES()` v4 expression); fixed the doc's own
612
+ `TranscriptPromptTerms` example, which still used `UUID()`. The fan-out `Client/` same-literal
613
+ reuse is the one intended exception (deliberate reuse for safe rollback, not incrementing).
614
+ (2) **One file per realm per turn** — renamed/strengthened the consolidation section: prefer a
615
+ single `.sql` file per realm (`Core`, `Client`, `Client_<Name>`, …) per turn; a strong preference,
616
+ not absolute; different realms still split (never mix two realms in one file). (jcardinal)
594
617
  - 2026-08-18 - Added **rule 10**: a fan-out `Client/` file must never reference a
595
618
  `customRecordFieldId` or a client-specific label, because `CustomRecordFields` ids are per-client
596
619
  `AUTO_INCREMENT` - the same numeric id names a different field in every tenant and mis-wires
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-08
9
+ updated: 2026-09-09
10
10
  owners: [bala, kyalamarthi, apeterson, ajean, jcardinal]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
@@ -52,6 +52,14 @@ and `SUBSTRING('89ab', …)` picks the variant nibble. Verified working on prod
52
52
  that is *not* v4-shaped (older migrations concatenated `RANDOM_BYTES` without the version/variant
53
53
  nibbles). They are fine and must not be "fixed" — but do not use them as the template for new SQL.
54
54
 
55
+ **⚠ When a file needs several hardcoded literals, generate each one independently at random — never
56
+ derive one from another.** Do not paste one uuid and bump a digit (`…-0001`, `…-0002`, …) or
57
+ copy-and-tweak to make the next. Patterned uuids are guessable and undercut the uniqueness the column
58
+ exists to give. Generate each literal fresh from a real v4 generator. (The **one** intended exception
59
+ is the fan-out rule just below, which reuses the **same** literal per row across tenants on purpose —
60
+ deliberate reuse for a safe rollback, not incrementing.) `UUID()` is never allowed, in single-row or
61
+ bulk inserts — bulk uses the `RANDOM_BYTES()` expression above.
62
+
55
63
  ### ⚠ In a FAN-OUT grant migration, use HARDCODED uuid literals — generated uuids make rollback unsafe
56
64
 
57
65
  The generator above is right for a normal additive insert. It is **wrong for a `Client/` fan-out
@@ -310,6 +318,11 @@ concluding a migration is production-safe.
310
318
  [FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
311
319
 
312
320
  ## Change history
321
+ - 2026-09-09 — Added the rule that **every hardcoded uuid literal must be generated independently at
322
+ random** — never bump a digit / copy-and-tweak / pattern them (guessable, defeats uniqueness); the
323
+ fan-out same-literal reuse is the one intended exception. Restated **never `UUID()`, single-row or
324
+ bulk** (bulk uses the `RANDOM_BYTES()` v4 expression). Team-wide authoring change alongside
325
+ architecture rule #6. (jcardinal)
313
326
  - 2026-09-08 — Extended the **`Core.CronJobs` registration** shape with three mechanics confirmed on
314
327
  `Core/2026-09-08e - Insert QcBatch CronJob.sql`: reference `CronJobs` **unqualified** in a `Core/`
315
328
  file (a `Core.` prefix is a cross-database reference that breaks in prod); **always set
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: nychh
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-08
21
+ updated: 2026-09-09
22
22
  owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
23
23
  files:
24
24
  - dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
@@ -85,6 +85,27 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
85
85
  `NETSUITE_LAST_SYNC_CURSOR_ITEM_RECEIPTS` is stale at 2024-08-06**, so re-enabling that feed
86
86
  replays ~2 years. See
87
87
  [NYCHH NetSuite → TransferOrders import](./features/netsuite-transfer-order-import.md).
88
+ - **Missing NetSuite records (item fulfillments + others) root-caused and fixed (2026-09-09).** Two
89
+ separate bugs in the 1.0 NetSuite→TOGa Supply sync starved NYCHH imports, both deployed and verified
90
+ in prod: (1) the resume cursor was **read on the wrong clock** (`strtotime()` in Chicago vs the
91
+ Eastern-stored value), shifting the fetch window +1h and **silently skipping** any record in that
92
+ 1-hour shadow — this hit all 6 sections and all clients; and (2) the fulfillment `GET /sales-orders`
93
+ ran at `depth=5` and **timed out at 900s**, freezing the section before the fulfillment row was
94
+ written. Proven on IF `6138135` (Eastern 2025-09-04 17:26:58). Fixes and the standing "cursor is
95
+ always Eastern" rule live in the
96
+ [per-client sync feature doc](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
97
+ - **NYCHH sync scope + data-gap scale + backfill approach (2026-09-09).**
98
+ - **Scope = 22 NetSuite customers:** parent **28908** "NYC Health + Hospitals" + 21 hospital child
99
+ customers (`24145, 29273, 29276, 31584, 31910-31916, 31918-31925, 32229, 33674`).
100
+ - **Gap report (NetSuite scope vs `Client_Nychh` imported):** Item Fulfillments **2986 vs 2472**
101
+ (~514 missing, 17%); Sales+Transfer Orders 2121 NS vs 2098 TOGa (159 SalesOrders + 1939
102
+ TransferOrders — roughly complete); Invoices **2128 vs 213** (~1915 missing, 90% — but that is
103
+ mostly because the **invoices feed was toggled OFF on 2026-09-03**, not the timezone bug).
104
+ POs/Receipts/Adjustments are vendor/location-scoped (not measured) and several are toggled off.
105
+ - **Backfill approach:** prefer a **targeted re-import of the specific missing NetSuite ids** (diff
106
+ NetSuite scope vs TOGa) over a **blind cursor rewind** — a full rescan re-pulls thousands of heavy
107
+ serial records and stresses the shared NetSuite auth quota. Re-import is safe (matches on the
108
+ NetSuite internal id). Any manual cursor rewind value must be written in **Eastern**.
88
109
  - 🚩 **OPEN, not root-caused (2026-09-08): a duplicate item INSERT aborts the whole NYCHH sales-order
89
110
  sync run.** `Items` id 5429 (658-BFZH) was created and then re-inserted in the same run —
90
111
  `Duplicate entry '26-658-BFZH-1' for key 'Items.manufacturerId_partNumber_catalogId'` (EV-10). The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.787",
3
+ "version": "1.0.789",
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",