toga-ai 1.0.802 → 1.0.804

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.
@@ -70,6 +70,16 @@
70
70
  "timeout": 3000
71
71
  }
72
72
  ]
73
+ },
74
+ {
75
+ "matcher": "Edit|Write|MultiEdit",
76
+ "hooks": [
77
+ {
78
+ "type": "command",
79
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/design-fidelity-reminder.js\"",
80
+ "timeout": 3000
81
+ }
82
+ ]
73
83
  }
74
84
  ],
75
85
  "PostToolUse": [
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-13
9
+ updated: 2026-09-14
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -441,6 +441,54 @@ true all use `ZERO_DOLLAR_HOLD` detection.
441
441
  **Status: written, NOT committed and NOT deployed** as of 2026-09-08. Touches `worker` and `library`,
442
442
  so both deploy together (the standing rule for this pair).
443
443
 
444
+ ### ⚠ An OUT-OF-SCOPE transfer-order destination customer wedged INVENTORY_ADJUSTMENTS — the importer now SKIPS it (fixed + deployed 2026-09-14)
445
+
446
+ The INVENTORY_ADJUSTMENTS section froze at `1-RUNNING` (window shrinking) on NYCHH because a
447
+ transfer-order-backed fulfillment imported by its on-demand cascade hit
448
+ `POST /v2/transfer-orders` → **EV-10 "Column 'destinationLocationId' cannot be null"** every run.
449
+
450
+ - **Root cause:** on a stocking-SO transfer order the **destination** resolves from the NetSuite
451
+ **customer** → `Locations.c_netsuiteInternalCustomerId`. The offending order's customer was **not
452
+ in scope** — no client `Locations` row — so the destination was omitted and the NOT NULL insert
453
+ 1048'd. The **origin** (the warehouse) resolved fine.
454
+ - **How an out-of-scope customer gets there:** the INVENTORY_ADJUSTMENTS cascade scopes by
455
+ **warehouse**, then imports the referenced fulfillment whose sales order can belong to a
456
+ **different** customer. A shared warehouse (e.g. NYCHH's HQ) fulfills for many customers, so
457
+ out-of-scope orders follow.
458
+ - **"In the Customers table" is NOT a tenant-scope signal.** The offending customer existed in the
459
+ client's `Customers` table but had no `Locations` row. The reliable in-scope test is **"maps to a
460
+ client Location"**, not "is in Customers".
461
+ - **Fix (skip, don't force):** `App_Api_Toga2::syncTransferOrderFromNetsuite()` now `error_log`s and
462
+ `return null`s when the destination customer maps to no client Location, instead of POSTing a null
463
+ destination. Two log messages distinguish **out-of-scope customer** from **in-scope but no Location
464
+ (backfill a Location)**. Destination resolves **before** origin and the skip runs first, so a
465
+ skipped order writes nothing (no stray origin-warehouse Location row). The cascade
466
+ (`syncItemFulfillmentFromNetsuite` returns the fulfillment uuid or null;
467
+ `attachInventoryAdjustmentToItemFulfillment` treats null as nothing-to-link) skips cleanly on the
468
+ null instead of re-throwing at the fulfillment item-match. **No** empty-map guard: an empty
469
+ customer→Location map is a real warehouse-only-client state where skip-with-loud-log is correct.
470
+ Full detail on the [NYCHH transfer-order import doc](../../../clients/nychh/features/netsuite-transfer-order-import.md).
471
+
472
+ ### ⚠ A shared library function signature has THREE call sites — one is a DIRECT cron call (grep ALL repos)
473
+
474
+ The fix above added a param to `syncTransferOrderFromNetsuite()`. Only the two **library** callers
475
+ (`library/app/api/toga2.php:923` from `syncSalesOrder`, `:4984` from the `syncItemFulfillment`
476
+ cascade) were updated. The **third** caller is a **direct cron call** in this engine —
477
+ `common_sync_togasupply.php:782` (the SALES_ORDERS section) — and it still passed 7 args, so
478
+ SALES_ORDERS froze at `1-RUNNING` with `Logs.Event` issue **754** *"Too few arguments to function
479
+ `App_Api_Toga2::syncTransferOrderFromNetsuite()`, 7 passed … at least 8 expected"*. It surfaced only
480
+ when SALES_ORDERS ran; INVENTORY_ADJUSTMENTS reaches the function via the library-internal callers.
481
+ Fixed by adding the 3rd arg at `:782`.
482
+
483
+ > **Lesson:** when changing a shared **library** function signature, **grep ALL repos** (library +
484
+ > worker) for callers. Worker crons call library classes through the autoloader with **no `require`**,
485
+ > so an import/`require` search misses the cron call sites. `php -l` on the library file also will not
486
+ > catch a stale caller in another repo.
487
+
488
+ Also corrected two stale comments in `common_sync_togasupply.php` (ITEM_FULFILLMENTS and
489
+ INVENTORY_ADJUSTMENTS sections) that claimed a per-record/per-client try/catch that no longer exists
490
+ (all such catches were removed 2026-08-27 — the sync fails loud).
491
+
444
492
  ### WARNING: transfer-order status refresh is nested INSIDE `IS_ENABLED_INTEGRATION_SALES_ORDERS`
445
493
 
446
494
  Turning sales orders off silently turns **transfer-order status updates** off too - the refresh lives
@@ -6,7 +6,7 @@ project: Library
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-13
9
+ updated: 2026-09-14
10
10
  owners: [jcardinal, bala, rgirish]
11
11
  files:
12
12
  - library/app/api/toga2.php
@@ -122,14 +122,74 @@ full evidence table (location 75 → warehouse 117, the only `allowSending` sour
122
122
  locations carry a customer id, 7 carry a location id) is in
123
123
  [NYCHH TransferOrder → NetSuite push](./transfer-order-netsuite-push.md).
124
124
 
125
- > **⚠ OPEN, not fixed (2026-09-13) — a transfer order's DESTINATION location does not resolve, and
126
- > it is now collapsing the INVENTORY_ADJUSTMENTS section.** `TransferOrders.destinationLocationId` is
127
- > also NOT NULL. The INVENTORY_ADJUSTMENTS section (window shrinking, stuck `RUNNING`) runs an
128
- > on-demand cascade that imports a transfer-order-backed fulfillment, and the
129
- > `POST /v2/transfer-orders` for **TO 277871** (`c_netsuiteInternalSalesOrderId` **7102750**) fails
130
- > **EV-10 "Column 'destinationLocationId' cannot be null"** the **origin** resolved but the
131
- > **destination** did not. This is a **separate** destination-resolution bug from the origin
132
- > fallback above (and from the 128829 no-location edge case). Next task; not yet root-caused.
125
+ ### RESOLVED (2026-09-14) — an OUT-OF-SCOPE destination customer was wedging INVENTORY_ADJUSTMENTS; the importer now SKIPS such transfer orders
126
+
127
+ `TransferOrders.destinationLocationId` is also NOT NULL, and the INVENTORY_ADJUSTMENTS section was
128
+ stuck `RUNNING` (window shrinking) because `POST /v2/transfer-orders` for **TO 277871**
129
+ (SO **7102750** / tranId **277871**) failed **EV-10 "Column 'destinationLocationId' cannot be null"**
130
+ every run — origin resolved, destination did not.
131
+
132
+ **Root cause (proven in prod):** the destination resolves from the NetSuite **customer**
133
+ `Locations.c_netsuiteInternalCustomerId`. TO 277871's customer (entity) is **3997 "1896 Laurel Hill
134
+ Management, LLC"** — **not** a NYCHH customer, no parent in NetSuite, and **no row in
135
+ `Client_Nychh.Locations`**. So the destination could not resolve, `destinationLocation` was omitted,
136
+ and the NOT NULL insert 1048'd. The **origin** resolved fine (line location 6 "HQ" → warehouse
137
+ Location 78). With fail-loud + no per-record try/catch, the one bad record threw every run and shrank
138
+ the adaptive window → the whole section wedged.
139
+
140
+ **Why an out-of-scope customer reaches NYCHH at all:** the INVENTORY_ADJUSTMENTS cascade scopes by
141
+ **warehouse** (an adjustment at the shared **HQ** warehouse, loc 6, maps to NYCHH), then imports the
142
+ referenced fulfillment whose sales order belongs to a **different** customer. HQ fulfills for many
143
+ customers, so more out-of-scope orders follow.
144
+
145
+ > **Key data fact — "in the Customers table" is NOT a tenant-scope signal.** Customer 3997 DOES exist
146
+ > in `Client_Nychh.Customers` (id 19) but has **no Location**. The reliable in-scope test is **"maps
147
+ > to a client Location"** (`c_netsuiteInternalCustomerId` on a `Locations` row), not "is in
148
+ > Customers".
149
+
150
+ **Fix (developer decision "skip out-of-scope TOs"; CTO AGREE, php-reviewer clean, cso SAFE TO SHIP):**
151
+ `App_Api_Toga2::syncTransferOrderFromNetsuite()` now **SKIPS** (`error_log` + `return null`) when a
152
+ stocking-SO transfer order's destination customer maps to no client Location, instead of POSTing a
153
+ null destination. Two distinct log messages: **"out-of-scope customer"** vs **"in-scope but no
154
+ Location (backfill a Location)"**. A new 3rd param
155
+ `$lookupCustomerByNetsuiteCustomerInternalId` is used **only** to classify the log line.
156
+
157
+ - **Destination resolves BEFORE origin, and the skip runs first** (cso refinement) — a skipped order
158
+ writes nothing, so no stray origin-warehouse `Location` row is left behind.
159
+ - **Cascade skips cleanly on the null return.** `syncItemFulfillmentFromNetsuite` returns the
160
+ fulfillment uuid on a real import and **null** when not imported;
161
+ `attachInventoryAdjustmentToItemFulfillment` treats null as "nothing to link" (no throw) instead of
162
+ the old "still not found" throw that would just re-wedge the crash at the fulfillment item-match.
163
+ - **No `!empty(customer→Location map)` guard** (cso refinement) — the map is built in the cron's
164
+ up-front LOOKUPS phase and a failure there aborts the whole cron, so by sync time the map has
165
+ loaded. An empty map is a **real** state (a warehouse-only client) where skip-with-loud-log is
166
+ correct and freezing is wrong.
167
+
168
+ **VERIFIED in prod:** after the library deploy the INVENTORY_ADJUSTMENTS cursor advanced past the
169
+ blocker and ran forward through most of 2026; skipped TOs logged, no more EV-10 destination wedge.
170
+
171
+ This is **separate** from the origin fallback above and from the 128829 no-location edge case.
172
+
173
+ #### Follow-on regression: the added param broke a THIRD call site → SALES_ORDERS froze at 1-RUNNING
174
+
175
+ `syncTransferOrderFromNetsuite()` has **three** callers, not two: `library/app/api/toga2.php:923`
176
+ (from `syncSalesOrder`) and `:4984` (the `syncItemFulfillment` cascade), **plus a DIRECT cron call**
177
+ in `worker/crons/toga2/netsuite/common_sync_togasupply.php:782` (the SALES_ORDERS section). The skip
178
+ fix added a param but updated only the **two library callers**; the worker cron still passed 7 args →
179
+ `Logs.Event` issue **754**: *"Too few arguments to function
180
+ `App_Api_Toga2::syncTransferOrderFromNetsuite()`, 7 passed in
181
+ `crons/toga2/netsuite/common_sync_togasupply.php` on line 789 and at least 8 expected."* It surfaced
182
+ only when SALES_ORDERS ran (cursors reset to recheck), because INVENTORY_ADJUSTMENTS reaches the
183
+ function through the library-internal callers. **Fix:** added the missing 3rd arg
184
+ `$lookupCustomerByClientUuidAndNetsuiteCustomerInternalId[$uuidClient]` to the `:782` call.
185
+
186
+ > **Lesson (recorded on the shared engine doc too):** when you change a shared **library** function
187
+ > signature, **grep ALL repos** (library + worker) for callers — worker crons call library classes
188
+ > via the autoloader with **no `require`**, so an import search misses the cron call sites.
189
+
190
+ **Deploy:** the library change (the skip) is deployed and verified over many cron cycles; the worker
191
+ change (the missing 3rd arg + two stale-comment fixes) is the current fix — **redeploy worker** to
192
+ pick it up.
133
193
 
134
194
  ## Item-fulfillment transfer-order line link
135
195
 
@@ -324,12 +384,27 @@ item receipts replays about two years unless it is rewound forward first.
324
384
 
325
385
  ## Change history
326
386
 
327
- - 2026-09-13 - **OPEN: INVENTORY_ADJUSTMENTS collapsing on an unresolved transfer-order
328
- DESTINATION.** The section's on-demand cascade imports a transfer-order-backed fulfillment, and
329
- `POST /v2/transfer-orders` for **TO 277871** (`c_netsuiteInternalSalesOrderId` **7102750**) fails
330
- **EV-10 "Column 'destinationLocationId' cannot be null"** origin resolved, destination did not.
331
- Separate from the origin fallback and the 128829 no-location case; next task, not yet root-caused.
332
- Recorded in the origin/destination section. (jcardinal)
387
+ - 2026-09-14 - **RESOLVED: the INVENTORY_ADJUSTMENTS destination wedge.** Root cause (prod): TO 277871
388
+ (SO 7102750) has customer **3997 "1896 Laurel Hill Management, LLC"** — not a NYCHH customer and with
389
+ **no `Client_Nychh.Locations` row**, so the destination never resolved and the NOT NULL insert 1048'd
390
+ (EV-10) every run. It reaches NYCHH because the INVENTORY_ADJUSTMENTS cascade scopes by the shared HQ
391
+ **warehouse** (loc 6) then imports a fulfillment for a different customer. Key fact: customer 3997 IS
392
+ in `Client_Nychh.Customers` (id 19) but has no Location — "maps to a client Location" is the tenant
393
+ signal, not "is in Customers". **Fix** (skip out-of-scope TOs; CTO AGREE, php-reviewer clean, cso SAFE
394
+ TO SHIP): `syncTransferOrderFromNetsuite()` now SKIPS (error_log + return null) when the destination
395
+ customer maps to no Location, with two log messages (out-of-scope vs in-scope-no-Location); new 3rd
396
+ param `$lookupCustomerByNetsuiteCustomerInternalId` classifies the log; destination resolves before
397
+ origin and the skip runs first (no stray origin row); the cascade treats the null return as
398
+ nothing-to-link (no re-wedge). Library deployed + verified (cursor advanced through 2026).
399
+ **Follow-on:** the added param broke a THIRD, direct cron call site
400
+ (`common_sync_togasupply.php:782`, SALES_ORDERS) → issue 754 "Too few arguments" → froze SALES_ORDERS;
401
+ fixed by passing the 3rd arg there. Also fixed two stale comments claiming a per-record try/catch that
402
+ was removed 2026-08-27. Redeploy **worker** for the cron fix. (jcardinal)
403
+ - 2026-09-13 - **OPEN (now resolved 2026-09-14, above): INVENTORY_ADJUSTMENTS collapsing on an
404
+ unresolved transfer-order DESTINATION.** The section's on-demand cascade imports a
405
+ transfer-order-backed fulfillment, and `POST /v2/transfer-orders` for **TO 277871**
406
+ (`c_netsuiteInternalSalesOrderId` **7102750**) failed **EV-10 "Column 'destinationLocationId' cannot
407
+ be null"** — origin resolved, destination did not. (jcardinal)
333
408
  - 2026-09-10 - Recorded that the import concentrates **1,832 of 1,939** transfer orders on catch-all
334
409
  destination **location 2**, all with `createdByUserId` NULL (so none came from the Create Transfer
335
410
  Order screen), and that whether it should resolve the real hospital instead is still open with the
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: nychh
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-13
21
+ updated: 2026-09-14
22
22
  owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
23
23
  files:
24
24
  - dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
@@ -138,11 +138,15 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
138
138
  unit. Engine + shim detail:
139
139
  [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md),
140
140
  [SuiteQL/REST shim](../../1.0/apps/library/features/netsuite-suiteql-rest-shim.md).
141
- - 🚩 **OPEN (2026-09-13): INVENTORY_ADJUSTMENTS collapsing on an unresolved transfer-order
142
- DESTINATION.** The section's on-demand cascade imports a transfer-order-backed fulfillment and
143
- `POST /v2/transfer-orders` for **TO 277871** (`c_netsuiteInternalSalesOrderId` 7102750) fails
144
- **EV-10 "destinationLocationId cannot be null"**origin resolved, destination did not. Separate
145
- destination-resolution bug; next task. See
141
+ - **RESOLVED (2026-09-14): INVENTORY_ADJUSTMENTS destination wedge.** `POST /v2/transfer-orders` for
142
+ **TO 277871** (SO 7102750) failed **EV-10 "destinationLocationId cannot be null"** every run because
143
+ its customer **3997 "1896 Laurel Hill Management, LLC"** is not a NYCHH customer and has **no
144
+ `Client_Nychh.Locations` row** (though it IS in `Customers`, id 19 "maps to a Location" is the
145
+ scope signal, not "is in Customers"). It reached NYCHH because the adjustment cascade scopes by the
146
+ shared HQ warehouse then imports a fulfillment for a different customer. Fix: the importer now SKIPS
147
+ out-of-scope transfer orders (loud log) instead of forcing a null destination; library deployed +
148
+ verified. A follow-on regression from the added param froze SALES_ORDERS (a 3rd, direct cron call
149
+ site, issue 754 "Too few arguments") — fixed; **redeploy worker**. See
146
150
  [NYCHH NetSuite → TransferOrders import](./features/netsuite-transfer-order-import.md).
147
151
  - **NYCHH sync scope + data-gap scale + backfill approach (2026-09-09).**
148
152
  - **Scope = 22 NetSuite customers:** parent **28908** "NYC Health + Hospitals" + 21 hospital child
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.802",
3
+ "version": "1.0.804",
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",
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * design-fidelity-reminder.js — PRE-EDIT enforcement of the Design Contract discipline.
6
+ *
7
+ * Why this exists: on Desk (and the other 2.0 React apps) the model keeps improvising
8
+ * UI — inventing button text, helper lines, field order and labels instead of matching
9
+ * the designer's on-disk design. That drift ships UIs that "function" but do not match
10
+ * what Matt designed, and the developer has to catch it every time. A rule in a file is
11
+ * passive; over a long session the model slides back to improvising. This is the
12
+ * mechanical backstop, modeled on plain-talk.js.
13
+ *
14
+ * How it works: registered on PreToolUse for Edit|Write|MultiEdit. Just before the model
15
+ * edits a UI source file — a path under a `src/` tree ending .tsx / .jsx / .css — it prints
16
+ * a short <system-reminder> to stdout with the Design Contract rules, so the discipline is
17
+ * in context at the moment the UI is written. On any non-UI file it stays silent.
18
+ *
19
+ * It never blocks. It always exits 0 — a reminder must never stop or deny an edit. It
20
+ * fails open on any error: a crashing nudge must never brick a session. Per team hook
21
+ * convention DENY/error text goes to STDERR — this hook never denies, so it never uses it.
22
+ *
23
+ * Escape hatch: set TOGA_DESIGN_FIDELITY_DISABLED=1 to silence the reminder.
24
+ */
25
+
26
+ const fs = require('fs');
27
+
28
+ /* Read the CC hook payload. PreToolUse delivers the tool input on stdin as
29
+ * { tool_name, tool_input: { file_path, ... } }; fall back to the CLAUDE_TOOL_INPUT env
30
+ * (what the sibling PostToolUse reminders read) and to a top-level file_path. */
31
+ function readFilePath() {
32
+ let raw = '';
33
+ try { raw = fs.readFileSync(0, 'utf8'); } catch (e) { /* no stdin */ }
34
+
35
+ let data = {};
36
+ if (raw && raw.trim()) {
37
+ try { data = JSON.parse(raw); } catch (e) { data = {}; }
38
+ }
39
+
40
+ let fp =
41
+ (data.tool_input && typeof data.tool_input.file_path === 'string' && data.tool_input.file_path) ||
42
+ (typeof data.file_path === 'string' && data.file_path) ||
43
+ '';
44
+
45
+ if (!fp && process.env.CLAUDE_TOOL_INPUT) {
46
+ try {
47
+ const env = JSON.parse(process.env.CLAUDE_TOOL_INPUT);
48
+ if (env && typeof env.file_path === 'string') fp = env.file_path;
49
+ } catch (e) { /* ignore */ }
50
+ }
51
+ return fp;
52
+ }
53
+
54
+ /* A Desk / React-app UI source file: under a `src/` tree AND ending .tsx / .jsx / .css. */
55
+ function isUiSourceFile(filePath) {
56
+ if (!filePath) return false;
57
+ const p = filePath.replace(/\\/g, '/').toLowerCase();
58
+ return /\/src\//.test(p) && /\.(tsx|jsx|css)$/.test(p);
59
+ }
60
+
61
+ function reminder() {
62
+ return [
63
+ '<system-reminder>',
64
+ 'DESIGN CONTRACT — this edit touches a UI source file. Match the designer\'s design,',
65
+ 'do not improvise UI. Reusing a shell or making it "function" is NOT fidelity.',
66
+ '',
67
+ ' 1. The on-disk design is the authority. Open the exact design source file FIRST and',
68
+ ' build from it — match its LAYOUT/placement, not just pixels.',
69
+ ' 2. Build to a written Design Contract: every field + its order, each label/placeholder,',
70
+ ' required flags, button text, modal size/layout, and the header/footer.',
71
+ ' 3. Invent NOTHING user-visible — button text, helper/info lines, labels, required flags.',
72
+ ' Copy comes from the design + Surface/settings. If it is not in the design, ASK.',
73
+ ' 4. Design-vs-data conflict → STOP and ask. Do not decide it yourself.',
74
+ ' 5. No self-sign-off. Hand the developer a labeled diff (mine vs the design). "It',
75
+ ' functions" is not fidelity.',
76
+ '</system-reminder>',
77
+ ].join('\n');
78
+ }
79
+
80
+ function main() {
81
+ if (process.env.TOGA_DESIGN_FIDELITY_DISABLED === '1') process.exit(0);
82
+
83
+ const filePath = readFilePath();
84
+
85
+ if (isUiSourceFile(filePath)) {
86
+ // Inject the reminder into the model's context via the PreToolUse additionalContext
87
+ // channel (raw stdout on PreToolUse only shows in transcript, not context). Non-blocking:
88
+ // additionalContext adds context, it does not deny the edit.
89
+ console.log(JSON.stringify({
90
+ hookSpecificOutput: {
91
+ hookEventName: 'PreToolUse',
92
+ additionalContext: reminder(),
93
+ },
94
+ }));
95
+ }
96
+ process.exit(0);
97
+ }
98
+
99
+ try { main(); } catch (e) { process.exit(0); } // fail-open: never block or brick an edit