@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,256 @@
1
+ #!/usr/bin/env node
2
+ // E1 task 4.15 (CU-17): the namespaced, extensible operation registry and its
3
+ // logical-controls metadata seam (E1 design Decision 16).
4
+ //
5
+ // WHY THIS EXISTS: E1 owns exactly six public composition operations. E2 and E3
6
+ // must consume the same envelope, fingerprint, mount, and diagnostics contracts
7
+ // without redefining E1's engine, and E1 must not hardcode their behavior. This
8
+ // module is that seam — a registry keyed by the namespaced `op` string plus a
9
+ // pure, additive registration contract. Registration never mutates the registry
10
+ // it extends; it returns a new frozen registry. Production therefore stays
11
+ // closed: the E1 registry carries only its six operations, and any later-change
12
+ // operation remains `E_UNKNOWN_OPERATION` until a later change explicitly builds
13
+ // a registry that includes it. There is no environment switch and no backdoor.
14
+ //
15
+ // Two kinds of registration, deliberately separated:
16
+ //
17
+ // * An operation is BEHAVIOR: `{ name, required, optional, reducer }`. The
18
+ // `reducer` is a pure `(manifest, op, ctx) -> { ok, manifest?, diagnostics?,
19
+ // removals? }` function, the same contract `reduceManifest` dispatches.
20
+ // * A logical control is METADATA: it declares that a control slot contributes
21
+ // to one already-rendered presentation (`kind: "logical-control"`, `host`,
22
+ // `slot`, `presentation`, optional `capability`). It carries no render-owned
23
+ // field (`block`, `element`), no behavior (`reducer`), and no operation. This
24
+ // is the distinction E2 relies on when it contributes search/filters to the
25
+ // single `CategoryFilter2` listing with no new operation (E2 design; E1
26
+ // Decision 16). Registering a whole view is the catalog's job, not this
27
+ // seam's, so a contribution that tries to carry a render field is rejected.
28
+
29
+ export const OPERATION_NAMESPACE_SEPARATOR = '.'
30
+
31
+ export const LOGICAL_CONTROL_KIND = 'logical-control'
32
+
33
+ // The exact fields a logical-control contribution may carry. This is the
34
+ // consumed contract E2 uses: `feature` (the contributing feature), `host`/`slot`
35
+ // (the control slot, matching the feature's `mounts: [{ id, host, slot }]`
36
+ // shape), `presentation` (the one rendered block it feeds), and optional
37
+ // `capability` (the capability id it implements).
38
+ const LOGICAL_CONTROL_FIELDS = Object.freeze([
39
+ 'kind',
40
+ 'feature',
41
+ 'host',
42
+ 'slot',
43
+ 'presentation',
44
+ 'capability',
45
+ ])
46
+
47
+ // Render- and behavior-owned fields a logical control must NOT carry. Their
48
+ // presence is the signal that a caller is trying to register a whole view or an
49
+ // operation through the metadata seam; it is rejected with a clear message.
50
+ const LOGICAL_CONTROL_FORBIDDEN_FIELDS = Object.freeze([
51
+ 'block',
52
+ 'element',
53
+ 'reducer',
54
+ 'variants',
55
+ 'surfaces',
56
+ 'mounts',
57
+ 'route',
58
+ ])
59
+
60
+ function isPlainObject(value) {
61
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
62
+ }
63
+
64
+ function isNonEmptyString(value) {
65
+ return typeof value === 'string' && value.length > 0
66
+ }
67
+
68
+ function hasOwn(object, key) {
69
+ return Object.prototype.hasOwnProperty.call(object, key)
70
+ }
71
+
72
+ /**
73
+ * The namespace of a namespaced operation name: the segment before the first
74
+ * `.` (`view.add` -> `view`), or the empty string when the name is not
75
+ * namespaced. Each registered operation name must be namespaced so later changes
76
+ * extend the registry without colliding with E1's names.
77
+ */
78
+ export function operationNamespace(name) {
79
+ if (typeof name !== 'string') return ''
80
+ const index = name.indexOf(OPERATION_NAMESPACE_SEPARATOR)
81
+ return index === -1 ? '' : name.slice(0, index)
82
+ }
83
+
84
+ function assertOperationDefinition(definition) {
85
+ if (!isPlainObject(definition)) {
86
+ throw new TypeError('operation definition must be an object.')
87
+ }
88
+ if (!isNonEmptyString(definition.name) || operationNamespace(definition.name) === '') {
89
+ throw new TypeError(
90
+ `operation name must be a non-empty namespaced string such as "view.add"; got ${JSON.stringify(definition.name)}.`,
91
+ )
92
+ }
93
+ const required = definition.required ?? []
94
+ const optional = definition.optional ?? []
95
+ if (!Array.isArray(required) || !required.every(isNonEmptyString)) {
96
+ throw new TypeError(`operation "${definition.name}" required fields must be an array of strings.`)
97
+ }
98
+ if (!Array.isArray(optional) || !optional.every(isNonEmptyString)) {
99
+ throw new TypeError(`operation "${definition.name}" optional fields must be an array of strings.`)
100
+ }
101
+ const overlap = required.filter((field) => optional.includes(field))
102
+ if (overlap.length > 0) {
103
+ throw new TypeError(
104
+ `operation "${definition.name}" declares field(s) as both required and optional: ${overlap.join(', ')}.`,
105
+ )
106
+ }
107
+ if (typeof definition.reducer !== 'function') {
108
+ throw new TypeError(`operation "${definition.name}" must declare a reducer function.`)
109
+ }
110
+ }
111
+
112
+ function freezeOperation(definition) {
113
+ assertOperationDefinition(definition)
114
+ return Object.freeze({
115
+ name: definition.name,
116
+ namespace: operationNamespace(definition.name),
117
+ required: Object.freeze([...(definition.required ?? [])]),
118
+ optional: Object.freeze([...(definition.optional ?? [])]),
119
+ reducer: definition.reducer,
120
+ })
121
+ }
122
+
123
+ function assertLogicalControl(contribution) {
124
+ if (!isPlainObject(contribution)) {
125
+ throw new TypeError('logical control must be an object.')
126
+ }
127
+ if (contribution.kind !== LOGICAL_CONTROL_KIND) {
128
+ throw new TypeError(
129
+ `logical control must declare kind "${LOGICAL_CONTROL_KIND}"; got ${JSON.stringify(contribution.kind)}.`,
130
+ )
131
+ }
132
+ for (const field of ['feature', 'host', 'slot', 'presentation']) {
133
+ if (!isNonEmptyString(contribution[field])) {
134
+ throw new TypeError(`logical control must declare a non-empty string "${field}".`)
135
+ }
136
+ }
137
+ if (hasOwn(contribution, 'capability') && !isNonEmptyString(contribution.capability)) {
138
+ throw new TypeError('logical control "capability" must be a non-empty string when present.')
139
+ }
140
+ for (const field of Object.keys(contribution)) {
141
+ if (LOGICAL_CONTROL_FIELDS.includes(field)) continue
142
+ if (LOGICAL_CONTROL_FORBIDDEN_FIELDS.includes(field)) {
143
+ throw new TypeError(
144
+ `logical control is metadata and must not carry render/behavior field "${field}"; register a whole view in the catalog or a new operation instead.`,
145
+ )
146
+ }
147
+ throw new TypeError(
148
+ `logical control has unknown field "${field}". Allowed fields: ${LOGICAL_CONTROL_FIELDS.join(', ')}.`,
149
+ )
150
+ }
151
+ }
152
+
153
+ function freezeLogicalControl(contribution) {
154
+ assertLogicalControl(contribution)
155
+ const out = {}
156
+ for (const field of LOGICAL_CONTROL_FIELDS) {
157
+ if (hasOwn(contribution, field)) out[field] = contribution[field]
158
+ }
159
+ return Object.freeze(out)
160
+ }
161
+
162
+ function freezeRegistry(operations, logicalControls) {
163
+ return Object.freeze({
164
+ operations: Object.freeze({ ...operations }),
165
+ logicalControls: Object.freeze({ ...logicalControls }),
166
+ })
167
+ }
168
+
169
+ /**
170
+ * Build an immutable registry from an extension `{ operations?, logicalControls? }`.
171
+ * An empty extension yields the empty registry; E1 builds its six operations
172
+ * from this factory (`scripts/operations.mjs`). Malformed definitions throw a
173
+ * `TypeError` — this is an internal programming contract, not a user diagnostic.
174
+ */
175
+ export function createOperationRegistry(extension = {}) {
176
+ return extendOperationRegistry(freezeRegistry({}, {}), extension)
177
+ }
178
+
179
+ /**
180
+ * Return a NEW registry that adds the extension's operations and logical
181
+ * controls on top of `registry`. The input registry is never mutated, so a
182
+ * caller's base (production's E1 registry) can never be widened by an
183
+ * extension. A duplicate operation name or a duplicate `(host, slot)` logical
184
+ * control throws.
185
+ */
186
+ export function extendOperationRegistry(registry, extension = {}) {
187
+ if (
188
+ !isPlainObject(registry) ||
189
+ !isPlainObject(registry.operations) ||
190
+ !isPlainObject(registry.logicalControls)
191
+ ) {
192
+ throw new TypeError('extendOperationRegistry requires a registry produced by createOperationRegistry.')
193
+ }
194
+ if (!isPlainObject(extension)) {
195
+ throw new TypeError('operation registry extension must be an object.')
196
+ }
197
+ const operations = { ...registry.operations }
198
+ const logicalControls = { ...registry.logicalControls }
199
+ for (const definition of extension.operations ?? []) {
200
+ const frozen = freezeOperation(definition)
201
+ if (hasOwn(operations, frozen.name)) {
202
+ throw new TypeError(`operation "${frozen.name}" is already registered; names must be unique.`)
203
+ }
204
+ operations[frozen.name] = frozen
205
+ }
206
+ for (const contribution of extension.logicalControls ?? []) {
207
+ const frozen = freezeLogicalControl(contribution)
208
+ const key = `${frozen.host}.${frozen.slot}`
209
+ if (hasOwn(logicalControls, key)) {
210
+ throw new TypeError(
211
+ `logical control slot "${key}" is already registered; a host slot has one contribution.`,
212
+ )
213
+ }
214
+ logicalControls[key] = frozen
215
+ }
216
+ return freezeRegistry(operations, logicalControls)
217
+ }
218
+
219
+ /** The registered operation names, in registration order (E1's order is stable). */
220
+ export function operationNames(registry) {
221
+ return Object.keys(registry.operations)
222
+ }
223
+
224
+ /** The distinct namespaces across the registered operations. */
225
+ export function operationNamespaces(registry) {
226
+ return [...new Set(operationNames(registry).map(operationNamespace))]
227
+ }
228
+
229
+ /** The `{ required, optional }` schema of a registered operation, or null. */
230
+ export function operationSchema(registry, name) {
231
+ const operation = registry.operations[name]
232
+ return operation
233
+ ? { required: [...operation.required], optional: [...operation.optional] }
234
+ : null
235
+ }
236
+
237
+ /** The canonical field order (required then optional) of a registered operation. */
238
+ export function operationFieldOrder(registry, name) {
239
+ const operation = registry.operations[name]
240
+ return operation ? [...operation.required, ...operation.optional] : []
241
+ }
242
+
243
+ /** The reducer of a registered operation, or null for an unregistered name. */
244
+ export function operationReducer(registry, name) {
245
+ return registry.operations[name]?.reducer ?? null
246
+ }
247
+
248
+ /** The registered logical-control contributions as an array. */
249
+ export function logicalControls(registry) {
250
+ return Object.values(registry.logicalControls)
251
+ }
252
+
253
+ /** The logical control registered for a host slot, or null. */
254
+ export function logicalControlFor(registry, host, slot) {
255
+ return registry.logicalControls[`${host}.${slot}`] ?? null
256
+ }