riverbankcms 0.1.0 → 0.2.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.
@@ -0,0 +1,4940 @@
1
+ #!/usr/bin/env node
2
+ import { a as classifySdkEventPricing, i as classifySdkEventSchedule, o as classifySdkEventPaymentTerms, r as sdkEventUpsertOptionalFieldSchemas } from "./sdk-event-upsert-CTqsrhFT.mjs";
3
+ import { z } from "zod";
4
+
5
+ //#region ../blocks/src/lib/typeGuards.ts
6
+ /**
7
+ * Strict record guard: excludes null, primitives, and arrays.
8
+ */
9
+ function isRecord$3(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ /**
13
+ * Non-null object guard: includes arrays for behavior parity with legacy helpers.
14
+ *
15
+ * NOTE: This intentionally narrows to UnknownRecord for compatibility with existing
16
+ * callsites even though arrays pass at runtime. Follow-up cleanup should replace
17
+ * this API with a more precise predicate type and migrate consumers.
18
+ */
19
+ function isObjectRecord(value) {
20
+ return typeof value === "object" && value !== null;
21
+ }
22
+ /**
23
+ * Coerces unknown input into a strict record, returning an empty object fallback.
24
+ */
25
+ function asRecord(value) {
26
+ return isRecord$3(value) ? value : {};
27
+ }
28
+
29
+ //#endregion
30
+ //#region ../blocks/src/system/manifest/richTextSchema.ts
31
+ /**
32
+ * Shared Rich Text Schema
33
+ *
34
+ * This module provides a single source of truth for TipTap rich text validation
35
+ * across both the block-form editor and the block rendering system.
36
+ *
37
+ * Rich-text fields use one canonical root shape:
38
+ * `{ type: 'doc', content: [...] }`.
39
+ */
40
+ /**
41
+ * TipTap document node schema (recursive structure)
42
+ *
43
+ * Supports the full TipTap document structure including:
44
+ * - type: Node type (e.g., 'doc', 'paragraph', 'text')
45
+ * - content: Nested array of child nodes
46
+ * - text: Text content for text nodes
47
+ * - attrs: Node attributes (e.g., alignment, color)
48
+ * - marks: Text formatting marks (e.g., bold, italic)
49
+ */
50
+ const TipTapMarkSchema = z.object({
51
+ type: z.string().min(1),
52
+ attrs: z.record(z.string(), z.unknown()).optional()
53
+ }).strict();
54
+ const TipTapNodeSchema = z.lazy(() => z.object({
55
+ type: z.string().min(1),
56
+ content: z.array(TipTapNodeSchema).optional(),
57
+ text: z.string().optional(),
58
+ attrs: z.record(z.string(), z.unknown()).optional(),
59
+ marks: z.array(TipTapMarkSchema).optional()
60
+ }).strict().superRefine((node, ctx) => {
61
+ if (node.type === "doc") {
62
+ ctx.addIssue({
63
+ code: "custom",
64
+ path: ["type"],
65
+ message: "Document nodes are only valid at the rich-text root"
66
+ });
67
+ }
68
+ if (node.type === "text") {
69
+ if (node.text === undefined || node.text.length === 0) {
70
+ ctx.addIssue({
71
+ code: "custom",
72
+ path: ["text"],
73
+ message: "Text nodes require a non-empty text value"
74
+ });
75
+ }
76
+ if (node.content !== undefined) {
77
+ ctx.addIssue({
78
+ code: "custom",
79
+ path: ["content"],
80
+ message: "Text nodes cannot contain child nodes"
81
+ });
82
+ }
83
+ return;
84
+ }
85
+ if (node.text !== undefined) {
86
+ ctx.addIssue({
87
+ code: "custom",
88
+ path: ["text"],
89
+ message: "Only text nodes can contain a text value"
90
+ });
91
+ }
92
+ }));
93
+ /**
94
+ * Canonical schema for a TipTap rich-text document root.
95
+ *
96
+ * Stricter than {@link TipTapNodeSchema}: `type` is literally `'doc'` and
97
+ * `content` is required (possibly empty). This is the shape every producer
98
+ * (dummy content, editor output, migrations) must emit, and the shape every
99
+ * non-legacy consumer can rely on.
100
+ *
101
+ */
102
+ const RichTextDocSchema = z.object({
103
+ type: z.literal("doc"),
104
+ content: z.array(TipTapNodeSchema)
105
+ }).strict();
106
+ /**
107
+ * Type guard for {@link RichTextDoc}.
108
+ *
109
+ * Returns `true` only for the canonical unwrapped form. The shared structural
110
+ * contract lives in `@riverbankcms/core`; parity tests keep its dependency-free
111
+ * guard aligned with this detailed Zod validator.
112
+ */
113
+ function isRichTextDoc(value) {
114
+ return RichTextDocSchema.safeParse(value).success;
115
+ }
116
+ /**
117
+ * Build a canonical {@link RichTextDoc} from plain-text paragraphs.
118
+ *
119
+ * Single source of truth for constructing a TipTap doc from strings.
120
+ * Used by:
121
+ * - `@riverbankcms/dummy-content` (fixture body generators)
122
+ * - `@riverbankcms/content-model` (default template `cta-full` content)
123
+ *
124
+ * Empty strings produce a paragraph with no `content` array — matching
125
+ * the TipTap convention for empty paragraphs. The shape is exactly what
126
+ * {@link RichTextDocSchema} validates, so the type and runtime check
127
+ * never drift from the constructor.
128
+ */
129
+ function createRichTextDoc(paragraphs) {
130
+ return {
131
+ type: "doc",
132
+ content: paragraphs.map((text) => text ? {
133
+ type: "paragraph",
134
+ content: [{
135
+ type: "text",
136
+ text
137
+ }]
138
+ } : { type: "paragraph" })
139
+ };
140
+ }
141
+ function createHardBreakNode() {
142
+ return { type: "hardBreak" };
143
+ }
144
+ /**
145
+ * Parse a small markdown subset into TipTap inline nodes.
146
+ *
147
+ * This intentionally supports only bold (`**text**`) because the SDK/content
148
+ * import paths use it for lightweight default content, not full markdown.
149
+ */
150
+ function parseRichTextInlineMarkdown(text) {
151
+ const out = [];
152
+ const pushText = (value, marks) => {
153
+ if (!value) return;
154
+ out.push(marks && marks.length > 0 ? {
155
+ type: "text",
156
+ text: value,
157
+ marks
158
+ } : {
159
+ type: "text",
160
+ text: value
161
+ });
162
+ };
163
+ let rest = text;
164
+ while (rest.length > 0) {
165
+ const start = rest.indexOf("**");
166
+ if (start === -1) {
167
+ pushText(rest);
168
+ break;
169
+ }
170
+ const end = rest.indexOf("**", start + 2);
171
+ if (end === -1) {
172
+ pushText(rest);
173
+ break;
174
+ }
175
+ pushText(rest.slice(0, start));
176
+ pushText(rest.slice(start + 2, end), [{ type: "bold" }]);
177
+ rest = rest.slice(end + 2);
178
+ }
179
+ return out;
180
+ }
181
+ function parseRichTextParagraphInlineMarkdown(text) {
182
+ return text.split("\n").flatMap((line, index) => {
183
+ const lineNodes = parseRichTextInlineMarkdown(line.trim());
184
+ return index === 0 ? lineNodes : [createHardBreakNode(), ...lineNodes];
185
+ });
186
+ }
187
+ /**
188
+ * Convert a plain string with a tiny markdown subset to canonical TipTap JSON.
189
+ *
190
+ * Double newlines create semantic paragraphs. Single newlines remain inside the
191
+ * current paragraph as TipTap `hardBreak` nodes for line-oriented content such
192
+ * as addresses, opening hours, and contact details.
193
+ */
194
+ function createRichTextDocFromMarkdown(markdown) {
195
+ const normalized = markdown.replace(/\r\n/g, "\n").trim();
196
+ if (!normalized) return {
197
+ type: "doc",
198
+ content: []
199
+ };
200
+ const paragraphs = normalized.split(/\n{2,}/).map((paragraph) => paragraph.trim()).filter(Boolean);
201
+ return {
202
+ type: "doc",
203
+ content: paragraphs.map((paragraph) => {
204
+ const inline = parseRichTextParagraphInlineMarkdown(paragraph);
205
+ return inline.length > 0 ? {
206
+ type: "paragraph",
207
+ content: inline
208
+ } : { type: "paragraph" };
209
+ })
210
+ };
211
+ }
212
+ function buildRichTextSchema(options) {
213
+ let schema = RichTextDocSchema;
214
+ if (options?.required) {
215
+ const label = options.label ?? "This field";
216
+ schema = schema.refine((val) => {
217
+ const content = val.content;
218
+ if (!Array.isArray(content) || content.length === 0) return false;
219
+ const first = content[0];
220
+ if (content.length === 1 && isRecord$3(first) && first.type === "paragraph" && (!("content" in first) || first.content == null)) {
221
+ return false;
222
+ }
223
+ return true;
224
+ }, { message: `${label} is required` });
225
+ }
226
+ return schema;
227
+ }
228
+
229
+ //#endregion
230
+ //#region ../content-model/src/title-resolution.ts
231
+ /**
232
+ * Content title resolution utilities.
233
+ *
234
+ * Provides a single source of truth for resolving display titles from content data.
235
+ */
236
+ /**
237
+ * Default fields to check when titleField is not configured.
238
+ * Order matters - first match wins.
239
+ */
240
+ const TITLE_FIELD_CANDIDATES = ["title", "name"];
241
+ /**
242
+ * Resolve title from content data using configured titleField with fallbacks.
243
+ *
244
+ * Resolution order:
245
+ * 1. Explicit titleField (if configured)
246
+ * 2. 'title' field
247
+ * 3. 'name' field
248
+ * 4. Fallback title
249
+ *
250
+ * @param content - Content data object
251
+ * @param titleField - Configured title field ID (optional)
252
+ * @param fallbackTitle - Fallback if no title found
253
+ * @returns Resolved title string
254
+ */
255
+ function resolveContentTitle(content, titleField, fallbackTitle) {
256
+ if (!content) return fallbackTitle;
257
+ const candidates = [titleField?.trim(), ...TITLE_FIELD_CANDIDATES].filter((f) => Boolean(f?.trim()));
258
+ for (const field of candidates) {
259
+ const value = content[field];
260
+ if (typeof value === "string") {
261
+ const trimmed = value.trim();
262
+ if (trimmed) return trimmed;
263
+ }
264
+ }
265
+ return fallbackTitle;
266
+ }
267
+
268
+ //#endregion
269
+ //#region ../content-model/src/metadataConfig.ts
270
+ const ROUTE_METADATA_MANIFEST = {
271
+ name: "content-entry-metadata",
272
+ version: "1.0.0",
273
+ title: "Details",
274
+ description: "",
275
+ component: "ContentEntryMetadata",
276
+ fields: [
277
+ {
278
+ id: "draftTitle",
279
+ type: "text",
280
+ label: "Title",
281
+ required: true,
282
+ multiline: false
283
+ },
284
+ {
285
+ id: "draftSlug",
286
+ type: "slug",
287
+ label: "Slug",
288
+ description: "Used in the URL.",
289
+ required: true,
290
+ sourceFieldId: "draftTitle",
291
+ statusFieldId: "draftStatus",
292
+ maxLength: 96,
293
+ ui: { showSlugPreview: true }
294
+ },
295
+ {
296
+ id: "draftStatus",
297
+ type: "select",
298
+ label: "Draft status",
299
+ description: "Schedule to publish automatically, or keep as draft.",
300
+ required: true,
301
+ defaultValue: "draft",
302
+ options: [
303
+ {
304
+ value: "draft",
305
+ label: "Draft"
306
+ },
307
+ {
308
+ value: "scheduled",
309
+ label: "Scheduled"
310
+ },
311
+ {
312
+ value: "published",
313
+ label: "Published"
314
+ },
315
+ {
316
+ value: "archived",
317
+ label: "Archived"
318
+ }
319
+ ],
320
+ multiple: false
321
+ },
322
+ {
323
+ id: "scheduledPublishAt",
324
+ type: "datetime",
325
+ label: "Publish at",
326
+ description: "When scheduled, the entry will publish at this time.",
327
+ required: false,
328
+ ui: { visibleWhen: {
329
+ field: "draftStatus",
330
+ equals: "scheduled"
331
+ } }
332
+ },
333
+ {
334
+ id: "draftMetaTitle",
335
+ type: "text",
336
+ label: "Meta title",
337
+ description: "Used for SEO and social previews.",
338
+ multiline: true,
339
+ required: false,
340
+ ui: { showCharCount: { max: 60 } }
341
+ },
342
+ {
343
+ id: "draftMetaDescription",
344
+ type: "text",
345
+ label: "Meta description",
346
+ description: "Optional description for SEO and previews.",
347
+ required: false,
348
+ multiline: true,
349
+ ui: { showCharCount: { max: 160 } }
350
+ },
351
+ {
352
+ id: "ogImageId",
353
+ type: "text",
354
+ label: "Social image asset ID",
355
+ description: "Optional media asset ID used for social sharing.",
356
+ required: false,
357
+ multiline: false
358
+ }
359
+ ],
360
+ slots: []
361
+ };
362
+ const METADATA_WORKFLOW_FIELDS = {
363
+ title: "draftTitle",
364
+ slug: "draftSlug",
365
+ status: "draftStatus",
366
+ publishAt: "scheduledPublishAt"
367
+ };
368
+ const HOMEPAGE_PAGE_ROUTE_SLUG = { kind: "homepage" };
369
+ function asNonEmptyPageRouteSlug(value) {
370
+ return value;
371
+ }
372
+ function pageRouteSlugFromEditorValue(value) {
373
+ const slug = sanitizeSlug(value ?? "");
374
+ return slug.length > 0 ? {
375
+ kind: "slug",
376
+ value: asNonEmptyPageRouteSlug(slug)
377
+ } : HOMEPAGE_PAGE_ROUTE_SLUG;
378
+ }
379
+ function pageRouteSlugToEditorValue(slug) {
380
+ return slug.kind === "homepage" ? "" : slug.value;
381
+ }
382
+ function pageRouteSlugToStoredValue(slug) {
383
+ return slug.kind === "homepage" ? null : slug.value;
384
+ }
385
+ function isHomepagePageRouteSlug(slug) {
386
+ return slug.kind === "homepage";
387
+ }
388
+ function resolvePageRouteSlug(entry, currentSlug) {
389
+ if (entry.kind !== "routable") return null;
390
+ if (entry.draft.path === "/" || entry.published.path === "/") {
391
+ return HOMEPAGE_PAGE_ROUTE_SLUG;
392
+ }
393
+ if (currentSlug !== undefined) {
394
+ return pageRouteSlugFromEditorValue(currentSlug);
395
+ }
396
+ return pageRouteSlugFromEditorValue(entry.draft.slug);
397
+ }
398
+ const ROUTE_METADATA_MANAGED_FIELDS_BY_TYPE = {
399
+ event: [
400
+ "draftTitle",
401
+ "draftSlug",
402
+ "draftStatus",
403
+ "scheduledPublishAt"
404
+ ],
405
+ course: [
406
+ "draftTitle",
407
+ "draftSlug",
408
+ "draftStatus",
409
+ "scheduledPublishAt"
410
+ ],
411
+ product: [
412
+ "draftTitle",
413
+ "draftSlug",
414
+ "draftStatus",
415
+ "scheduledPublishAt"
416
+ ],
417
+ "product-category": [
418
+ "draftTitle",
419
+ "draftSlug",
420
+ "draftStatus",
421
+ "scheduledPublishAt"
422
+ ],
423
+ "team-member": [
424
+ "draftTitle",
425
+ "draftSlug",
426
+ "draftStatus",
427
+ "scheduledPublishAt"
428
+ ]
429
+ };
430
+ const EMPTY_METADATA_FIELDS = [];
431
+ function normalizeContentTypeKey(typeKey) {
432
+ if (typeof typeKey !== "string") return null;
433
+ const trimmed = typeKey.trim().toLowerCase();
434
+ return trimmed.length > 0 ? trimmed : null;
435
+ }
436
+ function getManagedRouteMetadataFieldIdsForType(typeKey) {
437
+ const normalizedTypeKey = normalizeContentTypeKey(typeKey);
438
+ if (!normalizedTypeKey) return EMPTY_METADATA_FIELDS;
439
+ return ROUTE_METADATA_MANAGED_FIELDS_BY_TYPE[normalizedTypeKey] ?? EMPTY_METADATA_FIELDS;
440
+ }
441
+ function isRouteMetadataFieldEditableForType(typeKey, fieldId) {
442
+ const managedFields = getManagedRouteMetadataFieldIdsForType(typeKey);
443
+ return !managedFields.includes(fieldId);
444
+ }
445
+ function getEditableRouteMetadataFieldIdsForType(typeKey) {
446
+ return ROUTE_METADATA_MANIFEST.fields.map((field) => String(field.id)).filter((fieldId) => isRouteMetadataFieldEditableForType(typeKey, fieldId));
447
+ }
448
+ function getRouteMetadataManifestForType(typeKey) {
449
+ return {
450
+ ...ROUTE_METADATA_MANIFEST,
451
+ fields: ROUTE_METADATA_MANIFEST.fields.filter((field) => isRouteMetadataFieldEditableForType(typeKey, String(field.id)))
452
+ };
453
+ }
454
+ function getRouteMetadataManifestForContext(context) {
455
+ if (context.kind === "content") {
456
+ return getRouteMetadataManifestForType(context.typeKey);
457
+ }
458
+ const fields = isHomepagePageRouteSlug(context.routeSlug) ? ROUTE_METADATA_MANIFEST.fields.filter((field) => String(field.id) !== "draftSlug") : ROUTE_METADATA_MANIFEST.fields;
459
+ return {
460
+ ...ROUTE_METADATA_MANIFEST,
461
+ fields
462
+ };
463
+ }
464
+ function deriveRouteMetadataPresentation(context, options = {}) {
465
+ const contextManifest = getRouteMetadataManifestForContext(context);
466
+ const manifest = options.omitTitle ? omitRouteTitleFieldFromManifest(contextManifest) : contextManifest;
467
+ if (context.kind === "content") {
468
+ return {
469
+ kind: "content",
470
+ manifest,
471
+ typeKey: normalizeContentTypeKey(context.typeKey)
472
+ };
473
+ }
474
+ if (context.routeSlug.kind === "homepage") {
475
+ return {
476
+ kind: "page-homepage",
477
+ manifest,
478
+ routeSlug: context.routeSlug
479
+ };
480
+ }
481
+ return {
482
+ kind: "page-routable",
483
+ manifest,
484
+ routeSlug: context.routeSlug
485
+ };
486
+ }
487
+ /**
488
+ * Remove the legacy route title editor from a route metadata manifest.
489
+ *
490
+ * Entry titles are now derived from entry data (single source of truth) and the route title is a denormalized copy.
491
+ * We still keep `draftTitle` in the underlying manifest for page creation flows, but we omit it from entry editors.
492
+ */
493
+ function omitRouteTitleFieldFromManifest(manifest) {
494
+ const fields = (manifest.fields ?? []).filter((field) => String(field.id) !== "draftTitle").map((field) => {
495
+ if (field && typeof field === "object" && "sourceFieldId" in field) {
496
+ const sourceFieldId = field.sourceFieldId;
497
+ if (String(sourceFieldId) === "draftTitle") {
498
+ return {
499
+ ...field,
500
+ sourceFieldId: undefined
501
+ };
502
+ }
503
+ }
504
+ return field;
505
+ });
506
+ return {
507
+ ...manifest,
508
+ fields
509
+ };
510
+ }
511
+ function sanitizeSlug(value) {
512
+ if (!value) return "";
513
+ return value.split("/").map((segment) => normalizeSlugSegment(segment)).filter(Boolean).join("/");
514
+ }
515
+ function normalizeSlugSegment(segment) {
516
+ const normalized = segment.trim().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
517
+ return normalized;
518
+ }
519
+
520
+ //#endregion
521
+ //#region ../content-model/src/referenceFields.ts
522
+ const BUILT_IN_REFERENCE_KINDS = [
523
+ "post",
524
+ "form",
525
+ "bookingForm",
526
+ "appointmentService",
527
+ "appointmentResource",
528
+ "page",
529
+ "newsletterList",
530
+ "mailchimpAudience"
531
+ ];
532
+ const builtInReferenceKindSet = new Set(BUILT_IN_REFERENCE_KINDS);
533
+ function isBuiltInReferenceKind(referenceKind) {
534
+ return builtInReferenceKindSet.has(referenceKind);
535
+ }
536
+ function isDynamicContentReferenceKind(referenceKind) {
537
+ return parseReferenceKind(referenceKind).kind === "dynamicContent";
538
+ }
539
+ function isAdapterReferenceKind(referenceKind) {
540
+ const parsed = parseReferenceKind(referenceKind);
541
+ return parsed.kind === "builtIn" || parsed.kind === "dynamicContent";
542
+ }
543
+ function parseReferenceContentTypeKey(contentTypeKey) {
544
+ const trimmed = contentTypeKey.trim();
545
+ return trimmed.length > 0 ? trimmed : null;
546
+ }
547
+ function parseReferenceKind(referenceKind) {
548
+ if (isBuiltInReferenceKind(referenceKind)) {
549
+ return {
550
+ kind: "builtIn",
551
+ referenceKind
552
+ };
553
+ }
554
+ if (referenceKind.startsWith("content:")) {
555
+ const contentTypeKey = parseReferenceContentTypeKey(referenceKind.slice("content:".length));
556
+ if (contentTypeKey) {
557
+ return {
558
+ kind: "dynamicContent",
559
+ referenceKind: `content:${contentTypeKey}`,
560
+ contentTypeKey
561
+ };
562
+ }
563
+ }
564
+ return {
565
+ kind: "unsupported",
566
+ referenceKind
567
+ };
568
+ }
569
+
570
+ //#endregion
571
+ //#region ../content-model/src/contentManifest.ts
572
+ const CONTENT_MODEL_ICON_PICKER_PRESETS = ["cta", "feature"];
573
+ function assertNever$1(value) {
574
+ throw new Error(`Unhandled content field mapping decision: ${JSON.stringify(value)}`);
575
+ }
576
+ function resolveContentReferenceKind(field) {
577
+ return field.referenceKind ?? field.reference?.type ?? null;
578
+ }
579
+ function resolveReferenceFieldMapping(referenceKind, context = {}, valueFrom = "identifier") {
580
+ if (referenceKind === null) {
581
+ return { kind: "missingReferenceKind" };
582
+ }
583
+ if (isAdapterReferenceKind(referenceKind)) {
584
+ return {
585
+ kind: "adapterReference",
586
+ referenceKind
587
+ };
588
+ }
589
+ if (context.referenceContentTypeKeys && !context.referenceContentTypeKeys.includes(referenceKind)) {
590
+ return {
591
+ kind: "unresolvedReference",
592
+ referenceKind
593
+ };
594
+ }
595
+ return {
596
+ kind: "contentEntryReference",
597
+ contentType: referenceKind,
598
+ valueFrom
599
+ };
600
+ }
601
+ const DEFAULT_BUTTON_VARIANTS = [
602
+ {
603
+ value: "primary",
604
+ label: "Primary"
605
+ },
606
+ {
607
+ value: "secondary",
608
+ label: "Secondary"
609
+ },
610
+ {
611
+ value: "outline",
612
+ label: "Outline"
613
+ }
614
+ ];
615
+ const DISPLAY_MEDIA_KINDS$1 = [
616
+ "image",
617
+ "video",
618
+ "audio"
619
+ ];
620
+ function toFieldDefinition(field, context = {}) {
621
+ const label = field.label ?? startCase(field.id);
622
+ const base = {
623
+ id: field.id,
624
+ label,
625
+ description: field.description ?? undefined,
626
+ required: field.required ?? false,
627
+ visibleRoles: undefined,
628
+ defaultValue: field.defaultValue,
629
+ ui: normalizeFieldUi(field.ui)
630
+ };
631
+ switch (field.type) {
632
+ case "text": return {
633
+ ...base,
634
+ type: "text",
635
+ multiline: Boolean(field.multiline),
636
+ maxLength: field.maxLength
637
+ };
638
+ case "textarea": return {
639
+ ...base,
640
+ type: "text",
641
+ multiline: true,
642
+ maxLength: field.maxLength
643
+ };
644
+ case "number": return {
645
+ ...base,
646
+ type: "number",
647
+ min: field.min,
648
+ max: field.max,
649
+ step: field.step
650
+ };
651
+ case "richText": return {
652
+ ...base,
653
+ type: "richText"
654
+ };
655
+ case "media": return {
656
+ ...base,
657
+ type: "media",
658
+ mediaKinds: normalizeDisplayMediaKinds(field.mediaKinds)
659
+ };
660
+ case "file": return {
661
+ ...base,
662
+ type: "file",
663
+ fileKinds: field.fileKinds && field.fileKinds.length ? field.fileKinds : [
664
+ "document",
665
+ "spreadsheet",
666
+ "archive",
667
+ "audio"
668
+ ]
669
+ };
670
+ case "boolean": return {
671
+ ...base,
672
+ type: "boolean"
673
+ };
674
+ case "date": return {
675
+ ...base,
676
+ type: "date"
677
+ };
678
+ case "datetime": return {
679
+ ...base,
680
+ type: "datetime"
681
+ };
682
+ case "url": return {
683
+ ...base,
684
+ type: "url",
685
+ allowRelative: field.allowRelative ?? false
686
+ };
687
+ case "select": {
688
+ const options = field.options ?? [];
689
+ if (!options.length) return null;
690
+ return {
691
+ ...base,
692
+ type: "select",
693
+ options: options.map((option) => ({
694
+ value: option.value,
695
+ label: option.label ?? option.value
696
+ })),
697
+ multiple: Boolean(field.multiple)
698
+ };
699
+ }
700
+ case "reference": {
701
+ const referenceKind = resolveContentReferenceKind(field);
702
+ const mapping = resolveReferenceFieldMapping(referenceKind, context, field.valueFrom ?? "identifier");
703
+ switch (mapping.kind) {
704
+ case "adapterReference":
705
+ case "unresolvedReference": return {
706
+ ...base,
707
+ type: "reference",
708
+ referenceKind: mapping.referenceKind,
709
+ allowManualEntry: false
710
+ };
711
+ case "missingReferenceKind": return null;
712
+ case "contentEntryReference": return {
713
+ ...base,
714
+ type: "entryPicker",
715
+ fixedContentType: mapping.contentType,
716
+ valueFrom: mapping.valueFrom
717
+ };
718
+ default: return assertNever$1(mapping);
719
+ }
720
+ }
721
+ case "link": return {
722
+ ...base,
723
+ type: "link"
724
+ };
725
+ case "button": {
726
+ const buttonContext = { variants: context.buttonVariants };
727
+ return expandButtonFieldGroup(field.id, label, buttonContext);
728
+ }
729
+ case "group": {
730
+ const nestedFields$1 = (field.fields ?? field.schema?.fields ?? []).map((f) => toFieldDefinition(f, context)).filter((f) => f !== null);
731
+ if (!nestedFields$1.length) return null;
732
+ return {
733
+ id: field.id,
734
+ label,
735
+ description: field.description ?? undefined,
736
+ required: field.required ?? false,
737
+ type: "group",
738
+ ui: normalizeFieldUi(field.ui),
739
+ schema: { fields: nestedFields$1 }
740
+ };
741
+ }
742
+ case "repeater": {
743
+ const nestedFields$1 = (field.fields ?? field.schema?.fields ?? []).map((f) => toFieldDefinition(f, context)).filter((f) => f !== null);
744
+ if (!nestedFields$1.length) return null;
745
+ return {
746
+ id: field.id,
747
+ label,
748
+ description: field.description ?? undefined,
749
+ required: field.required ?? false,
750
+ type: "repeater",
751
+ ui: normalizeFieldUi(field.ui),
752
+ itemLabel: field.itemLabel ?? "Item",
753
+ schema: { fields: nestedFields$1 }
754
+ };
755
+ }
756
+ default: return null;
757
+ }
758
+ }
759
+ function normalizeDisplayMediaKinds(mediaKinds) {
760
+ const displayKinds = (mediaKinds ?? []).filter(isDisplayMediaKind$1);
761
+ return displayKinds.length > 0 ? displayKinds : ["image"];
762
+ }
763
+ function isDisplayMediaKind$1(value) {
764
+ return DISPLAY_MEDIA_KINDS$1.includes(value);
765
+ }
766
+ function startCase(value) {
767
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]/g, " ").replace(/\s+/g, " ").replace(/^./, (match) => match.toUpperCase());
768
+ }
769
+ /**
770
+ * Maps a content type schema to form field definitions.
771
+ *
772
+ * @param schema - The content type schema
773
+ * @param context - Optional context with theme variants for button fields
774
+ * @returns Array of field definitions for form rendering
775
+ */
776
+ function mapContentSchemaToFormFields(schema, context = {}) {
777
+ const fields = [];
778
+ for (const field of schema.fields ?? []) {
779
+ const definition = toFieldDefinition(field, context);
780
+ if (definition) {
781
+ fields.push(definition);
782
+ }
783
+ }
784
+ return fields;
785
+ }
786
+ function expandButtonFieldGroup(fieldId, fieldLabel, context = {}) {
787
+ const variants = context.variants ?? [...DEFAULT_BUTTON_VARIANTS];
788
+ const mainFields = [
789
+ {
790
+ id: "label",
791
+ type: "text",
792
+ label: "Label",
793
+ required: true,
794
+ multiline: false,
795
+ ui: { row: "primary" }
796
+ },
797
+ {
798
+ id: "variant",
799
+ type: "select",
800
+ label: "Style",
801
+ options: variants.map((variant) => ({ ...variant })),
802
+ required: false,
803
+ multiple: false,
804
+ ui: { row: "primary" }
805
+ },
806
+ {
807
+ id: "link",
808
+ type: "link",
809
+ label: "Link",
810
+ required: true,
811
+ ui: { colSpan: 2 }
812
+ }
813
+ ];
814
+ return {
815
+ id: fieldId,
816
+ type: "group",
817
+ label: fieldLabel,
818
+ required: false,
819
+ ui: {
820
+ layout: "grid",
821
+ columns: 2
822
+ },
823
+ schema: { fields: [...mainFields, {
824
+ id: "icons",
825
+ type: "group",
826
+ label: "Icons",
827
+ required: false,
828
+ ui: {
829
+ preset: "disclosure",
830
+ colSpan: 2
831
+ },
832
+ schema: { fields: [{
833
+ id: "iconLeft",
834
+ type: "text",
835
+ label: "Left icon",
836
+ required: false,
837
+ multiline: false,
838
+ ui: {
839
+ widget: "iconPicker",
840
+ iconPickerPreset: "cta"
841
+ }
842
+ }, {
843
+ id: "iconRight",
844
+ type: "text",
845
+ label: "Right icon",
846
+ required: false,
847
+ multiline: false,
848
+ ui: {
849
+ widget: "iconPicker",
850
+ iconPickerPreset: "cta"
851
+ }
852
+ }] }
853
+ }] }
854
+ };
855
+ }
856
+ function normalizeFieldUi(value) {
857
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
858
+ return undefined;
859
+ }
860
+ return value;
861
+ }
862
+
863
+ //#endregion
864
+ //#region ../content-model/src/routeTemplate.ts
865
+ const ROUTE_TEMPLATE_SLUG_SEGMENT = "{slug}";
866
+ var InvalidRouteTemplateError = class extends Error {
867
+ constructor(error) {
868
+ super(describeRouteTemplateParseError(error));
869
+ this.name = "InvalidRouteTemplateError";
870
+ this.error = error;
871
+ }
872
+ };
873
+ function safeParseRouteTemplate(routePattern) {
874
+ if (routePattern === null || routePattern === "") {
875
+ return {
876
+ success: true,
877
+ routeTemplate: { kind: "fallback-slug" }
878
+ };
879
+ }
880
+ const segments = splitRouteSegments(routePattern);
881
+ const slugSegmentIndices = segments.flatMap((segment, index) => segment === ROUTE_TEMPLATE_SLUG_SEGMENT ? [index] : []);
882
+ if (slugSegmentIndices.length > 1) {
883
+ return {
884
+ success: false,
885
+ error: {
886
+ code: "multiple-slug-segments",
887
+ routePattern,
888
+ segment: ROUTE_TEMPLATE_SLUG_SEGMENT
889
+ }
890
+ };
891
+ }
892
+ const invalidSegment = segments.find((segment) => segment !== ROUTE_TEMPLATE_SLUG_SEGMENT && isUnsupportedDynamicSegment(segment));
893
+ if (invalidSegment) {
894
+ return {
895
+ success: false,
896
+ error: {
897
+ code: invalidSegment.startsWith(":") ? "legacy-dynamic-segment" : "unsupported-dynamic-segment",
898
+ routePattern,
899
+ segment: invalidSegment
900
+ }
901
+ };
902
+ }
903
+ if (slugSegmentIndices.length === 1) {
904
+ const [prefix = "", suffix = ""] = routePattern.split(ROUTE_TEMPLATE_SLUG_SEGMENT);
905
+ const slugIndex = slugSegmentIndices[0] ?? 0;
906
+ return {
907
+ success: true,
908
+ routeTemplate: {
909
+ kind: "slug-template",
910
+ routePattern,
911
+ prefix,
912
+ suffix,
913
+ prefixSegments: segments.slice(0, slugIndex),
914
+ suffixSegments: segments.slice(slugIndex + 1)
915
+ }
916
+ };
917
+ }
918
+ return {
919
+ success: true,
920
+ routeTemplate: {
921
+ kind: "fixed-path",
922
+ routePattern,
923
+ path: routePattern
924
+ }
925
+ };
926
+ }
927
+ function parseRouteTemplate(routePattern) {
928
+ const result = safeParseRouteTemplate(routePattern);
929
+ if (result.success) {
930
+ return result.routeTemplate;
931
+ }
932
+ throw new InvalidRouteTemplateError(result.error);
933
+ }
934
+ function isSlugTemplate(routeTemplate) {
935
+ return routeTemplate.kind === "slug-template";
936
+ }
937
+ function fixedPathOrNull(routeTemplate) {
938
+ return routeTemplate.kind === "fixed-path" ? routeTemplate.path : null;
939
+ }
940
+ /** Returns the first path segment, or undefined when the template has no fixed prefix segment. */
941
+ function getRouteTemplatePrefix(routeTemplate) {
942
+ switch (routeTemplate.kind) {
943
+ case "fallback-slug": return undefined;
944
+ case "fixed-path": {
945
+ const fixedSegments = routeTemplate.path.split("/").filter((segment) => segment.length > 0);
946
+ return fixedSegments[0];
947
+ }
948
+ case "slug-template": return routeTemplate.prefixSegments[0];
949
+ }
950
+ }
951
+ function expandSlug(routeTemplate, slug) {
952
+ switch (routeTemplate.kind) {
953
+ case "fallback-slug": return `/${slug}`;
954
+ case "fixed-path": return routeTemplate.path;
955
+ case "slug-template": return `${routeTemplate.prefix}${slug}${routeTemplate.suffix}`;
956
+ }
957
+ }
958
+ function buildContentEntryRoutePath(input) {
959
+ return expandSlug(parseRouteTemplate(input.routePattern), input.slug);
960
+ }
961
+ function describeRouteTemplateParseError(error) {
962
+ switch (error.code) {
963
+ case "legacy-dynamic-segment": return `Route pattern "${error.routePattern}" uses unsupported dynamic segment "${error.segment ?? ":slug"}". Use "${ROUTE_TEMPLATE_SLUG_SEGMENT}" as a full path segment.`;
964
+ case "multiple-slug-segments": return `Route pattern "${error.routePattern}" must not contain "${ROUTE_TEMPLATE_SLUG_SEGMENT}" more than once.`;
965
+ case "unsupported-dynamic-segment": return `Route pattern "${error.routePattern}" contains unsupported dynamic segment "${error.segment ?? ""}". Route patterns may only use "${ROUTE_TEMPLATE_SLUG_SEGMENT}" as a full path segment.`;
966
+ }
967
+ }
968
+ function splitRouteSegments(routePattern) {
969
+ return routePattern.split("/").filter((segment) => segment.length > 0);
970
+ }
971
+ function isUnsupportedDynamicSegment(segment) {
972
+ return segment.startsWith(":") || segment.includes("{") || segment.includes("}");
973
+ }
974
+
975
+ //#endregion
976
+ //#region ../content-model/src/definitions/canonicalizePageRouteMetadata.ts
977
+ function normalizePathInput(value) {
978
+ if (typeof value !== "string") return null;
979
+ const trimmed = value.trim();
980
+ return trimmed.length > 0 ? trimmed : null;
981
+ }
982
+ function canonicalizePageRoutePair(input, routing, options) {
983
+ const normalizedPath = normalizePathInput(input.path);
984
+ if (normalizedPath !== null) {
985
+ const slug$1 = routing.toSlug(normalizedPath);
986
+ return {
987
+ path: routing.buildRoutePath(slug$1),
988
+ slug: slug$1
989
+ };
990
+ }
991
+ const trimmedInputSlug = typeof input.slug === "string" ? input.slug.trim() : "";
992
+ const trimmedFallbackSlug = typeof options.fallbackSlug === "string" ? options.fallbackSlug.trim() : "";
993
+ const rawSlug = trimmedInputSlug.length > 0 ? trimmedInputSlug : trimmedFallbackSlug;
994
+ const slug = routing.sanitize(rawSlug);
995
+ if (slug.length > 0) {
996
+ return {
997
+ path: routing.buildRoutePath(slug),
998
+ slug
999
+ };
1000
+ }
1001
+ if (options.allowMissing) {
1002
+ return {
1003
+ path: null,
1004
+ slug: null
1005
+ };
1006
+ }
1007
+ return {
1008
+ path: routing.buildRoutePath(""),
1009
+ slug: ""
1010
+ };
1011
+ }
1012
+ /**
1013
+ * Normalize routable page metadata into one editor/store-facing representation.
1014
+ *
1015
+ * Contract:
1016
+ * - Prefer explicit paths when present, deriving slugs from those paths so `/`, `''`, and persisted `NULL` homepage
1017
+ * variants collapse to the same draft/published shape.
1018
+ * - Preserve a missing published route as `{ path: null, slug: null }` so callers can distinguish "never published"
1019
+ * from "published homepage".
1020
+ * - Always return a concrete draft route for editor state, using `draftFallbackSlug` when both draft path and slug
1021
+ * are absent.
1022
+ *
1023
+ * This helper intentionally returns homepage slugs as empty strings for editor/store use. DB-facing callers that store
1024
+ * homepage slugs as `NULL` should map the output through `toStoredPageRouteSlug()`.
1025
+ */
1026
+ function canonicalizePageRouteMetadata(input, routing, options = {}) {
1027
+ const published = canonicalizePageRoutePair({
1028
+ path: input.publishedPath,
1029
+ slug: input.publishedSlug
1030
+ }, routing, { allowMissing: true });
1031
+ const draft = canonicalizePageRoutePair({
1032
+ path: input.draftPath,
1033
+ slug: input.draftSlug ?? published.slug
1034
+ }, routing, {
1035
+ allowMissing: false,
1036
+ fallbackSlug: options.draftFallbackSlug
1037
+ });
1038
+ return {
1039
+ draft: {
1040
+ path: draft.path ?? routing.buildRoutePath(""),
1041
+ slug: draft.slug ?? ""
1042
+ },
1043
+ published
1044
+ };
1045
+ }
1046
+ /**
1047
+ * Convert editor/store-facing page slugs back into the DB storage convention.
1048
+ *
1049
+ * Homepage slugs are stored as `NULL` in `site_routes`, while editor state uses empty strings to represent the
1050
+ * homepage. This helper trims incidental whitespace and returns `null` for empty homepage-equivalent values.
1051
+ */
1052
+ function toStoredPageRouteSlug(slug) {
1053
+ return pageRouteSlugToStoredValue(pageRouteSlugFromEditorValue(slug));
1054
+ }
1055
+
1056
+ //#endregion
1057
+ //#region ../content-model/src/definitions/shared.ts
1058
+ function createRoutingHelpers(schema, options = {}) {
1059
+ const rawPrefix = schema.routing?.prefix ?? "/";
1060
+ const slugPrefix = ensureSlugPrefix(rawPrefix);
1061
+ const normalizedPrefix = normalizeRoutePrefix(slugPrefix);
1062
+ const routePattern = schema.routing?.pattern ?? (schema.routing?.enabled ? "/{slug}" : undefined) ?? null;
1063
+ const routeTemplate = parseRouteTemplate(routePattern);
1064
+ const defaultSlug = options.slug ?? null;
1065
+ const sanitize = (value) => sanitizeSlug(value ?? "");
1066
+ const toSlug = (input) => {
1067
+ if (!input) return "";
1068
+ let normalized = input.trim();
1069
+ if (!normalized) return "";
1070
+ const sanitized = sanitize(normalized);
1071
+ let result = sanitized;
1072
+ if (result.startsWith(slugPrefix)) {
1073
+ result = result.slice(slugPrefix.length);
1074
+ }
1075
+ const prefixed = normalizedPrefix !== "/" ? `${normalizedPrefix}/` : "/";
1076
+ if (prefixed !== "/" && result.startsWith(prefixed)) {
1077
+ result = result.slice(prefixed.length);
1078
+ }
1079
+ if (normalizedPrefix !== "/" && !result.startsWith("/")) {
1080
+ const prefixWithoutLeadingSlash = normalizedPrefix.slice(1) + "/";
1081
+ if (result.startsWith(prefixWithoutLeadingSlash)) {
1082
+ result = result.slice(prefixWithoutLeadingSlash.length);
1083
+ }
1084
+ }
1085
+ if (prefixed === "/" && result.startsWith("/")) {
1086
+ result = result.slice(1);
1087
+ }
1088
+ return result;
1089
+ };
1090
+ const toPath = (slug) => {
1091
+ const cleaned = sanitize(slug);
1092
+ if (!cleaned) {
1093
+ return normalizedPrefix === "/" ? "/" : normalizedPrefix;
1094
+ }
1095
+ if (normalizedPrefix === "/") {
1096
+ return `/${cleaned}`;
1097
+ }
1098
+ return `${normalizedPrefix}/${cleaned}`;
1099
+ };
1100
+ const buildRoutePath = (slug) => {
1101
+ const cleaned = sanitize(slug);
1102
+ const effectiveSlug = cleaned || defaultSlug || "";
1103
+ if (routePattern) {
1104
+ return expandSlug(routeTemplate, effectiveSlug);
1105
+ }
1106
+ return toPath(effectiveSlug);
1107
+ };
1108
+ return {
1109
+ slugPrefix,
1110
+ normalizedPrefix,
1111
+ routePattern,
1112
+ defaultSlug,
1113
+ toSlug,
1114
+ toPath,
1115
+ buildRoutePath,
1116
+ sanitize
1117
+ };
1118
+ }
1119
+ function ensureSlugPrefix(prefix) {
1120
+ const base = prefix?.trim();
1121
+ if (!base) {
1122
+ return "/";
1123
+ }
1124
+ const withLeading = base.startsWith("/") ? base : `/${base}`;
1125
+ return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
1126
+ }
1127
+ function normalizeRoutePrefix(prefix) {
1128
+ if (prefix === "/") return "/";
1129
+ return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1130
+ }
1131
+ /**
1132
+ * Build snapshot for routable content (content with an associated route)
1133
+ */
1134
+ function buildRoutableSnapshot(entry, route, routing, defaults) {
1135
+ const fallbackSlug = defaults.slug ?? routing.defaultSlug ?? "";
1136
+ const rawDraftTitle = route.draftTitle ?? route.publishedTitle ?? null;
1137
+ if (!rawDraftTitle || rawDraftTitle.trim().length === 0) {
1138
+ throw new Error(`Missing draft title in route for entry ${entry.id}. Route data: ${JSON.stringify(route)}`);
1139
+ }
1140
+ const draftTitle = rawDraftTitle.trim();
1141
+ const rawDraftSlug = route.draftSlug ?? fallbackSlug;
1142
+ const draftSlug = routing.sanitize(rawDraftSlug);
1143
+ const draftPath = route.draftPath ?? routing.toPath(draftSlug);
1144
+ const draftStatus = route.draftStatus ?? "draft";
1145
+ const publishAt = route.scheduledPublishAt ?? null;
1146
+ const rawPublishedTitle = route.publishedTitle ?? route.draftTitle;
1147
+ const publishedTitle = rawPublishedTitle ? rawPublishedTitle.trim() : null;
1148
+ const publishedPath = route.publishedPath ?? null;
1149
+ const publishedStatus = route.status ?? null;
1150
+ const publishedAt = route.publishedAt ?? null;
1151
+ const currentStatus = route.status ?? route.draftStatus ?? "draft";
1152
+ return {
1153
+ kind: "routable",
1154
+ id: entry.id,
1155
+ draft: {
1156
+ title: draftTitle,
1157
+ slug: draftSlug,
1158
+ status: draftStatus,
1159
+ publishAt,
1160
+ path: draftPath
1161
+ },
1162
+ published: {
1163
+ title: publishedTitle,
1164
+ path: publishedPath,
1165
+ status: publishedStatus ?? null,
1166
+ publishAt: publishedAt ?? null
1167
+ },
1168
+ status: currentStatus ?? "draft",
1169
+ route: {
1170
+ draftMetaTitle: route.draftMetaTitle ?? null,
1171
+ draftMetaDescription: route.draftMetaDescription ?? null,
1172
+ publishedMetaTitle: route.publishedMetaTitle ?? null,
1173
+ publishedMetaDescription: route.publishedMetaDescription ?? null,
1174
+ ogImageId: route.ogImageId ?? null
1175
+ }
1176
+ };
1177
+ }
1178
+ /**
1179
+ * Build snapshot for non-routable content (content without a route, like testimonials)
1180
+ */
1181
+ function buildNonRoutableSnapshot(entry, title) {
1182
+ const safeTitle = title.trim() || "Untitled Entry";
1183
+ const hasBeenPublished = !!entry.publishedVersionId;
1184
+ const hasDraftVersionChanges = !!entry.publishedVersionId && !!entry.draftVersionId;
1185
+ const lifecycleStatus = hasBeenPublished ? "published" : "draft";
1186
+ return {
1187
+ kind: "non-routable",
1188
+ id: entry.id,
1189
+ draft: {
1190
+ title: safeTitle,
1191
+ status: lifecycleStatus,
1192
+ publishAt: null
1193
+ },
1194
+ published: {
1195
+ title: hasBeenPublished ? safeTitle : null,
1196
+ status: hasBeenPublished ? lifecycleStatus : null,
1197
+ publishAt: null
1198
+ },
1199
+ status: lifecycleStatus,
1200
+ hasDraftVersionChanges
1201
+ };
1202
+ }
1203
+ function buildSnapshot(entry, route, routing, defaults) {
1204
+ if (route !== null && route !== undefined) {
1205
+ return buildRoutableSnapshot(entry, route, routing, defaults);
1206
+ }
1207
+ const title = defaults.title || "Untitled Entry";
1208
+ return buildNonRoutableSnapshot(entry, title);
1209
+ }
1210
+ function createSnapshotFromContentEntry(data, routing, defaults, titleField) {
1211
+ let resolvedDefaults = defaults;
1212
+ if (!data.route && data.draftContent) {
1213
+ const extractedTitle = resolveContentTitle(data.draftContent, titleField, "");
1214
+ if (extractedTitle) {
1215
+ resolvedDefaults = {
1216
+ ...defaults,
1217
+ title: extractedTitle
1218
+ };
1219
+ }
1220
+ }
1221
+ return buildSnapshot(data.entry, data.route ?? null, routing, resolvedDefaults);
1222
+ }
1223
+ function createSnapshotFromRoutableContent(owner, route, routing, defaults) {
1224
+ return buildSnapshot(owner, route, routing, defaults);
1225
+ }
1226
+ function createSnapshotFromPageRecord(record, routing, defaults) {
1227
+ const canonicalRoute = canonicalizePageRouteMetadata({
1228
+ draftPath: record.draftPath ?? null,
1229
+ publishedPath: record.path ?? null
1230
+ }, routing, { draftFallbackSlug: defaults.slug ?? null });
1231
+ const routeLike = {
1232
+ draftTitle: record.draftTitle ?? record.title ?? null,
1233
+ draftPath: canonicalRoute.draft.path,
1234
+ draftSlug: canonicalRoute.draft.slug,
1235
+ draftStatus: record.draftStatus ?? null,
1236
+ publishedTitle: record.title ?? null,
1237
+ publishedPath: canonicalRoute.published.path,
1238
+ publishedSlug: canonicalRoute.published.slug,
1239
+ status: record.status ?? null,
1240
+ publishedAt: record.publishedAt ?? null,
1241
+ scheduledPublishAt: record.scheduledPublishAt ?? null,
1242
+ draftMetaTitle: record.draftMetaTitle ?? null,
1243
+ draftMetaDescription: record.draftMetaDescription ?? null,
1244
+ publishedMetaTitle: record.publishedMetaTitle ?? null,
1245
+ publishedMetaDescription: record.publishedMetaDescription ?? null,
1246
+ ogImageId: record.ogImageId ?? null
1247
+ };
1248
+ return buildSnapshot({ id: record.id }, routeLike, routing, defaults);
1249
+ }
1250
+ function createMetadataStateFromSnapshot(snapshot) {
1251
+ if (snapshot.kind === "non-routable") {
1252
+ const empty = {};
1253
+ return {
1254
+ values: empty,
1255
+ baseline: empty
1256
+ };
1257
+ }
1258
+ const values = {
1259
+ draftTitle: snapshot.draft.title,
1260
+ draftSlug: snapshot.draft.slug,
1261
+ draftStatus: snapshot.draft.status,
1262
+ scheduledPublishAt: snapshot.draft.publishAt,
1263
+ draftMetaTitle: snapshot.route.draftMetaTitle ?? null,
1264
+ draftMetaDescription: snapshot.route.draftMetaDescription ?? null,
1265
+ ogImageId: snapshot.route.ogImageId ?? null
1266
+ };
1267
+ return {
1268
+ values,
1269
+ baseline: { ...values }
1270
+ };
1271
+ }
1272
+ /**
1273
+ * Extract published metadata values from a snapshot.
1274
+ * Used to set the publishedBaseline for unpublished changes detection.
1275
+ *
1276
+ * Returns null if the entry has never been published.
1277
+ */
1278
+ function getPublishedMetadataFromSnapshot(snapshot, routing) {
1279
+ if (snapshot.kind === "non-routable") {
1280
+ return null;
1281
+ }
1282
+ const hasPublishedSignal = snapshot.published.status === "published" || Boolean(snapshot.published.publishAt);
1283
+ if (!snapshot.published.title && !hasPublishedSignal) {
1284
+ return null;
1285
+ }
1286
+ const publishedTitle = snapshot.published.title ?? snapshot.draft.title;
1287
+ if (!publishedTitle) return null;
1288
+ const publishedPath = snapshot.published.path ?? snapshot.draft.path ?? "";
1289
+ const publishedSlug = routing ? routing.toSlug(publishedPath) : publishedPath ? publishedPath.replace(/^\//, "").replace(/\/$/, "") : "";
1290
+ return {
1291
+ draftTitle: publishedTitle,
1292
+ draftSlug: publishedSlug,
1293
+ draftStatus: snapshot.published.status ?? snapshot.draft.status ?? "draft",
1294
+ scheduledPublishAt: null,
1295
+ draftMetaTitle: snapshot.route.publishedMetaTitle ?? null,
1296
+ draftMetaDescription: snapshot.route.publishedMetaDescription ?? null,
1297
+ ogImageId: snapshot.route.ogImageId ?? null
1298
+ };
1299
+ }
1300
+ function buildToolbarState(snapshot, context, options) {
1301
+ const formatPublishMessage = ({ status, publishedAt, scheduledAt }) => {
1302
+ if (status === "published") {
1303
+ if (!publishedAt) return "Published";
1304
+ const date = new Date(publishedAt);
1305
+ if (Number.isNaN(date.getTime())) return "Published";
1306
+ return `Published ${date.toLocaleString()}`;
1307
+ }
1308
+ if (status === "scheduled" && scheduledAt) {
1309
+ const date = new Date(scheduledAt);
1310
+ if (!Number.isNaN(date.getTime())) {
1311
+ return `Scheduled for ${date.toLocaleString()}`;
1312
+ }
1313
+ }
1314
+ return "Not yet published";
1315
+ };
1316
+ const { status: statusField } = METADATA_WORKFLOW_FIELDS;
1317
+ const publishedMessage = formatPublishMessage({
1318
+ status: snapshot.status,
1319
+ publishedAt: snapshot.published.publishAt,
1320
+ scheduledAt: snapshot.draft.publishAt
1321
+ });
1322
+ let publishLabel = `Publish ${options.entityLabel.toLowerCase()}`;
1323
+ if (statusField && context.values[statusField] === "scheduled") {
1324
+ publishLabel = "Schedule publish";
1325
+ }
1326
+ return {
1327
+ currentStatus: snapshot.status,
1328
+ publishLabel,
1329
+ publishedMessage,
1330
+ disablePublish: false,
1331
+ disableDiscard: false,
1332
+ values: context.values
1333
+ };
1334
+ }
1335
+
1336
+ //#endregion
1337
+ //#region ../content-model/src/content-types/schema.ts
1338
+ /**
1339
+ * Content field types supported in custom content types.
1340
+ *
1341
+ * - text, textarea, richText: Text-based fields
1342
+ * - number: Numeric input
1343
+ * - date, datetime: Date/time pickers
1344
+ * - media: Image/video uploads
1345
+ * - file: Public file uploads/downloads
1346
+ * - boolean: Toggle/checkbox
1347
+ * - select: Dropdown with options
1348
+ * - reference: Link to another content type
1349
+ * - link: URL/page link picker
1350
+ * - url: Plain URL input
1351
+ * - button: Expands to a group with label, variant, link, and icons
1352
+ * - group: Nested field container (for advanced use cases)
1353
+ */
1354
+ const contentFieldTypes = [
1355
+ "text",
1356
+ "textarea",
1357
+ "richText",
1358
+ "number",
1359
+ "date",
1360
+ "url",
1361
+ "media",
1362
+ "file",
1363
+ "boolean",
1364
+ "select",
1365
+ "reference",
1366
+ "link",
1367
+ "button",
1368
+ "group",
1369
+ "repeater",
1370
+ "datetime"
1371
+ ];
1372
+ const DISPLAY_MEDIA_KINDS = [
1373
+ "image",
1374
+ "video",
1375
+ "audio"
1376
+ ];
1377
+ const FILE_FIELD_KINDS = [
1378
+ "document",
1379
+ "spreadsheet",
1380
+ "archive",
1381
+ "audio"
1382
+ ];
1383
+ const LEGACY_MEDIA_KINDS = [
1384
+ "image",
1385
+ "video",
1386
+ "audio",
1387
+ "document",
1388
+ "spreadsheet",
1389
+ "archive"
1390
+ ];
1391
+ /**
1392
+ * Base content field schema (non-recursive properties)
1393
+ */
1394
+ const BaseContentFieldSchema = z.object({
1395
+ id: z.string(),
1396
+ type: z.enum(contentFieldTypes),
1397
+ label: z.string().optional(),
1398
+ description: z.string().optional(),
1399
+ required: z.boolean().optional(),
1400
+ defaultValue: z.unknown().optional(),
1401
+ multiline: z.boolean().optional(),
1402
+ rows: z.number().int().positive().optional(),
1403
+ maxLength: z.number().int().positive().optional(),
1404
+ min: z.number().optional(),
1405
+ max: z.number().optional(),
1406
+ step: z.number().positive().optional(),
1407
+ mediaKinds: z.array(z.enum(LEGACY_MEDIA_KINDS)).optional(),
1408
+ fileKinds: z.array(z.enum(FILE_FIELD_KINDS)).optional(),
1409
+ allowRelative: z.boolean().optional(),
1410
+ options: z.array(z.object({
1411
+ value: z.string(),
1412
+ label: z.string().optional()
1413
+ })).optional(),
1414
+ multiple: z.boolean().optional(),
1415
+ referenceKind: z.string().trim().optional(),
1416
+ reference: z.object({ type: z.string().trim() }).optional(),
1417
+ valueFrom: z.enum([
1418
+ "id",
1419
+ "identifier",
1420
+ "slug"
1421
+ ]).optional(),
1422
+ ui: z.record(z.string(), z.unknown()).optional(),
1423
+ itemLabel: z.string().optional()
1424
+ }).superRefine((field, ctx) => {
1425
+ if ((field.type === "media" || field.type === "file") && field.defaultValue !== undefined && field.defaultValue !== null && (typeof field.defaultValue !== "object" || Array.isArray(field.defaultValue) || typeof field.defaultValue.identifier !== "string" || !field.defaultValue.identifier.trim())) {
1426
+ ctx.addIssue({
1427
+ code: z.ZodIssueCode.custom,
1428
+ path: ["defaultValue"],
1429
+ message: `${field.type} defaultValue must be null or an object with a non-empty identifier`
1430
+ });
1431
+ }
1432
+ if (field.multiple !== undefined && field.type !== "select") {
1433
+ ctx.addIssue({
1434
+ code: z.ZodIssueCode.custom,
1435
+ path: ["multiple"],
1436
+ message: "multiple is only valid for select fields"
1437
+ });
1438
+ }
1439
+ if (field.type !== "reference") {
1440
+ return;
1441
+ }
1442
+ const referenceKind = field.referenceKind;
1443
+ const legacyReferenceType = field.reference?.type;
1444
+ const effectiveReferenceKind$1 = referenceKind ?? legacyReferenceType;
1445
+ if (field.referenceKind !== undefined && !referenceKind) {
1446
+ ctx.addIssue({
1447
+ code: z.ZodIssueCode.custom,
1448
+ path: ["referenceKind"],
1449
+ message: "referenceKind must not be empty"
1450
+ });
1451
+ }
1452
+ if (field.reference?.type !== undefined && !legacyReferenceType) {
1453
+ ctx.addIssue({
1454
+ code: z.ZodIssueCode.custom,
1455
+ path: ["reference", "type"],
1456
+ message: "reference.type must not be empty"
1457
+ });
1458
+ }
1459
+ if (referenceKind && legacyReferenceType && referenceKind !== legacyReferenceType) {
1460
+ ctx.addIssue({
1461
+ code: z.ZodIssueCode.custom,
1462
+ path: ["referenceKind"],
1463
+ message: "referenceKind must match reference.type when both are provided"
1464
+ });
1465
+ }
1466
+ if (field.valueFrom !== undefined && effectiveReferenceKind$1 && isAdapterReferenceKind(effectiveReferenceKind$1)) {
1467
+ ctx.addIssue({
1468
+ code: z.ZodIssueCode.custom,
1469
+ path: ["valueFrom"],
1470
+ message: "valueFrom is only valid for content entry reference fields"
1471
+ });
1472
+ }
1473
+ }).passthrough();
1474
+ const ContentFieldSchema = BaseContentFieldSchema.extend({
1475
+ fields: z.lazy(() => z.array(ContentFieldSchema)).optional(),
1476
+ schema: z.lazy(() => z.object({ fields: z.array(ContentFieldSchema).optional() }).passthrough()).optional()
1477
+ });
1478
+ const RoutingSchema = z.object({
1479
+ enabled: z.boolean().default(false),
1480
+ pattern: z.string().optional(),
1481
+ slugField: z.string().optional(),
1482
+ pathField: z.string().optional(),
1483
+ prefix: z.string().optional(),
1484
+ subroutes: z.array(z.object({
1485
+ kind: z.string().min(1),
1486
+ segmentType: z.enum([
1487
+ "uuid",
1488
+ "iso-date",
1489
+ "uuid-or-date",
1490
+ "slug"
1491
+ ]),
1492
+ precedence: z.enum(["subroute-first", "content-first"]).optional()
1493
+ })).optional()
1494
+ }).superRefine((routing, ctx) => {
1495
+ if (!routing.pattern) {
1496
+ return;
1497
+ }
1498
+ const parseResult = safeParseRouteTemplate(routing.pattern);
1499
+ if (parseResult.success) {
1500
+ return;
1501
+ }
1502
+ ctx.addIssue({
1503
+ code: z.ZodIssueCode.custom,
1504
+ path: ["pattern"],
1505
+ message: describeRouteTemplateParseError(parseResult.error)
1506
+ });
1507
+ });
1508
+ const LabelsSchema = z.object({
1509
+ singular: z.string(),
1510
+ plural: z.string()
1511
+ });
1512
+ const OrderingSchema = z.object({ mode: z.enum(["default", "manual"]).default("default") }).default({ mode: "default" });
1513
+ const ContentTypeSchemaZ = z.object({
1514
+ version: z.number().default(1),
1515
+ fields: z.array(ContentFieldSchema).default([]),
1516
+ routing: RoutingSchema.optional(),
1517
+ titleField: z.string().optional(),
1518
+ widgets: z.record(z.string(), z.unknown()).optional(),
1519
+ labels: LabelsSchema.default({
1520
+ singular: "Entry",
1521
+ plural: "Entries"
1522
+ }),
1523
+ ordering: OrderingSchema.optional()
1524
+ }).refine((data) => {
1525
+ if (data.titleField) {
1526
+ const fieldExists = data.fields.some((f) => f.id === data.titleField);
1527
+ return fieldExists;
1528
+ }
1529
+ return true;
1530
+ }, {
1531
+ message: "titleField must reference an existing field ID in the fields array",
1532
+ path: ["titleField"]
1533
+ });
1534
+ function parseContentTypeSchema(input) {
1535
+ if (!input || typeof input !== "object") {
1536
+ throw new Error("[content-types] Missing or invalid schema payload");
1537
+ }
1538
+ const result = ContentTypeSchemaZ.safeParse(input);
1539
+ if (!result.success) {
1540
+ const message = result.error?.message ?? "Invalid content type schema";
1541
+ throw new Error(`[content-types] ${message}`);
1542
+ }
1543
+ return {
1544
+ ...result.data,
1545
+ fields: normalizeContentFields(result.data.fields ?? [])
1546
+ };
1547
+ }
1548
+ function isManualEntryOrderingEnabled(schema) {
1549
+ return schema?.ordering?.mode === "manual";
1550
+ }
1551
+ function normalizeContentFields(fields) {
1552
+ return fields.map((field) => {
1553
+ const schemaFields = field.schema?.fields ?? undefined;
1554
+ const nextFields = field.fields ?? schemaFields;
1555
+ const normalizedFields = nextFields ? normalizeContentFields(nextFields) : undefined;
1556
+ const next = normalizeLegacyFileMediaField({
1557
+ ...field,
1558
+ ...normalizedFields ? { fields: normalizedFields } : {}
1559
+ });
1560
+ return next;
1561
+ });
1562
+ }
1563
+ function normalizeLegacyFileMediaField(field) {
1564
+ if (field.type !== "media") return field;
1565
+ const mediaKinds = field.mediaKinds ?? [];
1566
+ const displayKinds = mediaKinds.filter(isDisplayMediaKind);
1567
+ const fileKinds = mediaKinds.filter(isFileFieldKind);
1568
+ if (fileKinds.length === 0 || displayKinds.length > 0) {
1569
+ return {
1570
+ ...field,
1571
+ mediaKinds: displayKinds.length > 0 ? displayKinds : field.mediaKinds
1572
+ };
1573
+ }
1574
+ const { mediaKinds: _legacyMediaKinds, ...rest } = field;
1575
+ return {
1576
+ ...rest,
1577
+ type: "file",
1578
+ fileKinds
1579
+ };
1580
+ }
1581
+ function isDisplayMediaKind(value) {
1582
+ return DISPLAY_MEDIA_KINDS.includes(value);
1583
+ }
1584
+ function isFileFieldKind(value) {
1585
+ return FILE_FIELD_KINDS.includes(value);
1586
+ }
1587
+
1588
+ //#endregion
1589
+ //#region ../content-model/src/content-types/sentinels.ts
1590
+ /** Leaf field types that receive a placeholder sentinel (containers recurse). */
1591
+ const SENTINEL_LEAF_TYPES = [
1592
+ "text",
1593
+ "textarea",
1594
+ "richText",
1595
+ "number",
1596
+ "date",
1597
+ "datetime",
1598
+ "url",
1599
+ "link",
1600
+ "media",
1601
+ "file",
1602
+ "boolean",
1603
+ "select",
1604
+ "reference"
1605
+ ];
1606
+ /** Matches the closed sentinel shape `"<REQUIRED: …>"`. */
1607
+ const SENTINEL_PATTERN = /^<REQUIRED:.*>$/;
1608
+ /**
1609
+ * True when a value is an unfilled placeholder sentinel. Shared by the emitter
1610
+ * (self-check) and the validator (rejection) so recognition can't drift.
1611
+ */
1612
+ function isPlaceholderSentinel(value) {
1613
+ return typeof value === "string" && SENTINEL_PATTERN.test(value);
1614
+ }
1615
+ function effectiveReferenceKind(field) {
1616
+ const kind = field.referenceKind;
1617
+ const legacy = field.reference?.type;
1618
+ return kind ?? legacy;
1619
+ }
1620
+ function selectOptionValues$1(field) {
1621
+ const options = field.options;
1622
+ return options ? options.map((o) => o.value) : [];
1623
+ }
1624
+ /**
1625
+ * The placeholder sentinel for a leaf field. Container types (group/button/
1626
+ * repeater) do not have a leaf sentinel — they expand structurally — and calling
1627
+ * this for them returns a generic marker only for completeness.
1628
+ */
1629
+ function buildSentinel(field) {
1630
+ switch (field.type) {
1631
+ case "text":
1632
+ case "textarea": return "<REQUIRED: text>";
1633
+ case "richText": return "<REQUIRED: rich-text>";
1634
+ case "number": return "<REQUIRED: number>";
1635
+ case "date": return "<REQUIRED: date (YYYY-MM-DD)>";
1636
+ case "datetime": return "<REQUIRED: datetime (ISO 8601)>";
1637
+ case "url": return "<REQUIRED: url>";
1638
+ case "link": return "<REQUIRED: link>";
1639
+ case "media": return "<REQUIRED: media-identifier>";
1640
+ case "file": return "<REQUIRED: file-identifier>";
1641
+ case "boolean": return "<REQUIRED: boolean>";
1642
+ case "select": {
1643
+ const values = selectOptionValues$1(field);
1644
+ return values.length > 0 ? `<REQUIRED: select (${values.join("|")})>` : "<REQUIRED: select>";
1645
+ }
1646
+ case "reference": {
1647
+ const kind = effectiveReferenceKind(field);
1648
+ return kind ? `<REQUIRED: reference-identifier (${kind})>` : "<REQUIRED: reference-identifier>";
1649
+ }
1650
+ case "group": return "<REQUIRED: group>";
1651
+ case "button": return "<REQUIRED: button>";
1652
+ case "repeater": return "<REQUIRED: repeater>";
1653
+ }
1654
+ }
1655
+ /**
1656
+ * Deep-scan arbitrary entry data for any unfilled placeholder sentinels. Needs no
1657
+ * schema, so it powers the CLI's pre-HTTP upsert pre-flight.
1658
+ */
1659
+ function findUnfilledSentinels(data, pathPrefix = "") {
1660
+ const found = [];
1661
+ const walk = (value, path) => {
1662
+ if (isPlaceholderSentinel(value)) {
1663
+ found.push({
1664
+ path: path || "(root)",
1665
+ value
1666
+ });
1667
+ return;
1668
+ }
1669
+ if (Array.isArray(value)) {
1670
+ value.forEach((item, index) => walk(item, `${path}[${index}]`));
1671
+ return;
1672
+ }
1673
+ if (value !== null && typeof value === "object") {
1674
+ for (const [key, child] of Object.entries(value)) {
1675
+ walk(child, path ? `${path}.${key}` : key);
1676
+ }
1677
+ }
1678
+ };
1679
+ walk(data, pathPrefix);
1680
+ return found;
1681
+ }
1682
+
1683
+ //#endregion
1684
+ //#region ../content-model/src/content-types/entryValidation.ts
1685
+ function subfieldsOf$1(field) {
1686
+ const direct = field.fields;
1687
+ if (direct && direct.length > 0) return direct;
1688
+ const nested = field.schema?.fields;
1689
+ return nested ?? [];
1690
+ }
1691
+ function repeaterVariantFields(field, item) {
1692
+ const itemTypes = field.itemTypes;
1693
+ if (!itemTypes) return subfieldsOf$1(field);
1694
+ const typeId = item._type;
1695
+ if (typeof typeId !== "string") return null;
1696
+ return itemTypes[typeId]?.fields ?? null;
1697
+ }
1698
+ function selectOptionValues(field) {
1699
+ const options = field.options;
1700
+ return options ? options.map((o) => o.value) : [];
1701
+ }
1702
+ function isPlainObject(value) {
1703
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1704
+ }
1705
+ const STRING_TYPES = new Set([
1706
+ "text",
1707
+ "textarea",
1708
+ "url",
1709
+ "link",
1710
+ "reference",
1711
+ "date",
1712
+ "datetime",
1713
+ "select"
1714
+ ]);
1715
+ function isAllowedIdentifierBearingMedia(field, value) {
1716
+ if (!isPlainObject(value) || typeof value.identifier !== "string" || value.identifier.trim().length === 0) {
1717
+ return false;
1718
+ }
1719
+ if (value.type === undefined) return true;
1720
+ const allowedKinds = field.type === "media" ? field.mediaKinds : field.fileKinds;
1721
+ if (typeof value.type !== "string") return false;
1722
+ const effectiveAllowedKinds = allowedKinds ?? (field.type === "media" ? ["image"] : ["document"]);
1723
+ return effectiveAllowedKinds.some((kind) => kind === value.type);
1724
+ }
1725
+ function checkLeafType(field, value, path, errors) {
1726
+ const push = (code, message) => {
1727
+ errors.push({
1728
+ path,
1729
+ fieldType: field.type,
1730
+ code,
1731
+ message
1732
+ });
1733
+ };
1734
+ if (field.type === "number") {
1735
+ if (typeof value !== "number" || Number.isNaN(value)) {
1736
+ push("wrong_type", `"${path}" must be a number.`);
1737
+ }
1738
+ return;
1739
+ }
1740
+ if (field.type === "boolean") {
1741
+ if (typeof value !== "boolean") push("wrong_type", `"${path}" must be a boolean.`);
1742
+ return;
1743
+ }
1744
+ if (field.type === "richText") {
1745
+ if (typeof value !== "string" && !isPlainObject(value)) {
1746
+ push("wrong_type", `"${path}" must be rich-text (string or document object).`);
1747
+ }
1748
+ return;
1749
+ }
1750
+ if (field.type === "media" || field.type === "file") {
1751
+ if (isPlainObject(value) && isPlaceholderSentinel(value.identifier)) {
1752
+ push("unfilled_placeholder", `"${path}" still contains an unfilled placeholder (${value.identifier}).`);
1753
+ return;
1754
+ }
1755
+ if (!isAllowedIdentifierBearingMedia(field, value)) {
1756
+ push("wrong_type", `"${path}" must be a media object with an identifier.`);
1757
+ }
1758
+ return;
1759
+ }
1760
+ if (field.type === "select") {
1761
+ if (typeof value !== "string") {
1762
+ push("wrong_type", `"${path}" must be a string.`);
1763
+ return;
1764
+ }
1765
+ const options = selectOptionValues(field);
1766
+ if (options.length > 0 && !options.includes(value)) {
1767
+ push("invalid_option", `"${path}" must be one of: ${options.join(", ")}.`);
1768
+ }
1769
+ return;
1770
+ }
1771
+ if (STRING_TYPES.has(field.type)) {
1772
+ if (typeof value !== "string") push("wrong_type", `"${path}" must be a string.`);
1773
+ }
1774
+ }
1775
+ function validateField(field, value, path, errors, allowIncomplete) {
1776
+ const missing = value === undefined || value === null || value === "";
1777
+ if (missing) {
1778
+ if (field.required === true && !allowIncomplete) {
1779
+ errors.push({
1780
+ path,
1781
+ fieldType: field.type,
1782
+ code: "missing_required",
1783
+ message: `Required field "${path}" is missing.`
1784
+ });
1785
+ }
1786
+ return;
1787
+ }
1788
+ if (isPlaceholderSentinel(value)) {
1789
+ errors.push({
1790
+ path,
1791
+ fieldType: field.type,
1792
+ code: "unfilled_placeholder",
1793
+ message: `"${path}" still contains an unfilled placeholder (${value}).`
1794
+ });
1795
+ return;
1796
+ }
1797
+ if (field.type === "group" || field.type === "button") {
1798
+ if (!isPlainObject(value)) {
1799
+ errors.push({
1800
+ path,
1801
+ fieldType: field.type,
1802
+ code: "wrong_type",
1803
+ message: `"${path}" must be an object.`
1804
+ });
1805
+ return;
1806
+ }
1807
+ validateFields(subfieldsOf$1(field), value, path, errors, allowIncomplete);
1808
+ return;
1809
+ }
1810
+ if (field.type === "repeater") {
1811
+ if (!Array.isArray(value)) {
1812
+ errors.push({
1813
+ path,
1814
+ fieldType: field.type,
1815
+ code: "wrong_type",
1816
+ message: `"${path}" must be an array.`
1817
+ });
1818
+ return;
1819
+ }
1820
+ value.forEach((item, index) => {
1821
+ const itemPath = `${path}[${index}]`;
1822
+ if (!isPlainObject(item)) {
1823
+ errors.push({
1824
+ path: itemPath,
1825
+ fieldType: "repeater",
1826
+ code: "wrong_type",
1827
+ message: `"${itemPath}" must be an object.`
1828
+ });
1829
+ return;
1830
+ }
1831
+ const itemFields = repeaterVariantFields(field, item);
1832
+ if (!itemFields) {
1833
+ errors.push({
1834
+ path: itemPath,
1835
+ fieldType: "repeater",
1836
+ code: "invalid_option",
1837
+ message: `"${itemPath}" has an unknown or missing repeater item type.`
1838
+ });
1839
+ return;
1840
+ }
1841
+ validateFields(itemFields, item, itemPath, errors, allowIncomplete);
1842
+ });
1843
+ return;
1844
+ }
1845
+ checkLeafType(field, value, path, errors);
1846
+ }
1847
+ function validateFields(fields, data, pathPrefix, errors, allowIncomplete) {
1848
+ for (const field of fields) {
1849
+ const path = pathPrefix ? `${pathPrefix}.${field.id}` : field.id;
1850
+ validateField(field, data[field.id], path, errors, allowIncomplete);
1851
+ }
1852
+ }
1853
+ /**
1854
+ * Validate entry data against a content-type schema. Returns all errors so
1855
+ * callers can list everything an agent must fix in one pass.
1856
+ */
1857
+ function validateEntryInstance(input) {
1858
+ const errors = [];
1859
+ validateFields(input.schema.fields, input.data, "", errors, input.allowIncomplete ?? false);
1860
+ return {
1861
+ valid: errors.length === 0,
1862
+ errors
1863
+ };
1864
+ }
1865
+
1866
+ //#endregion
1867
+ //#region ../content-model/src/content-types/scaffold.ts
1868
+ /** Nested subfields for group/repeater/button-style fields (both DSL shapes). */
1869
+ function subfieldsOf(field) {
1870
+ const direct = field.fields;
1871
+ if (direct && direct.length > 0) return direct;
1872
+ const nested = field.schema?.fields;
1873
+ return nested ?? [];
1874
+ }
1875
+ function resourceKindForFieldType(fieldType) {
1876
+ if (fieldType === "media") return "media";
1877
+ if (fieldType === "file") return "file";
1878
+ if (fieldType === "reference") return "reference";
1879
+ return undefined;
1880
+ }
1881
+ const CONTAINER_TYPES = new Set([
1882
+ "group",
1883
+ "button",
1884
+ "repeater"
1885
+ ]);
1886
+ /**
1887
+ * Build the placeholder value for a required field. Group/button fields recurse
1888
+ * into their required subfields; repeaters emit a single seed item so agents see
1889
+ * the item shape.
1890
+ */
1891
+ function buildFieldValue(field) {
1892
+ if (field.type === "repeater") {
1893
+ return [buildObjectFromFields(subfieldsOf(field))];
1894
+ }
1895
+ if (CONTAINER_TYPES.has(field.type)) {
1896
+ const subfields = subfieldsOf(field);
1897
+ if (subfields.length > 0) {
1898
+ return buildObjectFromFields(subfields);
1899
+ }
1900
+ if (field.type === "button") {
1901
+ return {
1902
+ label: "<REQUIRED: text>",
1903
+ link: "<REQUIRED: link>"
1904
+ };
1905
+ }
1906
+ return {};
1907
+ }
1908
+ if (field.type === "media" || field.type === "file") {
1909
+ return { identifier: buildSentinel(field) };
1910
+ }
1911
+ return buildSentinel(field);
1912
+ }
1913
+ /**
1914
+ * Build a template object from a list of fields, stubbing required fields and
1915
+ * omitting optional ones.
1916
+ */
1917
+ function buildObjectFromFields(fields) {
1918
+ const template = {};
1919
+ for (const field of fields) {
1920
+ if (field.required !== true) continue;
1921
+ template[field.id] = buildFieldValue(field);
1922
+ }
1923
+ return template;
1924
+ }
1925
+ function diagnosticFromValidationError(error) {
1926
+ const needsResource = resourceKindForFieldType(error.fieldType);
1927
+ const message = needsResource === "media" ? `Provide an existing media identifier for "${error.path}".` : needsResource === "file" ? `Provide an existing file identifier for "${error.path}".` : needsResource === "reference" ? `Provide an existing entry identifier for "${error.path}".` : error.message;
1928
+ return {
1929
+ path: error.path,
1930
+ fieldType: error.fieldType,
1931
+ ...needsResource ? { needsResource } : {},
1932
+ message
1933
+ };
1934
+ }
1935
+ /**
1936
+ * Emit a placeholder template + diagnostics for a content type from its field DSL.
1937
+ * Diagnostics come from the entry-instance validator run against the template,
1938
+ * merged with a sentinel sweep so no emitted placeholder goes unreported.
1939
+ */
1940
+ function buildEntryScaffold(input) {
1941
+ const template = buildObjectFromFields(input.schema.fields);
1942
+ const { errors } = validateEntryInstance({
1943
+ schema: input.schema,
1944
+ data: template
1945
+ });
1946
+ const diagnostics = errors.map(diagnosticFromValidationError);
1947
+ const seen = new Set(diagnostics.map((d) => d.path));
1948
+ for (const sentinel of findUnfilledSentinels(template)) {
1949
+ if (!seen.has(sentinel.path)) {
1950
+ seen.add(sentinel.path);
1951
+ diagnostics.push({
1952
+ path: sentinel.path,
1953
+ fieldType: "unknown",
1954
+ message: `Replace the placeholder at "${sentinel.path}" (${sentinel.value}).`
1955
+ });
1956
+ }
1957
+ }
1958
+ return {
1959
+ contentType: input.key,
1960
+ template,
1961
+ diagnostics,
1962
+ ready: diagnostics.length === 0
1963
+ };
1964
+ }
1965
+
1966
+ //#endregion
1967
+ //#region ../content-model/src/content-types/defaults.ts
1968
+ function applyContentTypeDefaults(schema) {
1969
+ return {
1970
+ version: schema.version ?? 1,
1971
+ fields: cloneFieldCollection(schema.fields ?? []),
1972
+ routing: schema.routing ? { ...schema.routing } : undefined,
1973
+ widgets: schema.widgets ? { ...schema.widgets } : undefined,
1974
+ labels: schema.labels ? { ...schema.labels } : {
1975
+ singular: "Entry",
1976
+ plural: "Entries"
1977
+ }
1978
+ };
1979
+ }
1980
+ function getWorkflowFieldIds(schema) {
1981
+ if (!schema.routing?.enabled) {
1982
+ return [
1983
+ null,
1984
+ null,
1985
+ null,
1986
+ null
1987
+ ];
1988
+ }
1989
+ return [
1990
+ "title",
1991
+ "slug",
1992
+ "status",
1993
+ "publishAt"
1994
+ ];
1995
+ }
1996
+ function serializeContentTypeSchema(schema) {
1997
+ return JSON.parse(JSON.stringify(applyContentTypeDefaults(schema)));
1998
+ }
1999
+ function cloneFieldCollection(collection) {
2000
+ return JSON.parse(JSON.stringify(collection));
2001
+ }
2002
+
2003
+ //#endregion
2004
+ //#region ../content-model/src/content-types/builtin-schemas.ts
2005
+ const POST_CONTENT_TYPE_SCHEMA = {
2006
+ version: 1,
2007
+ fields: [
2008
+ {
2009
+ id: "image",
2010
+ type: "media",
2011
+ label: "Featured image",
2012
+ description: "Shown in the post hero and previews."
2013
+ },
2014
+ {
2015
+ id: "excerpt",
2016
+ type: "textarea",
2017
+ label: "Excerpt",
2018
+ description: "Short summary shown in listings.",
2019
+ rows: 3
2020
+ },
2021
+ {
2022
+ id: "body",
2023
+ type: "richText",
2024
+ label: "Body",
2025
+ description: "Write the full post content. Paragraphs are separated automatically.",
2026
+ required: true,
2027
+ ui: { richTextVariant: "media" }
2028
+ }
2029
+ ],
2030
+ routing: {
2031
+ enabled: true,
2032
+ pattern: "/blog/{slug}",
2033
+ slugField: "slug",
2034
+ prefix: "/blog/"
2035
+ },
2036
+ labels: {
2037
+ singular: "Blog post",
2038
+ plural: "Blog posts"
2039
+ }
2040
+ };
2041
+ const EVENT_CONTENT_TYPE_SCHEMA = {
2042
+ version: 1,
2043
+ fields: [
2044
+ {
2045
+ id: "summary",
2046
+ type: "textarea",
2047
+ label: "Summary",
2048
+ description: "Short teaser shown in listings.",
2049
+ rows: 3
2050
+ },
2051
+ {
2052
+ id: "image",
2053
+ type: "media",
2054
+ label: "Listing image",
2055
+ description: "Optional image used for event cards and previews."
2056
+ },
2057
+ {
2058
+ id: "body",
2059
+ type: "richText",
2060
+ label: "Details",
2061
+ description: "Full event details and agenda."
2062
+ }
2063
+ ],
2064
+ routing: {
2065
+ enabled: true,
2066
+ pattern: "/events/{slug}",
2067
+ slugField: "slug",
2068
+ prefix: "/events/",
2069
+ subroutes: [{
2070
+ kind: "event-occurrence",
2071
+ segmentType: "uuid-or-date",
2072
+ precedence: "subroute-first"
2073
+ }]
2074
+ },
2075
+ labels: {
2076
+ singular: "Event",
2077
+ plural: "Events"
2078
+ }
2079
+ };
2080
+ const COURSE_CONTENT_TYPE_SCHEMA = {
2081
+ version: 1,
2082
+ fields: [
2083
+ {
2084
+ id: "summary",
2085
+ type: "textarea",
2086
+ label: "Summary",
2087
+ description: "Short teaser shown in listings.",
2088
+ rows: 3
2089
+ },
2090
+ {
2091
+ id: "image",
2092
+ type: "media",
2093
+ label: "Listing image",
2094
+ description: "Optional image used for course cards and previews."
2095
+ },
2096
+ {
2097
+ id: "body",
2098
+ type: "richText",
2099
+ label: "Details",
2100
+ description: "Full course details and curriculum."
2101
+ }
2102
+ ],
2103
+ routing: {
2104
+ enabled: true,
2105
+ pattern: "/courses/{slug}",
2106
+ slugField: "slug",
2107
+ prefix: "/courses/"
2108
+ },
2109
+ labels: {
2110
+ singular: "Course",
2111
+ plural: "Courses"
2112
+ }
2113
+ };
2114
+ const TEAM_MEMBER_CONTENT_TYPE_SCHEMA = {
2115
+ version: 1,
2116
+ fields: [
2117
+ {
2118
+ id: "intro",
2119
+ type: "textarea",
2120
+ label: "Profile intro",
2121
+ description: "A short opening paragraph for the profile page.",
2122
+ rows: 3,
2123
+ maxLength: 360
2124
+ },
2125
+ {
2126
+ id: "bio",
2127
+ type: "richText",
2128
+ label: "Profile body",
2129
+ description: "Longer profile story, approach, experience, and background."
2130
+ },
2131
+ {
2132
+ id: "credentials",
2133
+ type: "textarea",
2134
+ label: "Credentials and training",
2135
+ description: "Qualifications, training, or experience to highlight on the page.",
2136
+ rows: 4
2137
+ },
2138
+ {
2139
+ id: "specialties",
2140
+ type: "textarea",
2141
+ label: "Specialties and focus areas",
2142
+ description: "Areas this team member works with most often.",
2143
+ rows: 4
2144
+ },
2145
+ {
2146
+ id: "pullQuote",
2147
+ type: "textarea",
2148
+ label: "Pull quote",
2149
+ description: "Optional quote or short statement to feature on the page.",
2150
+ rows: 2,
2151
+ maxLength: 240
2152
+ },
2153
+ {
2154
+ id: "ctaLabel",
2155
+ type: "text",
2156
+ label: "Call-to-action label",
2157
+ description: "Optional button label for the profile page.",
2158
+ maxLength: 80
2159
+ },
2160
+ {
2161
+ id: "ctaHref",
2162
+ type: "url",
2163
+ label: "Call-to-action link",
2164
+ description: "Optional internal or external link for the profile page button.",
2165
+ allowRelative: true
2166
+ }
2167
+ ],
2168
+ routing: {
2169
+ enabled: true,
2170
+ pattern: "/team/{slug}",
2171
+ slugField: "slug",
2172
+ prefix: "/team/"
2173
+ },
2174
+ labels: {
2175
+ singular: "Team member",
2176
+ plural: "Team members"
2177
+ }
2178
+ };
2179
+ const PRODUCT_CONTENT_TYPE_SCHEMA = {
2180
+ version: 1,
2181
+ fields: [
2182
+ {
2183
+ id: "summary",
2184
+ type: "textarea",
2185
+ label: "Summary",
2186
+ description: "Short teaser shown in listings.",
2187
+ rows: 3
2188
+ },
2189
+ {
2190
+ id: "gallery",
2191
+ type: "media",
2192
+ label: "Listing image",
2193
+ description: "Optional image used for product cards and previews."
2194
+ },
2195
+ {
2196
+ id: "body",
2197
+ type: "richText",
2198
+ label: "Details",
2199
+ description: "Full product details and buying notes."
2200
+ }
2201
+ ],
2202
+ routing: {
2203
+ enabled: true,
2204
+ pattern: "/products/{slug}",
2205
+ slugField: "slug",
2206
+ prefix: "/products/"
2207
+ },
2208
+ labels: {
2209
+ singular: "Product",
2210
+ plural: "Products"
2211
+ }
2212
+ };
2213
+ const TESTIMONIAL_CONTENT_TYPE_SCHEMA = {
2214
+ version: 1,
2215
+ fields: [
2216
+ {
2217
+ id: "name",
2218
+ type: "text",
2219
+ label: "Name",
2220
+ description: "Who provided this testimonial?",
2221
+ required: true,
2222
+ maxLength: 120
2223
+ },
2224
+ {
2225
+ id: "jobTitle",
2226
+ type: "text",
2227
+ label: "Job title",
2228
+ description: "Optional title or role for the person.",
2229
+ maxLength: 120
2230
+ },
2231
+ {
2232
+ id: "company",
2233
+ type: "text",
2234
+ label: "Company",
2235
+ description: "Optional company or organisation.",
2236
+ maxLength: 120
2237
+ },
2238
+ {
2239
+ id: "headshot",
2240
+ type: "media",
2241
+ label: "Headshot",
2242
+ description: "Optional portrait or logo for this testimonial.",
2243
+ mediaKinds: ["image"]
2244
+ },
2245
+ {
2246
+ id: "body",
2247
+ type: "richText",
2248
+ label: "Testimonial",
2249
+ description: "Full testimonial text.",
2250
+ required: true,
2251
+ ui: { richTextVariant: "limited" }
2252
+ }
2253
+ ],
2254
+ routing: { enabled: false },
2255
+ titleField: "name",
2256
+ labels: {
2257
+ singular: "Testimonial",
2258
+ plural: "Testimonials"
2259
+ }
2260
+ };
2261
+ const PAGE_CONTENT_TYPE_SCHEMA = {
2262
+ version: 1,
2263
+ fields: [],
2264
+ routing: {
2265
+ enabled: true,
2266
+ pattern: "/{slug}",
2267
+ slugField: "slug",
2268
+ prefix: "/"
2269
+ },
2270
+ labels: {
2271
+ singular: "Page",
2272
+ plural: "Pages"
2273
+ }
2274
+ };
2275
+ const PRODUCT_CATEGORY_CONTENT_TYPE_SCHEMA = {
2276
+ version: 1,
2277
+ fields: [
2278
+ {
2279
+ id: "summary",
2280
+ type: "textarea",
2281
+ label: "Summary",
2282
+ description: "Short teaser shown above category product lists.",
2283
+ rows: 3
2284
+ },
2285
+ {
2286
+ id: "heroImage",
2287
+ type: "media",
2288
+ label: "Hero image",
2289
+ description: "Optional category landing page image."
2290
+ },
2291
+ {
2292
+ id: "body",
2293
+ type: "richText",
2294
+ label: "Body",
2295
+ description: "Long-form category landing page content."
2296
+ }
2297
+ ],
2298
+ routing: {
2299
+ enabled: true,
2300
+ pattern: "/shop/{slug}",
2301
+ slugField: "slug",
2302
+ prefix: "/shop/"
2303
+ },
2304
+ labels: {
2305
+ singular: "Product category",
2306
+ plural: "Product categories"
2307
+ }
2308
+ };
2309
+
2310
+ //#endregion
2311
+ //#region ../content-model/src/content-types/builtins.ts
2312
+ /**
2313
+ * Built-in `ContentTypeSchema` registry. Keyed exhaustively on
2314
+ * `BuiltinContentTypeKey` via `satisfies` — adding a new builtin key
2315
+ * fails the compile here until a schema is provided. The `as const`
2316
+ * import preserves field literals so `BUILTIN_CONTENT_TYPE_FIELD_IDS`
2317
+ * below can derive an accurate per-builtin field set.
2318
+ */
2319
+ const BUILTIN_CONTENT_TYPE_SCHEMAS = {
2320
+ post: POST_CONTENT_TYPE_SCHEMA,
2321
+ event: EVENT_CONTENT_TYPE_SCHEMA,
2322
+ course: COURSE_CONTENT_TYPE_SCHEMA,
2323
+ "team-member": TEAM_MEMBER_CONTENT_TYPE_SCHEMA,
2324
+ product: PRODUCT_CONTENT_TYPE_SCHEMA,
2325
+ testimonial: TESTIMONIAL_CONTENT_TYPE_SCHEMA,
2326
+ page: PAGE_CONTENT_TYPE_SCHEMA,
2327
+ "product-category": PRODUCT_CATEGORY_CONTENT_TYPE_SCHEMA
2328
+ };
2329
+ function deriveFieldIdSet(schema) {
2330
+ return new Set(schema.fields.map((f) => f.id));
2331
+ }
2332
+ /**
2333
+ * Field-id sets for every built-in content type — DERIVED from
2334
+ * `BUILTIN_CONTENT_TYPE_SCHEMAS`, so adding a field to a schema flows
2335
+ * through automatically. The previous hand-maintained registry left
2336
+ * drift to a snapshot test; now drift is impossible.
2337
+ *
2338
+ * Kept exported with the same shape consumers were used to.
2339
+ */
2340
+ const BUILTIN_CONTENT_TYPE_FIELD_IDS = {
2341
+ post: deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS.post),
2342
+ event: deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS.event),
2343
+ course: deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS.course),
2344
+ "team-member": deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS["team-member"]),
2345
+ product: deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS.product),
2346
+ testimonial: deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS.testimonial),
2347
+ page: deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS.page),
2348
+ "product-category": deriveFieldIdSet(BUILTIN_CONTENT_TYPE_SCHEMAS["product-category"])
2349
+ };
2350
+ const DOMAIN_BACKED_LINKED_ENTRY_REASON = "Domain-backed entries need a dedicated editor workflow.";
2351
+ /**
2352
+ * Preview editing support for built-in content entries when they appear as
2353
+ * linked data inside a block. Generic editorial content can use the shared
2354
+ * content-entry form; domain-backed content stays closed until its workflow
2355
+ * can preserve domain invariants such as occurrences, booking, or commerce.
2356
+ */
2357
+ const BUILTIN_PREVIEW_LINKED_ENTRY_EDITING = {
2358
+ post: { kind: "generic" },
2359
+ event: {
2360
+ kind: "unsupported-domain-backed",
2361
+ reason: DOMAIN_BACKED_LINKED_ENTRY_REASON
2362
+ },
2363
+ course: {
2364
+ kind: "unsupported-domain-backed",
2365
+ reason: DOMAIN_BACKED_LINKED_ENTRY_REASON
2366
+ },
2367
+ "team-member": { kind: "generic" },
2368
+ product: {
2369
+ kind: "unsupported-domain-backed",
2370
+ reason: DOMAIN_BACKED_LINKED_ENTRY_REASON
2371
+ },
2372
+ testimonial: { kind: "generic" },
2373
+ page: { kind: "generic" },
2374
+ "product-category": {
2375
+ kind: "unsupported-domain-backed",
2376
+ reason: DOMAIN_BACKED_LINKED_ENTRY_REASON
2377
+ }
2378
+ };
2379
+ function getBuiltinPreviewLinkedEntryEditing(key) {
2380
+ return BUILTIN_PREVIEW_LINKED_ENTRY_EDITING[key] ?? null;
2381
+ }
2382
+
2383
+ //#endregion
2384
+ //#region ../content-model/src/content-types/labels.ts
2385
+ function getContentTypeLabelsFromSchema(schema, _fallbackName) {
2386
+ return {
2387
+ singular: schema.labels.singular,
2388
+ plural: schema.labels.plural
2389
+ };
2390
+ }
2391
+
2392
+ //#endregion
2393
+ //#region ../field-primitives/src/bindings/index.ts
2394
+ const BINDING_TRANSFORMS = {
2395
+ RICHTEXT_FIRST_PARAGRAPH: "richtext.firstParagraph",
2396
+ DATE_FORMAT_SHORT: "date.formatShort",
2397
+ DATE_FORMAT_LONG: "date.formatLong",
2398
+ TEXT_INLINE_TEXT: "text.inlineText"
2399
+ };
2400
+ const BINDING_TRANSFORM_VALUES = Object.values(BINDING_TRANSFORMS);
2401
+
2402
+ //#endregion
2403
+ //#region ../content-model/src/templates/types.ts
2404
+ const TEMPLATE_BINDING_TRANSFORMS = BINDING_TRANSFORMS;
2405
+
2406
+ //#endregion
2407
+ //#region ../content-model/src/templates/bindingBuilders.ts
2408
+ function fieldBinding(path) {
2409
+ return {
2410
+ source: "field",
2411
+ path
2412
+ };
2413
+ }
2414
+ function metaBinding(path) {
2415
+ return {
2416
+ source: "meta",
2417
+ path
2418
+ };
2419
+ }
2420
+ function literalBinding(value) {
2421
+ return {
2422
+ source: "literal",
2423
+ value
2424
+ };
2425
+ }
2426
+ function transformBinding(transform, options = {}) {
2427
+ return {
2428
+ source: "transform",
2429
+ transform,
2430
+ ...options.input === undefined ? {} : { input: options.input },
2431
+ ...options.path === undefined ? {} : { path: options.path }
2432
+ };
2433
+ }
2434
+ function fallbackBinding(...options) {
2435
+ return {
2436
+ source: "fallback",
2437
+ options: [...options]
2438
+ };
2439
+ }
2440
+
2441
+ //#endregion
2442
+ //#region ../content-model/src/entry-slots/types.ts
2443
+ function ok(value) {
2444
+ return {
2445
+ ok: true,
2446
+ value
2447
+ };
2448
+ }
2449
+ function err(error) {
2450
+ return {
2451
+ ok: false,
2452
+ error
2453
+ };
2454
+ }
2455
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2456
+ const SLOT_KEY_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
2457
+ function unsafeAsSlotKey(value) {
2458
+ return value;
2459
+ }
2460
+ function unsafeAsEntryBlockSlotId(value) {
2461
+ return value;
2462
+ }
2463
+ function unsafeAsEntrySlotContentEntryId(value) {
2464
+ return value;
2465
+ }
2466
+ function unsafeAsEntrySlotPageSurfaceId(value) {
2467
+ return value;
2468
+ }
2469
+ function unsafeAsPageSurfaceSlotId(value) {
2470
+ return value;
2471
+ }
2472
+ function unsafeAsEntrySlotPageId(value) {
2473
+ return value;
2474
+ }
2475
+ function unsafeAsEntrySlotBlockId(value) {
2476
+ return value;
2477
+ }
2478
+ function unsafeAsEntrySlotContentVersionId(value) {
2479
+ return value;
2480
+ }
2481
+ function unsafeAsEntrySlotBlockOrderIndex(value) {
2482
+ return value;
2483
+ }
2484
+ function parseSlotKey(value) {
2485
+ if (typeof value !== "string") return err({ kind: "not-string" });
2486
+ const normalized = value.trim();
2487
+ if (!normalized) return err({ kind: "empty" });
2488
+ if (!SLOT_KEY_PATTERN.test(normalized)) {
2489
+ return err({
2490
+ kind: "invalid-format",
2491
+ value: normalized
2492
+ });
2493
+ }
2494
+ return ok(unsafeAsSlotKey(normalized));
2495
+ }
2496
+ function parseEntryBlockSlotId(value) {
2497
+ return parseUuidBrand(value, unsafeAsEntryBlockSlotId);
2498
+ }
2499
+ function parseEntrySlotPageSurfaceId(value) {
2500
+ return parseUuidBrand(value, unsafeAsEntrySlotPageSurfaceId);
2501
+ }
2502
+ function parsePageSurfaceSlotId(value) {
2503
+ return parseUuidBrand(value, unsafeAsPageSurfaceSlotId);
2504
+ }
2505
+ function parseUuidBrand(value, brand) {
2506
+ if (typeof value !== "string") return err({ kind: "not-string" });
2507
+ const normalized = value.trim().toLowerCase();
2508
+ if (!normalized) return err({ kind: "empty" });
2509
+ if (!UUID_PATTERN.test(normalized)) {
2510
+ return err({
2511
+ kind: "invalid-uuid",
2512
+ value: normalized
2513
+ });
2514
+ }
2515
+ return ok(brand(normalized));
2516
+ }
2517
+ function parseNonEmptyStringBrand(value, brand) {
2518
+ if (typeof value !== "string") return err({ kind: "not-string" });
2519
+ const normalized = value.trim();
2520
+ if (!normalized) return err({ kind: "empty" });
2521
+ return ok(brand(normalized));
2522
+ }
2523
+ const SYSTEM_ENTRY_SLOT_BLOCK_KIND = "system.entry-slot";
2524
+ const DEFAULT_ENTRY_SLOT_ALLOWED_BLOCKS = [
2525
+ "block.body-text",
2526
+ "block.rich-text",
2527
+ "block.image-gallery",
2528
+ "block.cta-full"
2529
+ ];
2530
+ const TEMPLATE_ONLY_BLOCK_KINDS = [
2531
+ SYSTEM_ENTRY_SLOT_BLOCK_KIND,
2532
+ "block.event-details",
2533
+ "block.event-registration"
2534
+ ];
2535
+ function parseEntrySlotDefinitions(template) {
2536
+ const definitions = [];
2537
+ const seen = new Map();
2538
+ for (const [blockIndex, block] of template.blocks.entries()) {
2539
+ if (!isEntrySlotTemplateBlock(block)) continue;
2540
+ const parsed = parseEntrySlotDefinition(block, blockIndex);
2541
+ if (!parsed.ok) return parsed;
2542
+ const firstBlockIndex = seen.get(parsed.value.slotKey);
2543
+ if (firstBlockIndex !== undefined) {
2544
+ return err({
2545
+ kind: "duplicate-slot-key",
2546
+ slotKey: parsed.value.slotKey,
2547
+ firstBlockIndex,
2548
+ blockIndex
2549
+ });
2550
+ }
2551
+ seen.set(parsed.value.slotKey, blockIndex);
2552
+ definitions.push(parsed.value);
2553
+ }
2554
+ return ok(definitions);
2555
+ }
2556
+ function isEntrySlotTemplateBlock(block) {
2557
+ return block.blockKind === SYSTEM_ENTRY_SLOT_BLOCK_KIND;
2558
+ }
2559
+ function parseEntrySlotDefinition(block, blockIndex) {
2560
+ if (block.scope !== "template") {
2561
+ return err({
2562
+ kind: "invalid-slot-scope",
2563
+ blockIndex,
2564
+ scope: block.scope
2565
+ });
2566
+ }
2567
+ if (!isRecord$2(block.content)) {
2568
+ return err({
2569
+ kind: "invalid-slot-content",
2570
+ blockIndex
2571
+ });
2572
+ }
2573
+ const slotKeyResult = parseSlotKey(block.content.slotKey);
2574
+ if (!slotKeyResult.ok) {
2575
+ return err({
2576
+ kind: "invalid-slot-key",
2577
+ blockIndex,
2578
+ error: slotKeyResult.error
2579
+ });
2580
+ }
2581
+ const label = typeof block.content.label === "string" ? block.content.label.trim() : "";
2582
+ if (!label) {
2583
+ return err({
2584
+ kind: "missing-label",
2585
+ blockIndex,
2586
+ slotKey: slotKeyResult.value
2587
+ });
2588
+ }
2589
+ const allowedBlocks = parseAllowedBlocks(block.content.allowedBlocks, slotKeyResult.value, blockIndex);
2590
+ if (!allowedBlocks.ok) return allowedBlocks;
2591
+ return ok({
2592
+ slotKey: slotKeyResult.value,
2593
+ label,
2594
+ allowedBlocks: allowedBlocks.value
2595
+ });
2596
+ }
2597
+ function parseAllowedBlocks(value, slotKey, blockIndex) {
2598
+ if (value === undefined) {
2599
+ return ok({
2600
+ kind: "default",
2601
+ blockKinds: DEFAULT_ENTRY_SLOT_ALLOWED_BLOCKS
2602
+ });
2603
+ }
2604
+ if (!Array.isArray(value)) {
2605
+ return err({
2606
+ kind: "invalid-allowed-blocks",
2607
+ blockIndex,
2608
+ slotKey
2609
+ });
2610
+ }
2611
+ const allowedBlockValues = value;
2612
+ if (allowedBlockValues.length === 0) {
2613
+ return ok({
2614
+ kind: "none",
2615
+ blockKinds: []
2616
+ });
2617
+ }
2618
+ const blockKinds = [];
2619
+ const seen = new Set();
2620
+ for (const blockKind of allowedBlockValues) {
2621
+ if (typeof blockKind !== "string" || !blockKind.trim()) {
2622
+ return err({
2623
+ kind: "invalid-allowed-block-kind",
2624
+ blockIndex,
2625
+ slotKey,
2626
+ value: blockKind
2627
+ });
2628
+ }
2629
+ const normalized = blockKind.trim();
2630
+ if (isTemplateOnlyBlockKind(normalized)) {
2631
+ return err({
2632
+ kind: "template-only-block-kind",
2633
+ blockIndex,
2634
+ slotKey,
2635
+ blockKind: normalized
2636
+ });
2637
+ }
2638
+ if (seen.has(normalized)) {
2639
+ return err({
2640
+ kind: "duplicate-allowed-block-kind",
2641
+ blockIndex,
2642
+ slotKey,
2643
+ blockKind: normalized
2644
+ });
2645
+ }
2646
+ seen.add(normalized);
2647
+ blockKinds.push(normalized);
2648
+ }
2649
+ return ok({
2650
+ kind: "restricted",
2651
+ blockKinds
2652
+ });
2653
+ }
2654
+ function validateBlockKindForEntrySlot(definition, blockKind) {
2655
+ const normalized = blockKind.trim();
2656
+ if (isTemplateOnlyBlockKind(normalized)) {
2657
+ return err({
2658
+ kind: "template-only-block-kind",
2659
+ blockKind: normalized
2660
+ });
2661
+ }
2662
+ if (definition.allowedBlocks.kind === "none") {
2663
+ return err({
2664
+ kind: "block-kind-not-allowed",
2665
+ blockKind: normalized,
2666
+ slotKey: definition.slotKey
2667
+ });
2668
+ }
2669
+ if (!definition.allowedBlocks.blockKinds.includes(normalized)) {
2670
+ return err({
2671
+ kind: "block-kind-not-allowed",
2672
+ blockKind: normalized,
2673
+ slotKey: definition.slotKey
2674
+ });
2675
+ }
2676
+ return ok(normalized);
2677
+ }
2678
+ function isTemplateOnlyBlockKind(blockKind) {
2679
+ return blockKind.startsWith("system.") || TEMPLATE_ONLY_BLOCK_KINDS.includes(blockKind);
2680
+ }
2681
+ function resolveEntrySlotVisibility(input) {
2682
+ const storedByKey = new Map();
2683
+ for (const slot of input.storedSlots) {
2684
+ storedByKey.set(slot.slotKey, slot);
2685
+ }
2686
+ const visible = input.definitions.map((definition) => ({
2687
+ kind: "defined",
2688
+ definition,
2689
+ entrySlot: storedByKey.get(definition.slotKey) ?? null
2690
+ }));
2691
+ const definedKeys = new Set(input.definitions.map((definition) => definition.slotKey));
2692
+ for (const slot of input.storedSlots) {
2693
+ if (!definedKeys.has(slot.slotKey)) {
2694
+ visible.push({
2695
+ kind: "orphaned",
2696
+ slotKey: slot.slotKey,
2697
+ entrySlot: slot
2698
+ });
2699
+ }
2700
+ }
2701
+ return visible;
2702
+ }
2703
+ function resolveEntryPageCompositionState(slot) {
2704
+ if (slot.kind === "orphaned") {
2705
+ return {
2706
+ kind: "invalid",
2707
+ reason: "orphaned-slot"
2708
+ };
2709
+ }
2710
+ if (!slot.entrySlot) return { kind: "absent" };
2711
+ const { draftCompositionVersionId, publishedCompositionVersionId } = slot.entrySlot;
2712
+ if (draftCompositionVersionId && publishedCompositionVersionId) {
2713
+ return {
2714
+ kind: "draft-and-published",
2715
+ draftCompositionVersionId,
2716
+ publishedCompositionVersionId
2717
+ };
2718
+ }
2719
+ if (draftCompositionVersionId) return {
2720
+ kind: "draft-only",
2721
+ draftCompositionVersionId
2722
+ };
2723
+ if (publishedCompositionVersionId) return {
2724
+ kind: "published-only",
2725
+ publishedCompositionVersionId
2726
+ };
2727
+ return { kind: "absent" };
2728
+ }
2729
+ function selectBlocksForDeclaredEntrySlot(input) {
2730
+ return input.entrySlots.find((slot) => slot.slotKey === input.declaredSlotKey)?.blocks ?? [];
2731
+ }
2732
+ function buildDraftEntrySlotCompositionVersionData(input) {
2733
+ return {
2734
+ schemaVersion: 1,
2735
+ stage: "draft",
2736
+ ...input
2737
+ };
2738
+ }
2739
+ function entrySlotCompositionOwnerFromV1(versionData) {
2740
+ return {
2741
+ kind: "entrySlot",
2742
+ entrySlotId: versionData.entrySlotId,
2743
+ contentEntryId: versionData.contentEntryId
2744
+ };
2745
+ }
2746
+ function buildDraftSlotCompositionVersionData(input) {
2747
+ return {
2748
+ schemaVersion: 2,
2749
+ stage: "draft",
2750
+ ...input
2751
+ };
2752
+ }
2753
+ function buildPublishedEntrySlotCompositionVersionData(input) {
2754
+ const published = filterPublishedSlotBlocks(input.blockSequence);
2755
+ if (!published.ok) return published;
2756
+ return ok({
2757
+ schemaVersion: 1,
2758
+ stage: "published",
2759
+ entrySlotId: input.entrySlotId,
2760
+ contentEntryId: input.contentEntryId,
2761
+ slotKey: input.slotKey,
2762
+ blockSequence: published.value
2763
+ });
2764
+ }
2765
+ function buildPublishedSlotCompositionVersionData(input) {
2766
+ const published = filterPublishedSlotBlocks(input.blockSequence);
2767
+ if (!published.ok) return published;
2768
+ return ok({
2769
+ schemaVersion: 2,
2770
+ stage: "published",
2771
+ owner: input.owner,
2772
+ slotKey: input.slotKey,
2773
+ blockSequence: published.value
2774
+ });
2775
+ }
2776
+ function filterPublishedSlotBlocks(blockSequence) {
2777
+ for (const block of blockSequence) {
2778
+ if (block.kind !== "published-block-content") {
2779
+ return err({
2780
+ kind: "published-composition-contains-unpublished-block",
2781
+ blockId: block.blockId,
2782
+ snapshotKind: block.kind
2783
+ });
2784
+ }
2785
+ }
2786
+ return ok(blockSequence.filter((block) => block.kind === "published-block-content"));
2787
+ }
2788
+ function parseEntrySlotCompositionVersionData(input) {
2789
+ if (!isRecord$2(input)) return err({ kind: "not-object" });
2790
+ if (input.schemaVersion !== 1) {
2791
+ return err({
2792
+ kind: "unsupported-schema-version",
2793
+ schemaVersion: input.schemaVersion
2794
+ });
2795
+ }
2796
+ const stage = parseSlotCompositionStage(input.stage);
2797
+ if (!stage.ok) {
2798
+ return err({
2799
+ kind: "invalid-stage",
2800
+ stage: input.stage
2801
+ });
2802
+ }
2803
+ const entrySlotId = parseEntryBlockSlotId(input.entrySlotId);
2804
+ if (!entrySlotId.ok) return err({
2805
+ kind: "invalid-entry-slot-id",
2806
+ error: entrySlotId.error
2807
+ });
2808
+ const contentEntryId = parseNonEmptyStringBrand(input.contentEntryId, unsafeAsEntrySlotContentEntryId);
2809
+ if (!contentEntryId.ok) return err({
2810
+ kind: "invalid-content-entry-id",
2811
+ error: contentEntryId.error
2812
+ });
2813
+ const slotKey = parseSlotKey(input.slotKey);
2814
+ if (!slotKey.ok) return err({
2815
+ kind: "invalid-slot-key",
2816
+ error: slotKey.error
2817
+ });
2818
+ const parsedBlockSequence = parseSlotCompositionBlockSequence(stage.value, input.blockSequence);
2819
+ if (!parsedBlockSequence.ok) return parsedBlockSequence;
2820
+ if (parsedBlockSequence.value.stage === "draft") {
2821
+ return ok(buildDraftEntrySlotCompositionVersionData({
2822
+ entrySlotId: entrySlotId.value,
2823
+ contentEntryId: contentEntryId.value,
2824
+ slotKey: slotKey.value,
2825
+ blockSequence: parsedBlockSequence.value.blockSequence
2826
+ }));
2827
+ }
2828
+ return ok({
2829
+ schemaVersion: 1,
2830
+ stage: "published",
2831
+ entrySlotId: entrySlotId.value,
2832
+ contentEntryId: contentEntryId.value,
2833
+ slotKey: slotKey.value,
2834
+ blockSequence: parsedBlockSequence.value.blockSequence
2835
+ });
2836
+ }
2837
+ function parseSlotCompositionVersionData(input) {
2838
+ if (!isRecord$2(input)) return err({ kind: "not-object" });
2839
+ if (input.schemaVersion === 1) {
2840
+ return parseEntrySlotCompositionVersionData(input);
2841
+ }
2842
+ if (input.schemaVersion !== 2) {
2843
+ return err({
2844
+ kind: "unsupported-schema-version",
2845
+ schemaVersion: input.schemaVersion
2846
+ });
2847
+ }
2848
+ const stage = parseSlotCompositionStage(input.stage);
2849
+ if (!stage.ok) {
2850
+ return err({
2851
+ kind: "invalid-stage",
2852
+ stage: input.stage
2853
+ });
2854
+ }
2855
+ const owner = parseSlotCompositionOwner(input.owner);
2856
+ if (!owner.ok) return err({
2857
+ kind: "invalid-owner",
2858
+ error: owner.error
2859
+ });
2860
+ const slotKey = parseSlotKey(input.slotKey);
2861
+ if (!slotKey.ok) return err({
2862
+ kind: "invalid-slot-key",
2863
+ error: slotKey.error
2864
+ });
2865
+ const parsedBlockSequence = parseSlotCompositionBlockSequence(stage.value, input.blockSequence);
2866
+ if (!parsedBlockSequence.ok) return parsedBlockSequence;
2867
+ if (parsedBlockSequence.value.stage === "draft") {
2868
+ return ok(buildDraftSlotCompositionVersionData({
2869
+ owner: owner.value,
2870
+ slotKey: slotKey.value,
2871
+ blockSequence: parsedBlockSequence.value.blockSequence
2872
+ }));
2873
+ }
2874
+ return ok({
2875
+ schemaVersion: 2,
2876
+ stage: "published",
2877
+ owner: owner.value,
2878
+ slotKey: slotKey.value,
2879
+ blockSequence: parsedBlockSequence.value.blockSequence
2880
+ });
2881
+ }
2882
+ function parseSlotCompositionStage(value) {
2883
+ return value === "draft" || value === "published" ? ok(value) : err("invalid");
2884
+ }
2885
+ function parseSlotCompositionBlockSequence(stage, value) {
2886
+ if (!Array.isArray(value)) return err({ kind: "invalid-block-sequence" });
2887
+ const blockSequence = [];
2888
+ const publishedBlockSequence = [];
2889
+ for (const [index, snapshot] of value.entries()) {
2890
+ const parsed = parseEntrySlotBlockSnapshot(snapshot);
2891
+ if (!parsed.ok) return err({
2892
+ kind: "invalid-block-snapshot",
2893
+ index
2894
+ });
2895
+ if (stage === "published" && parsed.value.kind !== "published-block-content") {
2896
+ return err({
2897
+ kind: "published-composition-contains-unpublished-block",
2898
+ index
2899
+ });
2900
+ }
2901
+ if (parsed.value.kind === "published-block-content") {
2902
+ publishedBlockSequence.push(parsed.value);
2903
+ }
2904
+ blockSequence.push(parsed.value);
2905
+ }
2906
+ return stage === "draft" ? ok({
2907
+ stage,
2908
+ blockSequence
2909
+ }) : ok({
2910
+ stage,
2911
+ blockSequence: publishedBlockSequence
2912
+ });
2913
+ }
2914
+ function parseSlotCompositionOwner(input) {
2915
+ if (!isRecord$2(input)) return err({ kind: "not-object" });
2916
+ if (input.kind === "entrySlot") {
2917
+ const entrySlotId = parseEntryBlockSlotId(input.entrySlotId);
2918
+ if (!entrySlotId.ok) return err({
2919
+ kind: "invalid-entry-slot-id",
2920
+ error: entrySlotId.error
2921
+ });
2922
+ const contentEntryId = parseNonEmptyStringBrand(input.contentEntryId, unsafeAsEntrySlotContentEntryId);
2923
+ if (!contentEntryId.ok) return err({
2924
+ kind: "invalid-content-entry-id",
2925
+ error: contentEntryId.error
2926
+ });
2927
+ return ok({
2928
+ kind: "entrySlot",
2929
+ entrySlotId: entrySlotId.value,
2930
+ contentEntryId: contentEntryId.value
2931
+ });
2932
+ }
2933
+ if (input.kind === "surfaceSlot") {
2934
+ const surfaceSlotId = parsePageSurfaceSlotId(input.surfaceSlotId);
2935
+ if (!surfaceSlotId.ok) return err({
2936
+ kind: "invalid-surface-slot-id",
2937
+ error: surfaceSlotId.error
2938
+ });
2939
+ const pageSurfaceId = parseEntrySlotPageSurfaceId(input.pageSurfaceId);
2940
+ if (!pageSurfaceId.ok) return err({
2941
+ kind: "invalid-page-surface-id",
2942
+ error: pageSurfaceId.error
2943
+ });
2944
+ return ok({
2945
+ kind: "surfaceSlot",
2946
+ surfaceSlotId: surfaceSlotId.value,
2947
+ pageSurfaceId: pageSurfaceId.value
2948
+ });
2949
+ }
2950
+ return err({
2951
+ kind: "unsupported-owner-kind",
2952
+ ownerKind: input.kind
2953
+ });
2954
+ }
2955
+ function parseEntrySlotBlockSnapshot(input) {
2956
+ if (!isRecord$2(input)) return err("invalid");
2957
+ const blockId = parseNonEmptyStringBrand(input.blockId, unsafeAsEntrySlotBlockId);
2958
+ const orderIndex = parseBlockOrderIndex(input.orderIndex);
2959
+ const blockKind = typeof input.blockKind === "string" && input.blockKind.trim() ? input.blockKind.trim() : null;
2960
+ const purpose = input.purpose === undefined ? "content" : typeof input.purpose === "string" && input.purpose.trim() ? input.purpose.trim() : null;
2961
+ const identifier = input.identifier === null || input.identifier === undefined ? null : typeof input.identifier === "string" ? input.identifier : undefined;
2962
+ if (!blockId.ok || !orderIndex.ok || !blockKind || !purpose || identifier === undefined) return err("invalid");
2963
+ if (input.kind === "published-block-content") {
2964
+ const contentVersionId = parseNonEmptyStringBrand(input.contentVersionId, unsafeAsEntrySlotContentVersionId);
2965
+ if (!contentVersionId.ok) return err("invalid");
2966
+ return ok({
2967
+ kind: "published-block-content",
2968
+ blockId: blockId.value,
2969
+ contentVersionId: contentVersionId.value,
2970
+ identifier,
2971
+ blockKind,
2972
+ purpose,
2973
+ orderIndex: orderIndex.value
2974
+ });
2975
+ }
2976
+ if (input.kind === "draft-block-content") {
2977
+ const contentVersionId = input.contentVersionId === null ? ok(null) : parseNonEmptyStringBrand(input.contentVersionId, unsafeAsEntrySlotContentVersionId);
2978
+ if (!contentVersionId.ok) return err("invalid");
2979
+ return ok({
2980
+ kind: "draft-block-content",
2981
+ blockId: blockId.value,
2982
+ contentVersionId: contentVersionId.value,
2983
+ identifier,
2984
+ blockKind,
2985
+ purpose,
2986
+ orderIndex: orderIndex.value
2987
+ });
2988
+ }
2989
+ if (input.kind === "missing-block-content" && (input.reason === "new-block" || input.reason === "deleted-content-version" || input.reason === "unpublished-block")) {
2990
+ return ok({
2991
+ kind: "missing-block-content",
2992
+ blockId: blockId.value,
2993
+ identifier,
2994
+ blockKind,
2995
+ purpose,
2996
+ orderIndex: orderIndex.value,
2997
+ reason: input.reason
2998
+ });
2999
+ }
3000
+ return err("invalid");
3001
+ }
3002
+ function parseBlockOrderIndex(value) {
3003
+ if (!Number.isInteger(value) || typeof value !== "number" || value < 0) return err("invalid");
3004
+ return ok(unsafeAsEntrySlotBlockOrderIndex(value));
3005
+ }
3006
+ function blockContentVersionIdsForEntrySlotCompositionVersionData(versionData) {
3007
+ return blockContentVersionIdsForSlotCompositionVersionData(versionData);
3008
+ }
3009
+ function blockContentVersionIdsForSlotCompositionVersionData(versionData) {
3010
+ return versionData.blockSequence.flatMap((block) => block.kind === "missing-block-content" || block.contentVersionId === null ? [] : [block.contentVersionId]);
3011
+ }
3012
+ function planEntrySlotPublish(input) {
3013
+ if (input.slot.kind === "orphaned") {
3014
+ return {
3015
+ kind: "warning-only",
3016
+ slotKey: input.slot.slotKey,
3017
+ warning: "orphaned-slot-hidden"
3018
+ };
3019
+ }
3020
+ const missingBlocks = input.draftBlockSequence.filter((block) => block.kind === "missing-block-content").map((block) => block.blockId);
3021
+ if (missingBlocks.length > 0) {
3022
+ return {
3023
+ kind: "blocked",
3024
+ slotKey: input.slot.definition.slotKey,
3025
+ reason: "missing-block-content",
3026
+ blockIds: missingBlocks
3027
+ };
3028
+ }
3029
+ return {
3030
+ kind: "publishable",
3031
+ slotKey: input.slot.definition.slotKey,
3032
+ needsBlockContentPublish: input.draftBlockSequence.some((block) => block.kind === "draft-block-content")
3033
+ };
3034
+ }
3035
+ function planEntrySlotDiscard(slot) {
3036
+ return slot.kind === "orphaned" ? {
3037
+ kind: "skip-hidden-orphan",
3038
+ slotKey: slot.slotKey
3039
+ } : {
3040
+ kind: "discardable",
3041
+ slotKey: slot.definition.slotKey
3042
+ };
3043
+ }
3044
+ function parseSdkEntryBlocksPatch(blocks) {
3045
+ if (blocks === undefined) return ok({ kind: "leave-unchanged" });
3046
+ if (!isRecord$2(blocks)) return err({ kind: "invalid-blocks-object" });
3047
+ const slots = [];
3048
+ for (const [rawSlotKey, rawBlocks] of Object.entries(blocks)) {
3049
+ const slotKey = parseSlotKey(rawSlotKey);
3050
+ if (!slotKey.ok) return err({
3051
+ kind: "invalid-slot-key",
3052
+ key: rawSlotKey,
3053
+ error: slotKey.error
3054
+ });
3055
+ if (!Array.isArray(rawBlocks)) return err({
3056
+ kind: "invalid-slot-blocks",
3057
+ slotKey: slotKey.value
3058
+ });
3059
+ if (rawBlocks.length === 0) {
3060
+ slots.push({
3061
+ kind: "clear-slot",
3062
+ slotKey: slotKey.value
3063
+ });
3064
+ continue;
3065
+ }
3066
+ const parsedBlocks = [];
3067
+ const blockIndexByIdentifier = new Map();
3068
+ for (const [blockIndex, rawBlock] of rawBlocks.entries()) {
3069
+ const parsed = parseSdkEntrySlotBlockInput(rawBlock, slotKey.value, blockIndex);
3070
+ if (!parsed.ok) return parsed;
3071
+ if (parsed.value.identifier) {
3072
+ const firstBlockIndex = blockIndexByIdentifier.get(parsed.value.identifier);
3073
+ if (firstBlockIndex !== undefined) {
3074
+ return err({
3075
+ kind: "duplicate-block-identifier",
3076
+ slotKey: slotKey.value,
3077
+ identifier: parsed.value.identifier,
3078
+ firstBlockIndex,
3079
+ blockIndex
3080
+ });
3081
+ }
3082
+ blockIndexByIdentifier.set(parsed.value.identifier, blockIndex);
3083
+ }
3084
+ parsedBlocks.push(parsed.value);
3085
+ }
3086
+ slots.push({
3087
+ kind: "replace-slot",
3088
+ slotKey: slotKey.value,
3089
+ blocks: parsedBlocks
3090
+ });
3091
+ }
3092
+ return ok({
3093
+ kind: "patch-slots",
3094
+ slots
3095
+ });
3096
+ }
3097
+ function parseSdkEntrySlotBlockInput(input, slotKey, blockIndex) {
3098
+ if (!isRecord$2(input)) return err({
3099
+ kind: "invalid-block",
3100
+ slotKey,
3101
+ blockIndex
3102
+ });
3103
+ const blockKind = typeof input.kind === "string" ? input.kind.trim() : "";
3104
+ if (!blockKind || !isRecord$2(input.content)) return err({
3105
+ kind: "invalid-block",
3106
+ slotKey,
3107
+ blockIndex
3108
+ });
3109
+ if (isTemplateOnlyBlockKind(blockKind)) {
3110
+ return err({
3111
+ kind: "template-only-block-kind",
3112
+ slotKey,
3113
+ blockIndex,
3114
+ blockKind
3115
+ });
3116
+ }
3117
+ const identifier = input.identifier === undefined || input.identifier === null ? null : typeof input.identifier === "string" ? input.identifier.trim() : undefined;
3118
+ if (identifier === undefined || identifier === "") return err({
3119
+ kind: "invalid-block",
3120
+ slotKey,
3121
+ blockIndex
3122
+ });
3123
+ return ok({
3124
+ identifier,
3125
+ kind: blockKind,
3126
+ content: input.content
3127
+ });
3128
+ }
3129
+ function describeSdkEntryBlocksPatchParseError(error) {
3130
+ switch (error.kind) {
3131
+ case "invalid-blocks-object": return "Entry slot blocks payload must be an object";
3132
+ case "invalid-slot-key": return `Entry slot key is invalid: ${error.key}`;
3133
+ case "invalid-slot-blocks": return `Entry slot "${error.slotKey}" blocks must be an array`;
3134
+ case "invalid-block": return `Entry slot "${error.slotKey}" block at index ${error.blockIndex} is invalid`;
3135
+ case "duplicate-block-identifier": return `Entry slot "${error.slotKey}" has duplicate block identifier "${error.identifier}" at index ${error.blockIndex}; first used at index ${error.firstBlockIndex}`;
3136
+ case "template-only-block-kind": return `Entry slot "${error.slotKey}" block at index ${error.blockIndex} uses template-only block type: ${error.blockKind}`;
3137
+ }
3138
+ const exhaustive = error;
3139
+ return exhaustive;
3140
+ }
3141
+ function parseSlotContainerOwner(input) {
3142
+ if (!isRecord$2(input)) return err({ kind: "not-object" });
3143
+ if (input.kind === "contentEntry") {
3144
+ const contentEntryId = parseNonEmptyStringBrand(input.contentEntryId, unsafeAsEntrySlotContentEntryId);
3145
+ if (!contentEntryId.ok) {
3146
+ return err({
3147
+ kind: "invalid-content-entry-id",
3148
+ error: contentEntryId.error
3149
+ });
3150
+ }
3151
+ return ok({
3152
+ kind: "contentEntry",
3153
+ contentEntryId: contentEntryId.value
3154
+ });
3155
+ }
3156
+ if (input.kind === "pageSurface") {
3157
+ const pageSurfaceId = parseEntrySlotPageSurfaceId(input.pageSurfaceId);
3158
+ if (!pageSurfaceId.ok) {
3159
+ return err({
3160
+ kind: "invalid-page-surface-id",
3161
+ error: pageSurfaceId.error
3162
+ });
3163
+ }
3164
+ return ok({
3165
+ kind: "pageSurface",
3166
+ pageSurfaceId: pageSurfaceId.value
3167
+ });
3168
+ }
3169
+ return err({
3170
+ kind: "unsupported-owner-kind",
3171
+ ownerKind: input.kind
3172
+ });
3173
+ }
3174
+ function slotContainerOwnerToContentVersionOwner(owner) {
3175
+ return {
3176
+ kind: "entry",
3177
+ contentEntryId: owner.contentEntryId
3178
+ };
3179
+ }
3180
+ function slotContainerOwnerToEntrySlotLookup(owner) {
3181
+ return { contentEntryId: owner.contentEntryId };
3182
+ }
3183
+ function planSlotCompositionMutation(input) {
3184
+ switch (input.mutation.kind) {
3185
+ case "leaveUnchanged": return {
3186
+ kind: "noChange",
3187
+ owner: input.owner,
3188
+ slotKey: input.slotKey
3189
+ };
3190
+ case "replaceDraftComposition": return {
3191
+ kind: "replaceDraftComposition",
3192
+ owner: input.owner,
3193
+ slotKey: input.slotKey,
3194
+ blocks: input.mutation.blocks
3195
+ };
3196
+ case "publishDraftComposition": return {
3197
+ kind: "publishDraftComposition",
3198
+ owner: input.owner,
3199
+ slotKey: input.slotKey,
3200
+ draftVersionId: input.mutation.draftVersionId
3201
+ };
3202
+ default: return assertNever(input.mutation);
3203
+ }
3204
+ }
3205
+ function planSlotCompositionTransfer(input) {
3206
+ if (sameSlotCompositionOwner(input.from, input.to)) {
3207
+ return {
3208
+ kind: "noChange",
3209
+ owner: input.to,
3210
+ slotKey: input.slotKey
3211
+ };
3212
+ }
3213
+ if (input.to.kind !== "surfaceSlot") {
3214
+ return {
3215
+ kind: "manualReviewRequired",
3216
+ reason: {
3217
+ kind: "target-is-not-surface-slot",
3218
+ targetKind: input.to.kind
3219
+ }
3220
+ };
3221
+ }
3222
+ switch (input.from.kind) {
3223
+ case "entrySlot": return {
3224
+ kind: "copyEntrySlotToSurfaceSlot",
3225
+ from: input.from,
3226
+ to: input.to,
3227
+ slotKey: input.slotKey
3228
+ };
3229
+ case "surfaceSlot": return {
3230
+ kind: "forkSurfaceSlotComposition",
3231
+ from: input.from,
3232
+ to: input.to,
3233
+ slotKey: input.slotKey
3234
+ };
3235
+ default: return assertNever(input.from);
3236
+ }
3237
+ }
3238
+ function sameSlotCompositionOwner(left, right) {
3239
+ switch (left.kind) {
3240
+ case "entrySlot": return right.kind === "entrySlot" && right.entrySlotId === left.entrySlotId && right.contentEntryId === left.contentEntryId;
3241
+ case "surfaceSlot": return right.kind === "surfaceSlot" && right.surfaceSlotId === left.surfaceSlotId && right.pageSurfaceId === left.pageSurfaceId;
3242
+ default: return assertNever(left);
3243
+ }
3244
+ }
3245
+ const ENTRY_SLOT_TEMPLATE_LIFECYCLE_DECISION = {
3246
+ publicTemplateDefinitions: "effective-current",
3247
+ previewTemplateDefinitions: "draft-current",
3248
+ removedSlotData: "retained-hidden-until-slot-reintroduced"
3249
+ };
3250
+ function isRecord$2(value) {
3251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3252
+ }
3253
+ function assertNever(value) {
3254
+ throw new Error(`Unhandled entry-slot variant: ${JSON.stringify(value)}`);
3255
+ }
3256
+
3257
+ //#endregion
3258
+ //#region ../content-model/src/templates/defaults.ts
3259
+ const POST_EXCERPT_FALLBACK = fallbackBinding(fieldBinding("excerpt"), transformBinding(TEMPLATE_BINDING_TRANSFORMS.RICHTEXT_FIRST_PARAGRAPH, { input: fieldBinding("body") }));
3260
+ const EVENT_SUBHEADLINE_FALLBACK = fallbackBinding(fieldBinding("summary"), transformBinding(TEMPLATE_BINDING_TRANSFORMS.RICHTEXT_FIRST_PARAGRAPH, { input: fieldBinding("body") }));
3261
+ const COURSE_SUMMARY_FALLBACK = fallbackBinding(fieldBinding("summary"), transformBinding(TEMPLATE_BINDING_TRANSFORMS.RICHTEXT_FIRST_PARAGRAPH, { input: fieldBinding("body") }));
3262
+ const ENTRY_TITLE_INLINE_TEXT = transformBinding(TEMPLATE_BINDING_TRANSFORMS.TEXT_INLINE_TEXT, { input: metaBinding("title") });
3263
+ const POST_BODY_TEXT_BINDINGS = {
3264
+ body: fieldBinding("body"),
3265
+ alignment: literalBinding("left")
3266
+ };
3267
+ const EVENT_BODY_TEXT_BINDINGS = {
3268
+ body: fieldBinding("body"),
3269
+ alignment: literalBinding("left")
3270
+ };
3271
+ const TEAM_MEMBER_BODY_TEXT_BINDINGS = {
3272
+ body: fieldBinding("bio"),
3273
+ alignment: literalBinding("left")
3274
+ };
3275
+ const TESTIMONIAL_BODY_TEXT_BINDINGS = {
3276
+ body: fieldBinding("body"),
3277
+ alignment: literalBinding("center")
3278
+ };
3279
+ /**
3280
+ * Built-in content type keys that intentionally ship without a default
3281
+ * template — they use the block editor directly rather than a templated
3282
+ * render. Single source of truth for the carve-out so callers and tests
3283
+ * don't drift on which keys are "missing on purpose".
3284
+ */
3285
+ const CONTENT_TYPES_WITHOUT_DEFAULT_TEMPLATE = ["page", "product-category"];
3286
+ const DEFAULT_TEMPLATE_DEFINITIONS = {
3287
+ post: {
3288
+ templateKey: "default",
3289
+ name: "Blog post detail",
3290
+ description: "Default article layout for blog posts.",
3291
+ metadata: {},
3292
+ blocks: [
3293
+ {
3294
+ blockKind: "block.hero",
3295
+ scope: "entry",
3296
+ content: { _sectionStyles: { background: { type: "image" } } },
3297
+ bindings: {
3298
+ eyebrow: literalBinding("Article"),
3299
+ headline: metaBinding("title"),
3300
+ headlineInline: ENTRY_TITLE_INLINE_TEXT,
3301
+ subheadline: POST_EXCERPT_FALLBACK,
3302
+ "_sectionStyles.background.image": fieldBinding("image")
3303
+ }
3304
+ },
3305
+ {
3306
+ blockKind: "block.body-text",
3307
+ scope: "entry",
3308
+ bindings: POST_BODY_TEXT_BINDINGS
3309
+ },
3310
+ {
3311
+ blockKind: "block.cta-full",
3312
+ scope: "template",
3313
+ content: {
3314
+ eyebrow: "Stay in the loop",
3315
+ title: "Get the latest updates",
3316
+ content: createRichTextDoc(["Subscribe to our newsletter to receive new articles and resources right in your inbox."]),
3317
+ ctas: [{
3318
+ label: "Subscribe now",
3319
+ link: {
3320
+ kind: "url",
3321
+ href: "/contact"
3322
+ }
3323
+ }, {
3324
+ label: "View all posts",
3325
+ link: {
3326
+ kind: "url",
3327
+ href: "/blog"
3328
+ }
3329
+ }]
3330
+ }
3331
+ }
3332
+ ]
3333
+ },
3334
+ event: {
3335
+ templateKey: "default",
3336
+ name: "Event detail",
3337
+ description: "Standard detail page for events.",
3338
+ metadata: {},
3339
+ blocks: [
3340
+ {
3341
+ blockKind: "block.hero",
3342
+ scope: "entry",
3343
+ bindings: {
3344
+ eyebrow: literalBinding("Event"),
3345
+ headline: metaBinding("title"),
3346
+ headlineInline: ENTRY_TITLE_INLINE_TEXT,
3347
+ subheadline: EVENT_SUBHEADLINE_FALLBACK
3348
+ }
3349
+ },
3350
+ {
3351
+ blockKind: "block.event-details",
3352
+ scope: "entry",
3353
+ content: {
3354
+ heading: "Event details",
3355
+ showVenue: true,
3356
+ showMap: true,
3357
+ showOtherDates: true,
3358
+ otherDatesLimit: "5"
3359
+ }
3360
+ },
3361
+ {
3362
+ blockKind: "block.body-text",
3363
+ scope: "entry",
3364
+ bindings: EVENT_BODY_TEXT_BINDINGS
3365
+ },
3366
+ {
3367
+ blockKind: SYSTEM_ENTRY_SLOT_BLOCK_KIND,
3368
+ scope: "template",
3369
+ content: {
3370
+ slotKey: "main",
3371
+ label: "Page content"
3372
+ }
3373
+ },
3374
+ {
3375
+ blockKind: "block.event-registration",
3376
+ scope: "entry",
3377
+ content: {
3378
+ heading: "Register for this event",
3379
+ maxTickets: "5",
3380
+ showVenue: true,
3381
+ showCapacity: true,
3382
+ successMessage: "Thank you for registering! Check your email for confirmation details.",
3383
+ waitlistMessage: "You've been added to the waitlist. We'll email you a claim link if a spot opens up.",
3384
+ buttonText: "Complete Registration",
3385
+ buttonVariant: "primary"
3386
+ },
3387
+ bindings: {
3388
+ occurrenceContext: { $bind: { from: "$root.occurrenceContext" } },
3389
+ contentEntry: { $bind: { from: "$root.contentEntry" } }
3390
+ }
3391
+ }
3392
+ ]
3393
+ },
3394
+ course: {
3395
+ templateKey: "default",
3396
+ name: "Course detail",
3397
+ description: "Standard landing page for multi-session courses.",
3398
+ metadata: {},
3399
+ blocks: [
3400
+ {
3401
+ blockKind: "block.hero",
3402
+ scope: "entry",
3403
+ bindings: {
3404
+ eyebrow: literalBinding("Course"),
3405
+ headline: metaBinding("title"),
3406
+ headlineInline: ENTRY_TITLE_INLINE_TEXT,
3407
+ subheadline: COURSE_SUMMARY_FALLBACK
3408
+ }
3409
+ },
3410
+ {
3411
+ blockKind: "block.course-details",
3412
+ scope: "entry",
3413
+ content: {
3414
+ heading: "Course details",
3415
+ showTitle: false,
3416
+ showDescription: false,
3417
+ showSessions: true,
3418
+ sessionsLimit: "10",
3419
+ showVenue: true,
3420
+ showPrice: true,
3421
+ showCapacity: true,
3422
+ showEnrollmentStatus: true,
3423
+ showMembershipNote: true
3424
+ }
3425
+ },
3426
+ {
3427
+ blockKind: SYSTEM_ENTRY_SLOT_BLOCK_KIND,
3428
+ scope: "template",
3429
+ content: {
3430
+ slotKey: "main",
3431
+ label: "Page content"
3432
+ }
3433
+ },
3434
+ {
3435
+ blockKind: "block.course-registration",
3436
+ scope: "entry",
3437
+ content: {
3438
+ heading: "Enroll in this course",
3439
+ showSessions: true,
3440
+ showPrice: true,
3441
+ showCapacity: true,
3442
+ showVenue: true,
3443
+ successMessage: "You're enrolled! Check your email for confirmation and session details.",
3444
+ waitlistMessage: "You've been added to the waitlist. We'll email you a claim link if a spot opens up.",
3445
+ membershipMessage: "Enrolled using your membership! Check your email for session details.",
3446
+ buttonText: "Complete Enrollment",
3447
+ buttonVariant: "primary"
3448
+ }
3449
+ }
3450
+ ]
3451
+ },
3452
+ "team-member": {
3453
+ templateKey: "default",
3454
+ name: "Team member profile",
3455
+ description: "Default layout for team member profile pages.",
3456
+ metadata: {},
3457
+ blocks: [{
3458
+ blockKind: "block.hero",
3459
+ scope: "entry",
3460
+ bindings: {
3461
+ eyebrow: literalBinding("About"),
3462
+ headline: metaBinding("title"),
3463
+ headlineInline: ENTRY_TITLE_INLINE_TEXT,
3464
+ subheadline: fieldBinding("intro")
3465
+ }
3466
+ }, {
3467
+ blockKind: "block.body-text",
3468
+ scope: "entry",
3469
+ bindings: TEAM_MEMBER_BODY_TEXT_BINDINGS
3470
+ }]
3471
+ },
3472
+ product: {
3473
+ templateKey: "default",
3474
+ name: "Product detail",
3475
+ description: "Standard landing page for shop products.",
3476
+ metadata: {},
3477
+ blocks: [{
3478
+ blockKind: "block.product-detail",
3479
+ scope: "entry",
3480
+ content: {
3481
+ heading: "",
3482
+ showSummary: true,
3483
+ showBody: true,
3484
+ addButtonText: "Add to cart",
3485
+ emptyStateText: "Product unavailable."
3486
+ }
3487
+ }]
3488
+ },
3489
+ testimonial: {
3490
+ templateKey: "default",
3491
+ name: "Testimonial detail",
3492
+ description: "Rich layout for individual testimonials.",
3493
+ metadata: {},
3494
+ blocks: [{
3495
+ blockKind: "block.hero",
3496
+ scope: "entry",
3497
+ bindings: {
3498
+ eyebrow: literalBinding("Customer story"),
3499
+ headline: metaBinding("title"),
3500
+ headlineInline: ENTRY_TITLE_INLINE_TEXT,
3501
+ subheadline: fallbackBinding(fieldBinding("jobTitle"), fieldBinding("company"))
3502
+ }
3503
+ }, {
3504
+ blockKind: "block.body-text",
3505
+ scope: "entry",
3506
+ bindings: TESTIMONIAL_BODY_TEXT_BINDINGS
3507
+ }]
3508
+ }
3509
+ };
3510
+ function getDefaultTemplateDefinition(contentTypeKey) {
3511
+ return DEFAULT_TEMPLATE_DEFINITIONS[contentTypeKey] ?? null;
3512
+ }
3513
+ const DEFAULT_TEMPLATE_KEYS = Object.freeze(Object.keys(DEFAULT_TEMPLATE_DEFINITIONS));
3514
+
3515
+ //#endregion
3516
+ //#region ../content-model/src/templates/bindingTypeCompatibility.ts
3517
+ const SCALAR_BLOCK_FIELD_TYPES = [
3518
+ "text",
3519
+ "inlineText",
3520
+ "number",
3521
+ "slug",
3522
+ "url",
3523
+ "richText",
3524
+ "media",
3525
+ "file",
3526
+ "boolean",
3527
+ "select",
3528
+ "reference",
3529
+ "link",
3530
+ "date",
3531
+ "datetime"
3532
+ ];
3533
+ function isScalarBlockFieldType(type) {
3534
+ return SCALAR_BLOCK_FIELD_TYPES.includes(type);
3535
+ }
3536
+ /**
3537
+ * Compatibility matrix: content field type → compatible block field types
3538
+ */
3539
+ const CONTENT_TO_BLOCK_COMPATIBILITY = {
3540
+ text: [
3541
+ "text",
3542
+ "inlineText",
3543
+ "slug",
3544
+ "url"
3545
+ ],
3546
+ textarea: ["text", "inlineText"],
3547
+ number: [
3548
+ "number",
3549
+ "text",
3550
+ "inlineText"
3551
+ ],
3552
+ date: [
3553
+ "date",
3554
+ "datetime",
3555
+ "text",
3556
+ "inlineText"
3557
+ ],
3558
+ url: [
3559
+ "url",
3560
+ "text",
3561
+ "inlineText"
3562
+ ],
3563
+ richText: ["richText"],
3564
+ media: ["media"],
3565
+ file: ["file"],
3566
+ boolean: ["boolean"],
3567
+ select: [
3568
+ "text",
3569
+ "inlineText",
3570
+ "select"
3571
+ ],
3572
+ reference: ["reference", "link"],
3573
+ link: ["link", "reference"],
3574
+ button: ["link"],
3575
+ group: [],
3576
+ repeater: ["repeater"],
3577
+ datetime: [
3578
+ "datetime",
3579
+ "date",
3580
+ "text",
3581
+ "inlineText"
3582
+ ]
3583
+ };
3584
+ /**
3585
+ * Canonical meta-field registry.
3586
+ *
3587
+ * Single source of truth for "what entry meta fields exist and what content-field
3588
+ * type they resolve to". Other modules derive their meta-related tables from
3589
+ * this one to prevent the three-source-of-truth drift that previously existed
3590
+ * across `META_FIELD_COMPATIBILITY` (this file), `getAvailableFieldPaths`
3591
+ * (fieldPaths.ts), and `resolveMetaPathType` (bindingValidation.ts).
3592
+ */
3593
+ const META_FIELDS = {
3594
+ title: "text",
3595
+ slug: "text",
3596
+ status: "text",
3597
+ publishedAt: "date",
3598
+ createdAt: "date",
3599
+ updatedAt: "date"
3600
+ };
3601
+ /** Set of all known meta-field keys, derived from {@link META_FIELDS}. */
3602
+ const META_FIELD_IDS = new Set(Object.keys(META_FIELDS));
3603
+ /**
3604
+ * Resolve a meta path to its content-field type. Returns `null` for unknown paths.
3605
+ *
3606
+ * Use this when validating bindings; the returned type participates in the
3607
+ * standard `isCompatibleBinding` matrix. For target-aware compatibility
3608
+ * (which adds aliases like `title → slug → url`), use
3609
+ * {@link isCompatibleMetaBinding}.
3610
+ */
3611
+ function resolveMetaFieldType(path) {
3612
+ return isMetaFieldPath(path) ? META_FIELDS[path] : null;
3613
+ }
3614
+ /** Type guard narrowing `string` to the union of declared meta-field paths. */
3615
+ function isMetaFieldPath(path) {
3616
+ return path in META_FIELDS;
3617
+ }
3618
+ /**
3619
+ * Meta field → compatible block targets.
3620
+ *
3621
+ * Includes target-specific aliases that the generic field-type matrix
3622
+ * doesn't (e.g. `title` is text but is also a valid `slug`/`url` source).
3623
+ * Indexed by the same keys as {@link META_FIELDS} — adding a new meta field
3624
+ * means updating both maps.
3625
+ */
3626
+ const META_FIELD_COMPATIBILITY = {
3627
+ title: [
3628
+ "text",
3629
+ "inlineText",
3630
+ "slug",
3631
+ "url"
3632
+ ],
3633
+ slug: [
3634
+ "text",
3635
+ "inlineText",
3636
+ "slug",
3637
+ "url"
3638
+ ],
3639
+ status: [
3640
+ "text",
3641
+ "inlineText",
3642
+ "select"
3643
+ ],
3644
+ publishedAt: [
3645
+ "text",
3646
+ "inlineText",
3647
+ "date",
3648
+ "datetime"
3649
+ ],
3650
+ createdAt: [
3651
+ "text",
3652
+ "inlineText",
3653
+ "date",
3654
+ "datetime"
3655
+ ],
3656
+ updatedAt: [
3657
+ "text",
3658
+ "inlineText",
3659
+ "date",
3660
+ "datetime"
3661
+ ]
3662
+ };
3663
+ /**
3664
+ * Nested path suffixes and their compatible types (for example media.url).
3665
+ */
3666
+ const NESTED_PATH_COMPATIBILITY = {
3667
+ "media.url": [
3668
+ "text",
3669
+ "inlineText",
3670
+ "url"
3671
+ ],
3672
+ "media.alt": ["text", "inlineText"],
3673
+ "file.url": [
3674
+ "text",
3675
+ "inlineText",
3676
+ "url"
3677
+ ],
3678
+ "file.filename": ["text", "inlineText"]
3679
+ };
3680
+ /**
3681
+ * Check if a content field type is compatible with a block field type
3682
+ */
3683
+ function isCompatibleBinding(sourceType, targetType) {
3684
+ if (targetType === "repeater") {
3685
+ return sourceType === "repeater";
3686
+ }
3687
+ const compatibleTypes = CONTENT_TO_BLOCK_COMPATIBILITY[sourceType];
3688
+ if (compatibleTypes) {
3689
+ return compatibleTypes.includes(targetType);
3690
+ }
3691
+ return false;
3692
+ }
3693
+ /**
3694
+ * Check if a meta field is compatible with a block field type
3695
+ */
3696
+ function isCompatibleMetaBinding(metaFieldName, targetType) {
3697
+ if (targetType === "repeater") return false;
3698
+ if (!isMetaFieldPath(metaFieldName)) return false;
3699
+ const compatibleTypes = META_FIELD_COMPATIBILITY[metaFieldName];
3700
+ return compatibleTypes.includes(targetType);
3701
+ }
3702
+ /**
3703
+ * Check if a nested path is compatible with a block field type
3704
+ */
3705
+ function isCompatibleNestedPath(parentType, nestedSuffix, targetType) {
3706
+ if (targetType === "repeater") return false;
3707
+ const pathKey = `${parentType}.${nestedSuffix}`;
3708
+ const compatibleTypes = NESTED_PATH_COMPATIBILITY[pathKey];
3709
+ if (compatibleTypes) {
3710
+ return compatibleTypes.includes(targetType);
3711
+ }
3712
+ return false;
3713
+ }
3714
+ /**
3715
+ * Get compatible block field types for a given content field type
3716
+ */
3717
+ function getCompatibleBlockTypes(sourceType) {
3718
+ return CONTENT_TO_BLOCK_COMPATIBILITY[sourceType] || [];
3719
+ }
3720
+ /**
3721
+ * Get all compatible content field types for a given block field type (reverse lookup)
3722
+ */
3723
+ function getCompatibleContentTypes(targetType) {
3724
+ const compatible = [];
3725
+ for (const [contentType, blockTypes] of Object.entries(CONTENT_TO_BLOCK_COMPATIBILITY)) {
3726
+ if (blockTypes.includes(targetType)) {
3727
+ compatible.push(contentType);
3728
+ }
3729
+ }
3730
+ return compatible;
3731
+ }
3732
+
3733
+ //#endregion
3734
+ //#region ../content-model/src/templates/fieldPaths.ts
3735
+ const META_FIELD_LABELS = {
3736
+ title: "Title",
3737
+ slug: "Slug",
3738
+ status: "Status",
3739
+ publishedAt: "Published Date",
3740
+ createdAt: "Created Date",
3741
+ updatedAt: "Updated Date"
3742
+ };
3743
+ /**
3744
+ * Extract all available field paths from a content type schema
3745
+ * for use in binding editor autocomplete.
3746
+ */
3747
+ function getAvailableFieldPaths(schema) {
3748
+ const paths = [];
3749
+ for (const [path, type] of Object.entries(META_FIELDS)) {
3750
+ paths.push({
3751
+ path,
3752
+ label: META_FIELD_LABELS[path],
3753
+ type,
3754
+ category: "meta"
3755
+ });
3756
+ }
3757
+ paths.push(...contentFieldPaths(schema.fields ?? []));
3758
+ return paths;
3759
+ }
3760
+ function contentFieldPaths(fields, parentPath = "", parentLabel = "") {
3761
+ const paths = [];
3762
+ for (const field of fields) {
3763
+ const basePath = parentPath ? `${parentPath}.${field.id}` : field.id;
3764
+ const fieldLabel = field.label ?? field.id;
3765
+ const baseLabel = parentLabel ? `${parentLabel} / ${fieldLabel}` : fieldLabel;
3766
+ if (field.type === "group") {
3767
+ paths.push(...contentFieldPaths(nestedFields(field), basePath, baseLabel));
3768
+ continue;
3769
+ }
3770
+ if (field.type === "repeater") {
3771
+ continue;
3772
+ }
3773
+ if (field.type === "richText") {
3774
+ paths.push({
3775
+ path: basePath,
3776
+ label: baseLabel,
3777
+ type: "richText",
3778
+ category: "content"
3779
+ });
3780
+ continue;
3781
+ }
3782
+ if (field.type === "media") {
3783
+ paths.push({
3784
+ path: basePath,
3785
+ label: baseLabel,
3786
+ type: "media",
3787
+ category: "content"
3788
+ }, {
3789
+ path: `${basePath}.url`,
3790
+ label: `${baseLabel} (URL)`,
3791
+ type: "media.url",
3792
+ category: "content"
3793
+ }, {
3794
+ path: `${basePath}.alt`,
3795
+ label: `${baseLabel} (Alt Text)`,
3796
+ type: "media.alt",
3797
+ category: "content"
3798
+ });
3799
+ continue;
3800
+ }
3801
+ if (field.type === "file") {
3802
+ paths.push({
3803
+ path: basePath,
3804
+ label: baseLabel,
3805
+ type: "file",
3806
+ category: "content"
3807
+ }, {
3808
+ path: `${basePath}.url`,
3809
+ label: `${baseLabel} (URL)`,
3810
+ type: "file.url",
3811
+ category: "content"
3812
+ }, {
3813
+ path: `${basePath}.filename`,
3814
+ label: `${baseLabel} (Filename)`,
3815
+ type: "file.filename",
3816
+ category: "content"
3817
+ });
3818
+ continue;
3819
+ }
3820
+ paths.push({
3821
+ path: basePath,
3822
+ label: baseLabel,
3823
+ type: field.type,
3824
+ category: "content"
3825
+ });
3826
+ }
3827
+ return paths;
3828
+ }
3829
+ function nestedFields(field) {
3830
+ const record = field;
3831
+ return record.fields ?? record.schema?.fields ?? [];
3832
+ }
3833
+ /**
3834
+ * Get field type from path for validation
3835
+ */
3836
+ function getFieldTypeFromPath(schema, path) {
3837
+ const descriptor = getFieldDescriptorFromPath(schema, path);
3838
+ return descriptor?.kind === "scalar" ? descriptor.type : descriptor?.kind ?? null;
3839
+ }
3840
+ function getFieldDescriptorFromPath(schema, path) {
3841
+ const paths = getAvailableFieldPaths(schema);
3842
+ const match = paths.find((p) => p.path === path);
3843
+ if (match) return {
3844
+ kind: "scalar",
3845
+ type: match.type
3846
+ };
3847
+ const field = findContentFieldByPath(schema.fields ?? [], path.split("."));
3848
+ if (!field || field.type !== "repeater") return null;
3849
+ return {
3850
+ kind: "repeater",
3851
+ shape: buildRepeaterFieldShape(field)
3852
+ };
3853
+ }
3854
+ function findContentFieldByPath(fields, segments) {
3855
+ if (segments.length === 0) return null;
3856
+ const [head, ...tail] = segments;
3857
+ const field = fields.find((candidate) => candidate.id === head);
3858
+ if (!field) return null;
3859
+ if (tail.length === 0) return field;
3860
+ if (field.type !== "group") return null;
3861
+ return findContentFieldByPath(nestedFields(field), tail);
3862
+ }
3863
+ function buildRepeaterFieldShape(field) {
3864
+ const record = field;
3865
+ if (record.polymorphic || record.itemTypes) {
3866
+ return {
3867
+ kind: "unsupported",
3868
+ reason: "polymorphic_unsupported"
3869
+ };
3870
+ }
3871
+ const fields = nestedFields(field);
3872
+ if (fields.length === 0) {
3873
+ return {
3874
+ kind: "unsupported",
3875
+ reason: "missing_item_schema"
3876
+ };
3877
+ }
3878
+ const itemFields = [];
3879
+ for (const child of fields) {
3880
+ if (child.type === "repeater") {
3881
+ return {
3882
+ kind: "unsupported",
3883
+ reason: "nested_repeater_unsupported"
3884
+ };
3885
+ }
3886
+ if (!isRepeaterItemScalarType(child.type)) {
3887
+ return {
3888
+ kind: "unsupported",
3889
+ reason: "unsupported_item_field"
3890
+ };
3891
+ }
3892
+ itemFields.push({
3893
+ id: child.id,
3894
+ type: child.type,
3895
+ required: Boolean(child.required)
3896
+ });
3897
+ }
3898
+ return {
3899
+ kind: "supported",
3900
+ itemFields
3901
+ };
3902
+ }
3903
+ function isRepeaterItemScalarType(type) {
3904
+ switch (type) {
3905
+ case "text":
3906
+ case "textarea":
3907
+ case "richText":
3908
+ case "number":
3909
+ case "date":
3910
+ case "url":
3911
+ case "media":
3912
+ case "file":
3913
+ case "boolean":
3914
+ case "select":
3915
+ case "reference":
3916
+ case "link":
3917
+ case "button":
3918
+ case "datetime": return true;
3919
+ case "group":
3920
+ case "repeater": return false;
3921
+ default: {
3922
+ const _exhaustive = type;
3923
+ return _exhaustive;
3924
+ }
3925
+ }
3926
+ }
3927
+ /**
3928
+ * Validate if a path exists in the schema
3929
+ */
3930
+ function isValidFieldPath(schema, path) {
3931
+ const paths = getAvailableFieldPaths(schema);
3932
+ return paths.some((fieldPath) => fieldPath.path === path);
3933
+ }
3934
+ /**
3935
+ * Get field paths compatible with a specific block field type
3936
+ *
3937
+ * Filters available paths to only show those that can be safely bound
3938
+ * to the target field type without requiring transforms.
3939
+ */
3940
+ function getCompatibleFieldPaths(schema, targetFieldType) {
3941
+ const allPaths = getAvailableFieldPaths(schema);
3942
+ return allPaths.filter((fieldPath) => {
3943
+ if (fieldPath.category === "meta") {
3944
+ return isCompatibleMetaBinding(fieldPath.path, targetFieldType);
3945
+ }
3946
+ if (fieldPath.type.includes(".")) {
3947
+ const parts = fieldPath.type.split(".");
3948
+ const parentType = parts[0];
3949
+ const nestedSuffix = parts[1];
3950
+ if (parentType && nestedSuffix) {
3951
+ return isCompatibleNestedPath(parentType, nestedSuffix, targetFieldType);
3952
+ }
3953
+ return false;
3954
+ }
3955
+ return isCompatibleBinding(fieldPath.type, targetFieldType);
3956
+ });
3957
+ }
3958
+
3959
+ //#endregion
3960
+ //#region ../content-model/src/templates/bindingValidation.ts
3961
+ /**
3962
+ * What each transform accepts and produces. The `satisfies` clause forces
3963
+ * every `TemplateBindingTransformName` to have an entry — adding a new
3964
+ * transform name without an IO declaration is a compile error rather than
3965
+ * a runtime "unknown transform" error.
3966
+ *
3967
+ * Source-of-truth status:
3968
+ * - The names live in three places (`TRANSFORMS` in blocks, `BINDING_TRANSFORMS`
3969
+ * in sdk, `TEMPLATE_BINDING_TRANSFORMS` here) for package-boundary
3970
+ * reasons — blocks doesn't depend on content-model. A cross-package
3971
+ * equivalence test (`bindingValidation.test.ts`) pins the three sets
3972
+ * so drift fails CI rather than going unnoticed.
3973
+ * - The IO declaration lives only here; the runtime implementations in
3974
+ * `blocks/bindings/index.ts:applyTransform` are checked against this
3975
+ * table by the same cross-package test.
3976
+ */
3977
+ const TRANSFORM_IO = {
3978
+ [TEMPLATE_BINDING_TRANSFORMS.RICHTEXT_FIRST_PARAGRAPH]: {
3979
+ acceptsInputTypes: ["richText"],
3980
+ outputType: "text"
3981
+ },
3982
+ [TEMPLATE_BINDING_TRANSFORMS.DATE_FORMAT_SHORT]: {
3983
+ acceptsInputTypes: ["date", "datetime"],
3984
+ outputType: "text"
3985
+ },
3986
+ [TEMPLATE_BINDING_TRANSFORMS.DATE_FORMAT_LONG]: {
3987
+ acceptsInputTypes: ["date", "datetime"],
3988
+ outputType: "text"
3989
+ },
3990
+ [TEMPLATE_BINDING_TRANSFORMS.TEXT_INLINE_TEXT]: {
3991
+ acceptsInputTypes: [
3992
+ "text",
3993
+ "textarea",
3994
+ "select"
3995
+ ],
3996
+ outputType: "text"
3997
+ }
3998
+ };
3999
+ function getTransformIO(name) {
4000
+ return name in TRANSFORM_IO ? TRANSFORM_IO[name] : undefined;
4001
+ }
4002
+ /**
4003
+ * Resolve a meta path to its content-field type. Returns `null` for unknown
4004
+ * meta paths. Delegates entirely to the canonical {@link resolveMetaFieldType}
4005
+ * registry, which {@link getAvailableFieldPaths} also reads.
4006
+ */
4007
+ function resolveMetaPathType(path) {
4008
+ return resolveMetaFieldType(path);
4009
+ }
4010
+ function isRecord$1(value) {
4011
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4012
+ }
4013
+ function isBindingDescriptor(value) {
4014
+ if (!isRecord$1(value)) return false;
4015
+ const source = value.source;
4016
+ return source === "field" || source === "meta" || source === "literal" || source === "transform" || source === "fallback";
4017
+ }
4018
+ /**
4019
+ * Detect a root-context (`{$bind: {from: '$root.X'}}`) binding.
4020
+ *
4021
+ * These are block-internal data-wiring bindings, not user-authored entry-field
4022
+ * bindings. They appear on blocks like `block.event-registration` to plumb
4023
+ * ambient context (occurrence, content entry root, portal token) from the
4024
+ * renderer's data context into the block. The validator should NOT flag them
4025
+ * as `invalidBindingKey` — they're never expected to live on the manifest's
4026
+ * additionalFields list. Phase 9 will migrate this stored shape away.
4027
+ *
4028
+ * Matches:
4029
+ * - `{ $bind: { from: '$root.foo' } }`
4030
+ * - `{ $bind: { from: '$root.foo.bar' } }`
4031
+ *
4032
+ * Does NOT match field/meta-source bindings whose path happens to start with
4033
+ * `$root` — those use a `source` discriminator, not the `$bind` envelope.
4034
+ */
4035
+ function isRootContextBinding(value) {
4036
+ if (!isRecord$1(value)) return false;
4037
+ const bind = value.$bind;
4038
+ if (!isRecord$1(bind)) return false;
4039
+ return typeof bind.from === "string" && bind.from.startsWith("$root.");
4040
+ }
4041
+ /**
4042
+ * Check compatibility between a resolved source type (for example `'text'`,
4043
+ * `'richText'`, or `'media.url'`) and a target block field type.
4044
+ *
4045
+ * Handles three shapes of source type:
4046
+ * - plain content-field type (e.g. `'text'`)
4047
+ * - dotted nested suffix (e.g. `'media.url'`)
4048
+ * - meta-typed value (treated as plain content type, since meta types
4049
+ * already map cleanly into the matrix)
4050
+ */
4051
+ function isSourceCompatibleWithTarget(sourceType, targetType) {
4052
+ if (sourceType.includes(".")) {
4053
+ const [parent, suffix] = sourceType.split(".", 2);
4054
+ if (!parent || !suffix) return false;
4055
+ return isCompatibleNestedPath(parent, suffix, targetType);
4056
+ }
4057
+ return isCompatibleBinding(sourceType, targetType);
4058
+ }
4059
+ function resolveFieldSource(path, context) {
4060
+ const descriptor = getFieldDescriptorFromPath(context.schema, path);
4061
+ if (descriptor === null) {
4062
+ return {
4063
+ kind: "invalidPath",
4064
+ error: {
4065
+ kind: "invalidFieldPath",
4066
+ blockIdentifier: context.blockIdentifier,
4067
+ fieldKey: context.fieldKey,
4068
+ path
4069
+ }
4070
+ };
4071
+ }
4072
+ if (descriptor.kind === "repeater") {
4073
+ return {
4074
+ kind: "ok",
4075
+ type: "repeater",
4076
+ path,
4077
+ shape: descriptor.shape
4078
+ };
4079
+ }
4080
+ return {
4081
+ kind: "ok",
4082
+ type: descriptor.type,
4083
+ path
4084
+ };
4085
+ }
4086
+ function resolveMetaSource(path, context) {
4087
+ const type = resolveMetaPathType(path);
4088
+ if (type === null) {
4089
+ return {
4090
+ kind: "invalidPath",
4091
+ error: {
4092
+ kind: "invalidMetaPath",
4093
+ blockIdentifier: context.blockIdentifier,
4094
+ fieldKey: context.fieldKey,
4095
+ path
4096
+ }
4097
+ };
4098
+ }
4099
+ return {
4100
+ kind: "ok",
4101
+ type,
4102
+ path
4103
+ };
4104
+ }
4105
+ /**
4106
+ * Check a resolved source type against the target. Meta paths get the
4107
+ * target-aware aliases (e.g. `title` → `slug`/`url`); plain field paths use
4108
+ * the generic compatibility matrix.
4109
+ */
4110
+ function checkDirectCompatibility(resolved, context, isMetaSource) {
4111
+ if (isMetaSource) {
4112
+ if (isCompatibleMetaBinding(resolved.path, context.target.type)) return null;
4113
+ return {
4114
+ kind: "typeIncompatible",
4115
+ blockIdentifier: context.blockIdentifier,
4116
+ fieldKey: context.fieldKey,
4117
+ sourceType: `meta.${resolved.path}`,
4118
+ sourcePath: resolved.path,
4119
+ targetType: context.target.type,
4120
+ via: "direct"
4121
+ };
4122
+ }
4123
+ if (context.target.type === "repeater") {
4124
+ const mismatch = checkRepeaterCompatibility(resolved, context);
4125
+ return mismatch;
4126
+ }
4127
+ if (resolved.type === "repeater") {
4128
+ return {
4129
+ kind: "typeIncompatible",
4130
+ blockIdentifier: context.blockIdentifier,
4131
+ fieldKey: context.fieldKey,
4132
+ sourceType: describeResolvedSourceType(resolved),
4133
+ sourcePath: resolved.path,
4134
+ targetType: context.target.type,
4135
+ via: "direct"
4136
+ };
4137
+ }
4138
+ if (isSourceCompatibleWithTarget(resolved.type, context.target.type)) return null;
4139
+ return {
4140
+ kind: "typeIncompatible",
4141
+ blockIdentifier: context.blockIdentifier,
4142
+ fieldKey: context.fieldKey,
4143
+ sourceType: resolved.type,
4144
+ sourcePath: resolved.path,
4145
+ targetType: context.target.type,
4146
+ via: "direct"
4147
+ };
4148
+ }
4149
+ function describeResolvedSourceType(resolved) {
4150
+ if (resolved.type !== "repeater") return resolved.type;
4151
+ return resolved.shape?.kind === "unsupported" ? `repeater(${resolved.shape.reason})` : "repeater";
4152
+ }
4153
+ function describeTargetType(target) {
4154
+ return target.type;
4155
+ }
4156
+ function checkRepeaterCompatibility(resolved, context) {
4157
+ if (context.target.type !== "repeater") return null;
4158
+ const incompatible = (sourceType) => ({
4159
+ kind: "typeIncompatible",
4160
+ blockIdentifier: context.blockIdentifier,
4161
+ fieldKey: context.fieldKey,
4162
+ sourceType,
4163
+ sourcePath: resolved.path,
4164
+ targetType: describeTargetType(context.target),
4165
+ via: "direct"
4166
+ });
4167
+ if (resolved.type !== "repeater" || !resolved.shape) {
4168
+ return incompatible(describeResolvedSourceType(resolved));
4169
+ }
4170
+ const sourceShape = resolved.shape;
4171
+ const targetShape = context.target.shape;
4172
+ if (sourceShape.kind === "unsupported") {
4173
+ return incompatible(`repeater(${sourceShape.reason})`);
4174
+ }
4175
+ if (targetShape.kind === "unsupported") {
4176
+ return incompatible(`target-repeater(${targetShape.reason})`);
4177
+ }
4178
+ const sourceById = new Map(sourceShape.itemFields.map((field) => [field.id, field]));
4179
+ for (const targetField of targetShape.itemFields) {
4180
+ const sourceField = sourceById.get(targetField.id);
4181
+ if (!sourceField) {
4182
+ if (targetField.required) return incompatible("repeater(missing_required_target_field)");
4183
+ continue;
4184
+ }
4185
+ if (targetField.required && !sourceField.required) {
4186
+ return incompatible("repeater(requiredness_mismatch)");
4187
+ }
4188
+ if (!isScalarBlockFieldType(targetField.type) || !isSourceCompatibleWithTarget(sourceField.type, targetField.type)) {
4189
+ return incompatible(`repeater(type_mismatch:${sourceField.id})`);
4190
+ }
4191
+ }
4192
+ return null;
4193
+ }
4194
+ /**
4195
+ * Validate one binding value against one target field.
4196
+ *
4197
+ * Handles every shape `TemplateBindingValue` can take:
4198
+ * - descriptor (`field`/`meta`/`literal`/`transform`/`fallback`)
4199
+ * - string shorthand (treated as a `field` path)
4200
+ * - nested object (each key maps to a nested manifest target at
4201
+ * `${parentKey}.${childKey}`; missing nested targets are reported as
4202
+ * `invalidBindingKey` rather than silently inheriting the parent's type)
4203
+ */
4204
+ function validateBinding(binding, context) {
4205
+ if (typeof binding === "string") {
4206
+ const resolved = resolveFieldSource(binding, context);
4207
+ if (resolved.kind === "invalidPath") return [resolved.error];
4208
+ const mismatch = checkDirectCompatibility(resolved, context, false);
4209
+ return mismatch ? [mismatch] : [];
4210
+ }
4211
+ if (!isRecord$1(binding)) {
4212
+ return [{
4213
+ kind: "typeIncompatible",
4214
+ blockIdentifier: context.blockIdentifier,
4215
+ fieldKey: context.fieldKey,
4216
+ sourceType: typeof binding,
4217
+ targetType: context.target.type,
4218
+ via: "direct"
4219
+ }];
4220
+ }
4221
+ if (isBindingDescriptor(binding)) {
4222
+ return validateDescriptor(binding, context);
4223
+ }
4224
+ const errors = [];
4225
+ for (const [childKey, child] of Object.entries(binding)) {
4226
+ if (child === null || child === undefined) continue;
4227
+ const nestedKey = `${context.fieldKey}.${childKey}`;
4228
+ const nestedTarget = context.targetByKey.get(nestedKey);
4229
+ if (!nestedTarget) {
4230
+ errors.push({
4231
+ kind: "invalidBindingKey",
4232
+ blockIdentifier: context.blockIdentifier,
4233
+ blockKind: context.blockKind,
4234
+ fieldKey: nestedKey
4235
+ });
4236
+ continue;
4237
+ }
4238
+ errors.push(...validateBinding(child, {
4239
+ ...context,
4240
+ fieldKey: nestedKey,
4241
+ target: nestedTarget
4242
+ }));
4243
+ }
4244
+ return errors;
4245
+ }
4246
+ function validateDescriptor(binding, context) {
4247
+ switch (binding.source) {
4248
+ case "field": {
4249
+ const resolved = resolveFieldSource(binding.path, context);
4250
+ if (resolved.kind === "invalidPath") return [resolved.error];
4251
+ const mismatch = checkDirectCompatibility(resolved, context, false);
4252
+ return mismatch ? [mismatch] : [];
4253
+ }
4254
+ case "meta": {
4255
+ const resolved = resolveMetaSource(binding.path, context);
4256
+ if (resolved.kind === "invalidPath") return [resolved.error];
4257
+ const mismatch = checkDirectCompatibility(resolved, context, true);
4258
+ return mismatch ? [mismatch] : [];
4259
+ }
4260
+ case "literal": return [];
4261
+ case "transform": return validateTransform(binding, context);
4262
+ case "fallback": return readFallbackOptions(binding).flatMap((option) => validateBinding(option, { ...context }).map((err$1) => err$1.kind === "typeIncompatible" && err$1.via === "direct" ? {
4263
+ ...err$1,
4264
+ via: "fallbackOption"
4265
+ } : err$1));
4266
+ default: {
4267
+ const _exhaustive = binding;
4268
+ return _exhaustive;
4269
+ }
4270
+ }
4271
+ }
4272
+ function validateTransform(binding, context) {
4273
+ const io = getTransformIO(binding.transform);
4274
+ if (!io) {
4275
+ return [{
4276
+ kind: "unknownTransform",
4277
+ blockIdentifier: context.blockIdentifier,
4278
+ fieldKey: context.fieldKey,
4279
+ transform: binding.transform
4280
+ }];
4281
+ }
4282
+ const inputDescriptor = resolveTransformInput(binding);
4283
+ if (inputDescriptor === null) {
4284
+ return [{
4285
+ kind: "typeIncompatible",
4286
+ blockIdentifier: context.blockIdentifier,
4287
+ fieldKey: context.fieldKey,
4288
+ sourceType: "transform(no input)",
4289
+ targetType: context.target.type,
4290
+ via: "transformOutput"
4291
+ }];
4292
+ }
4293
+ const resolved = inputDescriptor.source === "field" ? resolveFieldSource(inputDescriptor.path, context) : inputDescriptor.source === "meta" ? resolveMetaSource(inputDescriptor.path, context) : null;
4294
+ if (resolved && resolved.kind === "invalidPath") {
4295
+ return [resolved.error];
4296
+ }
4297
+ const errors = [];
4298
+ if (resolved && resolved.kind === "ok") {
4299
+ const inputType = resolved.type;
4300
+ if (!io.acceptsInputTypes.includes(inputType)) {
4301
+ errors.push({
4302
+ kind: "transformInputIncompatible",
4303
+ blockIdentifier: context.blockIdentifier,
4304
+ fieldKey: context.fieldKey,
4305
+ transform: binding.transform,
4306
+ inputType,
4307
+ acceptedTypes: io.acceptsInputTypes
4308
+ });
4309
+ }
4310
+ }
4311
+ if (!isCompatibleBinding(io.outputType, context.target.type)) {
4312
+ errors.push({
4313
+ kind: "typeIncompatible",
4314
+ blockIdentifier: context.blockIdentifier,
4315
+ fieldKey: context.fieldKey,
4316
+ sourceType: io.outputType,
4317
+ targetType: context.target.type,
4318
+ via: "transformOutput"
4319
+ });
4320
+ }
4321
+ return errors;
4322
+ }
4323
+ function isTemplateBindingValueArray(value) {
4324
+ return Array.isArray(value);
4325
+ }
4326
+ function readFallbackOptions(binding) {
4327
+ if (isTemplateBindingValueArray(binding.options)) return binding.options;
4328
+ const legacy = binding.sources;
4329
+ return isTemplateBindingValueArray(legacy) ? legacy : [];
4330
+ }
4331
+ function resolveTransformInput(binding) {
4332
+ if (binding.input !== undefined) {
4333
+ if (typeof binding.input === "string") {
4334
+ return {
4335
+ source: "field",
4336
+ path: binding.input
4337
+ };
4338
+ }
4339
+ return binding.input;
4340
+ }
4341
+ if (typeof binding.path === "string") {
4342
+ return {
4343
+ source: "field",
4344
+ path: binding.path
4345
+ };
4346
+ }
4347
+ return null;
4348
+ }
4349
+ /**
4350
+ * Validate every binding on one block and assert that every required manifest
4351
+ * field has a binding present.
4352
+ */
4353
+ function validateBlockBindings(block, schema, manifest) {
4354
+ const blockIdentifier = block.identifier ?? block.blockKind;
4355
+ if (block.scope === "template") return [];
4356
+ const errors = [];
4357
+ const bindings = block.bindings ?? {};
4358
+ const targetByKey = new Map(manifest.fields.map((target) => [target.id, target]));
4359
+ for (const [fieldKey, binding] of Object.entries(bindings)) {
4360
+ if (binding === null || binding === undefined) continue;
4361
+ if (isRootContextBinding(binding)) continue;
4362
+ const target = targetByKey.get(fieldKey);
4363
+ if (!target) {
4364
+ errors.push({
4365
+ kind: "invalidBindingKey",
4366
+ blockIdentifier,
4367
+ blockKind: block.blockKind,
4368
+ fieldKey
4369
+ });
4370
+ continue;
4371
+ }
4372
+ errors.push(...validateBinding(binding, {
4373
+ blockIdentifier,
4374
+ fieldKey,
4375
+ target,
4376
+ schema,
4377
+ targetByKey,
4378
+ blockKind: block.blockKind
4379
+ }));
4380
+ }
4381
+ for (const target of manifest.fields) {
4382
+ if (!target.required) continue;
4383
+ const value = bindings[target.id];
4384
+ if (value === undefined || value === null) {
4385
+ errors.push({
4386
+ kind: "requiredBindingMissing",
4387
+ blockIdentifier,
4388
+ blockKind: block.blockKind,
4389
+ fieldKey: target.id
4390
+ });
4391
+ }
4392
+ }
4393
+ return errors;
4394
+ }
4395
+ /**
4396
+ * Validate every block in a template definition.
4397
+ *
4398
+ * Unknown block kinds are surfaced as `unknownBlockKind` errors and that
4399
+ * block is skipped for further checks (the rest of the template is still
4400
+ * validated, so callers see all problems at once).
4401
+ */
4402
+ function validateTemplateBindings(template, schema, getManifest) {
4403
+ const errors = [];
4404
+ for (const [index, block] of template.blocks.entries()) {
4405
+ const blockIdentifier = block.identifier ?? `${block.blockKind}#${index}`;
4406
+ if (block.scope === "template") continue;
4407
+ const manifest = getManifest(block.blockKind);
4408
+ if (!manifest) {
4409
+ errors.push({
4410
+ kind: "unknownBlockKind",
4411
+ blockIdentifier,
4412
+ blockKind: block.blockKind
4413
+ });
4414
+ continue;
4415
+ }
4416
+ errors.push(...validateBlockBindings({
4417
+ ...block,
4418
+ identifier: blockIdentifier
4419
+ }, schema, manifest));
4420
+ }
4421
+ return errors;
4422
+ }
4423
+ function matchBindingError(error, handlers) {
4424
+ const handler = handlers[error.kind];
4425
+ return handler(error);
4426
+ }
4427
+ /**
4428
+ * Human-readable rendering of a validation error. Suitable for editor
4429
+ * inline-error display and audit reports. Server-side error mappers should
4430
+ * use the error's `kind` discriminator (via `matchBindingError`) instead
4431
+ * of string-matching the message.
4432
+ */
4433
+ function bindingValidationErrorMessage(error) {
4434
+ return matchBindingError(error, {
4435
+ unknownBlockKind: (e) => `Unknown block kind "${e.blockKind}"`,
4436
+ invalidBindingKey: (e) => `Binding key "${e.fieldKey}" is not a field of block "${e.blockKind}"`,
4437
+ invalidFieldPath: (e) => `Field path "${e.path}" does not exist in the content type`,
4438
+ invalidMetaPath: (e) => `Meta path "${e.path}" is not a known entry meta field`,
4439
+ typeIncompatible: (e) => {
4440
+ const ctx = e.via === "transformOutput" ? " (transform output)" : e.via === "fallbackOption" ? " (fallback option)" : "";
4441
+ const pathSuffix = e.sourcePath ? ` (path "${e.sourcePath}")` : "";
4442
+ return `Source type "${e.sourceType}" is not compatible with target field type "${e.targetType}"${pathSuffix}${ctx}`;
4443
+ },
4444
+ unknownTransform: (e) => `Unknown transform "${e.transform}"`,
4445
+ transformInputIncompatible: (e) => `Transform "${e.transform}" does not accept input type "${e.inputType}" (accepts: ${e.acceptedTypes.join(", ")})`,
4446
+ requiredBindingMissing: (e) => `Required field "${e.fieldKey}" of block "${e.blockKind}" has no binding`
4447
+ });
4448
+ }
4449
+
4450
+ //#endregion
4451
+ //#region ../content-model/src/templates/manifestAdapter.ts
4452
+ const FIELD_TYPE_CLASSIFICATION = {
4453
+ text: "leaf",
4454
+ inlineText: "leaf",
4455
+ number: "leaf",
4456
+ slug: "leaf",
4457
+ url: "leaf",
4458
+ richText: "leaf",
4459
+ media: "leaf",
4460
+ file: "leaf",
4461
+ boolean: "leaf",
4462
+ select: "leaf",
4463
+ reference: "leaf",
4464
+ link: "leaf",
4465
+ date: "leaf",
4466
+ datetime: "leaf",
4467
+ group: "composite",
4468
+ modal: "composite",
4469
+ tabGroup: "composite",
4470
+ repeater: "whole",
4471
+ presetOrCustom: "opaque",
4472
+ contentTypeSelect: "opaque",
4473
+ entryPicker: "opaque",
4474
+ time: "opaque"
4475
+ };
4476
+ function classify(type) {
4477
+ return FIELD_TYPE_CLASSIFICATION[type] ?? "unknown";
4478
+ }
4479
+ function leafToBlockFieldType(type) {
4480
+ return classify(type) === "leaf" ? type : null;
4481
+ }
4482
+ /**
4483
+ * Recursively collect every bindable leaf in a field tree, emitting
4484
+ * dot-notated ids. `prefix` carries the parent path during recursion.
4485
+ */
4486
+ function collectLeaves(fields, prefix, out) {
4487
+ if (!fields) return;
4488
+ for (const field of fields) {
4489
+ const id = prefix ? `${prefix}.${field.id}` : field.id;
4490
+ const cls = classify(field.type);
4491
+ if (cls === "leaf") {
4492
+ out.push({
4493
+ id,
4494
+ type: field.type,
4495
+ required: Boolean(field.required)
4496
+ });
4497
+ continue;
4498
+ }
4499
+ if (cls === "whole") {
4500
+ out.push({
4501
+ id,
4502
+ type: "repeater",
4503
+ required: Boolean(field.required),
4504
+ shape: buildManifestRepeaterShape(field)
4505
+ });
4506
+ continue;
4507
+ }
4508
+ if (cls === "composite") {
4509
+ const groupChildren = field.schema?.fields;
4510
+ if (groupChildren) collectLeaves(groupChildren, id, out);
4511
+ for (const tab of field.tabs ?? []) {
4512
+ if (tab.fields) collectLeaves(tab.fields, id, out);
4513
+ }
4514
+ }
4515
+ void leafToBlockFieldType;
4516
+ }
4517
+ }
4518
+ function buildManifestRepeaterShape(field) {
4519
+ if (field.polymorphic || field.itemTypes) {
4520
+ return {
4521
+ kind: "unsupported",
4522
+ reason: "polymorphic_unsupported"
4523
+ };
4524
+ }
4525
+ const fields = field.repeatedItem?.fields ?? field.schema?.fields;
4526
+ if (!fields || fields.length === 0) {
4527
+ return {
4528
+ kind: "unsupported",
4529
+ reason: "missing_item_schema"
4530
+ };
4531
+ }
4532
+ const itemFields = [];
4533
+ for (const child of fields) {
4534
+ if (child.type === "repeater") {
4535
+ return {
4536
+ kind: "unsupported",
4537
+ reason: "nested_repeater_unsupported"
4538
+ };
4539
+ }
4540
+ const type = leafToBlockFieldType(child.type);
4541
+ if (!type) {
4542
+ return {
4543
+ kind: "unsupported",
4544
+ reason: "unsupported_item_field"
4545
+ };
4546
+ }
4547
+ itemFields.push({
4548
+ id: child.id,
4549
+ type,
4550
+ required: Boolean(child.required)
4551
+ });
4552
+ }
4553
+ return {
4554
+ kind: "supported",
4555
+ itemFields
4556
+ };
4557
+ }
4558
+ /**
4559
+ * Build the validator-facing shape from a block manifest. Composite fields
4560
+ * are walked; their leaves emerge with dot-notated ids. Opaque/unknown
4561
+ * fields are skipped.
4562
+ *
4563
+ * Accepts any value satisfying {@link AdaptableManifest}.
4564
+ */
4565
+ function manifestToValidationShape(manifest) {
4566
+ const fields = [];
4567
+ collectLeaves(manifest.fields, "", fields);
4568
+ return {
4569
+ id: manifest.name,
4570
+ fields
4571
+ };
4572
+ }
4573
+
4574
+ //#endregion
4575
+ //#region ../content-model/src/templates/auditBindings.ts
4576
+ /**
4577
+ * Build a zero-initialised counter map keyed on every error variant.
4578
+ *
4579
+ * The literal *is* the source of truth: adding a new variant to
4580
+ * `BindingValidationError` without a key here is a compile error. The
4581
+ * `ALL_ERROR_KINDS` derived list and the runtime exhaustiveness in
4582
+ * `emptyByKind` both flow from this single declaration.
4583
+ */
4584
+ function emptyByKind() {
4585
+ const counters = {
4586
+ unknownBlockKind: 0,
4587
+ invalidBindingKey: 0,
4588
+ invalidFieldPath: 0,
4589
+ invalidMetaPath: 0,
4590
+ typeIncompatible: 0,
4591
+ unknownTransform: 0,
4592
+ transformInputIncompatible: 0,
4593
+ requiredBindingMissing: 0
4594
+ };
4595
+ return counters;
4596
+ }
4597
+ const ALL_ERROR_KINDS = Object.keys(emptyByKind());
4598
+ /**
4599
+ * Audit one site's templates. Returns a report with every finding from
4600
+ * every template, plus a per-`kind` summary. Pure function: no I/O, no side
4601
+ * effects.
4602
+ */
4603
+ function auditSite(input, getManifest) {
4604
+ const findings = [];
4605
+ const byKind = emptyByKind();
4606
+ for (const ct of input.contentTypes) {
4607
+ for (const template of ct.templates) {
4608
+ const errors = validateTemplateBindings(template, ct.schema, getManifest);
4609
+ for (const error of errors) {
4610
+ findings.push({
4611
+ siteId: input.siteId,
4612
+ contentTypeKey: ct.contentTypeKey,
4613
+ templateKey: template.templateKey,
4614
+ templateName: template.name,
4615
+ error
4616
+ });
4617
+ byKind[error.kind] += 1;
4618
+ }
4619
+ }
4620
+ }
4621
+ return {
4622
+ siteId: input.siteId,
4623
+ findings,
4624
+ byKind
4625
+ };
4626
+ }
4627
+ function auditSites(inputs, getManifest) {
4628
+ const reports = inputs.map((input) => auditSite(input, getManifest));
4629
+ const totalsByKind = emptyByKind();
4630
+ let totalFindings = 0;
4631
+ for (const report of reports) {
4632
+ totalFindings += report.findings.length;
4633
+ for (const kind of ALL_ERROR_KINDS) {
4634
+ totalsByKind[kind] += report.byKind[kind];
4635
+ }
4636
+ }
4637
+ return {
4638
+ reports,
4639
+ totalFindings,
4640
+ totalsByKind
4641
+ };
4642
+ }
4643
+ /**
4644
+ * Render a `MultiSiteAuditReport` as a markdown document suitable for
4645
+ * committing to `.project/audits/`. Stable ordering: sites by id, findings
4646
+ * within a site by content-type → template → block-identifier → kind.
4647
+ */
4648
+ function formatAuditReportMarkdown(report, options) {
4649
+ const lines = [];
4650
+ const iso = options.generatedAt.toISOString();
4651
+ lines.push(`# Template binding audit — ${options.environment}`);
4652
+ lines.push("");
4653
+ lines.push(`Generated: ${iso}`);
4654
+ lines.push(`Total findings: **${report.totalFindings}**`);
4655
+ lines.push("");
4656
+ if (report.totalFindings === 0) {
4657
+ lines.push("No binding violations found. ✅");
4658
+ lines.push("");
4659
+ return lines.join("\n");
4660
+ }
4661
+ lines.push("## Summary by error kind");
4662
+ lines.push("");
4663
+ lines.push("| Kind | Count |");
4664
+ lines.push("| --- | ---: |");
4665
+ for (const kind of ALL_ERROR_KINDS) {
4666
+ const count = report.totalsByKind[kind];
4667
+ if (count > 0) lines.push(`| \`${kind}\` | ${count} |`);
4668
+ }
4669
+ lines.push("");
4670
+ for (const site of [...report.reports].sort((a, b) => a.siteId.localeCompare(b.siteId))) {
4671
+ if (site.findings.length === 0) continue;
4672
+ lines.push(`## Site \`${site.siteId}\` (${site.findings.length} finding${site.findings.length === 1 ? "" : "s"})`);
4673
+ lines.push("");
4674
+ const grouped = groupFindings(site.findings);
4675
+ for (const [contentTypeKey, templates] of grouped) {
4676
+ lines.push(`### Content type \`${contentTypeKey}\``);
4677
+ lines.push("");
4678
+ for (const [templateKey, findings] of templates) {
4679
+ const templateName = findings[0]?.templateName ?? "";
4680
+ lines.push(`**Template \`${templateKey}\`** — ${templateName}`);
4681
+ lines.push("");
4682
+ lines.push("| Block | Field | Kind | Detail |");
4683
+ lines.push("| --- | --- | --- | --- |");
4684
+ for (const finding of findings) {
4685
+ const err$1 = finding.error;
4686
+ const blockId = "blockIdentifier" in err$1 ? err$1.blockIdentifier : "-";
4687
+ const fieldKey = "fieldKey" in err$1 ? err$1.fieldKey : "-";
4688
+ const detail = describeError(err$1).replace(/\|/g, "\\|");
4689
+ lines.push(`| \`${blockId}\` | \`${fieldKey}\` | \`${err$1.kind}\` | ${detail} |`);
4690
+ }
4691
+ lines.push("");
4692
+ }
4693
+ }
4694
+ }
4695
+ return lines.join("\n");
4696
+ }
4697
+ /**
4698
+ * Group findings by content type → template, preserving stable ordering
4699
+ * inside each group. Pure helper for the formatter; exposed for tests.
4700
+ */
4701
+ function groupFindings(findings) {
4702
+ const byContentType = new Map();
4703
+ for (const finding of findings) {
4704
+ const inner = byContentType.get(finding.contentTypeKey) ?? new Map();
4705
+ const list = inner.get(finding.templateKey) ?? [];
4706
+ list.push(finding);
4707
+ inner.set(finding.templateKey, list);
4708
+ byContentType.set(finding.contentTypeKey, inner);
4709
+ }
4710
+ const ctEntries = [...byContentType.entries()].sort((a, b) => a[0].localeCompare(b[0]));
4711
+ return ctEntries.map(([ctKey, tplMap]) => {
4712
+ const tplEntries = [...tplMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
4713
+ return [ctKey, tplEntries.map(([tplKey, list]) => [tplKey, [...list].sort((a, b) => "blockIdentifier" in a.error && "blockIdentifier" in b.error ? a.error.blockIdentifier.localeCompare(b.error.blockIdentifier) || a.error.kind.localeCompare(b.error.kind) : a.error.kind.localeCompare(b.error.kind))])];
4714
+ });
4715
+ }
4716
+ /**
4717
+ * Audit-report rendering of a validation error.
4718
+ *
4719
+ * Delegates to the canonical `bindingValidationErrorMessage` so a single
4720
+ * source of truth covers both editor inline-display and audit report
4721
+ * surfaces — adding a new error variant updates both consumers from one
4722
+ * exhaustive switch.
4723
+ */
4724
+ const describeError = bindingValidationErrorMessage;
4725
+
4726
+ //#endregion
4727
+ //#region ../content-model/src/templates/editorState.ts
4728
+ function isRecord(value) {
4729
+ return !!value && typeof value === "object" && !Array.isArray(value);
4730
+ }
4731
+ function isTemplateEditorBindingDescriptor(value) {
4732
+ return typeof value === "string" || isRecord(value);
4733
+ }
4734
+ function normalizeTemplateEditorBindings(bindings) {
4735
+ const next = {};
4736
+ for (const [key, value] of Object.entries(bindings ?? {})) {
4737
+ if (!isTemplateEditorBindingDescriptor(value)) continue;
4738
+ next[key] = value;
4739
+ }
4740
+ return next;
4741
+ }
4742
+ function serializeTemplateEditorBindingsJson(bindings) {
4743
+ return JSON.stringify(bindings, null, 2);
4744
+ }
4745
+ function parseTemplateEditorBindingsJson(jsonValue) {
4746
+ try {
4747
+ const parsed = JSON.parse(jsonValue);
4748
+ if (!isRecord(parsed)) {
4749
+ return {
4750
+ ok: false,
4751
+ message: "Bindings must be an object"
4752
+ };
4753
+ }
4754
+ const bindings = normalizeTemplateEditorBindings(parsed);
4755
+ if (Object.keys(bindings).length !== Object.keys(parsed).length) {
4756
+ return {
4757
+ ok: false,
4758
+ message: "Binding values must be strings or objects"
4759
+ };
4760
+ }
4761
+ return {
4762
+ ok: true,
4763
+ bindings
4764
+ };
4765
+ } catch (error) {
4766
+ return {
4767
+ ok: false,
4768
+ message: error instanceof Error ? error.message : "Invalid JSON"
4769
+ };
4770
+ }
4771
+ }
4772
+ function mergeTemplateBindingEdits(currentBindings, editedBindings) {
4773
+ return {
4774
+ ...currentBindings,
4775
+ ...editedBindings
4776
+ };
4777
+ }
4778
+ function setTemplateEditorBinding(bindings, propId, binding) {
4779
+ return {
4780
+ ...bindings,
4781
+ [propId]: binding
4782
+ };
4783
+ }
4784
+ function removeTemplateEditorBinding(bindings, propId) {
4785
+ const next = { ...bindings };
4786
+ delete next[propId];
4787
+ return next;
4788
+ }
4789
+ function createEntryTemplateBindingRecords(blocks) {
4790
+ const next = {};
4791
+ for (const block of blocks) {
4792
+ if (block.scope !== "entry") continue;
4793
+ next[block.id] = normalizeTemplateEditorBindings(block.bindings);
4794
+ }
4795
+ return next;
4796
+ }
4797
+ function setTemplateBlockBinding(allBindings, blockId, propId, binding) {
4798
+ return {
4799
+ ...allBindings,
4800
+ [blockId]: setTemplateEditorBinding(allBindings[blockId] ?? {}, propId, binding)
4801
+ };
4802
+ }
4803
+ function removeTemplateBlockBinding(allBindings, blockId, propId) {
4804
+ return {
4805
+ ...allBindings,
4806
+ [blockId]: removeTemplateEditorBinding(allBindings[blockId] ?? {}, propId)
4807
+ };
4808
+ }
4809
+
4810
+ //#endregion
4811
+ //#region ../content-model/src/entry-slots/sdkBlocks.ts
4812
+ const defaultContentSchema = z.record(z.string(), z.unknown());
4813
+ function createSdkEntrySlotBlockInputSchema(contentSchema = defaultContentSchema) {
4814
+ return z.object({
4815
+ identifier: z.string().nullable().optional(),
4816
+ kind: z.string().min(1, "Block kind is required"),
4817
+ content: contentSchema
4818
+ });
4819
+ }
4820
+ function createSdkEntrySlotBlocksInputSchema(contentSchema) {
4821
+ return z.record(z.string(), z.array(createSdkEntrySlotBlockInputSchema(contentSchema)));
4822
+ }
4823
+ const sdkEntrySlotBlockInputSchema = createSdkEntrySlotBlockInputSchema();
4824
+ const sdkEntrySlotBlocksInputSchema = createSdkEntrySlotBlocksInputSchema();
4825
+
4826
+ //#endregion
4827
+ //#region src/commands/eventUpsertInputSchema.ts
4828
+ const eventScheduleInputSchema = z.object({
4829
+ recurrenceRule: z.string().nullable().optional(),
4830
+ startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(),
4831
+ defaultStartTime: z.string().min(1).optional(),
4832
+ durationMinutes: z.number().int().positive().optional(),
4833
+ startsAt: z.string().min(1).optional(),
4834
+ endsAt: z.string().min(1).optional()
4835
+ }).superRefine((schedule, ctx) => {
4836
+ const result = classifySdkEventSchedule(schedule, { requireCanonicalFields: true });
4837
+ if (result.ok) return;
4838
+ result.issues.forEach((issue) => ctx.addIssue({
4839
+ code: "custom",
4840
+ path: [...issue.path],
4841
+ message: issue.message
4842
+ }));
4843
+ }).transform((schedule) => {
4844
+ const result = classifySdkEventSchedule(schedule, { requireCanonicalFields: true });
4845
+ if (!result.ok) throw new Error("Unreachable invalid event schedule");
4846
+ if (result.kind === "start-end") {
4847
+ if (typeof schedule.startsAt !== "string" || typeof schedule.endsAt !== "string") throw new Error("Unreachable invalid start/end event schedule");
4848
+ return {
4849
+ startsAt: schedule.startsAt,
4850
+ endsAt: schedule.endsAt
4851
+ };
4852
+ }
4853
+ if (result.kind === "start-duration") {
4854
+ if (typeof schedule.startsAt !== "string" || typeof schedule.durationMinutes !== "number") throw new Error("Unreachable invalid start/duration event schedule");
4855
+ return {
4856
+ startsAt: schedule.startsAt,
4857
+ durationMinutes: schedule.durationMinutes
4858
+ };
4859
+ }
4860
+ return result.canonical;
4861
+ });
4862
+ function classifiedSchema(classify$1) {
4863
+ return z.unknown().superRefine((input, ctx) => {
4864
+ const result = classify$1(input);
4865
+ if (result.ok) return;
4866
+ result.issues.forEach((issue) => ctx.addIssue({
4867
+ code: "custom",
4868
+ path: issue.path.map(String),
4869
+ message: issue.message
4870
+ }));
4871
+ }).transform((input) => {
4872
+ const result = classify$1(input);
4873
+ if (!result.ok) throw new Error("Unreachable invalid classified input");
4874
+ return result.value;
4875
+ });
4876
+ }
4877
+ const eventPricingInputSchema = classifiedSchema((input) => {
4878
+ const result = classifySdkEventPricing(input);
4879
+ return result.ok ? {
4880
+ ok: true,
4881
+ value: result.pricing
4882
+ } : result;
4883
+ });
4884
+ const eventPaymentTermsInputSchema = classifiedSchema((input) => {
4885
+ const result = classifySdkEventPaymentTerms(input);
4886
+ return result.ok ? {
4887
+ ok: true,
4888
+ value: result.paymentTerms
4889
+ } : result;
4890
+ });
4891
+ const eventRunInputSchema = z.object({
4892
+ identifier: z.string().min(1),
4893
+ eventRef: z.string().min(1),
4894
+ name: z.string().nullable().optional(),
4895
+ status: sdkEventUpsertOptionalFieldSchemas.status,
4896
+ visibility: sdkEventUpsertOptionalFieldSchemas.visibility,
4897
+ bookingScope: z.enum(["single_occurrence", "whole_series"]),
4898
+ occurrences: sdkEventUpsertOptionalFieldSchemas.occurrences.unwrap(),
4899
+ schedule: eventScheduleInputSchema.optional(),
4900
+ pricing: eventPricingInputSchema.optional(),
4901
+ paymentTerms: eventPaymentTermsInputSchema.optional(),
4902
+ capacity: sdkEventUpsertOptionalFieldSchemas.capacity,
4903
+ venueRef: z.string().nullable().optional(),
4904
+ categoryRef: z.string().nullable().optional(),
4905
+ formRef: z.string().nullable().optional(),
4906
+ settings: z.record(z.string(), z.unknown()).optional(),
4907
+ presentation: z.record(z.string(), z.unknown()).nullable().optional(),
4908
+ tags: z.array(z.string()).optional()
4909
+ });
4910
+ const eventUpsertInputSchema = z.object({
4911
+ identifier: z.string().min(1).optional(),
4912
+ title: z.string().min(1, "title is required"),
4913
+ slug: z.string().min(1).optional(),
4914
+ content: z.record(z.string(), z.unknown()).optional(),
4915
+ blocks: sdkEntrySlotBlocksInputSchema.optional(),
4916
+ preset: z.enum([
4917
+ "event",
4918
+ "class",
4919
+ "course",
4920
+ "workshop",
4921
+ "retreat",
4922
+ "performance"
4923
+ ]),
4924
+ runRefs: z.tuple([z.string().min(1)], z.string().min(1)),
4925
+ runs: z.tuple([eventRunInputSchema], eventRunInputSchema)
4926
+ }).superRefine((input, ctx) => input.runs.forEach((run, index) => {
4927
+ if (new Set(run.occurrences.map((item) => item.timeZone)).size <= 1) return;
4928
+ ctx.addIssue({
4929
+ code: "custom",
4930
+ path: [
4931
+ "runs",
4932
+ index,
4933
+ "occurrences"
4934
+ ],
4935
+ message: "all occurrences must use the same timeZone"
4936
+ });
4937
+ }));
4938
+
4939
+ //#endregion
4940
+ export { RichTextDocSchema as a, isRecord$3 as c, findUnfilledSentinels as i, buildEntryScaffold as n, isRichTextDoc as o, validateEntryInstance as r, isObjectRecord as s, eventUpsertInputSchema as t };