@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,128 @@
1
+ #!/usr/bin/env node
2
+ // Semantic executable catalog: the single contract that states, per view slot
3
+ // and candidate block variant, whether the variant is `executable` or `held`,
4
+ // the block it renders, the canonical data shape it consumes, the events it
5
+ // emits, its controlled/uncontrolled state, its requirements, and an optional
6
+ // prop bridge from the canonical shape to the block props.
7
+ //
8
+ // The published shape lives in `recipes/catalog.schema.json`; this module loads
9
+ // that same artifact and adds only the invariants the supported schema subset
10
+ // cannot express (a held entry must carry a reason; a slot's default must be a
11
+ // declared variant; an unverified capability must name its owning change). It
12
+ // does not classify the initial set, derive `supported`, or wire any
13
+ // generator/introspection — those are owned by later tasks.
14
+
15
+ import { existsSync, readFileSync } from 'node:fs'
16
+ import { dirname, join } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+ import { validateAgainstSchema, validateSchemaSupported } from '../scripts/schema-validate.mjs'
19
+
20
+ const here = dirname(fileURLToPath(import.meta.url))
21
+
22
+ // Layout-agnostic catalog location: at the package root the catalog lives beside
23
+ // this module (`recipes/`); in an installed skill it is distributed under
24
+ // `skills/sf-view-gen/assets/recipes/` and addressed by relative path. Either
25
+ // layout holds the same synced artifact, so there is one catalog source. The
26
+ // `../scripts/schema-validate.mjs` import resolves to the package `scripts/` at
27
+ // the root and to the skill's own `scripts/` once distributed (same directory).
28
+ const catalogDir = existsSync(join(here, 'catalog.json'))
29
+ ? here
30
+ : join(here, '..', 'assets', 'recipes')
31
+
32
+ export const CATALOG_VERSION = 1
33
+ export const CATALOG_SCHEMA_ID = 'https://thorcommerce.dev/storefront/catalog.schema.json'
34
+
35
+ const catalogSchema = JSON.parse(readFileSync(join(catalogDir, 'catalog.schema.json'), 'utf8'))
36
+
37
+ function isPlainObject(value) {
38
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
39
+ }
40
+
41
+ function hasOwn(object, key) {
42
+ return Object.prototype.hasOwnProperty.call(object, key)
43
+ }
44
+
45
+ export function loadCatalogSchema() {
46
+ return catalogSchema
47
+ }
48
+
49
+ /**
50
+ * Load the published semantic catalog document (`recipes/catalog.json`) — the
51
+ * single source of the executable/held classification. Callers validate it with
52
+ * `validateCatalog`; classification is data, not logic (E1 task 1.2).
53
+ */
54
+ export function loadCatalog() {
55
+ return JSON.parse(readFileSync(join(catalogDir, 'catalog.json'), 'utf8'))
56
+ }
57
+
58
+ /**
59
+ * The slot variant entry schema, read from the published catalog schema so the
60
+ * per-entry contract has exactly one source. Throws loudly if the schema is
61
+ * reshaped, rather than silently validating against nothing.
62
+ */
63
+ function variantEntrySchema() {
64
+ const entry =
65
+ catalogSchema?.properties?.slots?.additionalProperties?.properties?.variants
66
+ ?.additionalProperties
67
+ if (!isPlainObject(entry)) {
68
+ throw new Error('catalog.schema.json does not declare a slot variant entry schema.')
69
+ }
70
+ return entry
71
+ }
72
+
73
+ /**
74
+ * Validate one slot variant entry against the published contract.
75
+ * Returns { ok, errors }.
76
+ */
77
+ export function validateCatalogEntry(entry) {
78
+ const errors = validateAgainstSchema(entry, variantEntrySchema(), 'catalog entry')
79
+ return { ok: errors.length === 0, errors }
80
+ }
81
+
82
+ /**
83
+ * Validate a complete catalog document against the published contract plus the
84
+ * invariants the schema subset cannot express. Returns { ok, errors }.
85
+ */
86
+ export function validateCatalog(catalog) {
87
+ if (!isPlainObject(catalog)) {
88
+ return { ok: false, errors: ['Catalog must be a JSON object.'] }
89
+ }
90
+ const errors = validateSchemaSupported(catalogSchema)
91
+ errors.push(...validateAgainstSchema(catalog, catalogSchema, 'catalog'))
92
+
93
+ const slots = isPlainObject(catalog.slots) ? catalog.slots : {}
94
+ for (const [slotId, slot] of Object.entries(slots)) {
95
+ if (!isPlainObject(slot)) continue
96
+ const variants = isPlainObject(slot.variants) ? slot.variants : {}
97
+ if (Object.keys(variants).length === 0) {
98
+ errors.push(`Slot "${slotId}" must declare at least one variant.`)
99
+ }
100
+ if (typeof slot.default === 'string' && !hasOwn(variants, slot.default)) {
101
+ errors.push(`Slot "${slotId}" default "${slot.default}" is not a declared variant.`)
102
+ }
103
+ for (const [variantName, entry] of Object.entries(variants)) {
104
+ if (!isPlainObject(entry)) continue
105
+ if (entry.status === 'held' && !(typeof entry.reason === 'string' && entry.reason.length > 0)) {
106
+ errors.push(`Held variant "${slotId}.${variantName}" must declare a non-empty reason.`)
107
+ }
108
+ }
109
+ }
110
+
111
+ // An unverified capability is held: it must name the change that owns its
112
+ // implementation, so a consumer can see that the capability is not available
113
+ // and who will make it available. This is the honest gate the schema subset
114
+ // cannot express (E1 task 4.2 / CU-04).
115
+ const capabilities = isPlainObject(catalog.capabilities) ? catalog.capabilities : {}
116
+ for (const [capabilityId, capability] of Object.entries(capabilities)) {
117
+ if (!isPlainObject(capability)) continue
118
+ if (
119
+ capability.verification === 'unverified' &&
120
+ !(typeof capability.owner === 'string' && capability.owner.length > 0)
121
+ ) {
122
+ errors.push(
123
+ `Unverified capability "${capabilityId}" must declare the owning change in "owner".`
124
+ )
125
+ }
126
+ }
127
+ return { ok: errors.length === 0, errors }
128
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://thorcommerce.dev/storefront/catalog.schema.json",
4
+ "title": "Thor Commerce Storefront Semantic Catalog",
5
+ "description": "The semantic executable catalog: per view slot and candidate variant, its execution status (executable | held), the block it renders, its canonical data shape, emitted events, controlled/uncontrolled state, requirements, and an optional prop bridge. Held entries carry a reason. Also declares operational capabilities with their backend/data requirements and a verification state (verified | unverified); an unverified capability names its owning change in `owner`. Versioned by catalogVersion.",
6
+ "type": "object",
7
+ "required": ["catalogVersion", "slots"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "catalogVersion": { "type": "number", "enum": [1] },
12
+ "description": { "type": "string" },
13
+ "slots": {
14
+ "type": "object",
15
+ "additionalProperties": {
16
+ "type": "object",
17
+ "required": ["default", "variants"],
18
+ "additionalProperties": false,
19
+ "properties": {
20
+ "default": { "type": "string", "minLength": 1 },
21
+ "variants": {
22
+ "type": "object",
23
+ "additionalProperties": {
24
+ "type": "object",
25
+ "required": ["status", "block", "dataShape", "events", "state", "requirements"],
26
+ "additionalProperties": false,
27
+ "properties": {
28
+ "status": { "type": "string", "enum": ["executable", "held"] },
29
+ "reason": { "type": "string", "minLength": 1 },
30
+ "block": { "type": "string", "minLength": 1 },
31
+ "dataShape": { "type": "string", "minLength": 1 },
32
+ "events": { "type": "array", "items": { "type": "string" } },
33
+ "state": { "type": "string", "enum": ["controlled", "uncontrolled"] },
34
+ "requirements": { "type": "array", "items": { "type": "string" } },
35
+ "bridge": {
36
+ "type": "object",
37
+ "required": ["from", "to"],
38
+ "additionalProperties": false,
39
+ "properties": {
40
+ "from": { "type": "string", "minLength": 1 },
41
+ "to": { "type": "string", "minLength": 1 }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ },
50
+ "capabilities": {
51
+ "type": "object",
52
+ "additionalProperties": {
53
+ "type": "object",
54
+ "required": ["requirements", "verification"],
55
+ "additionalProperties": false,
56
+ "properties": {
57
+ "requirements": { "type": "array", "items": { "type": "string" } },
58
+ "verification": { "type": "string", "enum": ["verified", "unverified"] },
59
+ "owner": { "type": "string", "minLength": 1 }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://thorcommerce.dev/storefront/feature.schema.json",
4
+ "title": "Storefront feature registry",
5
+ "type": "object",
6
+ "required": ["version", "features"],
7
+ "properties": {
8
+ "version": { "type": "string" },
9
+ "features": {
10
+ "type": "array",
11
+ "items": {
12
+ "type": "object",
13
+ "required": ["name", "views", "state", "handlers", "adapter", "surfaces", "config"],
14
+ "properties": {
15
+ "name": { "type": "string", "minLength": 1 },
16
+ "description": { "type": "string" },
17
+ "mvp": { "type": "boolean" },
18
+ "route": { "type": "string", "minLength": 1 },
19
+ "views": { "type": "array", "items": { "type": "string" } },
20
+ "state": { "type": "array", "items": { "type": "string" } },
21
+ "handlers": { "type": "array", "items": { "type": "string" } },
22
+ "adapter": {
23
+ "type": "object",
24
+ "required": ["needs"],
25
+ "properties": {
26
+ "needs": { "type": "array", "items": { "type": "string" } },
27
+ "fallback": { "type": "string" }
28
+ }
29
+ },
30
+ "config": {
31
+ "type": "object",
32
+ "description": "Per-feature configuration contract: a JSON Schema (the dependency-free subset) that the manifest's features[].config is validated against and whose `default` annotations seed absent fields."
33
+ },
34
+ "configRejection": {
35
+ "type": "object",
36
+ "description": "Stable diagnostic for configuration the feature does not accept. With no `when`, any non-empty config key is rejected with this code/message while an empty object stays the no-op (E1 task 3.5, e.g. checkout E_UNPROVEN_CHECKOUT_OPTION). With `when: { field, equals }`, only a config where that field equals the value is rejected (E2 task 4.12); an optional `capability` makes the rejection apply only while that catalog capability is not `verified`, so an unproven option fails loudly instead of being silently coerced to its default.",
37
+ "required": ["code", "message"],
38
+ "properties": {
39
+ "code": { "type": "string", "minLength": 1 },
40
+ "message": { "type": "string", "minLength": 1 },
41
+ "when": {
42
+ "type": "object",
43
+ "required": ["field", "equals"],
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "field": { "type": "string", "minLength": 1 },
47
+ "equals": {}
48
+ }
49
+ },
50
+ "capability": { "type": "string", "minLength": 1 }
51
+ }
52
+ },
53
+ "presentations": {
54
+ "type": "object",
55
+ "description": "Generation-owned block wiring: maps a config value (e.g. config.presentation) to the block variant the feature mount renders.",
56
+ "additionalProperties": { "type": "string" }
57
+ },
58
+ "mounts": {
59
+ "type": "array",
60
+ "description": "Feature-instance mounts into a host view's slot. The engine owns the block wiring; a mount never carries an element.",
61
+ "items": {
62
+ "type": "object",
63
+ "required": ["id", "host", "slot"],
64
+ "additionalProperties": false,
65
+ "properties": {
66
+ "id": { "type": "string", "minLength": 1 },
67
+ "host": { "type": "string", "minLength": 1 },
68
+ "slot": { "type": "string", "minLength": 1 }
69
+ }
70
+ }
71
+ },
72
+ "surfaces": {
73
+ "type": "array",
74
+ "description": "Legacy additive alias for existing held registry entries; derived from mounts where both exist.",
75
+ "items": {
76
+ "type": "object",
77
+ "required": ["host", "slot", "element"],
78
+ "properties": {
79
+ "host": { "type": "string" },
80
+ "slot": { "type": "string" },
81
+ "element": { "type": "string" }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,162 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "features": [
4
+ {
5
+ "name": "checkout",
6
+ "mvp": true,
7
+ "description": "Multi-step checkout: cart validation, shipping, payment, and order placement.",
8
+ "route": "/checkout",
9
+ "views": ["checkout"],
10
+ "state": [
11
+ "checkout",
12
+ "checkoutCountries",
13
+ "checkoutPaymentMethods",
14
+ "checkoutPending",
15
+ "checkoutError",
16
+ "lastOrder",
17
+ "lastOrderLines",
18
+ "orderPlaced"
19
+ ],
20
+ "handlers": ["placeOrder", "goToCheckout", "continueShopping"],
21
+ "adapter": { "needs": ["createCart", "completeCheckout"] },
22
+ "config": { "type": "object", "additionalProperties": false, "properties": {} },
23
+ "configRejection": {
24
+ "code": "E_UNPROVEN_CHECKOUT_OPTION",
25
+ "message": "checkout variants and route options are not proven; the checkout feature accepts an empty config only"
26
+ },
27
+ "surfaces": []
28
+ },
29
+ {
30
+ "name": "customer-auth",
31
+ "mvp": true,
32
+ "description": "Customer authentication: registration, login, logout, and a minimal signed-in state. Owns the account view.",
33
+ "route": "/account",
34
+ "views": ["account"],
35
+ "state": [
36
+ "accountCustomer",
37
+ "accountAuthenticated",
38
+ "accountMode",
39
+ "accountPending",
40
+ "accountError"
41
+ ],
42
+ "handlers": ["logout", "showRegister", "showLogin"],
43
+ "adapter": { "needs": [] },
44
+ "config": { "type": "object", "additionalProperties": false, "properties": {} },
45
+ "surfaces": []
46
+ },
47
+ {
48
+ "name": "static-pages",
49
+ "mvp": true,
50
+ "description": "Content-driven static pages (contact, legal, about) served with per-page metadata. Owns the static view.",
51
+ "route": "/contact",
52
+ "views": ["static"],
53
+ "state": [],
54
+ "handlers": [],
55
+ "adapter": { "needs": [] },
56
+ "config": { "type": "object", "additionalProperties": false, "properties": {} },
57
+ "surfaces": []
58
+ },
59
+ {
60
+ "name": "benefits",
61
+ "mvp": true,
62
+ "description": "Bounded editorial benefits feature that mounts at the home benefits slot; config selects the Feature1/Feature2 presentation while the item content stays user-owned.",
63
+ "views": [],
64
+ "state": [],
65
+ "handlers": [],
66
+ "adapter": { "needs": [] },
67
+ "config": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "presentation": { "type": "string", "enum": ["compact", "detailed"], "default": "compact" }
72
+ }
73
+ },
74
+ "presentations": { "compact": "Feature1", "detailed": "Feature2" },
75
+ "mounts": [{ "id": "home-benefits", "host": "home", "slot": "benefits" }],
76
+ "surfaces": []
77
+ },
78
+ {
79
+ "name": "catalog-search",
80
+ "mvp": true,
81
+ "description": "Optional category-listing search control (E2). Installation is presence, not an enabled flag; the config only supplies an optional placeholder. The search input contributes to the single controlled CategoryFilter2 at the category.search logical slot and never adds a block.",
82
+ "views": [],
83
+ "state": [],
84
+ "handlers": [],
85
+ "adapter": { "needs": [] },
86
+ "config": {
87
+ "type": "object",
88
+ "additionalProperties": false,
89
+ "properties": {
90
+ "placeholder": { "type": "string" }
91
+ }
92
+ },
93
+ "mounts": [{ "id": "category-search", "host": "category", "slot": "search" }],
94
+ "surfaces": []
95
+ },
96
+ {
97
+ "name": "category-filters",
98
+ "mvp": true,
99
+ "description": "Optional category-listing filter controls (E2). Installation is presence, not an enabled flag; the config selects which demonstrated basic filters show (filterSet) and whether the price control is shown (showPrice). The controls contribute to the single controlled CategoryFilter2 at the category.filters logical slot and never add a block.",
100
+ "views": [],
101
+ "state": [],
102
+ "handlers": [],
103
+ "adapter": { "needs": [] },
104
+ "config": {
105
+ "type": "object",
106
+ "additionalProperties": false,
107
+ "properties": {
108
+ "filterSet": {
109
+ "type": "array",
110
+ "items": { "type": "string", "enum": ["categories", "collections"] },
111
+ "default": ["categories", "collections"]
112
+ },
113
+ "showPrice": { "type": "boolean", "default": false }
114
+ }
115
+ },
116
+ "configRejection": {
117
+ "code": "E_UNVERIFIED_PRICE_RANGE",
118
+ "message": "showPrice: true requires a verified price range for the listing scope; capability \"category.price.range\" is unverified (held), so configure showPrice: false until a real range is demonstrated",
119
+ "when": { "field": "showPrice", "equals": true },
120
+ "capability": "category.price.range"
121
+ },
122
+ "mounts": [{ "id": "category-filters", "host": "category", "slot": "filters" }],
123
+ "surfaces": []
124
+ },
125
+ {
126
+ "name": "navigability",
127
+ "mvp": true,
128
+ "description": "Cross-cutting navigability feature: entity-derived breadcrumbs with BreadcrumbList structured data on the product and category surfaces, plus skip-to-content, current-page indication, back-to-top, and a mobile bottom navigation in the shared shell.",
129
+ "views": [],
130
+ "state": [],
131
+ "handlers": [],
132
+ "adapter": { "needs": [] },
133
+ "config": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "properties": {
137
+ "presentation": { "type": "string", "enum": ["default"], "default": "default" },
138
+ "showBackToTop": { "type": "boolean", "default": true }
139
+ }
140
+ },
141
+ "presentations": { "default": "Breadcrumb" },
142
+ "mounts": [
143
+ { "id": "category-breadcrumb", "host": "category", "slot": "breadcrumb" },
144
+ { "id": "product-breadcrumb", "host": "product", "slot": "breadcrumb" }
145
+ ],
146
+ "surfaces": []
147
+ },
148
+ {
149
+ "name": "recommendations",
150
+ "mvp": false,
151
+ "description": "Related-products surface injected into the product view.",
152
+ "views": [],
153
+ "state": ["recommendedProducts"],
154
+ "handlers": [],
155
+ "adapter": { "needs": ["getRelatedProducts"], "fallback": "catalog" },
156
+ "config": { "type": "object", "additionalProperties": false, "properties": {} },
157
+ "surfaces": [
158
+ { "host": "product", "slot": "product.related", "element": "ProductCard" }
159
+ ]
160
+ }
161
+ ]
162
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://thorcommerce.dev/storefront/manifest.schema.json",
4
+ "title": "Thor Commerce Storefront Composition Manifest",
5
+ "type": "object",
6
+ "required": ["version", "name", "theme", "views", "features"],
7
+ "properties": {
8
+ "$schema": { "type": "string" },
9
+ "version": { "type": "string", "minLength": 1 },
10
+ "name": { "type": "string", "minLength": 1 },
11
+ "siteName": { "type": "string" },
12
+ "theme": {
13
+ "type": "object",
14
+ "required": ["mode"],
15
+ "properties": {
16
+ "mode": { "type": "string", "enum": ["light", "dark", "system"] }
17
+ }
18
+ },
19
+ "views": {
20
+ "type": "array",
21
+ "items": {
22
+ "type": "object",
23
+ "required": ["name"],
24
+ "properties": {
25
+ "name": { "type": "string", "minLength": 1 },
26
+ "intents": {
27
+ "type": "object",
28
+ "additionalProperties": { "type": "string" }
29
+ },
30
+ "variants": {
31
+ "type": "object",
32
+ "additionalProperties": { "type": "string" }
33
+ },
34
+ "sections": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "properties": {
40
+ "id": { "type": "string", "minLength": 1 },
41
+ "type": { "type": "string", "minLength": 1 },
42
+ "name": { "type": "string", "minLength": 1 },
43
+ "variant": { "type": "string" }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ },
50
+ "preservedSectionIds": {
51
+ "type": "object",
52
+ "additionalProperties": {
53
+ "type": "array",
54
+ "items": { "type": "string", "minLength": 1 }
55
+ }
56
+ },
57
+ "features": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "object",
61
+ "required": ["name"],
62
+ "properties": {
63
+ "name": { "type": "string", "minLength": 1 },
64
+ "config": { "type": "object" }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://thorcommerce.dev/storefront/operations.schema.json",
4
+ "title": "Thor Commerce Storefront Operations Envelope",
5
+ "description": "The versioned input envelope for `storefront plan --operations`. `formatVersion` must equal 1, aligned with the catalog `catalogVersion` (both 1 in E1). E1 accepts exactly six namespaced operations: view.add, view.remove, view.configure, feature.add, feature.remove, feature.configure. The per-operation field contract (which fields are required, which optional, and the exactly-one `intent` / `variant` / `reset: true` rule) is declared by OPERATION_SCHEMAS in scripts/operations.mjs, which is the runtime authority; this published schema is the closed structural contract. Unknown operation names, unknown fields, and a `force` field are not part of version 1 (`force` is an internal add/refresh control used by the legacy `view add --force` alias and by plan/apply, never a public operation field).",
6
+ "type": "object",
7
+ "required": ["formatVersion", "operations"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "formatVersion": { "type": "number", "enum": [1] },
12
+ "operations": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "object",
16
+ "required": ["op"],
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "op": {
20
+ "type": "string",
21
+ "enum": [
22
+ "view.add",
23
+ "view.remove",
24
+ "view.configure",
25
+ "feature.add",
26
+ "feature.remove",
27
+ "feature.configure"
28
+ ]
29
+ },
30
+ "view": { "type": "string", "minLength": 1 },
31
+ "slot": { "type": "string", "minLength": 1 },
32
+ "intent": { "type": "string", "minLength": 1 },
33
+ "variant": { "type": "string", "minLength": 1 },
34
+ "reset": { "type": "boolean" },
35
+ "intents": { "type": "object", "additionalProperties": { "type": "string" } },
36
+ "variants": { "type": "object", "additionalProperties": { "type": "string" } },
37
+ "feature": { "type": "string", "minLength": 1 },
38
+ "config": { "type": "object" }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
@@ -23,7 +23,7 @@
23
23
  "properties": {
24
24
  "name": {
25
25
  "type": "string",
26
- "enum": ["home", "product", "category", "checkout", "user-panel"]
26
+ "enum": ["home", "product", "cart", "category", "checkout", "account", "static"]
27
27
  },
28
28
  "description": { "type": "string" },
29
29
  "route": { "type": "string" },
@@ -51,7 +51,16 @@
51
51
  "type": "string",
52
52
  "enum": ["testimonials", "faq", "statistics", "logoCloud", "pricing"]
53
53
  }
54
- }
54
+ },
55
+ "registeredSlots": {
56
+ "type": "array",
57
+ "items": { "type": "string" }
58
+ },
59
+ "allowedSections": {
60
+ "type": "array",
61
+ "items": { "type": "string", "enum": ["faq", "cta"] }
62
+ },
63
+ "maxSections": { "type": "number" }
55
64
  }
56
65
  }
57
66
  }