toga-ai 1.0.147 → 1.0.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-08
10
- owners: [jcardinal]
9
+ updated: 2026-06-19
10
+ owners: [jcardinal, rgirish]
11
11
  files:
12
12
  - _underscore/Model/Client/ItemFulfillment.php
13
13
  - _underscore/Model/Client/ItemFulfillmentItem.php
@@ -101,15 +101,18 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
101
101
  post-delete hooks never fire; a standalone downstream DELETE doesn't immediately remove its
102
102
  upstream mirror. A later reconciling PUT cleans up stale records via set comparison. Full
103
103
  delete handling would need a `preDelete` mechanism + `PRE/DELETE` interceptor rows.
104
+ - **Transfer-order fulfillments are explicitly excluded from the upstream walk.** `reconcileUpstreamLevel()` returns `null` immediately when the downstream IF has `transferOrderId` set (no `salesOrderId`). This is intentional for standalone TOs (GroWrk v1). When SO→TO→SO chain scenarios are needed, Phase 5 of the GroWrk transfer order plan extends this method to walk `TransferOrders_SalesOrders` bridge tables — see `clients/growrk/features/transfer-order-flow.md`.
104
105
  - **Out of scope:** NetSuite sync of upstream IFs; transfer-order fulfillments
105
106
  (`transferOrderId`) are skipped.
106
107
  - **Performance:** every child write re-runs a full upstream walk (read-heavy, but each
107
108
  upstream record is written at most once — idempotent). Fine for normal fulfillment sizes.
108
109
 
109
110
  ## Change history
111
+ - 2026-06-19 — Documented explicit TO exclusion in reconcileUpstreamLevel; cross-linked GroWrk transfer order plan. (rgirish)
110
112
  - 2026-06-08 — Documented the Recursive Item Fulfillments engine (interceptor-driven upstream fulfillment mirroring, bundle scaling, reconcile loop). (jcardinal)
111
113
 
112
114
  ## Related docs
115
+ - GroWrk transfer order flow plan: `clients/growrk/features/transfer-order-flow.md`.
113
116
  - `_underscore` architecture (interceptors, `internalApiRequest`, `_Model` layer).
114
117
  - `api2` architecture (V2 metadata engine that fires these interceptors; the
115
118
  commit-logs / rollback-data-on-failure transaction invariant the throws rely on).
@@ -8,10 +8,13 @@
8
8
 
9
9
  ## The two axes
10
10
 
11
- 1. **Framework first.** Everything app- or framework-related lives under `1.0/` or
12
- `2.0/`. These are the two PHP frameworks:
13
- - **1.0** — the `App_` framework; core repo **`library`**.
14
- - **2.0** — the `_underscore` framework; core repo **`_underscore`**.
11
+ 1. **Framework first.** Everything app- or framework-related lives under `1.0/`,
12
+ `2.0/`, or `standalone/`:
13
+ - **1.0** — the `App_` PHP framework; core repo **`library`**.
14
+ - **2.0** — the `_underscore` PHP framework; core repo **`_underscore`**.
15
+ - **standalone** — apps that belong to **no** PHP framework (e.g. `togatech`, a React/TS
16
+ site; `forward`). They live under `standalone/apps/<repo>/`, have **no framework core**,
17
+ and depend on nothing implicitly. Not everything is 1.0 or 2.0.
15
18
  2. **Shared vs. client.** Within a framework, app/feature/architecture knowledge
16
19
  lives under `apps/<repo>/`. Anything specific to a single client lives at the top
17
20
  level under `clients/<client>/` and tags its `framework` in frontmatter.
@@ -29,6 +32,8 @@ knowledge/
29
32
  ├── 2.0/
30
33
  │ ├── apps/<repo>/architecture.md + features/*.md + workflows/*.md
31
34
  │ └── standards/*.md # 2.0 coding standards (_underscore)
35
+ ├── standalone/
36
+ │ └── apps/<repo>/architecture.md + features/*.md # non-PHP apps (e.g. togatech); no core
32
37
  └── clients/<client>/
33
38
  ├── profile.md
34
39
  ├── features/*.md # client-specific feature overrides (link back to apps/)
@@ -49,13 +54,14 @@ The shared identity/topology of every repo. Maintained **only by the skills**.
49
54
 
50
55
  - `repo` — on-disk repo/folder name (the key; unique).
51
56
  - `project` — logical project name.
52
- - `framework` — `"1.0"` or `"2.0"`.
57
+ - `framework` — `"1.0"`, `"2.0"`, or `"standalone"` (non-PHP apps with no framework core).
53
58
  - `role` — `"core"` (the framework base every same-framework repo depends on) or `"app"`.
54
59
  - `dependsOn` — additional repos this one depends on beyond the framework core
55
60
  (e.g. `api2` may later depend on `apiproxy`).
56
61
 
57
62
  **Dependency loading:** every repo implicitly depends on its framework's `core` repo
58
- (`_underscore` for 2.0, `library` for 1.0), plus any transitive `dependsOn`.
63
+ (`_underscore` for 2.0, `library` for 1.0), plus any transitive `dependsOn`. `standalone`
64
+ apps have no framework core, so they load only themselves plus any explicit `dependsOn`.
59
65
  `node knowledge.js deps --repo=<repo>` resolves the full load order.
60
66
 
61
67
  ## Client app-scope (`apps:` on `profile.md`)
@@ -14,7 +14,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
14
14
 
15
15
  ## 2.0 framework
16
16
 
17
- - **_underscore** (_Underscore) _(framework core)_ — 8 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
17
+ - **_underscore** (_Underscore) _(framework core)_ — 9 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
18
18
  - **worker2** (Worker) — 8 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
19
19
  - **api2** (API) — 4 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
20
20
  - **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -38,6 +38,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
38
38
  - **Compass Canada** (`compass-canada`) → [clients/compass-canada/INDEX.md](clients/compass-canada/INDEX.md)
39
39
  - **Compass USA** (`compass-usa`) → [clients/compass-usa/INDEX.md](clients/compass-usa/INDEX.md)
40
40
  - **Elite** (`elite`) → [clients/elite/INDEX.md](clients/elite/INDEX.md)
41
+ - **GroWrk** (`growrk`) → [clients/growrk/INDEX.md](clients/growrk/INDEX.md)
41
42
  - **New York City Department of Education** (`nycdoe`) → [clients/nycdoe/INDEX.md](clients/nycdoe/INDEX.md)
42
43
  - **Office Depot** (`office-depot`) → [clients/office-depot/INDEX.md](clients/office-depot/INDEX.md)
43
44
  - **Prudential Financial** (`prudential`) → [clients/prudential/INDEX.md](clients/prudential/INDEX.md)
@@ -0,0 +1,6 @@
1
+ # Client: GroWrk `growrk`
2
+
3
+ | Doc | Framework | Summary | Files |
4
+ |-----|-----------|---------|-------|
5
+ | [Transfer Order Flow (NetSuite SalesOrder workaround → dedicated TransferOrders)](features/transfer-order-flow.md) | 2.0 | GroWrk moves inventory between their own locations (warehouses, distribution points). | _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemReceipt.php, _underscore/Model/Client/ItemReceiptItem.php, _underscore/Trait/Netsuite/TransferOrder.php, dbchanges2/Client_Growrk/, dbchanges2/Client/ |
6
+ | [GroWrk](profile.md) | 2.0 | GroWrk is a 2.0 client using TOGA for inventory and order management. | |
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: "Transfer Order Flow (NetSuite SalesOrder workaround → dedicated TransferOrders)"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: growrk
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-06-19
10
+ owners: ["rgirish"]
11
+ files:
12
+ - _underscore/Model/Client/TransferOrder.php
13
+ - _underscore/Model/Client/TransferOrderItem.php
14
+ - _underscore/Model/Client/ItemFulfillment.php
15
+ - _underscore/Model/Client/ItemReceipt.php
16
+ - _underscore/Model/Client/ItemReceiptItem.php
17
+ - _underscore/Trait/Netsuite/TransferOrder.php
18
+ - dbchanges2/Client_Growrk/
19
+ - dbchanges2/Client/
20
+ related:
21
+ - 2.0/apps/_underscore/features/recursive-item-fulfillments.md
22
+ - 2.0/apps/worker2/architecture.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ GroWrk moves inventory between their own locations (warehouses, distribution points). Historically this was done by creating zero-dollar SalesOrders with `holdInvoice=true` in NetSuite — polluting sales reporting, distorting demand planning, and creating audit confusion. This feature introduces a dedicated `TransferOrders` record type in the TOGA database. NetSuite continues to receive transfer orders as SalesOrders with `holdInvoice=true` (no NS-side change); the distinction is made and stored only on the TOGA side.
28
+
29
+ ## Key files / entry points
30
+
31
+ | File | Role |
32
+ |---|---|
33
+ | `_underscore/Model/Client/TransferOrder.php` | TO model — origin/destination locations, number, serviceRequestId, purchaseOrderId, c_netsuiteInternalSalesOrderId |
34
+ | `_underscore/Model/Client/TransferOrderItem.php` | TO line items — itemId, quantity, lineNumber |
35
+ | `_underscore/Trait/Netsuite/TransferOrder.php` | NS sync trait — **currently a stub** (only declares c_netsuiteInternalSalesOrderId); `syncNetsuiteTransferOrder()` not yet implemented |
36
+ | `_underscore/Model/Client/ItemFulfillment.php` | Outbound closure — `transferOrderId` FK already present; `salesOrderId` must be made nullable |
37
+ | `_underscore/Model/Client/ItemReceipt.php` | Inbound closure — `transferOrderId` FK already present |
38
+ | `dbchanges2/Client_Growrk/` | Client-specific schema fixes (dtCreated/dtUpdated/isActive on TransferOrders, nullable XOR columns) |
39
+ | `dbchanges2/Client/` | All-client bridge tables (8 new SO↔TO and PO↔TO bridge tables) |
40
+
41
+ ## How it works
42
+
43
+ ### Identification signal
44
+ A NetSuite SalesOrder is a TransferOrder if and only if: `holdInvoice === true` AND `total === 0`. Both conditions must hold — `holdInvoice` alone is not sufficient.
45
+
46
+ ### Two-stage closure
47
+ - **Outbound:** An `ItemFulfillment` with `transferOrderId` set (not `salesOrderId`) closes the outbound side — items leave the origin location.
48
+ - **Inbound:** An `ItemReceipt` with `transferOrderId` set (not `purchaseOrderId`) closes the inbound side — items arrive at the destination location.
49
+
50
+ ### Chain positions
51
+ Transfer orders can sit in several positions in the order chain:
52
+ 1. **Standalone TO** — pure location-to-location move, no SO/PO involved
53
+ 2. **SO → TO** — customer SO triggers an internal TO (outbound from origin)
54
+ 3. **PO → TO** — vendor PO received via TO (move to warehouse)
55
+ 4. **SO → TO → SO** — customer-facing SO, internal move, downstream SO to fulfill
56
+
57
+ ## Data model
58
+
59
+ ### TransferOrders (prod — `Client_Growrk`)
60
+ | Column | Type | Notes |
61
+ |---|---|---|
62
+ | id | int PK | |
63
+ | uuid | char(36) | |
64
+ | originLocationId | int FK | NOT NULL |
65
+ | destinationLocationId | int FK | NOT NULL |
66
+ | dateOrder | date | nullable |
67
+ | number | varchar | currently varchar — generate as `TO-<tranId>` from NS in sync trait |
68
+ | serviceRequestId | int FK | nullable |
69
+ | purchaseOrderId | int FK | nullable |
70
+ | customerIdentificationNumber | varchar | nullable |
71
+ | c_netsuiteInternalSalesOrderId | varchar | NS SalesOrder internal ID |
72
+ | **dtCreated** | datetime | **MISSING — must add in Phase 1** |
73
+ | **dtUpdated** | datetime | **MISSING — must add in Phase 1** |
74
+ | **isActive** | tinyint | **MISSING — must add in Phase 1** |
75
+
76
+ ### Columns that need schema fixes before first use
77
+ - `ItemFulfillments.salesOrderId` — currently NOT NULL; must be made nullable (XOR with transferOrderId)
78
+ - `ItemFulfillmentItems.salesOrderItemId` — currently NOT NULL; must be made nullable
79
+ - `ItemReceiptItems.purchaseOrderItemId` — currently NOT NULL; must be made nullable (XOR with transferOrderItemId)
80
+
81
+ ### New bridge tables (all 8 in `dbchanges2/Client/` — applies to all clients)
82
+ | Table | FK A | FK B |
83
+ |---|---|---|
84
+ | SalesOrders_TransferOrders | salesOrderId | transferOrderId |
85
+ | TransferOrders_SalesOrders | transferOrderId | salesOrderId |
86
+ | PurchaseOrders_TransferOrders | purchaseOrderId | transferOrderId |
87
+ | TransferOrders_PurchaseOrders | transferOrderId | purchaseOrderId |
88
+ | SalesOrderItems_TransferOrderItems | salesOrderItemId | transferOrderItemId |
89
+ | TransferOrderItems_SalesOrderItems | transferOrderItemId | salesOrderItemId |
90
+ | PurchaseOrderItems_TransferOrderItems | purchaseOrderItemId | transferOrderItemId |
91
+ | TransferOrderItems_PurchaseOrderItems | transferOrderItemId | purchaseOrderItemId |
92
+
93
+ `Core.Records` already has id=312 (`_Model_Client_TransferOrder`) and id=313 (`_Model_Client_TransferOrderItem`) registered.
94
+
95
+ ## Implementation plan (5 phases — team review pending)
96
+
97
+ Full plan saved at `/Applications/AMPPS/www/transfer-order-plan.md`.
98
+
99
+ | Phase | Work | Status |
100
+ |---|---|---|
101
+ | 1 | Schema fixes (dtCreated/dtUpdated/isActive, nullable XOR columns, 8 bridge tables, Core RecordFields) | Pending |
102
+ | 2 | PHP model updates (TransferOrder.php fields, 8 bridge model stubs) | Pending |
103
+ | 3 | _Trait_Netsuite_TransferOrder sync impl + ItemReceipt TO path | Pending |
104
+ | 4 | worker2 _Worker_Netsuite_TransferOrder action class | Pending |
105
+ | 5 | Upstream IF chain extension (SO→TO→SO walk) | Deferred |
106
+
107
+ ## Gotchas / known issues
108
+
109
+ - **`dtCreated`/`dtUpdated` MISSING from `TransferOrders` in prod.** Any `_Model_Client_TransferOrder::save()` call will fail silently until Phase 1 runs. This is a hard blocker — nothing else works until the schema fix lands.
110
+ - **XOR is application-layer only.** MySQL does not enforce that exactly one of `salesOrderId`/`transferOrderId` is set on `ItemFulfillments`. The sync trait and any POST interceptor must validate this explicitly.
111
+ - **`_Trait_Netsuite_TransferOrder` is a stub.** It declares only `$c_netsuiteInternalSalesOrderId` — there is no `syncNetsuiteTransferOrder()` method yet.
112
+ - **Upstream IF chain explicitly skips TOs today.** `ItemFulfillment::reconcileUpstreamLevel()` returns null when `transferOrderId` is set — the comment reads "no salesOrderId (transfer order) → return null (out of scope)". Phase 5 will extend this.
113
+ - **Lazy transaction gotcha.** `_Database::register()` auto-starts a lazy transaction since Apr 2026. The worker must call `_Database::transactionCommit(DB_CLIENT)` before any SQS dispatch or the TO insert rolls back silently.
114
+ - **Bridge table scope confirmed as all-clients.** The 8 bridge tables go in `dbchanges2/Client/` (not `Client_Growrk/`). Schema fixes (dtCreated etc.) stay in `Client_Growrk/` — other clients don't need them.
115
+ - **NS identification: both conditions required.** `holdInvoice=true` alone is not a safe signal — must also confirm `total === 0`.
116
+
117
+ ## Change history
118
+
119
+ - 2026-06-19 — Designed full transfer order flow; confirmed DB state in prod; produced 5-phase implementation plan pending team review (rgirish)
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: "GroWrk"
3
+ framework: "2.0"
4
+ apps:
5
+ - _underscore
6
+ - api2
7
+ - worker2
8
+ - dbchanges2
9
+ project: _Underscore
10
+ client: growrk
11
+ type: profile
12
+ status: active
13
+ updated: 2026-06-19
14
+ owners: ["rgirish"]
15
+ files: []
16
+ related:
17
+ - clients/growrk/features/transfer-order-flow.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ GroWrk is a 2.0 client using TOGA for inventory and order management. Their primary integration involves inventory movements between locations (warehouses, distribution points). NetSuite is the ERP. The key engineering challenge is that GroWrk historically used zero-dollar SalesOrders with holdInvoice=true in NetSuite as a workaround to represent internal inventory transfers — work is underway to replace this with a dedicated TransferOrders record type in the TOGA DB while keeping NetSuite unchanged.
@@ -6,8 +6,8 @@ project: TOGA Technology Website
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-18
10
- owners: ["ajean"]
9
+ updated: 2026-06-19
10
+ owners: ["ajean", "jcardinal"]
11
11
  files:
12
12
  - togatech/vite.config.ts
13
13
  - togatech/scripts/prerender.mjs
@@ -24,7 +24,7 @@ related: []
24
24
  ---
25
25
 
26
26
  ## Summary
27
- Makes togatech.com visible and citable to search engines **and** AI answer engines (ChatGPT/Perplexity/Claude search, Google AI Overviews). The site is a CSR-only Vite/React SPA, so non-JS crawlers saw an empty `<div id="root">`. This feature fixes a production de-indexing bug and adds **build-time prerendering** so every static route ships real HTML, plus single-source per-page meta and JSON-LD structured data. Shipped on branches `fix-seo-deindex-emergency` (PR #29) and `feature-seo-aeo-prerender` (PR #30) **not yet merged/deployed** as of 2026-06-18.
27
+ Makes togatech.com visible and citable to search engines **and** AI answer engines (ChatGPT/Perplexity/Claude search, Google AI Overviews). The site is a CSR-only Vite/React SPA, so non-JS crawlers saw an empty `<div id="root">`. This feature fixes a production de-indexing bug and adds **build-time prerendering** so every static route ships real HTML, plus single-source per-page meta and JSON-LD structured data. Shipped on branches `fix-seo-deindex-emergency` (PR #29) and `feature-seo-aeo-prerender` (PR #30); **merged into `_production` on 2026-06-19** (Contact page conflict resolved — see gotchas).
28
28
 
29
29
  ## Key files / entry points
30
30
  - `vite.config.ts` — `robotsPlugin()` now gates on **Vite `mode`** (was `NODE_ENV && VITE_ENV`).
@@ -35,7 +35,7 @@ Makes togatech.com visible and citable to search engines **and** AI answer engin
35
35
 
36
36
  ## How it works
37
37
  1. **robots:** `robotsPlugin` copies `robots.prod.txt` only when `mode === "production"`; beta/gamma/dev get `robots.dev.txt` (`Disallow: /`). `robots.prod.txt` carries an explicit AI-bot allow-list (GPTBot, ClaudeBot, PerplexityBot, …) + `https://` sitemap.
38
- 2. **prerender:** `npm run build` = `tsc && vite build && node scripts/prerender.mjs`. The script serves `dist/` (reading the original shell once, serving it for all nav routes), snapshots each route in headless Chrome, and writes `dist/<route>/index.html`. Runs in a real browser, so **no SSR/window guards needed**.
38
+ 2. **prerender:** `npm run build` = `tsc && vite build && npx puppeteer browsers install chrome && node scripts/prerender.mjs` (the Chrome-install step was added 2026-06-19 — see gotchas). The script serves `dist/` (reading the original shell once, serving it for all nav routes), snapshots each route in headless Chrome, and writes `dist/<route>/index.html`. Runs in a real browser, so **no SSR/window guards needed**.
39
39
  3. **meta:** each page renders `<Seo {...FIELDS.meta} />` — title/description/keywords/canonical/OG/Twitter from one place per page; canonical/OG URLs derived from one `SITE_URL`. `<JsonLd>` (Helmet `<script type="application/ld+json">`) is baked into the snapshot.
40
40
  4. **structured data:** Organization + WebSite site-wide (AppLayout), per-page BreadcrumbList (Seo), platform SoftwareApplication (OurPlatform), LocalBusiness×5 (from Contact locations), Person (from About team) — all **derived from existing FIELDS** (single source).
41
41
 
@@ -48,7 +48,10 @@ None — uniform.
48
48
  ## Gotchas / known issues
49
49
  - **De-indexing bug (root cause):** `robotsPlugin` required `NODE_ENV && VITE_ENV==="production"`, but build scripts set neither → every build shipped `robots.dev.txt` (`Disallow: /`). **Do NOT gate robots on `NODE_ENV`** — `vite build` sets it to `production` for *every* mode, which would expose beta/gamma. Gate on Vite `mode`.
50
50
  - **`.npmrc` has `ignore-scripts=true`** → the `postbuild` lifecycle hook never fires. Prerender is therefore chained directly in the `build` script, not a `postbuild` hook.
51
- - **Prerender tooling:** `vite-react-ssg` supports React Router **6 only** (project is RR 7.9); `react-snap` is stale (2022) and won't launch on Node 24. Hence a custom Puppeteer snapshot. Puppeteer's Chrome must be installed at build time: `npx puppeteer browsers install chrome`.
51
+ - **Prerender tooling:** `vite-react-ssg` supports React Router **6 only** (project is RR 7.9); `react-snap` is stale (2022) and won't launch on Node 24. Hence a custom Puppeteer snapshot.
52
+ - **🔴 Chrome must be installed in the build (now wired in):** Amplify/CodeBuild's `npm ci` does **not** download a browser, so `puppeteer.launch()` fails with `Could not find Chrome (ver. …)` at `/root/.cache/puppeteer`. Fixed 2026-06-19 by adding `npx puppeteer browsers install chrome` to the `build` script immediately before `node scripts/prerender.mjs` (same default cache `launch()` resolves from). puppeteer 25 also requires **Node ≥ 22.12** — set the Amplify image accordingly. (The step re-downloads ~150 MB per build unless `/root/.cache/puppeteer` is cached.)
53
+ - **🔴 `package-lock.json` must be committed:** Amplify preBuild runs `npm ci`, which refuses to run without a committed lockfile (`npm error code EUSAGE`) and will **not** generate one. The lockfile is not gitignored — it was simply never committed; `npm install` once and commit it.
54
+ - **Contact page merge (2026-06-19):** when merging the SEO/AEO feature into `_production`, `ContactPage.tsx` conflicted. Resolution: keep production's reCAPTCHA conditional wrap (`GoogleReCaptchaProvider` mounts only when `VITE_RECAPTCHA_V3_SITE_KEY` is set) **and** the `<Seo>`/`<JsonLd>` components; drop the incoming `<Helmet>` block (superseded by `<Seo>`).
52
55
  - **Prerender fail-fast:** a route that never mounts (`#root > *` times out) aborts the build with `process.exit(1)` — otherwise it would snapshot the empty shell (no SEO/JSON-LD) and still exit 0.
53
56
  - **🔴 Hosting requirement:** CloudFront/S3 must serve each prerendered `dist/<route>/index.html` for its path; a blanket SPA rewrite to root `index.html` serves the empty shell and defeats the prerender.
54
57
  - **og:image dedupe:** static OG/Twitter tags were removed from `index.html` because `<Seo>` emits per-page ones (Helmet appends rather than replacing pre-existing static tags → duplicates otherwise).
@@ -56,6 +59,7 @@ None — uniform.
56
59
  - **Build size:** JS bundle is ~14.8 MB (4.9 MB gzip) — a real CWV/LCP risk; code-splitting is a separate follow-up.
57
60
 
58
61
  ## Change history
62
+ - 2026-06-19 — Merged SEO/AEO into `_production` (resolved ContactPage conflict: reCAPTCHA + Seo/JsonLd, dropped Helmet). Fixed Amplify build: committed `package-lock.json` (npm ci EUSAGE) and added `npx puppeteer browsers install chrome` to the build before prerender. (jcardinal)
59
63
  - 2026-06-18 — Initial SEO/AEO/GEO implementation: robots mode-gate + AI allow-list + sitemap/llms; Puppeteer prerender (fail-fast); shared `<Seo>` single-source meta + canonical; JSON-LD (Organization/WebSite/Breadcrumb/SoftwareApplication/LocalBusiness×5/Person×12). PRs #29 + #30, pending merge. (ajean)
60
64
 
61
65
  ## Related docs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.147",
3
+ "version": "1.0.149",
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",