@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.
- package/CHANGELOG.md +23 -0
- package/README.md +248 -99
- package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
- package/assets/templates/app/app/layout.tsx +18 -4
- package/assets/templates/app/app/robots.ts +13 -0
- package/assets/templates/app/app/sitemap.ts +12 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +232 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +96 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +142 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +17 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +153 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +36 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +378 -0
- package/assets/templates/views/account/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +28 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +230 -0
- package/assets/templates/views/category/props.ts +16 -0
- package/assets/templates/views/category/registry.tsx +103 -0
- package/assets/templates/views/category/schema.ts +78 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +24 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +15 -0
- package/assets/templates/views/home/index.tsx +38 -84
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +48 -0
- package/assets/templates/views/product/index.tsx +104 -0
- package/assets/templates/views/product/props.ts +20 -0
- package/assets/templates/views/product/registry.tsx +78 -0
- package/assets/templates/views/product/schema.ts +108 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
- package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
- package/assets/templates/views/user-panel/schema.ts +79 -0
- package/bin/storefront.js +1278 -79
- package/commands/sf-add-view.md +44 -13
- package/commands/sf-feature.md +28 -0
- package/commands/sf-theme.md +14 -8
- package/commands/sf-view.md +54 -12
- package/package.json +6 -4
- package/recipes/archetypes.json +28 -1
- package/recipes/catalog.json +361 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +139 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +60 -5
- package/recipes/sections.json +24 -5
- package/recipes/validate.mjs +784 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +140 -0
- package/scripts/file-classes.mjs +116 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +504 -0
- package/scripts/plan-apply.mjs +843 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +511 -0
- package/scripts/render-templates.mjs +245 -28
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/view-assembly.mjs +291 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +67 -0
- package/skills/sf-theme-gen/SKILL.md +12 -5
- package/skills/sf-view-gen/SKILL.md +123 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
- package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +139 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
- package/skills/sf-view-gen/assets/recipes/sections.json +24 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- package/skills/sf-view-gen/references/views.md +44 -10
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +784 -32
- package/skills/storefront-workflow/SKILL.md +116 -0
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- 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
|
+
}
|