toga-ai 1.0.760 → 1.0.762

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.
@@ -202,6 +202,43 @@ Prudential-EDI, S&P Global**.
202
202
  - **Lesson:** any `sync*FromNetsuite()` that takes the record `&$byReference` and refetches must have
203
203
  its fetch-by-id shim carry **every** field the caller reads afterward — `lastModifiedDate` first.
204
204
 
205
+ ### ⚠ …but the by-reference shim wrote the cursor on the WRONG CLOCK → ITEM_RECEIPTS + ITEM_FULFILLMENTS re-matched forever (found + fixed 2026-09-03, NYCHH prod)
206
+
207
+ A follow-on to the by-reference fix directly above. Once the two shims carried a `lastModifiedDate`
208
+ again, the sections still could **not advance** — the value was in the **wrong timezone**, so
209
+ `lastmodifieddate > cursor` re-matched the **same record every run** (an infinite loop, no crash).
210
+
211
+ - **Root cause — two clocks.** The prior fix set `fetchItemFulfillmentById` / `fetchItemReceiptById`'s
212
+ `->lastModifiedDate` from the **REST record GET**, which returns **UTC** (e.g. `2024-08-06T14:45:00Z`).
213
+ `common_sync_togasupply.php` then `date()`-formats it in the script timezone **America/Chicago** →
214
+ `2024-08-06 09:45:00`, and stores that as the cursor. But the LIST fetch's `WHERE`
215
+ (`App_Api_Netsuite_Rest::listItem*`) compares `lastmodifieddate` via SuiteQL
216
+ `TO_CHAR(lastmodifieddate,'YYYY-MM-DD HH24:MI:SS')`, which is the **NetSuite account timezone =
217
+ Eastern** → `2024-08-06 10:45:16`. So the cursor was written **~1 hour behind** the clock the fetch
218
+ reads → `lastmodifieddate > cursor` was always still true for the record just processed → it was
219
+ re-fetched and re-processed forever and the watermark never moved.
220
+ - **Proven on NYCHH prod** (item receipt internalId `4953915`): REST GET `lastModifiedDate`
221
+ `2024-08-06T14:45:00Z` (UTC) · SuiteQL `2024-08-06 10:45:16` (Eastern) · stored cursor
222
+ `2024-08-06 09:45:00` (Chicago). ITEM_RECEIPTS was jammed on it; because that heavy receipt (nested
223
+ PO→SO reconcile) runs **before** fulfillments and starved the run, ITEM_FULFILLMENTS' window kept
224
+ collapsing to 6s and never grew to clear the empty gap to its first real record (2025-01-06; 1,294
225
+ NYCHH fulfillments waited after 2025-01-01).
226
+ - **INVENTORY_ADJUSTMENTS was NOT stuck** — `fetchInventoryAdjustmentById` already reads
227
+ `lastmodifieddate` from SuiteQL `TO_CHAR` (the right clock), so it advanced normally. **That is the
228
+ correct pattern**; the other two shims did not follow it.
229
+ - **Fix (library only, `php -l` clean, fail-loud preserved):** in `library/app/api/netsuite/rest.php`,
230
+ `fetchItemFulfillmentById` (~L2093) and `fetchItemReceiptById` (~L2254) now set
231
+ `$shim->lastModifiedDate` from a SuiteQL
232
+ `SELECT TO_CHAR(lastmodifieddate,'YYYY-MM-DD HH24:MI:SS') AS lastmodifieddate FROM transaction WHERE id = $id`
233
+ (Eastern) — matching `fetchInventoryAdjustmentById` — **instead of** the REST header's UTC
234
+ `$header->lastModifiedDate`. No shim reads the REST/UTC `lastModifiedDate` anymore. No worker change,
235
+ no SQL. After deploy the two sections resume and backfill on their own (idempotent).
236
+ - **Lesson:** the per-record cursor value MUST be written in the **same timezone the fetch `WHERE`
237
+ compares against** — SuiteQL account time (Eastern), never the REST record GET's UTC
238
+ `lastModifiedDate`. A cursor even an hour behind the fetch clock re-matches the same record forever.
239
+ This is the same SuiteQL-vs-REST clock trap as the date-only-format bug above — see the
240
+ [SuiteQL/REST shim doc](../../library/features/netsuite-suiteql-rest-shim.md).
241
+
205
242
  ### ⚠ Per-record isolation is SECTION-SPECIFIC — and SALES_ORDERS has NONE (corrected 2026-08-17)
206
243
 
207
244
  Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
@@ -1175,6 +1212,15 @@ the code deploy).
1175
1212
 
1176
1213
  ## Change history
1177
1214
 
1215
+ - 2026-09-03 — **By-reference shim wrote the cursor on the WRONG CLOCK → ITEM_RECEIPTS + ITEM_FULFILLMENTS
1216
+ re-matched the same record forever (fixed, `library` only).** Follow-on to the same-day by-reference
1217
+ fix below: the shims set `lastModifiedDate` from the REST GET (UTC), `date()`-formatted in
1218
+ America/Chicago, but the LIST `WHERE` compares SuiteQL `TO_CHAR(lastmodifieddate)` in the NetSuite
1219
+ account tz (Eastern), so the cursor sat ~1h behind the fetch clock and `lastmodifieddate > cursor`
1220
+ always re-matched — infinite loop, no crash. `fetchItemFulfillmentById`/`fetchItemReceiptById`
1221
+ (`rest.php` ~L2093/~L2254) now read `lastmodifieddate` from SuiteQL `TO_CHAR` (Eastern), matching
1222
+ `fetchInventoryAdjustmentById` (already correct). Proven on NYCHH prod (item receipt 4953915). No
1223
+ worker/SQL change; sections self-backfill after deploy. (jcardinal)
1178
1224
  - 2026-09-03 — **By-reference refetch dropped `lastModifiedDate` → ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS
1179
1225
  froze (fixed, `library` only).** GroWrk's ITEM_FULFILLMENTS was re-throwing every 5 min since the
1180
1226
  2026-09-02 per-record cursor deploy (`Undefined property: stdClass::$lastModifiedDate` at
@@ -14,5 +14,5 @@
14
14
  | [Talos AI-Assistant Component (launcher + docked/drawer chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a chat panel (`TalosPanel`) tha | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/Talos/TalosPanel.test.tsx, toga-blox/src/components/index.ts, toga-blox/src/components/Talos/README.md, toga-blox/src/components/TableRecordModal/TableRecordModal.tsx |
15
15
  | [Toaster — host-token styling contract](features/toaster.md) | The blox `Toaster` is a CSS-Module component whose every visual property reads a `--toaster-*` custom property supplied by the **host app**. | toga-blox/src/components/Toaster/Toaster.module.css, toga-blox/src/components/Toaster/Toaster.tsx, toga-blox/src/components/Toaster/ToasterContext.tsx, toga2-commerce/src/styles/index.css, toga2-commerce/src/components/Toasters/ToasterList.tsx |
16
16
  | [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/package.json, toga-blox/src/utils/getFontAwesomeIcon.tsx |
17
- | [Landing a Large Long-Lived Feature Branch (merge, never rebase)](workflows/landing-a-large-feature-branch.md) | The procedure for landing a long-lived, very large feature branch (months old, hundreds of commits) into `_production` in `toga-blox-npm`. | toga-blox/package.json, toga-blox/.github/workflows/publish.yml |
18
- | [Local-linking an unpublished blox into a consumer app (incl. a React 19 / Next host)](workflows/local-link-into-a-consumer-app.md) | How to develop a **blox component that is not published yet** against a real consuming app, on Windows, including the hard case: a **React 19 / Next.js (Turbopa | toga-blox/package.json, toga-blox/postcss.config.cjs, toga25-supply/vite.config.ts |
17
+ | [Landing a Large Long-Lived Feature Branch (merge, never rebase)](workflows/landing-a-large-feature-branch.md) | The procedure for landing a long-lived, very large feature branch (months old, hundreds of commits) into `_production` in `toga-blox-npm`. | toga-blox/package.json, toga-blox/.github/workflows/publish.yml, toga25-supply/vite.config.ts, toga25-supply/package-lock.json |
18
+ | [Local-linking an unpublished blox into a consumer app (incl. a React 19 / Next host)](workflows/local-link-into-a-consumer-app.md) | How to develop a **blox component that is not published yet** against a real consuming app, on Windows, including the hard case: a **React 19 / Next.js (Turbopa | toga-blox/package.json, toga-blox/postcss.config.cjs, toga25-supply/vite.config.ts, toga25-supply/package.json, toga25-supply/tsconfig.node.json |
@@ -6,13 +6,16 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-26
9
+ updated: 2026-09-03
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga-blox/package.json
13
13
  - toga-blox/.github/workflows/publish.yml
14
+ - toga25-supply/vite.config.ts
15
+ - toga25-supply/package-lock.json
14
16
  related:
15
17
  - ./dynamic-publish-pipeline.md
18
+ - ./local-link-into-a-consumer-app.md
16
19
  - ../architecture.md
17
20
  ---
18
21
 
@@ -23,6 +26,9 @@ commits) into `_production` in `toga-blox-npm`. The decision recorded here is **
23
26
  `_production` into the feature branch — do not rebase.** Use this doc before starting any
24
27
  big-branch integration; the recon step is the transferable part.
25
28
 
29
+ Steps 4–5 cover the routine case too: merging the **same** `_production` commit into **several**
30
+ feature branches at once (proven on `toga25-supply`, 2026-09-03).
31
+
26
32
  ## How it works
27
33
 
28
34
  ### 1. Measure the divergence first
@@ -56,6 +62,42 @@ This shows the exact conflicting files without touching the working tree, which
56
62
  2. Resolve the (small, known) conflict set **once**.
57
63
  3. Open a normal PR: feature → `_production`.
58
64
 
65
+ ### 4. Regenerate `package-lock.json` — never hand-merge it
66
+
67
+ ```bash
68
+ npm install --package-lock-only
69
+ ```
70
+
71
+ It rebuilds the lockfile from the **merged `package.json`** and does **not** touch
72
+ `node_modules` — which matters in this repo family because it therefore cannot destroy a blox
73
+ symlink (see [local-link](./local-link-into-a-consumer-app.md) §1a). `toga25-supply/.npmrc`
74
+ already sets `legacy-peer-deps=true`, so no flag is needed. Confirm afterwards that the
75
+ regenerated lock matches the pin in `package.json`.
76
+
77
+ ### 5. Copying one resolution across several branches — diff BOTH sides first
78
+
79
+ When the same `_production` commit is merged into several feature branches, the conflict often
80
+ *looks* identical and the resolution can be copied:
81
+
82
+ ```bash
83
+ git show <already-resolved-branch>:<file> > <file> # then verify byte-identical
84
+ ```
85
+
86
+ **But check first — "the same conflict" is frequently not the same.** Before copying, diff both
87
+ conflict sides against the branch you are copying from:
88
+
89
+ ```bash
90
+ git show :2:<file> # ours (the feature branch)
91
+ git show :3:<file> # theirs (_production)
92
+ ```
93
+
94
+ Concrete case (toga25-supply, 2026-09-03, all three branches merging `_production` `4c99ced`,
95
+ blox pin `1.1.2-production.144`): `TRUE-81379` and `TRUE-81217` had identical sides and copied
96
+ cleanly. `TRUE-79020-v2` had **drifted** — its side was space-indented (52 space lines against
97
+ the repo's tabs-only standard) and was missing the react-query alias, the `isBloxLinked`
98
+ branching, and the whole `server.fs.allow` block, while already carrying two `dedupe` members the
99
+ others lacked. A blind copy would have silently reverted work.
100
+
59
101
  ### Why not rebase
60
102
 
61
103
  Rebasing 281 commits onto `_production` replays every commit, forces the **same conflicts to
@@ -70,10 +112,23 @@ conflict set exactly once and preserves the pushed history.
70
112
  at `1.0.314`, so the merge publishes the *branch's* number. See
71
113
  [dynamic-publish-pipeline](./dynamic-publish-pipeline.md) for how CI derives the published
72
114
  version from that base.
115
+ - **⚠ git can auto-merge a DUPLICATE import with no conflict marker.** One side had
116
+ `import fs from "fs"`, the other `import fs from "node:fs"`. Git saw two independent additions,
117
+ merged **both**, and flagged nothing. The result is a duplicate identifier that breaks the
118
+ build. **After resolving any conflict, scan the whole file for duplicate imports** — "no
119
+ conflict markers left" does not mean "the file is correct."
73
120
  - **Do not read a behind-count as staleness** in this repo — much of it is release plumbing
74
121
  generated on the release branch and never merged back (see the publish-pipeline doc).
75
122
 
76
123
  ## Change history
124
+ - 2026-09-03 — Broadened past the huge-branch case to cover merging one `_production` commit into
125
+ **several** feature branches. Added step 4 (`npm install --package-lock-only` regenerates the
126
+ lockfile from the merged `package.json` without touching `node_modules`, so it cannot break a
127
+ blox symlink) and step 5 (**diff `git show :2:`/`:3:` before copying a resolution between
128
+ branches** — of three toga25-supply branches merging the same commit, one had drifted to space
129
+ indentation and was missing three config blocks). Added the gotcha that git **auto-merged a
130
+ duplicate `import fs`** — `fs` vs `node:fs` — with **no conflict marker**, breaking the build.
131
+ (apeterson)
77
132
  - 2026-08-26 — Created. Recorded the merge-not-rebase decision for landing `feature-new-table`
78
133
  (281 ahead / 8 behind, 530 files) into `_production`, and the
79
134
  `rev-list --left-right --count` → `merge-tree` recon pair that narrowed the conflict surface
@@ -6,12 +6,14 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-02
9
+ updated: 2026-09-03
10
10
  owners: [tcox, apeterson]
11
11
  files:
12
12
  - toga-blox/package.json
13
13
  - toga-blox/postcss.config.cjs
14
14
  - toga25-supply/vite.config.ts
15
+ - toga25-supply/package.json
16
+ - toga25-supply/tsconfig.node.json
15
17
  related:
16
18
  - ./dynamic-publish-pipeline.md
17
19
  - ../features/talos-assistant.md
@@ -73,6 +75,15 @@ rm -rf node_modules/.vite
73
75
  (see §2c).
74
76
  - Same rule after editing blox source: `npm run build` in `toga-blox-npm`, **then** clear
75
77
  `node_modules/.vite` in the consumer.
78
+ - **`npm ci` does it too, and switching branches forces it.** Each feature branch pins its own
79
+ blox version, so a branch switch means an install, which silently replaces the link. There is
80
+ no config fix — re-linking is a required manual step in the branch-switch routine. Shortest
81
+ re-link from the consumer root: `npm install --no-save ../toga-blox-npm`.
82
+ - **The one npm command that CANNOT break the link: `npm install --package-lock-only`.** It
83
+ regenerates `package-lock.json` from the merged `package.json` **without touching
84
+ `node_modules`**. That is also the only correct way to resolve a lockfile merge conflict —
85
+ never hand-merge one. (`toga25-supply/.npmrc` sets `legacy-peer-deps=true`, so no flag is
86
+ needed.) Verified 2026-09-03 on toga25-supply at blox `1.1.2-production.144`.
76
87
 
77
88
  ### 2. Collapse to ONE React (the React 19 case)
78
89
 
@@ -127,6 +138,30 @@ dedupe: [
127
138
  `@tanstack/react-query`, `@tanstack/react-query-persist-client`, `@tanstack/react-table`,
128
139
  `react-hook-form`. `react-hook-form` was nested under blox as well — the same latent bug for
129
140
  `BaseInput` / `useFormContext`, just not throwing yet.
141
+ - **Closed out 2026-09-03: `react-hook-form` was the next one to bite, in `toga25-supply`.** It
142
+ was in **neither** `dedupe` **nor** `resolve.alias`, while **24 files** under
143
+ `toga25-supply/src` import it and blox's `BaseInput` calls `useFormContext()` — `<FormProvider>`
144
+ fills one copy, `BaseInput` reads the other. blox's own `node_modules` ships
145
+ `react-hook-form@7.82.0` (alongside `react`/`react-dom` 18.3.1, `@tanstack/react-query`
146
+ 5.101.4, `@tanstack/react-table` 8.21.3, `framer-motion`, `react-select`, `react-day-picker`,
147
+ and the react-query persist/devtools packages). Same version on both sides does **not** save
148
+ you — two folders means two module instances. `toga25-desk` already deduped it; supply had the
149
+ gap. Supply's list is now:
150
+
151
+ ```ts
152
+ dedupe: [
153
+ "react", "react-dom",
154
+ "@tanstack/react-table",
155
+ "@tanstack/react-query",
156
+ "@tanstack/react-query-persist-client",
157
+ "@tanstack/react-query-devtools",
158
+ "react-hook-form",
159
+ ]
160
+ ```
161
+
162
+ **Check it after every install:** `npm ls react react-dom @tanstack/react-query
163
+ @tanstack/react-table react-hook-form` must show exactly one copy of each (verified after
164
+ `npm ci`, 2026-09-03).
130
165
  - **`optimizeDeps.include` is why the throw appears to come from inside blox.** With blox in
131
166
  `optimizeDeps.include`, esbuild pre-bundles blox **together with its nested copy**, so the error
132
167
  surfaces from `@agilant_toga-blox.js` rather than from app code, and the `?v=<hash>` in the error
@@ -183,6 +218,42 @@ const isLinked = fs.lstatSync(bloxDir).isSymbolicLink();
183
218
  - **Result: a blox rebuild now just triggers HMR.** No `rm -rf node_modules/.vite`, no browser cache
184
219
  fight. For the **linked** case this **supersedes** §2b's "editing `dedupe` is not enough — clear
185
220
  `.vite` and hard-reload"; that step remains true for a published install.
221
+ - **Switching branches between a linked and a published blox also self-heals — verified in Vite
222
+ source 2026-09-03.** Vite 7.3.6's `getConfigHash()` hashes `resolve` **and**
223
+ `optimizeDeps.include`/`exclude` (read at
224
+ `node_modules/vite/dist/node/chunks/config.js:32429`). Because the config branches those exact
225
+ values on `isBloxLinked`, moving between a symlinked and a published blox changes the hash and
226
+ Vite re-runs optimizeDeps by itself — **no `rm -rf node_modules/.vite` needed on a branch
227
+ switch.** This was asserted in a code comment before; it is now checked against Vite's source.
228
+ It does **not** cover a blox *rebuild*: the hash never sees a linked package's `dist`, which is
229
+ why the linked branch keeps blox out of the pre-bundle entirely.
230
+
231
+ **The settled `vite.config.ts` shape (toga25-supply, 2026-09-03 — now identical on the three
232
+ in-flight branches `TRUE-81379`, `TRUE-81217`, `TRUE-79020-v2`):**
233
+
234
+ ```ts
235
+ const bloxDir = path.resolve(__dirname, "node_modules/@agilant/toga-blox");
236
+ const isBloxLinked = fs.existsSync(bloxDir) && fs.lstatSync(bloxDir).isSymbolicLink();
237
+ ```
238
+
239
+ - **Auto-detect, never a developer-flipped flag.** A hand-set boolean is always wrong on someone
240
+ else's machine. Keep the `existsSync` guard first — `lstatSync` throws when blox is not installed.
241
+ - **`optimizeDeps` has exactly two branches.** LINKED → `exclude: [BLOX]` + `include:
242
+ BLOX_EXTERNALS`. PUBLISHED → `include: [BLOX]`.
243
+ - **`server.fs.allow` — use `searchForWorkspaceRoot(process.cwd())`, NOT `path.resolve(__dirname)`:**
244
+
245
+ ```ts
246
+ server: { fs: { allow: [searchForWorkspaceRoot(process.cwd()), ...(bloxRealPath ? [bloxRealPath] : [])] } }
247
+ ```
248
+
249
+ Setting `allow` **replaces** Vite's default list rather than extending it, so hardcoding the
250
+ project root silently narrows what the dev server will serve. This form also needs no
251
+ `isBloxLinked` branch — it is a no-op on a normal install.
252
+ - **The explicit `resolve.alias` for `@tanstack/react-query` was kept on top of `dedupe`** —
253
+ `_production` had deleted it, the original author's comment says dedupe alone did not beat the
254
+ symlink. It is a no-op on a published install, so keeping it costs nothing. ⚠ **Still unverified
255
+ in an actual symlinked run — open question.** If you reproduce it either way, record the answer
256
+ here.
186
257
 
187
258
  **Trade-off: `BLOX_EXTERNALS` is a hand-maintained list** (the grep that regenerates it sits in a
188
259
  comment beside it). If blox adds a dependency and the list is not updated, the symptom is **"does not
@@ -253,11 +324,29 @@ edit; clearing `node_modules/.vite` matters because Vite's pre-bundle cache hold
253
324
  believing a deploy will work.
254
325
  - **Linked ≠ shippable.** A consumer on a link **cannot deploy**. The component has to merge and
255
326
  publish, and the consumer has to move to the npm version, before anything ships.
327
+ - **Type-check `vite.config.ts` with `-p tsconfig.node.json`.** The config file is covered by
328
+ `tsconfig.node.json`, not `tsconfig.app.json`, so the usual
329
+ `npx tsc --noEmit -p tsconfig.app.json` never looks at it. After editing the Vite config run
330
+ `npx tsc --noEmit -p tsconfig.node.json` as well.
256
331
  - **Import subpaths to keep the bundle small.** blox's `exports` allows `./dist/*`, so
257
332
  `@agilant/toga-blox/dist/components/<X>/index.js` pulls just that component; the package barrel
258
333
  drags every component's CSS into the consumer bundle.
259
334
 
260
335
  ## Change history
336
+ - 2026-09-03 — Recorded the **settled `vite.config.ts` shape** now shared by supply's three
337
+ in-flight branches, and closed two open threads. (1) `react-hook-form` was in **neither**
338
+ `dedupe` nor `alias` in `toga25-supply` while 24 `src` files import it and blox's `BaseInput`
339
+ calls `useFormContext()` — added, and supply's full 7-member `dedupe` list is written down.
340
+ (2) Verified in Vite source (`chunks/config.js:32429`) that `getConfigHash()` hashes `resolve`
341
+ **and** `optimizeDeps.include`/`exclude`, so branching them on `isBloxLinked` makes a
342
+ linked↔published branch switch invalidate the dep cache **by itself** — no
343
+ `rm -rf node_modules/.vite`. Also: `server.fs.allow` must use
344
+ `searchForWorkspaceRoot(process.cwd())` because `allow` **replaces** Vite's default rather than
345
+ extending it; `npm ci` un-links blox just like `npm install`, and
346
+ `npm install --package-lock-only` is the one command that cannot (so it is also how you resolve
347
+ a lockfile conflict); and `vite.config.ts` is type-checked with `-p tsconfig.node.json`, not the
348
+ app config. The kept `@tanstack/react-query` alias remains **unverified** under a real symlink.
349
+ (apeterson)
261
350
  - 2026-09-02 — Expanded step 4: the consumer's `tsc` resolves blox **types from source** through the
262
351
  symlink while Vite loads the package from **`dist/`**, so a clean `tsc --noEmit` does **not** prove
263
352
  the browser has your change. Every blox edit needs `npm run build` in blox **plus**
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-03
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/main.tsx
@@ -98,11 +98,25 @@ toga-blox table/modal templates.
98
98
  ## toga-blox relationship
99
99
 
100
100
  `@agilant/toga-blox` is the shared frontend component/template/utility library (repo
101
- `toga-blox-npm`). It is **not published to npm for this app's purposes** — the app is currently
102
- its primary consumer and resolves it via a symlinked `node_modules/@agilant/toga-blox` →
103
- `toga-blox-npm`. Because the app imports the built `dist/` (JS + `.d.ts`), any change to toga-blox
104
- **source** requires a rebuild before it is visible at runtime; in urgent cases the `dist/` files
105
- (and their `.d.ts`) have been patched directly. The `/update-blox` skill covers version bumps.
101
+ `toga-blox-npm`). The app consumes it in **one of two states, and you must check which you are in**
102
+ before trusting anything else in this section:
103
+
104
+ - **Published (the normal state, incl. every CI/Amplify build).** `package.json` pins an exact
105
+ version — `1.1.2-production.144` as of 2026-09-03. A blox source change reaches the app only via
106
+ publish + pin bump. The `/update-blox` skill covers the bump.
107
+ - **Symlinked (a local dev loop only).** `node_modules/@agilant/toga-blox` points at the
108
+ `toga-blox-npm` checkout. The app imports the built `dist/`, so **every** blox source edit needs
109
+ `npm run build` in blox first.
110
+
111
+ `vite.config.ts` **auto-detects** which state it is in
112
+ (`fs.existsSync(bloxDir) && fs.lstatSync(bloxDir).isSymbolicLink()`) and flips `optimizeDeps`
113
+ accordingly — there is no flag to set by hand.
114
+
115
+ **⚠ Any `npm install` or `npm ci` silently replaces the symlink with the published package.**
116
+ Switching branches changes the blox pin, which forces an install, which un-links it. There is no
117
+ config fix; re-link with `npm install --no-save ../toga-blox-npm`. The symptom of missing this is a
118
+ misleading "does not provide an export named X". Full procedure and the `dedupe`/`alias` one-copy
119
+ rules: [local-link-into-a-consumer-app](../toga-blox/workflows/local-link-into-a-consumer-app.md).
106
120
 
107
121
  ## Conventions & gotchas
108
122
 
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-09-01
8
+ updated: 2026-09-03
9
9
  owners: [jcardinal, apeterson]
10
10
  files: []
11
11
  related:
@@ -132,6 +132,13 @@ npx tsc --noEmit -p tsconfig.app.json # ✅
132
132
  npx tsc --noEmit # ❌ silent no-op
133
133
  ```
134
134
 
135
+ **MUST: `vite.config.ts` is checked by `tsconfig.node.json`, not the app config.** The app-config
136
+ run never looks at it, so a broken Vite config type-checks green. After editing it, also run:
137
+
138
+ ```bash
139
+ npx tsc --noEmit -p tsconfig.node.json
140
+ ```
141
+
135
142
  Documented independently in two repos on this tsconfig shape — `toga25-supply` (verified
136
143
  2026-09-01) and `toga2-view` ([architecture](../apps/toga2-view/architecture.md)) — so assume it
137
144
  for any app sharing the shape, but **verify per repo before asserting it**. Note also that a repo
@@ -359,15 +366,28 @@ memory.
359
366
  because they follow that app's shared-lib surface — verified 2026-08-18:
360
367
 
361
368
  ```ts
362
- // toga25-supply/vite.config.ts
363
- dedupe: ["react", "react-dom", "@tanstack/react-table"]
369
+ // toga25-supply/vite.config.ts — verified 2026-09-03
370
+ dedupe: [
371
+ "react", "react-dom",
372
+ "@tanstack/react-table",
373
+ "@tanstack/react-query",
374
+ "@tanstack/react-query-persist-client",
375
+ "@tanstack/react-query-devtools",
376
+ "react-hook-form",
377
+ ]
364
378
  // toga25-desk/vite.config.ts
365
379
  dedupe: ["react", "react-dom", "react-hook-form"]
366
380
  ```
367
381
 
368
- Supply dedupes the table lib (it consumes blox's table context); desk dedupes react-hook-form (it
369
- consumes blox's `BaseInput`, which reads `useFormContext()`). See §13(c) for why a second copy
370
- silently breaks blox's hooks.
382
+ Supply's earlier three-member list (`react`, `react-dom`, `@tanstack/react-table`) is
383
+ **superseded** — it was incomplete, and each missing member has since caused or nearly caused a
384
+ live defect. Verified 2026-09-03: `toga-blox-npm/node_modules/` ships its own
385
+ `react-hook-form@7.82.0`, and supply had it in **neither** `dedupe` **nor** `resolve.alias`
386
+ while 24 `src` files import it and blox's `BaseInput` calls `useFormContext()` — a
387
+ `<FormProvider>` filling one copy and `BaseInput` reading the other. **Same version on both
388
+ sides does not save you**; two folders means two module instances. Desk already deduped it.
389
+ Apply the principle, then audit the actual list per app: dedupe every peer the shared lib
390
+ provides context or hooks for. See §13(c).
371
391
 
372
392
  Verified 2026-08-26: `toga2-commerce` had **no** `dedupe` at all and now declares
373
393
  `["react","react-dom","react-hook-form","react-router-dom","framer-motion","@tanstack/react-query","@tanstack/react-table"]`
@@ -375,6 +395,15 @@ memory.
375
395
  ships its own `react@18.3.1`/`react-dom@18.3.1`, so consuming blox through a **local symlink**
376
396
  resolves React to blox's own copy. Dedupe is what makes the symlink workflow safe — it is not
377
397
  optional polish.
398
+ - **SHOULD: branch `optimizeDeps` on whether the shared lib is symlinked, and let Vite's config
399
+ hash do the cache-busting.** Verified in Vite 7.3.6 source 2026-09-03
400
+ (`node_modules/vite/dist/node/chunks/config.js:32429`): `getConfigHash()` hashes `resolve` **and**
401
+ `optimizeDeps.include`/`exclude`. So a config whose `optimizeDeps` differs between a linked and a
402
+ published blox re-optimizes **by itself** on a branch switch — no `rm -rf node_modules/.vite`.
403
+ Also: `server.fs.allow` **replaces** Vite's default allow-list, it does not extend it — use
404
+ `searchForWorkspaceRoot(process.cwd())`, never a hardcoded `path.resolve(__dirname)`, or you
405
+ silently narrow what the dev server will serve. Mechanics:
406
+ [local-link-into-a-consumer-app](../apps/toga-blox/workflows/local-link-into-a-consumer-app.md) §2c.
378
407
  - **MUST: one env-read chokepoint.** Read `VITE_API` in exactly **one** module (see §17). Verified:
379
408
  both apps read it (plus a per-host override) only in `src/api/api.ts`.
380
409
  - **SHOULD: ESLint flat config**, verified identical in both apps:
@@ -680,6 +709,13 @@ consuming app, guard against them and move on.
680
709
 
681
710
  ## Change history
682
711
 
712
+ - 2026-09-03 — §11: replaced supply's stale three-member `dedupe` list with the verified
713
+ seven-member one and added `react-hook-form` (it was in neither `dedupe` nor `alias` while 24
714
+ `src` files import it and blox's `BaseInput` calls `useFormContext()`). Added a SHOULD for
715
+ branching `optimizeDeps` on the symlink — Vite's `getConfigHash()` hashes `resolve` +
716
+ `optimizeDeps.include`/`exclude` (verified in source), so a branch switch self-invalidates the
717
+ dep cache — plus the trap that `server.fs.allow` **replaces** Vite's default list. §2: added a
718
+ MUST to type-check `vite.config.ts` with `-p tsconfig.node.json`. (apeterson)
683
719
  - 2026-09-01 — §2: added a MUST to **type-check with `-p tsconfig.app.json` (or `tsc -b`), never
684
720
  bare `npx tsc --noEmit`** — the root `tsconfig.json` is references-only (`files: []`), so a bare
685
721
  root run compiles nothing and always exits 0, a false green in any pre-commit/CI check. Verified
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.760",
3
+ "version": "1.0.762",
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",