@weatherboard/cms 0.7.0 → 0.8.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/server.ts", "../src/portable/array.ts", "../src/portable/menu-options.ts", "../src/portable/menu.ts", "../src/portable/site-content.ts", "../src/contracts/query.ts", "../src/runtime/reader.ts"],
4
- "sourcesContent": ["import \"server-only\";\nexport { createCmsReader } from \"./runtime/reader.js\";\nexport type {\n CmsFailure, CmsDiagnostic, CmsFetch, CmsReadPolicy, CmsAdapterResult,\n CmsAdapter, CmsReadResult, CmsReaderOptions, CmsReader,\n} from \"./runtime/reader.js\";\nexport { MENU_LAYOUTS, MENU_DECORATIONS, MENU_LAYOUT_DEFAULTS, parseMenuDocument, menuTags } from \"./portable/menu.js\";\nexport { menuQuery } from \"./contracts/query.js\";\nexport type { MenuDocument, MenuVariant, MenuSection, MenuItem, MenuPrice, MenuExtra, MenuMoney, MenuLayout, MenuDecoration, MenuSectionInput } from \"./portable/menu.js\";\n", "/** Content lists must contain an own entry at every index. */\nexport function isDenseArray(value: unknown): value is unknown[] {\n if (!Array.isArray(value)) return false;\n for (let index = 0; index < value.length; index += 1) {\n if (!Object.hasOwn(value, index)) return false;\n }\n return true;\n}\n", "export const MENU_LAYOUTS = [\"stacked\", \"columns\"] as const;\nexport const MENU_DECORATIONS = [\"none\", \"playful\"] as const;\nexport const MENU_LAYOUT_DEFAULTS = { layout: \"stacked\", decoration: \"none\" } as const;\n\n/** Explicit saved fields, shared by the standalone query and page dereference. */\nexport const MENU_DOCUMENT_PROJECTION = `_type, _id, _rev, _updatedAt, title, locationKeys, note, signOff,\n variants[]{_key, label, note, sections[]{_key, title, service, items[]{\n _key, name, description, priceNote, dietary, allergenNote, availability,\n prices[]{_key, label, amount{minor, currency}}, extras[]{_key, name, amount{minor, currency}}\n }}}`;\n\n", "import { isDenseArray } from \"./array.js\";\nimport { MalformedContentError } from \"./site-content.js\";\nimport { cmsTags } from \"../contracts/query.js\";\n\nimport { MENU_LAYOUTS, MENU_DECORATIONS } from \"./menu-options.js\";\nexport { MENU_LAYOUTS, MENU_DECORATIONS, MENU_LAYOUT_DEFAULTS } from \"./menu-options.js\";\nexport type MenuLayout = (typeof MENU_LAYOUTS)[number];\nexport type MenuDecoration = (typeof MENU_DECORATIONS)[number];\n\n/** CMS-owned saved shapes. `_key` remains the mapper's stable array identity. */\nexport type MenuMoney = { readonly minor: number; readonly currency: string };\nexport type MenuPrice = { readonly _key: string; readonly amount: MenuMoney; readonly label?: string };\nexport type MenuExtra = { readonly _key: string; readonly name: string; readonly amount: MenuMoney };\nexport type MenuItem = {\n readonly _key: string; readonly name: string; readonly description?: string;\n readonly prices: readonly MenuPrice[]; readonly priceNote?: string;\n readonly extras: readonly MenuExtra[]; readonly dietary: readonly string[];\n readonly allergenNote?: string; readonly availability: \"available\" | \"sold-out\" | \"hidden\";\n};\nexport type MenuSection = { readonly _key: string; readonly title: string; readonly service?: string; readonly items: readonly MenuItem[] };\nexport type MenuVariant = { readonly _key: string; readonly label: string; readonly note?: string; readonly sections: readonly MenuSection[] };\nexport type MenuDocument = {\n /** The schema discriminator is required by the consuming site's mapper. */\n readonly _type: \"menu\";\n readonly _id: string; readonly _rev: string;\n /** The real published update date, preserved verbatim for stale displays. */\n readonly _updatedAt: string;\n readonly title: string; readonly locationKeys: readonly string[];\n readonly variants: readonly MenuVariant[]; readonly note?: string;\n /** Approval is opaque to this reader. The site validates it with its contract. */\n readonly signOff?: unknown;\n};\nexport type MenuSectionInput = { readonly kind: \"menu\"; readonly layout: MenuLayout; readonly decoration: MenuDecoration; readonly menu: MenuDocument };\n\n/** Uses the same scoped type/id tags as every other CMS query. */\nexport function menuTags(scope: string, id?: string): readonly string[] {\n return cmsTags(scope, { types: [\"menu\"], ...(id === undefined ? {} : { ids: [id] }) });\n}\n\ntype Raw = Record<string, unknown>;\nfunction fail(path: string, reason: string): never {\n throw new MalformedContentError(\"menu\", [`${path} ${reason}`]);\n}\n/** Capture each own data property once; never invoke an accessor or caller get trap. */\nfunction snapshot(value: unknown, path: string, ancestors = new Set<object>(), identity?: PropertyDescriptor): unknown {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"object\" || value === null) return fail(path, \"is not plain JSON data\");\n try {\n if (ancestors.has(value)) return fail(path, \"is not acyclic JSON data\");\n const array = Array.isArray(value);\n const prototype: unknown = Object.getPrototypeOf(value);\n if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {\n return fail(path, \"is not an ordinary saved object\");\n }\n ancestors.add(value);\n const keys = Reflect.ownKeys(value);\n const copied: Record<string, unknown> | unknown[] = array ? [] : {};\n let length = 0;\n let entries = 0;\n for (const key of keys) {\n if (typeof key !== \"string\") return fail(path, \"has a symbol key\");\n const at = array && key !== \"length\" ? `${path}[${key}]` : `${path}.${key}`;\n let descriptor: PropertyDescriptor | undefined;\n try { descriptor = key === \"_id\" && identity !== undefined ? identity : Object.getOwnPropertyDescriptor(value, key); }\n catch { return fail(at, \"could not be snapshotted as plain JSON data\"); }\n if (!descriptor || !Object.hasOwn(descriptor, \"value\")) return fail(at, \"is not a saved data property\");\n if (array && key === \"length\") {\n const savedLength: unknown = descriptor.value;\n if (typeof savedLength !== \"number\" || !Number.isSafeInteger(savedLength) || savedLength < 0) return fail(at, \"is not a saved list length\");\n length = savedLength;\n continue;\n }\n if (!descriptor.enumerable || (array && !/^(0|[1-9]\\d*)$/.test(key))) return fail(at, \"is not a JSON data property\");\n const saved: unknown = descriptor.value;\n const entry = snapshot(saved, at, ancestors);\n Object.defineProperty(copied, key, { value: entry, enumerable: true, writable: true, configurable: true });\n entries++;\n }\n if (array && (entries !== length || (copied as unknown[]).length !== length)) return fail(path, \"is not a complete saved list\");\n return copied;\n } catch (error) {\n if (error instanceof MalformedContentError) throw error;\n return fail(path, \"could not be snapshotted as plain JSON data\");\n } finally {\n ancestors.delete(value);\n }\n}\nfunction object(value: unknown, path: string): Raw {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)\n || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) return fail(path, \"is not an ordinary saved object\");\n return value as Raw;\n}\nfunction text(value: unknown, path: string, required = true): string {\n if (typeof value !== \"string\" || (required && value.trim() === \"\")) return fail(path, \"is not saved text\");\n return value;\n}\nfunction optional(raw: Raw, field: string, path: string): Record<string, string> {\n return raw[field] === undefined || raw[field] === null ? {} : { [field]: text(raw[field], `${path}.${field}`, false) };\n}\nfunction list<T>(value: unknown, path: string, read: (value: unknown, path: string) => T): T[] {\n if (value === undefined || value === null) return [];\n if (!isDenseArray(value)) return fail(path, \"is not a complete saved list\");\n return value.map((entry, index) => read(entry, `${path}[${String(index)}]`));\n}\nfunction money(value: unknown, path: string): MenuMoney {\n const raw = object(value, path);\n if (typeof raw.minor !== \"number\" || !Number.isSafeInteger(raw.minor) || raw.minor < 0) return fail(`${path}.minor`, \"is not a whole non-negative safe amount\");\n const currency = text(raw.currency, `${path}.currency`);\n if (!/^[A-Z]{3}$/.test(currency)) return fail(`${path}.currency`, \"is not a three-letter currency code\");\n return { minor: raw.minor, currency };\n}\nfunction price(value: unknown, path: string): MenuPrice {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), amount: money(raw.amount, `${path}.amount`), ...optional(raw, \"label\", path) };\n}\nfunction extra(value: unknown, path: string): MenuExtra {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), name: text(raw.name, `${path}.name`), amount: money(raw.amount, `${path}.amount`) };\n}\nfunction item(value: unknown, path: string): MenuItem {\n const raw = object(value, path);\n const availability = raw.availability ?? \"available\";\n if (availability !== \"available\" && availability !== \"sold-out\" && availability !== \"hidden\") return fail(`${path}.availability`, \"is not an availability choice\");\n return {\n _key: text(raw._key, `${path}._key`), name: text(raw.name, `${path}.name`),\n ...optional(raw, \"description\", path), ...optional(raw, \"priceNote\", path), ...optional(raw, \"allergenNote\", path),\n prices: list(raw.prices, `${path}.prices`, price), extras: list(raw.extras, `${path}.extras`, extra),\n dietary: list(raw.dietary, `${path}.dietary`, text), availability,\n };\n}\nfunction section(value: unknown, path: string): MenuSection {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), title: text(raw.title, `${path}.title`), ...optional(raw, \"service\", path), items: list(raw.items, `${path}.items`, item) };\n}\nfunction variant(value: unknown, path: string): MenuVariant {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), label: text(raw.label, `${path}.label`), ...optional(raw, \"note\", path), sections: list(raw.sections, `${path}.sections`, section) };\n}\n\n/** A reference has resolved only when it carries a published document identity. */\nfunction isPublishedMenuDocument(value: unknown): boolean {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return false;\n const id = (value as Raw)._id;\n return typeof id === \"string\" && id.trim() !== \"\" && !/^(drafts|versions)\\./.test(id);\n}\n\n/** Validate saved field shapes without inferring approval or changing copy/order. */\nexport function parseMenuDocument(document: unknown, where = \"menu\"): MenuDocument {\n return parseSnapshot(snapshot(document, where), where);\n}\n\n/** Page references use the same single snapshot before checking their identity. */\nexport function parseReferencedMenuDocument(document: unknown, where: string): MenuDocument | null {\n if (document === undefined || document === null) return null;\n if (typeof document !== \"object\") return null;\n // Missing/draft references remain warnings. Cache the identity descriptor so\n // an available document still contributes each caller property exactly once.\n let identity: PropertyDescriptor | undefined;\n try { identity = Object.getOwnPropertyDescriptor(document, \"_id\"); }\n catch { return fail(`${where}._id`, \"could not be snapshotted as plain JSON data\"); }\n if (identity && !Object.hasOwn(identity, \"value\")) return fail(`${where}._id`, \"is not a saved data property\");\n const id: unknown = identity?.value;\n if (typeof id !== \"string\" || id.trim() === \"\" || /^(drafts|versions)\\./.test(id)) return null;\n return parseSnapshot(snapshot(document, where, new Set(), identity), where);\n}\n\nfunction parseSnapshot(document: unknown, where: string): MenuDocument {\n const raw = object(document, where);\n if (raw._type !== \"menu\") return fail(`${where}._type`, \"is not menu\");\n if (!isPublishedMenuDocument(raw)) return fail(`${where}._id`, \"is not a published menu identity\");\n const updatedAt = text(raw._updatedAt, `${where}._updatedAt`);\n if (!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/.test(updatedAt) || !Number.isFinite(Date.parse(updatedAt))) return fail(`${where}._updatedAt`, \"is not an update date\");\n const variants = list(raw.variants, `${where}.variants`, variant);\n if (variants.length === 0) return fail(`${where}.variants`, \"has no menu choices\");\n return {\n _type: \"menu\", _id: text(raw._id, `${where}._id`), _rev: text(raw._rev, `${where}._rev`), _updatedAt: updatedAt,\n title: text(raw.title, `${where}.title`), locationKeys: list(raw.locationKeys, `${where}.locationKeys`, text),\n variants, ...optional(raw, \"note\", where), ...(Object.hasOwn(raw, \"signOff\") ? { signOff: raw.signOff } : {}),\n };\n}\n\nexport function validMenuFallback(value: unknown): value is MenuDocument | null {\n if (value === null) return true;\n try { parseMenuDocument(value); return true; } catch { return false; }\n}\n", "/**\n * A-1713. The site content model, read: the home page, content pages and site\n * settings, validated on read and returned in the shape a site's page\n * composition consumes.\n *\n * ===========================================================================\n * READ ONLY, BY CONSTRUCTION\n * ===========================================================================\n *\n * A client site reads its content with a read-only token. All editing goes\n * through the studio's dashboard, which writes to Sanity; nothing on a client\n * site writes, and this module has no write path to call. It is handed a\n * `ContentClient` (a `query` function) and nothing else.\n *\n * ===========================================================================\n * VALIDATED ON READ, BECAUSE SANITY VALIDATION ONLY ADVISES\n * ===========================================================================\n *\n * The schema's rules warn an editor; they do not refuse a publish, and\n * anything writing over the API skips them entirely. So every document is\n * checked here again, and a malformed one is refused BY NAME\n * (`MalformedContentError`, code `malformed-content`) listing every problem\n * with its path, rather than rendered half-empty. A missing home headline\n * uses siteHero only when the home document explicitly selects it and that\n * document supplies a valid heading. Otherwise the home page is refused.\n *\n * Absent is not malformed: `null` from `home()` means nobody has created the\n * home page yet, and a site decides what to do about that.\n *\n * ===========================================================================\n * THE SHAPE IT RETURNS\n * ===========================================================================\n *\n * The INPUT shape of a site's composition schema (the reference build's\n * frontmatter): a copy value is a plain string for the client's own words\n * with no claim about their source, `{ text, copySource: \"captured\" }` for\n * words claimed verbatim from the capture (the site checks every such claim\n * against its capture evidence, so the claim must survive Sanity), and\n * `{ text, copySource: \"proposed\" }` for studio-written words awaiting\n * approval. All three round-trip exactly; a photo is its library slug; every key a block declares is\n * present, `null` where there is nothing. So a site validates this with the\n * same schema it validates its Markdown frontmatter with, and its components\n * never know where the content came from.\n *\n * Mapping from the generic model to the composition's names:\n *\n * hero.headline / subline / lead hero.heading / subline / intro\n * hero.photos[0], [1] hero.image, hero.inset\n * hero.primaryAction / secondary... hero.cta / secondaryCta\n * campaign (when shown) the FIRST block, kind `campaign`, and\n * the page body (`# title` + its text)\n * featureStoriesBlock kind `feature_stories`\n * surfaceCardBlock kind `cta`\n * accentBandBlock kind `accent_band`\n * locationsBlock kind `locations` (jumpLink \u2192 anchor)\n *\n * A content page's `sections[]` (A-1915) reads those four kinds plus\n * `text_media`, `faq`, `gallery` and `contact` into `PageContent.sections`,\n * with their named warnings in `PageContent.warnings`. `sections.ts` holds the\n * kinds, the A-1876 layout names and the warning codes.\n *\n * Two places the composition cannot carry a proposed marker: a `cta` block's\n * plain strings, and the campaign's title and body (which travel as rich body content).\n * There the text is returned plain and the line is still listed in\n * `proposedCopy`, which is the list a dashboard and a site both read for\n * approval. Nothing is dropped silently.\n */\n\nimport { parseReferencedMenuDocument } from \"./menu.js\";\nimport { MENU_DECORATIONS, MENU_DOCUMENT_PROJECTION, MENU_LAYOUT_DEFAULTS, MENU_LAYOUTS } from \"./menu-options.js\";\nimport { isDenseArray } from \"./array.js\";\nimport { defineCmsQuery, type CmsQuery, type CmsResolution } from \"../contracts/query.js\";\nimport { SITE_HERO, validateTextDocument } from \"../contracts/text.js\";\nimport { validHomeFallback, validPageFallback, validSettingsFallback } from \"./fallback.js\";\nimport {\n HEADING_LIST_LAYOUTS, READER_GALLERY_PHOTO_LIMIT, READER_GALLERY_PHOTO_MIN, SECTION_LAYOUT_DEFAULTS, SECTION_WARNINGS,\n SPLIT_LAYOUTS, STAGGER_ROW_LAYOUTS,\n type FaqSectionInput, type PageSectionInput, type SectionImageInput, type SectionWarning,\n} from \"./sections.js\";\n\n// ---------------------------------------------------------------------------\n// Output types\n// ---------------------------------------------------------------------------\n\nexport type CopyInput = string | { readonly text: string; readonly copySource: \"captured\" | \"proposed\" };\nexport type RichInline = {\n readonly children: readonly { readonly text: string; readonly marks: readonly string[] }[];\n readonly markDefs: readonly { readonly key: string; readonly href: string }[];\n};\nexport type RichBodyPart =\n | { readonly kind: \"block\"; readonly style: \"normal\" | \"h2\" | \"h3\" | \"blockquote\"; readonly listItem: \"bullet\" | \"number\" | null; readonly level: number; readonly content: RichInline }\n | { readonly kind: \"heading\"; readonly text: string }\n | { readonly kind: \"table\"; readonly rows: readonly (readonly RichInline[])[] };\nexport type RichBody = string | readonly RichBodyPart[];\nexport type ActionInput = { readonly label: CopyInput; readonly href: string };\n\nexport type HeroInput = {\n readonly heading: CopyInput;\n readonly subline: CopyInput | null;\n readonly intro: CopyInput | null;\n readonly image: string | null;\n readonly inset: string | null;\n readonly cta: ActionInput | null;\n readonly secondaryCta: ActionInput | null;\n};\n\nexport type StoryInput = {\n readonly heading: CopyInput;\n readonly body: CopyInput;\n readonly action: ActionInput;\n readonly image: string | null;\n readonly shape: \"wide\" | \"tall\" | \"round\" | \"free\";\n readonly card: boolean;\n};\n\nexport type CompositionBlockInput =\n | { readonly kind: \"campaign\"; readonly action: ActionInput | null; readonly images: readonly string[] }\n | {\n readonly kind: \"locations\";\n readonly anchor: string | null;\n readonly heading: CopyInput;\n readonly tail: CopyInput | null;\n readonly image: string | null;\n }\n | {\n readonly kind: \"feature_stories\";\n readonly heading: CopyInput;\n readonly tail: CopyInput | null;\n readonly items: readonly StoryInput[];\n }\n | {\n readonly kind: \"accent_band\";\n readonly heading: CopyInput;\n readonly tail?: CopyInput | null;\n readonly body: CopyInput;\n readonly action: ActionInput | null;\n readonly image: string | null;\n }\n | {\n readonly kind: \"cta\";\n readonly heading: string;\n readonly image?: string | null;\n readonly body: string | null;\n readonly link: { readonly label: string; readonly href: string };\n };\n\nexport type CompositionInput = {\n readonly intro: string | null;\n readonly photo: string | null;\n readonly hero: HeroInput | null;\n readonly blocks: readonly CompositionBlockInput[];\n};\n\n/** One line of studio-written copy awaiting approval, and where it is in the document. */\nexport type ProposedCopy = { readonly where: string; readonly text: string };\n\nexport type CampaignState = \"shown\" | \"off\" | \"not-started\" | \"ended\" | \"none\";\n\nexport type HomeContent = {\n readonly composition: CompositionInput;\n /** Campaign text as Markdown, or structured blocks and tables when it contains a table. */\n readonly body: RichBody;\n readonly campaign: CampaignState;\n readonly proposedCopy: readonly ProposedCopy[];\n};\n\nexport type PageContent = {\n readonly slug: string;\n readonly title: string;\n readonly titleTail?: string;\n readonly composition: CompositionInput;\n readonly body: RichBody;\n readonly migrated: boolean;\n readonly proposedCopy: readonly ProposedCopy[];\n /**\n * A-1915. Present only when the stored page has a `sections` list, so a page\n * written before sections existed reads exactly as it did in 0.2.0.\n */\n readonly sections?: readonly PageSectionInput[];\n /** A-1915. Present with `sections`: every named section warning, with its path. */\n readonly warnings?: readonly SectionWarning[];\n};\n\nexport type SiteSettingsContent = {\n readonly name: string;\n readonly description: string | null;\n readonly contact: { readonly email: string | null; readonly phone: string | null };\n readonly social: readonly { readonly network: string; readonly url: string }[];\n};\n\n// ---------------------------------------------------------------------------\n// The refusal\n// ---------------------------------------------------------------------------\n\nexport const MALFORMED_CONTENT = \"malformed-content\";\n\nexport class MalformedContentError extends Error {\n readonly code = MALFORMED_CONTENT;\n readonly documentId: string;\n readonly problems: readonly string[];\n\n constructor(documentId: string, problems: readonly string[]) {\n super(`${MALFORMED_CONTENT}: ${documentId}: ${problems.join(\"; \")}`);\n this.name = \"MalformedContentError\";\n this.documentId = documentId;\n this.problems = problems;\n }\n}\n\n// ---------------------------------------------------------------------------\n// The rules restated\n// ---------------------------------------------------------------------------\n\n/*\n * Runtime-free rules shared with schema-base. The schema adapter imports these\n * objects; content-client compatibility tests pin the shared references.\n */\nexport const READER_PHOTO_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nexport const READER_ACTION_HREF =\n /^(?:\\/(?![/\\\\])[^\\s]*|#[a-z][a-z0-9]*(?:-[a-z0-9]+)*|mailto:[^\\s@]+@[^\\s@]+|tel:\\+?[0-9][0-9 ()-]*|https?:\\/\\/[^\\s/]+[^\\s]*)$/;\nexport const READER_JUMP_LINK = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;\nexport const READER_STORY_LAYOUTS = [\"wide\", \"tall\", \"round\", \"free\"] as const;\nexport const READER_HOME_PAGE_ID = \"homePage\";\nexport const READER_SITE_SETTINGS_ID = \"siteSettings\";\n\n// ---------------------------------------------------------------------------\n// A small path-aware reader\n// ---------------------------------------------------------------------------\n\ntype Raw = Record<string, unknown>;\n\nclass Reading {\n readonly problems: string[] = [];\n readonly proposed: ProposedCopy[] = [];\n\n fail(path: string, message: string): null {\n this.problems.push(`${path} ${message}`);\n return null;\n }\n\n object(value: unknown, path: string): Raw | null {\n if (typeof value === \"object\" && value !== null && !Array.isArray(value)) return value as Raw;\n return this.fail(path, \"is not an object\");\n }\n\n dense(value: unknown[], path: string): boolean {\n if (isDenseArray(value)) return true;\n this.fail(path, \"contains missing entries\");\n return false;\n }\n\n optionalString(value: unknown, path: string): string | null {\n if (value === undefined || value === null) return null;\n if (typeof value === \"string\") return value.trim() === \"\" ? null : value;\n return this.fail(path, \"is not text\");\n }\n\n /** A copy value, or null when absent or empty. Records a proposed line. */\n copy(value: unknown, path: string): CopyInput | null {\n if (value === undefined || value === null) return null;\n const raw = this.object(value, path);\n if (raw === null) return null;\n const text = raw[\"text\"];\n const source = raw[\"copySource\"];\n if (text !== undefined && typeof text !== \"string\") return this.fail(`${path}.text`, \"is not text\");\n if (source !== undefined && source !== \"captured\" && source !== \"proposed\") {\n return this.fail(`${path}.copySource`, `is ${JSON.stringify(source)}, not captured or proposed`);\n }\n if (typeof text !== \"string\" || text.trim() === \"\") return null;\n if (source === \"proposed\") {\n this.proposed.push({ where: path, text });\n return { text, copySource: \"proposed\" };\n }\n if (source === \"captured\") return { text, copySource: \"captured\" };\n return text;\n }\n\n requiredCopy(value: unknown, path: string): CopyInput | null {\n const copy = this.copy(value, path);\n if (copy === null && !this.problems.some((p) => p.startsWith(path))) this.fail(path, \"is missing its words\");\n return copy;\n }\n\n photo(value: unknown, path: string): string | null {\n if (value === undefined || value === null) return null;\n if (typeof value === \"string\" && READER_PHOTO_SLUG.test(value)) return value;\n return this.fail(path, `is ${JSON.stringify(value)}, not a photo library slug`);\n }\n\n photos(value: unknown, path: string, max: number): string[] {\n if (value === undefined || value === null) return [];\n if (!Array.isArray(value)) {\n this.fail(path, \"is not a list of photos\");\n return [];\n }\n if (!this.dense(value, path)) return [];\n if (value.length > max) this.fail(path, `holds ${String(value.length)} photos; at most ${String(max)}`);\n return value.map((entry, index) => this.photo(entry, `${path}[${String(index)}]`)).filter((p): p is string => p !== null);\n }\n\n action(value: unknown, path: string): ActionInput | null {\n if (value === undefined || value === null) return null;\n const raw = this.object(value, path);\n if (raw === null) return null;\n const label = this.requiredCopy(raw[\"label\"], `${path}.label`);\n const href = raw[\"href\"];\n if (typeof href !== \"string\" || !READER_ACTION_HREF.test(href)) {\n return this.fail(`${path}.href`, `is ${JSON.stringify(href)}, not a site path, #jump-link, mailto:, tel: or web address`);\n }\n return label === null ? null : { label, href };\n }\n\n requiredAction(value: unknown, path: string): ActionInput | null {\n if (value === undefined || value === null) return this.fail(path, \"is missing\");\n return this.action(value, path);\n }\n}\n\nfunction plain(copy: CopyInput): string {\n return typeof copy === \"string\" ? copy : copy.text;\n}\n\n// ---------------------------------------------------------------------------\n// Portable Text to rich body content\n// ---------------------------------------------------------------------------\n\n/**\n * The narrow Portable Text `portableText` declares, back to Markdown: normal,\n * h2, h3 and quote blocks, bullet and numbered lists, bold, italic, code and\n * links. It is the inverse of the seed package's Markdown mapper, and the\n * round-trip test holds the two to each other.\n *\n * An uploaded image inside rich text is REFUSED, not dropped: nothing can\n * upload one yet (the dashboard slice decides how), so one arriving means\n * something wrote the document by hand, and a page silently missing a picture\n * is the failure this reader exists to prevent.\n * Text-only input remains a Markdown string. Input with a table produces\n * ordered structured blocks and cells that never enter a Markdown parser.\n */\nexport function portableTextToMarkdown(value: unknown): RichBody {\n const reading = new Reading();\n const markdown = richTextToBody(value, \"body\", reading);\n if (reading.problems.length > 0) throw new MalformedContentError(\"body\", reading.problems);\n return markdown;\n}\n\nfunction richTextToBody(value: unknown, path: string, reading: Reading): RichBody {\n if (value === undefined || value === null) return \"\";\n if (!Array.isArray(value)) {\n reading.fail(path, \"is not rich text\");\n return \"\";\n }\n if (!reading.dense(value, path)) return \"\";\n if (value.some((member) => member !== null && typeof member === \"object\" && !Array.isArray(member) && (member as Raw)[\"_type\"] === \"portableTable\")) {\n return richTextToParts(value, path, reading);\n }\n const out: string[] = [];\n let previousWasList = false;\n value.forEach((member, index) => {\n const at = `${path}[${String(index)}]`;\n const block = reading.object(member, at);\n if (block === null) return;\n if (block[\"_type\"] !== \"block\") {\n reading.fail(at, `is a ${JSON.stringify(block[\"_type\"])}; only text is supported in rich text so far`);\n return;\n }\n const text = spansToMarkdown(block, at, reading);\n const listItem = block[\"listItem\"];\n const style = block[\"style\"] ?? \"normal\";\n let line: string;\n if (listItem === \"bullet\" || listItem === \"number\") {\n const level = typeof block[\"level\"] === \"number\" && block[\"level\"] > 1 ? block[\"level\"] : 1;\n line = `${\" \".repeat(level - 1)}${listItem === \"bullet\" ? \"-\" : \"1.\"} ${text}`;\n } else if (style === \"h2\") line = `## ${text}`;\n else if (style === \"h3\") line = `### ${text}`;\n else if (style === \"blockquote\") line = `> ${text}`;\n else if (style === \"normal\") line = text;\n else {\n reading.fail(`${at}.style`, `is ${JSON.stringify(style)}, which the schema does not offer`);\n return;\n }\n const isList = listItem === \"bullet\" || listItem === \"number\";\n if (out.length > 0) out.push(isList && previousWasList ? \"\\n\" : \"\\n\\n\");\n out.push(line);\n previousWasList = isList;\n });\n return out.join(\"\");\n}\n\nfunction bodyText(body: RichBody): string {\n return typeof body === \"string\" ? body : body.map((part) => part.kind === \"heading\" ? part.text\n : part.kind === \"block\" ? part.content.children.map((child) => child.text).join(\"\")\n : part.rows.map((row) => row.map((cell) => cell.children.map((child) => child.text).join(\"\")).join(\" \")).join(\"\\n\")).join(\"\\n\\n\");\n}\n\nfunction inlineFromBlock(block: Raw, path: string, reading: Reading): RichInline {\n const rawDefs = Array.isArray(block[\"markDefs\"]) ? block[\"markDefs\"] as unknown[] : [];\n if (!reading.dense(rawDefs, `${path}.markDefs`)) return { children: [], markDefs: [] };\n const markDefs: { key: string; href: string }[] = [];\n rawDefs.forEach((entry, index) => {\n const def = reading.object(entry, `${path}.markDefs[${String(index)}]`);\n if (def === null) return;\n if (def[\"_type\"] !== \"link\" || typeof def[\"_key\"] !== \"string\" || typeof def[\"href\"] !== \"string\") {\n reading.fail(`${path}.markDefs[${String(index)}]`, \"is not a link annotation\");\n return;\n }\n markDefs.push({ key: def[\"_key\"], href: def[\"href\"] });\n });\n const rawChildren = block[\"children\"];\n if (!Array.isArray(rawChildren) || !reading.dense(rawChildren, `${path}.children`)) {\n reading.fail(`${path}.children`, \"needs text spans\");\n return { children: [], markDefs };\n }\n const children: { text: string; marks: string[] }[] = [];\n rawChildren.forEach((entry, index) => {\n const at = `${path}.children[${String(index)}]`;\n const span = reading.object(entry, at);\n if (span === null) return;\n if (span[\"_type\"] !== \"span\" || typeof span[\"text\"] !== \"string\") {\n reading.fail(at, \"is not a text span\");\n return;\n }\n const marks = span[\"marks\"] === undefined ? [] : span[\"marks\"];\n if (!Array.isArray(marks) || !reading.dense(marks, `${at}.marks`) || marks.some((mark) => typeof mark !== \"string\"\n || (![\"strong\", \"em\", \"code\"].includes(mark) && !markDefs.some((def) => def.key === mark)))) {\n reading.fail(`${at}.marks`, \"contains an unsupported mark\");\n return;\n }\n children.push({ text: span[\"text\"], marks });\n });\n return { children, markDefs };\n}\n\nfunction richTextToParts(value: unknown[], path: string, reading: Reading): RichBodyPart[] {\n const parts: RichBodyPart[] = [];\n value.forEach((member, index) => {\n const at = `${path}[${String(index)}]`;\n const block = reading.object(member, at);\n if (block === null) return;\n if (block[\"_type\"] === \"portableTable\") {\n parts.push(tableToData(block, at, reading));\n return;\n }\n if (block[\"_type\"] !== \"block\") {\n reading.fail(at, `is a ${JSON.stringify(block[\"_type\"])}; only text is supported in rich text so far`);\n return;\n }\n const style = block[\"style\"] ?? \"normal\";\n if (style !== \"normal\" && style !== \"h2\" && style !== \"h3\" && style !== \"blockquote\") {\n reading.fail(`${at}.style`, `is ${JSON.stringify(style)}, which the schema does not offer`);\n return;\n }\n const listItem = block[\"listItem\"];\n const level = typeof block[\"level\"] === \"number\" && block[\"level\"] > 1 ? block[\"level\"] : 1;\n parts.push({ kind: \"block\", style, listItem: listItem === \"bullet\" || listItem === \"number\" ? listItem : null,\n level, content: inlineFromBlock(block, at, reading) });\n });\n return parts;\n}\n\nfunction tableToData(table: Raw, path: string, reading: Reading): Extract<RichBodyPart, { kind: \"table\" }> {\n const rows = table[\"rows\"];\n if (!Array.isArray(rows) || rows.length === 0 || !reading.dense(rows, `${path}.rows`)) {\n reading.fail(`${path}.rows`, \"needs at least one row\");\n return { kind: \"table\", rows: [] };\n }\n let width = 0;\n const parsedRows: RichInline[][] = [];\n rows.forEach((entry, index) => {\n const at = `${path}.rows[${String(index)}]`;\n const row = reading.object(entry, at);\n if (row === null) return;\n const cells = row[\"cells\"];\n if (!Array.isArray(cells) || cells.length === 0 || !reading.dense(cells, `${at}.cells`)) {\n reading.fail(`${at}.cells`, \"needs at least one cell\");\n return;\n }\n if (index === 0) width = cells.length;\n if (cells.length !== width) reading.fail(`${at}.cells`, \"has a different number of cells\");\n const parsedCells = cells.map((cell, cellIndex) => {\n const cat = `${at}.cells[${String(cellIndex)}]`;\n const inline = typeof cell === \"string\"\n ? { children: [{ text: cell, marks: [] }], markDefs: [] }\n : (() => {\n const rawCell = reading.object(cell, cat);\n if (rawCell === null) return { children: [], markDefs: [] };\n if (rawCell[\"_type\"] !== \"block\" || (rawCell[\"style\"] !== undefined && rawCell[\"style\"] !== \"normal\") || rawCell[\"listItem\"] !== undefined) {\n reading.fail(cat, \"is not an inline text block\");\n return { children: [], markDefs: [] };\n }\n return inlineFromBlock(rawCell, cat, reading);\n })();\n if (inline.children.map((child) => child.text).join(\"\").trim() === \"\") reading.fail(cat, \"is empty or not text\");\n return inline;\n });\n parsedRows.push(parsedCells);\n });\n return { kind: \"table\", rows: parsedRows };\n}\n\nconst DECORATORS: Record<string, string> = { strong: \"**\", em: \"*\", code: \"`\" };\n\nfunction spansToMarkdown(block: Raw, path: string, reading: Reading): string {\n const links = new Map<string, string>();\n const markDefs = Array.isArray(block[\"markDefs\"]) ? (block[\"markDefs\"] as unknown[]) : [];\n if (!reading.dense(markDefs, `${path}.markDefs`)) return \"\";\n for (const def of markDefs) {\n const d = def as Raw;\n if (d[\"_type\"] === \"link\" && typeof d[\"_key\"] === \"string\" && typeof d[\"href\"] === \"string\") links.set(d[\"_key\"], d[\"href\"]);\n }\n const children = Array.isArray(block[\"children\"]) ? (block[\"children\"] as unknown[]) : [];\n if (!reading.dense(children, `${path}.children`)) return \"\";\n return children\n .map((child, index) => {\n const span = child as Raw;\n if (span[\"_type\"] !== \"span\" || typeof span[\"text\"] !== \"string\") {\n reading.fail(`${path}.children[${String(index)}]`, \"is not a text span\");\n return \"\";\n }\n let text = span[\"text\"];\n const marks = Array.isArray(span[\"marks\"]) ? (span[\"marks\"] as unknown[]) : [];\n if (!reading.dense(marks, `${path}.children[${String(index)}].marks`)) return \"\";\n let href: string | null = null;\n for (const mark of marks) {\n if (typeof mark !== \"string\") continue;\n const fence = DECORATORS[mark];\n if (fence !== undefined) text = `${fence}${text}${fence}`;\n else if (links.has(mark)) href = links.get(mark) ?? null;\n else reading.fail(`${path}.children[${String(index)}].marks`, `names ${JSON.stringify(mark)}, which is neither a style nor a link`);\n }\n return href === null ? text : `[${text}](${href})`;\n })\n .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// The home page\n// ---------------------------------------------------------------------------\n\n/** Whether a campaign is shown at `now`, and if not, why not. */\nexport function campaignState(campaign: unknown, now: Date): CampaignState {\n if (typeof campaign !== \"object\" || campaign === null) return \"none\";\n const c = campaign as Raw;\n if (c[\"enabled\"] !== true) return \"off\";\n const starts = typeof c[\"startsAt\"] === \"string\" ? Date.parse(c[\"startsAt\"]) : null;\n const ends = typeof c[\"endsAt\"] === \"string\" ? Date.parse(c[\"endsAt\"]) : null;\n if (starts !== null && now.getTime() < starts) return \"not-started\";\n if (ends !== null && now.getTime() >= ends) return \"ended\";\n return \"shown\";\n}\n\nfunction keyed(path: string, raw: Raw, index: number): string {\n return typeof raw[\"_key\"] === \"string\" ? `${path}[_key==\"${raw[\"_key\"]}\"]` : `${path}[${String(index)}]`;\n}\n\n/**\n * A stored home page document, validated and mapped. Throws\n * `MalformedContentError` naming every problem. `now` decides whether a\n * scheduled campaign is shown, and is a parameter so a build, a preview and a\n * test each say which moment they mean.\n */\nexport function parseHomePage(document: unknown, options: { readonly now: Date }): HomeContent {\n const reading = new Reading();\n const doc = reading.object(document, \"homePage\");\n if (doc === null) throw new MalformedContentError(READER_HOME_PAGE_ID, reading.problems);\n const id = typeof doc[\"_id\"] === \"string\" ? doc[\"_id\"] : READER_HOME_PAGE_ID;\n if (doc[\"_type\"] !== \"homePage\") reading.fail(\"_type\", `is ${JSON.stringify(doc[\"_type\"])}, not homePage`);\n\n let hero: HeroInput | null = null;\n const rawHero = doc[\"hero\"] === undefined ? reading.fail(\"hero\", \"is missing\") : reading.object(doc[\"hero\"], \"hero\");\n if (rawHero !== null) {\n const photos = reading.photos(rawHero[\"photos\"], \"hero.photos\", 2);\n const source = doc[\"heroSource\"] ?? \"homePage\";\n if (source !== \"homePage\" && source !== \"siteHero\") reading.fail(\"heroSource\", \"must be homePage or siteHero\");\n let heading = rawHero[\"headline\"] === undefined ? null : reading.requiredCopy(rawHero[\"headline\"], \"hero.headline\");\n if (heading === null && rawHero[\"headline\"] === undefined && source === \"siteHero\") {\n const siteHero = doc[\"_siteHero\"];\n if (siteHero !== null && typeof siteHero === \"object\" && !Array.isArray(siteHero)) {\n const headingField = SITE_HERO.fields.find((field) => field.name === \"heading\");\n if (headingField === undefined) throw new TypeError(\"Missing siteHero heading definition\");\n const validated = validateTextDocument({ ...SITE_HERO, fields: [headingField] }, siteHero as Record<string, unknown>);\n if (validated.ok) heading = validated.value.heading ?? null;\n }\n }\n if (heading === null && rawHero[\"headline\"] === undefined) {\n reading.fail(\"hero.headline\", source === \"siteHero\"\n ? \"is missing and siteHero.heading is missing or invalid\"\n : \"is missing; write a headline or select siteHero with a published heading\");\n }\n const subline = reading.copy(rawHero[\"subline\"], \"hero.subline\");\n const intro = reading.copy(rawHero[\"lead\"], \"hero.lead\");\n const cta = reading.action(rawHero[\"primaryAction\"], \"hero.primaryAction\");\n const secondaryCta = reading.action(rawHero[\"secondaryAction\"], \"hero.secondaryAction\");\n if (heading !== null) {\n hero = { heading, subline, intro, image: photos[0] ?? null, inset: photos[1] ?? null, cta, secondaryCta };\n }\n }\n\n const blocks: CompositionBlockInput[] = [];\n const blockJumps: { where: string; href: string }[] = [];\n let body: RichBody = \"\";\n const state = campaignState(doc[\"campaign\"], options.now);\n if (state === \"shown\") {\n const campaign = reading.object(doc[\"campaign\"], \"campaign\");\n if (campaign !== null) {\n const title = reading.requiredCopy(campaign[\"title\"], \"campaign.title\");\n const images = reading.photos(campaign[\"photos\"], \"campaign.photos\", 3);\n if (images.length === 0) reading.fail(\"campaign.photos\", \"is empty; a shown campaign needs at least one photo\");\n const text = richTextToBody(campaign[\"body\"], \"campaign.body\", reading);\n if (bodyText(text) === \"\") reading.fail(\"campaign.body\", \"is empty; a shown campaign needs its text\");\n if (campaign[\"bodyCopySource\"] === \"proposed\") reading.proposed.push({ where: \"campaign.body\", text: bodyText(text) });\n const action = reading.action(campaign[\"action\"], \"campaign.action\");\n if (action !== null) blockJumps.push({ where: \"campaign.action.href\", href: action.href });\n if (title !== null) body = typeof text === \"string\" ? `# ${plain(title)}\\n\\n${text}` : [{ kind: \"heading\", text: plain(title) }, ...text];\n blocks.push({ kind: \"campaign\", action, images });\n }\n }\n\n const anchors: string[] = [];\n const rawBlocks = doc[\"blocks\"] ?? [];\n if (!Array.isArray(rawBlocks)) reading.fail(\"blocks\", \"is not a list\");\n else if (reading.dense(rawBlocks, \"blocks\")) {\n rawBlocks.forEach((member, index) => {\n const raw = reading.object(member, `blocks[${String(index)}]`);\n if (raw === null) return;\n const at = keyed(\"blocks\", raw, index);\n const mapped = mapBlock(raw, at, reading);\n if (mapped === null) return;\n blockJumps.push(...buttonsOf(mapped, raw, at));\n if (mapped.kind === \"locations\" && mapped.anchor !== null) {\n if (anchors.includes(mapped.anchor)) reading.fail(`${at}.jumpLink`, `repeats \"${mapped.anchor}\", already used by another section`);\n anchors.push(mapped.anchor);\n }\n blocks.push(mapped);\n });\n }\n\n // A jump link on ANY button must land on a section of this page.\n const jumps: { where: string; href: string }[] = [];\n if (hero?.cta) jumps.push({ where: \"hero.primaryAction.href\", href: hero.cta.href });\n if (hero?.secondaryCta) jumps.push({ where: \"hero.secondaryAction.href\", href: hero.secondaryCta.href });\n jumps.push(...blockJumps);\n for (const { where, href } of jumps) {\n if (href.startsWith(\"#\") && !anchors.includes(href.slice(1))) {\n reading.fail(where, `jumps to ${href}, which no section on the page is named`);\n }\n }\n\n if (reading.problems.length > 0) throw new MalformedContentError(id, reading.problems);\n return {\n composition: { intro: null, photo: null, hero, blocks },\n body,\n campaign: state,\n proposedCopy: reading.proposed,\n };\n}\n\n/** Every button a mapped section carries, with its path in the stored document. */\nfunction buttonsOf(block: CompositionBlockInput, raw: Raw, at: string): { where: string; href: string }[] {\n switch (block.kind) {\n case \"feature_stories\": {\n const stories = Array.isArray(raw[\"stories\"]) ? (raw[\"stories\"] as Raw[]) : [];\n return block.items.map((item, index) => ({\n where: `${keyed(`${at}.stories`, stories[index] ?? {}, index)}.action.href`,\n href: item.action.href,\n }));\n }\n case \"accent_band\":\n return block.action === null ? [] : [{ where: `${at}.action.href`, href: block.action.href }];\n case \"cta\":\n return [{ where: `${at}.action.href`, href: block.link.href }];\n default:\n return [];\n }\n}\n\nfunction jumpLinkOf(raw: Raw, at: string, reading: Reading): string | null {\n const jump = raw[\"jumpLink\"];\n if (jump === undefined || jump === null || jump === \"\") return null;\n if (typeof jump !== \"string\" || !READER_JUMP_LINK.test(jump)) return reading.fail(`${at}.jumpLink`, `is ${JSON.stringify(jump)}, not lowercase words joined by hyphens`);\n return jump;\n}\n\nfunction mapBlock(raw: Raw, at: string, reading: Reading): CompositionBlockInput | null {\n switch (raw[\"_type\"]) {\n case \"featureStoriesBlock\": {\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const stories = raw[\"stories\"];\n if (!Array.isArray(stories) || stories.length === 0) return reading.fail(`${at}.stories`, \"has no stories\");\n if (!reading.dense(stories, `${at}.stories`)) return null;\n const items: StoryInput[] = [];\n stories.forEach((member, index) => {\n const story = reading.object(member, `${at}.stories[${String(index)}]`);\n if (story === null) return;\n const sat = keyed(`${at}.stories`, story, index);\n const sHeading = reading.requiredCopy(story[\"heading\"], `${sat}.heading`);\n const sBody = reading.requiredCopy(story[\"body\"], `${sat}.body`);\n const action = reading.requiredAction(story[\"action\"], `${sat}.action`);\n const image = reading.photo(story[\"photo\"], `${sat}.photo`);\n const layout = story[\"layout\"] ?? \"wide\";\n if (!(READER_STORY_LAYOUTS as readonly unknown[]).includes(layout)) {\n reading.fail(`${sat}.layout`, `is ${JSON.stringify(layout)}, not wide, tall, round or free`);\n return;\n }\n const onCard = story[\"onCard\"] ?? false;\n if (typeof onCard !== \"boolean\") {\n reading.fail(`${sat}.onCard`, \"is not on or off\");\n return;\n }\n if (sHeading === null || sBody === null || action === null) return;\n items.push({ heading: sHeading, body: sBody, action, image, shape: layout as StoryInput[\"shape\"], card: onCard });\n });\n return heading === null ? null : { kind: \"feature_stories\", heading, tail, items };\n }\n case \"surfaceCardBlock\": {\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const body = reading.copy(raw[\"body\"], `${at}.body`);\n const action = reading.requiredAction(raw[\"action\"], `${at}.action`);\n const image = reading.photo(raw[\"photo\"], `${at}.photo`);\n if (heading === null || action === null) return null;\n return {\n kind: \"cta\",\n heading: plain(heading),\n body: body === null ? null : plain(body),\n link: { label: plain(action.label), href: action.href },\n ...(raw[\"photo\"] === undefined ? {} : { image }),\n };\n }\n case \"accentBandBlock\": {\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const body = reading.requiredCopy(raw[\"body\"], `${at}.body`);\n const action = reading.action(raw[\"action\"], `${at}.action`);\n const image = reading.photo(raw[\"photo\"], `${at}.photo`);\n if (heading === null || body === null) return null;\n return { kind: \"accent_band\", heading, body, action, image, ...(raw[\"tail\"] === undefined ? {} : { tail }) };\n }\n case \"locationsBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const image = reading.photo(raw[\"photo\"], `${at}.photo`);\n return heading === null ? null : { kind: \"locations\", anchor, heading, tail, image };\n }\n default:\n return reading.fail(`${at}._type`, `is ${JSON.stringify(raw[\"_type\"])}, not a section type this reader knows`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Page sections (A-1915)\n// ---------------------------------------------------------------------------\n\n/** A stored `layout`, or the composition default with a named warning. Never throws. */\nfunction sectionLayout<T extends string>(raw: Raw, at: string, layouts: readonly T[], fallback: T, warnings: SectionWarning[]): T {\n const value = raw[\"layout\"];\n const layout = layouts.find((name) => name === value);\n if (layout !== undefined) return layout;\n warnings.push({ code: SECTION_WARNINGS.layoutDefaulted, where: `${at}.layout` });\n return fallback;\n}\n\n/** An `imageWithHotspot`, or null. Without alt text or an asset it is dropped with a named warning. */\nfunction sectionImage(value: unknown, path: string, reading: Reading, warnings: SectionWarning[]): SectionImageInput | null {\n if (value === undefined || value === null) return null;\n const raw = reading.object(value, path);\n if (raw === null) return null;\n const asset = raw[\"asset\"];\n const ref = typeof asset === \"object\" && asset !== null ? (asset as Raw)[\"_ref\"] : undefined;\n if (typeof ref !== \"string\" || ref === \"\") {\n warnings.push({ code: SECTION_WARNINGS.imageMissingAsset, where: `${path}.asset` });\n return null;\n }\n const alt = raw[\"alt\"];\n if (typeof alt !== \"string\" || alt.trim() === \"\") {\n warnings.push({ code: SECTION_WARNINGS.imageMissingAlt, where: `${path}.alt` });\n return null;\n }\n const caption = reading.optionalString(raw[\"caption\"], `${path}.caption`);\n let hotspot: SectionImageInput[\"hotspot\"] = null;\n if (raw[\"hotspot\"] !== undefined && raw[\"hotspot\"] !== null) {\n const spot = reading.object(raw[\"hotspot\"], `${path}.hotspot`);\n const x = spot?.[\"x\"];\n const y = spot?.[\"y\"];\n if (typeof x === \"number\" && typeof y === \"number\" && x >= 0 && x <= 1 && y >= 0 && y <= 1) hotspot = { x, y };\n else if (spot !== null) reading.fail(`${path}.hotspot`, \"is not a point inside the image\");\n }\n return { asset: ref, alt, caption, hotspot };\n}\n\n/** Only a bounded schema name may travel in a warning; never coerce non-text values. */\nfunction sectionTypeForWarning(value: unknown): string {\n return typeof value === \"string\" ? value.slice(0, 64).replace(/[^A-Za-z0-9_.-]/g, \"?\") : \"?\";\n}\n\nfunction mapSection(raw: Raw, at: string, reading: Reading, warnings: SectionWarning[]): PageSectionInput | null {\n const textItems = (value: unknown, path: string, min: number, max: number): { title: CopyInput; body: CopyInput }[] => {\n if (!Array.isArray(value) || value.length < min || value.length > max || !reading.dense(value, path)) {\n reading.fail(path, `needs ${String(min)} to ${String(max)} items`);\n return [];\n }\n const items: { title: CopyInput; body: CopyInput }[] = [];\n value.forEach((member, index) => {\n const item = reading.object(member, `${path}[${String(index)}]`);\n if (item === null) return;\n const atItem = keyed(path, item, index);\n const title = reading.requiredCopy(item[\"title\"], `${atItem}.title`);\n const body = reading.requiredCopy(item[\"body\"], `${atItem}.body`);\n if (title !== null && body !== null) items.push({ title, body });\n });\n return items;\n };\n switch (raw[\"_type\"]) {\n case \"menuBlock\": {\n const menu = parseReferencedMenuDocument(raw[\"menu\"], `${at}.menu`);\n if (menu === null) {\n warnings.push({ code: SECTION_WARNINGS.menuUnavailable, where: `${at}.menu` });\n return null;\n }\n const layout = sectionLayout(raw, at, MENU_LAYOUTS, MENU_LAYOUT_DEFAULTS.layout, warnings);\n const decoration = MENU_DECORATIONS.find((name) => name === raw[\"decoration\"]);\n if (decoration === undefined) warnings.push({ code: SECTION_WARNINGS.layoutDefaulted, where: `${at}.decoration` });\n return { kind: \"menu\", menu, layout, decoration: decoration ?? MENU_LAYOUT_DEFAULTS.decoration };\n }\n case \"featureStoriesBlock\":\n case \"surfaceCardBlock\":\n case \"accentBandBlock\":\n case \"locationsBlock\": {\n const block = mapBlock(raw, at, reading);\n return block === null || block.kind === \"campaign\" ? null : block;\n }\n case \"textMediaBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const body = reading.requiredCopy(raw[\"body\"], `${at}.body`);\n const action = reading.action(raw[\"action\"], `${at}.action`);\n const image = sectionImage(raw[\"photo\"], `${at}.photo`, reading, warnings);\n const layout = sectionLayout(raw, at, SPLIT_LAYOUTS, SECTION_LAYOUT_DEFAULTS.Split, warnings);\n if (heading === null || body === null) return null;\n return { kind: \"text_media\", anchor, layout, heading, tail, body, action, image };\n }\n case \"faqBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.copy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const intro = reading.copy(raw[\"intro\"], `${at}.intro`);\n const layout = sectionLayout(raw, at, HEADING_LIST_LAYOUTS, SECTION_LAYOUT_DEFAULTS.HeadingList, warnings);\n const rawItems = raw[\"items\"];\n if (!Array.isArray(rawItems) || rawItems.length === 0) return reading.fail(`${at}.items`, \"has no questions\");\n if (!reading.dense(rawItems, `${at}.items`)) return null;\n const items: FaqSectionInput[\"items\"][number][] = [];\n rawItems.forEach((member, index) => {\n const item = reading.object(member, `${at}.items[${String(index)}]`);\n if (item === null) return;\n const iat = keyed(`${at}.items`, item, index);\n if (item[\"_type\"] !== undefined && item[\"_type\"] !== \"faqItem\") {\n reading.fail(`${iat}._type`, `is ${JSON.stringify(item[\"_type\"])}, not faqItem`);\n return;\n }\n const question = reading.requiredCopy(item[\"question\"], `${iat}.question`);\n const savedAnswer = item[\"answer\"];\n const answerObject = typeof savedAnswer === \"object\" && savedAnswer !== null && !Array.isArray(savedAnswer) ? savedAnswer as Raw : null;\n const rich = Array.isArray(savedAnswer) || (answerObject !== null && answerObject[\"content\"] !== undefined);\n const answer = rich\n ? { kind: \"rich\" as const, body: richTextToBody(Array.isArray(savedAnswer) ? savedAnswer : answerObject?.[\"content\"],\n Array.isArray(savedAnswer) ? `${iat}.answer` : `${iat}.answer.content`, reading) }\n : typeof savedAnswer === \"string\" ? savedAnswer : reading.requiredCopy(savedAnswer, `${iat}.answer`);\n if (answer === \"\" || (typeof answer === \"object\" && answer !== null && \"kind\" in answer && bodyText(answer.body) === \"\")) reading.fail(`${iat}.answer`, \"is empty\");\n if (question !== null && answer !== null) items.push({ question, answer });\n });\n return { kind: \"faq\", anchor, layout, heading, tail, intro, items };\n }\n case \"textItemsBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const layout = sectionLayout(raw, at, STAGGER_ROW_LAYOUTS, SECTION_LAYOUT_DEFAULTS.StaggerRow, warnings);\n const items = textItems(raw[\"items\"], `${at}.items`, 2, 4);\n return { kind: \"text_items\", anchor, layout, items };\n }\n case \"stepsBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const layout = sectionLayout(raw, at, HEADING_LIST_LAYOUTS, SECTION_LAYOUT_DEFAULTS.HeadingList, warnings);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const intro = reading.requiredCopy(raw[\"intro\"], `${at}.intro`);\n const items = textItems(raw[\"items\"], `${at}.items`, 1, 100);\n return heading === null || intro === null ? null : { kind: \"steps\", anchor, layout, heading, intro, items };\n }\n case \"linkListBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const layout = sectionLayout(raw, at, HEADING_LIST_LAYOUTS, SECTION_LAYOUT_DEFAULTS.HeadingList, warnings);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const saved = raw[\"links\"];\n if (!Array.isArray(saved) || saved.length === 0 || !reading.dense(saved, `${at}.links`)) return reading.fail(`${at}.links`, \"needs at least one page\");\n const links: { title: string; href: string }[] = [];\n saved.forEach((member, index) => {\n const item = reading.object(member, `${at}.links[${String(index)}]`);\n if (item === null) return;\n const page = reading.object(item[\"page\"], `${at}.links[${String(index)}].page`);\n if (page === null) return;\n const title = reading.optionalString(page[\"title\"], `${at}.links[${String(index)}].page.title`);\n const slug = reading.optionalString(page[\"slug\"], `${at}.links[${String(index)}].page.slug`);\n if (!title || !slug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) reading.fail(`${at}.links[${String(index)}].page`, \"has no readable page title or slug\");\n else links.push({ title, href: `/pages/${slug}/` });\n });\n return heading === null ? null : { kind: \"link_list\", anchor, layout, heading, tail, links };\n }\n case \"galleryBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.copy(raw[\"heading\"], `${at}.heading`);\n const layout = sectionLayout(raw, at, STAGGER_ROW_LAYOUTS, SECTION_LAYOUT_DEFAULTS.StaggerRow, warnings);\n const photos = raw[\"photos\"] ?? [];\n if (!Array.isArray(photos)) return reading.fail(`${at}.photos`, \"is not a list of photos\");\n if (!reading.dense(photos, `${at}.photos`)) return null;\n if (photos.length < READER_GALLERY_PHOTO_MIN || photos.length > READER_GALLERY_PHOTO_LIMIT) {\n return reading.fail(`${at}.photos`, `holds ${String(photos.length)} photos; a gallery shows ${String(READER_GALLERY_PHOTO_MIN)} to ${String(READER_GALLERY_PHOTO_LIMIT)}`);\n }\n const images = photos.flatMap((photo, index) => {\n const image = sectionImage(photo, keyed(`${at}.photos`, typeof photo === \"object\" && photo !== null ? (photo as Raw) : {}, index), reading, warnings);\n return image === null ? [] : [image];\n });\n if (images.length === 0) {\n warnings.push({ code: SECTION_WARNINGS.emptySkipped, where: at });\n return null;\n }\n return { kind: \"gallery\", anchor, layout, heading, images };\n }\n case \"contactBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const body = reading.copy(raw[\"body\"], `${at}.body`);\n const shown = (field: \"showEmail\" | \"showPhone\"): boolean => {\n const value = raw[field] ?? true;\n if (typeof value === \"boolean\") return value;\n reading.fail(`${at}.${field}`, \"is not on or off\");\n return false;\n };\n const showEmail = shown(\"showEmail\");\n const showPhone = shown(\"showPhone\");\n const action = reading.action(raw[\"action\"], `${at}.action`);\n const image = sectionImage(raw[\"photo\"], `${at}.photo`, reading, warnings);\n const layout = sectionLayout(raw, at, SPLIT_LAYOUTS, SECTION_LAYOUT_DEFAULTS.Split, warnings);\n if (heading === null) return null;\n if (!showEmail && !showPhone && body === null && action === null) {\n warnings.push({ code: SECTION_WARNINGS.emptySkipped, where: at });\n return null;\n }\n return { kind: \"contact\", anchor, layout, heading, body, showEmail, showPhone, action, image };\n }\n default:\n warnings.push({ code: SECTION_WARNINGS.unknownType, where: `${at}._type`, type: sectionTypeForWarning(raw[\"_type\"]) });\n return null;\n }\n}\n\n/** Every button a mapped page section carries, with its path in the stored document. */\nfunction sectionButtons(section: PageSectionInput, raw: Raw, at: string): { where: string; href: string }[] {\n switch (section.kind) {\n case \"text_media\":\n case \"contact\":\n return section.action === null ? [] : [{ where: `${at}.action.href`, href: section.action.href }];\n case \"menu\":\n case \"faq\":\n case \"gallery\":\n case \"text_items\":\n case \"steps\":\n case \"link_list\":\n return [];\n default:\n return buttonsOf(section, raw, at);\n }\n}\n\n/** `page.sections[]`, mapped in order. Unknown types are skipped with a warning; malformed content is a problem. */\nfunction parseSections(value: unknown, reading: Reading): { sections: PageSectionInput[]; warnings: SectionWarning[] } {\n const sections: PageSectionInput[] = [];\n const warnings: SectionWarning[] = [];\n if (!Array.isArray(value)) {\n reading.fail(\"sections\", \"is not a list\");\n return { sections, warnings };\n }\n if (!reading.dense(value, \"sections\")) return { sections, warnings };\n const anchors: string[] = [];\n const jumps: { where: string; href: string }[] = [];\n value.forEach((member, index) => {\n const raw = reading.object(member, `sections[${String(index)}]`);\n if (raw === null) return;\n const at = keyed(\"sections\", raw, index);\n const section = mapSection(raw, at, reading, warnings);\n if (section === null) return;\n jumps.push(...sectionButtons(section, raw, at));\n const anchor = \"anchor\" in section ? section.anchor : null;\n if (anchor !== null) {\n if (anchors.includes(anchor)) reading.fail(`${at}.jumpLink`, `repeats \"${anchor}\", already used by another section`);\n anchors.push(anchor);\n }\n sections.push(section);\n });\n for (const { where, href } of jumps) {\n if (href.startsWith(\"#\") && !anchors.includes(href.slice(1))) reading.fail(where, `jumps to ${href}, which no section on the page is named`);\n }\n return { sections, warnings };\n}\n\n// ---------------------------------------------------------------------------\n// Content pages and settings\n// ---------------------------------------------------------------------------\n\nexport function parseContentPage(document: unknown): PageContent {\n const reading = new Reading();\n const doc = reading.object(document, \"page\");\n if (doc === null) throw new MalformedContentError(\"page\", reading.problems);\n const id = typeof doc[\"_id\"] === \"string\" ? doc[\"_id\"] : \"page\";\n if (doc[\"_type\"] !== \"page\") reading.fail(\"_type\", `is ${JSON.stringify(doc[\"_type\"])}, not page`);\n const title = reading.optionalString(doc[\"title\"], \"title\");\n const titleTail = reading.optionalString(doc[\"titleTail\"], \"titleTail\");\n if (title === null) reading.fail(\"title\", \"is missing\");\n if (doc[\"titleCopySource\"] === \"proposed\" && title !== null) reading.proposed.push({ where: \"title\", text: title });\n const slugObject = doc[\"slug\"] as Raw | undefined;\n const slug = typeof slugObject?.[\"current\"] === \"string\" && slugObject[\"current\"] !== \"\" ? slugObject[\"current\"] : null;\n if (slug === null) reading.fail(\"slug.current\", \"is missing\");\n const lead = reading.copy(doc[\"lead\"], \"lead\");\n const photo = reading.photo(doc[\"photo\"], \"photo\");\n const body = richTextToBody(doc[\"body\"], \"body\", reading);\n if (doc[\"bodyCopySource\"] === \"proposed\") reading.proposed.push({ where: \"body\", text: bodyText(body) });\n const sections = doc[\"sections\"] === undefined || doc[\"sections\"] === null ? null : parseSections(doc[\"sections\"], reading);\n if (reading.problems.length > 0 || title === null || slug === null) throw new MalformedContentError(id, reading.problems);\n return {\n slug,\n title,\n ...(doc[\"titleTail\"] === undefined ? {} : { titleTail: titleTail ?? \"\" }),\n composition: { intro: lead === null ? null : plain(lead), photo, hero: null, blocks: [] },\n body,\n migrated: doc[\"migrated\"] === true,\n proposedCopy: reading.proposed,\n ...(sections === null ? {} : sections),\n };\n}\n\nexport function parseSiteSettings(document: unknown): SiteSettingsContent {\n const reading = new Reading();\n const doc = reading.object(document, \"siteSettings\");\n if (doc === null) throw new MalformedContentError(READER_SITE_SETTINGS_ID, reading.problems);\n const name = reading.optionalString(doc[\"title\"], \"title\");\n if (name === null) reading.fail(\"title\", \"is missing; a site needs its name\");\n const description = reading.optionalString(doc[\"description\"], \"description\");\n const contact = doc[\"contact\"] === undefined ? {} : (reading.object(doc[\"contact\"], \"contact\") ?? {});\n const email = reading.optionalString(contact[\"email\"], \"contact.email\");\n const phone = reading.optionalString(contact[\"phone\"], \"contact.phone\");\n const social: { network: string; url: string }[] = [];\n const rawSocial = doc[\"social\"] ?? [];\n if (!Array.isArray(rawSocial)) reading.fail(\"social\", \"is not a list\");\n else if (reading.dense(rawSocial, \"social\"))\n rawSocial.forEach((member, index) => {\n const link = reading.object(member, `social[${String(index)}]`);\n if (link === null) return;\n const network = reading.optionalString(link[\"network\"], `social[${String(index)}].network`);\n const url = reading.optionalString(link[\"url\"], `social[${String(index)}].url`);\n if (network === null || url === null || !/^https?:\\/\\//.test(url)) {\n reading.fail(`social[${String(index)}]`, \"needs a network and a web address\");\n return;\n }\n social.push({ network, url });\n });\n if (reading.problems.length > 0 || name === null) throw new MalformedContentError(READER_SITE_SETTINGS_ID, reading.problems);\n return { name, description, contact: { email, phone }, social };\n}\n\n// ---------------------------------------------------------------------------\n// Queries\n// ---------------------------------------------------------------------------\n\n/** By fixed id: a second home page at a random id is never read. */\nexport const HOME_PAGE_DOCUMENT_QUERY = `*[_id == $id][0]`;\n/** One snapshot contains both possible headline sources. */\nexport const HOME_PAGE_WITH_SITE_HERO_QUERY = `*[_id == $id][0]{..., \"_siteHero\": *[_id == \"siteHero\" && _type == \"siteHero\"][0]{heading}}`;\nconst MENU_PAGE_PROJECTION = `{..., \"sections\": coalesce(sections[]{..., _type == \"menuBlock\" => {menu->{${MENU_DOCUMENT_PROJECTION}}}, _type == \"linkListBlock\" => {links[]{..., \"page\": page->{title, \"slug\": slug.current}}}}, sections)}`;\nexport const CONTENT_PAGES_QUERY = `*[_type == \"page\" && defined(slug.current)]|order(slug.current asc)${MENU_PAGE_PROJECTION}`;\nexport const CONTENT_PAGE_BY_SLUG_QUERY = `*[_type == \"page\" && slug.current == $slug][0]${MENU_PAGE_PROJECTION}`;\n\nexport interface SiteContentReader {\n /** `null` when no home page document exists yet. */\n home: (options: { readonly now: Date }) => Promise<HomeContent | null>;\n pages: () => Promise<PageContent[]>;\n /** `null` when no page has that slug. */\n pageBySlug: (slug: string) => Promise<PageContent | null>;\n /** `null` before anyone has filled in the site's details. */\n siteSettings: () => Promise<SiteSettingsContent | null>;\n}\n\nexport function createSiteContentReader(client: { query: <T>(groq: string, options?: { params?: Record<string, unknown>; tags?: readonly string[] }) => Promise<T> }): SiteContentReader {\n return {\n home: async ({ now }) => {\n const doc = await client.query<unknown>(HOME_PAGE_WITH_SITE_HERO_QUERY, { params: { id: READER_HOME_PAGE_ID } });\n return doc === null || doc === undefined ? null : parseHomePage(doc, { now });\n },\n pages: async () => {\n const docs = await client.query<unknown>(CONTENT_PAGES_QUERY);\n if (!Array.isArray(docs)) throw new MalformedContentError(\"page\", [\"the page query did not return a list\"]);\n if (!isDenseArray(docs)) throw new MalformedContentError(\"page\", [\"the page query contains missing entries\"]);\n return docs.map((doc) => parseContentPage(doc));\n },\n pageBySlug: async (slug) => {\n const doc = await client.query<unknown>(CONTENT_PAGE_BY_SLUG_QUERY, { params: { slug } });\n return doc === null || doc === undefined ? null : parseContentPage(doc);\n },\n siteSettings: async () => {\n const doc = await client.query<unknown>(HOME_PAGE_DOCUMENT_QUERY, { params: { id: READER_SITE_SETTINGS_ID } });\n return doc === null || doc === undefined ? null : parseSiteSettings(doc);\n },\n };\n}\n\nfunction atomic<T>(raw: unknown, fallback: T, parse: (value: unknown) => T, validFallback: (value: unknown) => boolean): CmsResolution<T> {\n if (!validFallback(fallback)) throw new TypeError(\"Invalid CMS content fallback\");\n if (raw === null || raw === undefined) return { data: fallback, fallbackFields: [\"$\"] };\n // The server reader catches parser errors and classifies the whole fallback\n // as malformed. Returning fallback here would lose that diagnostic reason.\n return { data: parse(raw), fallbackFields: [] };\n}\n\nexport function homeContentQuery(now: Date): CmsQuery<HomeContent | null> {\n if (!(now instanceof Date) || !Number.isFinite(now.getTime())) throw new TypeError(\"Invalid CMS campaign date\");\n const captured = now.getTime();\n return defineCmsQuery({\n name: \"homePage.v1\", query: HOME_PAGE_WITH_SITE_HERO_QUERY, params: { id: READER_HOME_PAGE_ID },\n dependencies: { types: [\"homePage\", \"siteHero\"], ids: [READER_HOME_PAGE_ID, \"siteHero\"] },\n resolve: (raw, fallback) => atomic(raw, fallback, (value) => parseHomePage(value, { now: new Date(captured) }), validHomeFallback),\n });\n}\n\nexport function contentPageQuery(slug: string): CmsQuery<PageContent | null> {\n if (typeof slug !== \"string\" || slug === \"\") throw new TypeError(\"Invalid CMS page slug\");\n return defineCmsQuery({\n name: \"page.v1\", query: CONTENT_PAGE_BY_SLUG_QUERY, params: { slug }, dependencies: { types: [\"page\", \"menu\"] },\n resolve: (raw, fallback) => atomic(raw, fallback, parseContentPage, validPageFallback),\n });\n}\n\nexport function contentPagesQuery(): CmsQuery<readonly PageContent[]> {\n return defineCmsQuery({\n name: \"pages.v1\", query: CONTENT_PAGES_QUERY, params: {}, dependencies: { types: [\"page\", \"menu\"] },\n resolve: (raw, fallback) => atomic(raw, fallback, (value) => {\n if (!Array.isArray(value)) throw new MalformedContentError(\"page\", [\"the page query did not return a list\"]);\n if (!isDenseArray(value)) throw new MalformedContentError(\"page\", [\"the page query contains missing entries\"]);\n return value.map((document: unknown) => parseContentPage(document));\n }, (value) => isDenseArray(value) && value.every((page: unknown) => page !== null && validPageFallback(page))),\n });\n}\n\nexport function siteSettingsQuery(): CmsQuery<SiteSettingsContent | null> {\n return defineCmsQuery({\n name: \"siteSettings.v1\", query: HOME_PAGE_DOCUMENT_QUERY, params: { id: READER_SITE_SETTINGS_ID },\n dependencies: { types: [\"siteSettings\"], ids: [READER_SITE_SETTINGS_ID] },\n resolve: (raw, fallback) => atomic(raw, fallback, parseSiteSettings, validSettingsFallback),\n });\n}\n", "import { MalformedContentError } from \"../portable/site-content.js\";\nimport { MENU_DOCUMENT_PROJECTION } from \"../portable/menu-options.js\";\nimport { parseMenuDocument, validMenuFallback, type MenuDocument } from \"../portable/menu.js\";\n\n/** A-1822 S1 owns this implementation. S0 registers the public signatures. */\nexport type CmsParams = Readonly<Record<string, string | number | boolean | null>>;\nexport type CmsDependencies = {\n readonly types: readonly string[];\n readonly ids?: readonly string[];\n};\nexport type CmsResolution<T> = {\n readonly data: T;\n readonly fallbackFields: readonly string[];\n readonly warnings?: readonly (\"siteHero.layout.defaulted\" | \"siteHero.tagline.dropped\" | \"siteHero.taglines.defaulted\")[];\n};\nexport type CmsQuery<T> = {\n readonly name: string;\n readonly query: string;\n readonly params: CmsParams;\n readonly dependencies: CmsDependencies;\n readonly resolve: (raw: unknown, fallback: T) => CmsResolution<T>;\n};\n\nconst TYPE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;\nconst ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;\nconst SCOPE = /^[a-z0-9]{6,12}\\.[a-z][a-z0-9_-]{0,63}$/;\n\nfunction dependenciesCopy(dependencies: CmsDependencies): CmsDependencies {\n if (!dependencies || !Array.isArray(dependencies.types) || dependencies.types.length === 0) {\n throw new TypeError(\"CMS dependencies require document types\");\n }\n const types = [...dependencies.types];\n for (const type of types) {\n if (typeof type !== \"string\" || !TYPE.test(type)) throw new TypeError(\"Invalid CMS document type\");\n }\n if (dependencies.ids !== undefined && !Array.isArray(dependencies.ids)) throw new TypeError(\"Invalid CMS document IDs\");\n const ids = dependencies.ids === undefined ? undefined : [...dependencies.ids];\n for (const id of ids ?? []) {\n if (typeof id !== \"string\" || !ID.test(id) || /^(drafts|versions)\\./.test(id)) {\n throw new TypeError(\"Invalid CMS document ID\");\n }\n }\n if (new Set(types).size + new Set(ids).size > 128) throw new TypeError(\"Too many CMS tags\");\n return Object.freeze({ types: Object.freeze(types), ...(ids === undefined ? {} : { ids: Object.freeze(ids) }) });\n}\n\nexport function defineCmsQuery<T>(query: CmsQuery<T>): CmsQuery<T> {\n if (!query || typeof query.name !== \"string\" || !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(query.name)\n || typeof query.query !== \"string\" || query.query.trim() === \"\" || typeof query.resolve !== \"function\") {\n throw new TypeError(\"Invalid CMS query configuration\");\n }\n const params = query.params;\n if (!params || typeof params !== \"object\" || (Object.getPrototypeOf(params) !== Object.prototype && Object.getPrototypeOf(params) !== null)) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const copied: Record<string, string | number | boolean | null> = {};\n for (const key of Reflect.ownKeys(params)) {\n const descriptor = Object.getOwnPropertyDescriptor(params, key);\n if (typeof key !== \"string\" || !descriptor || !Object.hasOwn(descriptor, \"value\") || !descriptor.enumerable) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const value: unknown = descriptor.value;\n if (!(value === null || typeof value === \"string\" || typeof value === \"boolean\" || (typeof value === \"number\" && Number.isFinite(value)))) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n Object.defineProperty(copied, key, { value, enumerable: true });\n }\n return Object.freeze({ name: query.name, query: query.query, params: Object.freeze(copied), dependencies: dependenciesCopy(query.dependencies), resolve: query.resolve });\n}\n\nexport function cmsTags(scope: string, dependencies: CmsDependencies): readonly string[] {\n if (typeof scope !== \"string\" || scope.length > 77 || !SCOPE.test(scope)) throw new TypeError(\"Invalid CMS scope\");\n const copied = dependenciesCopy(dependencies);\n const tags = [\n ...[...new Set(copied.types)].sort().map((type) => `cms:${scope}:type:${type}`),\n ...[...new Set(copied.ids ?? [])].sort().map((id) => `cms:${scope}:id:${id}`),\n ];\n return Object.freeze(tags);\n}\n\n/** Read one published menu by its document id; host supplies the last good snapshot. */\nexport function menuQuery(id: string): CmsQuery<MenuDocument | null> {\n return defineCmsQuery({\n name: \"menu.v1\",\n query: `*[_type == \"menu\" && _id == $id && !(_id in path(\"drafts.**\")) && !(_id in path(\"versions.**\"))][0]{${MENU_DOCUMENT_PROJECTION}}`,\n params: { id }, dependencies: { types: [\"menu\"], ids: [id] },\n resolve: (raw, fallback) => {\n if (!validMenuFallback(fallback) || (fallback !== null && fallback._id !== id)) throw new TypeError(\"Invalid CMS menu fallback\");\n if (raw === null || raw === undefined) return { data: fallback, fallbackFields: [\"$\"] };\n const data = parseMenuDocument(raw);\n if (data._id !== id) throw new MalformedContentError(\"menu\", [\"menu._id does not match the requested document\"]);\n return { data, fallbackFields: [] };\n },\n });\n}\n", "import { cmsTags, defineCmsQuery, type CmsQuery, type CmsResolution } from \"../contracts/query.js\";\n\nexport type CmsFailure = \"configuration\" | \"network\" | \"timeout\" | \"rate-limit\"\n | \"upstream\" | \"plan-limit\" | \"permission\" | \"malformed\";\nexport type CmsDiagnostic = {\n readonly event: \"cms.fallback\" | \"cms.partial-fallback\" | \"cms.refused\";\n readonly query: string;\n readonly reason: CmsFailure | \"missing\" | \"invalid-field\";\n readonly status?: number;\n readonly fields: readonly string[];\n readonly warnings?: CmsResolution<unknown>[\"warnings\"];\n};\nexport type CmsFetch = (url: string, init: RequestInit & {\n next?: { revalidate: number; tags: string[] };\n}) => Promise<Response>;\nexport type CmsReadPolicy = {\n readonly tags: readonly string[];\n readonly revalidateSeconds: number;\n};\nexport type CmsAdapterResult =\n | { readonly ok: true; readonly value: unknown }\n | { readonly ok: false; readonly reason: CmsFailure; readonly status?: number };\nexport type CmsAdapter = {\n readonly scope: string;\n read(query: Pick<CmsQuery<unknown>, \"name\" | \"query\" | \"params\">,\n policy: CmsReadPolicy): Promise<CmsAdapterResult>;\n};\nexport type CmsReadResult<T> = {\n readonly data: T;\n readonly source: \"cms\" | \"mixed\" | \"fallback\";\n readonly fallbackFields: readonly string[];\n readonly reason?: CmsFailure | \"missing\" | \"invalid-field\";\n readonly warnings?: CmsResolution<unknown>[\"warnings\"];\n};\nexport type CmsReaderOptions = {\n readonly adapter: CmsAdapter;\n readonly revalidateSeconds?: number;\n readonly onDiagnostic?: (event: CmsDiagnostic) => void;\n};\nexport type CmsReader = {\n read<T>(query: CmsQuery<T>, options: { readonly fallback: T }): Promise<CmsReadResult<T>>;\n};\n\nfunction resolution<T>(value: CmsResolution<T>): CmsResolution<T> {\n if (!value || !Object.hasOwn(value, \"data\") || !Array.isArray(value.fallbackFields)\n || ![...value.fallbackFields].every((field: unknown) => typeof field === \"string\"\n && (field === \"$\" || /^[A-Za-z][A-Za-z0-9_.[\\]-]{0,127}$/.test(field)))) {\n throw new TypeError(\"Invalid CMS resolution\");\n }\n if (value.warnings !== undefined && (!Array.isArray(value.warnings)\n || ![...value.warnings].every((warning) => warning === \"siteHero.layout.defaulted\"\n || warning === \"siteHero.tagline.dropped\" || warning === \"siteHero.taglines.defaulted\"))) {\n throw new TypeError(\"Invalid CMS resolution warnings\");\n }\n return { data: value.data, fallbackFields: Object.freeze([...new Set(value.fallbackFields)]),\n ...(value.warnings?.length ? { warnings: Object.freeze([...new Set(value.warnings)]) } : {}) };\n}\n\n/** Forwards cache policy; persistence and rendered-output invalidation belong to the host. */\nexport function createCmsReader(options: CmsReaderOptions): CmsReader {\n const seconds = options?.revalidateSeconds ?? 3600;\n if (!Number.isSafeInteger(seconds) || seconds < 3600 || !options?.adapter\n || typeof options.adapter.read !== \"function\"\n || (options.onDiagnostic !== undefined && typeof options.onDiagnostic !== \"function\")) {\n throw new TypeError(\"Invalid CMS reader configuration\");\n }\n const adapter = options.adapter;\n // Validate the scope without assuming any particular shared content types.\n cmsTags(adapter.scope, { types: [\"ScopeValidation\"] });\n const notify = (event: CmsDiagnostic): void => {\n try {\n if (options.onDiagnostic) options.onDiagnostic(event);\n else console.warn(JSON.stringify(event));\n } catch { /* Reporting must not break visitor rendering. */ }\n };\n return {\n async read<T>(input: CmsQuery<T>, readOptions: { readonly fallback: T }): Promise<CmsReadResult<T>> {\n let query: CmsQuery<T>;\n let local: CmsResolution<T>;\n let tags: readonly string[];\n try {\n query = defineCmsQuery(input);\n tags = cmsTags(adapter.scope, query.dependencies);\n if (!readOptions || !Object.hasOwn(readOptions, \"fallback\")) throw new TypeError();\n // Built-in resolvers validate fallback here, before any I/O.\n local = resolution(query.resolve(null, readOptions.fallback));\n } catch {\n throw new TypeError(\"Invalid CMS query or fallback configuration\");\n }\n const fallback = (reason: CmsFailure | \"missing\" | \"invalid-field\", status?: number, warnings?: CmsResolution<T>[\"warnings\"]): CmsReadResult<T> => {\n notify({ event: \"cms.fallback\", query: query.name, reason, fields: [\"$\"],\n ...(status === undefined ? {} : { status }), ...(warnings ? { warnings } : {}) });\n return { data: local.data, source: \"fallback\", fallbackFields: [\"$\"], reason, ...(warnings ? { warnings } : {}) };\n };\n let result: CmsAdapterResult;\n try {\n result = await adapter.read({ name: query.name, query: query.query, params: query.params },\n { tags, revalidateSeconds: seconds });\n } catch { return fallback(\"network\"); }\n if (!result.ok) {\n const reasons: readonly CmsFailure[] = [\"configuration\", \"network\", \"timeout\", \"rate-limit\", \"upstream\", \"plan-limit\", \"permission\", \"malformed\"];\n const reason = reasons.includes(result.reason) ? result.reason : \"malformed\";\n const status = Number.isInteger(result.status) && (result.status ?? 0) >= 100 && (result.status ?? 0) <= 599 ? result.status : undefined;\n return fallback(reason, status);\n }\n if (result.value === null || result.value === undefined) return fallback(\"missing\");\n let resolved: CmsResolution<T>;\n try { resolved = resolution(query.resolve(result.value, readOptions.fallback)); }\n catch { return fallback(\"malformed\"); }\n if (resolved.fallbackFields.includes(\"$\")) {\n return { ...fallback(\"invalid-field\", undefined, resolved.warnings), data: resolved.data };\n }\n if (resolved.fallbackFields.length > 0) {\n notify({ event: \"cms.partial-fallback\", query: query.name, reason: \"invalid-field\", fields: resolved.fallbackFields, ...(resolved.warnings ? { warnings: resolved.warnings } : {}) });\n return { ...resolved, source: \"mixed\", reason: \"invalid-field\" };\n }\n return { ...resolved, source: \"cms\" };\n },\n };\n}\n"],
5
- "mappings": "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",
6
- "names": ["isDenseArray", "value", "index", "MENU_LAYOUTS", "MENU_DECORATIONS", "MENU_LAYOUT_DEFAULTS", "MENU_DOCUMENT_PROJECTION", "menuTags", "scope", "id", "cmsTags", "fail", "path", "reason", "MalformedContentError", "snapshot", "value", "ancestors", "identity", "array", "prototype", "keys", "copied", "length", "entries", "key", "at", "descriptor", "savedLength", "saved", "entry", "error", "object", "text", "required", "optional", "raw", "field", "list", "read", "isDenseArray", "index", "money", "currency", "price", "extra", "item", "availability", "section", "variant", "isPublishedMenuDocument", "parseMenuDocument", "document", "where", "parseSnapshot", "parseSnapshot", "document", "where", "raw", "object", "fail", "isPublishedMenuDocument", "updatedAt", "text", "variants", "list", "variant", "optional", "validMenuFallback", "value", "parseMenuDocument", "MALFORMED_CONTENT", "MalformedContentError", "documentId", "problems", "MENU_PAGE_PROJECTION", "MENU_DOCUMENT_PROJECTION", "CONTENT_PAGES_QUERY", "CONTENT_PAGE_BY_SLUG_QUERY", "TYPE", "ID", "SCOPE", "dependenciesCopy", "dependencies", "types", "type", "ids", "id", "defineCmsQuery", "query", "params", "copied", "key", "descriptor", "value", "cmsTags", "scope", "tags", "menuQuery", "MENU_DOCUMENT_PROJECTION", "raw", "fallback", "validMenuFallback", "data", "parseMenuDocument", "MalformedContentError", "resolution", "value", "field", "warning", "createCmsReader", "options", "seconds", "adapter", "cmsTags", "notify", "event", "input", "readOptions", "query", "local", "tags", "defineCmsQuery", "fallback", "reason", "status", "warnings", "result", "resolved"]
3
+ "sources": ["../src/server.ts", "../src/portable/array.ts", "../src/portable/menu-options.ts", "../src/portable/menu.ts", "../src/portable/site-content.ts", "../src/contracts/query.ts", "../src/runtime/reader.ts", "../src/runtime/site-readers.ts"],
4
+ "sourcesContent": ["import \"server-only\";\nexport { createCmsReader } from \"./runtime/reader.js\";\nexport type {\n CmsFailure, CmsDiagnostic, CmsFetch, CmsReadPolicy, CmsAdapterResult,\n CmsAdapter, CmsReadResult, CmsReaderOptions, CmsReader,\n} from \"./runtime/reader.js\";\nexport { MENU_LAYOUTS, MENU_DECORATIONS, MENU_LAYOUT_DEFAULTS, parseMenuDocument, menuTags } from \"./portable/menu.js\";\nexport { menuQuery } from \"./contracts/query.js\";\nexport type { MenuDocument, MenuVariant, MenuSection, MenuItem, MenuPrice, MenuExtra, MenuMoney, MenuLayout, MenuDecoration, MenuSectionInput } from \"./portable/menu.js\";\nexport { resolveWithFallback, parseVacancyDocument, parseVacancies, vacancyTags, openVacanciesQuery,\n parseLocationDocument, parseBusinessLocation, locationTags, locationsQuery, businessLocationsQuery,\n parseScheduleDocument, scheduleTags, scheduleQuery } from \"./runtime/site-readers.js\";\nexport type { Vacancy, LocationFields, BusinessLocationLike, ScheduleStop, WeeklyScheduleInput } from \"./runtime/site-readers.js\";\n", "/** Content lists must contain an own entry at every index. */\nexport function isDenseArray(value: unknown): value is unknown[] {\n if (!Array.isArray(value)) return false;\n for (let index = 0; index < value.length; index += 1) {\n if (!Object.hasOwn(value, index)) return false;\n }\n return true;\n}\n", "export const MENU_LAYOUTS = [\"stacked\", \"columns\"] as const;\nexport const MENU_DECORATIONS = [\"none\", \"playful\"] as const;\nexport const MENU_LAYOUT_DEFAULTS = { layout: \"stacked\", decoration: \"none\" } as const;\n\n/** Explicit saved fields, shared by the standalone query and page dereference. */\nexport const MENU_DOCUMENT_PROJECTION = `_type, _id, _rev, _updatedAt, title, locationKeys, note, signOff,\n variants[]{_key, label, note, sections[]{_key, title, service, items[]{\n _key, name, description, priceNote, dietary, allergenNote, availability,\n prices[]{_key, label, amount{minor, currency}}, extras[]{_key, name, amount{minor, currency}}\n }}}`;\n\n", "import { isDenseArray } from \"./array.js\";\nimport { MalformedContentError } from \"./site-content.js\";\nimport { cmsTags } from \"../contracts/query.js\";\n\nimport { MENU_LAYOUTS, MENU_DECORATIONS } from \"./menu-options.js\";\nexport { MENU_LAYOUTS, MENU_DECORATIONS, MENU_LAYOUT_DEFAULTS } from \"./menu-options.js\";\nexport type MenuLayout = (typeof MENU_LAYOUTS)[number];\nexport type MenuDecoration = (typeof MENU_DECORATIONS)[number];\n\n/** CMS-owned saved shapes. `_key` remains the mapper's stable array identity. */\nexport type MenuMoney = { readonly minor: number; readonly currency: string };\nexport type MenuPrice = { readonly _key: string; readonly amount: MenuMoney; readonly label?: string };\nexport type MenuExtra = { readonly _key: string; readonly name: string; readonly amount: MenuMoney };\nexport type MenuItem = {\n readonly _key: string; readonly name: string; readonly description?: string;\n readonly prices: readonly MenuPrice[]; readonly priceNote?: string;\n readonly extras: readonly MenuExtra[]; readonly dietary: readonly string[];\n readonly allergenNote?: string; readonly availability: \"available\" | \"sold-out\" | \"hidden\";\n};\nexport type MenuSection = { readonly _key: string; readonly title: string; readonly service?: string; readonly items: readonly MenuItem[] };\nexport type MenuVariant = { readonly _key: string; readonly label: string; readonly note?: string; readonly sections: readonly MenuSection[] };\nexport type MenuDocument = {\n /** The schema discriminator is required by the consuming site's mapper. */\n readonly _type: \"menu\";\n readonly _id: string; readonly _rev: string;\n /** The real published update date, preserved verbatim for stale displays. */\n readonly _updatedAt: string;\n readonly title: string; readonly locationKeys: readonly string[];\n readonly variants: readonly MenuVariant[]; readonly note?: string;\n /** Approval is opaque to this reader. The site validates it with its contract. */\n readonly signOff?: unknown;\n};\nexport type MenuSectionInput = { readonly kind: \"menu\"; readonly layout: MenuLayout; readonly decoration: MenuDecoration; readonly menu: MenuDocument };\n\n/** Uses the same scoped type/id tags as every other CMS query. */\nexport function menuTags(scope: string, id?: string): readonly string[] {\n return cmsTags(scope, { types: [\"menu\"], ...(id === undefined ? {} : { ids: [id] }) });\n}\n\ntype Raw = Record<string, unknown>;\nfunction fail(path: string, reason: string): never {\n throw new MalformedContentError(\"menu\", [`${path} ${reason}`]);\n}\n/** Capture each own data property once; never invoke an accessor or caller get trap. */\nfunction snapshot(value: unknown, path: string, ancestors = new Set<object>(), identity?: PropertyDescriptor): unknown {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"object\" || value === null) return fail(path, \"is not plain JSON data\");\n try {\n if (ancestors.has(value)) return fail(path, \"is not acyclic JSON data\");\n const array = Array.isArray(value);\n const prototype: unknown = Object.getPrototypeOf(value);\n if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {\n return fail(path, \"is not an ordinary saved object\");\n }\n ancestors.add(value);\n const keys = Reflect.ownKeys(value);\n const copied: Record<string, unknown> | unknown[] = array ? [] : {};\n let length = 0;\n let entries = 0;\n for (const key of keys) {\n if (typeof key !== \"string\") return fail(path, \"has a symbol key\");\n const at = array && key !== \"length\" ? `${path}[${key}]` : `${path}.${key}`;\n let descriptor: PropertyDescriptor | undefined;\n try { descriptor = key === \"_id\" && identity !== undefined ? identity : Object.getOwnPropertyDescriptor(value, key); }\n catch { return fail(at, \"could not be snapshotted as plain JSON data\"); }\n if (!descriptor || !Object.hasOwn(descriptor, \"value\")) return fail(at, \"is not a saved data property\");\n if (array && key === \"length\") {\n const savedLength: unknown = descriptor.value;\n if (typeof savedLength !== \"number\" || !Number.isSafeInteger(savedLength) || savedLength < 0) return fail(at, \"is not a saved list length\");\n length = savedLength;\n continue;\n }\n if (!descriptor.enumerable || (array && !/^(0|[1-9]\\d*)$/.test(key))) return fail(at, \"is not a JSON data property\");\n const saved: unknown = descriptor.value;\n const entry = snapshot(saved, at, ancestors);\n Object.defineProperty(copied, key, { value: entry, enumerable: true, writable: true, configurable: true });\n entries++;\n }\n if (array && (entries !== length || (copied as unknown[]).length !== length)) return fail(path, \"is not a complete saved list\");\n return copied;\n } catch (error) {\n if (error instanceof MalformedContentError) throw error;\n return fail(path, \"could not be snapshotted as plain JSON data\");\n } finally {\n ancestors.delete(value);\n }\n}\nfunction object(value: unknown, path: string): Raw {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)\n || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) return fail(path, \"is not an ordinary saved object\");\n return value as Raw;\n}\nfunction text(value: unknown, path: string, required = true): string {\n if (typeof value !== \"string\" || (required && value.trim() === \"\")) return fail(path, \"is not saved text\");\n return value;\n}\nfunction optional(raw: Raw, field: string, path: string): Record<string, string> {\n return raw[field] === undefined || raw[field] === null ? {} : { [field]: text(raw[field], `${path}.${field}`, false) };\n}\nfunction list<T>(value: unknown, path: string, read: (value: unknown, path: string) => T): T[] {\n if (value === undefined || value === null) return [];\n if (!isDenseArray(value)) return fail(path, \"is not a complete saved list\");\n return value.map((entry, index) => read(entry, `${path}[${String(index)}]`));\n}\nfunction money(value: unknown, path: string): MenuMoney {\n const raw = object(value, path);\n if (typeof raw.minor !== \"number\" || !Number.isSafeInteger(raw.minor) || raw.minor < 0) return fail(`${path}.minor`, \"is not a whole non-negative safe amount\");\n const currency = text(raw.currency, `${path}.currency`);\n if (!/^[A-Z]{3}$/.test(currency)) return fail(`${path}.currency`, \"is not a three-letter currency code\");\n return { minor: raw.minor, currency };\n}\nfunction price(value: unknown, path: string): MenuPrice {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), amount: money(raw.amount, `${path}.amount`), ...optional(raw, \"label\", path) };\n}\nfunction extra(value: unknown, path: string): MenuExtra {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), name: text(raw.name, `${path}.name`), amount: money(raw.amount, `${path}.amount`) };\n}\nfunction item(value: unknown, path: string): MenuItem {\n const raw = object(value, path);\n const availability = raw.availability ?? \"available\";\n if (availability !== \"available\" && availability !== \"sold-out\" && availability !== \"hidden\") return fail(`${path}.availability`, \"is not an availability choice\");\n return {\n _key: text(raw._key, `${path}._key`), name: text(raw.name, `${path}.name`),\n ...optional(raw, \"description\", path), ...optional(raw, \"priceNote\", path), ...optional(raw, \"allergenNote\", path),\n prices: list(raw.prices, `${path}.prices`, price), extras: list(raw.extras, `${path}.extras`, extra),\n dietary: list(raw.dietary, `${path}.dietary`, text), availability,\n };\n}\nfunction section(value: unknown, path: string): MenuSection {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), title: text(raw.title, `${path}.title`), ...optional(raw, \"service\", path), items: list(raw.items, `${path}.items`, item) };\n}\nfunction variant(value: unknown, path: string): MenuVariant {\n const raw = object(value, path);\n return { _key: text(raw._key, `${path}._key`), label: text(raw.label, `${path}.label`), ...optional(raw, \"note\", path), sections: list(raw.sections, `${path}.sections`, section) };\n}\n\n/** A reference has resolved only when it carries a published document identity. */\nfunction isPublishedMenuDocument(value: unknown): boolean {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return false;\n const id = (value as Raw)._id;\n return typeof id === \"string\" && id.trim() !== \"\" && !/^(drafts|versions)\\./.test(id);\n}\n\n/** Validate saved field shapes without inferring approval or changing copy/order. */\nexport function parseMenuDocument(document: unknown, where = \"menu\"): MenuDocument {\n return parseSnapshot(snapshot(document, where), where);\n}\n\n/** Page references use the same single snapshot before checking their identity. */\nexport function parseReferencedMenuDocument(document: unknown, where: string): MenuDocument | null {\n if (document === undefined || document === null) return null;\n if (typeof document !== \"object\") return null;\n // Missing/draft references remain warnings. Cache the identity descriptor so\n // an available document still contributes each caller property exactly once.\n let identity: PropertyDescriptor | undefined;\n try { identity = Object.getOwnPropertyDescriptor(document, \"_id\"); }\n catch { return fail(`${where}._id`, \"could not be snapshotted as plain JSON data\"); }\n if (identity && !Object.hasOwn(identity, \"value\")) return fail(`${where}._id`, \"is not a saved data property\");\n const id: unknown = identity?.value;\n if (typeof id !== \"string\" || id.trim() === \"\" || /^(drafts|versions)\\./.test(id)) return null;\n return parseSnapshot(snapshot(document, where, new Set(), identity), where);\n}\n\nfunction parseSnapshot(document: unknown, where: string): MenuDocument {\n const raw = object(document, where);\n if (raw._type !== \"menu\") return fail(`${where}._type`, \"is not menu\");\n if (!isPublishedMenuDocument(raw)) return fail(`${where}._id`, \"is not a published menu identity\");\n const updatedAt = text(raw._updatedAt, `${where}._updatedAt`);\n if (!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/.test(updatedAt) || !Number.isFinite(Date.parse(updatedAt))) return fail(`${where}._updatedAt`, \"is not an update date\");\n const variants = list(raw.variants, `${where}.variants`, variant);\n if (variants.length === 0) return fail(`${where}.variants`, \"has no menu choices\");\n return {\n _type: \"menu\", _id: text(raw._id, `${where}._id`), _rev: text(raw._rev, `${where}._rev`), _updatedAt: updatedAt,\n title: text(raw.title, `${where}.title`), locationKeys: list(raw.locationKeys, `${where}.locationKeys`, text),\n variants, ...optional(raw, \"note\", where), ...(Object.hasOwn(raw, \"signOff\") ? { signOff: raw.signOff } : {}),\n };\n}\n\nexport function validMenuFallback(value: unknown): value is MenuDocument | null {\n if (value === null) return true;\n try { parseMenuDocument(value); return true; } catch { return false; }\n}\n", "/**\n * A-1713. The site content model, read: the home page, content pages and site\n * settings, validated on read and returned in the shape a site's page\n * composition consumes.\n *\n * ===========================================================================\n * READ ONLY, BY CONSTRUCTION\n * ===========================================================================\n *\n * A client site reads its content with a read-only token. All editing goes\n * through the studio's dashboard, which writes to Sanity; nothing on a client\n * site writes, and this module has no write path to call. It is handed a\n * `ContentClient` (a `query` function) and nothing else.\n *\n * ===========================================================================\n * VALIDATED ON READ, BECAUSE SANITY VALIDATION ONLY ADVISES\n * ===========================================================================\n *\n * The schema's rules warn an editor; they do not refuse a publish, and\n * anything writing over the API skips them entirely. So every document is\n * checked here again, and a malformed one is refused BY NAME\n * (`MalformedContentError`, code `malformed-content`) listing every problem\n * with its path, rather than rendered half-empty. A missing home headline\n * uses siteHero only when the home document explicitly selects it and that\n * document supplies a valid heading. Otherwise the home page is refused.\n *\n * Absent is not malformed: `null` from `home()` means nobody has created the\n * home page yet, and a site decides what to do about that.\n *\n * ===========================================================================\n * THE SHAPE IT RETURNS\n * ===========================================================================\n *\n * The INPUT shape of a site's composition schema (the reference build's\n * frontmatter): a copy value is a plain string for the client's own words\n * with no claim about their source, `{ text, copySource: \"captured\" }` for\n * words claimed verbatim from the capture (the site checks every such claim\n * against its capture evidence, so the claim must survive Sanity), and\n * `{ text, copySource: \"proposed\" }` for studio-written words awaiting\n * approval. All three round-trip exactly; a photo is its library slug; every key a block declares is\n * present, `null` where there is nothing. So a site validates this with the\n * same schema it validates its Markdown frontmatter with, and its components\n * never know where the content came from.\n *\n * Mapping from the generic model to the composition's names:\n *\n * hero.headline / subline / lead hero.heading / subline / intro\n * hero.photos[0], [1] hero.image, hero.inset\n * hero.primaryAction / secondary... hero.cta / secondaryCta\n * campaign (when shown) the FIRST block, kind `campaign`, and\n * the page body (`# title` + its text)\n * featureStoriesBlock kind `feature_stories`\n * surfaceCardBlock kind `cta`\n * accentBandBlock kind `accent_band`\n * locationsBlock kind `locations` (jumpLink \u2192 anchor)\n *\n * A content page's `sections[]` (A-1915) reads those four kinds plus\n * `text_media`, `faq`, `gallery` and `contact` into `PageContent.sections`,\n * with their named warnings in `PageContent.warnings`. `sections.ts` holds the\n * kinds, the A-1876 layout names and the warning codes.\n *\n * Two places the composition cannot carry a proposed marker: a `cta` block's\n * plain strings, and the campaign's title and body (which travel as rich body content).\n * There the text is returned plain and the line is still listed in\n * `proposedCopy`, which is the list a dashboard and a site both read for\n * approval. Nothing is dropped silently.\n */\n\nimport { parseReferencedMenuDocument } from \"./menu.js\";\nimport { MENU_DECORATIONS, MENU_DOCUMENT_PROJECTION, MENU_LAYOUT_DEFAULTS, MENU_LAYOUTS } from \"./menu-options.js\";\nimport { isDenseArray } from \"./array.js\";\nimport { defineCmsQuery, type CmsQuery, type CmsResolution } from \"../contracts/query.js\";\nimport { SITE_HERO, validateTextDocument } from \"../contracts/text.js\";\nimport { validHomeFallback, validPageFallback, validSettingsFallback } from \"./fallback.js\";\nimport {\n HEADING_LIST_LAYOUTS, READER_GALLERY_PHOTO_LIMIT, READER_GALLERY_PHOTO_MIN, SECTION_LAYOUT_DEFAULTS, SECTION_WARNINGS,\n SPLIT_LAYOUTS, STAGGER_ROW_LAYOUTS,\n type FaqSectionInput, type PageSectionInput, type SectionImageInput, type SectionWarning,\n} from \"./sections.js\";\n\n// ---------------------------------------------------------------------------\n// Output types\n// ---------------------------------------------------------------------------\n\nexport type CopyInput = string | { readonly text: string; readonly copySource: \"captured\" | \"proposed\" };\nexport type RichInline = {\n readonly children: readonly { readonly text: string; readonly marks: readonly string[] }[];\n readonly markDefs: readonly { readonly key: string; readonly href: string }[];\n};\nexport type RichBodyPart =\n | { readonly kind: \"block\"; readonly style: \"normal\" | \"h2\" | \"h3\" | \"blockquote\"; readonly listItem: \"bullet\" | \"number\" | null; readonly level: number; readonly content: RichInline }\n | { readonly kind: \"heading\"; readonly text: string }\n | { readonly kind: \"table\"; readonly rows: readonly (readonly RichInline[])[] };\nexport type RichBody = string | readonly RichBodyPart[];\nexport type ActionInput = { readonly label: CopyInput; readonly href: string };\n\nexport type HeroInput = {\n readonly heading: CopyInput;\n readonly subline: CopyInput | null;\n readonly intro: CopyInput | null;\n readonly image: string | null;\n readonly inset: string | null;\n readonly cta: ActionInput | null;\n readonly secondaryCta: ActionInput | null;\n};\n\nexport type StoryInput = {\n readonly heading: CopyInput;\n readonly body: CopyInput;\n readonly action: ActionInput;\n readonly image: string | null;\n readonly shape: \"wide\" | \"tall\" | \"round\" | \"free\";\n readonly card: boolean;\n};\n\nexport type CompositionBlockInput =\n | { readonly kind: \"campaign\"; readonly action: ActionInput | null; readonly images: readonly string[] }\n | {\n readonly kind: \"locations\";\n readonly anchor: string | null;\n readonly heading: CopyInput;\n readonly tail: CopyInput | null;\n readonly image: string | null;\n }\n | {\n readonly kind: \"feature_stories\";\n readonly heading: CopyInput;\n readonly tail: CopyInput | null;\n readonly items: readonly StoryInput[];\n }\n | {\n readonly kind: \"accent_band\";\n readonly heading: CopyInput;\n readonly tail?: CopyInput | null;\n readonly body: CopyInput;\n readonly action: ActionInput | null;\n readonly image: string | null;\n }\n | {\n readonly kind: \"cta\";\n readonly heading: string;\n readonly image?: string | null;\n readonly body: string | null;\n readonly link: { readonly label: string; readonly href: string };\n };\n\nexport type CompositionInput = {\n readonly intro: string | null;\n readonly photo: string | null;\n readonly hero: HeroInput | null;\n readonly blocks: readonly CompositionBlockInput[];\n};\n\n/** One line of studio-written copy awaiting approval, and where it is in the document. */\nexport type ProposedCopy = { readonly where: string; readonly text: string };\n\nexport type CampaignState = \"shown\" | \"off\" | \"not-started\" | \"ended\" | \"none\";\n\nexport type HomeContent = {\n readonly composition: CompositionInput;\n /** Campaign text as Markdown, or structured blocks and tables when it contains a table. */\n readonly body: RichBody;\n readonly campaign: CampaignState;\n readonly proposedCopy: readonly ProposedCopy[];\n};\n\nexport type PageContent = {\n readonly slug: string;\n readonly title: string;\n readonly titleTail?: string;\n readonly composition: CompositionInput;\n readonly body: RichBody;\n readonly migrated: boolean;\n readonly proposedCopy: readonly ProposedCopy[];\n /**\n * A-1915. Present only when the stored page has a `sections` list, so a page\n * written before sections existed reads exactly as it did in 0.2.0.\n */\n readonly sections?: readonly PageSectionInput[];\n /** A-1915. Present with `sections`: every named section warning, with its path. */\n readonly warnings?: readonly SectionWarning[];\n};\n\nexport type SiteSettingsContent = {\n readonly name: string;\n readonly description: string | null;\n readonly contact: { readonly email: string | null; readonly phone: string | null };\n readonly social: readonly { readonly network: string; readonly url: string }[];\n};\n\n// ---------------------------------------------------------------------------\n// The refusal\n// ---------------------------------------------------------------------------\n\nexport const MALFORMED_CONTENT = \"malformed-content\";\n\nexport class MalformedContentError extends Error {\n readonly code = MALFORMED_CONTENT;\n readonly documentId: string;\n readonly problems: readonly string[];\n\n constructor(documentId: string, problems: readonly string[]) {\n super(`${MALFORMED_CONTENT}: ${documentId}: ${problems.join(\"; \")}`);\n this.name = \"MalformedContentError\";\n this.documentId = documentId;\n this.problems = problems;\n }\n}\n\n// ---------------------------------------------------------------------------\n// The rules restated\n// ---------------------------------------------------------------------------\n\n/*\n * Runtime-free rules shared with schema-base. The schema adapter imports these\n * objects; content-client compatibility tests pin the shared references.\n */\nexport const READER_PHOTO_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nexport const READER_ACTION_HREF =\n /^(?:\\/(?![/\\\\])[^\\s]*|#[a-z][a-z0-9]*(?:-[a-z0-9]+)*|mailto:[^\\s@]+@[^\\s@]+|tel:\\+?[0-9][0-9 ()-]*|https?:\\/\\/[^\\s/]+[^\\s]*)$/;\nexport const READER_JUMP_LINK = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;\nexport const READER_STORY_LAYOUTS = [\"wide\", \"tall\", \"round\", \"free\"] as const;\nexport const READER_HOME_PAGE_ID = \"homePage\";\nexport const READER_SITE_SETTINGS_ID = \"siteSettings\";\n\n// ---------------------------------------------------------------------------\n// A small path-aware reader\n// ---------------------------------------------------------------------------\n\ntype Raw = Record<string, unknown>;\n\nclass Reading {\n readonly problems: string[] = [];\n readonly proposed: ProposedCopy[] = [];\n\n fail(path: string, message: string): null {\n this.problems.push(`${path} ${message}`);\n return null;\n }\n\n object(value: unknown, path: string): Raw | null {\n if (typeof value === \"object\" && value !== null && !Array.isArray(value)) return value as Raw;\n return this.fail(path, \"is not an object\");\n }\n\n dense(value: unknown[], path: string): boolean {\n if (isDenseArray(value)) return true;\n this.fail(path, \"contains missing entries\");\n return false;\n }\n\n optionalString(value: unknown, path: string): string | null {\n if (value === undefined || value === null) return null;\n if (typeof value === \"string\") return value.trim() === \"\" ? null : value;\n return this.fail(path, \"is not text\");\n }\n\n /** A copy value, or null when absent or empty. Records a proposed line. */\n copy(value: unknown, path: string): CopyInput | null {\n if (value === undefined || value === null) return null;\n const raw = this.object(value, path);\n if (raw === null) return null;\n const text = raw[\"text\"];\n const source = raw[\"copySource\"];\n if (text !== undefined && typeof text !== \"string\") return this.fail(`${path}.text`, \"is not text\");\n if (source !== undefined && source !== \"captured\" && source !== \"proposed\") {\n return this.fail(`${path}.copySource`, `is ${JSON.stringify(source)}, not captured or proposed`);\n }\n if (typeof text !== \"string\" || text.trim() === \"\") return null;\n if (source === \"proposed\") {\n this.proposed.push({ where: path, text });\n return { text, copySource: \"proposed\" };\n }\n if (source === \"captured\") return { text, copySource: \"captured\" };\n return text;\n }\n\n requiredCopy(value: unknown, path: string): CopyInput | null {\n const copy = this.copy(value, path);\n if (copy === null && !this.problems.some((p) => p.startsWith(path))) this.fail(path, \"is missing its words\");\n return copy;\n }\n\n photo(value: unknown, path: string): string | null {\n if (value === undefined || value === null) return null;\n if (typeof value === \"string\" && READER_PHOTO_SLUG.test(value)) return value;\n return this.fail(path, `is ${JSON.stringify(value)}, not a photo library slug`);\n }\n\n photos(value: unknown, path: string, max: number): string[] {\n if (value === undefined || value === null) return [];\n if (!Array.isArray(value)) {\n this.fail(path, \"is not a list of photos\");\n return [];\n }\n if (!this.dense(value, path)) return [];\n if (value.length > max) this.fail(path, `holds ${String(value.length)} photos; at most ${String(max)}`);\n return value.map((entry, index) => this.photo(entry, `${path}[${String(index)}]`)).filter((p): p is string => p !== null);\n }\n\n action(value: unknown, path: string): ActionInput | null {\n if (value === undefined || value === null) return null;\n const raw = this.object(value, path);\n if (raw === null) return null;\n const label = this.requiredCopy(raw[\"label\"], `${path}.label`);\n const href = raw[\"href\"];\n if (typeof href !== \"string\" || !READER_ACTION_HREF.test(href)) {\n return this.fail(`${path}.href`, `is ${JSON.stringify(href)}, not a site path, #jump-link, mailto:, tel: or web address`);\n }\n return label === null ? null : { label, href };\n }\n\n requiredAction(value: unknown, path: string): ActionInput | null {\n if (value === undefined || value === null) return this.fail(path, \"is missing\");\n return this.action(value, path);\n }\n}\n\nfunction plain(copy: CopyInput): string {\n return typeof copy === \"string\" ? copy : copy.text;\n}\n\n// ---------------------------------------------------------------------------\n// Portable Text to rich body content\n// ---------------------------------------------------------------------------\n\n/**\n * The narrow Portable Text `portableText` declares, back to Markdown: normal,\n * h2, h3 and quote blocks, bullet and numbered lists, bold, italic, code and\n * links. It is the inverse of the seed package's Markdown mapper, and the\n * round-trip test holds the two to each other.\n *\n * An uploaded image inside rich text is REFUSED, not dropped: nothing can\n * upload one yet (the dashboard slice decides how), so one arriving means\n * something wrote the document by hand, and a page silently missing a picture\n * is the failure this reader exists to prevent.\n * Text-only input remains a Markdown string. Input with a table produces\n * ordered structured blocks and cells that never enter a Markdown parser.\n */\nexport function portableTextToMarkdown(value: unknown): RichBody {\n const reading = new Reading();\n const markdown = richTextToBody(value, \"body\", reading);\n if (reading.problems.length > 0) throw new MalformedContentError(\"body\", reading.problems);\n return markdown;\n}\n\nfunction richTextToBody(value: unknown, path: string, reading: Reading): RichBody {\n if (value === undefined || value === null) return \"\";\n if (!Array.isArray(value)) {\n reading.fail(path, \"is not rich text\");\n return \"\";\n }\n if (!reading.dense(value, path)) return \"\";\n if (value.some((member) => member !== null && typeof member === \"object\" && !Array.isArray(member) && (member as Raw)[\"_type\"] === \"portableTable\")) {\n return richTextToParts(value, path, reading);\n }\n const out: string[] = [];\n let previousWasList = false;\n value.forEach((member, index) => {\n const at = `${path}[${String(index)}]`;\n const block = reading.object(member, at);\n if (block === null) return;\n if (block[\"_type\"] !== \"block\") {\n reading.fail(at, `is a ${JSON.stringify(block[\"_type\"])}; only text is supported in rich text so far`);\n return;\n }\n const text = spansToMarkdown(block, at, reading);\n const listItem = block[\"listItem\"];\n const style = block[\"style\"] ?? \"normal\";\n let line: string;\n if (listItem === \"bullet\" || listItem === \"number\") {\n const level = typeof block[\"level\"] === \"number\" && block[\"level\"] > 1 ? block[\"level\"] : 1;\n line = `${\" \".repeat(level - 1)}${listItem === \"bullet\" ? \"-\" : \"1.\"} ${text}`;\n } else if (style === \"h2\") line = `## ${text}`;\n else if (style === \"h3\") line = `### ${text}`;\n else if (style === \"blockquote\") line = `> ${text}`;\n else if (style === \"normal\") line = text;\n else {\n reading.fail(`${at}.style`, `is ${JSON.stringify(style)}, which the schema does not offer`);\n return;\n }\n const isList = listItem === \"bullet\" || listItem === \"number\";\n if (out.length > 0) out.push(isList && previousWasList ? \"\\n\" : \"\\n\\n\");\n out.push(line);\n previousWasList = isList;\n });\n return out.join(\"\");\n}\n\nfunction bodyText(body: RichBody): string {\n return typeof body === \"string\" ? body : body.map((part) => part.kind === \"heading\" ? part.text\n : part.kind === \"block\" ? part.content.children.map((child) => child.text).join(\"\")\n : part.rows.map((row) => row.map((cell) => cell.children.map((child) => child.text).join(\"\")).join(\" \")).join(\"\\n\")).join(\"\\n\\n\");\n}\n\nfunction inlineFromBlock(block: Raw, path: string, reading: Reading): RichInline {\n const rawDefs = Array.isArray(block[\"markDefs\"]) ? block[\"markDefs\"] as unknown[] : [];\n if (!reading.dense(rawDefs, `${path}.markDefs`)) return { children: [], markDefs: [] };\n const markDefs: { key: string; href: string }[] = [];\n rawDefs.forEach((entry, index) => {\n const def = reading.object(entry, `${path}.markDefs[${String(index)}]`);\n if (def === null) return;\n if (def[\"_type\"] !== \"link\" || typeof def[\"_key\"] !== \"string\" || typeof def[\"href\"] !== \"string\") {\n reading.fail(`${path}.markDefs[${String(index)}]`, \"is not a link annotation\");\n return;\n }\n markDefs.push({ key: def[\"_key\"], href: def[\"href\"] });\n });\n const rawChildren = block[\"children\"];\n if (!Array.isArray(rawChildren) || !reading.dense(rawChildren, `${path}.children`)) {\n reading.fail(`${path}.children`, \"needs text spans\");\n return { children: [], markDefs };\n }\n const children: { text: string; marks: string[] }[] = [];\n rawChildren.forEach((entry, index) => {\n const at = `${path}.children[${String(index)}]`;\n const span = reading.object(entry, at);\n if (span === null) return;\n if (span[\"_type\"] !== \"span\" || typeof span[\"text\"] !== \"string\") {\n reading.fail(at, \"is not a text span\");\n return;\n }\n const marks = span[\"marks\"] === undefined ? [] : span[\"marks\"];\n if (!Array.isArray(marks) || !reading.dense(marks, `${at}.marks`) || marks.some((mark) => typeof mark !== \"string\"\n || (![\"strong\", \"em\", \"code\"].includes(mark) && !markDefs.some((def) => def.key === mark)))) {\n reading.fail(`${at}.marks`, \"contains an unsupported mark\");\n return;\n }\n children.push({ text: span[\"text\"], marks });\n });\n return { children, markDefs };\n}\n\nfunction richTextToParts(value: unknown[], path: string, reading: Reading): RichBodyPart[] {\n const parts: RichBodyPart[] = [];\n value.forEach((member, index) => {\n const at = `${path}[${String(index)}]`;\n const block = reading.object(member, at);\n if (block === null) return;\n if (block[\"_type\"] === \"portableTable\") {\n parts.push(tableToData(block, at, reading));\n return;\n }\n if (block[\"_type\"] !== \"block\") {\n reading.fail(at, `is a ${JSON.stringify(block[\"_type\"])}; only text is supported in rich text so far`);\n return;\n }\n const style = block[\"style\"] ?? \"normal\";\n if (style !== \"normal\" && style !== \"h2\" && style !== \"h3\" && style !== \"blockquote\") {\n reading.fail(`${at}.style`, `is ${JSON.stringify(style)}, which the schema does not offer`);\n return;\n }\n const listItem = block[\"listItem\"];\n const level = typeof block[\"level\"] === \"number\" && block[\"level\"] > 1 ? block[\"level\"] : 1;\n parts.push({ kind: \"block\", style, listItem: listItem === \"bullet\" || listItem === \"number\" ? listItem : null,\n level, content: inlineFromBlock(block, at, reading) });\n });\n return parts;\n}\n\nfunction tableToData(table: Raw, path: string, reading: Reading): Extract<RichBodyPart, { kind: \"table\" }> {\n const rows = table[\"rows\"];\n if (!Array.isArray(rows) || rows.length === 0 || !reading.dense(rows, `${path}.rows`)) {\n reading.fail(`${path}.rows`, \"needs at least one row\");\n return { kind: \"table\", rows: [] };\n }\n let width = 0;\n const parsedRows: RichInline[][] = [];\n rows.forEach((entry, index) => {\n const at = `${path}.rows[${String(index)}]`;\n const row = reading.object(entry, at);\n if (row === null) return;\n const cells = row[\"cells\"];\n if (!Array.isArray(cells) || cells.length === 0 || !reading.dense(cells, `${at}.cells`)) {\n reading.fail(`${at}.cells`, \"needs at least one cell\");\n return;\n }\n if (index === 0) width = cells.length;\n if (cells.length !== width) reading.fail(`${at}.cells`, \"has a different number of cells\");\n const parsedCells = cells.map((cell, cellIndex) => {\n const cat = `${at}.cells[${String(cellIndex)}]`;\n const inline = typeof cell === \"string\"\n ? { children: [{ text: cell, marks: [] }], markDefs: [] }\n : (() => {\n const rawCell = reading.object(cell, cat);\n if (rawCell === null) return { children: [], markDefs: [] };\n if (rawCell[\"_type\"] !== \"block\" || (rawCell[\"style\"] !== undefined && rawCell[\"style\"] !== \"normal\") || rawCell[\"listItem\"] !== undefined) {\n reading.fail(cat, \"is not an inline text block\");\n return { children: [], markDefs: [] };\n }\n return inlineFromBlock(rawCell, cat, reading);\n })();\n if (inline.children.map((child) => child.text).join(\"\").trim() === \"\") reading.fail(cat, \"is empty or not text\");\n return inline;\n });\n parsedRows.push(parsedCells);\n });\n return { kind: \"table\", rows: parsedRows };\n}\n\nconst DECORATORS: Record<string, string> = { strong: \"**\", em: \"*\", code: \"`\" };\n\nfunction spansToMarkdown(block: Raw, path: string, reading: Reading): string {\n const links = new Map<string, string>();\n const markDefs = Array.isArray(block[\"markDefs\"]) ? (block[\"markDefs\"] as unknown[]) : [];\n if (!reading.dense(markDefs, `${path}.markDefs`)) return \"\";\n for (const def of markDefs) {\n const d = def as Raw;\n if (d[\"_type\"] === \"link\" && typeof d[\"_key\"] === \"string\" && typeof d[\"href\"] === \"string\") links.set(d[\"_key\"], d[\"href\"]);\n }\n const children = Array.isArray(block[\"children\"]) ? (block[\"children\"] as unknown[]) : [];\n if (!reading.dense(children, `${path}.children`)) return \"\";\n return children\n .map((child, index) => {\n const span = child as Raw;\n if (span[\"_type\"] !== \"span\" || typeof span[\"text\"] !== \"string\") {\n reading.fail(`${path}.children[${String(index)}]`, \"is not a text span\");\n return \"\";\n }\n let text = span[\"text\"];\n const marks = Array.isArray(span[\"marks\"]) ? (span[\"marks\"] as unknown[]) : [];\n if (!reading.dense(marks, `${path}.children[${String(index)}].marks`)) return \"\";\n let href: string | null = null;\n for (const mark of marks) {\n if (typeof mark !== \"string\") continue;\n const fence = DECORATORS[mark];\n if (fence !== undefined) text = `${fence}${text}${fence}`;\n else if (links.has(mark)) href = links.get(mark) ?? null;\n else reading.fail(`${path}.children[${String(index)}].marks`, `names ${JSON.stringify(mark)}, which is neither a style nor a link`);\n }\n return href === null ? text : `[${text}](${href})`;\n })\n .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// The home page\n// ---------------------------------------------------------------------------\n\n/** Whether a campaign is shown at `now`, and if not, why not. */\nexport function campaignState(campaign: unknown, now: Date): CampaignState {\n if (typeof campaign !== \"object\" || campaign === null) return \"none\";\n const c = campaign as Raw;\n if (c[\"enabled\"] !== true) return \"off\";\n const starts = typeof c[\"startsAt\"] === \"string\" ? Date.parse(c[\"startsAt\"]) : null;\n const ends = typeof c[\"endsAt\"] === \"string\" ? Date.parse(c[\"endsAt\"]) : null;\n if (starts !== null && now.getTime() < starts) return \"not-started\";\n if (ends !== null && now.getTime() >= ends) return \"ended\";\n return \"shown\";\n}\n\nfunction keyed(path: string, raw: Raw, index: number): string {\n return typeof raw[\"_key\"] === \"string\" ? `${path}[_key==\"${raw[\"_key\"]}\"]` : `${path}[${String(index)}]`;\n}\n\n/**\n * A stored home page document, validated and mapped. Throws\n * `MalformedContentError` naming every problem. `now` decides whether a\n * scheduled campaign is shown, and is a parameter so a build, a preview and a\n * test each say which moment they mean.\n */\nexport function parseHomePage(document: unknown, options: { readonly now: Date }): HomeContent {\n const reading = new Reading();\n const doc = reading.object(document, \"homePage\");\n if (doc === null) throw new MalformedContentError(READER_HOME_PAGE_ID, reading.problems);\n const id = typeof doc[\"_id\"] === \"string\" ? doc[\"_id\"] : READER_HOME_PAGE_ID;\n if (doc[\"_type\"] !== \"homePage\") reading.fail(\"_type\", `is ${JSON.stringify(doc[\"_type\"])}, not homePage`);\n\n let hero: HeroInput | null = null;\n const rawHero = doc[\"hero\"] === undefined ? reading.fail(\"hero\", \"is missing\") : reading.object(doc[\"hero\"], \"hero\");\n if (rawHero !== null) {\n const photos = reading.photos(rawHero[\"photos\"], \"hero.photos\", 2);\n const source = doc[\"heroSource\"] ?? \"homePage\";\n if (source !== \"homePage\" && source !== \"siteHero\") reading.fail(\"heroSource\", \"must be homePage or siteHero\");\n let heading = rawHero[\"headline\"] === undefined ? null : reading.requiredCopy(rawHero[\"headline\"], \"hero.headline\");\n if (heading === null && rawHero[\"headline\"] === undefined && source === \"siteHero\") {\n const siteHero = doc[\"_siteHero\"];\n if (siteHero !== null && typeof siteHero === \"object\" && !Array.isArray(siteHero)) {\n const headingField = SITE_HERO.fields.find((field) => field.name === \"heading\");\n if (headingField === undefined) throw new TypeError(\"Missing siteHero heading definition\");\n const validated = validateTextDocument({ ...SITE_HERO, fields: [headingField] }, siteHero as Record<string, unknown>);\n if (validated.ok) heading = validated.value.heading ?? null;\n }\n }\n if (heading === null && rawHero[\"headline\"] === undefined) {\n reading.fail(\"hero.headline\", source === \"siteHero\"\n ? \"is missing and siteHero.heading is missing or invalid\"\n : \"is missing; write a headline or select siteHero with a published heading\");\n }\n const subline = reading.copy(rawHero[\"subline\"], \"hero.subline\");\n const intro = reading.copy(rawHero[\"lead\"], \"hero.lead\");\n const cta = reading.action(rawHero[\"primaryAction\"], \"hero.primaryAction\");\n const secondaryCta = reading.action(rawHero[\"secondaryAction\"], \"hero.secondaryAction\");\n if (heading !== null) {\n hero = { heading, subline, intro, image: photos[0] ?? null, inset: photos[1] ?? null, cta, secondaryCta };\n }\n }\n\n const blocks: CompositionBlockInput[] = [];\n const blockJumps: { where: string; href: string }[] = [];\n let body: RichBody = \"\";\n const state = campaignState(doc[\"campaign\"], options.now);\n if (state === \"shown\") {\n const campaign = reading.object(doc[\"campaign\"], \"campaign\");\n if (campaign !== null) {\n const title = reading.requiredCopy(campaign[\"title\"], \"campaign.title\");\n const images = reading.photos(campaign[\"photos\"], \"campaign.photos\", 3);\n if (images.length === 0) reading.fail(\"campaign.photos\", \"is empty; a shown campaign needs at least one photo\");\n const text = richTextToBody(campaign[\"body\"], \"campaign.body\", reading);\n if (bodyText(text) === \"\") reading.fail(\"campaign.body\", \"is empty; a shown campaign needs its text\");\n if (campaign[\"bodyCopySource\"] === \"proposed\") reading.proposed.push({ where: \"campaign.body\", text: bodyText(text) });\n const action = reading.action(campaign[\"action\"], \"campaign.action\");\n if (action !== null) blockJumps.push({ where: \"campaign.action.href\", href: action.href });\n if (title !== null) body = typeof text === \"string\" ? `# ${plain(title)}\\n\\n${text}` : [{ kind: \"heading\", text: plain(title) }, ...text];\n blocks.push({ kind: \"campaign\", action, images });\n }\n }\n\n const anchors: string[] = [];\n const rawBlocks = doc[\"blocks\"] ?? [];\n if (!Array.isArray(rawBlocks)) reading.fail(\"blocks\", \"is not a list\");\n else if (reading.dense(rawBlocks, \"blocks\")) {\n rawBlocks.forEach((member, index) => {\n const raw = reading.object(member, `blocks[${String(index)}]`);\n if (raw === null) return;\n const at = keyed(\"blocks\", raw, index);\n const mapped = mapBlock(raw, at, reading);\n if (mapped === null) return;\n blockJumps.push(...buttonsOf(mapped, raw, at));\n if (mapped.kind === \"locations\" && mapped.anchor !== null) {\n if (anchors.includes(mapped.anchor)) reading.fail(`${at}.jumpLink`, `repeats \"${mapped.anchor}\", already used by another section`);\n anchors.push(mapped.anchor);\n }\n blocks.push(mapped);\n });\n }\n\n // A jump link on ANY button must land on a section of this page.\n const jumps: { where: string; href: string }[] = [];\n if (hero?.cta) jumps.push({ where: \"hero.primaryAction.href\", href: hero.cta.href });\n if (hero?.secondaryCta) jumps.push({ where: \"hero.secondaryAction.href\", href: hero.secondaryCta.href });\n jumps.push(...blockJumps);\n for (const { where, href } of jumps) {\n if (href.startsWith(\"#\") && !anchors.includes(href.slice(1))) {\n reading.fail(where, `jumps to ${href}, which no section on the page is named`);\n }\n }\n\n if (reading.problems.length > 0) throw new MalformedContentError(id, reading.problems);\n return {\n composition: { intro: null, photo: null, hero, blocks },\n body,\n campaign: state,\n proposedCopy: reading.proposed,\n };\n}\n\n/** Every button a mapped section carries, with its path in the stored document. */\nfunction buttonsOf(block: CompositionBlockInput, raw: Raw, at: string): { where: string; href: string }[] {\n switch (block.kind) {\n case \"feature_stories\": {\n const stories = Array.isArray(raw[\"stories\"]) ? (raw[\"stories\"] as Raw[]) : [];\n return block.items.map((item, index) => ({\n where: `${keyed(`${at}.stories`, stories[index] ?? {}, index)}.action.href`,\n href: item.action.href,\n }));\n }\n case \"accent_band\":\n return block.action === null ? [] : [{ where: `${at}.action.href`, href: block.action.href }];\n case \"cta\":\n return [{ where: `${at}.action.href`, href: block.link.href }];\n default:\n return [];\n }\n}\n\nfunction jumpLinkOf(raw: Raw, at: string, reading: Reading): string | null {\n const jump = raw[\"jumpLink\"];\n if (jump === undefined || jump === null || jump === \"\") return null;\n if (typeof jump !== \"string\" || !READER_JUMP_LINK.test(jump)) return reading.fail(`${at}.jumpLink`, `is ${JSON.stringify(jump)}, not lowercase words joined by hyphens`);\n return jump;\n}\n\nfunction mapBlock(raw: Raw, at: string, reading: Reading): CompositionBlockInput | null {\n switch (raw[\"_type\"]) {\n case \"featureStoriesBlock\": {\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const stories = raw[\"stories\"];\n if (!Array.isArray(stories) || stories.length === 0) return reading.fail(`${at}.stories`, \"has no stories\");\n if (!reading.dense(stories, `${at}.stories`)) return null;\n const items: StoryInput[] = [];\n stories.forEach((member, index) => {\n const story = reading.object(member, `${at}.stories[${String(index)}]`);\n if (story === null) return;\n const sat = keyed(`${at}.stories`, story, index);\n const sHeading = reading.requiredCopy(story[\"heading\"], `${sat}.heading`);\n const sBody = reading.requiredCopy(story[\"body\"], `${sat}.body`);\n const action = reading.requiredAction(story[\"action\"], `${sat}.action`);\n const image = reading.photo(story[\"photo\"], `${sat}.photo`);\n const layout = story[\"layout\"] ?? \"wide\";\n if (!(READER_STORY_LAYOUTS as readonly unknown[]).includes(layout)) {\n reading.fail(`${sat}.layout`, `is ${JSON.stringify(layout)}, not wide, tall, round or free`);\n return;\n }\n const onCard = story[\"onCard\"] ?? false;\n if (typeof onCard !== \"boolean\") {\n reading.fail(`${sat}.onCard`, \"is not on or off\");\n return;\n }\n if (sHeading === null || sBody === null || action === null) return;\n items.push({ heading: sHeading, body: sBody, action, image, shape: layout as StoryInput[\"shape\"], card: onCard });\n });\n return heading === null ? null : { kind: \"feature_stories\", heading, tail, items };\n }\n case \"surfaceCardBlock\": {\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const body = reading.copy(raw[\"body\"], `${at}.body`);\n const action = reading.requiredAction(raw[\"action\"], `${at}.action`);\n const image = reading.photo(raw[\"photo\"], `${at}.photo`);\n if (heading === null || action === null) return null;\n return {\n kind: \"cta\",\n heading: plain(heading),\n body: body === null ? null : plain(body),\n link: { label: plain(action.label), href: action.href },\n ...(raw[\"photo\"] === undefined ? {} : { image }),\n };\n }\n case \"accentBandBlock\": {\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const body = reading.requiredCopy(raw[\"body\"], `${at}.body`);\n const action = reading.action(raw[\"action\"], `${at}.action`);\n const image = reading.photo(raw[\"photo\"], `${at}.photo`);\n if (heading === null || body === null) return null;\n return { kind: \"accent_band\", heading, body, action, image, ...(raw[\"tail\"] === undefined ? {} : { tail }) };\n }\n case \"locationsBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const image = reading.photo(raw[\"photo\"], `${at}.photo`);\n return heading === null ? null : { kind: \"locations\", anchor, heading, tail, image };\n }\n default:\n return reading.fail(`${at}._type`, `is ${JSON.stringify(raw[\"_type\"])}, not a section type this reader knows`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Page sections (A-1915)\n// ---------------------------------------------------------------------------\n\n/** A stored `layout`, or the composition default with a named warning. Never throws. */\nfunction sectionLayout<T extends string>(raw: Raw, at: string, layouts: readonly T[], fallback: T, warnings: SectionWarning[]): T {\n const value = raw[\"layout\"];\n const layout = layouts.find((name) => name === value);\n if (layout !== undefined) return layout;\n warnings.push({ code: SECTION_WARNINGS.layoutDefaulted, where: `${at}.layout` });\n return fallback;\n}\n\n/** An `imageWithHotspot`, or null. Without alt text or an asset it is dropped with a named warning. */\nfunction sectionImage(value: unknown, path: string, reading: Reading, warnings: SectionWarning[]): SectionImageInput | null {\n if (value === undefined || value === null) return null;\n const raw = reading.object(value, path);\n if (raw === null) return null;\n const asset = raw[\"asset\"];\n const ref = typeof asset === \"object\" && asset !== null ? (asset as Raw)[\"_ref\"] : undefined;\n if (typeof ref !== \"string\" || ref === \"\") {\n warnings.push({ code: SECTION_WARNINGS.imageMissingAsset, where: `${path}.asset` });\n return null;\n }\n const alt = raw[\"alt\"];\n if (typeof alt !== \"string\" || alt.trim() === \"\") {\n warnings.push({ code: SECTION_WARNINGS.imageMissingAlt, where: `${path}.alt` });\n return null;\n }\n const caption = reading.optionalString(raw[\"caption\"], `${path}.caption`);\n let hotspot: SectionImageInput[\"hotspot\"] = null;\n if (raw[\"hotspot\"] !== undefined && raw[\"hotspot\"] !== null) {\n const spot = reading.object(raw[\"hotspot\"], `${path}.hotspot`);\n const x = spot?.[\"x\"];\n const y = spot?.[\"y\"];\n if (typeof x === \"number\" && typeof y === \"number\" && x >= 0 && x <= 1 && y >= 0 && y <= 1) hotspot = { x, y };\n else if (spot !== null) reading.fail(`${path}.hotspot`, \"is not a point inside the image\");\n }\n return { asset: ref, alt, caption, hotspot };\n}\n\n/** Only a bounded schema name may travel in a warning; never coerce non-text values. */\nfunction sectionTypeForWarning(value: unknown): string {\n return typeof value === \"string\" ? value.slice(0, 64).replace(/[^A-Za-z0-9_.-]/g, \"?\") : \"?\";\n}\n\nfunction mapSection(raw: Raw, at: string, reading: Reading, warnings: SectionWarning[]): PageSectionInput | null {\n const textItems = (value: unknown, path: string, min: number, max: number): { title: CopyInput; body: CopyInput }[] => {\n if (!Array.isArray(value) || value.length < min || value.length > max || !reading.dense(value, path)) {\n reading.fail(path, `needs ${String(min)} to ${String(max)} items`);\n return [];\n }\n const items: { title: CopyInput; body: CopyInput }[] = [];\n value.forEach((member, index) => {\n const item = reading.object(member, `${path}[${String(index)}]`);\n if (item === null) return;\n const atItem = keyed(path, item, index);\n const title = reading.requiredCopy(item[\"title\"], `${atItem}.title`);\n const body = reading.requiredCopy(item[\"body\"], `${atItem}.body`);\n if (title !== null && body !== null) items.push({ title, body });\n });\n return items;\n };\n switch (raw[\"_type\"]) {\n case \"menuBlock\": {\n const menu = parseReferencedMenuDocument(raw[\"menu\"], `${at}.menu`);\n if (menu === null) {\n warnings.push({ code: SECTION_WARNINGS.menuUnavailable, where: `${at}.menu` });\n return null;\n }\n const layout = sectionLayout(raw, at, MENU_LAYOUTS, MENU_LAYOUT_DEFAULTS.layout, warnings);\n const decoration = MENU_DECORATIONS.find((name) => name === raw[\"decoration\"]);\n if (decoration === undefined) warnings.push({ code: SECTION_WARNINGS.layoutDefaulted, where: `${at}.decoration` });\n return { kind: \"menu\", menu, layout, decoration: decoration ?? MENU_LAYOUT_DEFAULTS.decoration };\n }\n case \"featureStoriesBlock\":\n case \"surfaceCardBlock\":\n case \"accentBandBlock\":\n case \"locationsBlock\": {\n const block = mapBlock(raw, at, reading);\n return block === null || block.kind === \"campaign\" ? null : block;\n }\n case \"textMediaBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const body = reading.requiredCopy(raw[\"body\"], `${at}.body`);\n const action = reading.action(raw[\"action\"], `${at}.action`);\n const image = sectionImage(raw[\"photo\"], `${at}.photo`, reading, warnings);\n const layout = sectionLayout(raw, at, SPLIT_LAYOUTS, SECTION_LAYOUT_DEFAULTS.Split, warnings);\n if (heading === null || body === null) return null;\n return { kind: \"text_media\", anchor, layout, heading, tail, body, action, image };\n }\n case \"faqBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.copy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const intro = reading.copy(raw[\"intro\"], `${at}.intro`);\n const layout = sectionLayout(raw, at, HEADING_LIST_LAYOUTS, SECTION_LAYOUT_DEFAULTS.HeadingList, warnings);\n const rawItems = raw[\"items\"];\n if (!Array.isArray(rawItems) || rawItems.length === 0) return reading.fail(`${at}.items`, \"has no questions\");\n if (!reading.dense(rawItems, `${at}.items`)) return null;\n const items: FaqSectionInput[\"items\"][number][] = [];\n rawItems.forEach((member, index) => {\n const item = reading.object(member, `${at}.items[${String(index)}]`);\n if (item === null) return;\n const iat = keyed(`${at}.items`, item, index);\n if (item[\"_type\"] !== undefined && item[\"_type\"] !== \"faqItem\") {\n reading.fail(`${iat}._type`, `is ${JSON.stringify(item[\"_type\"])}, not faqItem`);\n return;\n }\n const question = reading.requiredCopy(item[\"question\"], `${iat}.question`);\n const savedAnswer = item[\"answer\"];\n const answerObject = typeof savedAnswer === \"object\" && savedAnswer !== null && !Array.isArray(savedAnswer) ? savedAnswer as Raw : null;\n const rich = Array.isArray(savedAnswer) || (answerObject !== null && answerObject[\"content\"] !== undefined);\n const answer = rich\n ? { kind: \"rich\" as const, body: richTextToBody(Array.isArray(savedAnswer) ? savedAnswer : answerObject?.[\"content\"],\n Array.isArray(savedAnswer) ? `${iat}.answer` : `${iat}.answer.content`, reading) }\n : typeof savedAnswer === \"string\" ? savedAnswer : reading.requiredCopy(savedAnswer, `${iat}.answer`);\n if (answer === \"\" || (typeof answer === \"object\" && answer !== null && \"kind\" in answer && bodyText(answer.body) === \"\")) reading.fail(`${iat}.answer`, \"is empty\");\n if (question !== null && answer !== null) items.push({ question, answer });\n });\n return { kind: \"faq\", anchor, layout, heading, tail, intro, items };\n }\n case \"textItemsBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const layout = sectionLayout(raw, at, STAGGER_ROW_LAYOUTS, SECTION_LAYOUT_DEFAULTS.StaggerRow, warnings);\n const items = textItems(raw[\"items\"], `${at}.items`, 2, 4);\n return { kind: \"text_items\", anchor, layout, items };\n }\n case \"stepsBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const layout = sectionLayout(raw, at, HEADING_LIST_LAYOUTS, SECTION_LAYOUT_DEFAULTS.HeadingList, warnings);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const intro = reading.requiredCopy(raw[\"intro\"], `${at}.intro`);\n const items = textItems(raw[\"items\"], `${at}.items`, 1, 100);\n return heading === null || intro === null ? null : { kind: \"steps\", anchor, layout, heading, intro, items };\n }\n case \"linkListBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const layout = sectionLayout(raw, at, HEADING_LIST_LAYOUTS, SECTION_LAYOUT_DEFAULTS.HeadingList, warnings);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const tail = reading.copy(raw[\"tail\"], `${at}.tail`);\n const saved = raw[\"links\"];\n if (!Array.isArray(saved) || saved.length === 0 || !reading.dense(saved, `${at}.links`)) return reading.fail(`${at}.links`, \"needs at least one page\");\n const links: { title: string; href: string }[] = [];\n saved.forEach((member, index) => {\n const item = reading.object(member, `${at}.links[${String(index)}]`);\n if (item === null) return;\n const page = reading.object(item[\"page\"], `${at}.links[${String(index)}].page`);\n if (page === null) return;\n const title = reading.optionalString(page[\"title\"], `${at}.links[${String(index)}].page.title`);\n const slug = reading.optionalString(page[\"slug\"], `${at}.links[${String(index)}].page.slug`);\n if (!title || !slug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) reading.fail(`${at}.links[${String(index)}].page`, \"has no readable page title or slug\");\n else links.push({ title, href: `/pages/${slug}/` });\n });\n return heading === null ? null : { kind: \"link_list\", anchor, layout, heading, tail, links };\n }\n case \"galleryBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.copy(raw[\"heading\"], `${at}.heading`);\n const layout = sectionLayout(raw, at, STAGGER_ROW_LAYOUTS, SECTION_LAYOUT_DEFAULTS.StaggerRow, warnings);\n const photos = raw[\"photos\"] ?? [];\n if (!Array.isArray(photos)) return reading.fail(`${at}.photos`, \"is not a list of photos\");\n if (!reading.dense(photos, `${at}.photos`)) return null;\n if (photos.length < READER_GALLERY_PHOTO_MIN || photos.length > READER_GALLERY_PHOTO_LIMIT) {\n return reading.fail(`${at}.photos`, `holds ${String(photos.length)} photos; a gallery shows ${String(READER_GALLERY_PHOTO_MIN)} to ${String(READER_GALLERY_PHOTO_LIMIT)}`);\n }\n const images = photos.flatMap((photo, index) => {\n const image = sectionImage(photo, keyed(`${at}.photos`, typeof photo === \"object\" && photo !== null ? (photo as Raw) : {}, index), reading, warnings);\n return image === null ? [] : [image];\n });\n if (images.length === 0) {\n warnings.push({ code: SECTION_WARNINGS.emptySkipped, where: at });\n return null;\n }\n return { kind: \"gallery\", anchor, layout, heading, images };\n }\n case \"contactBlock\": {\n const anchor = jumpLinkOf(raw, at, reading);\n const heading = reading.requiredCopy(raw[\"heading\"], `${at}.heading`);\n const body = reading.copy(raw[\"body\"], `${at}.body`);\n const shown = (field: \"showEmail\" | \"showPhone\"): boolean => {\n const value = raw[field] ?? true;\n if (typeof value === \"boolean\") return value;\n reading.fail(`${at}.${field}`, \"is not on or off\");\n return false;\n };\n const showEmail = shown(\"showEmail\");\n const showPhone = shown(\"showPhone\");\n const action = reading.action(raw[\"action\"], `${at}.action`);\n const image = sectionImage(raw[\"photo\"], `${at}.photo`, reading, warnings);\n const layout = sectionLayout(raw, at, SPLIT_LAYOUTS, SECTION_LAYOUT_DEFAULTS.Split, warnings);\n if (heading === null) return null;\n if (!showEmail && !showPhone && body === null && action === null) {\n warnings.push({ code: SECTION_WARNINGS.emptySkipped, where: at });\n return null;\n }\n return { kind: \"contact\", anchor, layout, heading, body, showEmail, showPhone, action, image };\n }\n default:\n warnings.push({ code: SECTION_WARNINGS.unknownType, where: `${at}._type`, type: sectionTypeForWarning(raw[\"_type\"]) });\n return null;\n }\n}\n\n/** Every button a mapped page section carries, with its path in the stored document. */\nfunction sectionButtons(section: PageSectionInput, raw: Raw, at: string): { where: string; href: string }[] {\n switch (section.kind) {\n case \"text_media\":\n case \"contact\":\n return section.action === null ? [] : [{ where: `${at}.action.href`, href: section.action.href }];\n case \"menu\":\n case \"faq\":\n case \"gallery\":\n case \"text_items\":\n case \"steps\":\n case \"link_list\":\n return [];\n default:\n return buttonsOf(section, raw, at);\n }\n}\n\n/** `page.sections[]`, mapped in order. Unknown types are skipped with a warning; malformed content is a problem. */\nfunction parseSections(value: unknown, reading: Reading): { sections: PageSectionInput[]; warnings: SectionWarning[] } {\n const sections: PageSectionInput[] = [];\n const warnings: SectionWarning[] = [];\n if (!Array.isArray(value)) {\n reading.fail(\"sections\", \"is not a list\");\n return { sections, warnings };\n }\n if (!reading.dense(value, \"sections\")) return { sections, warnings };\n const anchors: string[] = [];\n const jumps: { where: string; href: string }[] = [];\n value.forEach((member, index) => {\n const raw = reading.object(member, `sections[${String(index)}]`);\n if (raw === null) return;\n const at = keyed(\"sections\", raw, index);\n const section = mapSection(raw, at, reading, warnings);\n if (section === null) return;\n jumps.push(...sectionButtons(section, raw, at));\n const anchor = \"anchor\" in section ? section.anchor : null;\n if (anchor !== null) {\n if (anchors.includes(anchor)) reading.fail(`${at}.jumpLink`, `repeats \"${anchor}\", already used by another section`);\n anchors.push(anchor);\n }\n sections.push(section);\n });\n for (const { where, href } of jumps) {\n if (href.startsWith(\"#\") && !anchors.includes(href.slice(1))) reading.fail(where, `jumps to ${href}, which no section on the page is named`);\n }\n return { sections, warnings };\n}\n\n// ---------------------------------------------------------------------------\n// Content pages and settings\n// ---------------------------------------------------------------------------\n\nexport function parseContentPage(document: unknown): PageContent {\n const reading = new Reading();\n const doc = reading.object(document, \"page\");\n if (doc === null) throw new MalformedContentError(\"page\", reading.problems);\n const id = typeof doc[\"_id\"] === \"string\" ? doc[\"_id\"] : \"page\";\n if (doc[\"_type\"] !== \"page\") reading.fail(\"_type\", `is ${JSON.stringify(doc[\"_type\"])}, not page`);\n const title = reading.optionalString(doc[\"title\"], \"title\");\n const titleTail = reading.optionalString(doc[\"titleTail\"], \"titleTail\");\n if (title === null) reading.fail(\"title\", \"is missing\");\n if (doc[\"titleCopySource\"] === \"proposed\" && title !== null) reading.proposed.push({ where: \"title\", text: title });\n const slugObject = doc[\"slug\"] as Raw | undefined;\n const slug = typeof slugObject?.[\"current\"] === \"string\" && slugObject[\"current\"] !== \"\" ? slugObject[\"current\"] : null;\n if (slug === null) reading.fail(\"slug.current\", \"is missing\");\n const lead = reading.copy(doc[\"lead\"], \"lead\");\n const photo = reading.photo(doc[\"photo\"], \"photo\");\n const body = richTextToBody(doc[\"body\"], \"body\", reading);\n if (doc[\"bodyCopySource\"] === \"proposed\") reading.proposed.push({ where: \"body\", text: bodyText(body) });\n const sections = doc[\"sections\"] === undefined || doc[\"sections\"] === null ? null : parseSections(doc[\"sections\"], reading);\n if (reading.problems.length > 0 || title === null || slug === null) throw new MalformedContentError(id, reading.problems);\n return {\n slug,\n title,\n ...(doc[\"titleTail\"] === undefined ? {} : { titleTail: titleTail ?? \"\" }),\n composition: { intro: lead === null ? null : plain(lead), photo, hero: null, blocks: [] },\n body,\n migrated: doc[\"migrated\"] === true,\n proposedCopy: reading.proposed,\n ...(sections === null ? {} : sections),\n };\n}\n\nexport function parseSiteSettings(document: unknown): SiteSettingsContent {\n const reading = new Reading();\n const doc = reading.object(document, \"siteSettings\");\n if (doc === null) throw new MalformedContentError(READER_SITE_SETTINGS_ID, reading.problems);\n const name = reading.optionalString(doc[\"title\"], \"title\");\n if (name === null) reading.fail(\"title\", \"is missing; a site needs its name\");\n const description = reading.optionalString(doc[\"description\"], \"description\");\n const contact = doc[\"contact\"] === undefined ? {} : (reading.object(doc[\"contact\"], \"contact\") ?? {});\n const email = reading.optionalString(contact[\"email\"], \"contact.email\");\n const phone = reading.optionalString(contact[\"phone\"], \"contact.phone\");\n const social: { network: string; url: string }[] = [];\n const rawSocial = doc[\"social\"] ?? [];\n if (!Array.isArray(rawSocial)) reading.fail(\"social\", \"is not a list\");\n else if (reading.dense(rawSocial, \"social\"))\n rawSocial.forEach((member, index) => {\n const link = reading.object(member, `social[${String(index)}]`);\n if (link === null) return;\n const network = reading.optionalString(link[\"network\"], `social[${String(index)}].network`);\n const url = reading.optionalString(link[\"url\"], `social[${String(index)}].url`);\n if (network === null || url === null || !/^https?:\\/\\//.test(url)) {\n reading.fail(`social[${String(index)}]`, \"needs a network and a web address\");\n return;\n }\n social.push({ network, url });\n });\n if (reading.problems.length > 0 || name === null) throw new MalformedContentError(READER_SITE_SETTINGS_ID, reading.problems);\n return { name, description, contact: { email, phone }, social };\n}\n\n// ---------------------------------------------------------------------------\n// Queries\n// ---------------------------------------------------------------------------\n\n/** By fixed id: a second home page at a random id is never read. */\nexport const HOME_PAGE_DOCUMENT_QUERY = `*[_id == $id][0]`;\n/** One snapshot contains both possible headline sources. */\nexport const HOME_PAGE_WITH_SITE_HERO_QUERY = `*[_id == $id][0]{..., \"_siteHero\": *[_id == \"siteHero\" && _type == \"siteHero\"][0]{heading}}`;\nconst MENU_PAGE_PROJECTION = `{..., \"sections\": coalesce(sections[]{..., _type == \"menuBlock\" => {menu->{${MENU_DOCUMENT_PROJECTION}}}, _type == \"linkListBlock\" => {links[]{..., \"page\": page->{title, \"slug\": slug.current}}}}, sections)}`;\nexport const CONTENT_PAGES_QUERY = `*[_type == \"page\" && defined(slug.current)]|order(slug.current asc)${MENU_PAGE_PROJECTION}`;\nexport const CONTENT_PAGE_BY_SLUG_QUERY = `*[_type == \"page\" && slug.current == $slug][0]${MENU_PAGE_PROJECTION}`;\n\nexport interface SiteContentReader {\n /** `null` when no home page document exists yet. */\n home: (options: { readonly now: Date }) => Promise<HomeContent | null>;\n pages: () => Promise<PageContent[]>;\n /** `null` when no page has that slug. */\n pageBySlug: (slug: string) => Promise<PageContent | null>;\n /** `null` before anyone has filled in the site's details. */\n siteSettings: () => Promise<SiteSettingsContent | null>;\n}\n\nexport function createSiteContentReader(client: { query: <T>(groq: string, options?: { params?: Record<string, unknown>; tags?: readonly string[] }) => Promise<T> }): SiteContentReader {\n return {\n home: async ({ now }) => {\n const doc = await client.query<unknown>(HOME_PAGE_WITH_SITE_HERO_QUERY, { params: { id: READER_HOME_PAGE_ID } });\n return doc === null || doc === undefined ? null : parseHomePage(doc, { now });\n },\n pages: async () => {\n const docs = await client.query<unknown>(CONTENT_PAGES_QUERY);\n if (!Array.isArray(docs)) throw new MalformedContentError(\"page\", [\"the page query did not return a list\"]);\n if (!isDenseArray(docs)) throw new MalformedContentError(\"page\", [\"the page query contains missing entries\"]);\n return docs.map((doc) => parseContentPage(doc));\n },\n pageBySlug: async (slug) => {\n const doc = await client.query<unknown>(CONTENT_PAGE_BY_SLUG_QUERY, { params: { slug } });\n return doc === null || doc === undefined ? null : parseContentPage(doc);\n },\n siteSettings: async () => {\n const doc = await client.query<unknown>(HOME_PAGE_DOCUMENT_QUERY, { params: { id: READER_SITE_SETTINGS_ID } });\n return doc === null || doc === undefined ? null : parseSiteSettings(doc);\n },\n };\n}\n\nfunction atomic<T>(raw: unknown, fallback: T, parse: (value: unknown) => T, validFallback: (value: unknown) => boolean): CmsResolution<T> {\n if (!validFallback(fallback)) throw new TypeError(\"Invalid CMS content fallback\");\n if (raw === null || raw === undefined) return { data: fallback, fallbackFields: [\"$\"] };\n // The server reader catches parser errors and classifies the whole fallback\n // as malformed. Returning fallback here would lose that diagnostic reason.\n return { data: parse(raw), fallbackFields: [] };\n}\n\nexport function homeContentQuery(now: Date): CmsQuery<HomeContent | null> {\n if (!(now instanceof Date) || !Number.isFinite(now.getTime())) throw new TypeError(\"Invalid CMS campaign date\");\n const captured = now.getTime();\n return defineCmsQuery({\n name: \"homePage.v1\", query: HOME_PAGE_WITH_SITE_HERO_QUERY, params: { id: READER_HOME_PAGE_ID },\n dependencies: { types: [\"homePage\", \"siteHero\"], ids: [READER_HOME_PAGE_ID, \"siteHero\"] },\n resolve: (raw, fallback) => atomic(raw, fallback, (value) => parseHomePage(value, { now: new Date(captured) }), validHomeFallback),\n });\n}\n\nexport function contentPageQuery(slug: string): CmsQuery<PageContent | null> {\n if (typeof slug !== \"string\" || slug === \"\") throw new TypeError(\"Invalid CMS page slug\");\n return defineCmsQuery({\n name: \"page.v1\", query: CONTENT_PAGE_BY_SLUG_QUERY, params: { slug }, dependencies: { types: [\"page\", \"menu\"] },\n resolve: (raw, fallback) => atomic(raw, fallback, parseContentPage, validPageFallback),\n });\n}\n\nexport function contentPagesQuery(): CmsQuery<readonly PageContent[]> {\n return defineCmsQuery({\n name: \"pages.v1\", query: CONTENT_PAGES_QUERY, params: {}, dependencies: { types: [\"page\", \"menu\"] },\n resolve: (raw, fallback) => atomic(raw, fallback, (value) => {\n if (!Array.isArray(value)) throw new MalformedContentError(\"page\", [\"the page query did not return a list\"]);\n if (!isDenseArray(value)) throw new MalformedContentError(\"page\", [\"the page query contains missing entries\"]);\n return value.map((document: unknown) => parseContentPage(document));\n }, (value) => isDenseArray(value) && value.every((page: unknown) => page !== null && validPageFallback(page))),\n });\n}\n\nexport function siteSettingsQuery(): CmsQuery<SiteSettingsContent | null> {\n return defineCmsQuery({\n name: \"siteSettings.v1\", query: HOME_PAGE_DOCUMENT_QUERY, params: { id: READER_SITE_SETTINGS_ID },\n dependencies: { types: [\"siteSettings\"], ids: [READER_SITE_SETTINGS_ID] },\n resolve: (raw, fallback) => atomic(raw, fallback, parseSiteSettings, validSettingsFallback),\n });\n}\n", "import { MalformedContentError } from \"../portable/site-content.js\";\nimport { MENU_DOCUMENT_PROJECTION } from \"../portable/menu-options.js\";\nimport { parseMenuDocument, validMenuFallback, type MenuDocument } from \"../portable/menu.js\";\n\n/** A-1822 S1 owns this implementation. S0 registers the public signatures. */\nexport type CmsParams = Readonly<Record<string, string | number | boolean | null>>;\nexport type CmsDependencies = {\n readonly types: readonly string[];\n readonly ids?: readonly string[];\n};\nexport type CmsResolution<T> = {\n readonly data: T;\n readonly fallbackFields: readonly string[];\n readonly warnings?: readonly (\"siteHero.layout.defaulted\" | \"siteHero.tagline.dropped\" | \"siteHero.taglines.defaulted\")[];\n};\nexport type CmsQuery<T> = {\n readonly name: string;\n readonly query: string;\n readonly params: CmsParams;\n readonly dependencies: CmsDependencies;\n readonly resolve: (raw: unknown, fallback: T) => CmsResolution<T>;\n};\n\nconst TYPE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;\nconst ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;\nconst SCOPE = /^[a-z0-9]{6,12}\\.[a-z][a-z0-9_-]{0,63}$/;\n\nfunction dependenciesCopy(dependencies: CmsDependencies): CmsDependencies {\n if (!dependencies || !Array.isArray(dependencies.types) || dependencies.types.length === 0) {\n throw new TypeError(\"CMS dependencies require document types\");\n }\n const types = [...dependencies.types];\n for (const type of types) {\n if (typeof type !== \"string\" || !TYPE.test(type)) throw new TypeError(\"Invalid CMS document type\");\n }\n if (dependencies.ids !== undefined && !Array.isArray(dependencies.ids)) throw new TypeError(\"Invalid CMS document IDs\");\n const ids = dependencies.ids === undefined ? undefined : [...dependencies.ids];\n for (const id of ids ?? []) {\n if (typeof id !== \"string\" || !ID.test(id) || /^(drafts|versions)\\./.test(id)) {\n throw new TypeError(\"Invalid CMS document ID\");\n }\n }\n if (new Set(types).size + new Set(ids).size > 128) throw new TypeError(\"Too many CMS tags\");\n return Object.freeze({ types: Object.freeze(types), ...(ids === undefined ? {} : { ids: Object.freeze(ids) }) });\n}\n\nexport function defineCmsQuery<T>(query: CmsQuery<T>): CmsQuery<T> {\n if (!query || typeof query.name !== \"string\" || !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(query.name)\n || typeof query.query !== \"string\" || query.query.trim() === \"\" || typeof query.resolve !== \"function\") {\n throw new TypeError(\"Invalid CMS query configuration\");\n }\n const params = query.params;\n if (!params || typeof params !== \"object\" || (Object.getPrototypeOf(params) !== Object.prototype && Object.getPrototypeOf(params) !== null)) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const copied: Record<string, string | number | boolean | null> = {};\n for (const key of Reflect.ownKeys(params)) {\n const descriptor = Object.getOwnPropertyDescriptor(params, key);\n if (typeof key !== \"string\" || !descriptor || !Object.hasOwn(descriptor, \"value\") || !descriptor.enumerable) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const value: unknown = descriptor.value;\n if (!(value === null || typeof value === \"string\" || typeof value === \"boolean\" || (typeof value === \"number\" && Number.isFinite(value)))) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n Object.defineProperty(copied, key, { value, enumerable: true });\n }\n return Object.freeze({ name: query.name, query: query.query, params: Object.freeze(copied), dependencies: dependenciesCopy(query.dependencies), resolve: query.resolve });\n}\n\nexport function cmsTags(scope: string, dependencies: CmsDependencies): readonly string[] {\n if (typeof scope !== \"string\" || scope.length > 77 || !SCOPE.test(scope)) throw new TypeError(\"Invalid CMS scope\");\n const copied = dependenciesCopy(dependencies);\n const tags = [\n ...[...new Set(copied.types)].sort().map((type) => `cms:${scope}:type:${type}`),\n ...[...new Set(copied.ids ?? [])].sort().map((id) => `cms:${scope}:id:${id}`),\n ];\n return Object.freeze(tags);\n}\n\n/** Read one published menu by its document id; host supplies the last good snapshot. */\nexport function menuQuery(id: string): CmsQuery<MenuDocument | null> {\n return defineCmsQuery({\n name: \"menu.v1\",\n query: `*[_type == \"menu\" && _id == $id && !(_id in path(\"drafts.**\")) && !(_id in path(\"versions.**\"))][0]{${MENU_DOCUMENT_PROJECTION}}`,\n params: { id }, dependencies: { types: [\"menu\"], ids: [id] },\n resolve: (raw, fallback) => {\n if (!validMenuFallback(fallback) || (fallback !== null && fallback._id !== id)) throw new TypeError(\"Invalid CMS menu fallback\");\n if (raw === null || raw === undefined) return { data: fallback, fallbackFields: [\"$\"] };\n const data = parseMenuDocument(raw);\n if (data._id !== id) throw new MalformedContentError(\"menu\", [\"menu._id does not match the requested document\"]);\n return { data, fallbackFields: [] };\n },\n });\n}\n", "import { cmsTags, defineCmsQuery, type CmsQuery, type CmsResolution } from \"../contracts/query.js\";\n\nexport type CmsFailure = \"configuration\" | \"network\" | \"timeout\" | \"rate-limit\"\n | \"upstream\" | \"plan-limit\" | \"permission\" | \"malformed\";\nexport type CmsDiagnostic = {\n readonly event: \"cms.fallback\" | \"cms.partial-fallback\" | \"cms.refused\";\n readonly query: string;\n readonly reason: CmsFailure | \"missing\" | \"invalid-field\";\n readonly status?: number;\n readonly fields: readonly string[];\n readonly warnings?: CmsResolution<unknown>[\"warnings\"];\n};\nexport type CmsFetch = (url: string, init: RequestInit & {\n next?: { revalidate: number; tags: string[] };\n}) => Promise<Response>;\nexport type CmsReadPolicy = {\n readonly tags: readonly string[];\n readonly revalidateSeconds: number;\n};\nexport type CmsAdapterResult =\n | { readonly ok: true; readonly value: unknown }\n | { readonly ok: false; readonly reason: CmsFailure; readonly status?: number };\nexport type CmsAdapter = {\n readonly scope: string;\n read(query: Pick<CmsQuery<unknown>, \"name\" | \"query\" | \"params\">,\n policy: CmsReadPolicy): Promise<CmsAdapterResult>;\n};\nexport type CmsReadResult<T> = {\n readonly data: T;\n readonly source: \"cms\" | \"mixed\" | \"fallback\";\n readonly fallbackFields: readonly string[];\n readonly reason?: CmsFailure | \"missing\" | \"invalid-field\";\n readonly warnings?: CmsResolution<unknown>[\"warnings\"];\n};\nexport type CmsReaderOptions = {\n readonly adapter: CmsAdapter;\n readonly revalidateSeconds?: number;\n readonly onDiagnostic?: (event: CmsDiagnostic) => void;\n};\nexport type CmsReader = {\n read<T>(query: CmsQuery<T>, options: { readonly fallback: T }): Promise<CmsReadResult<T>>;\n};\n\nfunction resolution<T>(value: CmsResolution<T>): CmsResolution<T> {\n if (!value || !Object.hasOwn(value, \"data\") || !Array.isArray(value.fallbackFields)\n || ![...value.fallbackFields].every((field: unknown) => typeof field === \"string\"\n && (field === \"$\" || /^[A-Za-z][A-Za-z0-9_.[\\]-]{0,127}$/.test(field)))) {\n throw new TypeError(\"Invalid CMS resolution\");\n }\n if (value.warnings !== undefined && (!Array.isArray(value.warnings)\n || ![...value.warnings].every((warning) => warning === \"siteHero.layout.defaulted\"\n || warning === \"siteHero.tagline.dropped\" || warning === \"siteHero.taglines.defaulted\"))) {\n throw new TypeError(\"Invalid CMS resolution warnings\");\n }\n return { data: value.data, fallbackFields: Object.freeze([...new Set(value.fallbackFields)]),\n ...(value.warnings?.length ? { warnings: Object.freeze([...new Set(value.warnings)]) } : {}) };\n}\n\n/** Forwards cache policy; persistence and rendered-output invalidation belong to the host. */\nexport function createCmsReader(options: CmsReaderOptions): CmsReader {\n const seconds = options?.revalidateSeconds ?? 3600;\n if (!Number.isSafeInteger(seconds) || seconds < 3600 || !options?.adapter\n || typeof options.adapter.read !== \"function\"\n || (options.onDiagnostic !== undefined && typeof options.onDiagnostic !== \"function\")) {\n throw new TypeError(\"Invalid CMS reader configuration\");\n }\n const adapter = options.adapter;\n // Validate the scope without assuming any particular shared content types.\n cmsTags(adapter.scope, { types: [\"ScopeValidation\"] });\n const notify = (event: CmsDiagnostic): void => {\n try {\n if (options.onDiagnostic) options.onDiagnostic(event);\n else console.warn(JSON.stringify(event));\n } catch { /* Reporting must not break visitor rendering. */ }\n };\n return {\n async read<T>(input: CmsQuery<T>, readOptions: { readonly fallback: T }): Promise<CmsReadResult<T>> {\n let query: CmsQuery<T>;\n let local: CmsResolution<T>;\n let tags: readonly string[];\n try {\n query = defineCmsQuery(input);\n tags = cmsTags(adapter.scope, query.dependencies);\n if (!readOptions || !Object.hasOwn(readOptions, \"fallback\")) throw new TypeError();\n // Built-in resolvers validate fallback here, before any I/O.\n local = resolution(query.resolve(null, readOptions.fallback));\n } catch {\n throw new TypeError(\"Invalid CMS query or fallback configuration\");\n }\n const fallback = (reason: CmsFailure | \"missing\" | \"invalid-field\", status?: number, warnings?: CmsResolution<T>[\"warnings\"]): CmsReadResult<T> => {\n notify({ event: \"cms.fallback\", query: query.name, reason, fields: [\"$\"],\n ...(status === undefined ? {} : { status }), ...(warnings ? { warnings } : {}) });\n return { data: local.data, source: \"fallback\", fallbackFields: [\"$\"], reason, ...(warnings ? { warnings } : {}) };\n };\n let result: CmsAdapterResult;\n try {\n result = await adapter.read({ name: query.name, query: query.query, params: query.params },\n { tags, revalidateSeconds: seconds });\n } catch { return fallback(\"network\"); }\n if (!result.ok) {\n const reasons: readonly CmsFailure[] = [\"configuration\", \"network\", \"timeout\", \"rate-limit\", \"upstream\", \"plan-limit\", \"permission\", \"malformed\"];\n const reason = reasons.includes(result.reason) ? result.reason : \"malformed\";\n const status = Number.isInteger(result.status) && (result.status ?? 0) >= 100 && (result.status ?? 0) <= 599 ? result.status : undefined;\n return fallback(reason, status);\n }\n if (result.value === null || result.value === undefined) return fallback(\"missing\");\n let resolved: CmsResolution<T>;\n try { resolved = resolution(query.resolve(result.value, readOptions.fallback)); }\n catch { return fallback(\"malformed\"); }\n if (resolved.fallbackFields.includes(\"$\")) {\n return { ...fallback(\"invalid-field\", undefined, resolved.warnings), data: resolved.data };\n }\n if (resolved.fallbackFields.length > 0) {\n notify({ event: \"cms.partial-fallback\", query: query.name, reason: \"invalid-field\", fields: resolved.fallbackFields, ...(resolved.warnings ? { warnings: resolved.warnings } : {}) });\n return { ...resolved, source: \"mixed\", reason: \"invalid-field\" };\n }\n return { ...resolved, source: \"cms\" };\n },\n };\n}\n", "import { cmsTags, defineCmsQuery, type CmsQuery, type CmsResolution } from \"../contracts/query.js\";\nimport type { CmsReader, CmsReadResult } from \"./reader.js\";\n\ntype RecordValue = Record<string, unknown>;\nconst object = (value: unknown): value is RecordValue => value !== null && typeof value === \"object\" && !Array.isArray(value);\nconst nonempty = (value: unknown): string | null => typeof value === \"string\" && value.trim() ? value.trim() : null;\nconst published = (value: RecordValue, kind: string): boolean => value._type === kind && typeof value._id === \"string\"\n && value._id.length > 0 && !/^(drafts|versions)\\./.test(value._id);\nconst dayPattern = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst timePattern = /^([01]\\d|2[0-3]):[0-5]\\d$/;\nconst days = [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"] as const;\n\n/** Saved shapes copied from schema-base 0.3.0; no schema package is loaded by CMS. */\ntype SavedDocument = { readonly _id: string; readonly _createdAt: string; readonly _updatedAt: string; readonly _rev: string };\nexport type SavedVacancyDocument = SavedDocument & {\n readonly _type: \"vacancy\"; readonly title?: string; readonly location?: string; readonly locationNote?: string;\n readonly summary?: string; readonly description?: readonly unknown[];\n readonly jobDescriptionPdf?: { readonly _type: \"file\"; readonly asset?: { readonly _type: \"reference\"; readonly _ref: string }; readonly media?: unknown };\n readonly applyUrl?: string; readonly applyEmail?: string; readonly status?: \"open\" | \"closed\";\n readonly closingDate?: string; readonly order?: number;\n};\nexport type SavedLocationDocument = SavedDocument & {\n readonly _type: \"location\"; readonly name?: string; readonly slug?: { readonly _type: \"slug\"; readonly current?: string };\n readonly addressLines?: readonly string[]; readonly postcode?: string; readonly mapLink?: string;\n readonly coordinates?: { readonly _type: \"geopoint\"; readonly lat?: number; readonly lng?: number; readonly alt?: number };\n readonly hours?: { readonly _type: \"openingHours\" } & Readonly<Partial<Record<typeof weekDays[number], readonly { readonly opens?: string; readonly closes?: string }[]>>>;\n readonly phone?: string; readonly email?: string;\n};\nexport type SavedScheduleStop = { readonly _type: \"scheduleStop\"; readonly name?: string; readonly from?: string; readonly to?: string; readonly address?: string; readonly mapLink?: string };\nexport type SavedScheduleDay = { readonly _type: \"scheduleDay\"; readonly day?: typeof days[number]; readonly rest?: boolean; readonly stops?: readonly (SavedScheduleStop & { readonly _key: string })[] };\nexport type SavedScheduleDocument = SavedDocument & { readonly _type: \"schedule\"; readonly timeZone?: string; readonly days?: readonly (SavedScheduleDay & { readonly _key: string })[] };\n\n/** The existing reader owns fetch, timeout, cache policy and diagnostics. */\nexport async function resolveWithFallback<T>(reader: CmsReader, query: CmsQuery<T>, fallback: T): Promise<Omit<CmsReadResult<T>, \"source\"> & { readonly source: \"sanity\" | \"mixed\" | \"fallback\" }> {\n try {\n const result = await reader.read(query, { fallback });\n return { ...result, source: result.source === \"cms\" ? \"sanity\" : result.source };\n } catch {\n return { data: fallback, source: \"fallback\", fallbackFields: [\"$\"], reason: \"configuration\" };\n }\n}\n\nexport type Vacancy = {\n readonly id: string; readonly title: string; readonly location: string; readonly summary: string | null;\n readonly pdfUrl: string | null; readonly applyUrl: string | null; readonly applyEmail: string | null;\n};\nconst emailPattern = /^[A-Za-z0-9._+-]{1,64}@[A-Za-z0-9-]{1,63}(?:\\.[A-Za-z0-9-]{1,63})*\\.[A-Za-z]{2,63}$/;\nfunction httpUrl(value: unknown): string | null {\n const written = nonempty(value);\n if (!written) return null;\n try { const url = new URL(written); return url.protocol === \"http:\" || url.protocol === \"https:\" ? url.href : null; }\n catch { return null; }\n}\nexport type VacancyValidator = (entry: Record<string, unknown>) => string | null;\nfunction requireValidator(validate: unknown): void {\n if (typeof validate !== \"function\") throw new TypeError(\"A client validator is required\");\n}\nexport function parseVacancyDocument(raw: unknown, today: string, validate: VacancyValidator): Vacancy | null {\n requireValidator(validate);\n // DK vacancies.ts @ 2c8c586c1e985bd7b1e0e1334e9a360282b1fd86:\n // status and closing-date filtering precede required-field validation.\n if (!object(raw) || raw.status !== \"open\") return null;\n const closing = nonempty(raw.closingDate);\n if (closing !== null && closing < today) return null;\n if (validate(raw) !== null) return null;\n const title = nonempty(raw.title);\n const location = raw.location === \"Other\" ? nonempty(raw.locationNote) : nonempty(raw.location);\n if (!title || !location) return null;\n const pdf = nonempty(raw.pdfUrl);\n return { id: String(nonempty(raw._id)), title, location, summary: nonempty(raw.summary),\n pdfUrl: pdf?.startsWith(\"https://cdn.sanity.io/files/\") ? pdf : null,\n applyUrl: httpUrl(raw.applyUrl), applyEmail: typeof raw.applyEmail === \"string\" && raw.applyEmail.length <= 254 && emailPattern.test(raw.applyEmail) ? raw.applyEmail : null };\n}\nexport function parseVacancies(raw: unknown, today: string, validate: VacancyValidator): readonly Vacancy[] {\n requireValidator(validate);\n if (!Array.isArray(raw)) throw new TypeError(\"Sanity returned vacancies that are not a list\");\n return raw.map((value) => ({ value: parseVacancyDocument(value, today, validate), order: object(value) && typeof value.order === \"number\" && Number.isFinite(value.order) ? value.order : 1_000_000 }))\n .filter((entry): entry is { value: Vacancy; order: number } => entry.value !== null)\n .sort((a, b) => a.order - b.order || a.value.title.toLowerCase().localeCompare(b.value.title.toLowerCase()))\n .map((entry) => entry.value);\n}\nexport function vacancyTags(scope: string): readonly string[] { return cmsTags(scope, { types: [\"vacancy\"] }); }\nexport function openVacanciesQuery(today: string, validate: VacancyValidator): CmsQuery<readonly Vacancy[]> {\n if (!dayPattern.test(today)) throw new TypeError(\"Invalid calendar date\");\n requireValidator(validate);\n return defineCmsQuery({ name: \"vacancy.open.v1\",\n query: '*[_type == \"vacancy\" && status == \"open\" && !(_id in path(\"drafts.**\")) && !(_id in path(\"versions.**\")) && (!defined(closingDate) || closingDate >= $today)]|order(coalesce(order, 1000000) asc, lower(title) asc){_id, _type, title, location, locationNote, summary, \"pdfUrl\": jobDescriptionPdf.asset->url, applyUrl, applyEmail, status, closingDate, order}',\n params: { today }, dependencies: { types: [\"vacancy\"] },\n resolve: (raw, fallback) => ({ data: raw == null ? fallback : parseVacancies(raw, today, validate), fallbackFields: raw == null ? [\"$\"] : [] }) });\n}\n\nexport type LocationFields = {\n readonly id: string; readonly label: string; readonly addressLines: readonly string[] | null;\n readonly postcode: string | null; readonly geo: { readonly latitude: number; readonly longitude: number } | null;\n readonly telephone: string | null; readonly email: string | null;\n readonly hours: Readonly<Record<string, readonly { readonly opens: string; readonly closes: string }[]>> | null;\n};\nconst weekDays = [\"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\", \"saturday\", \"sunday\"] as const;\nconst slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\ntype LocationIdentity = {\n readonly name: string; readonly slug: string; readonly addressLines?: string[] | null;\n readonly postcode?: string | null; readonly coordinates?: { lat: number; lng: number } | null;\n readonly phone?: string | null; readonly email?: string | null; readonly hours?: unknown;\n};\n/** DK locations.ts @ 2c8c586c1e985bd7b1e0e1334e9a360282b1fd86: document.safeParse is the identity/duplicate stage. */\nfunction locationIdentity(raw: unknown): LocationIdentity | null {\n if (!object(raw) || typeof raw._id !== \"string\" || !object(raw.slug)\n || typeof raw.slug.current !== \"string\" || !slugPattern.test(raw.slug.current)\n || nonempty(raw.name) === null) return null;\n if (raw.addressLines !== undefined && raw.addressLines !== null\n && (!Array.isArray(raw.addressLines) || Array.from(raw.addressLines).some((line) => nonempty(line) === null))) return null;\n if (raw.postcode !== undefined && raw.postcode !== null && nonempty(raw.postcode) === null) return null;\n if (raw.coordinates !== undefined && raw.coordinates !== null\n && (!object(raw.coordinates) || typeof raw.coordinates.lat !== \"number\" || !Number.isFinite(raw.coordinates.lat)\n || typeof raw.coordinates.lng !== \"number\" || !Number.isFinite(raw.coordinates.lng))) return null;\n if (raw.phone !== undefined && raw.phone !== null && typeof raw.phone !== \"string\") return null;\n if (raw.email !== undefined && raw.email !== null && typeof raw.email !== \"string\") return null;\n return { name: nonempty(raw.name)!, slug: raw.slug.current,\n addressLines: raw.addressLines == null ? raw.addressLines : raw.addressLines.map((line) => line.trim()),\n postcode: raw.postcode == null ? raw.postcode : String(raw.postcode).trim(),\n coordinates: raw.coordinates == null ? raw.coordinates : { lat: raw.coordinates.lat as number, lng: raw.coordinates.lng as number }, phone: raw.phone,\n email: raw.email, hours: raw.hours };\n}\nfunction parseHours(raw: unknown): LocationFields[\"hours\"] | null {\n if (!object(raw)) return null;\n const output: Record<string, { opens: string; closes: string }[]> = {};\n for (const day of weekDays) {\n const ranges = raw[day];\n if (!Array.isArray(ranges) || !Array.from(ranges).every((range) => object(range) && typeof range.opens === \"string\" && typeof range.closes === \"string\")) return null;\n output[day] = ranges.map((range: RecordValue) => ({ opens: range.opens as string, closes: range.closes as string }));\n }\n return output;\n}\nexport type LocationValidator<T> = (place: T) => T;\nexport function parseLocationDocument(raw: unknown, fallback: LocationFields, validate: LocationValidator<LocationFields>): CmsResolution<LocationFields> {\n requireValidator(validate);\n const doc = locationIdentity(raw);\n if (doc === null || doc.slug !== fallback.id) return { data: fallback, fallbackFields: [\"$\"] };\n const parsedHours = parseHours(doc.hours);\n const merged: LocationFields = { id: fallback.id, label: doc.name,\n addressLines: doc.addressLines ?? fallback.addressLines, postcode: doc.postcode ?? fallback.postcode,\n geo: doc.coordinates == null ? fallback.geo : { latitude: doc.coordinates.lat, longitude: doc.coordinates.lng },\n telephone: doc.phone === undefined ? fallback.telephone : doc.phone,\n email: doc.email === undefined ? fallback.email : doc.email, hours: parsedHours ?? fallback.hours };\n try { return { data: validate(merged), fallbackFields: parsedHours === null ? [\"hours\"] : [] }; }\n catch {\n if (parsedHours !== null) {\n try { return { data: validate({ ...merged, hours: fallback.hours }), fallbackFields: [\"hours\"] }; }\n catch { /* A bad saved field leaves this whole location on repository data. */ }\n }\n return { data: fallback, fallbackFields: [\"$\"] };\n }\n}\n/** The host's location shape may carry more reviewed repository fields than Sanity owns. */\nexport type BusinessLocationLike = {\n readonly id: string; readonly label: string | null;\n readonly address: { readonly streetAddress: readonly string[]; readonly postalCode: string } | null;\n readonly telephone: string | null; readonly email: string | null;\n readonly geo: { readonly latitude: number; readonly longitude: number } | null;\n readonly hours: unknown;\n};\nexport function parseBusinessLocation<T extends BusinessLocationLike>(raw: unknown, fallback: T, validate: LocationValidator<T>): CmsResolution<T> {\n requireValidator(validate);\n const doc = locationIdentity(raw);\n if (doc === null || doc.slug !== fallback.id) return { data: fallback, fallbackFields: [\"$\"] };\n // DK locations.ts @ 2c8c586c1e985bd7b1e0e1334e9a360282b1fd86:\n // full merge and hours validation happen after duplicate detection; only hours retry on repository data.\n const parsedHours = parseHours(doc.hours);\n const merged = { ...fallback, label: doc.name, address: fallback.address === null ? null : {\n ...fallback.address, streetAddress: doc.addressLines ?? fallback.address.streetAddress,\n postalCode: doc.postcode ?? fallback.address.postalCode },\n telephone: doc.phone === undefined ? fallback.telephone : doc.phone,\n email: doc.email === undefined ? fallback.email : doc.email,\n geo: doc.coordinates == null ? fallback.geo : { latitude: doc.coordinates.lat, longitude: doc.coordinates.lng },\n hours: parsedHours ?? fallback.hours } as T;\n try { return { data: validate(merged), fallbackFields: parsedHours === null ? [\"hours\"] : [] }; }\n catch {\n if (parsedHours !== null) {\n try { return { data: validate({ ...merged, hours: fallback.hours }), fallbackFields: [\"hours\"] }; }\n catch { /* A bad saved field leaves this whole location on repository data. */ }\n }\n return { data: fallback, fallbackFields: [\"$\"] };\n }\n}\nexport function locationTags(scope: string): readonly string[] { return cmsTags(scope, { types: [\"location\"] }); }\nfunction locationListQuery<T extends { readonly id: string }>(name: string, parse: (raw: unknown, fallback: T) => CmsResolution<T>): CmsQuery<readonly T[]> {\n return defineCmsQuery({ name, query: '*[_type == \"location\" && defined(slug.current) && !(_id in path(\"drafts.**\")) && !(_id in path(\"versions.**\"))]{_id, _type, name, slug, addressLines, postcode, coordinates, phone, email, hours}',\n params: {}, dependencies: { types: [\"location\"] }, resolve: (raw, fallback) => {\n if (!Array.isArray(raw)) return { data: fallback, fallbackFields: [\"$\"] };\n // DK locations.ts @ 2c8c586c1e985bd7b1e0e1334e9a360282b1fd86:\n // document.safeParse first, duplicate slugs second, full merge last.\n const bySlug = new Map<string, unknown>();\n const duplicates = new Set<string>();\n for (const document of raw) {\n const identity = locationIdentity(document);\n if (identity === null) continue;\n if (bySlug.has(identity.slug)) duplicates.add(identity.slug);\n else bySlug.set(identity.slug, document);\n }\n const fields: string[] = [];\n const data = fallback.map((place, index) => {\n const document = bySlug.get(place.id);\n const resolved = document === undefined || duplicates.has(place.id)\n ? { data: place, fallbackFields: [\"$\"] } : parse(document, place);\n fields.push(...resolved.fallbackFields.map((field) => field === \"$\" ? `locations[${index}]` : `locations[${index}].${field}`));\n return resolved.data;\n });\n return { data, fallbackFields: fallback.length > 0 && fields.length === fallback.length && fields.every((field) => /^locations\\[\\d+\\]$/.test(field)) ? [\"$\"] : fields };\n } });\n}\nexport function locationsQuery(validate: LocationValidator<LocationFields>): CmsQuery<readonly LocationFields[]> {\n requireValidator(validate);\n return locationListQuery(\"locations.v1\", (raw, fallback) => parseLocationDocument(raw, fallback, validate));\n}\nexport function businessLocationsQuery<T extends BusinessLocationLike>(validate: LocationValidator<T>): CmsQuery<readonly T[]> {\n requireValidator(validate);\n return locationListQuery(\"locations.business.v1\", (raw, fallback) => parseBusinessLocation(raw, fallback, validate));\n}\n\nexport type ScheduleStop<Source = unknown> = {\n readonly id: string; readonly name: string; readonly from: string; readonly to: string; readonly address: string | null;\n readonly place: string | null; readonly postcode: string | null; readonly notes: string | null;\n readonly lat: number | null; readonly lng: number | null; readonly coordinateSource: Source | null;\n readonly approximate: boolean; readonly needsConfirmation: string | null; readonly captured: string;\n};\nexport type WeeklyScheduleInput<Zone extends string = string, Source = unknown> = { readonly timeZone: Zone; readonly days: readonly { readonly day: typeof days[number]; readonly rest: boolean; readonly stops: readonly ScheduleStop<Source>[] }[] };\nfunction stopId(day: string, name: string, used: Set<string>): string {\n const stem = `${day}-${name.normalize(\"NFKD\").toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-|-$/g, \"\") || \"stop\"}`;\n let id = stem;\n for (let n = 2; used.has(id); n++) id = `${stem}-${n}`;\n used.add(id);\n return id;\n}\nexport function parseScheduleDocument<Zone extends string, Source>(raw: unknown, fallback: WeeklyScheduleInput<Zone, Source>): WeeklyScheduleInput<Zone, Source> {\n if (!object(raw) || !published(raw, \"schedule\") || raw._id !== \"schedule\" || raw.timeZone !== fallback.timeZone || !Array.isArray(raw.days) || raw.days.length !== 7) throw new TypeError(\"Invalid schedule document\");\n const mapped = raw.days.map((candidate, index) => {\n if (!object(candidate) || candidate.day !== days[index] || typeof candidate.rest !== \"boolean\" || !Array.isArray(candidate.stops)) throw new TypeError(\"Invalid schedule day\");\n const dayKey = candidate.day as typeof days[number];\n const rest = candidate.rest;\n const stopsRaw = candidate.stops as unknown[];\n const prior = fallback.days.find((entry) => entry.day === dayKey);\n const used = new Set(prior?.stops.map((stop) => stop.id) ?? []);\n const matched = new Set<string>();\n const parsedStops = stopsRaw.map((candidate: unknown) => {\n if (!object(candidate)) throw new TypeError(\"Invalid schedule stop\");\n const stop = candidate;\n if (!nonempty(stop.name) || !timePattern.test(String(stop.from ?? \"\")) || !timePattern.test(String(stop.to ?? \"\")) || !nonempty(stop.address)) throw new TypeError(\"Invalid schedule stop\");\n const previous = prior?.stops.find((candidate) => candidate.name === stop.name && !matched.has(candidate.id));\n if (previous) { matched.add(previous.id); return { ...previous, name: String(stop.name), from: String(stop.from), to: String(stop.to), address: String(stop.address) }; }\n return { id: stopId(dayKey, String(stop.name), used), name: String(stop.name), from: String(stop.from), to: String(stop.to), address: String(stop.address), place: null, postcode: null, notes: null,\n lat: null, lng: null, coordinateSource: null, approximate: false, needsConfirmation: null, captured: `${stop.name}\\n${stop.from}-${stop.to}\\n${stop.address}` };\n });\n return { day: dayKey, rest, stops: rest ? [] : parsedStops };\n });\n return { timeZone: fallback.timeZone, days: mapped };\n}\nexport function scheduleTags(scope: string): readonly string[] { return cmsTags(scope, { types: [\"schedule\"], ids: [\"schedule\"] }); }\nexport function scheduleQuery<Zone extends string = string, Source = unknown>(): CmsQuery<WeeklyScheduleInput<Zone, Source>> {\n return defineCmsQuery<WeeklyScheduleInput<Zone, Source>>({ name: \"schedule.v1\", query: '*[_type == \"schedule\" && _id == \"schedule\" && !(_id in path(\"drafts.**\")) && !(_id in path(\"versions.**\"))][0]{_id, _type, timeZone, days}',\n params: {}, dependencies: { types: [\"schedule\"], ids: [\"schedule\"] },\n resolve: (raw, fallback) => ({ data: raw == null ? fallback : parseScheduleDocument(raw, fallback), fallbackFields: raw == null ? [\"$\"] : [] }) });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["isDenseArray", "value", "index", "MENU_LAYOUTS", "MENU_DECORATIONS", "MENU_LAYOUT_DEFAULTS", "MENU_DOCUMENT_PROJECTION", "menuTags", "scope", "id", "cmsTags", "fail", "path", "reason", "MalformedContentError", "snapshot", "value", "ancestors", "identity", "array", "prototype", "keys", "copied", "length", "entries", "key", "at", "descriptor", "savedLength", "saved", "entry", "error", "object", "text", "required", "optional", "raw", "field", "list", "read", "isDenseArray", "index", "money", "currency", "price", "extra", "item", "availability", "section", "variant", "isPublishedMenuDocument", "parseMenuDocument", "document", "where", "parseSnapshot", "parseSnapshot", "document", "where", "raw", "object", "fail", "isPublishedMenuDocument", "updatedAt", "text", "variants", "list", "variant", "optional", "validMenuFallback", "value", "parseMenuDocument", "MALFORMED_CONTENT", "MalformedContentError", "documentId", "problems", "MENU_PAGE_PROJECTION", "MENU_DOCUMENT_PROJECTION", "CONTENT_PAGES_QUERY", "CONTENT_PAGE_BY_SLUG_QUERY", "TYPE", "ID", "SCOPE", "dependenciesCopy", "dependencies", "types", "type", "ids", "id", "defineCmsQuery", "query", "params", "copied", "key", "descriptor", "value", "cmsTags", "scope", "tags", "menuQuery", "MENU_DOCUMENT_PROJECTION", "raw", "fallback", "validMenuFallback", "data", "parseMenuDocument", "MalformedContentError", "resolution", "value", "field", "warning", "createCmsReader", "options", "seconds", "adapter", "cmsTags", "notify", "event", "input", "readOptions", "query", "local", "tags", "defineCmsQuery", "fallback", "reason", "status", "warnings", "result", "resolved", "object", "value", "nonempty", "published", "kind", "dayPattern", "timePattern", "days", "resolveWithFallback", "reader", "query", "fallback", "result", "emailPattern", "httpUrl", "written", "url", "requireValidator", "validate", "parseVacancyDocument", "raw", "today", "closing", "title", "location", "pdf", "parseVacancies", "entry", "a", "b", "vacancyTags", "scope", "cmsTags", "openVacanciesQuery", "defineCmsQuery", "weekDays", "slugPattern", "locationIdentity", "line", "parseHours", "output", "day", "ranges", "range", "parseLocationDocument", "doc", "parsedHours", "merged", "parseBusinessLocation", "locationTags", "locationListQuery", "name", "parse", "bySlug", "duplicates", "document", "identity", "fields", "place", "index", "resolved", "field", "locationsQuery", "businessLocationsQuery", "stopId", "used", "stem", "id", "n", "parseScheduleDocument", "mapped", "candidate", "dayKey", "rest", "stopsRaw", "prior", "stop", "matched", "parsedStops", "previous", "scheduleTags", "scheduleQuery"]
7
7
  }