toga-ai 1.0.761 → 1.0.763
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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/error-reporting-issue-event.md +90 -1
- package/knowledge/2.0/apps/toga-blox/INDEX.md +2 -2
- package/knowledge/2.0/apps/toga-blox/workflows/landing-a-large-feature-branch.md +56 -1
- package/knowledge/2.0/apps/toga-blox/workflows/local-link-into-a-consumer-app.md +90 -1
- package/knowledge/2.0/apps/toga25-supply/architecture.md +20 -6
- package/knowledge/2.0/standards/backend-php.md +17 -1
- package/knowledge/2.0/standards/frontend.md +42 -6
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
22
22
|
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php, _underscore/Model/Client/Logs/Email.php, _underscore/Model/Client/Logs/EmailAttachment.php |
|
|
23
23
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Quad/ApprovalDecision.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
24
|
-
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/issue/get.php, tools/mvc/errors/post.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
24
|
+
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Query.php, _underscore/Database.php, _underscore/Exception/Business.php, _underscore/Exception/Database.php, _underscore/Test/DatabaseFingerprint.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/issue/get.php, tools/mvc/errors/post.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
25
25
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
26
26
|
| [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` is the 2.0 field type for blob-backed columns (S3 or local folder). | _underscore/Model.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/Invoice.php, dbchanges2/Core/HISTORIC/2024/2024-11b - item-fulfillments.sql |
|
|
27
27
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Forecast/Db/Db.php, _underscore/Component/Api/Netsuite/Netsuite.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/test_fetchrecord_routes.php, test/@dave/verify_je_classification.php, test/@dave/probe_je_accounts.php, test/@dave/probe_je_shape.php, test/@dave/fixer.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
@@ -6,12 +6,15 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-03
|
|
10
10
|
owners: ["dfranks", "jcardinal", "mhammontree", "ajean", "bala", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Error.php
|
|
13
|
+
- _underscore/Query.php
|
|
13
14
|
- _underscore/Database.php
|
|
14
15
|
- _underscore/Exception/Business.php
|
|
16
|
+
- _underscore/Exception/Database.php
|
|
17
|
+
- _underscore/Test/DatabaseFingerprint.php
|
|
15
18
|
- api2/Controller/Index.php
|
|
16
19
|
- worker2/Controller/Index.php
|
|
17
20
|
- _underscore/Model/Core/Logs/Issue.php
|
|
@@ -132,6 +135,61 @@ produced a different hash on every request — dedup never actually worked.
|
|
|
132
135
|
cannot orphan a curated Issue: an admin repoints the new fingerprint at the existing Issue
|
|
133
136
|
from the Tools console to merge them.
|
|
134
137
|
|
|
138
|
+
#### Database errors fingerprint on the MESSAGE, not the trace (added 2026-09-03)
|
|
139
|
+
|
|
140
|
+
The trace-based scheme above **over-merges database errors**, because they all surface from a
|
|
141
|
+
few shared query methods. Every DB failure is thrown as a plain `Exception` from `_Query`, so
|
|
142
|
+
their top ~5 frames are nearly identical (e.g. `_Query->getFoundRows()` → `processRoutePairs()`
|
|
143
|
+
→ `execute()`). The one thing that told two DB bugs apart — the message (errno + column/table) —
|
|
144
|
+
was exactly what the fingerprint **discards**. Result: production Issue `3L` (id 64) bundled a
|
|
145
|
+
`1146` "table doesn't exist" **plus four different `1054` "unknown column"** bugs, across
|
|
146
|
+
clients, into one Issue with ~2,514 occurrences — genuinely different defects hidden as one.
|
|
147
|
+
|
|
148
|
+
The fix routes DB errors down their own fingerprint path:
|
|
149
|
+
|
|
150
|
+
- **`_Query` now throws a typed `_Exception_Database`** (`_underscore/Exception/Database.php`,
|
|
151
|
+
`class _Exception_Database extends Exception`) instead of a plain `Exception`, carrying typed
|
|
152
|
+
`errorNumber` / `errorText` (getters `getErrorNumber()` / `getErrorText()`). It **extends plain
|
|
153
|
+
`Exception`, NOT `_Exception_Validation`**, so a DB error stays **HTTP 500 + creates a
|
|
154
|
+
`Logs.Issue`** (api2 maps only `_Exception_Validation` to 400). Every existing
|
|
155
|
+
`catch (Exception|Throwable)` and the deadlock-retry (which reads the stored errno, not the
|
|
156
|
+
type) are unaffected. It **deliberately does NOT keep the SQL** as a property/getter — that
|
|
157
|
+
avoids an easy leak of a query with literal values.
|
|
158
|
+
- **`_Error::buildFingerprint()` gains an early `instanceof _Exception_Database` branch** →
|
|
159
|
+
`buildDatabaseFingerprint()`, which keys the fingerprint on
|
|
160
|
+
**`errno + normalizeDatabaseErrorText(errorText)`** instead of stack frames. So DB errors
|
|
161
|
+
split by exact table/column, while everything else keeps the trace scheme.
|
|
162
|
+
|
|
163
|
+
`normalizeDatabaseErrorText()` masks the error text, **in this order**, so no per-row value or
|
|
164
|
+
echoed SQL fragment reaches the shared `Logs.IssueFingerprint` table:
|
|
165
|
+
|
|
166
|
+
1. Cap the input at 2000 chars.
|
|
167
|
+
2. **Strip the tenant DB prefix** `(Client|Archive|Logs)_<Tenant>.` from identifiers — so the
|
|
168
|
+
**same bug on two clients is ONE Issue** (which tenant is still recorded on `Event.clientId`).
|
|
169
|
+
3. Mask the value in `Duplicate entry '<value>' for key '<key>'` — **keep the key**, drop the
|
|
170
|
+
value (groups across values, still splits on a different key).
|
|
171
|
+
4. **DEFAULT-DENY:** keep a quoted token only if it matches a schema-identifier shape
|
|
172
|
+
`^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$`; otherwise mask it to `?`. This keeps
|
|
173
|
+
row values, password hashes, and echoed SQL fragments from a `1064` **out** of the shared
|
|
174
|
+
fingerprint table.
|
|
175
|
+
5. Digits → `N`.
|
|
176
|
+
|
|
177
|
+
`firstApplicationArea()` derives the context `areaPath` from the first application frame.
|
|
178
|
+
|
|
179
|
+
> **Forward-only, one-time re-grouping (expected).** Existing Issues keep their old fingerprint
|
|
180
|
+
> hashes; new events form new fingerprints. On deploy, Issue `3L` fans out into ~5 fresh Issues
|
|
181
|
+
> — fresh notifications and up to 5 new ClickUp tasks, **no carryover of any muting**. This is a
|
|
182
|
+
> one-time, deliberate effect. **No schema change.** (CTO-reviewed: AGREE-WITH-ALTERNATIVE —
|
|
183
|
+
> scope message-based grouping to DB errors ONLY, because arbitrary exception messages have no
|
|
184
|
+
> known shape to mask safely.)
|
|
185
|
+
|
|
186
|
+
> **No PHPUnit harness in `_underscore`.** There is only the vendored `library` test suite. The
|
|
187
|
+
> masking is covered by a **standalone CLI script**, `_underscore/Test/DatabaseFingerprint.php`
|
|
188
|
+
> (`php Test/DatabaseFingerprint.php`, 16 checks): the 5 errors that merged into `3L` now split
|
|
189
|
+
> into 5; same bug on two clients = one Issue; duplicate-entry groups across values but splits on
|
|
190
|
+
> the key; default-deny masks non-identifier values / hashes / SQL fragments; a different errno
|
|
191
|
+
> is a different Issue.
|
|
192
|
+
|
|
135
193
|
### Where each exception class routes (the class you throw IS the routing decision)
|
|
136
194
|
|
|
137
195
|
| Thrown class | HTTP | Sentry | `Logs.Issue` | Fingerprint / routing |
|
|
@@ -139,6 +197,7 @@ from the Tools console to merge them.
|
|
|
139
197
|
| `_Exception_Validation` | **400** | no | **no** | n/a — bad client input. Still leaves a full `Logs.Api` row (see [api2 request logging](../../api2/features/request-logging.md)) |
|
|
140
198
|
| `_Exception_Business` | **4xx** (was 500 before 2026-08-04) | yes | **yes** | stable `issueKey` (refactor-immune) → business recipients configured in the Tools console. **Now a subclass of `_Exception_Validation` — must be caught FIRST, see below** |
|
|
141
199
|
| plain global `Exception` | **500** | yes | **yes** | **trace**-based fingerprint (moves when code moves) → developer ClickUp task |
|
|
200
|
+
| `_Exception_Database` | **500** | yes | **yes** | **not thrown by hand** — `_Query` throws it for every DB error. **Fingerprinted by `errno + masked error text`, not trace** (below). Subclass of plain `Exception`, so it routes exactly like one → developer ClickUp task |
|
|
142
201
|
| `_Exception` | — | — | — | **never throw it deliberately** (below) |
|
|
143
202
|
|
|
144
203
|
Verified against `api2/Controller/Index.php` (≈L205-245, L369-377) and `_Error.php`
|
|
@@ -567,6 +626,16 @@ gracefully — it silently deletes all observability for the whole tier. Any cha
|
|
|
567
626
|
redactor (in `_underscore/Error.php` **and** `library/app/error/capture.php` together, per the
|
|
568
627
|
gotcha above), and until then **never paste a `Logs.Event.context` or `Logs_<Client>.Api` row
|
|
569
628
|
into a ticket, chat, or knowledge doc** — those rows carry live tokens and customer PII.
|
|
629
|
+
- **⚠ OPEN (pre-existing, not fixed 2026-09-03): the raw DB-error message — including ~120 chars
|
|
630
|
+
of SQL with literal values and the tenant DB name — is stored into `Issue.subject` /
|
|
631
|
+
`Event.errorMessage`.** `_Error.php` (~L413 and ~L487) does `substr(..., 255)` of the assembled
|
|
632
|
+
message ("Database Error Occurred… Query:\n<sql>") into those columns on the **shared** `Logs`
|
|
633
|
+
cluster. This predates the `_Exception_Database` work (the old plain `Exception` carried the
|
|
634
|
+
same message), so it did not block that change. **Recommended follow-up:** for
|
|
635
|
+
`_Exception_Database`, store the **masked signature**
|
|
636
|
+
(`'DB #' . errno . ': ' . normalizeDatabaseErrorText(errorText)`) as subject/errorMessage
|
|
637
|
+
instead of the raw SQL. The 2026-09-03 fix shipped **without** this — treat it as a known
|
|
638
|
+
open item.
|
|
570
639
|
- **Hand-written SQL is what a table rename actually breaks — and it fails *after* the model
|
|
571
640
|
succeeds.** The singular rename left plural literals in hand-written statements across four
|
|
572
641
|
files, so `$issue->save()` succeeded and then `UPDATE Issues …` threw **1146** before the
|
|
@@ -696,6 +765,26 @@ read you feed it — `_Error` promotes a *"read property on int"* warning into a
|
|
|
696
765
|
|
|
697
766
|
## Change history
|
|
698
767
|
|
|
768
|
+
- 2026-09-03 — **Database errors now fingerprint on the message, not the trace.** Root cause of
|
|
769
|
+
a real prod problem: Issue `3L` (id 64, ~2,514 occurrences) bundled a `1146` plus four
|
|
770
|
+
different `1054` bugs across clients into one Issue, because every DB error is thrown from a
|
|
771
|
+
few shared `_Query` methods with near-identical top frames and the trace scheme drops the
|
|
772
|
+
message — the only thing telling them apart. Fix: `_Query` (~L356) now throws a typed
|
|
773
|
+
**`_Exception_Database`** (`_underscore/Exception/Database.php`, extends **plain `Exception`**
|
|
774
|
+
so it stays 500 + `Logs.Issue`; typed `errorNumber`/`errorText`; deliberately keeps NO SQL to
|
|
775
|
+
avoid a value leak). `_Error::buildFingerprint()` gets an early `instanceof _Exception_Database`
|
|
776
|
+
branch → `buildDatabaseFingerprint()`, keyed on `errno + normalizeDatabaseErrorText(errorText)`.
|
|
777
|
+
The masker: cap 2000 chars; strip the tenant DB prefix so the same bug on two clients is ONE
|
|
778
|
+
Issue (tenant still on `Event.clientId`); mask the duplicate-entry value but keep the key;
|
|
779
|
+
**default-deny** — keep a quoted token only if it matches a schema-identifier shape, else `?`,
|
|
780
|
+
so no row values/hashes/SQL fragments reach the shared `Logs.IssueFingerprint`; digits → `N`.
|
|
781
|
+
Forward-only, one-time re-grouping on deploy (`3L` fans into ~5 fresh Issues, no muting
|
|
782
|
+
carryover, no schema change). CTO AGREE-WITH-ALTERNATIVE: scope message-grouping to DB errors
|
|
783
|
+
only. No PHPUnit harness in `_underscore`, so covered by a standalone CLI script
|
|
784
|
+
`_underscore/Test/DatabaseFingerprint.php` (16 checks). **Left OPEN (pre-existing, cso-flagged):**
|
|
785
|
+
the raw DB-error message with ~120 chars of literal-value SQL + the tenant DB name is still
|
|
786
|
+
`substr(...,255)` into `Issue.subject`/`Event.errorMessage` on the shared `Logs` cluster —
|
|
787
|
+
recommended follow-up is to store the masked signature there instead. (jcardinal)
|
|
699
788
|
- 2026-08-19 — Recorded that **email recipients are configured per `issueId`, not per `issueKey`**,
|
|
700
789
|
so they cannot be pre-configured before a condition first fires — the `Logs.Issue` row must exist
|
|
701
790
|
first, which means the **first occurrence of any new business exception always routes to a
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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`).
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
**
|
|
105
|
-
|
|
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-
|
|
8
|
+
updated: 2026-09-03
|
|
9
9
|
owners: [jcardinal, mhammontree, dfranks, bala, ajean]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -741,6 +741,15 @@ notified**. Pick deliberately:
|
|
|
741
741
|
> throw `_Exception_Validation` on an empty recipient.) Note `_Exception_Business` **extends**
|
|
742
742
|
> `_Exception_Validation`, so it also satisfies this `instanceof` test.
|
|
743
743
|
|
|
744
|
+
> **DB errors are their own subclass, and you never throw it.** `_Query` throws
|
|
745
|
+
> `_Exception_Database` (a subclass of plain `Exception`, so it routes identically — 500 +
|
|
746
|
+
> `Logs.Issue` → developer task) for every database failure. The one difference is identity:
|
|
747
|
+
> it is **fingerprinted by `errno + a masked error message`, not by the trace**, because all
|
|
748
|
+
> DB errors share the same few `_Query` frames and the trace scheme over-merged genuinely
|
|
749
|
+
> different bugs into one Issue. Do not "upgrade" a DB failure to another class to change its
|
|
750
|
+
> routing — the class and its message-based grouping are set in `_Query`/`_Error`. Detail:
|
|
751
|
+
> `2.0/apps/_underscore/features/error-reporting-issue-event.md`.
|
|
752
|
+
|
|
744
753
|
`_Exception_Business`'s constructor is `(issueKey, message, minimumUrgency)`. The `issueKey` is
|
|
745
754
|
the fingerprint, so the Issue's identity survives refactoring; a plain `Exception`'s
|
|
746
755
|
trace-based fingerprint changes when the code moves.
|
|
@@ -1024,6 +1033,13 @@ See: 2.0/apps/worker2/features/cross-account-aws-access.md
|
|
|
1024
1033
|
|
|
1025
1034
|
## Change history
|
|
1026
1035
|
|
|
1036
|
+
- 2026-09-03 — Error Handling: added a note under the exception-routing table that DB errors are
|
|
1037
|
+
their own subclass — `_Query` throws `_Exception_Database` (subclass of plain `Exception`, so it
|
|
1038
|
+
routes identically: 500 + `Logs.Issue` → developer task), but it is **fingerprinted by
|
|
1039
|
+
`errno` + a masked error message, not by the trace**, because all DB errors share the same few
|
|
1040
|
+
`_Query` frames and the trace scheme over-merged different bugs into one Issue. Do not "upgrade" a
|
|
1041
|
+
DB failure to another class to change its routing; the class and its message-based grouping live in
|
|
1042
|
+
`_Query`/`_Error`. Detail: `2.0/apps/_underscore/features/error-reporting-issue-event.md`. (jcardinal)
|
|
1027
1043
|
- 2026-09-02 — Database/SQL: any **cross-process counter or shared rollup needs its own DB alias,
|
|
1028
1044
|
committed immediately**. `Route.php` holds a transaction open on the default connection until the
|
|
1029
1045
|
request/job ends, so a counter written there is invisible to every other process (a fleet-wide
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-09-
|
|
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: [
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
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