@thorprovider/storefront 0.3.1 → 0.4.0-mvp.2

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.
Files changed (263) hide show
  1. package/README.md +321 -152
  2. package/VENDOR.md +69 -0
  3. package/assets/openspec/config.yaml +38 -0
  4. package/assets/templates/AGENTS.md +76 -0
  5. package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
  6. package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
  7. package/assets/templates/app/app/globals.css +5 -8
  8. package/assets/templates/app/app/layout.tsx +17 -7
  9. package/assets/templates/app/app/robots.ts +19 -0
  10. package/assets/templates/app/app/sitemap.ts +71 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  13. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
  14. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
  15. package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
  16. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
  17. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  18. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  20. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
  21. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
  22. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
  23. package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
  24. package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  27. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
  30. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  31. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
  32. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  34. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  39. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
  40. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  41. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  42. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
  43. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
  44. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  45. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  46. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
  47. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  48. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  49. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  50. package/assets/templates/features/checkout/handlers.ts +159 -0
  51. package/assets/templates/features/checkout/state.ts +61 -0
  52. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  53. package/assets/templates/features/customer-auth/state.ts +45 -0
  54. package/assets/templates/features/recommendations/state.ts +11 -0
  55. package/assets/templates/views/account/content.ts +35 -0
  56. package/assets/templates/views/account/index.tsx +114 -0
  57. package/assets/templates/views/account/registry.tsx +439 -0
  58. package/assets/templates/views/account/schema.ts +53 -0
  59. package/assets/templates/views/cart/content.ts +11 -0
  60. package/assets/templates/views/cart/data.ts +23 -0
  61. package/assets/templates/views/cart/index.tsx +53 -0
  62. package/assets/templates/views/cart/props.ts +12 -0
  63. package/assets/templates/views/cart/registry.tsx +81 -0
  64. package/assets/templates/views/cart/schema.ts +52 -0
  65. package/assets/templates/views/category/assembly/assembly.json +29 -0
  66. package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
  67. package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
  68. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
  69. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
  70. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
  71. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
  72. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  73. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  74. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  75. package/assets/templates/views/category/content.ts +30 -0
  76. package/assets/templates/views/category/data.ts +12 -0
  77. package/assets/templates/views/category/index.tsx +299 -0
  78. package/assets/templates/views/category/props.ts +23 -0
  79. package/assets/templates/views/category/registry.tsx +122 -0
  80. package/assets/templates/views/category/schema.ts +91 -0
  81. package/assets/templates/views/checkout/content.ts +105 -0
  82. package/assets/templates/views/checkout/data.ts +23 -0
  83. package/assets/templates/views/checkout/index.tsx +111 -0
  84. package/assets/templates/views/checkout/props.ts +49 -0
  85. package/assets/templates/views/checkout/registry.tsx +217 -0
  86. package/assets/templates/views/checkout/schema.ts +229 -0
  87. package/assets/templates/views/home/assembly/assembly.json +33 -0
  88. package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
  89. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  90. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  91. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  92. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  93. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  94. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  95. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  96. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  97. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  98. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  99. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  100. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  101. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  102. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  103. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  104. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  105. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  106. package/assets/templates/views/home/content.ts +70 -0
  107. package/assets/templates/views/home/data.ts +24 -0
  108. package/assets/templates/views/home/index.tsx +57 -85
  109. package/assets/templates/views/home/props.ts +22 -0
  110. package/assets/templates/views/home/registry.tsx +38 -22
  111. package/assets/templates/views/home/schema.ts +93 -0
  112. package/assets/templates/views/product/content.ts +25 -0
  113. package/assets/templates/views/product/data.ts +50 -0
  114. package/assets/templates/views/product/index.tsx +148 -0
  115. package/assets/templates/views/product/props.ts +27 -0
  116. package/assets/templates/views/product/registry.tsx +103 -0
  117. package/assets/templates/views/product/schema.ts +121 -0
  118. package/assets/templates/views/static/content.ts +99 -0
  119. package/assets/templates/views/static/index.tsx +60 -0
  120. package/assets/templates/views/static/registry.tsx +66 -0
  121. package/assets/templates/views/static/schema.ts +45 -0
  122. package/bin/storefront.js +2424 -148
  123. package/examples/merchandising/category-led.json +37 -0
  124. package/examples/merchandising/collection-led.json +37 -0
  125. package/package.json +15 -9
  126. package/recipes/archetypes.json +28 -14
  127. package/recipes/catalog.json +400 -0
  128. package/recipes/catalog.mjs +128 -0
  129. package/recipes/catalog.schema.json +64 -0
  130. package/recipes/feature.schema.json +89 -0
  131. package/recipes/features.json +162 -0
  132. package/recipes/manifest.schema.json +69 -0
  133. package/recipes/operations.schema.json +43 -0
  134. package/recipes/recipe.schema.json +11 -2
  135. package/recipes/recipes.json +76 -19
  136. package/recipes/sections.json +34 -5
  137. package/recipes/validate.mjs +838 -32
  138. package/scripts/candidate.mjs +236 -0
  139. package/scripts/composers.mjs +160 -0
  140. package/scripts/env-file.mjs +86 -0
  141. package/scripts/file-classes.mjs +121 -0
  142. package/scripts/logical-controls.mjs +61 -0
  143. package/scripts/manifest.mjs +298 -0
  144. package/scripts/migrate-english-routes.mjs +276 -0
  145. package/scripts/operation-registry.mjs +256 -0
  146. package/scripts/operations.mjs +720 -0
  147. package/scripts/ownership-conflict.mjs +356 -0
  148. package/scripts/pinning.mjs +573 -0
  149. package/scripts/plan-apply.mjs +987 -0
  150. package/scripts/preflight.mjs +51 -0
  151. package/scripts/reconcile.mjs +549 -0
  152. package/scripts/render-templates.mjs +338 -68
  153. package/scripts/schema-validate.mjs +132 -0
  154. package/scripts/section-compat.mjs +300 -0
  155. package/scripts/section-config.mjs +253 -0
  156. package/scripts/section-operations.mjs +508 -0
  157. package/scripts/starter.mjs +17 -0
  158. package/scripts/sync-assets.mjs +22 -15
  159. package/scripts/theme/color.mjs +185 -0
  160. package/scripts/theme/css-theme.mjs +233 -0
  161. package/scripts/theme/fonts.mjs +123 -0
  162. package/scripts/theme/import-export.mjs +125 -0
  163. package/scripts/theme/preset.mjs +93 -0
  164. package/scripts/theme/theme-plan.mjs +292 -0
  165. package/scripts/theme/tokens.mjs +137 -0
  166. package/scripts/view-assembly.mjs +337 -0
  167. package/scripts/view-selection.mjs +211 -0
  168. package/skills/sf-feature/SKILL.md +80 -0
  169. package/skills/sf-setup/SKILL.md +79 -0
  170. package/skills/sf-theme-gen/SKILL.md +100 -30
  171. package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
  172. package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
  173. package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
  174. package/skills/sf-view-gen/SKILL.md +146 -93
  175. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
  176. package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
  177. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  178. package/skills/sf-view-gen/assets/recipes/features.json +162 -0
  179. package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
  180. package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
  181. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  182. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  183. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  184. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  185. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  186. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  187. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  188. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  189. package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
  190. package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
  191. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  192. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  193. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  194. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  195. package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
  196. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  197. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  198. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  199. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  200. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  201. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  202. package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
  203. package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
  204. package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
  205. package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
  206. package/skills/sf-view-gen/references/views.md +58 -33
  207. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  208. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  209. package/skills/sf-view-gen/scripts/validate.mjs +838 -32
  210. package/skills/storefront-workflow/SKILL.md +179 -0
  211. package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
  212. package/CHANGELOG.md +0 -61
  213. package/commands/sf-add-view.md +0 -26
  214. package/commands/sf-theme.md +0 -20
  215. package/commands/sf-view.md +0 -25
  216. package/skills/building-storefronts/SKILL.md +0 -178
  217. package/skills/building-storefronts/references/frontend-integration.md +0 -229
  218. package/skills/json-render-remotion/SKILL.md +0 -111
  219. package/skills/json-render-solid/SKILL.md +0 -204
  220. package/skills/nextjs-shadcn/SKILL.md +0 -367
  221. package/skills/nextjs-shadcn/references/architecture.md +0 -499
  222. package/skills/nextjs-shadcn/references/project-setup.md +0 -162
  223. package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
  224. package/skills/nextjs-shadcn/references/sidebar.md +0 -274
  225. package/skills/nextjs-shadcn/references/styling.md +0 -555
  226. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  227. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  228. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  229. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  230. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  231. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
  232. package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
  233. package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
  234. package/skills/shadcn-component-discovery/SKILL.md +0 -273
  235. package/skills/shadcn-component-discovery/references/registries.md +0 -226
  236. package/skills/shadcn-theming/SKILL.md +0 -104
  237. package/skills/storefront-best-practices/SKILL.md +0 -421
  238. package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
  239. package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
  240. package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
  241. package/skills/storefront-best-practices/reference/components/footer.md +0 -112
  242. package/skills/storefront-best-practices/reference/components/hero.md +0 -241
  243. package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
  244. package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
  245. package/skills/storefront-best-practices/reference/components/popups.md +0 -221
  246. package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
  247. package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
  248. package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
  249. package/skills/storefront-best-practices/reference/components/search.md +0 -101
  250. package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
  251. package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
  252. package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
  253. package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
  254. package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
  255. package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
  256. package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
  257. package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
  258. package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
  259. package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
  260. package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
  261. package/skills/storefront-best-practices/reference/medusa.md +0 -307
  262. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
  263. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env node
2
+ // E3 tasks 3.1, 3.2, and 3.3 (CU-14, CU-15, CU-16): the `section.add`,
3
+ // `section.remove`, and `section.reorder` operations and the production E3
4
+ // operation registry.
5
+ //
6
+ // E3 extends E1's namespaced, extensible registry (E1 Decision 16) with the
7
+ // `section` namespace; it never widens `E1_OPERATION_REGISTRY` or
8
+ // `CATALOG_OPERATION_REGISTRY`. The production registry
9
+ // `SECTION_OPERATION_REGISTRY` is E1's six operations + E2's catalog logical
10
+ // controls + E3's `section.add`/`section.remove`/`section.reorder`, composed with
11
+ // `extendOperationRegistry`.
12
+ //
13
+ // Each reducer is pure over the manifest: it reads the view's declared
14
+ // `sections`, normalizes and validates it through `section-compat.mjs` (the
15
+ // single normalization authority), validates the host/type allowlist, the
16
+ // editorial `maxSections` bound, id uniqueness, the reserved `slot:` prefix, and
17
+ // (for a reorder) that `order` is a permutation of the view's current entry ids,
18
+ // then returns a NEW manifest (plus, for a removal, the preserved content
19
+ // location) with the change applied. It never mutates its input and returns no
20
+ // manifest on any failure, so a caller writes nothing (no partial writes).
21
+
22
+ import { extendOperationRegistry } from './operation-registry.mjs'
23
+ import { CATALOG_OPERATION_REGISTRY, DIAGNOSTIC_CODES, diagnostic } from './operations.mjs'
24
+ import {
25
+ isEditorialSectionEntry,
26
+ isSlotSectionEntry,
27
+ normalizeViewSections,
28
+ } from './section-compat.mjs'
29
+ import {
30
+ allowedViewNames,
31
+ getRecipe,
32
+ heldSectionReason,
33
+ loadSections,
34
+ } from '../recipes/validate.mjs'
35
+
36
+ // The E3 section-operation diagnostics, alongside E1's shared `DIAGNOSTIC_CODES`
37
+ // (E_INVALID_OPERATION, E_VIEW_NOT_INSTALLED). They mirror the codes
38
+ // `section-compat.mjs` emits so a normalization failure and a direct
39
+ // `section.add`/`section.remove` failure are named consistently.
40
+ export const SECTION_DIAGNOSTIC_CODES = Object.freeze({
41
+ E_UNKNOWN_HOST: 'E_UNKNOWN_HOST',
42
+ E_DISALLOWED_TYPE: 'E_DISALLOWED_TYPE',
43
+ E_DUPLICATE_ID: 'E_DUPLICATE_ID',
44
+ E_RESERVED_ID: 'E_RESERVED_ID',
45
+ E_SECTION_LIMIT: 'E_SECTION_LIMIT',
46
+ E_CORE_SLOT_REMOVAL: 'E_CORE_SLOT_REMOVAL',
47
+ E_UNKNOWN_SECTION_REF: 'E_UNKNOWN_SECTION_REF',
48
+ E_UNKNOWN_ORDER_REF: 'E_UNKNOWN_ORDER_REF',
49
+ })
50
+
51
+ const RESERVED_ID_PREFIX = 'slot:'
52
+
53
+ function hasOwn(object, key) {
54
+ return Object.prototype.hasOwnProperty.call(object, key)
55
+ }
56
+
57
+ function requireString(op, field) {
58
+ return typeof op[field] === 'string' && op[field].length > 0
59
+ }
60
+
61
+ function fail(code, message, extra = {}) {
62
+ return { ok: false, diagnostics: [diagnostic(code, message, extra)] }
63
+ }
64
+
65
+ // The preserved instance ids of the operated view: an editorial instance whose
66
+ // content `section.remove` kept on disk. `content.ts` (classified user-owned by
67
+ // `file-classes.mjs`) holds the content bytes and is never rewritten, but it
68
+ // cannot itself enumerate the ids, so the id record lives in the manifest as the
69
+ // top-level `preservedSectionIds` map (view name -> ordered ids, written by
70
+ // `section.remove`). It is top-level, not per-view, so every E1 `view.*`/
71
+ // `feature.*` reducer (which spreads the manifest) carries it forward and an
72
+ // explicit re-add cannot accidentally rebind. Auto-mint consults the set so a
73
+ // fresh id never rebinds preserved content; an explicit re-add of a preserved id
74
+ // leaves the set (rebind). This is the concrete disk-backed notion of "preserved"
75
+ // (E3 task 3.7, design §"Remove, content preservation, and re-add same id").
76
+ function preservedIdsFor(manifest, viewName) {
77
+ const map = manifest.preservedSectionIds
78
+ const list =
79
+ map && typeof map === 'object' && !Array.isArray(map) ? map[viewName] : undefined
80
+ const entries = Array.isArray(list) ? list : []
81
+ return new Set(entries.filter((id) => typeof id === 'string' && id.length > 0))
82
+ }
83
+
84
+ // Copy `manifest` with `preservedSectionIds[viewName]` set to exactly `ids`
85
+ // (deduplicated, insertion-ordered). An empty list drops the view's entry, and an
86
+ // empty map drops the top-level field, so a manifest that never preserved
87
+ // anything stays byte-identical to one that preserved and re-added. Never mutates
88
+ // the input.
89
+ function withPreservedIds(manifest, viewName, ids) {
90
+ const current = manifest.preservedSectionIds
91
+ const map = current && typeof current === 'object' && !Array.isArray(current) ? { ...current } : {}
92
+ const deduped = [...new Set(ids)]
93
+ if (deduped.length === 0) delete map[viewName]
94
+ else map[viewName] = deduped
95
+ const out = { ...manifest }
96
+ if (Object.keys(map).length === 0) delete out.preservedSectionIds
97
+ else out.preservedSectionIds = map
98
+ return out
99
+ }
100
+
101
+ function sectionDefinition(type) {
102
+ const sections = loadSections()?.sections
103
+ const list = Array.isArray(sections) ? sections : []
104
+ return list.find((section) => section.name === type) ?? null
105
+ }
106
+
107
+ // The smallest free `<view>-<type>-<n>` (n >= 1) not in `used ∪ preserved`, so an
108
+ // auto-minted id never rebinds preserved content (design: `section.add` auto-mint).
109
+ function mintSectionId(viewName, type, used, preserved) {
110
+ for (let n = 1; ; n += 1) {
111
+ const candidate = `${viewName}-${type}-${n}`
112
+ if (!used.has(candidate) && !preserved.has(candidate)) return candidate
113
+ }
114
+ }
115
+
116
+ // The user-owned file that holds a view's editorial instance content, keyed by
117
+ // instance id (design §"Remove, content preservation, and re-add same id"). It is
118
+ // the view's `content.ts` module, which `file-classes.mjs` classifies user-owned
119
+ // and `reconcile` seeds once and never rewrites, so `section.remove` preserves
120
+ // the content simply by not touching the file; the reducer reports this
121
+ // project-relative POSIX path through the operation's `removals` so the write
122
+ // boundary surfaces it in the result's `preserved` list. The helper is pure (it
123
+ // derives the path from the manifest name and the view), so the reducer stays
124
+ // filesystem-free.
125
+ function sectionContentPath(manifest, viewName) {
126
+ return `lib/${manifest.name}/views/${viewName}/content.ts`
127
+ }
128
+
129
+ /**
130
+ * Reduce a `section.add` operation over the manifest (E3 task 3.1). Validates the
131
+ * host allowlist (`E_UNKNOWN_HOST` for an unknown view, `E_DISALLOWED_TYPE` for a
132
+ * type the view does not allow), id uniqueness against `used` (`E_DUPLICATE_ID`),
133
+ * the reserved `slot:` prefix (`E_RESERVED_ID`), and the editorial `maxSections`
134
+ * bound (`E_SECTION_LIMIT`). An omitted `id` is minted from `used ∪ preserved`,
135
+ * where `preserved` is the view's recorded removed ids (so a fresh id never
136
+ * rebinds preserved content). An explicit `id` that is still present is
137
+ * `E_DUPLICATE_ID`; an explicit `id` that is preserved (removed, its content
138
+ * still on disk) succeeds and rebinds — it leaves the preserved record. Returns
139
+ * `{ ok: true, manifest }` with the instance inserted at `at` (0-based, default
140
+ * append) or `{ ok: false, diagnostics }`. The input manifest is never mutated.
141
+ */
142
+ export function reduceSectionAdd(manifest, op, options = {}) {
143
+ if (!requireString(op, 'view')) {
144
+ return fail(
145
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
146
+ 'section.add requires a non-empty string "view".',
147
+ )
148
+ }
149
+ if (!requireString(op, 'type')) {
150
+ return fail(
151
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
152
+ 'section.add requires a non-empty string "type".',
153
+ )
154
+ }
155
+ const viewName = op.view
156
+ const recipe = getRecipe(viewName)
157
+ if (!recipe) {
158
+ return fail(
159
+ SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_HOST,
160
+ `Unknown view "${viewName}" is not a section host. Allowed views: ${allowedViewNames().join(', ')}.`,
161
+ )
162
+ }
163
+ const allowedSections = Array.isArray(recipe.allowedSections) ? recipe.allowedSections : []
164
+ if (!allowedSections.includes(op.type)) {
165
+ const held = heldSectionReason(op.type)
166
+ const heldDetail = held ? ` Section "${op.type}" is held: ${held}.` : ''
167
+ return fail(
168
+ SECTION_DIAGNOSTIC_CODES.E_DISALLOWED_TYPE,
169
+ `Section type "${op.type}" is not an allowed section for view "${viewName}". Allowed sections: ${allowedSections.join(', ') || '(none)'}.${heldDetail}`,
170
+ )
171
+ }
172
+ const installed = manifest.views.find((view) => view.name === viewName)
173
+ if (!installed) {
174
+ return fail(
175
+ DIAGNOSTIC_CODES.E_VIEW_NOT_INSTALLED,
176
+ `View "${viewName}" is not installed; add the view before adding a section to it.`,
177
+ )
178
+ }
179
+ const registeredSlots = Array.isArray(recipe.registeredSlots) ? recipe.registeredSlots : []
180
+ const maxSections = typeof recipe.maxSections === 'number' ? recipe.maxSections : null
181
+ const preserved = preservedIdsFor(manifest, viewName)
182
+
183
+ // Normalize/validate the current composition through the single authority so a
184
+ // legacy or malformed list is adapted or refused consistently, with no output
185
+ // on failure.
186
+ const normalized = normalizeViewSections(viewName, installed.sections ?? [], registeredSlots, {
187
+ allowedSections,
188
+ maxSections,
189
+ })
190
+ if (!normalized.ok) return { ok: false, diagnostics: normalized.diagnostics }
191
+ const current = normalized.sections
192
+ const used = new Set(current.map((entry) => entry.id))
193
+
194
+ let id
195
+ if (hasOwn(op, 'id')) {
196
+ if (!requireString(op, 'id')) {
197
+ return fail(
198
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
199
+ 'section.add "id" must be a non-empty string when present.',
200
+ )
201
+ }
202
+ if (op.id.startsWith(RESERVED_ID_PREFIX)) {
203
+ return fail(
204
+ SECTION_DIAGNOSTIC_CODES.E_RESERVED_ID,
205
+ `section.add id "${op.id}" must not start with the reserved "${RESERVED_ID_PREFIX}" prefix.`,
206
+ )
207
+ }
208
+ if (used.has(op.id)) {
209
+ return fail(
210
+ SECTION_DIAGNOSTIC_CODES.E_DUPLICATE_ID,
211
+ `section.add id "${op.id}" is already present in view "${viewName}".`,
212
+ )
213
+ }
214
+ id = op.id
215
+ } else {
216
+ id = mintSectionId(viewName, op.type, used, preserved)
217
+ }
218
+
219
+ let variant
220
+ if (hasOwn(op, 'variant')) {
221
+ if (!requireString(op, 'variant')) {
222
+ return fail(
223
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
224
+ 'section.add "variant" must be a non-empty string when present.',
225
+ )
226
+ }
227
+ const definition = sectionDefinition(op.type)
228
+ const variants = Array.isArray(definition?.variants) ? definition.variants : []
229
+ if (variants.length > 0 && !variants.includes(op.variant)) {
230
+ return fail(
231
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
232
+ `Variant "${op.variant}" is not a declared variant for section "${op.type}". Declared: ${variants.join(', ')}.`,
233
+ )
234
+ }
235
+ variant = op.variant
236
+ }
237
+
238
+ const editorialCount = current.filter((entry) => isEditorialSectionEntry(entry)).length
239
+ if (maxSections !== null && editorialCount + 1 > maxSections) {
240
+ return fail(
241
+ SECTION_DIAGNOSTIC_CODES.E_SECTION_LIMIT,
242
+ `Adding a "${op.type}" instance to view "${viewName}" would exceed maxSections ${maxSections} (currently ${editorialCount}).`,
243
+ )
244
+ }
245
+
246
+ let at = current.length
247
+ if (hasOwn(op, 'at')) {
248
+ if (!Number.isInteger(op.at) || op.at < 0 || op.at > current.length) {
249
+ return fail(
250
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
251
+ `section.add "at" must be an integer between 0 and ${current.length}.`,
252
+ )
253
+ }
254
+ at = op.at
255
+ }
256
+
257
+ const entry = { id, type: op.type, ...(variant === undefined ? {} : { variant }) }
258
+ const sections = [...current.slice(0, at), entry, ...current.slice(at)]
259
+ const views = manifest.views.map((view) =>
260
+ view.name === viewName ? { ...view, sections } : view,
261
+ )
262
+ // Re-adding an explicit preserved id rebinds its content: the id leaves the
263
+ // preserved record and becomes a used id. An auto-minted id is already absent
264
+ // from the preserved set, so this is a no-op for it.
265
+ const next = withPreservedIds(
266
+ { ...manifest, views },
267
+ viewName,
268
+ [...preserved].filter((preservedId) => preservedId !== id),
269
+ )
270
+ return { ok: true, manifest: next }
271
+ }
272
+
273
+ /**
274
+ * Reduce a `section.remove` operation over the manifest (E3 task 3.2,
275
+ * CU-15/CU-16). A reserved `slot:` id is a core registered slot and is rejected
276
+ * with `E_CORE_SLOT_REMOVAL` (design's stable code); an id that does not name a
277
+ * present editorial instance is rejected with `E_UNKNOWN_SECTION_REF`. On
278
+ * success it drops the editorial entry from the ordered composition, records the
279
+ * removed id as preserved in the view's `preservedSectionIds`, and returns the
280
+ * removal holder carrying the instance's user-owned content location, which the
281
+ * write boundary reports as preserved while `reconcile` leaves the file
282
+ * byte-unchanged. Never mutates its input and returns no manifest on failure, so
283
+ * a caller writes nothing (no partial writes).
284
+ */
285
+ export function reduceSectionRemove(manifest, op, options = {}) {
286
+ if (!requireString(op, 'view')) {
287
+ return fail(
288
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
289
+ 'section.remove requires a non-empty string "view".',
290
+ )
291
+ }
292
+ if (!requireString(op, 'id')) {
293
+ return fail(
294
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
295
+ 'section.remove requires a non-empty string "id".',
296
+ )
297
+ }
298
+ const viewName = op.view
299
+ const recipe = getRecipe(viewName)
300
+ if (!recipe) {
301
+ return fail(
302
+ SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_HOST,
303
+ `Unknown view "${viewName}" is not a section host. Allowed views: ${allowedViewNames().join(', ')}.`,
304
+ )
305
+ }
306
+ const installed = manifest.views.find((view) => view.name === viewName)
307
+ if (!installed) {
308
+ return fail(
309
+ DIAGNOSTIC_CODES.E_VIEW_NOT_INSTALLED,
310
+ `View "${viewName}" is not installed; add the view before removing a section from it.`,
311
+ )
312
+ }
313
+ // Core registered slots are non-removable (design "core transactional slots are
314
+ // never removed"): any reserved `slot:` id is refused before normalization with
315
+ // the stable `E_CORE_SLOT_REMOVAL` code.
316
+ if (op.id.startsWith(RESERVED_ID_PREFIX)) {
317
+ return fail(
318
+ SECTION_DIAGNOSTIC_CODES.E_CORE_SLOT_REMOVAL,
319
+ `section.remove id "${op.id}" is a reserved core slot of view "${viewName}"; registered slots cannot be removed.`,
320
+ )
321
+ }
322
+ const registeredSlots = Array.isArray(recipe.registeredSlots) ? recipe.registeredSlots : []
323
+ const allowedSections = Array.isArray(recipe.allowedSections) ? recipe.allowedSections : []
324
+ const maxSections = typeof recipe.maxSections === 'number' ? recipe.maxSections : null
325
+
326
+ // Normalize/validate the current composition through the single authority so a
327
+ // legacy or malformed list is adapted or refused consistently, with no output
328
+ // on failure.
329
+ const normalized = normalizeViewSections(viewName, installed.sections ?? [], registeredSlots, {
330
+ allowedSections,
331
+ maxSections,
332
+ })
333
+ if (!normalized.ok) return { ok: false, diagnostics: normalized.diagnostics }
334
+ const current = normalized.sections
335
+
336
+ const target = current.find(
337
+ (entry) => entry.id === op.id && isEditorialSectionEntry(entry),
338
+ )
339
+ if (!target) {
340
+ return fail(
341
+ SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_SECTION_REF,
342
+ `section.remove id "${op.id}" does not name an editorial section instance in view "${viewName}".`,
343
+ )
344
+ }
345
+
346
+ const sections = current.filter((entry) => entry.id !== op.id)
347
+ const views = manifest.views.map((view) =>
348
+ view.name === viewName ? { ...view, sections } : view,
349
+ )
350
+ // Record the removed id as preserved: its user-owned content stays in the
351
+ // view's `content.ts` (unchanged), and the manifest holds the id so a later
352
+ // explicit re-add rebinds it while auto-mint avoids it.
353
+ const preserved = preservedIdsFor(manifest, viewName)
354
+ const next = withPreservedIds({ ...manifest, views }, viewName, [...preserved, op.id])
355
+ return {
356
+ ok: true,
357
+ manifest: next,
358
+ removals: [
359
+ { kind: 'section', view: viewName, id: op.id, contentPath: sectionContentPath(manifest, viewName) },
360
+ ],
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Reduce a `section.reorder` operation over the manifest (E3 task 3.3,
366
+ * CU-15/CU-16). `order` MUST be a permutation of the view's current entry ids:
367
+ * every current id exactly once, no unknown id, no duplicate, and every
368
+ * registered core slot present. The current composition is normalized through
369
+ * the single `section-compat.mjs` authority first, so a legacy or malformed list
370
+ * is adapted or refused consistently. A successful reorder keeps every entry's
371
+ * id, type, variant, and content: it returns a new manifest whose view's
372
+ * `sections` are the same normalized entries in the requested order, never
373
+ * mutating its input and returning no manifest on failure (no partial writes).
374
+ *
375
+ * Stable codes (design diagnostic table): an id not present in the view ->
376
+ * `E_UNKNOWN_ORDER_REF`; a registered core slot that is omitted or listed twice
377
+ * -> `E_CORE_SLOT_REMOVAL`; a present editorial id listed twice ->
378
+ * `E_DUPLICATE_ID`.
379
+ */
380
+ export function reduceSectionReorder(manifest, op, options = {}) {
381
+ if (!requireString(op, 'view')) {
382
+ return fail(
383
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
384
+ 'section.reorder requires a non-empty string "view".',
385
+ )
386
+ }
387
+ if (!Array.isArray(op.order)) {
388
+ return fail(
389
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
390
+ 'section.reorder requires an "order" array of entry ids.',
391
+ )
392
+ }
393
+ const viewName = op.view
394
+ const recipe = getRecipe(viewName)
395
+ if (!recipe) {
396
+ return fail(
397
+ SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_HOST,
398
+ `Unknown view "${viewName}" is not a section host. Allowed views: ${allowedViewNames().join(', ')}.`,
399
+ )
400
+ }
401
+ const installed = manifest.views.find((view) => view.name === viewName)
402
+ if (!installed) {
403
+ return fail(
404
+ DIAGNOSTIC_CODES.E_VIEW_NOT_INSTALLED,
405
+ `View "${viewName}" is not installed; add the view before reordering its sections.`,
406
+ )
407
+ }
408
+ const registeredSlots = Array.isArray(recipe.registeredSlots) ? recipe.registeredSlots : []
409
+ const allowedSections = Array.isArray(recipe.allowedSections) ? recipe.allowedSections : []
410
+ const maxSections = typeof recipe.maxSections === 'number' ? recipe.maxSections : null
411
+
412
+ // Normalize/validate the current composition through the single authority so a
413
+ // legacy or malformed list is adapted or refused consistently, with no output
414
+ // on failure.
415
+ const normalized = normalizeViewSections(viewName, installed.sections ?? [], registeredSlots, {
416
+ allowedSections,
417
+ maxSections,
418
+ })
419
+ if (!normalized.ok) return { ok: false, diagnostics: normalized.diagnostics }
420
+ const current = normalized.sections
421
+ const byId = new Map(current.map((entry) => [entry.id, entry]))
422
+ const slotIds = new Set(current.filter((entry) => isSlotSectionEntry(entry)).map((entry) => entry.id))
423
+
424
+ const order = op.order
425
+ for (const id of order) {
426
+ if (typeof id !== 'string' || id.length === 0) {
427
+ return fail(
428
+ DIAGNOSTIC_CODES.E_INVALID_OPERATION,
429
+ 'section.reorder "order" entries must be non-empty strings.',
430
+ )
431
+ }
432
+ }
433
+
434
+ const seen = new Set()
435
+ for (const id of order) {
436
+ if (!byId.has(id)) {
437
+ return fail(
438
+ SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_ORDER_REF,
439
+ `section.reorder names id "${id}" which is not present in view "${viewName}".`,
440
+ )
441
+ }
442
+ if (seen.has(id)) {
443
+ if (slotIds.has(id)) {
444
+ return fail(
445
+ SECTION_DIAGNOSTIC_CODES.E_CORE_SLOT_REMOVAL,
446
+ `section.reorder lists the core slot "${id}" more than once; registered slots appear exactly once.`,
447
+ )
448
+ }
449
+ return fail(
450
+ SECTION_DIAGNOSTIC_CODES.E_DUPLICATE_ID,
451
+ `section.reorder lists id "${id}" more than once.`,
452
+ )
453
+ }
454
+ seen.add(id)
455
+ }
456
+
457
+ // Every current entry id must appear exactly once. An omitted registered slot is
458
+ // `E_CORE_SLOT_REMOVAL` (design: "order omits ... a registered slot"); an omitted
459
+ // editorial id is the dual of an unknown reference, refused with
460
+ // `E_UNKNOWN_ORDER_REF` (the design named no separate code; recorded in
461
+ // `design.md`).
462
+ for (const id of byId.keys()) {
463
+ if (seen.has(id)) continue
464
+ if (slotIds.has(id)) {
465
+ return fail(
466
+ SECTION_DIAGNOSTIC_CODES.E_CORE_SLOT_REMOVAL,
467
+ `section.reorder omits the core slot "${id}"; registered slots cannot be removed or omitted.`,
468
+ )
469
+ }
470
+ return fail(
471
+ SECTION_DIAGNOSTIC_CODES.E_UNKNOWN_ORDER_REF,
472
+ `section.reorder "order" is not a permutation of view "${viewName}" entry ids: it omits "${id}".`,
473
+ )
474
+ }
475
+
476
+ const sections = order.map((id) => byId.get(id))
477
+ const views = manifest.views.map((view) =>
478
+ view.name === viewName ? { ...view, sections } : view,
479
+ )
480
+ return { ok: true, manifest: { ...manifest, views } }
481
+ }
482
+
483
+ // E3's production registry: E1's six operations + E2's catalog logical controls +
484
+ // E3's `section.add`/`section.remove`/`section.reorder`. `E1_OPERATION_REGISTRY`
485
+ // and `CATALOG_OPERATION_REGISTRY` are never mutated, so E1's vocabulary and E2's
486
+ // controls stay exactly as accepted.
487
+ export const SECTION_OPERATION_REGISTRY = extendOperationRegistry(CATALOG_OPERATION_REGISTRY, {
488
+ operations: [
489
+ {
490
+ name: 'section.add',
491
+ required: ['view', 'type'],
492
+ optional: ['id', 'variant', 'at'],
493
+ reducer: reduceSectionAdd,
494
+ },
495
+ {
496
+ name: 'section.remove',
497
+ required: ['view', 'id'],
498
+ optional: [],
499
+ reducer: reduceSectionRemove,
500
+ },
501
+ {
502
+ name: 'section.reorder',
503
+ required: ['view', 'order'],
504
+ optional: [],
505
+ reducer: reduceSectionReorder,
506
+ },
507
+ ],
508
+ })
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // Initial composition for `storefront create`.
3
+ //
4
+ // Without `--starter`, create seeds the minimal baseline (home + cart, no
5
+ // features). With `--starter`, it seeds the full curated set: every curated
6
+ // feature and every curated view — including the views a feature owns, so each
7
+ // installed feature satisfies the composition contract. The set is derived from
8
+ // the registry (`recipes/validate.mjs`), never a hand-maintained list, so held
9
+ // units are excluded by construction.
10
+
11
+ import { curatedViewNames, starterComposition } from '../recipes/validate.mjs'
12
+
13
+ export function scaffoldComposition({ starter = false } = {}) {
14
+ if (!starter) return { views: ['home', 'cart'], features: [] }
15
+ const { features } = starterComposition()
16
+ return { views: curatedViewNames(), features }
17
+ }
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // Keeps the per-skill copies of shared assets in sync with the single source.
3
3
  //
4
- // Source of truth: recipes/ at the package root.
5
- // Generated copies:
6
- // skills/sf-view-gen/assets/recipes/{recipes,archetypes,sections}.json
7
- // skills/sf-view-gen/scripts/validate.mjs
4
+ // Source of truth: `recipes/` and `scripts/schema-validate.mjs` at the package root.
5
+ // Generated copies (the installed skill must be executable from its own assets,
6
+ // so the catalog loader, its schema, and the schema validator travel with it):
7
+ // skills/sf-view-gen/assets/recipes/{recipes,archetypes,sections,features}.json
8
+ // skills/sf-view-gen/assets/recipes/{catalog,catalog.schema}.json
9
+ // skills/sf-view-gen/scripts/{validate,catalog,schema-validate}.mjs
8
10
  //
9
- // Usage:
11
+ // Paths are package-root-relative. Usage:
10
12
  // node scripts/sync-assets.mjs # write the copies from source
11
13
  // node scripts/sync-assets.mjs --check # report divergence, exit 1 if any
12
14
 
@@ -19,13 +21,16 @@ import { fileURLToPath } from 'node:url'
19
21
  const __dirname = dirname(fileURLToPath(import.meta.url))
20
22
  const PKG_ROOT = resolve(__dirname, '..')
21
23
 
22
- const SOURCE = join(PKG_ROOT, 'recipes')
23
-
24
24
  const MANIFEST = [
25
- ['recipes.json', join(PKG_ROOT, 'skills/sf-view-gen/assets/recipes/recipes.json')],
26
- ['archetypes.json', join(PKG_ROOT, 'skills/sf-view-gen/assets/recipes/archetypes.json')],
27
- ['sections.json', join(PKG_ROOT, 'skills/sf-view-gen/assets/recipes/sections.json')],
28
- ['validate.mjs', join(PKG_ROOT, 'skills/sf-view-gen/scripts/validate.mjs')],
25
+ ['recipes/recipes.json', 'skills/sf-view-gen/assets/recipes/recipes.json'],
26
+ ['recipes/archetypes.json', 'skills/sf-view-gen/assets/recipes/archetypes.json'],
27
+ ['recipes/sections.json', 'skills/sf-view-gen/assets/recipes/sections.json'],
28
+ ['recipes/features.json', 'skills/sf-view-gen/assets/recipes/features.json'],
29
+ ['recipes/catalog.json', 'skills/sf-view-gen/assets/recipes/catalog.json'],
30
+ ['recipes/catalog.schema.json', 'skills/sf-view-gen/assets/recipes/catalog.schema.json'],
31
+ ['recipes/catalog.mjs', 'skills/sf-view-gen/scripts/catalog.mjs'],
32
+ ['recipes/validate.mjs', 'skills/sf-view-gen/scripts/validate.mjs'],
33
+ ['scripts/schema-validate.mjs', 'skills/sf-view-gen/scripts/schema-validate.mjs'],
29
34
  ]
30
35
 
31
36
  function sha256(text) {
@@ -34,8 +39,9 @@ function sha256(text) {
34
39
 
35
40
  export async function checkAssets() {
36
41
  const diverged = []
37
- for (const [sourceName, destPath] of MANIFEST) {
38
- const source = await readFile(join(SOURCE, sourceName), 'utf8')
42
+ for (const [sourceRel, destRel] of MANIFEST) {
43
+ const source = await readFile(join(PKG_ROOT, sourceRel), 'utf8')
44
+ const destPath = join(PKG_ROOT, destRel)
39
45
  if (!existsSync(destPath)) {
40
46
  diverged.push(`${destPath} (missing)`)
41
47
  continue
@@ -50,8 +56,9 @@ export async function checkAssets() {
50
56
 
51
57
  async function syncAssets() {
52
58
  let written = 0
53
- for (const [sourceName, destPath] of MANIFEST) {
54
- const source = await readFile(join(SOURCE, sourceName), 'utf8')
59
+ for (const [sourceRel, destRel] of MANIFEST) {
60
+ const source = await readFile(join(PKG_ROOT, sourceRel), 'utf8')
61
+ const destPath = join(PKG_ROOT, destRel)
55
62
  await mkdir(dirname(destPath), { recursive: true })
56
63
  await writeFile(destPath, source, 'utf8')
57
64
  written++