@decocms/start 2.22.0 → 2.24.0

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.
@@ -43,7 +43,7 @@ Each phase has entry/exit criteria. Follow in order. Automation % indicates how
43
43
  | [2](#phase-2--signals--state) | Signals & State | ~50% | `references/signals/` |
44
44
  | [3](#phase-3--deco-framework) | Deco Framework Elimination | ~80% | `references/deco-framework/` |
45
45
  | [4](#phase-4--commerce--types) | Commerce Types & UI | ~70% | `references/commerce/` |
46
- | [5](#phase-5--platform-hooks) | Platform Hooks | 0% | `references/platform-hooks/` |
46
+ | [5](#phase-5--platform-hooks) | Platform Hooks (factories, W12+) | template | `references/platform-hooks-factories.md` |
47
47
  | [6](#phase-6--islands-elimination) | Islands Elimination | ~60% | `references/islands.md` |
48
48
  | [7](#phase-7--section-registry) | Section Registry & Setup | 0% | `references/async-rendering.md` |
49
49
  | [8](#phase-8--routes--cms) | Routes & CMS | template | `references/navigation.md` |
@@ -152,14 +152,18 @@ See: `references/commerce/README.md`, `references/vtex-commerce.md`
152
152
 
153
153
  **Entry**: Phase 4 complete
154
154
 
155
- **Actions** (manual implementation):
156
- 1. Create `src/hooks/useCart.ts` — module-level singleton + listener pattern
157
- 2. Create `src/hooks/useUser.ts`, `src/hooks/useWishlist.ts` (stubs or real)
158
- 3. Wire VTEX API calls via `@decocms/apps` invoke functions
155
+ **Actions (Wave 12+ factory-based — current)**:
156
+ 1. `src/hooks/useCart.ts` — 5-line shim around `createUseCart` from `@decocms/apps/vtex/hooks/createUseCart`
157
+ 2. `src/hooks/useUser.ts` 5-line shim around `createUseUser`
158
+ 3. `src/hooks/useWishlist.ts` 5-line shim around `createUseWishlist`
159
+ 4. The migration template (`scripts/migrate/templates/hooks.ts`) emits all three for VTEX sites automatically.
160
+
161
+ For non-VTEX platforms, scaffold no-op stubs using `@decocms/start/sdk/signal` (see factories doc § "Non-VTEX platforms").
159
162
 
160
163
  **Exit**: Cart add/remove works, no `apps/{platform}/hooks` imports
161
164
 
162
- See: `references/platform-hooks/README.md`
165
+ See: `references/platform-hooks-factories.md` (canonical, Wave 12+).
166
+ Pre-W12 manual approach is preserved at `references/platform-hooks/README.md` for sites that haven't migrated to factories yet.
163
167
 
164
168
  ---
165
169
 
@@ -356,7 +360,8 @@ For sites with 100+ sections:
356
360
  | Signals → TanStack Store | `references/signals/` |
357
361
  | Deco framework elimination | `references/deco-framework/` |
358
362
  | Commerce & widget types | `references/commerce/` |
359
- | Platform hooks (VTEX) | `references/platform-hooks/` |
363
+ | Platform hooks (VTEX, factories — Wave 12+) | `references/platform-hooks-factories.md` |
364
+ | Platform hooks (manual, legacy pre-W12) | `references/platform-hooks/README.md` |
360
365
  | Vite configuration | `references/vite-config/` |
361
366
  | Automation commands | `references/codemod-commands.md` |
362
367
  | Islands elimination | `references/islands.md` |
@@ -1,16 +1,29 @@
1
- # Platform Hooks Migration
1
+ # Platform Hooks Migration (legacy reference)
2
2
 
3
- Platform hooks (useCart, useUser, useWishlist) are the most complex migration target because they have real business logic.
3
+ > **This document describes the pre-Wave-12 manual approach.** New
4
+ > migrations should follow
5
+ > [`platform-hooks-factories.md`](../platform-hooks-factories.md), which
6
+ > covers the `createUseCart` / `createUseUser` / `createUseWishlist`
7
+ > factories from `@decocms/apps/vtex/hooks`. The factories collapse
8
+ > everything below into a 5-line site shim per hook.
9
+ >
10
+ > This file is kept for sites that scaffolded before the factories
11
+ > existed — typically sites with `src/lib/vtex-cart-server.ts` or
12
+ > hand-rolled `createServerFn` calls to VTEX endpoints inside
13
+ > `src/hooks/useCart.ts`. See the **"Migrating off the manual approach"**
14
+ > section in the new doc for the cleanup playbook.
4
15
 
5
- ## Strategy
16
+ ---
17
+
18
+ ## Strategy (legacy)
6
19
 
7
20
  All hooks are **site-local**. No Vite alias tricks. No compat layers.
8
21
 
9
- - Active platform hooks (VTEX for this store) -> `~/hooks/useCart.ts` with real implementation
10
- - Inactive platform hooks (Wake, Shopify, etc.) -> `~/hooks/platform/{name}.ts` with no-op stubs
11
- - Auth hooks -> `~/hooks/useUser.ts`, `~/hooks/useWishlist.ts`
22
+ - Active platform hooks (VTEX for this store) `~/hooks/useCart.ts` with real implementation
23
+ - Inactive platform hooks (Wake, Shopify, etc.) `~/hooks/platform/{name}.ts` with no-op stubs
24
+ - Auth hooks `~/hooks/useUser.ts`, `~/hooks/useWishlist.ts`
12
25
 
13
- ## VTEX useCart (Real Implementation)
26
+ ## VTEX useCart (Manual Implementation)
14
27
 
15
28
  ### Why Server Functions Are Required
16
29
 
@@ -18,7 +31,7 @@ The storefront domain (e.g., `my-store.deco.site`) differs from the VTEX checkou
18
31
 
19
32
  Use TanStack Start `createServerFn` to create server-side proxy functions that the client hook calls transparently.
20
33
 
21
- ### Server Functions (~/lib/vtex-cart-server.ts)
34
+ ### Server Functions (`~/lib/vtex-cart-server.ts`)
22
35
 
23
36
  ```typescript
24
37
  import { createServerFn } from "@tanstack/react-start";
@@ -40,50 +53,46 @@ export const getOrCreateCart = createServerFn({ method: "GET" })
40
53
  "X-VTEX-API-AppKey": API_KEY,
41
54
  "X-VTEX-API-AppToken": API_TOKEN,
42
55
  },
43
- body: JSON.stringify({ expectedOrderFormSections: ["items", "totalizers", "shippingData", "clientPreferencesData", "storePreferencesData", "marketingData"] }),
56
+ body: JSON.stringify({
57
+ expectedOrderFormSections: [
58
+ "items",
59
+ "totalizers",
60
+ "shippingData",
61
+ "clientPreferencesData",
62
+ "storePreferencesData",
63
+ "marketingData",
64
+ ],
65
+ }),
44
66
  });
45
67
  return res.json();
46
68
  });
47
-
48
- export const addItemsToCart = createServerFn({ method: "POST" })
49
- .validator((data: { orderFormId: string; items: any[] }) => data)
50
- .handler(async ({ data }) => {
51
- const res = await fetch(
52
- `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm/${data.orderFormId}/items`,
53
- {
54
- method: "POST",
55
- headers: { "Content-Type": "application/json", "X-VTEX-API-AppKey": API_KEY, "X-VTEX-API-AppToken": API_TOKEN },
56
- body: JSON.stringify({ orderItems: data.items }),
57
- },
58
- );
59
- return res.json();
60
- });
61
-
62
- export const updateCartItems = createServerFn({ method: "POST" })
63
- .validator((data: { orderFormId: string; items: any[] }) => data)
64
- .handler(async ({ data }) => {
65
- const res = await fetch(
66
- `https://${ACCOUNT}.vtexcommercestable.com.br/api/checkout/pub/orderForm/${data.orderFormId}/items/update`,
67
- {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json", "X-VTEX-API-AppKey": API_KEY, "X-VTEX-API-AppToken": API_TOKEN },
70
- body: JSON.stringify({ orderItems: data.items }),
71
- },
72
- );
73
- return res.json();
74
- });
75
69
  ```
76
70
 
77
- ### Hook (~/hooks/useCart.ts)
71
+ > **Don't write code like this in new sites.** The factories already wrap
72
+ > all canonical VTEX action endpoints (cart, session, masterdata,
73
+ > newsletter, checkout) in `@decocms/apps/vtex/actions/*`. The migration
74
+ > template scaffolds `src/server/invoke.gen.ts` which exposes them as
75
+ > typed server functions; `~/server/invoke.ts` then re-exports them
76
+ > under `invoke.vtex.actions.*`. The factory consumes that surface and
77
+ > returns the legacy hook shape.
78
78
 
79
- Key design decisions:
79
+ ### Hook (`~/hooks/useCart.ts`)
80
+
81
+ Key design decisions of the legacy manual hook:
80
82
  - **Module-level singleton state** shared across all component instances
81
83
  - **Pub/sub pattern** (`_listeners` Set) for notifying React components of changes
82
- - **Cookie-based session**: reads/writes `checkout.vtex.com__orderFormId` on the **client** side (not VTEX's domain cookie)
84
+ - **Cookie-based session**: reads/writes `checkout.vtex.com__orderFormId` on the **client** side
83
85
  - Returns `cart` and `loading` with `.value` getter/setter for backward compat with Preact-era components
84
86
  - Lazy initialization: cart is fetched on first component mount, not on module load
85
87
  - Exports `itemToAnalyticsItem` for cart-specific analytics mapping
86
88
 
89
+ The factory in `@decocms/apps/vtex/hooks/createUseCart` ships *exactly*
90
+ these semantics — that's the implementation behind the new shim. If your
91
+ site needs to extend behaviour (e.g. extra analytics events, custom
92
+ post-add hooks), prefer wrapping the factory's exports rather than
93
+ forking back to a manual hook; the factory leaves space for that
94
+ without giving up the upgrade path.
95
+
87
96
  ### Cross-Domain Checkout
88
97
 
89
98
  The minicart's "Finalizar Compra" button must link to the VTEX checkout domain with the `orderFormId` as a query parameter — the VTEX domain can't read the storefront's cookies:
@@ -92,10 +101,8 @@ The minicart's "Finalizar Compra" button must link to the VTEX checkout domain w
92
101
  const checkoutUrl = `https://secure.${STORE_DOMAIN}/checkout/?orderFormId=${orderFormId}`;
93
102
  ```
94
103
 
95
- ### VTEX Types (~/types/vtex.ts)
96
-
97
- Site-local types for VTEX-specific structures:
98
- - `OrderFormItem`, `SimulationOrderForm`, `Sla`, `SKU`, `VtexProduct`
104
+ This pattern is unchanged by the factory — it's a UI concern, not a hook
105
+ concern. Implement it in your minicart component as before.
99
106
 
100
107
  ## Inactive Platform Stubs
101
108
 
@@ -130,9 +137,13 @@ export function useWishlist() {
130
137
  }
131
138
  ```
132
139
 
133
- Create similar stubs for: `shopify.ts`, `linx.ts`, `vnda.ts`, `nuvemshop.ts`.
140
+ Create similar stubs for: `shopify.ts`, `linx.ts`, `vnda.ts`, `nuvemshop.ts`. Match the return shape to what each platform's AddToCartButton expects (some use `addItem`, others `addItems`).
134
141
 
135
- Match the return shape to what each platform's AddToCartButton expects (some use `addItem`, others `addItems`).
142
+ The factory equivalent for non-VTEX sites is documented in
143
+ [`platform-hooks-factories.md` § "Non-VTEX platforms"](../platform-hooks-factories.md#non-vtex-platforms).
144
+ Until each platform has its own factory in `@decocms/apps`, the stub
145
+ shape above is still correct — but use `@decocms/start/sdk/signal`
146
+ instead of hand-rolled `{ value: ... }` objects.
136
147
 
137
148
  ## Import Rewrites
138
149
 
@@ -143,7 +154,6 @@ sed -i '' 's|from "apps/vtex/hooks/useWishlist.ts"|from "~/hooks/useWishlist"|g'
143
154
  sed -i '' 's|from "apps/vtex/utils/types.ts"|from "~/types/vtex"|g'
144
155
  sed -i '' 's|from "apps/shopify/hooks/useCart.ts"|from "~/hooks/platform/shopify"|g'
145
156
  sed -i '' 's|from "apps/wake/hooks/useCart.ts"|from "~/hooks/platform/wake"|g'
146
- # etc. for all platforms
147
157
  ```
148
158
 
149
159
  ## Verification
@@ -0,0 +1,186 @@
1
+ # Platform Hooks — Factory Pattern
2
+
3
+ > **Canonical reference for `createUseCart` / `createUseUser` /
4
+ > `createUseWishlist` from `@decocms/apps/vtex/hooks`.** These factories
5
+ > ship the legacy invoke-based hook semantics that migrated Fresh sites
6
+ > depend on — module-level singleton state, listener-based re-render,
7
+ > awaitable async actions, signal-shaped accessors. Sites consume them
8
+ > as 5-line shims.
9
+
10
+ This doc replaces the pre-W12 "manual `createServerFn` per VTEX endpoint"
11
+ approach in
12
+ [`platform-hooks/README.md`](./platform-hooks/README.md). If you scaffolded
13
+ a site before Wave 12 (≤ `@decocms/apps@2.x` / `@decocms/start@2.18`), see
14
+ "Migrating off the manual approach" at the bottom.
15
+
16
+ ---
17
+
18
+ ## What the factories own
19
+
20
+ | Concern | Where it lives |
21
+ |---|---|
22
+ | Module-level singleton state (`cart`, `user`, `wishlist`) | Inside the factory closure |
23
+ | `useEffect` + `forceRender(c => c + 1)` re-render pattern | Factory |
24
+ | Signal-shaped accessors (`cart.value`, `user.value`) | Factory |
25
+ | Awaitable mutations (`await addItem(...)`) | Factory |
26
+ | `itemToAnalyticsItem` helper (cart) | Factory |
27
+ | Wishlist arg swap (`productId` ↔ `productGroupId`) | Factory |
28
+ | **VTEX HTTP calls** | NOT in the factory — provided by the `invoke` proxy you pass in |
29
+
30
+ The factory only wires state + listeners. The site provides an `invoke`
31
+ object whose shape is structurally typed against
32
+ `CreateUseCartInvoke` / `CreateUseUserInvoke` / `CreateUseWishlistInvoke`
33
+ (exported next to each factory). The migration template generates an
34
+ `invoke` proxy in `src/server/invoke.ts` that meets all three shapes
35
+ without any extra wiring.
36
+
37
+ ---
38
+
39
+ ## Site-local shim (the entire file)
40
+
41
+ ### `src/hooks/useCart.ts`
42
+
43
+ ```ts
44
+ import { createUseCart } from "@decocms/apps/vtex/hooks/createUseCart";
45
+ import { invoke } from "~/server/invoke";
46
+
47
+ export type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types";
48
+
49
+ export const { useCart, resetCart, itemToAnalyticsItem } = createUseCart({
50
+ invoke,
51
+ });
52
+ ```
53
+
54
+ ### `src/hooks/useUser.ts`
55
+
56
+ ```ts
57
+ import { createUseUser } from "@decocms/apps/vtex/hooks/createUseUser";
58
+ import { invoke } from "~/server/invoke";
59
+
60
+ export type { Person } from "@decocms/apps/vtex/loaders/user";
61
+
62
+ export const { useUser, resetUser } = createUseUser({ invoke });
63
+ ```
64
+
65
+ ### `src/hooks/useWishlist.ts`
66
+
67
+ ```ts
68
+ import { createUseWishlist } from "@decocms/apps/vtex/hooks/createUseWishlist";
69
+ import { invoke } from "~/server/invoke";
70
+
71
+ export type { WishlistItem } from "@decocms/apps/vtex/loaders/wishlist";
72
+
73
+ export const { useWishlist, resetWishlist } = createUseWishlist({ invoke });
74
+ ```
75
+
76
+ That's the whole hook — no `createServerFn`, no VTEX URLs, no `AppKey`
77
+ plumbing. `npm run migrate` scaffolds these three files automatically
78
+ when `--platform vtex` is set; if you regenerate, the migration template
79
+ in `scripts/migrate/templates/hooks.ts` is the source of truth.
80
+
81
+ ---
82
+
83
+ ## Why a factory and not a single hook?
84
+
85
+ > Two reasons that come up repeatedly when reviewing migration PRs.
86
+
87
+ 1. **`useCart` already exists in apps.** The canonical `vtex/hooks/useCart.ts`
88
+ is built on TanStack Query and exposes the `Minicart` shape — that is
89
+ the shape new code should target. The factory exists strictly so
90
+ already-migrated UIs keep working without a rewrite. Both can coexist
91
+ in one site.
92
+ 2. **Singletons can't live in a shared package without leaking across
93
+ sites.** The factory call instantiates a fresh module-level state per
94
+ site. Importing `useCart` directly from apps would share state across
95
+ any sites that ran in the same Worker (matters less in practice, but
96
+ it's the architectural reason the factory exists).
97
+
98
+ The factory boundary is also the seam where we'd later wire
99
+ `@tanstack/store` if we wanted to — the API shape is signal-compatible
100
+ already.
101
+
102
+ ---
103
+
104
+ ## Non-VTEX platforms
105
+
106
+ Sites that target Wake / Shopify / VNDA / Linx / Nuvemshop still need a
107
+ hook surface that AddToCartButtons can consume. Until each platform has
108
+ its own factory in `@decocms/apps`, scaffold a no-op shim:
109
+
110
+ ```ts
111
+ // src/hooks/useCart.ts (custom platform)
112
+ import { signal } from "@decocms/start/sdk/signal";
113
+
114
+ const cart = signal<unknown>(null);
115
+ const loading = signal(false);
116
+
117
+ export function useCart() {
118
+ return {
119
+ cart,
120
+ loading,
121
+ async getCart() {
122
+ // TODO: call your platform's cart API via ~/server/invoke
123
+ return null;
124
+ },
125
+ async addItems(_items: unknown[]) {
126
+ // TODO
127
+ },
128
+ async updateItems(_items: unknown[]) {
129
+ // TODO
130
+ },
131
+ setCart(next: unknown) {
132
+ cart.value = next;
133
+ },
134
+ };
135
+ }
136
+
137
+ export default useCart;
138
+ ```
139
+
140
+ `@decocms/start/sdk/signal` (re-exported via `~/sdk/signal` after
141
+ migration) gives you the same `.value` getter/setter the factory uses,
142
+ so AddToCart UI components don't need to know which platform is wired.
143
+
144
+ The migration template's `generateGenericUseCart()` emits this stub when
145
+ `--platform` is `custom` (or any non-VTEX value).
146
+
147
+ ---
148
+
149
+ ## Migrating off the manual approach (pre-W12 sites)
150
+
151
+ If your site contains files like `src/lib/vtex-cart-server.ts` or hand-rolled `createServerFn` blocks for VTEX endpoints in `src/hooks/useCart.ts`, the post-cleanup audit will not auto-fix them — the manual code accumulated 6+ months of site-specific edits and the per-site judgment call about "what's still needed?" is real. The mechanical part:
152
+
153
+ 1. Replace the entire body of `src/hooks/useCart.ts` with the 5-line factory shim above.
154
+ 2. Delete `src/lib/vtex-cart-server.ts` (the migration template's `src/server/invoke.gen.ts` provides equivalent server functions wrapping `@decocms/apps/vtex/actions/checkout`).
155
+ 3. Verify `src/server/invoke.ts` exports the proxy shape the factory needs (cart actions under `invoke.vtex.actions`). The migration template scaffolds this; older sites may need to add the missing entries by hand.
156
+ 4. Run `npm run typecheck` — TypeScript will surface any callsites that referenced removed helpers (e.g. local `getOrCreateCart` shims).
157
+ 5. Repeat for `useUser` / `useWishlist`.
158
+
159
+ For `useUser`, the analog of step 2 is removing any local `currentUser` /
160
+ `getUser` server functions in favor of `@decocms/apps/vtex/loaders/user`
161
+ exposed via `invoke.vtex.loaders.user()`. For `useWishlist`, the
162
+ canonical surface is `@decocms/apps/vtex/{actions,loaders}/wishlist`.
163
+
164
+ If you find yourself wanting to add behaviour to a factory (extra cart
165
+ actions, custom analytics events) rather than ripping out the factory and going back to a manual hook:
166
+
167
+ - **Extra read paths** → expose a new loader from
168
+ `@decocms/apps/vtex/loaders/*`, register it in `~/server/invoke.ts`,
169
+ call from your component (the factory doesn't need to know).
170
+ - **Extra write paths** → ditto for `@decocms/apps/vtex/actions/*`.
171
+ - **Cross-cutting business logic** (e.g. PIX-specific offer pricing) →
172
+ this is the kind of seam that justifies a parallel `useOffer` factory.
173
+ Talk to the apps maintainers; opening up a factory's plugin slots is a
174
+ one-PR change in apps, not a per-site rewrite.
175
+
176
+ ---
177
+
178
+ ## Related
179
+
180
+ - `scripts/migrate/templates/hooks.ts` — the template that emits the
181
+ shims above.
182
+ - `apps-start/vtex/hooks/createUseCart.ts` /
183
+ `createUseUser.ts` / `createUseWishlist.ts` — the factories
184
+ themselves; each docstring is the authoritative API reference.
185
+ - `references/platform-hooks/README.md` — historical reference for the
186
+ pre-W12 manual approach (kept for sites that haven't migrated yet).
@@ -18,8 +18,10 @@ npx -p @decocms/start deco-post-cleanup
18
18
 
19
19
  # Auto-apply mechanical fixes for the safe rules, then report what's left.
20
20
  # Safe rules: dead-lib-shims, dead-runtime-shim, local-widgets-types,
21
- # vtex-shim-regression (swap subset), obsolete-vite-plugins.
22
- # Other rules stay detect-only they require human judgment.
21
+ # vtex-shim-regression (swap subset), obsolete-vite-plugins,
22
+ # local-framework-duplicate (auto-fixable subset of the registry).
23
+ # Other rules — and the warn-only entries of local-framework-duplicate —
24
+ # stay detect-only. They require human judgment.
23
25
  npx -p @decocms/start deco-post-cleanup --fix
24
26
 
25
27
  # Combine for CI: auto-fix safe rules, fail (exit 2) if warnings remain.
@@ -29,14 +31,16 @@ npx -p @decocms/start deco-post-cleanup --fix --strict
29
31
  npx -p @decocms/start deco-post-cleanup --json
30
32
  ```
31
33
 
32
- The audit covers all 7 rules below and prints the exact file path +
34
+ The audit covers all 9 rules below and prints the exact file path +
33
35
  suggested fix for each finding. With `--fix`, the safe rules
34
36
  auto-apply: `rm` for dead files, regex-anchored import rewrites for
35
37
  shadowed shims (`local-widgets-types`, `dead-runtime-shim`), the swap
36
- subset of `vtex-shim-regression`, and JS-aware removal of obsolete
37
- inline plugin literals from `vite.config.ts`. The output explicitly
38
- tags rules that require manual work as `(0 fixed, manual)`, so you
39
- always know what's left after auto-fix runs.
38
+ subset of `vtex-shim-regression`, JS-aware removal of obsolete
39
+ inline plugin literals from `vite.config.ts`, and rewrite-imports +
40
+ delete for the auto-fixable subset of `local-framework-duplicate`
41
+ (see § 8). The output explicitly tags rules that require manual work
42
+ as `(0 fixed, manual)`, so you always know what's left after auto-fix
43
+ runs.
40
44
 
41
45
  Real-world signal: on baggagio, `--fix` produced a byte-identical
42
46
  diff to the manual cleanup PR a human had just made (45 files,
@@ -398,7 +402,58 @@ In `--strict` mode any residue exits 2 — wire that into CI once a
398
402
  site has finished its HTMX rewrite to prevent regressions sneaking
399
403
  back in via copy-paste from a Fresh source.
400
404
 
401
- ## 8. Search for orphan `TODO: move into framework` comments
405
+ ## 8. Drop site-local copies of framework code (`local-framework-duplicate`)
406
+
407
+ The audit's `local-framework-duplicate` rule encodes a registry of
408
+ files we expect sites to NOT carry locally because the canonical
409
+ implementation already ships in `@decocms/start`. New entries go in
410
+ `scripts/migrate/post-cleanup/rules.ts → FRAMEWORK_DUPLICATES`.
411
+
412
+ Two kinds of finding:
413
+
414
+ | Kind | Auto-fix | Example | What you do |
415
+ |---|---|---|---|
416
+ | **Pure dup** (`safeToAutoFix: true`) | YES | `src/sdk/clx.ts` matches `@decocms/start/sdk/clx` byte-for-byte | `--fix` rewrites every `from "~/sdk/clx"` to `from "@decocms/start/sdk/clx"` and deletes the file. Zero behavior change. |
417
+ | **Partial overlap** (`safeToAutoFix: false`) | NO | `src/sdk/useSendEvent.ts` (typed) overlaps `@decocms/start/sdk/analytics → useSendEvent` (permissive) | The rule emits a `warning` with a `reason` explaining the manual judgement: widen the framework export, accept type loss, or fork on purpose. Human picks. |
418
+
419
+ ### How the rule fires
420
+
421
+ The site file must match every regex in `contentSignature` before
422
+ the rule treats it as the framework dup. This is conservative on
423
+ purpose — sites that genuinely forked the helper (added platform
424
+ logic, wrapped in something else) are skipped automatically.
425
+
426
+ ### Current registry
427
+
428
+ | Site path | Canonical | Auto-fix? | Reason / status |
429
+ |---|---|---|---|
430
+ | `src/sdk/clx.ts` | `@decocms/start/sdk/clx` | yes | Identical implementation; baggagio's extra `clsx` alias has zero callers. |
431
+ | `src/sdk/useSendEvent.ts` | `@decocms/start/sdk/analytics` | no | Site copy uses `<E extends AnalyticsEvent>` generic; framework export is permissive. Replace 1:1 = type-safety loss. Either widen the framework first or accept the loss. |
432
+ | `src/matchers/location.ts` | `@decocms/start/matchers/builtins` | no | Framework's `registerBuiltinMatchers()` ships a richer location matcher (`request.cf` first, geo cookies fallback, headers fallback) plus 10 sibling matchers. Adopting changes behaviour — verify country-name lookup parity, swap `setup.ts`'s `customMatchers` entry. |
433
+
434
+ ### Adding a new entry
435
+
436
+ When you spot a site carrying its own copy of code that lives in
437
+ `@decocms/start`, add an entry to `FRAMEWORK_DUPLICATES`:
438
+
439
+ ```ts
440
+ {
441
+ id: "<short-stable-id>",
442
+ sitePath: "src/<path>.ts",
443
+ canonicalImport: "@decocms/start/<path>",
444
+ contentSignature: [/<regex 1>/, /<regex 2>/],
445
+ safeToAutoFix: true | false,
446
+ reason: "<required when not safeToAutoFix>",
447
+ description: "<one-liner used in finding message>",
448
+ }
449
+ ```
450
+
451
+ Per **D4** in the migration tooling policy, the framework promotion
452
+ itself happens at 3+ sites. This registry is the *enforcement* layer
453
+ once promoted: every other site picks up the convergence
454
+ automatically the next time `deco-post-cleanup --fix` runs.
455
+
456
+ ## 9. Search for orphan `TODO: move into framework` comments
402
457
 
403
458
  Real sites accumulate `TODO` comments like `// TODO: move into decoVitePlugin
404
459
  in next @decocms/start release`. These are roadmap items the framework
@@ -415,7 +470,7 @@ For each hit, decide:
415
470
 
416
471
  ## Verification checklist
417
472
 
418
- After completing 1-8:
473
+ After completing 1-9:
419
474
 
420
475
  - [ ] `npm run typecheck` baseline matches pre-cleanup count (no new errors)
421
476
  - [ ] `npm run dev` starts and `/`, `/some-pdp/p`, `/s?q=foo` all render