@lamgig/content-schemas 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,6 +52,47 @@ get uniform strictness):
52
52
  deliberately kept commerce-free as the pinned example of that shape. A consumer must treat
53
53
  `modules.commerce` as possibly absent — `parsed.modules.commerce?.acceptsOnlineOrders`.
54
54
 
55
+ ## Menu option groups (1.3.0)
56
+
57
+ Sizes and add-ons, as **one** concept: an option group. A size is `min: 1, max: 1`; an add-on
58
+ list is `min: 0, max: n`. Two **optional** keys inside `modules.menu`; nothing existing changes
59
+ and `schemaVersion` stays 1.
60
+
61
+ | Key | Where | Notes |
62
+ |---|---|---|
63
+ | `optionGroups` | `menu` | The site's groups, each `{ id, name, min, max, sortOrder, options }`; each option `{ id, name, priceDelta, isAvailable, sortOrder }` |
64
+ | `optionGroupIds` | each item | Ids into `optionGroups`, **in display order** |
65
+
66
+ - **Absent ≡ `[]`** — the producer omits both keys when there is nothing to say, so a site that
67
+ uses no options publishes exactly its 1.2.0 shape, and every snapshot published before 1.3.0
68
+ stays valid (a rollback can activate one at any time). Never `null`.
69
+ - **Price rule:** unit price = `item.price.amount` + Σ chosen options' `priceDelta`. Deltas are
70
+ integer minor units, ≥ 0, **in the item's currency** — they carry no currency of their own.
71
+ - **Archived groups and options are absent**, and an archived group is dropped from every
72
+ item's `optionGroupIds`. An option that is merely unavailable is present with
73
+ `isAvailable: false` — show it as unavailable rather than hiding it. A group no item uses is
74
+ still published.
75
+ - **Strict-only publish gate** (`menuSchemaStrict` / `contentFileSchemaStrict`): a required group
76
+ has an available option; `max` ≤ available options; ≤ `MAX_OPTION_GROUPS_PER_ITEM` (10)
77
+ groups per item; ≤ `MAX_OPTIONS_PER_GROUP` (30) options per group; one currency per group;
78
+ references resolve, each once; ids unique. Each message names the group or item. The
79
+ **tolerant** schemas do not apply these — a consumer must never reject a payload over a rule
80
+ a later release may relax — but field bounds (`min`/`max` 0–30, `priceDelta` 0–100000, name
81
+ 1–120) hold in both modes, as for every other field.
82
+ - **Consumers on ≤ 1.2.x** strip both keys and keep working: they show the base price only.
83
+
84
+ Types: `MenuOptionGroup`, `MenuOption` (producer); read them off `ConsumerContentFile` in a
85
+ consumer.
86
+
87
+ ## Changelog
88
+
89
+ | Version | Change |
90
+ |---|---|
91
+ | 1.3.0 | Optional `menu.optionGroups` and `items[].optionGroupIds` (Online Commerce C3); `MenuOptionGroup`/`MenuOption` types; `MAX_OPTION_*` constants. `full.json` gains options (hash re-pinned); `minimal.json` unchanged |
92
+ | 1.2.0 | `commerce.provider` becomes an open enum for consumers (`ConsumerContentFile`, `isKnownCommerceProvider`) |
93
+ | 1.1.0 | Optional `commerce` module |
94
+ | 1.0.0 | The locked v1 contract: `siteSettings`, `branding`, `menu` |
95
+
55
96
  ## The `commerce` module (1.1.0)
56
97
 
57
98
  Online-ordering **configuration**. LamGig does not process orders and stores no provider
@@ -153,7 +194,8 @@ and the database can never disagree. Widening the locale subset later is additiv
153
194
 
154
195
  ## Fixtures (`fixtures/v1/`)
155
196
 
156
- - `full.json` — realistic restaurant, every optional field populated.
197
+ - `full.json` — realistic restaurant, every optional field populated (option groups since
198
+ 1.3.0, including an unavailable option).
157
199
  - `minimal.json` — empty menu, no optional fields; also the reference shape for the
158
200
  fresh-site validity check (a newly provisioned site must gather to this shape).
159
201
  - `hashes.json` — checked-in `hashContent` snapshots; a mismatch means serialization or a
@@ -75,6 +75,29 @@ var makeBrandingSchema = (o) => o({
75
75
  });
76
76
  var brandingSchemaStrict = makeBrandingSchema(strictFactory);
77
77
  var brandingSchema = makeBrandingSchema(tolerantFactory);
78
+ var MAX_OPTION_GROUPS_PER_ITEM = 10;
79
+ var MAX_OPTIONS_PER_GROUP = 30;
80
+ var MAX_OPTION_SELECT = 30;
81
+ var MAX_OPTION_PRICE_DELTA = 1e5;
82
+ var makeMenuOptionSchema = (o) => o({
83
+ id: z.uuid(),
84
+ name: z.string().min(1).max(120),
85
+ /** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
86
+ priceDelta: z.number().int().min(0).max(MAX_OPTION_PRICE_DELTA),
87
+ isAvailable: z.boolean(),
88
+ sortOrder: z.number().int().min(0).max(1e5)
89
+ });
90
+ var makeMenuOptionGroupSchema = (o) => o({
91
+ id: z.uuid(),
92
+ name: z.string().min(1).max(120),
93
+ /** Fewest options a customer must choose; ≥ 1 makes the group required. */
94
+ min: z.number().int().min(0).max(MAX_OPTION_SELECT),
95
+ /** Most options a customer may choose. */
96
+ max: z.number().int().min(0).max(MAX_OPTION_SELECT),
97
+ sortOrder: z.number().int().min(0).max(1e5),
98
+ /** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
99
+ options: z.array(makeMenuOptionSchema(o))
100
+ });
78
101
  var makeMenuItemSchema = (o) => o({
79
102
  /** Stable DB UUID — identical across publishes (phase-2.md T1.3). */
80
103
  id: z.uuid(),
@@ -88,7 +111,9 @@ var makeMenuItemSchema = (o) => o({
88
111
  imageUrl: z.url().optional(),
89
112
  tags: z.array(z.string().min(1)).max(20),
90
113
  sortOrder: z.number().int().min(0).max(1e5),
91
- isAvailable: z.boolean()
114
+ isAvailable: z.boolean(),
115
+ /** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
116
+ optionGroupIds: z.array(z.uuid()).optional()
92
117
  });
93
118
  var makeMenuCategorySchema = (o) => o({
94
119
  id: z.uuid(),
@@ -99,9 +124,75 @@ var makeMenuCategorySchema = (o) => o({
99
124
  });
100
125
  var makeMenuSchema = (o) => o({
101
126
  /** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
102
- categories: z.array(makeMenuCategorySchema(o))
127
+ categories: z.array(makeMenuCategorySchema(o)),
128
+ /** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
129
+ optionGroups: z.array(makeMenuOptionGroupSchema(o)).optional()
103
130
  });
104
- var menuSchemaStrict = makeMenuSchema(strictFactory);
131
+ function checkMenuOptions(menu, ctx) {
132
+ const issue = (path, message) => ctx.addIssue({ code: "custom", path, message });
133
+ const groups = menu.optionGroups ?? [];
134
+ const groupNames = /* @__PURE__ */ new Map();
135
+ const optionIds = /* @__PURE__ */ new Set();
136
+ groups.forEach((group, g) => {
137
+ const label = `Option group "${group.name}"`;
138
+ if (groupNames.has(group.id)) issue(["optionGroups", g, "id"], `${label} repeats another group's id`);
139
+ groupNames.set(group.id, group.name);
140
+ if (group.min > group.max) {
141
+ issue(["optionGroups", g, "min"], `${label} requires at least ${group.min} but allows at most ${group.max}`);
142
+ }
143
+ if (group.options.length > MAX_OPTIONS_PER_GROUP) {
144
+ issue(
145
+ ["optionGroups", g, "options"],
146
+ `${label} has ${group.options.length} options; the limit is ${MAX_OPTIONS_PER_GROUP}`
147
+ );
148
+ }
149
+ const available = group.options.filter((option) => option.isAvailable).length;
150
+ if (group.min >= 1 && available === 0) {
151
+ issue(["optionGroups", g, "options"], `${label} is required but has no available options`);
152
+ } else if (group.max > available) {
153
+ issue(
154
+ ["optionGroups", g, "max"],
155
+ `${label} allows up to ${group.max} choices but has only ${available} available option${available === 1 ? "" : "s"}`
156
+ );
157
+ }
158
+ group.options.forEach((option, k) => {
159
+ if (optionIds.has(option.id)) {
160
+ issue(["optionGroups", g, "options", k, "id"], `${label}: option "${option.name}" repeats another option's id`);
161
+ }
162
+ optionIds.add(option.id);
163
+ });
164
+ });
165
+ const firstUse = /* @__PURE__ */ new Map();
166
+ menu.categories.forEach((category, c) => {
167
+ category.items.forEach((item, i) => {
168
+ const ids = item.optionGroupIds ?? [];
169
+ const path = ["categories", c, "items", i, "optionGroupIds"];
170
+ if (ids.length > MAX_OPTION_GROUPS_PER_ITEM) {
171
+ issue(path, `Item "${item.name}" has ${ids.length} option groups; the limit is ${MAX_OPTION_GROUPS_PER_ITEM}`);
172
+ }
173
+ const seen = /* @__PURE__ */ new Set();
174
+ ids.forEach((id, k) => {
175
+ if (seen.has(id)) issue([...path, k], `Item "${item.name}" lists the same option group twice`);
176
+ seen.add(id);
177
+ const groupName = groupNames.get(id);
178
+ if (groupName === void 0) {
179
+ issue([...path, k], `Item "${item.name}" uses an option group that is not published`);
180
+ return;
181
+ }
182
+ const prior = firstUse.get(id);
183
+ if (!prior) {
184
+ firstUse.set(id, { currency: item.price.currency, itemName: item.name });
185
+ } else if (prior.currency !== item.price.currency) {
186
+ issue(
187
+ [...path, k],
188
+ `Option group "${groupName}" is used by items in two currencies: "${prior.itemName}" (${prior.currency}) and "${item.name}" (${item.price.currency})`
189
+ );
190
+ }
191
+ });
192
+ });
193
+ });
194
+ }
195
+ var menuSchemaStrict = makeMenuSchema(strictFactory).superRefine(checkMenuOptions);
105
196
  var menuSchema = makeMenuSchema(tolerantFactory);
106
197
  var makeCommerceHoursSchema = (o) => z.array(
107
198
  o({
@@ -159,7 +250,7 @@ var makeCommerceSchema = (o, provider) => o({
159
250
  });
160
251
  var commerceSchemaStrict = makeCommerceSchema(strictFactory, commerceProviderStrict);
161
252
  var commerceSchema = makeCommerceSchema(tolerantFactory, commerceProviderTolerant);
162
- var makeContentFileSchema = (o, commerceProvider) => o({
253
+ var makeContentFileSchema = (o, commerceProvider, menu) => o({
163
254
  schemaVersion: z.literal(1),
164
255
  site: o({ id: z.uuid(), slug: z.string().min(1) }),
165
256
  publishedAt: z.iso.datetime(),
@@ -176,12 +267,20 @@ var makeContentFileSchema = (o, commerceProvider) => o({
176
267
  modules: o({
177
268
  siteSettings: makeSiteSettingsSchema(o),
178
269
  branding: makeBrandingSchema(o),
179
- menu: makeMenuSchema(o),
270
+ menu,
180
271
  commerce: makeCommerceSchema(o, commerceProvider).optional()
181
272
  })
182
273
  });
183
- var contentFileSchemaStrict = makeContentFileSchema(strictFactory, commerceProviderStrict);
184
- var contentFileSchema = makeContentFileSchema(tolerantFactory, commerceProviderTolerant);
274
+ var contentFileSchemaStrict = makeContentFileSchema(
275
+ strictFactory,
276
+ commerceProviderStrict,
277
+ menuSchemaStrict
278
+ );
279
+ var contentFileSchema = makeContentFileSchema(
280
+ tolerantFactory,
281
+ commerceProviderTolerant,
282
+ menuSchema
283
+ );
185
284
  function canonicalize(payload) {
186
285
  const out = serialize(payload);
187
286
  if (out === void 0) {
@@ -246,6 +345,10 @@ export {
246
345
  siteSettingsSchema,
247
346
  brandingSchemaStrict,
248
347
  brandingSchema,
348
+ MAX_OPTION_GROUPS_PER_ITEM,
349
+ MAX_OPTIONS_PER_GROUP,
350
+ MAX_OPTION_SELECT,
351
+ MAX_OPTION_PRICE_DELTA,
249
352
  menuSchemaStrict,
250
353
  menuSchema,
251
354
  COMMERCE_PROVIDERS,
@@ -258,4 +361,4 @@ export {
258
361
  contentFileSchema,
259
362
  canonicalize
260
363
  };
261
- //# sourceMappingURL=chunk-PMUVTXAV.js.map
364
+ //# sourceMappingURL=chunk-7B6AENRI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Version of the published-JSON contract. Additive changes (new OPTIONAL fields\n * and new OPTIONAL module keys) keep this number; any required addition is\n * breaking and bumps it. The full compatibility rule lives in this package's\n * README. See PLAN.md § Published JSON delivery and phase-2.md T1.5.\n */\nexport const SCHEMA_VERSION = 1;\n\n/** Publisher-enforced ceiling on the canonical payload size (1 MiB). */\nexport const MAX_CONTENT_BYTES = 1_048_576;\n\n// ---------------------------------------------------------------------------\n// Shared value patterns — single source of truth.\n//\n// LOCALE_PATTERN_SOURCE is the EXACT string used by the `site_settings.locale`\n// DB CHECK (supabase/migrations/*_content_site_settings.sql). Contract and\n// database must never disagree (phase-2.md T1.1): v1 supports the named BCP-47\n// subset `ll` or `ll-CC` only (e.g. `en`, `en-US`); script/numeric-region forms\n// (`zh-Hant`, `es-419`) are out of scope — widening later is additive.\n// HEX_COLOR / CURRENCY are likewise mirrored by DB CHECKs on `branding.colors`\n// and `menu_items.price_currency`.\n// ---------------------------------------------------------------------------\n\nexport const LOCALE_PATTERN_SOURCE = '^[a-z]{2}(-[A-Z]{2})?$';\nexport const LOCALE_PATTERN = new RegExp(LOCALE_PATTERN_SOURCE);\n\nexport const HEX_COLOR_PATTERN_SOURCE = '^#[0-9A-Fa-f]{6}$';\nexport const HEX_COLOR_PATTERN = new RegExp(HEX_COLOR_PATTERN_SOURCE);\n\n/** ISO 4217 alpha code shape (mirrored by the `price_currency` DB CHECK). */\nexport const CURRENCY_PATTERN_SOURCE = '^[A-Z]{3}$';\nexport const CURRENCY_PATTERN = new RegExp(CURRENCY_PATTERN_SOURCE);\n\n/** ISO 3166-1 alpha-2 country code shape. */\nexport const COUNTRY_PATTERN_SOURCE = '^[A-Z]{2}$';\nexport const COUNTRY_PATTERN = new RegExp(COUNTRY_PATTERN_SOURCE);\n\n/**\n * Days of the week, in the canonical order `commerce.hours` must be sorted by.\n * The order is part of the contract: a consumer rendering opening hours reads\n * the array straight through, so a payload listing Sunday before Monday would\n * render wrong without being malformed.\n */\nexport const COMMERCE_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const;\n\n/**\n * 24-hour `HH:MM`. UNLIKE the four patterns above this one is deliberately NOT\n * mirrored by a DB CHECK: `commerce_settings.hours` is validated at the DB only\n * as \"an array of at most 7\" (see the migration header), and its element shape,\n * time syntax, day uniqueness and ordering are publish-gated here instead. A\n * schema-invalid draft may therefore exist in the table; it cannot be published.\n */\nexport const TIME_24H_PATTERN_SOURCE = '^([01][0-9]|2[0-3]):[0-5][0-9]$';\nexport const TIME_24H_PATTERN = new RegExp(TIME_24H_PATTERN_SOURCE);\n\n/** IANA time zone check via the runtime's own tz database. */\nexport function isIanaTimeZone(tz: string): boolean {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Two parse modes from ONE schema definition (phase-2.md T1):\n//\n// - producer-strict — `.strict()` everywhere; the dashboard validates its own\n// output against this before publishing. Unknown keys are defects.\n// - consumer-tolerant — unknown object keys are STRIPPED and unknown module\n// keys ignored, so a shipped consumer accepts a v1 payload that later gained\n// an optional field or a new module key (the additive changes the\n// compatibility rule promises).\n//\n// zod v4's `.strip()`/`.passthrough()` do NOT recurse into nested objects, so a\n// post-hoc transform of one tree silently leaves nested objects in the original\n// mode. Instead every object in the tree is built through an injected object\n// factory: `z.strictObject` for the strict pair, `z.object` (strips unknown\n// keys) for the tolerant pair. One definition, two instantiations.\n// ---------------------------------------------------------------------------\n\ntype ObjectFactory = <T extends z.ZodRawShape>(shape: T) => z.ZodObject<T>;\n\nconst strictFactory = z.strictObject as ObjectFactory;\nconst tolerantFactory = z.object as ObjectFactory;\n\n// --- siteSettings ----------------------------------------------------------\n\nconst makeSiteSettingsSchema = (o: ObjectFactory) =>\n o({\n siteName: z.string().min(1).max(120),\n tagline: z.string().max(200).optional(),\n contact: o({\n email: z.email().optional(),\n phone: z.string().min(1).optional(),\n address: o({\n line1: z.string().min(1),\n line2: z.string().min(1).optional(),\n city: z.string().min(1),\n region: z.string().min(1),\n postalCode: z.string().min(1),\n country: z.string().regex(COUNTRY_PATTERN),\n }).optional(),\n }),\n // Explicit known keys (NOT a record): strict rejects unknown platforms,\n // tolerant strips them — adding a platform later is additive.\n socialLinks: o({\n facebook: z.url().optional(),\n instagram: z.url().optional(),\n tiktok: z.url().optional(),\n x: z.url().optional(),\n youtube: z.url().optional(),\n yelp: z.url().optional(),\n google: z.url().optional(),\n }).optional(),\n locale: z.string().regex(LOCALE_PATTERN).default('en-US'),\n timezone: z\n .string()\n .min(1)\n .refine(isIanaTimeZone, 'must be an IANA time zone name')\n .default('America/Los_Angeles'),\n });\n\nexport const siteSettingsSchemaStrict = makeSiteSettingsSchema(strictFactory);\nexport const siteSettingsSchema = makeSiteSettingsSchema(tolerantFactory);\n\n// --- branding --------------------------------------------------------------\n\nconst makeBrandingSchema = (o: ObjectFactory) =>\n o({\n // Absolute URLs produced by the dashboard's storagePublicUrl builder\n // (apps/dashboard/src/lib/content-urls.ts). The DB stores storage PATHS;\n // the publish-time compose maps path -> URL. This package never imports\n // from the app (dependency direction, phase-2.md T1.7).\n logoUrl: z.url().optional(),\n faviconUrl: z.url().optional(),\n colors: o({\n primary: z.string().regex(HEX_COLOR_PATTERN),\n secondary: z.string().regex(HEX_COLOR_PATTERN).optional(),\n accent: z.string().regex(HEX_COLOR_PATTERN).optional(),\n background: z.string().regex(HEX_COLOR_PATTERN).optional(),\n text: z.string().regex(HEX_COLOR_PATTERN).optional(),\n }),\n fonts: o({\n heading: z.string().min(1).optional(),\n body: z.string().min(1).optional(),\n }).optional(),\n });\n\nexport const brandingSchemaStrict = makeBrandingSchema(strictFactory);\nexport const brandingSchema = makeBrandingSchema(tolerantFactory);\n\n// --- menu ------------------------------------------------------------------\n//\n// OPTION GROUPS (1.3.0, Online Commerce C3): one concept for sizes and add-ons.\n// A size is a group with min 1 / max 1; an add-on list is min 0 / max n. Groups\n// are site-level and reusable — defined once in `optionGroups`, attached to an\n// item by id in `optionGroupIds`, in display order. Both keys are OPTIONAL and\n// ABSENT when empty, so every pre-1.3.0 snapshot (and every site that uses no\n// options) stays valid — ARCHITECTURE.md § 5.\n//\n// Price rule: a line's unit price = item `price.amount` + Σ chosen options'\n// `priceDelta`. Deltas carry no currency of their own: they are integer minor\n// units in the currency of the ITEM they are chosen on.\n//\n// Field bounds below hold in BOTH modes and mirror the DB CHECKs. The rules that\n// relate objects to each other (references resolve, counts, required groups,\n// one currency per group) are the producer's PUBLISH GATE and live only in the\n// strict schema — see `checkMenuOptions`.\n\n/** At most this many option groups on one item (publish-gated, not DB-enforced). */\nexport const MAX_OPTION_GROUPS_PER_ITEM = 10;\n/** At most this many published options in one group (publish-gated). */\nexport const MAX_OPTIONS_PER_GROUP = 30;\n/** Upper bound of a group's `min`/`max` (mirrors the DB CHECK). */\nexport const MAX_OPTION_SELECT = 30;\n/** Upper bound of an option's `priceDelta`, minor units (mirrors the DB CHECK). */\nexport const MAX_OPTION_PRICE_DELTA = 100_000;\n\nconst makeMenuOptionSchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(120),\n /** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */\n priceDelta: z.number().int().min(0).max(MAX_OPTION_PRICE_DELTA),\n isAvailable: z.boolean(),\n sortOrder: z.number().int().min(0).max(100_000),\n });\n\nconst makeMenuOptionGroupSchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(120),\n /** Fewest options a customer must choose; ≥ 1 makes the group required. */\n min: z.number().int().min(0).max(MAX_OPTION_SELECT),\n /** Most options a customer may choose. */\n max: z.number().int().min(0).max(MAX_OPTION_SELECT),\n sortOrder: z.number().int().min(0).max(100_000),\n /** Archived options are absent; an unavailable one is present with `isAvailable: false`. */\n options: z.array(makeMenuOptionSchema(o)),\n });\n\nconst makeMenuItemSchema = (o: ObjectFactory) =>\n o({\n /** Stable DB UUID — identical across publishes (phase-2.md T1.3). */\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n price: o({\n /** Integer minor units (e.g. cents). */\n amount: z.number().int().min(0),\n currency: z.string().regex(CURRENCY_PATTERN),\n }),\n imageUrl: z.url().optional(),\n tags: z.array(z.string().min(1)).max(20),\n sortOrder: z.number().int().min(0).max(100_000),\n isAvailable: z.boolean(),\n /** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */\n optionGroupIds: z.array(z.uuid()).optional(),\n });\n\nconst makeMenuCategorySchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n sortOrder: z.number().int().min(0).max(100_000),\n items: z.array(makeMenuItemSchema(o)),\n });\n\nconst makeMenuSchema = (o: ObjectFactory) =>\n o({\n /** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */\n categories: z.array(makeMenuCategorySchema(o)),\n /** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */\n optionGroups: z.array(makeMenuOptionGroupSchema(o)).optional(),\n });\n\ntype MenuShape = z.output<ReturnType<typeof makeMenuSchema>>;\n\n/**\n * The menu's PUBLISH GATE for option groups (C3) — strict schema only. A draft\n * may break any of these and still be saved; it cannot be published. Every\n * message names the group or item, because it is shown to the merchant verbatim\n * by the publish screen.\n *\n * Strict-only on purpose: these are rules about what the PRODUCER may emit, and\n * a consumer must not reject a whole payload because a later release relaxed one\n * (say, more groups per item).\n */\nfunction checkMenuOptions(menu: MenuShape, ctx: z.RefinementCtx): void {\n const issue = (path: (string | number)[], message: string) =>\n ctx.addIssue({ code: 'custom', path, message });\n\n const groups = menu.optionGroups ?? [];\n const groupNames = new Map<string, string>();\n const optionIds = new Set<string>();\n groups.forEach((group, g) => {\n const label = `Option group \"${group.name}\"`;\n if (groupNames.has(group.id)) issue(['optionGroups', g, 'id'], `${label} repeats another group's id`);\n groupNames.set(group.id, group.name);\n\n if (group.min > group.max) {\n issue(['optionGroups', g, 'min'], `${label} requires at least ${group.min} but allows at most ${group.max}`);\n }\n if (group.options.length > MAX_OPTIONS_PER_GROUP) {\n issue(\n ['optionGroups', g, 'options'],\n `${label} has ${group.options.length} options; the limit is ${MAX_OPTIONS_PER_GROUP}`,\n );\n }\n const available = group.options.filter((option) => option.isAvailable).length;\n if (group.min >= 1 && available === 0) {\n issue(['optionGroups', g, 'options'], `${label} is required but has no available options`);\n } else if (group.max > available) {\n issue(\n ['optionGroups', g, 'max'],\n `${label} allows up to ${group.max} choices but has only ${available} available option${available === 1 ? '' : 's'}`,\n );\n }\n group.options.forEach((option, k) => {\n if (optionIds.has(option.id)) {\n issue(['optionGroups', g, 'options', k, 'id'], `${label}: option \"${option.name}\" repeats another option's id`);\n }\n optionIds.add(option.id);\n });\n });\n\n // Deltas are in the item's currency, so one group cannot serve two currencies.\n const firstUse = new Map<string, { currency: string; itemName: string }>();\n menu.categories.forEach((category, c) => {\n category.items.forEach((item, i) => {\n const ids = item.optionGroupIds ?? [];\n const path = ['categories', c, 'items', i, 'optionGroupIds'];\n if (ids.length > MAX_OPTION_GROUPS_PER_ITEM) {\n issue(path, `Item \"${item.name}\" has ${ids.length} option groups; the limit is ${MAX_OPTION_GROUPS_PER_ITEM}`);\n }\n const seen = new Set<string>();\n ids.forEach((id, k) => {\n if (seen.has(id)) issue([...path, k], `Item \"${item.name}\" lists the same option group twice`);\n seen.add(id);\n const groupName = groupNames.get(id);\n if (groupName === undefined) {\n issue([...path, k], `Item \"${item.name}\" uses an option group that is not published`);\n return;\n }\n const prior = firstUse.get(id);\n if (!prior) {\n firstUse.set(id, { currency: item.price.currency, itemName: item.name });\n } else if (prior.currency !== item.price.currency) {\n issue(\n [...path, k],\n `Option group \"${groupName}\" is used by items in two currencies: ` +\n `\"${prior.itemName}\" (${prior.currency}) and \"${item.name}\" (${item.price.currency})`,\n );\n }\n });\n });\n });\n}\n\nexport const menuSchemaStrict = makeMenuSchema(strictFactory).superRefine(checkMenuOptions);\nexport const menuSchema = makeMenuSchema(tolerantFactory);\n\n// --- commerce --------------------------------------------------------------\n//\n// Online-ordering CONFIGURATION, never orders and never provider secrets\n// (docs/seams/commerce.md § \"What does NOT go in the module\"): a published\n// payload is world-readable immutable JSON, so a credential written here would\n// be permanent and public. Everything below is a public identifier or a display\n// value.\n//\n// `provider` is the one OPEN enum in the contract (docs/proposals/online-commerce\n// OC-2). The PRODUCER enum is closed and grows by one member per phase, only\n// when the platform can actually serve the new value — today `none | other`.\n// The CONSUMER schema accepts any well-formed provider name and hands it through\n// unchanged, so a consumer that predates a new provider keeps parsing.\n//\n// That second half did not exist before 1.2.0. Until then the tolerant schema\n// declared the same closed enum as the strict one, so an unknown provider made\n// the WHOLE payload fail to parse — the comment here claimed widening was\n// \"additive for a tolerant consumer\", and the code said otherwise. Tolerance for\n// unknown KEYS (the object factories) never covered unknown enum VALUES.\n//\n// Consumer rule (README § Open enums): an unknown provider means \"newer than\n// me\" — treat it like `other`: offer the `orderingUrl` hand-off when\n// `acceptsOnlineOrders` is true, and never show a cart.\n//\n// TIMES ARE LOCAL to `siteSettings.timezone` and carry no offset — the site's\n// own clock is what a customer reads on the door. `closes < opens` is LEGAL and\n// means \"closes after midnight\" (docs/seams/commerce.md § 1); a consumer must\n// not treat it as an error.\n\nconst makeCommerceHoursSchema = (o: ObjectFactory) =>\n z\n .array(\n o({\n day: z.enum(COMMERCE_DAYS),\n opens: z.string().regex(TIME_24H_PATTERN),\n closes: z.string().regex(TIME_24H_PATTERN),\n }),\n )\n .max(7)\n // At most one entry per day, listed in COMMERCE_DAYS order. The DB CHECK\n // stops at \"an array of at most 7\" (deliberately — see the migration\n // header), so THIS is the layer that makes `hours` meaningful, and it runs\n // at publish time rather than at write time.\n .superRefine((entries, ctx) => {\n const seen = new Set<string>();\n let previous = -1;\n entries.forEach((entry, index) => {\n if (seen.has(entry.day)) {\n ctx.addIssue({\n code: 'custom',\n path: [index, 'day'],\n message: `duplicate day \"${entry.day}\"`,\n });\n }\n seen.add(entry.day);\n const position = COMMERCE_DAYS.indexOf(entry.day);\n if (position < previous) {\n ctx.addIssue({\n code: 'custom',\n path: [index, 'day'],\n message: 'hours must be ordered mon → sun',\n });\n }\n previous = position;\n });\n });\n\n/** Providers the PRODUCER may publish. Grows one member per phase (OC-2). */\nexport const COMMERCE_PROVIDERS = ['none', 'other'] as const;\nexport type KnownCommerceProvider = (typeof COMMERCE_PROVIDERS)[number];\n\n/**\n * Shape of a provider name a TOLERANT consumer accepts: lowercase, starts with a\n * letter, at most 32 characters. Tolerance is for FUTURE values, not malformed\n * ones — `\"Stripe!\"` or `\"\"` still fail.\n */\nexport const COMMERCE_PROVIDER_PATTERN_SOURCE = '^[a-z][a-z0-9_]{0,31}$';\nexport const COMMERCE_PROVIDER_PATTERN = new RegExp(COMMERCE_PROVIDER_PATTERN_SOURCE);\n\nconst commerceProviderStrict = z.enum(COMMERCE_PROVIDERS);\nconst commerceProviderTolerant = z.union([\n z.enum(COMMERCE_PROVIDERS),\n z.string().regex(COMMERCE_PROVIDER_PATTERN),\n]);\n\n/** Narrow a tolerantly-parsed provider to one this version of the package knows. */\nexport function isKnownCommerceProvider(provider: string): provider is KnownCommerceProvider {\n return (COMMERCE_PROVIDERS as readonly string[]).includes(provider);\n}\n\ntype CommerceProviderSchema = typeof commerceProviderStrict | typeof commerceProviderTolerant;\n\nconst makeCommerceSchema = <P extends CommerceProviderSchema>(o: ObjectFactory, provider: P) =>\n o({\n provider,\n /** Where a customer is sent to order. Absent when `provider` is `none`. */\n orderingUrl: z.url({ protocol: /^https$/ }).max(2048).optional(),\n /** PUBLIC provider identifiers — never a key, token or secret. */\n locationRef: z.string().min(1).max(128).optional(),\n menuRef: z.string().min(1).max(128).optional(),\n /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */\n currency: z.string().regex(CURRENCY_PATTERN),\n acceptsOnlineOrders: z.boolean(),\n /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */\n prepTimeMinutes: z.number().int().min(0).max(240).optional(),\n hours: makeCommerceHoursSchema(o),\n });\n\nexport const commerceSchemaStrict = makeCommerceSchema(strictFactory, commerceProviderStrict);\nexport const commerceSchema = makeCommerceSchema(tolerantFactory, commerceProviderTolerant);\n\n// --- content file ----------------------------------------------------------\n\n// `menu` is passed in rather than built from `o` for the same reason as the\n// provider: the two modes differ beyond the object factory — the strict menu\n// carries the option-group publish gate (`checkMenuOptions`), the tolerant one\n// does not.\nconst makeContentFileSchema = <P extends CommerceProviderSchema, M extends typeof menuSchema>(\n o: ObjectFactory,\n commerceProvider: P,\n menu: M,\n) =>\n o({\n schemaVersion: z.literal(1),\n site: o({ id: z.uuid(), slug: z.string().min(1) }),\n publishedAt: z.iso.datetime(),\n // The three v1 modules are REQUIRED; empty states are valid module payloads\n // (e.g. menu.categories: []), never missing keys. A new required module is\n // breaking (retained/rollback snapshots won't contain it) — bump\n // SCHEMA_VERSION instead.\n //\n // `commerce` is therefore OPTIONAL (phase-7 D-3), and that is not a\n // stylistic choice: every snapshot published before the module existed\n // lacks the key, and a ROLLBACK can activate one of those at any time. A\n // required `commerce` would retroactively invalidate the whole publish\n // history. See ARCHITECTURE.md § 5 and docs/seams/commerce.md § 1.\n modules: o({\n siteSettings: makeSiteSettingsSchema(o),\n branding: makeBrandingSchema(o),\n menu,\n commerce: makeCommerceSchema(o, commerceProvider).optional(),\n }),\n });\n\n/** Producer gate: the dashboard validates its own output against this. */\nexport const contentFileSchemaStrict = makeContentFileSchema(\n strictFactory,\n commerceProviderStrict,\n menuSchemaStrict,\n);\n/** Public consumer schema: tolerant of additive v1 changes. */\nexport const contentFileSchema = makeContentFileSchema(\n tolerantFactory,\n commerceProviderTolerant,\n menuSchema,\n);\n\nexport type SiteSettings = z.infer<typeof siteSettingsSchemaStrict>;\nexport type Branding = z.infer<typeof brandingSchemaStrict>;\nexport type Menu = z.infer<typeof menuSchemaStrict>;\nexport type MenuCategory = Menu['categories'][number];\nexport type MenuItem = MenuCategory['items'][number];\nexport type MenuOptionGroup = NonNullable<Menu['optionGroups']>[number];\nexport type MenuOption = MenuOptionGroup['options'][number];\nexport type Commerce = z.infer<typeof commerceSchemaStrict>;\nexport type CommerceDay = (typeof COMMERCE_DAYS)[number];\nexport type CommerceHours = Commerce['hours'];\nexport type ContentFile = z.infer<typeof contentFileSchemaStrict>;\n\n/**\n * What a CONSUMER gets back from `contentFileSchema.parse` (1.2.0+). Identical to\n * `ContentFile` except that open enums are widened — `commerce.provider` is any\n * well-formed string, because a payload may name a provider newer than the\n * consumer. Type parsed content with THIS, not with the producer's `ContentFile`:\n * annotating tolerant output as `ContentFile` claims the provider is one this\n * package version knows, which is exactly what tolerance cannot promise.\n */\nexport type ConsumerContentFile = z.infer<typeof contentFileSchema>;\nexport type ConsumerCommerce = z.infer<typeof commerceSchema>;\n\n// ---------------------------------------------------------------------------\n// Canonical serialization (phase-2.md T1.6).\n//\n// Recursively key-sorted JSON with no whitespace, produced by a hand-rolled\n// serializer rather than JSON.stringify over a re-built object — rebuilding\n// depends on JS insertion-order semantics and silently reorders integer-like\n// keys. Determinism here is what makes `hashContent` a byte-equality contract\n// shared by the publisher and every consumer test.\n//\n// Rules (documented in the README):\n// - arrays keep their order; object keys are sorted with Array.prototype.sort\n// (code-unit order);\n// - object properties whose value is `undefined` are omitted (JSON semantics);\n// - `undefined` anywhere else, non-finite numbers, bigint, functions and\n// symbols THROW — never silently coerced the way JSON.stringify would.\n// ---------------------------------------------------------------------------\n\nexport function canonicalize(payload: unknown): string {\n const out = serialize(payload);\n if (out === undefined) {\n throw new TypeError('canonicalize: payload itself serializes to nothing (undefined)');\n }\n return out;\n}\n\nfunction serialize(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (value === null) return 'null';\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return JSON.stringify(value);\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(`canonicalize: non-finite number ${String(value)}`);\n }\n return JSON.stringify(value);\n case 'object':\n break;\n default:\n throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);\n }\n if (Array.isArray(value)) {\n const parts = value.map((entry) => {\n const s = serialize(entry);\n if (s === undefined) {\n throw new TypeError('canonicalize: undefined array element');\n }\n return s;\n });\n return `[${parts.join(',')}]`;\n }\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort();\n const parts: string[] = [];\n for (const key of keys) {\n const s = serialize(record[key]);\n if (s === undefined) continue; // undefined property -> omitted key\n parts.push(`${JSON.stringify(key)}:${s}`);\n }\n return `{${parts.join(',')}}`;\n}\n"],"mappings":"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","names":["parts"]}
package/dist/hash.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  canonicalize
3
- } from "./chunk-PMUVTXAV.js";
3
+ } from "./chunk-7B6AENRI.js";
4
4
 
5
5
  // src/hash.ts
6
6
  import { createHash } from "crypto";
package/dist/index.d.ts CHANGED
@@ -121,6 +121,14 @@ declare const brandingSchema: z.ZodObject<{
121
121
  body: z.ZodOptional<z.ZodString>;
122
122
  }, z.core.$strip>>;
123
123
  }, z.core.$strip>;
124
+ /** At most this many option groups on one item (publish-gated, not DB-enforced). */
125
+ declare const MAX_OPTION_GROUPS_PER_ITEM = 10;
126
+ /** At most this many published options in one group (publish-gated). */
127
+ declare const MAX_OPTIONS_PER_GROUP = 30;
128
+ /** Upper bound of a group's `min`/`max` (mirrors the DB CHECK). */
129
+ declare const MAX_OPTION_SELECT = 30;
130
+ /** Upper bound of an option's `priceDelta`, minor units (mirrors the DB CHECK). */
131
+ declare const MAX_OPTION_PRICE_DELTA = 100000;
124
132
  declare const menuSchemaStrict: z.ZodObject<{
125
133
  /** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
126
134
  categories: z.ZodArray<z.ZodObject<{
@@ -142,8 +150,29 @@ declare const menuSchemaStrict: z.ZodObject<{
142
150
  tags: z.ZodArray<z.ZodString>;
143
151
  sortOrder: z.ZodNumber;
144
152
  isAvailable: z.ZodBoolean;
153
+ /** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
154
+ optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
145
155
  }, z.core.$strip>>;
146
156
  }, z.core.$strip>>;
157
+ /** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
158
+ optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
159
+ id: z.ZodUUID;
160
+ name: z.ZodString;
161
+ /** Fewest options a customer must choose; ≥ 1 makes the group required. */
162
+ min: z.ZodNumber;
163
+ /** Most options a customer may choose. */
164
+ max: z.ZodNumber;
165
+ sortOrder: z.ZodNumber;
166
+ /** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
167
+ options: z.ZodArray<z.ZodObject<{
168
+ id: z.ZodUUID;
169
+ name: z.ZodString;
170
+ /** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
171
+ priceDelta: z.ZodNumber;
172
+ isAvailable: z.ZodBoolean;
173
+ sortOrder: z.ZodNumber;
174
+ }, z.core.$strip>>;
175
+ }, z.core.$strip>>>;
147
176
  }, z.core.$strip>;
148
177
  declare const menuSchema: z.ZodObject<{
149
178
  /** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
@@ -166,8 +195,29 @@ declare const menuSchema: z.ZodObject<{
166
195
  tags: z.ZodArray<z.ZodString>;
167
196
  sortOrder: z.ZodNumber;
168
197
  isAvailable: z.ZodBoolean;
198
+ /** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
199
+ optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
169
200
  }, z.core.$strip>>;
170
201
  }, z.core.$strip>>;
202
+ /** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
203
+ optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
204
+ id: z.ZodUUID;
205
+ name: z.ZodString;
206
+ /** Fewest options a customer must choose; ≥ 1 makes the group required. */
207
+ min: z.ZodNumber;
208
+ /** Most options a customer may choose. */
209
+ max: z.ZodNumber;
210
+ sortOrder: z.ZodNumber;
211
+ /** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
212
+ options: z.ZodArray<z.ZodObject<{
213
+ id: z.ZodUUID;
214
+ name: z.ZodString;
215
+ /** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
216
+ priceDelta: z.ZodNumber;
217
+ isAvailable: z.ZodBoolean;
218
+ sortOrder: z.ZodNumber;
219
+ }, z.core.$strip>>;
220
+ }, z.core.$strip>>>;
171
221
  }, z.core.$strip>;
172
222
  /** Providers the PRODUCER may publish. Grows one member per phase (OC-2). */
173
223
  declare const COMMERCE_PROVIDERS: readonly ["none", "other"];
@@ -311,8 +361,29 @@ declare const contentFileSchemaStrict: z.ZodObject<{
311
361
  tags: z.ZodArray<z.ZodString>;
312
362
  sortOrder: z.ZodNumber;
313
363
  isAvailable: z.ZodBoolean;
364
+ /** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
365
+ optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
314
366
  }, z.core.$strip>>;
315
367
  }, z.core.$strip>>;
368
+ /** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
369
+ optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
370
+ id: z.ZodUUID;
371
+ name: z.ZodString;
372
+ /** Fewest options a customer must choose; ≥ 1 makes the group required. */
373
+ min: z.ZodNumber;
374
+ /** Most options a customer may choose. */
375
+ max: z.ZodNumber;
376
+ sortOrder: z.ZodNumber;
377
+ /** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
378
+ options: z.ZodArray<z.ZodObject<{
379
+ id: z.ZodUUID;
380
+ name: z.ZodString;
381
+ /** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
382
+ priceDelta: z.ZodNumber;
383
+ isAvailable: z.ZodBoolean;
384
+ sortOrder: z.ZodNumber;
385
+ }, z.core.$strip>>;
386
+ }, z.core.$strip>>>;
316
387
  }, z.core.$strip>;
317
388
  commerce: z.ZodOptional<z.ZodObject<{
318
389
  provider: z.ZodEnum<{
@@ -417,8 +488,29 @@ declare const contentFileSchema: z.ZodObject<{
417
488
  tags: z.ZodArray<z.ZodString>;
418
489
  sortOrder: z.ZodNumber;
419
490
  isAvailable: z.ZodBoolean;
491
+ /** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
492
+ optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
420
493
  }, z.core.$strip>>;
421
494
  }, z.core.$strip>>;
495
+ /** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
496
+ optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
497
+ id: z.ZodUUID;
498
+ name: z.ZodString;
499
+ /** Fewest options a customer must choose; ≥ 1 makes the group required. */
500
+ min: z.ZodNumber;
501
+ /** Most options a customer may choose. */
502
+ max: z.ZodNumber;
503
+ sortOrder: z.ZodNumber;
504
+ /** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
505
+ options: z.ZodArray<z.ZodObject<{
506
+ id: z.ZodUUID;
507
+ name: z.ZodString;
508
+ /** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
509
+ priceDelta: z.ZodNumber;
510
+ isAvailable: z.ZodBoolean;
511
+ sortOrder: z.ZodNumber;
512
+ }, z.core.$strip>>;
513
+ }, z.core.$strip>>>;
422
514
  }, z.core.$strip>;
423
515
  commerce: z.ZodOptional<z.ZodObject<{
424
516
  provider: z.ZodUnion<readonly [z.ZodEnum<{
@@ -456,6 +548,8 @@ type Branding = z.infer<typeof brandingSchemaStrict>;
456
548
  type Menu = z.infer<typeof menuSchemaStrict>;
457
549
  type MenuCategory = Menu['categories'][number];
458
550
  type MenuItem = MenuCategory['items'][number];
551
+ type MenuOptionGroup = NonNullable<Menu['optionGroups']>[number];
552
+ type MenuOption = MenuOptionGroup['options'][number];
459
553
  type Commerce = z.infer<typeof commerceSchemaStrict>;
460
554
  type CommerceDay = (typeof COMMERCE_DAYS)[number];
461
555
  type CommerceHours = Commerce['hours'];
@@ -472,4 +566,4 @@ type ConsumerContentFile = z.infer<typeof contentFileSchema>;
472
566
  type ConsumerCommerce = z.infer<typeof commerceSchema>;
473
567
  declare function canonicalize(payload: unknown): string;
474
568
 
475
- export { type Branding, COMMERCE_DAYS, COMMERCE_PROVIDERS, COMMERCE_PROVIDER_PATTERN, COMMERCE_PROVIDER_PATTERN_SOURCE, COUNTRY_PATTERN, COUNTRY_PATTERN_SOURCE, CURRENCY_PATTERN, CURRENCY_PATTERN_SOURCE, type Commerce, type CommerceDay, type CommerceHours, type ConsumerCommerce, type ConsumerContentFile, type ContentFile, HEX_COLOR_PATTERN, HEX_COLOR_PATTERN_SOURCE, type KnownCommerceProvider, LOCALE_PATTERN, LOCALE_PATTERN_SOURCE, MAX_CONTENT_BYTES, type Menu, type MenuCategory, type MenuItem, SCHEMA_VERSION, type SiteSettings, TIME_24H_PATTERN, TIME_24H_PATTERN_SOURCE, brandingSchema, brandingSchemaStrict, canonicalize, commerceSchema, commerceSchemaStrict, contentFileSchema, contentFileSchemaStrict, isIanaTimeZone, isKnownCommerceProvider, menuSchema, menuSchemaStrict, siteSettingsSchema, siteSettingsSchemaStrict };
569
+ export { type Branding, COMMERCE_DAYS, COMMERCE_PROVIDERS, COMMERCE_PROVIDER_PATTERN, COMMERCE_PROVIDER_PATTERN_SOURCE, COUNTRY_PATTERN, COUNTRY_PATTERN_SOURCE, CURRENCY_PATTERN, CURRENCY_PATTERN_SOURCE, type Commerce, type CommerceDay, type CommerceHours, type ConsumerCommerce, type ConsumerContentFile, type ContentFile, HEX_COLOR_PATTERN, HEX_COLOR_PATTERN_SOURCE, type KnownCommerceProvider, LOCALE_PATTERN, LOCALE_PATTERN_SOURCE, MAX_CONTENT_BYTES, MAX_OPTIONS_PER_GROUP, MAX_OPTION_GROUPS_PER_ITEM, MAX_OPTION_PRICE_DELTA, MAX_OPTION_SELECT, type Menu, type MenuCategory, type MenuItem, type MenuOption, type MenuOptionGroup, SCHEMA_VERSION, type SiteSettings, TIME_24H_PATTERN, TIME_24H_PATTERN_SOURCE, brandingSchema, brandingSchemaStrict, canonicalize, commerceSchema, commerceSchemaStrict, contentFileSchema, contentFileSchemaStrict, isIanaTimeZone, isKnownCommerceProvider, menuSchema, menuSchemaStrict, siteSettingsSchema, siteSettingsSchemaStrict };
package/dist/index.js CHANGED
@@ -12,6 +12,10 @@ import {
12
12
  LOCALE_PATTERN,
13
13
  LOCALE_PATTERN_SOURCE,
14
14
  MAX_CONTENT_BYTES,
15
+ MAX_OPTIONS_PER_GROUP,
16
+ MAX_OPTION_GROUPS_PER_ITEM,
17
+ MAX_OPTION_PRICE_DELTA,
18
+ MAX_OPTION_SELECT,
15
19
  SCHEMA_VERSION,
16
20
  TIME_24H_PATTERN,
17
21
  TIME_24H_PATTERN_SOURCE,
@@ -28,7 +32,7 @@ import {
28
32
  menuSchemaStrict,
29
33
  siteSettingsSchema,
30
34
  siteSettingsSchemaStrict
31
- } from "./chunk-PMUVTXAV.js";
35
+ } from "./chunk-7B6AENRI.js";
32
36
  export {
33
37
  COMMERCE_DAYS,
34
38
  COMMERCE_PROVIDERS,
@@ -43,6 +47,10 @@ export {
43
47
  LOCALE_PATTERN,
44
48
  LOCALE_PATTERN_SOURCE,
45
49
  MAX_CONTENT_BYTES,
50
+ MAX_OPTIONS_PER_GROUP,
51
+ MAX_OPTION_GROUPS_PER_ITEM,
52
+ MAX_OPTION_PRICE_DELTA,
53
+ MAX_OPTION_SELECT,
46
54
  SCHEMA_VERSION,
47
55
  TIME_24H_PATTERN,
48
56
  TIME_24H_PATTERN_SOURCE,
@@ -67,7 +67,8 @@
67
67
  "imageUrl": "https://example.supabase.co/storage/v1/object/public/assets/org_01example7fixture0000000000/b7e6a1d2-3c4f-4a5b-9e8d-1f2a3b4c5d6e/fd61a03af4f77d870fc21e05e7e80678095c92d808cfb3b5c279ee04c74aca13.webp",
68
68
  "tags": ["signature", "gluten-free"],
69
69
  "sortOrder": 0,
70
- "isAvailable": true
70
+ "isAvailable": true,
71
+ "optionGroupIds": ["c0693af5-7491-4b0b-bf44-b26acc3e2bee", "9c80e14f-f97a-41fc-87dc-ddf624aea9da"]
71
72
  },
72
73
  {
73
74
  "id": "6f5e4d3c-2b1a-4098-a7b6-c5d4e3f2a1b0",
@@ -79,7 +80,8 @@
79
80
  },
80
81
  "tags": ["gluten-free"],
81
82
  "sortOrder": 1,
82
- "isAvailable": true
83
+ "isAvailable": true,
84
+ "optionGroupIds": ["c0693af5-7491-4b0b-bf44-b26acc3e2bee", "9c80e14f-f97a-41fc-87dc-ddf624aea9da"]
83
85
  },
84
86
  {
85
87
  "id": "7a6b5c4d-3e2f-4109-b8a7-d6c5b4a3f2e1",
@@ -110,7 +112,8 @@
110
112
  },
111
113
  "tags": ["cold"],
112
114
  "sortOrder": 0,
113
- "isAvailable": true
115
+ "isAvailable": true,
116
+ "optionGroupIds": ["1f0a2da4-8111-47a9-be63-3ba3d8b820c1"]
114
117
  },
115
118
  {
116
119
  "id": "9c8b7a6d-5e4f-4321-90c9-f8e7d6c5b4a3",
@@ -125,6 +128,84 @@
125
128
  }
126
129
  ]
127
130
  }
131
+ ],
132
+ "optionGroups": [
133
+ {
134
+ "id": "c0693af5-7491-4b0b-bf44-b26acc3e2bee",
135
+ "name": "Size",
136
+ "min": 1,
137
+ "max": 1,
138
+ "sortOrder": 0,
139
+ "options": [
140
+ {
141
+ "id": "f02dd4ed-59bf-4285-928d-2c2f03d78c63",
142
+ "name": "Regular",
143
+ "priceDelta": 0,
144
+ "isAvailable": true,
145
+ "sortOrder": 0
146
+ },
147
+ {
148
+ "id": "a70e5eea-5e19-4ed5-bc86-f0ec60715ea2",
149
+ "name": "Large",
150
+ "priceDelta": 200,
151
+ "isAvailable": true,
152
+ "sortOrder": 1
153
+ }
154
+ ]
155
+ },
156
+ {
157
+ "id": "9c80e14f-f97a-41fc-87dc-ddf624aea9da",
158
+ "name": "Extras",
159
+ "min": 0,
160
+ "max": 2,
161
+ "sortOrder": 1,
162
+ "options": [
163
+ {
164
+ "id": "f1c60f21-f159-4f3c-90fa-94fb5c5cb3b6",
165
+ "name": "Extra brisket",
166
+ "priceDelta": 300,
167
+ "isAvailable": true,
168
+ "sortOrder": 0
169
+ },
170
+ {
171
+ "id": "bd120953-6ed4-482c-9764-2a5bbf96eec2",
172
+ "name": "Extra noodles",
173
+ "priceDelta": 150,
174
+ "isAvailable": true,
175
+ "sortOrder": 1
176
+ },
177
+ {
178
+ "id": "8d4fcac1-14f8-494b-83a4-ba64717fc8bb",
179
+ "name": "Poached egg",
180
+ "priceDelta": 100,
181
+ "isAvailable": false,
182
+ "sortOrder": 2
183
+ }
184
+ ]
185
+ },
186
+ {
187
+ "id": "1f0a2da4-8111-47a9-be63-3ba3d8b820c1",
188
+ "name": "Milk",
189
+ "min": 0,
190
+ "max": 1,
191
+ "sortOrder": 2,
192
+ "options": [
193
+ {
194
+ "id": "0d2377b3-03b1-467c-a007-b151fb156646",
195
+ "name": "Condensed milk",
196
+ "priceDelta": 0,
197
+ "isAvailable": true,
198
+ "sortOrder": 0
199
+ },
200
+ {
201
+ "id": "4c72ddc8-3416-46a7-b771-77e5f09f15ec",
202
+ "name": "Oat milk",
203
+ "priceDelta": 75,
204
+ "isAvailable": true,
205
+ "sortOrder": 1
206
+ }
207
+ ]
208
+ }
128
209
  ]
129
210
  },
130
211
  "commerce": {
@@ -136,12 +217,36 @@
136
217
  "acceptsOnlineOrders": true,
137
218
  "prepTimeMinutes": 25,
138
219
  "hours": [
139
- { "day": "mon", "opens": "11:00", "closes": "21:00" },
140
- { "day": "tue", "opens": "11:00", "closes": "21:00" },
141
- { "day": "wed", "opens": "11:00", "closes": "21:00" },
142
- { "day": "thu", "opens": "11:00", "closes": "21:00" },
143
- { "day": "fri", "opens": "11:00", "closes": "01:30" },
144
- { "day": "sat", "opens": "10:00", "closes": "01:30" }
220
+ {
221
+ "day": "mon",
222
+ "opens": "11:00",
223
+ "closes": "21:00"
224
+ },
225
+ {
226
+ "day": "tue",
227
+ "opens": "11:00",
228
+ "closes": "21:00"
229
+ },
230
+ {
231
+ "day": "wed",
232
+ "opens": "11:00",
233
+ "closes": "21:00"
234
+ },
235
+ {
236
+ "day": "thu",
237
+ "opens": "11:00",
238
+ "closes": "21:00"
239
+ },
240
+ {
241
+ "day": "fri",
242
+ "opens": "11:00",
243
+ "closes": "01:30"
244
+ },
245
+ {
246
+ "day": "sat",
247
+ "opens": "10:00",
248
+ "closes": "01:30"
249
+ }
145
250
  ]
146
251
  }
147
252
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Checked-in hashContent() snapshots of the v1 fixtures — the serialization-determinism contract. If a test fails against these, the canonical serialization or a fixture changed; BOTH are locked decisions (phase-2.md T1.6/T1.8). Regenerate only with founder sign-off. REGENERATED ONCE, 2026-08-25: full.json gained the optional `commerce` module (phase-7 Slice 2), signed off in the proposal's Slice 2 line — 'full.json fixture ... publish fixtures and hash updates'. minimal.json is UNCHANGED and must stay so: commerce is optional, so the minimal payload never carries it.",
3
- "full.json": "sha256:30bd220fda88f5dd511cd0c05fe888e9abad4f5e5946b398aade99ba8d3c8a08",
2
+ "_comment": "Checked-in hashContent() snapshots of the v1 fixtures — the serialization-determinism contract. If a test fails against these, the canonical serialization or a fixture changed; BOTH are locked decisions (phase-2.md T1.6/T1.8). Regenerate only with founder sign-off. REGENERATED TWICE: (1) 2026-08-25, full.json gained the optional `commerce` module (phase-7 Slice 2, signed off in the proposal's Slice 2 line); (2) 2026-10-06, full.json gained the optional `menu.optionGroups` / `optionGroupIds` (content-schemas 1.3.0, Online Commerce C3 T2, signed off with the approved spec's 'fixture and hash updates'; the previous full.json hash is kept in test/contract.test.ts as FULL_1_2_0_HASH — the rollback shape). minimal.json is UNCHANGED and must stay so: commerce and option groups are optional, so the minimal payload never carries them.",
3
+ "full.json": "sha256:b3262a49e000fec59b067f7aec07bc0b31855086f121a16a02af394da616ff4d",
4
4
  "minimal.json": "sha256:7b4ebc88bf2c17e1182a7e89fe994c3a38f70e0e638b37a0caa662f0c7e8db22"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamgig/content-schemas",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "The LamGig published-JSON contract: Zod schemas, canonical serialization, and fixtures for consuming content published by the LamGig platform.",
5
5
  "keywords": [
6
6
  "lamgig",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Version of the published-JSON contract. Additive changes (new OPTIONAL fields\n * and new OPTIONAL module keys) keep this number; any required addition is\n * breaking and bumps it. The full compatibility rule lives in this package's\n * README. See PLAN.md § Published JSON delivery and phase-2.md T1.5.\n */\nexport const SCHEMA_VERSION = 1;\n\n/** Publisher-enforced ceiling on the canonical payload size (1 MiB). */\nexport const MAX_CONTENT_BYTES = 1_048_576;\n\n// ---------------------------------------------------------------------------\n// Shared value patterns — single source of truth.\n//\n// LOCALE_PATTERN_SOURCE is the EXACT string used by the `site_settings.locale`\n// DB CHECK (supabase/migrations/*_content_site_settings.sql). Contract and\n// database must never disagree (phase-2.md T1.1): v1 supports the named BCP-47\n// subset `ll` or `ll-CC` only (e.g. `en`, `en-US`); script/numeric-region forms\n// (`zh-Hant`, `es-419`) are out of scope — widening later is additive.\n// HEX_COLOR / CURRENCY are likewise mirrored by DB CHECKs on `branding.colors`\n// and `menu_items.price_currency`.\n// ---------------------------------------------------------------------------\n\nexport const LOCALE_PATTERN_SOURCE = '^[a-z]{2}(-[A-Z]{2})?$';\nexport const LOCALE_PATTERN = new RegExp(LOCALE_PATTERN_SOURCE);\n\nexport const HEX_COLOR_PATTERN_SOURCE = '^#[0-9A-Fa-f]{6}$';\nexport const HEX_COLOR_PATTERN = new RegExp(HEX_COLOR_PATTERN_SOURCE);\n\n/** ISO 4217 alpha code shape (mirrored by the `price_currency` DB CHECK). */\nexport const CURRENCY_PATTERN_SOURCE = '^[A-Z]{3}$';\nexport const CURRENCY_PATTERN = new RegExp(CURRENCY_PATTERN_SOURCE);\n\n/** ISO 3166-1 alpha-2 country code shape. */\nexport const COUNTRY_PATTERN_SOURCE = '^[A-Z]{2}$';\nexport const COUNTRY_PATTERN = new RegExp(COUNTRY_PATTERN_SOURCE);\n\n/**\n * Days of the week, in the canonical order `commerce.hours` must be sorted by.\n * The order is part of the contract: a consumer rendering opening hours reads\n * the array straight through, so a payload listing Sunday before Monday would\n * render wrong without being malformed.\n */\nexport const COMMERCE_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const;\n\n/**\n * 24-hour `HH:MM`. UNLIKE the four patterns above this one is deliberately NOT\n * mirrored by a DB CHECK: `commerce_settings.hours` is validated at the DB only\n * as \"an array of at most 7\" (see the migration header), and its element shape,\n * time syntax, day uniqueness and ordering are publish-gated here instead. A\n * schema-invalid draft may therefore exist in the table; it cannot be published.\n */\nexport const TIME_24H_PATTERN_SOURCE = '^([01][0-9]|2[0-3]):[0-5][0-9]$';\nexport const TIME_24H_PATTERN = new RegExp(TIME_24H_PATTERN_SOURCE);\n\n/** IANA time zone check via the runtime's own tz database. */\nexport function isIanaTimeZone(tz: string): boolean {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Two parse modes from ONE schema definition (phase-2.md T1):\n//\n// - producer-strict — `.strict()` everywhere; the dashboard validates its own\n// output against this before publishing. Unknown keys are defects.\n// - consumer-tolerant — unknown object keys are STRIPPED and unknown module\n// keys ignored, so a shipped consumer accepts a v1 payload that later gained\n// an optional field or a new module key (the additive changes the\n// compatibility rule promises).\n//\n// zod v4's `.strip()`/`.passthrough()` do NOT recurse into nested objects, so a\n// post-hoc transform of one tree silently leaves nested objects in the original\n// mode. Instead every object in the tree is built through an injected object\n// factory: `z.strictObject` for the strict pair, `z.object` (strips unknown\n// keys) for the tolerant pair. One definition, two instantiations.\n// ---------------------------------------------------------------------------\n\ntype ObjectFactory = <T extends z.ZodRawShape>(shape: T) => z.ZodObject<T>;\n\nconst strictFactory = z.strictObject as ObjectFactory;\nconst tolerantFactory = z.object as ObjectFactory;\n\n// --- siteSettings ----------------------------------------------------------\n\nconst makeSiteSettingsSchema = (o: ObjectFactory) =>\n o({\n siteName: z.string().min(1).max(120),\n tagline: z.string().max(200).optional(),\n contact: o({\n email: z.email().optional(),\n phone: z.string().min(1).optional(),\n address: o({\n line1: z.string().min(1),\n line2: z.string().min(1).optional(),\n city: z.string().min(1),\n region: z.string().min(1),\n postalCode: z.string().min(1),\n country: z.string().regex(COUNTRY_PATTERN),\n }).optional(),\n }),\n // Explicit known keys (NOT a record): strict rejects unknown platforms,\n // tolerant strips them — adding a platform later is additive.\n socialLinks: o({\n facebook: z.url().optional(),\n instagram: z.url().optional(),\n tiktok: z.url().optional(),\n x: z.url().optional(),\n youtube: z.url().optional(),\n yelp: z.url().optional(),\n google: z.url().optional(),\n }).optional(),\n locale: z.string().regex(LOCALE_PATTERN).default('en-US'),\n timezone: z\n .string()\n .min(1)\n .refine(isIanaTimeZone, 'must be an IANA time zone name')\n .default('America/Los_Angeles'),\n });\n\nexport const siteSettingsSchemaStrict = makeSiteSettingsSchema(strictFactory);\nexport const siteSettingsSchema = makeSiteSettingsSchema(tolerantFactory);\n\n// --- branding --------------------------------------------------------------\n\nconst makeBrandingSchema = (o: ObjectFactory) =>\n o({\n // Absolute URLs produced by the dashboard's storagePublicUrl builder\n // (apps/dashboard/src/lib/content-urls.ts). The DB stores storage PATHS;\n // the publish-time compose maps path -> URL. This package never imports\n // from the app (dependency direction, phase-2.md T1.7).\n logoUrl: z.url().optional(),\n faviconUrl: z.url().optional(),\n colors: o({\n primary: z.string().regex(HEX_COLOR_PATTERN),\n secondary: z.string().regex(HEX_COLOR_PATTERN).optional(),\n accent: z.string().regex(HEX_COLOR_PATTERN).optional(),\n background: z.string().regex(HEX_COLOR_PATTERN).optional(),\n text: z.string().regex(HEX_COLOR_PATTERN).optional(),\n }),\n fonts: o({\n heading: z.string().min(1).optional(),\n body: z.string().min(1).optional(),\n }).optional(),\n });\n\nexport const brandingSchemaStrict = makeBrandingSchema(strictFactory);\nexport const brandingSchema = makeBrandingSchema(tolerantFactory);\n\n// --- menu ------------------------------------------------------------------\n\nconst makeMenuItemSchema = (o: ObjectFactory) =>\n o({\n /** Stable DB UUID — identical across publishes (phase-2.md T1.3). */\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n price: o({\n /** Integer minor units (e.g. cents). */\n amount: z.number().int().min(0),\n currency: z.string().regex(CURRENCY_PATTERN),\n }),\n imageUrl: z.url().optional(),\n tags: z.array(z.string().min(1)).max(20),\n sortOrder: z.number().int().min(0).max(100_000),\n isAvailable: z.boolean(),\n });\n\nconst makeMenuCategorySchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n sortOrder: z.number().int().min(0).max(100_000),\n items: z.array(makeMenuItemSchema(o)),\n });\n\nconst makeMenuSchema = (o: ObjectFactory) =>\n o({\n /** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */\n categories: z.array(makeMenuCategorySchema(o)),\n });\n\nexport const menuSchemaStrict = makeMenuSchema(strictFactory);\nexport const menuSchema = makeMenuSchema(tolerantFactory);\n\n// --- commerce --------------------------------------------------------------\n//\n// Online-ordering CONFIGURATION, never orders and never provider secrets\n// (docs/seams/commerce.md § \"What does NOT go in the module\"): a published\n// payload is world-readable immutable JSON, so a credential written here would\n// be permanent and public. Everything below is a public identifier or a display\n// value.\n//\n// `provider` is the one OPEN enum in the contract (docs/proposals/online-commerce\n// OC-2). The PRODUCER enum is closed and grows by one member per phase, only\n// when the platform can actually serve the new value — today `none | other`.\n// The CONSUMER schema accepts any well-formed provider name and hands it through\n// unchanged, so a consumer that predates a new provider keeps parsing.\n//\n// That second half did not exist before 1.2.0. Until then the tolerant schema\n// declared the same closed enum as the strict one, so an unknown provider made\n// the WHOLE payload fail to parse — the comment here claimed widening was\n// \"additive for a tolerant consumer\", and the code said otherwise. Tolerance for\n// unknown KEYS (the object factories) never covered unknown enum VALUES.\n//\n// Consumer rule (README § Open enums): an unknown provider means \"newer than\n// me\" — treat it like `other`: offer the `orderingUrl` hand-off when\n// `acceptsOnlineOrders` is true, and never show a cart.\n//\n// TIMES ARE LOCAL to `siteSettings.timezone` and carry no offset — the site's\n// own clock is what a customer reads on the door. `closes < opens` is LEGAL and\n// means \"closes after midnight\" (docs/seams/commerce.md § 1); a consumer must\n// not treat it as an error.\n\nconst makeCommerceHoursSchema = (o: ObjectFactory) =>\n z\n .array(\n o({\n day: z.enum(COMMERCE_DAYS),\n opens: z.string().regex(TIME_24H_PATTERN),\n closes: z.string().regex(TIME_24H_PATTERN),\n }),\n )\n .max(7)\n // At most one entry per day, listed in COMMERCE_DAYS order. The DB CHECK\n // stops at \"an array of at most 7\" (deliberately — see the migration\n // header), so THIS is the layer that makes `hours` meaningful, and it runs\n // at publish time rather than at write time.\n .superRefine((entries, ctx) => {\n const seen = new Set<string>();\n let previous = -1;\n entries.forEach((entry, index) => {\n if (seen.has(entry.day)) {\n ctx.addIssue({\n code: 'custom',\n path: [index, 'day'],\n message: `duplicate day \"${entry.day}\"`,\n });\n }\n seen.add(entry.day);\n const position = COMMERCE_DAYS.indexOf(entry.day);\n if (position < previous) {\n ctx.addIssue({\n code: 'custom',\n path: [index, 'day'],\n message: 'hours must be ordered mon → sun',\n });\n }\n previous = position;\n });\n });\n\n/** Providers the PRODUCER may publish. Grows one member per phase (OC-2). */\nexport const COMMERCE_PROVIDERS = ['none', 'other'] as const;\nexport type KnownCommerceProvider = (typeof COMMERCE_PROVIDERS)[number];\n\n/**\n * Shape of a provider name a TOLERANT consumer accepts: lowercase, starts with a\n * letter, at most 32 characters. Tolerance is for FUTURE values, not malformed\n * ones — `\"Stripe!\"` or `\"\"` still fail.\n */\nexport const COMMERCE_PROVIDER_PATTERN_SOURCE = '^[a-z][a-z0-9_]{0,31}$';\nexport const COMMERCE_PROVIDER_PATTERN = new RegExp(COMMERCE_PROVIDER_PATTERN_SOURCE);\n\nconst commerceProviderStrict = z.enum(COMMERCE_PROVIDERS);\nconst commerceProviderTolerant = z.union([\n z.enum(COMMERCE_PROVIDERS),\n z.string().regex(COMMERCE_PROVIDER_PATTERN),\n]);\n\n/** Narrow a tolerantly-parsed provider to one this version of the package knows. */\nexport function isKnownCommerceProvider(provider: string): provider is KnownCommerceProvider {\n return (COMMERCE_PROVIDERS as readonly string[]).includes(provider);\n}\n\ntype CommerceProviderSchema = typeof commerceProviderStrict | typeof commerceProviderTolerant;\n\nconst makeCommerceSchema = <P extends CommerceProviderSchema>(o: ObjectFactory, provider: P) =>\n o({\n provider,\n /** Where a customer is sent to order. Absent when `provider` is `none`. */\n orderingUrl: z.url({ protocol: /^https$/ }).max(2048).optional(),\n /** PUBLIC provider identifiers — never a key, token or secret. */\n locationRef: z.string().min(1).max(128).optional(),\n menuRef: z.string().min(1).max(128).optional(),\n /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */\n currency: z.string().regex(CURRENCY_PATTERN),\n acceptsOnlineOrders: z.boolean(),\n /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */\n prepTimeMinutes: z.number().int().min(0).max(240).optional(),\n hours: makeCommerceHoursSchema(o),\n });\n\nexport const commerceSchemaStrict = makeCommerceSchema(strictFactory, commerceProviderStrict);\nexport const commerceSchema = makeCommerceSchema(tolerantFactory, commerceProviderTolerant);\n\n// --- content file ----------------------------------------------------------\n\nconst makeContentFileSchema = <P extends CommerceProviderSchema>(\n o: ObjectFactory,\n commerceProvider: P,\n) =>\n o({\n schemaVersion: z.literal(1),\n site: o({ id: z.uuid(), slug: z.string().min(1) }),\n publishedAt: z.iso.datetime(),\n // The three v1 modules are REQUIRED; empty states are valid module payloads\n // (e.g. menu.categories: []), never missing keys. A new required module is\n // breaking (retained/rollback snapshots won't contain it) — bump\n // SCHEMA_VERSION instead.\n //\n // `commerce` is therefore OPTIONAL (phase-7 D-3), and that is not a\n // stylistic choice: every snapshot published before the module existed\n // lacks the key, and a ROLLBACK can activate one of those at any time. A\n // required `commerce` would retroactively invalidate the whole publish\n // history. See ARCHITECTURE.md § 5 and docs/seams/commerce.md § 1.\n modules: o({\n siteSettings: makeSiteSettingsSchema(o),\n branding: makeBrandingSchema(o),\n menu: makeMenuSchema(o),\n commerce: makeCommerceSchema(o, commerceProvider).optional(),\n }),\n });\n\n/** Producer gate: the dashboard validates its own output against this. */\nexport const contentFileSchemaStrict = makeContentFileSchema(strictFactory, commerceProviderStrict);\n/** Public consumer schema: tolerant of additive v1 changes. */\nexport const contentFileSchema = makeContentFileSchema(tolerantFactory, commerceProviderTolerant);\n\nexport type SiteSettings = z.infer<typeof siteSettingsSchemaStrict>;\nexport type Branding = z.infer<typeof brandingSchemaStrict>;\nexport type Menu = z.infer<typeof menuSchemaStrict>;\nexport type MenuCategory = Menu['categories'][number];\nexport type MenuItem = MenuCategory['items'][number];\nexport type Commerce = z.infer<typeof commerceSchemaStrict>;\nexport type CommerceDay = (typeof COMMERCE_DAYS)[number];\nexport type CommerceHours = Commerce['hours'];\nexport type ContentFile = z.infer<typeof contentFileSchemaStrict>;\n\n/**\n * What a CONSUMER gets back from `contentFileSchema.parse` (1.2.0+). Identical to\n * `ContentFile` except that open enums are widened — `commerce.provider` is any\n * well-formed string, because a payload may name a provider newer than the\n * consumer. Type parsed content with THIS, not with the producer's `ContentFile`:\n * annotating tolerant output as `ContentFile` claims the provider is one this\n * package version knows, which is exactly what tolerance cannot promise.\n */\nexport type ConsumerContentFile = z.infer<typeof contentFileSchema>;\nexport type ConsumerCommerce = z.infer<typeof commerceSchema>;\n\n// ---------------------------------------------------------------------------\n// Canonical serialization (phase-2.md T1.6).\n//\n// Recursively key-sorted JSON with no whitespace, produced by a hand-rolled\n// serializer rather than JSON.stringify over a re-built object — rebuilding\n// depends on JS insertion-order semantics and silently reorders integer-like\n// keys. Determinism here is what makes `hashContent` a byte-equality contract\n// shared by the publisher and every consumer test.\n//\n// Rules (documented in the README):\n// - arrays keep their order; object keys are sorted with Array.prototype.sort\n// (code-unit order);\n// - object properties whose value is `undefined` are omitted (JSON semantics);\n// - `undefined` anywhere else, non-finite numbers, bigint, functions and\n// symbols THROW — never silently coerced the way JSON.stringify would.\n// ---------------------------------------------------------------------------\n\nexport function canonicalize(payload: unknown): string {\n const out = serialize(payload);\n if (out === undefined) {\n throw new TypeError('canonicalize: payload itself serializes to nothing (undefined)');\n }\n return out;\n}\n\nfunction serialize(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (value === null) return 'null';\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return JSON.stringify(value);\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(`canonicalize: non-finite number ${String(value)}`);\n }\n return JSON.stringify(value);\n case 'object':\n break;\n default:\n throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);\n }\n if (Array.isArray(value)) {\n const parts = value.map((entry) => {\n const s = serialize(entry);\n if (s === undefined) {\n throw new TypeError('canonicalize: undefined array element');\n }\n return s;\n });\n return `[${parts.join(',')}]`;\n }\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort();\n const parts: string[] = [];\n for (const key of keys) {\n const s = serialize(record[key]);\n if (s === undefined) continue; // undefined property -> omitted key\n parts.push(`${JSON.stringify(key)}:${s}`);\n }\n return `{${parts.join(',')}}`;\n}\n"],"mappings":"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","names":["parts"]}