@thorprovider/storefront 0.3.1 → 0.4.0-mvp.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +248 -99
  3. package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
  4. package/assets/templates/app/app/layout.tsx +18 -4
  5. package/assets/templates/app/app/robots.ts +13 -0
  6. package/assets/templates/app/app/sitemap.ts +12 -0
  7. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
  8. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  9. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
  10. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  13. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  14. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  15. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +232 -51
  16. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
  17. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -0
  18. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  20. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +96 -0
  21. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
  22. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
  23. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  24. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  27. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +142 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  30. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  31. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  32. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +17 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  34. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  39. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  40. package/assets/templates/features/checkout/handlers.ts +153 -0
  41. package/assets/templates/features/checkout/state.ts +61 -0
  42. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  43. package/assets/templates/features/customer-auth/state.ts +36 -0
  44. package/assets/templates/features/recommendations/state.ts +11 -0
  45. package/assets/templates/views/account/content.ts +35 -0
  46. package/assets/templates/views/account/index.tsx +114 -0
  47. package/assets/templates/views/account/registry.tsx +378 -0
  48. package/assets/templates/views/account/schema.ts +52 -0
  49. package/assets/templates/views/category/assembly/assembly.json +28 -0
  50. package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
  51. package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -0
  52. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  53. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  54. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  55. package/assets/templates/views/category/content.ts +30 -0
  56. package/assets/templates/views/category/data.ts +12 -0
  57. package/assets/templates/views/category/index.tsx +230 -0
  58. package/assets/templates/views/category/props.ts +16 -0
  59. package/assets/templates/views/category/registry.tsx +103 -0
  60. package/assets/templates/views/category/schema.ts +78 -0
  61. package/assets/templates/views/checkout/content.ts +105 -0
  62. package/assets/templates/views/checkout/data.ts +23 -0
  63. package/assets/templates/views/checkout/index.tsx +111 -0
  64. package/assets/templates/views/checkout/props.ts +49 -0
  65. package/assets/templates/views/checkout/registry.tsx +217 -0
  66. package/assets/templates/views/checkout/schema.ts +229 -0
  67. package/assets/templates/views/home/assembly/assembly.json +24 -0
  68. package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -0
  69. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  70. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  71. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  72. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  73. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  74. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  75. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  76. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  77. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  78. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  79. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  80. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  81. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  82. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  83. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  84. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  85. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  86. package/assets/templates/views/home/content.ts +70 -0
  87. package/assets/templates/views/home/data.ts +15 -0
  88. package/assets/templates/views/home/index.tsx +38 -84
  89. package/assets/templates/views/home/props.ts +22 -0
  90. package/assets/templates/views/home/registry.tsx +38 -22
  91. package/assets/templates/views/home/schema.ts +93 -0
  92. package/assets/templates/views/product/content.ts +25 -0
  93. package/assets/templates/views/product/data.ts +48 -0
  94. package/assets/templates/views/product/index.tsx +104 -0
  95. package/assets/templates/views/product/props.ts +20 -0
  96. package/assets/templates/views/product/registry.tsx +78 -0
  97. package/assets/templates/views/product/schema.ts +108 -0
  98. package/assets/templates/views/static/content.ts +99 -0
  99. package/assets/templates/views/static/index.tsx +60 -0
  100. package/assets/templates/views/static/registry.tsx +66 -0
  101. package/assets/templates/views/static/schema.ts +45 -0
  102. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
  103. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
  104. package/assets/templates/views/user-panel/schema.ts +79 -0
  105. package/bin/storefront.js +1278 -79
  106. package/commands/sf-add-view.md +44 -13
  107. package/commands/sf-feature.md +28 -0
  108. package/commands/sf-theme.md +14 -8
  109. package/commands/sf-view.md +54 -12
  110. package/package.json +6 -4
  111. package/recipes/archetypes.json +28 -1
  112. package/recipes/catalog.json +361 -0
  113. package/recipes/catalog.mjs +128 -0
  114. package/recipes/catalog.schema.json +64 -0
  115. package/recipes/feature.schema.json +89 -0
  116. package/recipes/features.json +139 -0
  117. package/recipes/manifest.schema.json +69 -0
  118. package/recipes/operations.schema.json +43 -0
  119. package/recipes/recipe.schema.json +11 -2
  120. package/recipes/recipes.json +60 -5
  121. package/recipes/sections.json +24 -5
  122. package/recipes/validate.mjs +784 -32
  123. package/scripts/candidate.mjs +236 -0
  124. package/scripts/composers.mjs +140 -0
  125. package/scripts/file-classes.mjs +116 -0
  126. package/scripts/logical-controls.mjs +61 -0
  127. package/scripts/manifest.mjs +298 -0
  128. package/scripts/operation-registry.mjs +256 -0
  129. package/scripts/operations.mjs +720 -0
  130. package/scripts/ownership-conflict.mjs +356 -0
  131. package/scripts/pinning.mjs +504 -0
  132. package/scripts/plan-apply.mjs +843 -0
  133. package/scripts/preflight.mjs +51 -0
  134. package/scripts/reconcile.mjs +511 -0
  135. package/scripts/render-templates.mjs +245 -28
  136. package/scripts/schema-validate.mjs +132 -0
  137. package/scripts/section-compat.mjs +300 -0
  138. package/scripts/section-operations.mjs +508 -0
  139. package/scripts/sync-assets.mjs +22 -15
  140. package/scripts/view-assembly.mjs +291 -0
  141. package/scripts/view-selection.mjs +211 -0
  142. package/skills/sf-feature/SKILL.md +67 -0
  143. package/skills/sf-theme-gen/SKILL.md +12 -5
  144. package/skills/sf-view-gen/SKILL.md +123 -93
  145. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
  146. package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
  147. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  148. package/skills/sf-view-gen/assets/recipes/features.json +139 -0
  149. package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
  150. package/skills/sf-view-gen/assets/recipes/sections.json +24 -5
  151. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  152. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  153. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  154. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  155. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  156. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  157. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  158. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  159. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  160. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  161. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  162. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  163. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  164. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  165. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  166. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  167. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  168. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  169. package/skills/sf-view-gen/references/views.md +44 -10
  170. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  171. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  172. package/skills/sf-view-gen/scripts/validate.mjs +784 -32
  173. package/skills/storefront-workflow/SKILL.md +116 -0
  174. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  175. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  176. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  177. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  178. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  179. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic normalization and persisted legacy-id migration for a view's
3
+ // ordered `views[].sections` composition (E3 task 1.5, CU-15/CU-16).
4
+ //
5
+ // The pure entry point maps a declared section list — possibly missing, empty,
6
+ // or in the pre-E3 legacy `{ name, variant }` shape — to the ordered
7
+ // `{ id, type, variant? }` model:
8
+ // * a registered presentation slot is `{ id: "slot:<slot>", type: "<slot>" }`
9
+ // and is mandatory: the normalized list always contains every registered
10
+ // slot exactly once, in registry order unless the declared list reorders it;
11
+ // * a legacy entry migrates to `{ id: "<name>-<n>", type: "<name>",
12
+ // variant? }`, where `n` is the 1-based position among the view's legacy
13
+ // entries in declared order, appended after the slots;
14
+ // * anything that cannot be adapted safely (a held/disallowed legacy type, a
15
+ // reserved-prefixed id, a duplicate) is refused with a diagnostic and no
16
+ // output, so the caller writes nothing.
17
+ //
18
+ // It is a pure function over its arguments (same input -> identical output) and
19
+ // depends only on the Node standard library. Wiring it into the manifest
20
+ // load/validation and CLI paths is owned by the later migration/operation tasks
21
+ // (§3/§5); this module does not touch the filesystem beyond reading the registry
22
+ // for the optional convenience wrapper.
23
+
24
+ import { existsSync, readFileSync } from 'node:fs'
25
+ import { dirname, join } from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ const here = dirname(fileURLToPath(import.meta.url))
29
+
30
+ function isPlainObject(value) {
31
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
32
+ }
33
+
34
+ function isNonEmptyString(value) {
35
+ return typeof value === 'string' && value.length > 0
36
+ }
37
+
38
+ function slotEntry(slot) {
39
+ return { id: `slot:${slot}`, type: slot }
40
+ }
41
+
42
+ // Copy only the declared fields, so an entry carrying an unknown property never
43
+ // leaks into the normalized output and the input object is never mutated.
44
+ function copyEntry(entry) {
45
+ return {
46
+ id: entry.id,
47
+ type: entry.type,
48
+ ...(isNonEmptyString(entry.variant) ? { variant: entry.variant } : {}),
49
+ }
50
+ }
51
+
52
+ // A legacy `{ name, variant? }` entry (or its bare-string shorthand) — the
53
+ // pre-E3 shape that has no id/type yet.
54
+ export function isLegacySectionEntry(entry) {
55
+ const value = typeof entry === 'string' ? { name: entry } : entry
56
+ if (!isPlainObject(value)) return false
57
+ if (isNonEmptyString(value.id) || isNonEmptyString(value.type)) return false
58
+ return isNonEmptyString(value.name)
59
+ }
60
+
61
+ // A registered-slot entry: the reserved `id === "slot:" + type` form.
62
+ export function isSlotSectionEntry(entry) {
63
+ return (
64
+ isPlainObject(entry) &&
65
+ isNonEmptyString(entry.id) &&
66
+ isNonEmptyString(entry.type) &&
67
+ entry.id === `slot:${entry.type}`
68
+ )
69
+ }
70
+
71
+ // An editorial instance: a persisted id that must not start with `slot:`.
72
+ export function isEditorialSectionEntry(entry) {
73
+ return (
74
+ isPlainObject(entry) &&
75
+ isNonEmptyString(entry.id) &&
76
+ isNonEmptyString(entry.type) &&
77
+ !entry.id.startsWith('slot:')
78
+ )
79
+ }
80
+
81
+ // Discriminate a declared entry: 'slot' (reserved id form), 'editorial'
82
+ // (persisted non-reserved id), 'legacy' (`{ name }`), or 'invalid'.
83
+ export function classifySectionEntry(entry) {
84
+ const value = typeof entry === 'string' ? { name: entry } : entry
85
+ if (!isPlainObject(value)) return 'invalid'
86
+ const hasId = isNonEmptyString(value.id)
87
+ const hasType = isNonEmptyString(value.type)
88
+ const hasName = isNonEmptyString(value.name)
89
+ if (hasId && hasType && !hasName) {
90
+ return value.id.startsWith('slot:') ? 'slot' : 'editorial'
91
+ }
92
+ if (hasName && !hasId && !hasType) return 'legacy'
93
+ return 'invalid'
94
+ }
95
+
96
+ /**
97
+ * Normalize one view's declared sections to the ordered composition model.
98
+ *
99
+ * @param {string} viewName
100
+ * @param {unknown[]} declaredSections the manifest `views[].sections` (may be
101
+ * missing/empty, legacy, new-form, or a mix)
102
+ * @param {string[]} registeredSlots the view's ordered core slots
103
+ * @param {{ allowedSections?: string[], maxSections?: number }} [options]
104
+ * @returns {{ ok: boolean, sections: object[] | null, diagnostics: object[] }}
105
+ * On failure `sections` is null (no output) and `diagnostics` is non-empty.
106
+ */
107
+ export function normalizeViewSections(viewName, declaredSections, registeredSlots, options = {}) {
108
+ const slots = Array.isArray(registeredSlots) ? registeredSlots.filter(isNonEmptyString) : []
109
+ const allowedSections = Array.isArray(options.allowedSections) ? options.allowedSections : null
110
+ const maxSections = typeof options.maxSections === 'number' ? options.maxSections : null
111
+ const declared = Array.isArray(declaredSections) ? declaredSections : []
112
+
113
+ const diagnostics = []
114
+ const push = (code, message, path) =>
115
+ diagnostics.push({ code, severity: 'error', message, ...(path ? { path } : {}) })
116
+
117
+ // Missing/empty composition: the registered slots in current render order and
118
+ // no editorial instance.
119
+ if (declared.length === 0) {
120
+ return { ok: true, sections: slots.map(slotEntry), diagnostics: [] }
121
+ }
122
+
123
+ const emitted = []
124
+ const seenIds = new Set()
125
+ const emittedSlots = new Set()
126
+ const legacy = []
127
+
128
+ declared.forEach((raw, index) => {
129
+ const path = `views["${viewName}"].sections[${index}]`
130
+ const entry = typeof raw === 'string' ? { name: raw } : raw
131
+ const kind = classifySectionEntry(entry)
132
+
133
+ if (kind === 'slot') {
134
+ if (entry.id !== `slot:${entry.type}`) {
135
+ push(
136
+ 'E_RESERVED_ID',
137
+ `${path} reserved slot id "${entry.id}" must equal "slot:${entry.type}".`,
138
+ path,
139
+ )
140
+ return
141
+ }
142
+ if (!slots.includes(entry.type)) {
143
+ push('E_UNKNOWN_HOST', `${path} names unregistered slot "${entry.type}".`, path)
144
+ return
145
+ }
146
+ if (emittedSlots.has(entry.type) || seenIds.has(entry.id)) {
147
+ push('E_DUPLICATE_ID', `${path} duplicate section id "${entry.id}".`, path)
148
+ return
149
+ }
150
+ emitted.push(copyEntry(entry))
151
+ emittedSlots.add(entry.type)
152
+ seenIds.add(entry.id)
153
+ return
154
+ }
155
+
156
+ if (kind === 'editorial') {
157
+ if (seenIds.has(entry.id)) {
158
+ push('E_DUPLICATE_ID', `${path} duplicate section id "${entry.id}".`, path)
159
+ return
160
+ }
161
+ if (allowedSections && !allowedSections.includes(entry.type)) {
162
+ push(
163
+ 'E_DISALLOWED_TYPE',
164
+ `${path} section type "${entry.type}" is not an allowed section for view "${viewName}".`,
165
+ path,
166
+ )
167
+ return
168
+ }
169
+ emitted.push(copyEntry(entry))
170
+ seenIds.add(entry.id)
171
+ return
172
+ }
173
+
174
+ if (kind === 'legacy') {
175
+ legacy.push({ entry, path })
176
+ return
177
+ }
178
+
179
+ push(
180
+ 'E_UNKNOWN_SECTION_SHAPE',
181
+ `${path} is neither a legacy { name } entry nor an { id, type } entry.`,
182
+ path,
183
+ )
184
+ })
185
+
186
+ // Every registered slot is mandatory: append any the declared list omitted, in
187
+ // registry order, so the normalized composition always contains each slot
188
+ // exactly once.
189
+ for (const slot of slots) {
190
+ if (!emittedSlots.has(slot)) {
191
+ const entry = slotEntry(slot)
192
+ emitted.push(entry)
193
+ emittedSlots.add(slot)
194
+ seenIds.add(entry.id)
195
+ }
196
+ }
197
+
198
+ // Persisted legacy migration: a concrete `<name>-<n>` id derived once from the
199
+ // 1-based position among the view's legacy entries, appended after the slots.
200
+ legacy.forEach(({ entry, path }, index) => {
201
+ const id = `${entry.name}-${index + 1}`
202
+ if (id.startsWith('slot:')) {
203
+ push(
204
+ 'E_RESERVED_ID',
205
+ `${path} legacy name "${entry.name}" would mint a reserved id "${id}".`,
206
+ path,
207
+ )
208
+ return
209
+ }
210
+ if (allowedSections && !allowedSections.includes(entry.name)) {
211
+ push(
212
+ 'E_DISALLOWED_TYPE',
213
+ `${path} legacy section "${entry.name}" is not an allowed section for view "${viewName}" (held or missing a verified bridge/provenance or data source); it cannot be adapted.`,
214
+ path,
215
+ )
216
+ return
217
+ }
218
+ if (seenIds.has(id)) {
219
+ push(
220
+ 'E_DUPLICATE_ID',
221
+ `${path} legacy section "${entry.name}" would mint duplicate id "${id}".`,
222
+ path,
223
+ )
224
+ return
225
+ }
226
+ emitted.push({
227
+ id,
228
+ type: entry.name,
229
+ ...(isNonEmptyString(entry.variant) ? { variant: entry.variant } : {}),
230
+ })
231
+ seenIds.add(id)
232
+ })
233
+
234
+ if (diagnostics.length > 0) {
235
+ return { ok: false, sections: null, diagnostics }
236
+ }
237
+
238
+ const editorialCount = emitted.filter((entry) => isEditorialSectionEntry(entry)).length
239
+ if (maxSections !== null && editorialCount > maxSections) {
240
+ return {
241
+ ok: false,
242
+ sections: null,
243
+ diagnostics: [
244
+ {
245
+ code: 'E_SECTION_LIMIT',
246
+ severity: 'error',
247
+ message: `View "${viewName}" normalized to ${editorialCount} editorial instances, exceeding maxSections ${maxSections}.`,
248
+ },
249
+ ],
250
+ }
251
+ }
252
+
253
+ return { ok: true, sections: emitted, diagnostics: [] }
254
+ }
255
+
256
+ // Package-side seam over the view registry: resolve a view's `registeredSlots`,
257
+ // `allowedSections`, and `maxSections` and normalize its declared sections. The
258
+ // registry is read from the package (`recipes/`) or the installed-skill assets,
259
+ // so the caller passes only the view name and its declared list.
260
+ function resolveRegistryPath() {
261
+ const candidates = [
262
+ join(here, '..', 'recipes', 'recipes.json'),
263
+ join(here, 'recipes.json'),
264
+ join(here, '..', 'assets', 'recipes', 'recipes.json'),
265
+ ]
266
+ return candidates.find((candidate) => existsSync(candidate)) ?? null
267
+ }
268
+
269
+ let cachedRecipes = null
270
+
271
+ function loadRecipes() {
272
+ if (!cachedRecipes) {
273
+ const path = resolveRegistryPath()
274
+ if (!path) throw new Error('section-compat: could not locate the view recipe registry.')
275
+ cachedRecipes = JSON.parse(readFileSync(path, 'utf8'))
276
+ }
277
+ return cachedRecipes
278
+ }
279
+
280
+
281
+ export function normalizeViewSectionsForView(viewName, declaredSections) {
282
+ const recipe = loadRecipes().views.find((view) => view.name === viewName) ?? null
283
+ if (!recipe) {
284
+ return {
285
+ ok: false,
286
+ sections: null,
287
+ diagnostics: [
288
+ {
289
+ code: 'E_UNKNOWN_VIEW',
290
+ severity: 'error',
291
+ message: `Unknown view "${viewName}".`,
292
+ },
293
+ ],
294
+ }
295
+ }
296
+ return normalizeViewSections(viewName, declaredSections, recipe.registeredSlots ?? [], {
297
+ allowedSections: recipe.allowedSections ?? [],
298
+ maxSections: recipe.maxSections,
299
+ })
300
+ }