@vectoriox/iox-ui 5.54.0 → 5.56.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,6 +1,7 @@
1
- import { NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, elementStyleById, ownedTraitsOf, applyElementStyle, flavorPackById, parseHexColor, SPEC_AXES, LinkType, COLOR_SCHEMES, setColorScheme, resolveDesignChoices } from '@vectoriox/iox-ui/design-core';
2
- import { formPresetById } from '@vectoriox/iox-ui/forms-core';
1
+ import { NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, elementStyleById, ownedTraitsOf, applyElementStyle, flavorPackById, parseHexColor, SPEC_AXES, LinkType, COLOR_SCHEMES, setColorScheme, resolveDesignChoices, auditDesignContrast } from '@vectoriox/iox-ui/design-core';
2
+ import { formPresetById, ControlType, FORM_NODE_TYPE, collectFormSpec, FIELD_NODE_TYPE } from '@vectoriox/iox-ui/forms-core';
3
3
  import { segmentedOptions, ITEM_OPEN_STATE, CardRole, CardLayout, CARD_LAYOUTS, CARD_PARTS, BADGE_TONES, buildCardRecipe, findByRole, buildSegmentedRecipe, AccordionRole, buildAccordionRecipe, ACCORDION_WORDS, accordionItems, accordionQuestionOf, accordionAnswerOf, ACCORDION_BIND_PROP, pickAccordionAutoBinds, ACCORDION_CONTENT_LIMIT, buildTabsRecipe, TABS_WORDS, tabsTriggers, tabLabelNodeOf, TABS_LABEL_BIND_PROP, tabsPanels, tabPanelTextOf, TABS_TEXT_BIND_PROP, tabPanelImageOf, pickTabsAutoBinds, tabsListOf, tabsPanelsOf, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, buildCarouselRecipe, SLIDE_TYPE, buildLightboxRecipe, LIGHTBOX_LABEL, SLIDE_TO_ROW } from '@vectoriox/iox-ui/composite-core';
4
+ import { ELEMENT_ACTION_DESCRIPTORS, resolveActionAvailability } from '@vectoriox/iox-ui/actions-core';
4
5
 
5
6
  // GENERATED from iox-ai-guidance flavor-spec.v1.json — do not edit; re-run scripts/gen-flavor-spec.mjs
6
7
  // (npm run gen:flavor-spec). The JSON is itself generated by iox-ai-guidance
@@ -7567,7 +7568,7 @@ var SiteTemplateId;
7567
7568
  const STUDIO_TEMPLATE = SITE_TEMPLATES[SiteTemplateId.Studio];
7568
7569
  class SiteTemplateError extends Error {
7569
7570
  }
7570
- const clone$3 = (value) => JSON.parse(JSON.stringify(value));
7571
+ const clone$4 = (value) => JSON.parse(JSON.stringify(value));
7571
7572
  /**
7572
7573
  * The template as a Site Plan (`source.kind: template`), with the customer's business and look. A
7573
7574
  * required page cannot be left out, and an unknown key is refused — nothing is guessed.
@@ -7584,12 +7585,12 @@ function planFromTemplate(template, input) {
7584
7585
  return {
7585
7586
  version: SITE_PLAN_VERSION,
7586
7587
  source: { kind: SitePlanSourceKind.Template, templateId: template.id, templateVersion: template.version },
7587
- business: clone$3(input.business),
7588
- design: clone$3(input.design),
7589
- contentTypes: clone$3(template.contentTypes),
7590
- dataSources: clone$3(template.dataSources),
7591
- pages: clone$3(template.pages.filter(p => !omit.has(p.key))),
7592
- globals: clone$3(template.globals),
7588
+ business: clone$4(input.business),
7589
+ design: clone$4(input.design),
7590
+ contentTypes: clone$4(template.contentTypes),
7591
+ dataSources: clone$4(template.dataSources),
7592
+ pages: clone$4(template.pages.filter(p => !omit.has(p.key))),
7593
+ globals: clone$4(template.globals),
7593
7594
  };
7594
7595
  }
7595
7596
 
@@ -7598,7 +7599,7 @@ function planFromTemplate(template, input) {
7598
7599
  * wrong — never something to work around. */
7599
7600
  class ComposeError extends Error {
7600
7601
  }
7601
- const clone$2 = (value) => JSON.parse(JSON.stringify(value));
7602
+ const clone$3 = (value) => JSON.parse(JSON.stringify(value));
7602
7603
  /**
7603
7604
  * A NEW node of `type`, as the builder creates one (ComponentRegistryService.createConfig, serialized):
7604
7605
  * the type's constructor defaults, its text colour released to `inherit` (NEW_NODE_STYLE_DEFAULTS), then
@@ -7613,8 +7614,8 @@ function createSerializedNode(id, type, props = {}, elementStyle, options = {})
7613
7614
  const node = {
7614
7615
  id,
7615
7616
  type,
7616
- props: { ...clone$2(baseline.props), ...props },
7617
- styleProps: clone$2(baseline.styleProps),
7617
+ props: { ...clone$3(baseline.props), ...props },
7618
+ styleProps: clone$3(baseline.styleProps),
7618
7619
  bindings: [],
7619
7620
  interactions: [],
7620
7621
  };
@@ -7665,16 +7666,16 @@ function walkSerialized(nodes, visit, ancestors = []) {
7665
7666
  * own field names) whose key lists the gate READS — so the shape a model is told about is the shape refused —
7666
7667
  * and a collector that records each refusal at its dotted path. Generic over each gate's issue-code enum.
7667
7668
  */
7668
- const OPTIONAL_MARK = '?';
7669
+ const OPTIONAL_MARK$1 = '?';
7669
7670
  const DYNAMIC_KEY = /^\[[^\]]+\]\??$/;
7670
7671
  const isDynamic = (key) => DYNAMIC_KEY.test(key);
7671
7672
  /** Every fixed key an object of the shape may carry. */
7672
7673
  function shapeKeys(entry) {
7673
- return Object.keys(entry).filter(k => !isDynamic(k)).map(k => (k.endsWith(OPTIONAL_MARK) ? k.slice(0, -1) : k));
7674
+ return Object.keys(entry).filter(k => !isDynamic(k)).map(k => (k.endsWith(OPTIONAL_MARK$1) ? k.slice(0, -1) : k));
7674
7675
  }
7675
7676
  /** The keys an object of the shape must carry. */
7676
7677
  function requiredShapeKeys(entry) {
7677
- return Object.keys(entry).filter(k => !isDynamic(k) && !k.endsWith(OPTIONAL_MARK));
7678
+ return Object.keys(entry).filter(k => !isDynamic(k) && !k.endsWith(OPTIONAL_MARK$1));
7678
7679
  }
7679
7680
  const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
7680
7681
  const isNonEmptyString = (v) => typeof v === 'string' && v.trim() !== '';
@@ -7729,6 +7730,83 @@ class PathIssueCollector {
7729
7730
  }
7730
7731
  }
7731
7732
 
7733
+ const OPTIONAL_MARK = '?';
7734
+ class JsonSchemaShapeError extends Error {
7735
+ }
7736
+ const jsonText = () => ({ type: 'string' });
7737
+ const jsonBoolean = () => ({ type: 'boolean' });
7738
+ const jsonInteger = () => ({ type: 'integer' });
7739
+ const jsonOneOf = (values) => ({ type: 'string', enum: [...values] });
7740
+ const jsonListOf = (items) => ({ type: 'array', items });
7741
+ const jsonAnyOf = (options) => ({ anyOf: [...options] });
7742
+ /** A closed object with exactly these properties. */
7743
+ function jsonClosedObject(properties, required, description) {
7744
+ return {
7745
+ type: 'object',
7746
+ properties,
7747
+ required: [...required],
7748
+ additionalProperties: false,
7749
+ ...(description ? { description } : {}),
7750
+ };
7751
+ }
7752
+ /** The table's own words for a key (`key` or `key?`). */
7753
+ function describeShapeKey(entry, key) {
7754
+ return String(entry[key] ?? entry[`${key}${OPTIONAL_MARK}`] ?? '');
7755
+ }
7756
+ /**
7757
+ * An object of one shape-table entry: exactly its fixed keys, its required keys required, each described by the
7758
+ * table; an entry's `[name]` keys are not object keys — `dynamic` writes them as one list. Throws when the properties
7759
+ * given are not the table's keys.
7760
+ */
7761
+ function jsonShapedObject(entry, properties, name = 'shape', dynamic) {
7762
+ const expected = [...shapeKeys(entry)].sort();
7763
+ const given = Object.keys(properties).sort();
7764
+ if (expected.join() !== given.join()) {
7765
+ throw new JsonSchemaShapeError(`${name}: the format's keys (${given.join(', ')}) are not the shape table's (${expected.join(', ')})`);
7766
+ }
7767
+ const dynamicKey = Object.keys(entry).find(k => /^\[[^\]]+\]\??$/.test(k));
7768
+ if (!!dynamic !== !!dynamicKey)
7769
+ throw new JsonSchemaShapeError(`${name}: ${dynamicKey ? `its ${dynamicKey} keys need a list on the wire` : 'it has no [name] keys to list'}`);
7770
+ const described = Object.fromEntries(Object.entries(properties).map(([key, schema]) => [key, { ...schema, description: describeShapeKey(entry, key) }]));
7771
+ if (dynamic && dynamicKey)
7772
+ described[dynamic.key] = { ...dynamic.schema, description: `${dynamicKey}: ${entry[dynamicKey]}` };
7773
+ return jsonClosedObject(described, requiredShapeKeys(entry));
7774
+ }
7775
+ /** Every keyword a schema uses — what a spec checks against the strict subset. */
7776
+ function jsonSchemaKeywords(node, out = new Set()) {
7777
+ if (Array.isArray(node))
7778
+ node.forEach(n => jsonSchemaKeywords(n, out));
7779
+ else if (node && typeof node === 'object') {
7780
+ for (const [k, v] of Object.entries(node)) {
7781
+ out.add(k);
7782
+ if (k === 'properties')
7783
+ Object.values(v).forEach(p => jsonSchemaKeywords(p, out));
7784
+ else if (k !== 'enum' && k !== 'required' && k !== 'description')
7785
+ jsonSchemaKeywords(v, out);
7786
+ }
7787
+ }
7788
+ return out;
7789
+ }
7790
+ /** The keywords the strict subset never uses. */
7791
+ const JSON_SCHEMA_BANNED_KEYWORDS = [
7792
+ 'minLength', 'maxLength', 'minimum', 'maximum', 'minItems', 'maxItems', 'pattern', 'oneOf', '$ref', 'const', 'format',
7793
+ ];
7794
+ /** Every object node of a schema (through properties, items and anyOf). */
7795
+ function jsonSchemaObjects(node, out = []) {
7796
+ if (!node || typeof node !== 'object')
7797
+ return out;
7798
+ const n = node;
7799
+ if (n['type'] === 'object')
7800
+ out.push(n);
7801
+ for (const p of Object.values(n['properties'] ?? {}))
7802
+ jsonSchemaObjects(p, out);
7803
+ if (n['items'])
7804
+ jsonSchemaObjects(n['items'], out);
7805
+ for (const option of n['anyOf'] ?? [])
7806
+ jsonSchemaObjects(option, out);
7807
+ return out;
7808
+ }
7809
+
7732
7810
  /**
7733
7811
  * The Site Plan validator — the gate in front of the composer. The wizard's plan and (later) the AI's
7734
7812
  * plan go through the SAME checks: an unknown pattern, a slot the pattern does not declare, a source
@@ -7776,20 +7854,20 @@ const RESERVED_PREFIX = '$';
7776
7854
  const ID_FIELD = '_id';
7777
7855
  /** The documented plan shape (contract `site.planShape`): its key lists are READ (`shape-check`), so a key
7778
7856
  * the plan documents is a key the gate validates — and an undocumented one is refused. */
7779
- const SHAPE = SPEC_SITE_PLAN_SHAPE;
7780
- const PLAN_KEYS = shapeKeys(SHAPE['plan']);
7781
- const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE['sourceTemplate']);
7782
- const AI_SOURCE_KEYS = shapeKeys(SHAPE['sourceAi']);
7783
- const BUSINESS_KEYS = shapeKeys(SHAPE['business']);
7784
- const DESIGN_REQUIRED = requiredShapeKeys(SHAPE['design']);
7785
- const DESIGN_KEYS = shapeKeys(SHAPE['design']);
7786
- const CONTENT_TYPE_KEYS = shapeKeys(SHAPE['contentType']);
7787
- const FIELD_KEYS = shapeKeys(SHAPE['field']);
7788
- const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
7789
- const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
7790
- const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
7791
- const PAGE_KEYS = shapeKeys(SHAPE['page']);
7792
- const SECTION_KEYS = shapeKeys(SHAPE['section']);
7857
+ const SHAPE$1 = SPEC_SITE_PLAN_SHAPE;
7858
+ const PLAN_KEYS = shapeKeys(SHAPE$1['plan']);
7859
+ const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE$1['sourceTemplate']);
7860
+ const AI_SOURCE_KEYS = shapeKeys(SHAPE$1['sourceAi']);
7861
+ const BUSINESS_KEYS = shapeKeys(SHAPE$1['business']);
7862
+ const DESIGN_REQUIRED = requiredShapeKeys(SHAPE$1['design']);
7863
+ const DESIGN_KEYS = shapeKeys(SHAPE$1['design']);
7864
+ const CONTENT_TYPE_KEYS = shapeKeys(SHAPE$1['contentType']);
7865
+ const FIELD_KEYS = shapeKeys(SHAPE$1['field']);
7866
+ const SEED_FIXED_KEYS = shapeKeys(SHAPE$1['seed']);
7867
+ const CMS_SOURCE_KEYS = shapeKeys(SHAPE$1['cmsDataSource']);
7868
+ const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE$1['relativeDataSource']);
7869
+ const PAGE_KEYS = shapeKeys(SHAPE$1['page']);
7870
+ const SECTION_KEYS = shapeKeys(SHAPE$1['section']);
7793
7871
  const GLOBAL_POSITION = {
7794
7872
  [GlobalSlot.Header]: PatternGlobalPosition.Before,
7795
7873
  [GlobalSlot.Footer]: PatternGlobalPosition.After,
@@ -7801,11 +7879,11 @@ const has = (table, key) => Object.prototype.hasOwnProperty.call(table, key);
7801
7879
  function routeParams(route) {
7802
7880
  return [...route.matchAll(ROUTE_PARAM$1)].map(m => m[1]);
7803
7881
  }
7804
- class Collector extends PathIssueCollector {
7882
+ let Collector$1 = class Collector extends PathIssueCollector {
7805
7883
  constructor() {
7806
7884
  super('plan', { shape: SitePlanIssueCode.Shape, unknownKey: SitePlanIssueCode.UnknownKey, duplicate: SitePlanIssueCode.Duplicate });
7807
7885
  }
7808
- }
7886
+ };
7809
7887
  function validateSource(c, source) {
7810
7888
  if (!isPlainObject(source))
7811
7889
  return c.add(SitePlanIssueCode.Shape, 'source', 'must be an object');
@@ -8143,7 +8221,7 @@ function validateGlobals(c, globals) {
8143
8221
  }
8144
8222
  /** Check a Site Plan against the plan shape and the pattern library. Never throws, never repairs. */
8145
8223
  function validateSitePlan(plan) {
8146
- const c = new Collector();
8224
+ const c = new Collector$1();
8147
8225
  if (!isPlainObject(plan)) {
8148
8226
  c.add(SitePlanIssueCode.Shape, '', 'a site plan is an object');
8149
8227
  return { valid: false, issues: c.issues };
@@ -8506,7 +8584,7 @@ function bar(kit, root, pages, shape, count, drawerId, at) {
8506
8584
  return b;
8507
8585
  }
8508
8586
  // ── the edit ────────────────────────────────────────────────────────────────────────────────
8509
- const clone$1 = (v) => JSON.parse(JSON.stringify(v));
8587
+ const clone$2 = (v) => JSON.parse(JSON.stringify(v));
8510
8588
  function removeRole(node, role) {
8511
8589
  let removed = null;
8512
8590
  const visit = (n) => {
@@ -8558,7 +8636,7 @@ function clampBarCount(n, shape) {
8558
8636
  function planNavShape(globals, shape, options = {}, navId) {
8559
8637
  if (!Object.values(NavShape).includes(shape))
8560
8638
  throw new NavShapeError(`unknown phone menu shape "${shape}"`);
8561
- const out = clone$1(globals);
8639
+ const out = clone$2(globals);
8562
8640
  const root = findNavRoot(out, navId);
8563
8641
  if (!root)
8564
8642
  throw new NavShapeError('no Navigation bar among the globals');
@@ -8678,7 +8756,7 @@ function nameParts(root) {
8678
8756
  * bar does not link to yet (Home when every page is there). Refused at five (Q2). Input not mutated.
8679
8757
  */
8680
8758
  function addBarItem(globals, options = {}) {
8681
- const out = clone$1(globals);
8759
+ const out = clone$2(globals);
8682
8760
  const root = findNavRoot(out);
8683
8761
  const b = find(out, NavRole.Bar);
8684
8762
  if (!root || !b)
@@ -8698,7 +8776,7 @@ function addBarItem(globals, options = {}) {
8698
8776
  return { globals: out, added: true };
8699
8777
  }
8700
8778
  function barOf(globals) {
8701
- const out = clone$1(globals);
8779
+ const out = clone$2(globals);
8702
8780
  const b = find(out, NavRole.Bar);
8703
8781
  return { out, kids: b ? (b.children ?? (b.children = [])) : null };
8704
8782
  }
@@ -8839,7 +8917,7 @@ const COMPOSED = { releaseLayoutDefaults: true };
8839
8917
  const TEXT_VARIABLE = /\{(year|business\.name)\}/g;
8840
8918
  /** What `page.*` may read on a node stamped for a navigation page. */
8841
8919
  const STAMPED_PAGE_FIELDS = new Set(['name', 'route']);
8842
- const clone = (value) => JSON.parse(JSON.stringify(value));
8920
+ const clone$1 = (value) => JSON.parse(JSON.stringify(value));
8843
8921
  /** 32-bit FNV-1a, base 36 — a short stable id fragment. */
8844
8922
  function stableHash(text) {
8845
8923
  let h = 0x811c9dc5;
@@ -8850,7 +8928,7 @@ function stableHash(text) {
8850
8928
  return h.toString(36).padStart(7, '0').slice(-6);
8851
8929
  }
8852
8930
  const create = (id, type, props = {}, elementStyle) => createSerializedNode(id, type, props, elementStyle, COMPOSED);
8853
- function nonEmpty(v) {
8931
+ function nonEmpty$1(v) {
8854
8932
  return typeof v === 'string' && v.trim() !== '';
8855
8933
  }
8856
8934
  /** A pattern's own words with the compose-time variables filled: `{year}`, `{business.name}`. */
@@ -8862,16 +8940,16 @@ function fillVariables(text, ctx) {
8862
8940
  function chooseText(pn, ctx) {
8863
8941
  const fact = pn.ph && FACT_KEYS.includes(pn.ph) ? pn.ph : null;
8864
8942
  const slotValue = pn.slot ? ctx.section?.slots?.[pn.slot] : undefined;
8865
- if (nonEmpty(slotValue))
8943
+ if (nonEmpty$1(slotValue))
8866
8944
  return { value: slotValue };
8867
8945
  if (pn.fromBusiness) {
8868
8946
  const v = ctx.plan.business[pn.fromBusiness];
8869
- if (nonEmpty(v))
8947
+ if (nonEmpty$1(v))
8870
8948
  return { value: v };
8871
8949
  }
8872
8950
  if (fact) {
8873
8951
  const given = ctx.plan.business.facts?.[fact];
8874
- return nonEmpty(given) ? { value: given } : { value: SITE_FACT_LABELS[fact], placeholder: { kind: PlaceholderKind.Fact, fact } };
8952
+ return nonEmpty$1(given) ? { value: given } : { value: SITE_FACT_LABELS[fact], placeholder: { kind: PlaceholderKind.Fact, fact } };
8875
8953
  }
8876
8954
  const own = pn.text === undefined ? undefined : fillVariables(pn.text, ctx);
8877
8955
  if (pn.ph === PatternPlaceholder.Copy)
@@ -9579,7 +9657,7 @@ function composeSite(plan, context) {
9579
9657
  const issues = [];
9580
9658
  const routes = new Set(plan.pages.map(p => p.route));
9581
9659
  const relatives = new Map(plan.dataSources.map(d => [d.alias, d.type === PlanDataSourceType.Relative ? { type: d.type, of: d.of, op: d.op } : { type: d.type }]));
9582
- const email = nonEmpty(plan.business.facts?.email) ? plan.business.facts.email.trim() : null;
9660
+ const email = nonEmpty$1(plan.business.facts?.email) ? plan.business.facts.email.trim() : null;
9583
9661
  const base = { plan, placeholders, issues, routes, year: context.year, email, navPage: null };
9584
9662
  const pages = plan.pages.map(page => {
9585
9663
  const nextAlias = page.routeSource ? nextSourceOf(relatives, page.routeSource) : null;
@@ -9612,10 +9690,10 @@ function composeSite(plan, context) {
9612
9690
  globals = planNavShape(globals, shape, {}, slot).globals;
9613
9691
  }
9614
9692
  return {
9615
- source: clone(plan.source),
9616
- business: clone(plan.business),
9617
- design: resolveDesignChoices(clone(plan.design)),
9618
- schemas: clone(plan.contentTypes),
9693
+ source: clone$1(plan.source),
9694
+ business: clone$1(plan.business),
9695
+ design: resolveDesignChoices(clone$1(plan.design)),
9696
+ schemas: clone$1(plan.contentTypes),
9619
9697
  items: Object.fromEntries(plan.contentTypes.map(t => [t.name, composeItems(t)])),
9620
9698
  dataSources: plan.dataSources.map(composeDataSource),
9621
9699
  pages,
@@ -9653,14 +9731,14 @@ function navPagePlans(pages) {
9653
9731
  * no /contact page: an email to the business, else removed), and the pattern's phone shape. Pure.
9654
9732
  */
9655
9733
  function composeNavHeader(input, options) {
9656
- if (!nonEmpty(options?.id))
9734
+ if (!nonEmpty$1(options?.id))
9657
9735
  throw new ComposeError('composeNavHeader needs the header id');
9658
9736
  const pattern = patternById(SITE_HEADER_PATTERN);
9659
9737
  const all = navPagePlans(input.pages);
9660
9738
  const navRoutes = new Set(siteNavPages(input.pages).map(p => p.route));
9661
9739
  const pages = all.map(p => ({ ...p, inNav: navRoutes.has(p.route) }));
9662
9740
  const plan = { business: input.business, pages, dataSources: [], contentTypes: [] };
9663
- const email = nonEmpty(input.business.facts?.email) ? input.business.facts.email.trim() : null;
9741
+ const email = nonEmpty$1(input.business.facts?.email) ? input.business.facts.email.trim() : null;
9664
9742
  const issues = [];
9665
9743
  const ctx = {
9666
9744
  owner: GlobalSlot.Header, page: null, section: null, plan, year: 0, email, navPage: null, nextAlias: null,
@@ -9684,7 +9762,7 @@ const PRICING_PATTERN = 'pricing.tiers';
9684
9762
  * words, unlinked, for the author to link. Pure.
9685
9763
  */
9686
9764
  function composeSection(patternId, input, options) {
9687
- if (!nonEmpty(options?.id))
9765
+ if (!nonEmpty$1(options?.id))
9688
9766
  throw new ComposeError('composeSection needs the section id');
9689
9767
  const pattern = patternById(patternId);
9690
9768
  if (!pattern)
@@ -9694,7 +9772,7 @@ function composeSection(patternId, input, options) {
9694
9772
  const business = input.business ?? { name: '' };
9695
9773
  const pages = navPagePlans(input.pages ?? []);
9696
9774
  const plan = { business, pages, dataSources: [], contentTypes: [] };
9697
- const email = nonEmpty(business.facts?.email) ? business.facts.email.trim() : null;
9775
+ const email = nonEmpty$1(business.facts?.email) ? business.facts.email.trim() : null;
9698
9776
  const placeholders = [];
9699
9777
  const issues = [];
9700
9778
  const ctx = {
@@ -9731,13 +9809,13 @@ const ENTRANCE_TRIGGER = 'viewportEnter';
9731
9809
  const ENTRANCE_EASING = 'ease-out';
9732
9810
  const LEVELS = SPEC_AXES.animations;
9733
9811
  const FAMILIES = SPEC_AXES.animationFamilies;
9734
- const NOTHING = { content: null, parts: {} };
9812
+ const NOTHING$1 = { content: null, parts: {} };
9735
9813
  /** What a section of `family` moves at `level` — nothing for an unknown level or family. */
9736
9814
  function sectionEntrances(level, family) {
9737
9815
  const lv = level ? LEVELS[level] : undefined;
9738
9816
  const allow = FAMILIES[family] ?? [];
9739
9817
  if (!lv || !allow.length)
9740
- return NOTHING;
9818
+ return NOTHING$1;
9741
9819
  if (lv.parts.content && allow.includes(EntrancePart.Content))
9742
9820
  return { content: lv.parts.content, parts: {} };
9743
9821
  const parts = {};
@@ -9858,6 +9936,8 @@ var BuildStep;
9858
9936
  BuildStep["Preflight"] = "preflight";
9859
9937
  BuildStep["ContentTypes"] = "content-types";
9860
9938
  BuildStep["SampleImages"] = "sample-images";
9939
+ /** I2: the curated stock photos the plan's empty picture slots take, copied into the studio (never hot-linked). */
9940
+ BuildStep["StockPhotos"] = "stock-photos";
9861
9941
  BuildStep["Items"] = "items";
9862
9942
  BuildStep["DataSources"] = "data-sources";
9863
9943
  BuildStep["Pages"] = "pages";
@@ -9879,6 +9959,8 @@ var CreatedKind;
9879
9959
  /** Fields the build added to a content type it found (types are shared by every organization of a studio). */
9880
9960
  CreatedKind["ContentTypeFields"] = "content-type-fields";
9881
9961
  CreatedKind["SampleImage"] = "sample-image";
9962
+ /** A stock photo copied into the studio's storage (`media/stock/…`), with its assets row. */
9963
+ CreatedKind["StockPhoto"] = "stock-photo";
9882
9964
  CreatedKind["Item"] = "item";
9883
9965
  CreatedKind["DataSource"] = "data-source";
9884
9966
  CreatedKind["Page"] = "page";
@@ -9900,6 +9982,8 @@ var BuildNoticeCode;
9900
9982
  BuildNoticeCode["NoStorage"] = "no-storage";
9901
9983
  /** A content type the build reuses is not public in this studio: the site will not show its items. */
9902
9984
  BuildNoticeCode["ContentTypeNotPublic"] = "content-type-not-public";
9985
+ /** The stock library could not be read: the picture slots keep the empty box, and the run says so. */
9986
+ BuildNoticeCode["StockUnavailable"] = "stock-unavailable";
9903
9987
  })(BuildNoticeCode || (BuildNoticeCode = {}));
9904
9988
  var PreflightRefusalCode;
9905
9989
  (function (PreflightRefusalCode) {
@@ -9931,6 +10015,7 @@ const buildKeys = {
9931
10015
  contentTypeFields: (name) => `schema-fields:${name}`,
9932
10016
  item: (contentType, n) => `item:${contentType}:${n}`,
9933
10017
  sampleImage: (n) => `sample-image:${n}`,
10018
+ stockPhoto: (libraryKey) => `stock-photo:${libraryKey}`,
9934
10019
  dataSource: (alias) => `data-source:${alias}`,
9935
10020
  page: (key) => `page:${key}`,
9936
10021
  global: (nodeId) => `global:${nodeId}`,
@@ -10009,6 +10094,14 @@ var AiStep;
10009
10094
  AiStep["NameStructures"] = "scanNaming";
10010
10095
  })(AiStep || (AiStep = {}));
10011
10096
  const AI_STEPS = Object.values(AiStep);
10097
+ /** Each step's name on the wire (the control plane's `label`, the console) — the screens translate their own. */
10098
+ const AI_STEP_LABELS = {
10099
+ [AiStep.ReadMessage]: 'Read your message',
10100
+ [AiStep.PlanSite]: 'Plan the site',
10101
+ [AiStep.ChangePlan]: 'Change the plan',
10102
+ [AiStep.MakePictures]: 'Make pictures',
10103
+ [AiStep.NameStructures]: 'Name repeated structures',
10104
+ };
10012
10105
  /** What a model must be able to do for a step to run on it. */
10013
10106
  var AiCapability;
10014
10107
  (function (AiCapability) {
@@ -10028,7 +10121,8 @@ const AI_STEP_TYPICAL_TOKENS = {
10028
10121
  [AiStep.ReadMessage]: { input: 8_800, output: 1_500, cacheWrite: 0, cacheRead: 0 },
10029
10122
  [AiStep.PlanSite]: { input: 12_000, output: 7_000, cacheWrite: 0, cacheRead: 0 },
10030
10123
  [AiStep.ChangePlan]: { input: 6_000, output: 500, cacheWrite: 0, cacheRead: 0 },
10031
- [AiStep.MakePictures]: { input: 1_120, output: 0, cacheWrite: 0, cacheRead: 0 },
10124
+ // a 1 K picture is 1,120 image tokens (Google's figure for Nano Banana 2) — I-D4
10125
+ [AiStep.MakePictures]: { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, imageOutput: 1_120 },
10032
10126
  [AiStep.NameStructures]: { input: 80_000, output: 10_000, cacheWrite: 0, cacheRead: 0 },
10033
10127
  };
10034
10128
  /** 1 credit = 1 US cent of what the AI actually cost (decision D4). */
@@ -10042,7 +10136,8 @@ function costUsd(tokens, price) {
10042
10136
  return tokens.input * perToken(price.input, 0)
10043
10137
  + tokens.output * perToken(price.output, 0)
10044
10138
  + tokens.cacheWrite * perToken(price.cacheWrite, price.input)
10045
- + tokens.cacheRead * perToken(price.cacheRead, price.input);
10139
+ + tokens.cacheRead * perToken(price.cacheRead, price.input)
10140
+ + (tokens.imageOutput ?? 0) * perToken(price.imageOutput, price.output);
10046
10141
  }
10047
10142
  /** Dollars → credits, kept to a millionth of a credit (a short read costs a fraction of one). */
10048
10143
  function creditsOfUsd(usd) {
@@ -10122,6 +10217,1135 @@ var AiCallStatus;
10122
10217
  AiCallStatus["Interrupted"] = "interrupted";
10123
10218
  })(AiCallStatus || (AiCallStatus = {}));
10124
10219
 
10220
+ /**
10221
+ * The structured-output format a model writes a Site Plan in (AI plan §3d step 4, M1-3) — built from the SAME shape
10222
+ * table `validateSitePlan` reads (`SPEC_SITE_PLAN_SHAPE`) and the SAME pattern library, so the plan a model is told
10223
+ * about is the plan the gate refuses. ONE source: a key the table gains breaks this builder until it is written here.
10224
+ *
10225
+ * Two maps of the shape are written as LISTS on the wire, because no strict mode can say "any key": a section's
10226
+ * `slots` (`[{ slot, text }]`, the slot names closed per pattern) and a seed's field values (`values: [{ field,
10227
+ * value }]`). `sitePlanFromWire` turns them back; nothing else differs. Every section is one `anyOf` branch per
10228
+ * pattern — its id, its declared slots, its optional parts, a binding only when it takes one — and the design, the
10229
+ * source and the version are PINNED to the owner's pick and this run (one-value enums), so the provider cannot let
10230
+ * a model change the look. The gate still checks both (`plan.design` deep-equals the pick).
10231
+ */
10232
+ const SITE_PLAN_SCHEMA_NAME = 'site_plan';
10233
+ /** The wire keys of the two maps written as lists. */
10234
+ var SitePlanWireKey;
10235
+ (function (SitePlanWireKey) {
10236
+ SitePlanWireKey["SlotName"] = "slot";
10237
+ SitePlanWireKey["SlotText"] = "text";
10238
+ SitePlanWireKey["SeedValues"] = "values";
10239
+ SitePlanWireKey["SeedField"] = "field";
10240
+ SitePlanWireKey["SeedValue"] = "value";
10241
+ })(SitePlanWireKey || (SitePlanWireKey = {}));
10242
+ const SHAPE = SPEC_SITE_PLAN_SHAPE;
10243
+ const one$1 = (value) => jsonOneOf([value]);
10244
+ const oneBoolean = (value) => ({ type: 'boolean', enum: [value] });
10245
+ /** The pick, key by key, as one-value enums; an optional key the pick leaves out may only be left out (or empty). */
10246
+ function pinnedDesign(design) {
10247
+ const entry = SHAPE['design'];
10248
+ const values = design;
10249
+ const properties = {};
10250
+ for (const key of Object.keys(entry).map(k => k.replace(/\?$/, ''))) {
10251
+ const v = values[key];
10252
+ if (typeof v === 'string')
10253
+ properties[key] = one$1(v);
10254
+ else if (typeof v === 'boolean')
10255
+ properties[key] = oneBoolean(v);
10256
+ else if (isPlainObject(v))
10257
+ properties[key] = jsonClosedObject(Object.fromEntries(Object.entries(v).map(([role, family]) => [role, one$1(String(family))])), Object.keys(v));
10258
+ else
10259
+ properties[key] = jsonClosedObject({}, []);
10260
+ }
10261
+ const shaped = jsonShapedObject(entry, properties, 'design');
10262
+ // Everything the pick carries is required: a model may not leave a choice out and let a default stand in.
10263
+ const given = Object.keys(values).filter(k => values[k] !== undefined && k in properties);
10264
+ return { ...shaped, required: [...new Set([...shaped['required'], ...given])] };
10265
+ }
10266
+ function sourceSchema(source) {
10267
+ if (source.kind === SitePlanSourceKind.Ai)
10268
+ return jsonShapedObject(SHAPE['sourceAi'], { kind: one$1(source.kind), runId: one$1(source.runId) }, 'sourceAi');
10269
+ return jsonShapedObject(SHAPE['sourceTemplate'], {
10270
+ kind: one$1(source.kind), templateId: one$1(source.templateId), templateVersion: { type: 'integer', enum: [source.templateVersion] },
10271
+ }, 'sourceTemplate');
10272
+ }
10273
+ function businessSchema() {
10274
+ const facts = jsonClosedObject(Object.fromEntries(FACT_KEYS.map(key => [key, jsonText()])), []);
10275
+ return jsonShapedObject(SHAPE['business'], { name: jsonText(), tagline: jsonText(), facts }, 'business');
10276
+ }
10277
+ /** A content type's field. */
10278
+ function fieldJsonSchema() {
10279
+ return jsonShapedObject(SHAPE['field'], { name: jsonText(), type: jsonText(), controller: jsonText(), required: jsonBoolean(), note: jsonText() }, 'field');
10280
+ }
10281
+ /** A content type, its seed's field values as a list (`values: [{ field, value }]`). */
10282
+ function contentTypeJsonSchema() {
10283
+ const value = jsonAnyOf([jsonText(), jsonListOf(jsonText())]);
10284
+ const values = jsonListOf(jsonClosedObject({ [SitePlanWireKey.SeedField]: jsonText(), [SitePlanWireKey.SeedValue]: value }, [SitePlanWireKey.SeedField, SitePlanWireKey.SeedValue]));
10285
+ const seed = jsonShapedObject(SHAPE['seed'], { count: jsonInteger(), sample: jsonBoolean(), isPublished: jsonBoolean() }, 'seed', { key: SitePlanWireKey.SeedValues, schema: values });
10286
+ return jsonShapedObject(SHAPE['contentType'], {
10287
+ name: jsonText(), label: jsonText(), previewTextField: jsonText(), previewImageField: jsonText(), fields: jsonListOf(fieldJsonSchema()), seed,
10288
+ }, 'contentType');
10289
+ }
10290
+ /** A data source: a cms list / single item, or a relative one. */
10291
+ function dataSourceJsonSchema() {
10292
+ const byRoute = jsonClosedObject({ field: jsonText(), routeParam: jsonText() }, ['field', 'routeParam']);
10293
+ const cms = jsonShapedObject(SHAPE['cmsDataSource'], {
10294
+ alias: jsonText(), type: one$1(PlanDataSourceType.Cms), mode: jsonOneOf(Object.values(PlanDataSourceMode)),
10295
+ contentType: jsonText(), limit: jsonInteger(), filterByRouteParam: byRoute,
10296
+ }, 'cmsDataSource');
10297
+ const relative = jsonShapedObject(SHAPE['relativeDataSource'], {
10298
+ alias: jsonText(), type: one$1(PlanDataSourceType.Relative), of: jsonText(), op: jsonOneOf(Object.values(RelativeOp)), within: jsonText(),
10299
+ }, 'relativeDataSource');
10300
+ return jsonAnyOf([cms, relative]);
10301
+ }
10302
+ /** The patterns a page section may use (every non-global one), in library order. */
10303
+ function sectionPatternIds() {
10304
+ return Object.entries(SITE_PATTERNS).filter(([, p]) => !p.global).map(([id]) => id);
10305
+ }
10306
+ /** A key a pattern has nothing for: only `{}` (or leaving it out) — the decoder drops it. */
10307
+ const NOTHING = () => jsonClosedObject({}, []);
10308
+ /** One section branch: this pattern's id, its declared slots (a list), its optional parts, a binding when it takes one. */
10309
+ function sectionBranch(id, pattern) {
10310
+ const slots = patternSlots(pattern);
10311
+ const parts = patternOptionalParts(pattern);
10312
+ const slotList = slots.length
10313
+ ? jsonListOf(jsonClosedObject({ [SitePlanWireKey.SlotName]: jsonOneOf(slots), [SitePlanWireKey.SlotText]: jsonText() }, [SitePlanWireKey.SlotName, SitePlanWireKey.SlotText]))
10314
+ : NOTHING();
10315
+ const branch = jsonShapedObject(SHAPE['section'], {
10316
+ pattern: one$1(id),
10317
+ slots: slotList,
10318
+ binding: patternTakesBinding(pattern) ? jsonText() : NOTHING(),
10319
+ without: parts.length ? jsonListOf(jsonOneOf(parts)) : NOTHING(),
10320
+ }, 'section');
10321
+ return { ...branch, description: `${id}: ${pattern.label}` };
10322
+ }
10323
+ /** A page section: one branch per pattern. */
10324
+ function sectionJsonSchema() {
10325
+ return jsonAnyOf(sectionPatternIds().map(id => sectionBranch(id, SITE_PATTERNS[id])));
10326
+ }
10327
+ /** A page. */
10328
+ function pageJsonSchema() {
10329
+ return jsonShapedObject(SHAPE['page'], {
10330
+ key: jsonText(), name: jsonText(), route: jsonText(), isHome: jsonBoolean(), required: jsonBoolean(), inNav: jsonBoolean(),
10331
+ template: jsonText(), routeSource: jsonText(), sections: jsonListOf(sectionJsonSchema()),
10332
+ }, 'page');
10333
+ }
10334
+ const globalIds = (position) => Object.entries(SITE_PATTERNS).filter(([, p]) => p.global === position).map(([id]) => id);
10335
+ /** The Site Plan format, pinned to the owner's pick and this run. */
10336
+ function sitePlanJsonSchema(pin) {
10337
+ const globals = jsonShapedObject(SHAPE['globals'], {
10338
+ [GlobalSlot.Header]: jsonOneOf(globalIds(PatternGlobalPosition.Before)),
10339
+ [GlobalSlot.Footer]: jsonOneOf(globalIds(PatternGlobalPosition.After)),
10340
+ }, 'globals');
10341
+ return jsonShapedObject(SHAPE['plan'], {
10342
+ version: one$1(SITE_PLAN_VERSION),
10343
+ source: sourceSchema(pin.source),
10344
+ business: businessSchema(),
10345
+ design: pinnedDesign(pin.design),
10346
+ contentTypes: jsonListOf(contentTypeJsonSchema()),
10347
+ dataSources: jsonListOf(dataSourceJsonSchema()),
10348
+ pages: jsonListOf(pageJsonSchema()),
10349
+ globals,
10350
+ }, 'plan');
10351
+ }
10352
+ // ── The wire → the plan ─────────────────────────────────────────────────────────────────────────────
10353
+ const isEmptyObject = (v) => isPlainObject(v) && Object.keys(v).length === 0;
10354
+ /** `[{ slot, text }]` → `{ slot: text }`; anything that is not that list is left for the gate to refuse. */
10355
+ function slotsFromWire(slots) {
10356
+ if (!Array.isArray(slots))
10357
+ return slots;
10358
+ const out = {};
10359
+ for (const entry of slots) {
10360
+ if (!isPlainObject(entry) || typeof entry[SitePlanWireKey.SlotName] !== 'string')
10361
+ return slots;
10362
+ out[entry[SitePlanWireKey.SlotName]] = entry[SitePlanWireKey.SlotText];
10363
+ }
10364
+ return out;
10365
+ }
10366
+ /** A section off the wire: its slots back to a map; a key the pattern has nothing for (`{}`) dropped. */
10367
+ function sectionFromWire(section) {
10368
+ if (!isPlainObject(section))
10369
+ return section;
10370
+ const out = { ...section };
10371
+ for (const key of ['slots', 'binding', 'without'])
10372
+ if (isEmptyObject(out[key]))
10373
+ delete out[key];
10374
+ if ('slots' in out)
10375
+ out['slots'] = slotsFromWire(out['slots']);
10376
+ return out;
10377
+ }
10378
+ /** A content type off the wire: its seed's `values` list back to field keys. */
10379
+ function contentTypeFromWire(type) {
10380
+ if (!isPlainObject(type) || !isPlainObject(type['seed']))
10381
+ return type;
10382
+ const { [SitePlanWireKey.SeedValues]: values, ...seed } = type['seed'];
10383
+ if (Array.isArray(values)) {
10384
+ for (const entry of values) {
10385
+ if (isPlainObject(entry) && typeof entry[SitePlanWireKey.SeedField] === 'string')
10386
+ seed[entry[SitePlanWireKey.SeedField]] = entry[SitePlanWireKey.SeedValue];
10387
+ }
10388
+ }
10389
+ else if (values !== undefined)
10390
+ seed[SitePlanWireKey.SeedValues] = values;
10391
+ return { ...type, seed };
10392
+ }
10393
+ function pageFromWire(page) {
10394
+ if (!isPlainObject(page) || !Array.isArray(page['sections']))
10395
+ return page;
10396
+ return { ...page, sections: page['sections'].map(sectionFromWire) };
10397
+ }
10398
+ /** The design off the wire: an empty optional object (a choice the pick does not carry) dropped. */
10399
+ function designFromWire(design) {
10400
+ if (!isPlainObject(design))
10401
+ return design;
10402
+ return Object.fromEntries(Object.entries(design).filter(([, v]) => !isEmptyObject(v)));
10403
+ }
10404
+ /**
10405
+ * A model's answer → a Site Plan: the two listed maps back to maps, nothing else touched. Never repairs — whatever the
10406
+ * answer got wrong is still wrong, for `validateSitePlan` to refuse with a path.
10407
+ */
10408
+ function sitePlanFromWire(wire) {
10409
+ if (!isPlainObject(wire))
10410
+ return wire;
10411
+ const out = { ...wire };
10412
+ if (Array.isArray(out['contentTypes']))
10413
+ out['contentTypes'] = out['contentTypes'].map(contentTypeFromWire);
10414
+ if (Array.isArray(out['pages']))
10415
+ out['pages'] = out['pages'].map(pageFromWire);
10416
+ if ('design' in out)
10417
+ out['design'] = designFromWire(out['design']);
10418
+ return out;
10419
+ }
10420
+ /** The plan → the wire (what a re-ask shows the model, and what the plan edits' prompt carries). */
10421
+ function sitePlanToWire(plan) {
10422
+ const slotsToWire = (slots) => Object.entries(slots ?? {}).map(([slot, text]) => ({ [SitePlanWireKey.SlotName]: slot, [SitePlanWireKey.SlotText]: text }));
10423
+ return {
10424
+ ...plan,
10425
+ contentTypes: plan.contentTypes.map(t => {
10426
+ if (!t.seed)
10427
+ return t;
10428
+ const { count, sample, isPublished, ...values } = t.seed;
10429
+ return { ...t, seed: { count, sample, isPublished, [SitePlanWireKey.SeedValues]: Object.entries(values).map(([field, value]) => ({ [SitePlanWireKey.SeedField]: field, [SitePlanWireKey.SeedValue]: value })) } };
10430
+ }),
10431
+ pages: plan.pages.map(p => ({ ...p, sections: p.sections.map(s => (s.slots ? { ...s, slots: slotsToWire(s.slots) } : s)) })),
10432
+ };
10433
+ }
10434
+
10435
+ /**
10436
+ * Changing a plan by sentence (AI plan §3d "The Plan prompt box", M1-5; reused by M2 after a build): the CLOSED list
10437
+ * of operations (N-D6) a model may write against the current plan, and the ONE pure `applyPlanEdits` that carries them
10438
+ * out. Nothing is written until Build, so an edit is a plan → plan function; the result goes through
10439
+ * `validateSitePlan` like any plan. Pages are named by `key`, sections by their index on the page. A set of edits is
10440
+ * applied whole or not at all: one edit that cannot apply refuses the set, with a path, and the plan is unchanged.
10441
+ */
10442
+ var PlanEditOp;
10443
+ (function (PlanEditOp) {
10444
+ PlanEditOp["AddPage"] = "addPage";
10445
+ PlanEditOp["RemovePage"] = "removePage";
10446
+ PlanEditOp["RenamePage"] = "renamePage";
10447
+ PlanEditOp["MergePages"] = "mergePages";
10448
+ /** Its place in the menu (the plan's page order). */
10449
+ PlanEditOp["MovePage"] = "movePage";
10450
+ PlanEditOp["AddSection"] = "addSection";
10451
+ PlanEditOp["RemoveSection"] = "removeSection";
10452
+ PlanEditOp["MoveSection"] = "moveSection";
10453
+ PlanEditOp["SwapPattern"] = "swapPattern";
10454
+ PlanEditOp["SetSlot"] = "setSlot";
10455
+ PlanEditOp["AddContentType"] = "addContentType";
10456
+ PlanEditOp["AddField"] = "addField";
10457
+ PlanEditOp["SetSectionData"] = "setSectionData";
10458
+ })(PlanEditOp || (PlanEditOp = {}));
10459
+ /**
10460
+ * Every op's keys, explained — what the format is built from and what `validatePlanEdits` reads (`key?` optional).
10461
+ * A page is named by its `key`; a section by its 0-based `index` on that page.
10462
+ */
10463
+ const PLAN_EDIT_SHAPE = {
10464
+ [PlanEditOp.AddPage]: { op: "'addPage'", page: 'page — a whole new page (a unique key and route)', 'after?': 'string — the key of the page it follows in the menu; absent: last' },
10465
+ [PlanEditOp.RemovePage]: { op: "'removePage'", page: 'string — the key of the page to remove (never the home page)' },
10466
+ [PlanEditOp.RenamePage]: { op: "'renamePage'", page: 'string — a page key', 'name?': 'string — its new name (the menu label)', 'route?': 'string — its new route' },
10467
+ [PlanEditOp.MergePages]: { op: "'mergePages'", into: 'string — the page that stays', from: 'string — the page whose sections move into it, then removed' },
10468
+ [PlanEditOp.MovePage]: { op: "'movePage'", page: 'string — a page key', to: 'integer — its new 0-based place in the menu' },
10469
+ [PlanEditOp.AddSection]: { op: "'addSection'", page: 'string — a page key', section: 'section', 'at?': 'integer — its 0-based place on the page; absent: last' },
10470
+ [PlanEditOp.RemoveSection]: { op: "'removeSection'", page: 'string — a page key', index: 'integer — the section' },
10471
+ [PlanEditOp.MoveSection]: { op: "'moveSection'", page: 'string — a page key', from: 'integer — the section', to: 'integer — its new place' },
10472
+ [PlanEditOp.SwapPattern]: { op: "'swapPattern'", page: 'string — a page key', index: 'integer — the section', pattern: 'string — the pattern it becomes (its slots kept where the new one has them)' },
10473
+ [PlanEditOp.SetSlot]: { op: "'setSlot'", page: 'string — a page key', index: 'integer — the section', slot: "string — one of the pattern's slots", text: "string — the words, in the owner's language and voice" },
10474
+ [PlanEditOp.AddContentType]: { op: "'addContentType'", contentType: 'contentType — a unique name', 'dataSources?': 'dataSource[] — the list / single sources its pages read' },
10475
+ [PlanEditOp.AddField]: { op: "'addField'", contentType: 'string — a content type of the plan', field: 'field', 'sample?': "string | string[] — the samples' value for it" },
10476
+ [PlanEditOp.SetSectionData]: { op: "'setSectionData'", page: 'string — a page key', index: 'integer — the section', binding: 'string — the list source the section repeats' },
10477
+ };
10478
+ var PlanChangeKind;
10479
+ (function (PlanChangeKind) {
10480
+ PlanChangeKind["Page"] = "page";
10481
+ PlanChangeKind["Section"] = "section";
10482
+ PlanChangeKind["ContentType"] = "contentType";
10483
+ PlanChangeKind["Field"] = "field";
10484
+ PlanChangeKind["DataSource"] = "dataSource";
10485
+ })(PlanChangeKind || (PlanChangeKind = {}));
10486
+ var PlanChangeAction;
10487
+ (function (PlanChangeAction) {
10488
+ PlanChangeAction["Added"] = "added";
10489
+ PlanChangeAction["Removed"] = "removed";
10490
+ PlanChangeAction["Changed"] = "changed";
10491
+ PlanChangeAction["Moved"] = "moved";
10492
+ })(PlanChangeAction || (PlanChangeAction = {}));
10493
+ var PlanEditIssueCode;
10494
+ (function (PlanEditIssueCode) {
10495
+ PlanEditIssueCode["Shape"] = "shape";
10496
+ PlanEditIssueCode["UnknownKey"] = "unknown-key";
10497
+ PlanEditIssueCode["Duplicate"] = "duplicate";
10498
+ PlanEditIssueCode["UnknownOp"] = "unknown-op";
10499
+ PlanEditIssueCode["UnknownPage"] = "unknown-page";
10500
+ PlanEditIssueCode["UnknownSection"] = "unknown-section";
10501
+ PlanEditIssueCode["UnknownPattern"] = "unknown-pattern";
10502
+ PlanEditIssueCode["UnknownSlot"] = "unknown-slot";
10503
+ PlanEditIssueCode["UnknownContentType"] = "unknown-content-type";
10504
+ PlanEditIssueCode["UnknownDataSource"] = "unknown-data-source";
10505
+ /** The home page stays (a plan has exactly one). */
10506
+ PlanEditIssueCode["HomePage"] = "home-page";
10507
+ /** A template page (one item per route) and a plain page cannot be merged. */
10508
+ PlanEditIssueCode["MergeTemplate"] = "merge-template";
10509
+ })(PlanEditIssueCode || (PlanEditIssueCode = {}));
10510
+ class Collector extends PathIssueCollector {
10511
+ constructor() {
10512
+ super('plan edit', { shape: PlanEditIssueCode.Shape, unknownKey: PlanEditIssueCode.UnknownKey, duplicate: PlanEditIssueCode.Duplicate });
10513
+ }
10514
+ }
10515
+ const OPS = Object.values(PlanEditOp);
10516
+ /** A page's own header section (`page-header.simple`) — what a merge leaves behind with the page it named. */
10517
+ const PAGE_HEADER_FAMILY = 'header';
10518
+ const clone = (v) => JSON.parse(JSON.stringify(v));
10519
+ const clamp = (n, max) => Math.max(0, Math.min(Math.trunc(n), max));
10520
+ /** The edits' structure: a known op, its keys (`PLAN_EDIT_SHAPE`), integers where an index is. Never repairs. */
10521
+ function validatePlanEdits(edits) {
10522
+ const c = new Collector();
10523
+ if (!Array.isArray(edits)) {
10524
+ c.add(PlanEditIssueCode.Shape, 'edits', 'must be an array');
10525
+ return { valid: false, issues: c.issues };
10526
+ }
10527
+ edits.forEach((raw, i) => {
10528
+ const path = `edits[${i}]`;
10529
+ if (!isPlainObject(raw))
10530
+ return c.add(PlanEditIssueCode.Shape, path, 'must be an object');
10531
+ const op = raw['op'];
10532
+ if (typeof op !== 'string' || !OPS.includes(op))
10533
+ return c.add(PlanEditIssueCode.UnknownOp, `${path}.op`, `must be one of ${OPS.join(', ')}`);
10534
+ const entry = PLAN_EDIT_SHAPE[op];
10535
+ c.keys(raw, shapeKeys(entry), path);
10536
+ for (const [key, text] of Object.entries(entry)) {
10537
+ const optional = key.endsWith('?');
10538
+ const name = optional ? key.slice(0, -1) : key;
10539
+ const v = raw[name];
10540
+ if (v === undefined) {
10541
+ if (!optional)
10542
+ c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'is required');
10543
+ continue;
10544
+ }
10545
+ if (text.startsWith('string | string[]')) {
10546
+ if (typeof v !== 'string' && !(Array.isArray(v) && v.every(x => typeof x === 'string')))
10547
+ c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be a string or a list of strings');
10548
+ }
10549
+ else if (text.startsWith('string') && !isNonEmptyString(v))
10550
+ c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be a non-empty string');
10551
+ if (text.startsWith('integer') && !Number.isInteger(v))
10552
+ c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be a whole number');
10553
+ if (['page', 'section', 'contentType', 'field'].includes(text.split(' ')[0]) && !isPlainObject(v))
10554
+ c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be an object');
10555
+ if (text.startsWith('dataSource[]') && !Array.isArray(v))
10556
+ c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be an array');
10557
+ }
10558
+ });
10559
+ return { valid: c.issues.length === 0, issues: c.issues };
10560
+ }
10561
+ /** The aliases a page reads (its route source and its sections' bindings). */
10562
+ function aliasesOf(pages) {
10563
+ const out = new Set();
10564
+ for (const page of pages) {
10565
+ if (page.routeSource)
10566
+ out.add(page.routeSource);
10567
+ for (const s of page.sections)
10568
+ if (s.binding)
10569
+ out.add(s.binding);
10570
+ }
10571
+ return out;
10572
+ }
10573
+ /** A relative source (`project.next`'s) is read while its subject is, and keeps the list it walks. */
10574
+ function withDependencies(aliases, sources) {
10575
+ const out = new Set(aliases);
10576
+ for (const s of sources) {
10577
+ if (s.type === PlanDataSourceType.Relative && aliases.has(s.of)) {
10578
+ out.add(s.alias);
10579
+ out.add(s.within);
10580
+ }
10581
+ }
10582
+ return out;
10583
+ }
10584
+ const pageIndex = (d, key) => d.pages.findIndex(p => p.key === key);
10585
+ function findPage(d, key, c, path) {
10586
+ const at = pageIndex(d, key);
10587
+ if (at < 0)
10588
+ c.add(PlanEditIssueCode.UnknownPage, path, `no page "${String(key)}" in the plan (${d.pages.map(p => p.key).join(', ')})`);
10589
+ return at;
10590
+ }
10591
+ function findSection(d, pageAt, index, c, path) {
10592
+ const ok = Number.isInteger(index) && index >= 0 && index < d.pages[pageAt].sections.length;
10593
+ if (!ok)
10594
+ c.add(PlanEditIssueCode.UnknownSection, path, `page "${d.pages[pageAt].key}" has sections 0–${d.pages[pageAt].sections.length - 1}`);
10595
+ return ok;
10596
+ }
10597
+ const setSections = (d, at, sections) => { d.pages[at] = { ...d.pages[at], sections }; };
10598
+ /** One handler per op — a data table, never an if-chain. */
10599
+ const APPLY = {
10600
+ [PlanEditOp.AddPage]: (d, e, c, path, changed) => {
10601
+ const page = { ...e.page, sections: (e.page.sections ?? []).map(sectionFromWire) };
10602
+ if (pageIndex(d, page.key) >= 0)
10603
+ return c.add(PlanEditIssueCode.Duplicate, `${path}.page.key`, `page "${page.key}" is already in the plan`);
10604
+ const after = e.after === undefined ? d.pages.length - 1 : findPage(d, e.after, c, `${path}.after`);
10605
+ if (after < 0 && e.after !== undefined)
10606
+ return;
10607
+ d.pages.splice(after + 1, 0, page);
10608
+ changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Added, page: page.key, name: page.name });
10609
+ },
10610
+ [PlanEditOp.RemovePage]: (d, e, c, path, changed) => {
10611
+ const at = findPage(d, e.page, c, `${path}.page`);
10612
+ if (at < 0)
10613
+ return;
10614
+ if (d.pages[at].isHome)
10615
+ return c.add(PlanEditIssueCode.HomePage, `${path}.page`, 'the home page stays — rename or change it instead');
10616
+ const [gone] = d.pages.splice(at, 1);
10617
+ changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Removed, page: gone.key, name: gone.name });
10618
+ },
10619
+ [PlanEditOp.RenamePage]: (d, e, c, path, changed) => {
10620
+ const at = findPage(d, e.page, c, `${path}.page`);
10621
+ if (at < 0)
10622
+ return;
10623
+ d.pages[at] = { ...d.pages[at], ...(e.name !== undefined ? { name: e.name } : {}), ...(e.route !== undefined ? { route: e.route } : {}) };
10624
+ changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Changed, page: d.pages[at].key, name: d.pages[at].name });
10625
+ },
10626
+ [PlanEditOp.MergePages]: (d, e, c, path, changed) => {
10627
+ const into = findPage(d, e.into, c, `${path}.into`);
10628
+ const from = findPage(d, e.from, c, `${path}.from`);
10629
+ if (into < 0 || from < 0)
10630
+ return;
10631
+ if (into === from)
10632
+ return c.add(PlanEditIssueCode.Duplicate, `${path}.from`, 'a page is merged into another page');
10633
+ const a = d.pages[into];
10634
+ const b = d.pages[from];
10635
+ if (!!a.template !== !!b.template)
10636
+ return c.add(PlanEditIssueCode.MergeTemplate, `${path}.from`, 'a page per item and a plain page cannot be merged');
10637
+ if (b.isHome)
10638
+ return c.add(PlanEditIssueCode.HomePage, `${path}.from`, 'the home page stays — merge the other page into it');
10639
+ const moved = b.sections.filter((s, i) => !(i === 0 && patternById(s.pattern)?.family === PAGE_HEADER_FAMILY));
10640
+ setSections(d, into, [...a.sections, ...moved]);
10641
+ moved.forEach((_, k) => changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Added, page: a.key, index: a.sections.length + k }));
10642
+ d.pages.splice(from, 1);
10643
+ changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Changed, page: a.key, name: a.name });
10644
+ changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Removed, page: b.key, name: b.name });
10645
+ },
10646
+ [PlanEditOp.MovePage]: (d, e, c, path, changed) => {
10647
+ const at = findPage(d, e.page, c, `${path}.page`);
10648
+ if (at < 0)
10649
+ return;
10650
+ const [page] = d.pages.splice(at, 1);
10651
+ d.pages.splice(clamp(e.to, d.pages.length), 0, page);
10652
+ changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Moved, page: page.key, name: page.name });
10653
+ },
10654
+ [PlanEditOp.AddSection]: (d, e, c, path, changed) => {
10655
+ const at = findPage(d, e.page, c, `${path}.page`);
10656
+ if (at < 0)
10657
+ return;
10658
+ const sections = [...d.pages[at].sections];
10659
+ const index = e.at === undefined ? sections.length : clamp(e.at, sections.length);
10660
+ sections.splice(index, 0, sectionFromWire(e.section));
10661
+ setSections(d, at, sections);
10662
+ changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Added, page: d.pages[at].key, index });
10663
+ },
10664
+ [PlanEditOp.RemoveSection]: (d, e, c, path, changed) => {
10665
+ const at = findPage(d, e.page, c, `${path}.page`);
10666
+ if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
10667
+ return;
10668
+ setSections(d, at, d.pages[at].sections.filter((_, i) => i !== e.index));
10669
+ changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Removed, page: d.pages[at].key, index: e.index });
10670
+ },
10671
+ [PlanEditOp.MoveSection]: (d, e, c, path, changed) => {
10672
+ const at = findPage(d, e.page, c, `${path}.page`);
10673
+ if (at < 0 || !findSection(d, at, e.from, c, `${path}.from`))
10674
+ return;
10675
+ const sections = [...d.pages[at].sections];
10676
+ const [section] = sections.splice(e.from, 1);
10677
+ const to = clamp(e.to, sections.length);
10678
+ sections.splice(to, 0, section);
10679
+ setSections(d, at, sections);
10680
+ changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Moved, page: d.pages[at].key, index: to });
10681
+ },
10682
+ [PlanEditOp.SwapPattern]: (d, e, c, path, changed) => {
10683
+ const at = findPage(d, e.page, c, `${path}.page`);
10684
+ if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
10685
+ return;
10686
+ const next = patternById(e.pattern);
10687
+ if (!next || next.global)
10688
+ return c.add(PlanEditIssueCode.UnknownPattern, `${path}.pattern`, `no section pattern "${String(e.pattern)}"`);
10689
+ const old = d.pages[at].sections[e.index];
10690
+ const slots = Object.fromEntries(Object.entries(old.slots ?? {}).filter(([slot]) => patternSlots(next).includes(slot)));
10691
+ const without = (old.without ?? []).filter(w => patternOptionalParts(next).includes(w));
10692
+ const section = {
10693
+ pattern: e.pattern,
10694
+ ...(Object.keys(slots).length ? { slots } : {}),
10695
+ ...(old.binding && patternTakesBinding(next) ? { binding: old.binding } : {}),
10696
+ ...(without.length ? { without } : {}),
10697
+ };
10698
+ setSections(d, at, d.pages[at].sections.map((s, i) => (i === e.index ? section : s)));
10699
+ changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Changed, page: d.pages[at].key, index: e.index });
10700
+ },
10701
+ [PlanEditOp.SetSlot]: (d, e, c, path, changed) => {
10702
+ const at = findPage(d, e.page, c, `${path}.page`);
10703
+ if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
10704
+ return;
10705
+ const section = d.pages[at].sections[e.index];
10706
+ const declared = patternSlots(patternById(section.pattern));
10707
+ if (!declared.includes(e.slot))
10708
+ return c.add(PlanEditIssueCode.UnknownSlot, `${path}.slot`, `"${section.pattern}" has no slot "${e.slot}" (${declared.join(', ') || 'none'})`);
10709
+ setSections(d, at, d.pages[at].sections.map((s, i) => (i === e.index ? { ...s, slots: { ...(s.slots ?? {}), [e.slot]: e.text } } : s)));
10710
+ changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Changed, page: d.pages[at].key, index: e.index });
10711
+ },
10712
+ [PlanEditOp.AddContentType]: (d, e, c, path, changed) => {
10713
+ const type = contentTypeFromWire(e.contentType);
10714
+ if (d.contentTypes.some(t => t.name === type.name))
10715
+ return c.add(PlanEditIssueCode.Duplicate, `${path}.contentType.name`, `content type "${type.name}" is already in the plan`);
10716
+ d.contentTypes.push(type);
10717
+ changed.push({ kind: PlanChangeKind.ContentType, action: PlanChangeAction.Added, name: type.name });
10718
+ for (const [k, source] of (e.dataSources ?? []).entries()) {
10719
+ if (d.dataSources.some(s => s.alias === source.alias)) {
10720
+ c.add(PlanEditIssueCode.Duplicate, `${path}.dataSources[${k}].alias`, `data source "${source.alias}" is already in the plan`);
10721
+ continue;
10722
+ }
10723
+ d.dataSources.push(source);
10724
+ changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Added, name: source.alias });
10725
+ }
10726
+ },
10727
+ [PlanEditOp.AddField]: (d, e, c, path, changed) => {
10728
+ const at = d.contentTypes.findIndex(t => t.name === e.contentType);
10729
+ if (at < 0)
10730
+ return c.add(PlanEditIssueCode.UnknownContentType, `${path}.contentType`, `no content type "${String(e.contentType)}" in the plan`);
10731
+ const type = d.contentTypes[at];
10732
+ if (type.fields.some(f => f.name === e.field.name))
10733
+ return c.add(PlanEditIssueCode.Duplicate, `${path}.field.name`, `"${type.name}" already has a field "${e.field.name}"`);
10734
+ const seed = type.seed ? { ...type.seed, [e.field.name]: e.sample ?? '' } : undefined;
10735
+ d.contentTypes[at] = { ...type, fields: [...type.fields, e.field], ...(seed ? { seed } : {}) };
10736
+ changed.push({ kind: PlanChangeKind.Field, action: PlanChangeAction.Added, name: `${type.name}.${e.field.name}` });
10737
+ },
10738
+ [PlanEditOp.SetSectionData]: (d, e, c, path, changed) => {
10739
+ const at = findPage(d, e.page, c, `${path}.page`);
10740
+ if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
10741
+ return;
10742
+ const source = d.dataSources.find(s => s.alias === e.binding);
10743
+ if (!source || source.type !== PlanDataSourceType.Cms || source.mode !== PlanDataSourceMode.Query) {
10744
+ return c.add(PlanEditIssueCode.UnknownDataSource, `${path}.binding`, `no list source "${String(e.binding)}" in the plan`);
10745
+ }
10746
+ setSections(d, at, d.pages[at].sections.map((s, i) => (i === e.index ? { ...s, binding: e.binding } : s)));
10747
+ changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Changed, page: d.pages[at].key, index: e.index });
10748
+ },
10749
+ };
10750
+ /**
10751
+ * Apply a set of edits to a plan — all of them, or none (any issue refuses the set and returns the plan unchanged). The
10752
+ * sources only removed pages and sections read are dropped with them; sources nothing read before are kept.
10753
+ * The result still has to pass `validateSitePlan`.
10754
+ */
10755
+ function applyPlanEdits(plan, edits) {
10756
+ const structure = validatePlanEdits(edits);
10757
+ if (!structure.valid)
10758
+ return { plan, changed: [], issues: structure.issues };
10759
+ const draft = { pages: clone([...plan.pages]), contentTypes: clone([...plan.contentTypes]), dataSources: clone([...plan.dataSources]) };
10760
+ const c = new Collector();
10761
+ const changed = [];
10762
+ edits.forEach((edit, i) => APPLY[edit.op](draft, edit, c, `edits[${i}]`, changed));
10763
+ if (c.issues.length)
10764
+ return { plan, changed: [], issues: c.issues };
10765
+ const before = withDependencies(aliasesOf(plan.pages), plan.dataSources);
10766
+ const after = withDependencies(aliasesOf(draft.pages), draft.dataSources);
10767
+ const added = new Set(changed.filter(ch => ch.kind === PlanChangeKind.DataSource).map(ch => ch.name));
10768
+ const dataSources = draft.dataSources.filter(s => after.has(s.alias) || !before.has(s.alias) || added.has(s.alias));
10769
+ for (const s of draft.dataSources)
10770
+ if (!dataSources.includes(s))
10771
+ changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Removed, name: s.alias });
10772
+ return { plan: { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources }, changed, issues: [] };
10773
+ }
10774
+ // ── The format a model writes edits in ─────────────────────────────────────────────────────────────────
10775
+ const PLAN_EDITS_SCHEMA_NAME = 'plan_edits';
10776
+ /** A sentence's answer: the edits, or why the ops cannot say it (one line, in the owner's language). */
10777
+ var PlanEditsOutcome;
10778
+ (function (PlanEditsOutcome) {
10779
+ PlanEditsOutcome["Edits"] = "edits";
10780
+ PlanEditsOutcome["Cannot"] = "cannot";
10781
+ })(PlanEditsOutcome || (PlanEditsOutcome = {}));
10782
+ /** The answer's own keys, explained. */
10783
+ const PLAN_EDITS_ANSWER_SHAPE = {
10784
+ outcome: "'edits' | 'cannot' — cannot: the operations cannot do what the sentence asks",
10785
+ edits: 'edit[] — applied in order, all or none; empty when cannot',
10786
+ reason: "string — when cannot, ONE short line for the owner in their language (what to do instead); else ''",
10787
+ };
10788
+ const one = (value) => jsonOneOf([value]);
10789
+ /** The schema of each op's value types (what `PLAN_EDIT_SHAPE` names). */
10790
+ function opSchema(op) {
10791
+ const entry = PLAN_EDIT_SHAPE[op];
10792
+ const valueOf = (key) => {
10793
+ const text = entry[key] ?? entry[`${key}?`];
10794
+ if (key === 'op')
10795
+ return one(op);
10796
+ if (key === 'pattern')
10797
+ return jsonOneOf(sectionPatternIds());
10798
+ const head = text.split(' ')[0];
10799
+ const table = {
10800
+ page: pageJsonSchema, section: sectionJsonSchema, contentType: contentTypeJsonSchema, field: fieldJsonSchema,
10801
+ 'dataSource[]': () => jsonListOf(dataSourceJsonSchema()), integer: jsonInteger, string: jsonText,
10802
+ };
10803
+ if (text.startsWith('string | string[]'))
10804
+ return jsonAnyOf([jsonText(), jsonListOf(jsonText())]);
10805
+ return (table[head] ?? jsonText)();
10806
+ };
10807
+ return jsonShapedObject(entry, Object.fromEntries(shapeKeys(entry).map(key => [key, valueOf(key)])), op);
10808
+ }
10809
+ /** The format a sentence's answer is written in: the outcome, the edits (one `anyOf` branch per op), the reason. */
10810
+ function planEditsJsonSchema() {
10811
+ return jsonShapedObject(PLAN_EDITS_ANSWER_SHAPE, {
10812
+ outcome: jsonOneOf(Object.values(PlanEditsOutcome)),
10813
+ edits: jsonListOf(jsonAnyOf(OPS.map(op => opSchema(op)))),
10814
+ reason: jsonText(),
10815
+ }, 'planEdits');
10816
+ }
10817
+ /** A model's answer → edits (sections and content types off the wire); never repaired. */
10818
+ function planEditsFromWire(answer) {
10819
+ if (!isPlainObject(answer) || !Array.isArray(answer['edits']))
10820
+ return answer;
10821
+ const edits = answer['edits'].map(raw => {
10822
+ if (!isPlainObject(raw))
10823
+ return raw;
10824
+ const e = { ...raw };
10825
+ if (isPlainObject(e['page']) && Array.isArray(e['page']['sections'])) {
10826
+ e['page'] = { ...e['page'], sections: e['page']['sections'].map(sectionFromWire) };
10827
+ }
10828
+ if (isPlainObject(e['section']))
10829
+ e['section'] = sectionFromWire(e['section']);
10830
+ if (isPlainObject(e['contentType']))
10831
+ e['contentType'] = contentTypeFromWire(e['contentType']);
10832
+ return e;
10833
+ });
10834
+ return { ...answer, edits };
10835
+ }
10836
+
10837
+ /**
10838
+ * The plan stage's word checks (AI plan §3d / §3h, M1-3): every text a model WROTE into a plan — the tagline, page
10839
+ * names, section slots, content-type labels, sample values — scanned for the words the owner and their kind of
10840
+ * business must not use (`avoidWords`, the analysis's + the type's `words`), and for a fact the owner never gave
10841
+ * (an email, phone number, price or opening time that is not one of the owner's own facts). A finding is a refusal
10842
+ * with a path — the model rewrites; nothing here repairs a word. Pure.
10843
+ */
10844
+ var PlanWordIssueCode;
10845
+ (function (PlanWordIssueCode) {
10846
+ /** A word the owner (or their kind of business) must not use. */
10847
+ PlanWordIssueCode["AvoidWord"] = "avoid-word";
10848
+ /** Something that looks like a fact — an email, phone, price or time — that is not one of the owner's facts. */
10849
+ PlanWordIssueCode["InventedFact"] = "invented-fact";
10850
+ /** `business.facts` holds a fact the owner did not give, or not in the owner's words. */
10851
+ PlanWordIssueCode["FactNotTheOwners"] = "fact-not-the-owners";
10852
+ })(PlanWordIssueCode || (PlanWordIssueCode = {}));
10853
+ /** Every text in a plan a model writes (never ids, routes, field names or pattern ids). */
10854
+ function writtenTexts(plan) {
10855
+ const out = [];
10856
+ const add = (path, text) => { if (typeof text === 'string' && text.trim())
10857
+ out.push({ path, text }); };
10858
+ add('business.tagline', plan.business?.tagline);
10859
+ plan.pages?.forEach((page, p) => {
10860
+ add(`pages[${p}].name`, page.name);
10861
+ page.sections?.forEach((section, s) => {
10862
+ for (const [slot, text] of Object.entries(section.slots ?? {}))
10863
+ add(`pages[${p}].sections[${s}].slots.${slot}`, text);
10864
+ });
10865
+ });
10866
+ plan.contentTypes?.forEach((type, t) => {
10867
+ add(`contentTypes[${t}].label`, type.label);
10868
+ for (const [field, value] of Object.entries(type.seed ?? {})) {
10869
+ const values = Array.isArray(value) ? value : [value];
10870
+ values.forEach((v, k) => add(`contentTypes[${t}].seed.${field}${Array.isArray(value) ? `[${k}]` : ''}`, v));
10871
+ }
10872
+ });
10873
+ return out;
10874
+ }
10875
+ const HEBREW = /[֐-׿]/;
10876
+ /** Hebrew joins up to two one-letter words (and, the, in, to, from, that, as) to the next: "והטוב" holds "טוב". */
10877
+ const HEBREW_PREFIXES = '[והבלמשכ]{0,2}';
10878
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10879
+ const normalise = (s) => s.normalize('NFKC').toLowerCase();
10880
+ /** A word (or phrase) as a whole-word pattern; Hebrew allows its one-letter prefixes. */
10881
+ function wordPattern(word) {
10882
+ const w = escape(normalise(word.trim())).replace(/\s+/g, '\\s+');
10883
+ const prefix = HEBREW.test(word) ? HEBREW_PREFIXES : '';
10884
+ return new RegExp(`(?<![\\p{L}\\p{N}])${prefix}${w}(?![\\p{L}\\p{N}])`, 'u');
10885
+ }
10886
+ /** What a fact looks like when a model writes one — each judged against the owner's facts. */
10887
+ const FACT_SHAPES = [
10888
+ { kind: 'email', pattern: /[\p{L}\p{N}._%+-]+@[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+/gu, key: m => m.toLowerCase() },
10889
+ { kind: 'phone', pattern: /\+?\d[\d\s().-]{6,}\d/g, key: m => m.replace(/\D/g, '') },
10890
+ { kind: 'price', pattern: /(?:[$€£₪]\s?\d[\d,.]*|\d[\d,.]*\s?(?:[$€£₪]|ש"ח|NIS|USD|EUR|GBP))/gu, key: m => m.replace(/[^\d]/g, '') },
10891
+ { kind: 'time', pattern: /(?<!\d)\d{1,2}[:.]\d{2}(?!\d)/g, key: m => m.replace(/\D/g, '') },
10892
+ ];
10893
+ const MIN_PHONE_DIGITS = 8;
10894
+ /** "2019 – 2024" is two years, not a phone number. */
10895
+ const YEAR_RANGE = /^\s*\d{4}\s*[-–]\s*\d{4}\s*$/;
10896
+ function ownedKeys(facts) {
10897
+ const keys = new Set();
10898
+ for (const value of Object.values(facts)) {
10899
+ if (typeof value !== 'string')
10900
+ continue;
10901
+ for (const shape of FACT_SHAPES)
10902
+ for (const m of value.match(shape.pattern) ?? [])
10903
+ keys.add(`${shape.kind}:${shape.key(m)}`);
10904
+ }
10905
+ return keys;
10906
+ }
10907
+ /** The words to avoid and the facts never given, in every written text of the plan. */
10908
+ function planWordIssues(plan, ctx) {
10909
+ const issues = [];
10910
+ const avoid = [...new Set(ctx.avoidWords.map(w => w.trim()).filter(Boolean))].map(w => ({ word: w, pattern: wordPattern(w) }));
10911
+ const owned = ownedKeys(ctx.facts);
10912
+ for (const { path, text } of writtenTexts(plan)) {
10913
+ const words = normalise(text);
10914
+ for (const a of avoid)
10915
+ if (a.pattern.test(words))
10916
+ issues.push({ code: PlanWordIssueCode.AvoidWord, path, message: `uses "${a.word}" — a word this site must not use` });
10917
+ for (const shape of FACT_SHAPES) {
10918
+ for (const m of text.match(shape.pattern) ?? []) {
10919
+ const key = shape.key(m);
10920
+ if (shape.kind === 'phone' && (key.length < MIN_PHONE_DIGITS || YEAR_RANGE.test(m)))
10921
+ continue;
10922
+ if (!owned.has(`${shape.kind}:${key}`))
10923
+ issues.push({ code: PlanWordIssueCode.InventedFact, path, message: `"${m.trim()}" looks like a ${shape.kind} the owner never gave — leave it out` });
10924
+ }
10925
+ }
10926
+ }
10927
+ for (const [key, value] of Object.entries(plan.business?.facts ?? {})) {
10928
+ const own = ctx.facts[key];
10929
+ if (!FACT_KEYS.includes(key))
10930
+ continue;
10931
+ if (typeof own !== 'string' || normalise(own).trim() !== normalise(String(value)).trim()) {
10932
+ issues.push({ code: PlanWordIssueCode.FactNotTheOwners, path: `business.facts.${key}`, message: own ? `must be the owner's own words: "${own}"` : 'the owner did not give it — leave it out (it becomes a placeholder)' });
10933
+ }
10934
+ }
10935
+ return issues;
10936
+ }
10937
+
10938
+ /**
10939
+ * THE Publishable gate (AI plan §3e, M1-7): ONE pure function over a snapshot of a site — its pages, globals, the
10940
+ * connectors' health, the look's token rows, what a build left to fill — run by the Publish dialog, the builder's
10941
+ * Needs attention and the server on publish (iox-cms refuses with the same findings). Works on any site, generated
10942
+ * or hand-built. Severity is decision D8: an unfilled fact, a policy placeholder or an action that points at nothing
10943
+ * BLOCKS; samples, copy placeholders and stock photos ASK ("Publish anyway"); the rest WARN. Design advice
10944
+ * (harness-core `lintDesign`) is not one of the eight — the caller adds it, and it never blocks.
10945
+ */
10946
+ var SiteCheckId;
10947
+ (function (SiteCheckId) {
10948
+ SiteCheckId["Navigation"] = "navigation";
10949
+ SiteCheckId["Forms"] = "forms";
10950
+ SiteCheckId["Consent"] = "consent";
10951
+ SiteCheckId["Seo"] = "seo";
10952
+ SiteCheckId["Facts"] = "facts";
10953
+ SiteCheckId["Accessibility"] = "accessibility";
10954
+ SiteCheckId["Tokens"] = "tokens";
10955
+ SiteCheckId["Actions"] = "actions";
10956
+ })(SiteCheckId || (SiteCheckId = {}));
10957
+ /** The eight, in §3e's order. */
10958
+ const SITE_CHECKS = Object.values(SiteCheckId);
10959
+ var CheckSeverity;
10960
+ (function (CheckSeverity) {
10961
+ CheckSeverity["Block"] = "block";
10962
+ CheckSeverity["Ask"] = "ask";
10963
+ CheckSeverity["Warn"] = "warn";
10964
+ })(CheckSeverity || (CheckSeverity = {}));
10965
+ var SiteFindingCode;
10966
+ (function (SiteFindingCode) {
10967
+ SiteFindingCode["NoHeader"] = "no-header";
10968
+ SiteFindingCode["NoPhoneMenu"] = "no-phone-menu";
10969
+ SiteFindingCode["FormWithoutId"] = "form-without-id";
10970
+ SiteFindingCode["EmailNeedsMailer"] = "email-needs-mailer";
10971
+ SiteFindingCode["TrackerWithoutBanner"] = "tracker-without-banner";
10972
+ SiteFindingCode["NoTitle"] = "no-title";
10973
+ SiteFindingCode["NoDescription"] = "no-description";
10974
+ SiteFindingCode["UnfilledFact"] = "unfilled-fact";
10975
+ SiteFindingCode["CopyPlaceholder"] = "copy-placeholder";
10976
+ SiteFindingCode["SampleItems"] = "sample-items";
10977
+ SiteFindingCode["StockPhoto"] = "stock-photo";
10978
+ SiteFindingCode["NoPrivacyPage"] = "no-privacy-page";
10979
+ SiteFindingCode["PrivacyPagePlaceholder"] = "privacy-page-placeholder";
10980
+ SiteFindingCode["LowContrast"] = "low-contrast";
10981
+ SiteFindingCode["HeadingOrder"] = "heading-order";
10982
+ SiteFindingCode["ImageWithoutAlt"] = "image-without-alt";
10983
+ SiteFindingCode["LiteralColour"] = "literal-colour";
10984
+ SiteFindingCode["DeadLink"] = "dead-link";
10985
+ SiteFindingCode["DeadDialogTarget"] = "dead-dialog-target";
10986
+ SiteFindingCode["ActionNeedsConnector"] = "action-needs-connector";
10987
+ })(SiteFindingCode || (SiteFindingCode = {}));
10988
+ /** Each finding's severity — one table (D8), never decided at a call site. */
10989
+ const FINDING_SEVERITY = {
10990
+ [SiteFindingCode.NoHeader]: CheckSeverity.Warn,
10991
+ [SiteFindingCode.NoPhoneMenu]: CheckSeverity.Warn,
10992
+ [SiteFindingCode.FormWithoutId]: CheckSeverity.Block,
10993
+ [SiteFindingCode.EmailNeedsMailer]: CheckSeverity.Warn,
10994
+ [SiteFindingCode.TrackerWithoutBanner]: CheckSeverity.Warn,
10995
+ [SiteFindingCode.NoTitle]: CheckSeverity.Warn,
10996
+ [SiteFindingCode.NoDescription]: CheckSeverity.Warn,
10997
+ [SiteFindingCode.UnfilledFact]: CheckSeverity.Block,
10998
+ [SiteFindingCode.CopyPlaceholder]: CheckSeverity.Ask,
10999
+ [SiteFindingCode.SampleItems]: CheckSeverity.Ask,
11000
+ [SiteFindingCode.StockPhoto]: CheckSeverity.Ask,
11001
+ [SiteFindingCode.NoPrivacyPage]: CheckSeverity.Ask,
11002
+ [SiteFindingCode.PrivacyPagePlaceholder]: CheckSeverity.Block,
11003
+ [SiteFindingCode.LowContrast]: CheckSeverity.Warn,
11004
+ [SiteFindingCode.HeadingOrder]: CheckSeverity.Warn,
11005
+ [SiteFindingCode.ImageWithoutAlt]: CheckSeverity.Warn,
11006
+ [SiteFindingCode.LiteralColour]: CheckSeverity.Warn,
11007
+ [SiteFindingCode.DeadLink]: CheckSeverity.Block,
11008
+ [SiteFindingCode.DeadDialogTarget]: CheckSeverity.Block,
11009
+ [SiteFindingCode.ActionNeedsConnector]: CheckSeverity.Block,
11010
+ };
11011
+ const CHECK_OF = {
11012
+ [SiteFindingCode.NoHeader]: SiteCheckId.Navigation,
11013
+ [SiteFindingCode.NoPhoneMenu]: SiteCheckId.Navigation,
11014
+ [SiteFindingCode.FormWithoutId]: SiteCheckId.Forms,
11015
+ [SiteFindingCode.EmailNeedsMailer]: SiteCheckId.Forms,
11016
+ [SiteFindingCode.TrackerWithoutBanner]: SiteCheckId.Consent,
11017
+ [SiteFindingCode.NoTitle]: SiteCheckId.Seo,
11018
+ [SiteFindingCode.NoDescription]: SiteCheckId.Seo,
11019
+ [SiteFindingCode.UnfilledFact]: SiteCheckId.Facts,
11020
+ [SiteFindingCode.CopyPlaceholder]: SiteCheckId.Facts,
11021
+ [SiteFindingCode.SampleItems]: SiteCheckId.Facts,
11022
+ [SiteFindingCode.StockPhoto]: SiteCheckId.Facts,
11023
+ [SiteFindingCode.NoPrivacyPage]: SiteCheckId.Facts,
11024
+ [SiteFindingCode.PrivacyPagePlaceholder]: SiteCheckId.Facts,
11025
+ [SiteFindingCode.LowContrast]: SiteCheckId.Accessibility,
11026
+ [SiteFindingCode.HeadingOrder]: SiteCheckId.Accessibility,
11027
+ [SiteFindingCode.ImageWithoutAlt]: SiteCheckId.Accessibility,
11028
+ [SiteFindingCode.LiteralColour]: SiteCheckId.Tokens,
11029
+ [SiteFindingCode.DeadLink]: SiteCheckId.Actions,
11030
+ [SiteFindingCode.DeadDialogTarget]: SiteCheckId.Actions,
11031
+ [SiteFindingCode.ActionNeedsConnector]: SiteCheckId.Actions,
11032
+ };
11033
+ const LINKING_TYPES = new Set(['Link', 'Heading', 'Button', 'ButtonBlock', 'LinkedContainer', 'Image']);
11034
+ const PAGE_LINK = 'page';
11035
+ const PERSONAL_CONTROLS = new Set([ControlType.Email, ControlType.Tel, ControlType.Text, ControlType.Textarea]);
11036
+ const PRIVACY_PAGE = /privacy|פרטיות|מדיניות/i;
11037
+ const MAILER = 'mailer';
11038
+ const EMAIL_DELIVERY = 'email';
11039
+ const LITERAL_COLOUR = /#[0-9a-f]{3,8}\b|rgba?\(|hsla?\(/i;
11040
+ const COLOUR_TRAITS = /color|Color|^background$|boxShadow|textShadow/;
11041
+ const HEADING_LEVEL = /^h([1-6])$/;
11042
+ const DIALOG_TYPE = 'Dialog';
11043
+ const SELF = 'self';
11044
+ /** A route pattern (`/work/:slug`) matches a concrete path (`/work/blue-bowl`). */
11045
+ function routeMatches(pattern, path) {
11046
+ const a = pattern.split('/').filter(Boolean);
11047
+ const b = String(path ?? '').split(/[?#]/)[0].split('/').filter(Boolean);
11048
+ return a.length === b.length && a.every((seg, i) => seg.startsWith(':') || seg === b[i]);
11049
+ }
11050
+ const nonEmpty = (v) => typeof v === 'string' && v.trim() !== '';
11051
+ /** A stored SEO field (a string, or `{ override, binding, auto }`) has a value, or may be derived. */
11052
+ function seoField(raw, derivable) {
11053
+ if (nonEmpty(raw))
11054
+ return true;
11055
+ if (raw && typeof raw === 'object') {
11056
+ const v = raw;
11057
+ if (nonEmpty(v.override) || v.binding)
11058
+ return true;
11059
+ if (v.auto === false)
11060
+ return false;
11061
+ }
11062
+ return derivable;
11063
+ }
11064
+ function hasContent(layout, type) {
11065
+ let found = false;
11066
+ walkSerialized(layout, n => {
11067
+ if (n.type !== type || found)
11068
+ return;
11069
+ found = nonEmpty(n.props['content']) || n.bindings.some(b => b.prop === 'content');
11070
+ });
11071
+ return found;
11072
+ }
11073
+ function add(ctx, code, message, where = {}) {
11074
+ ctx.out.push({ check: CHECK_OF[code], severity: FINDING_SEVERITY[code], code, message, ...where });
11075
+ }
11076
+ /** Every node of a page or a global, with where it lives. */
11077
+ function eachNode(snapshot, pages, visit) {
11078
+ for (const page of pages)
11079
+ walkSerialized(page.layout, n => visit(n, { pageId: page.id }));
11080
+ for (const global of snapshot.globals) {
11081
+ const owner = global.globalPosition === PatternGlobalPosition.Before ? 'header' : 'footer';
11082
+ walkSerialized([global], n => visit(n, { owner }));
11083
+ }
11084
+ }
11085
+ function checkNavigation(ctx, snapshot) {
11086
+ const header = snapshot.globals.find(g => g.globalPosition === PatternGlobalPosition.Before);
11087
+ if (!header)
11088
+ return add(ctx, SiteFindingCode.NoHeader, 'The site has no header, so visitors have no menu to move around with.');
11089
+ if (!navShapeOf(snapshot.globals))
11090
+ add(ctx, SiteFindingCode.NoPhoneMenu, 'The header has no phone menu — on a phone its links are hard to reach.', { owner: 'header', nodeId: header.id });
11091
+ }
11092
+ function checkForms(ctx, snapshot, pages) {
11093
+ let personal = false;
11094
+ eachNode(snapshot, pages, (n, where) => {
11095
+ if (n.type !== FORM_NODE_TYPE)
11096
+ return;
11097
+ const spec = collectFormSpec(n);
11098
+ if (!spec || !nonEmpty(spec.formId))
11099
+ add(ctx, SiteFindingCode.FormWithoutId, 'A form has no id, so what visitors send is never stored.', { ...where, nodeId: n.id });
11100
+ if (spec?.actions.some(a => a.type === EMAIL_DELIVERY) && snapshot.connectors[MAILER] !== true) {
11101
+ add(ctx, SiteFindingCode.EmailNeedsMailer, 'A form emails what visitors send, but Email is not connected — it is stored and never sent.', { ...where, nodeId: n.id });
11102
+ }
11103
+ walkSerialized(n.children ?? [], f => {
11104
+ if (f.type === FIELD_NODE_TYPE && PERSONAL_CONTROLS.has(String(f.props['control'] ?? ControlType.Text)))
11105
+ personal = true;
11106
+ });
11107
+ });
11108
+ return personal;
11109
+ }
11110
+ function checkConsent(ctx, snapshot) {
11111
+ if (!snapshot.trackers?.length)
11112
+ return;
11113
+ let banner = false;
11114
+ for (const g of snapshot.globals)
11115
+ walkSerialized([g], n => { if (n.type === DIALOG_TYPE && n.props['consentRole'] === true)
11116
+ banner = true; });
11117
+ if (!banner)
11118
+ add(ctx, SiteFindingCode.TrackerWithoutBanner, `${snapshot.trackers.join(', ')} is connected, but the site has no cookie banner — it stays off for every visitor.`);
11119
+ }
11120
+ function checkSeo(ctx, pages) {
11121
+ for (const page of pages) {
11122
+ const seo = (page.seo ?? {});
11123
+ if (!seoField(seo['title'], hasContent(page.layout, 'Heading')))
11124
+ add(ctx, SiteFindingCode.NoTitle, `"${page.name}" has no title for search results.`, { pageId: page.id });
11125
+ if (!seoField(seo['description'], hasContent(page.layout, 'Text')))
11126
+ add(ctx, SiteFindingCode.NoDescription, `"${page.name}" has no description for search results.`, { pageId: page.id });
11127
+ }
11128
+ }
11129
+ function checkFacts(ctx, snapshot, pages, personal) {
11130
+ const inScope = (p) => !p.pageId || pages.some(page => page.id === p.pageId);
11131
+ for (const p of (snapshot.placeholders ?? []).filter(p => !p.filled && inScope(p))) {
11132
+ const where = { pageId: p.pageId, nodeId: p.nodeId, ...(p.pageId ? {} : { owner: p.owner }) };
11133
+ if (blocksPublish(p.kind))
11134
+ add(ctx, SiteFindingCode.UnfilledFact, `"${p.text || p.fact || p.kind}" is still blank — fill it in before the site goes live.`, where);
11135
+ else if (p.kind === PlaceholderKind.Copy)
11136
+ add(ctx, SiteFindingCode.CopyPlaceholder, 'Some words are still the template\'s.', where);
11137
+ }
11138
+ for (const s of snapshot.samples ?? [])
11139
+ add(ctx, SiteFindingCode.SampleItems, `"${s.label ?? s.id}" is a sample — replace or remove it.`, { nodeId: s.id });
11140
+ for (const s of (snapshot.stock ?? []).filter(inScope))
11141
+ add(ctx, SiteFindingCode.StockPhoto, 'A stock photo stands in for one of yours.', { pageId: s.pageId, nodeId: s.nodeId, owner: s.owner });
11142
+ if (!personal)
11143
+ return;
11144
+ const privacy = snapshot.pages.find(p => PRIVACY_PAGE.test(p.route) || PRIVACY_PAGE.test(p.name));
11145
+ if (!privacy)
11146
+ return add(ctx, SiteFindingCode.NoPrivacyPage, 'A form asks for personal details, and the site has no privacy page saying what you do with them.');
11147
+ if ((snapshot.placeholders ?? []).some(p => p.pageId === privacy.id && !p.filled)) {
11148
+ add(ctx, SiteFindingCode.PrivacyPagePlaceholder, 'The privacy page still holds template words — only you can write it.', { pageId: privacy.id });
11149
+ }
11150
+ }
11151
+ function checkAccessibility(ctx, snapshot, pages) {
11152
+ for (const mode of snapshot.design?.modes ?? []) {
11153
+ const failing = auditDesignContrast(snapshot.design.rows, mode).filter(f => !f.pass);
11154
+ if (failing.length)
11155
+ add(ctx, SiteFindingCode.LowContrast, `${failing.length} text and background pair(s) are hard to read in ${mode} mode.`);
11156
+ }
11157
+ for (const page of pages) {
11158
+ let last = 0;
11159
+ let h1 = 0;
11160
+ walkSerialized(page.layout, n => {
11161
+ const level = n.type === 'Heading' ? HEADING_LEVEL.exec(String(n.props['level'] ?? ''))?.[1] : undefined;
11162
+ if (!level)
11163
+ return;
11164
+ const l = Number(level);
11165
+ if (l === 1)
11166
+ h1++;
11167
+ if (last && l > last + 1)
11168
+ add(ctx, SiteFindingCode.HeadingOrder, `A heading on "${page.name}" skips a level (h${last} → h${l}).`, { pageId: page.id, nodeId: n.id });
11169
+ last = l;
11170
+ });
11171
+ if (h1 > 1)
11172
+ add(ctx, SiteFindingCode.HeadingOrder, `"${page.name}" has ${h1} main headings (h1); one is clearer.`, { pageId: page.id });
11173
+ }
11174
+ eachNode(snapshot, pages, (n, where) => {
11175
+ if (n.type !== 'Image')
11176
+ return;
11177
+ const decorative = n.props['alt'] === '' && n.props['decorative'] === true;
11178
+ const bound = n.bindings.some(b => b.prop === 'alt');
11179
+ if (!decorative && !bound && !nonEmpty(n.props['alt']))
11180
+ add(ctx, SiteFindingCode.ImageWithoutAlt, 'A picture has no description for people who cannot see it.', { ...where, nodeId: n.id });
11181
+ });
11182
+ }
11183
+ function checkTokens(ctx, snapshot, pages) {
11184
+ eachNode(snapshot, pages, (n, where) => {
11185
+ if (!n.elementStyle)
11186
+ return;
11187
+ const maps = [n.styleProps, ...Object.values(n.stateStyles ?? {}), ...Object.values(n.responsive ?? {}).map(l => l?.styleProps)];
11188
+ const literal = maps.some(map => Object.entries(map ?? {}).some(([k, v]) => COLOUR_TRAITS.test(k) && LITERAL_COLOUR.test(String(v)) && !(k === 'borderColor' && n.styleProps['borderWidth'] === '0px')));
11189
+ if (literal)
11190
+ add(ctx, SiteFindingCode.LiteralColour, 'An element paints a fixed colour, so a new look will not reach it.', { ...where, nodeId: n.id });
11191
+ });
11192
+ }
11193
+ function checkActions(ctx, snapshot, pages) {
11194
+ const routeExists = (route) => typeof route === 'string' && ctx.routes.some(r => routeMatches(r, route));
11195
+ eachNode(snapshot, pages, (n, where) => {
11196
+ if (LINKING_TYPES.has(n.type) && n.props['linkType'] === PAGE_LINK && !routeExists(n.props['pageRoute'])) {
11197
+ add(ctx, SiteFindingCode.DeadLink, `"${String(n.props['label'] ?? n.props['content'] ?? n.label ?? n.type)}" links to ${String(n.props['pageRoute'] || 'no page')}, which is not a page of the site.`, { ...where, nodeId: n.id });
11198
+ }
11199
+ for (const ix of (n.interactions ?? [])) {
11200
+ for (const action of ix?.actions ?? []) {
11201
+ if (!action || action.disabled)
11202
+ continue;
11203
+ const descriptor = ELEMENT_ACTION_DESCRIPTORS.find(d => d.type === action.type);
11204
+ if (!descriptor)
11205
+ continue;
11206
+ const types = descriptor.target?.types;
11207
+ if (descriptor.target?.required && action.target !== SELF) {
11208
+ const targetType = action.target ? ctx.nodeIds.get(action.target) : undefined;
11209
+ if (!targetType || (types && !types.includes(targetType))) {
11210
+ add(ctx, SiteFindingCode.DeadDialogTarget, `"${n.label ?? n.type}" opens something that is no longer on the site.`, { ...where, nodeId: n.id });
11211
+ }
11212
+ }
11213
+ if (action.type === 'navigate' && action.params?.['navigateTo'] === PAGE_LINK && !routeExists(action.params?.['page'])) {
11214
+ add(ctx, SiteFindingCode.DeadLink, `"${n.label ?? n.type}" goes to a page the site does not have.`, { ...where, nodeId: n.id });
11215
+ }
11216
+ // A connector the snapshot does not say is usable is not: unknown is never "fine".
11217
+ const connectors = descriptor.connector ? { [descriptor.connector]: snapshot.connectors[descriptor.connector] === true } : {};
11218
+ const availability = resolveActionAvailability(descriptor, { connectors });
11219
+ if (availability.state === 'connector-missing') {
11220
+ add(ctx, SiteFindingCode.ActionNeedsConnector, `"${n.label ?? n.type}" needs ${availability.connector} connected.`, { ...where, nodeId: n.id });
11221
+ }
11222
+ }
11223
+ }
11224
+ });
11225
+ }
11226
+ /** Run the eight checks. `pageId` narrows the page checks to the page being published; site-wide ones always run. */
11227
+ function auditSite(snapshot, options = {}) {
11228
+ const pages = options.pageId ? snapshot.pages.filter(p => p.id === options.pageId) : snapshot.pages;
11229
+ const nodeIds = new Map();
11230
+ eachNode(snapshot, snapshot.pages, n => nodeIds.set(n.id, n.type));
11231
+ const ctx = { out: [], routes: snapshot.pages.map(p => p.route), nodeIds };
11232
+ checkNavigation(ctx, snapshot);
11233
+ const personal = checkForms(ctx, snapshot, pages);
11234
+ checkConsent(ctx, snapshot);
11235
+ checkSeo(ctx, pages);
11236
+ checkFacts(ctx, snapshot, pages, personal);
11237
+ checkAccessibility(ctx, snapshot, pages);
11238
+ checkTokens(ctx, snapshot, pages);
11239
+ checkActions(ctx, snapshot, pages);
11240
+ const checks = SITE_CHECKS.map(id => {
11241
+ const findings = ctx.out.filter(f => f.check === id);
11242
+ return { id, passed: !findings.some(f => f.severity !== CheckSeverity.Warn), findings };
11243
+ });
11244
+ return {
11245
+ checks,
11246
+ blocking: ctx.out.filter(f => f.severity === CheckSeverity.Block).length,
11247
+ asking: ctx.out.filter(f => f.severity === CheckSeverity.Ask).length,
11248
+ };
11249
+ }
11250
+ /** What a publish request must acknowledge: every ASK finding's code (a BLOCK cannot be acknowledged). */
11251
+ function requiredAcknowledgements(audit) {
11252
+ return [...new Set(audit.checks.flatMap(c => c.findings).filter(f => f.severity === CheckSeverity.Ask).map(f => f.code))];
11253
+ }
11254
+ /** The publish verdict: blocked (never acknowledgeable), needs these acknowledgements, or free to go. */
11255
+ function publishVerdict(audit, acknowledged = []) {
11256
+ const missing = requiredAcknowledgements(audit).filter(code => !acknowledged.includes(code));
11257
+ const blocked = audit.blocking > 0;
11258
+ return { ok: !blocked && missing.length === 0, blocked, missing };
11259
+ }
11260
+
11261
+ /**
11262
+ * Stock photos in a built site (AI plan §3g / §3m, I2): a FREE picture from the platform's curated library, copied
11263
+ * into the studio's own storage (never hot-linked), placed only in an EMPTY picture slot of a section that may show
11264
+ * one — never the owner's work (a collection, a detail page, a gallery: what the nine portfolio types sell is theirs),
11265
+ * never a person (testimonials, the team), never anywhere when the business type says `stock: false`. Its alt text is
11266
+ * the provider's description of what it shows (never a claim), and the credit line its licence requires is placed in
11267
+ * the footer. Pure: the build step does the copying.
11268
+ */
11269
+ /** Where a stock photo comes from (the library's provider). */
11270
+ var StockProviderId;
11271
+ (function (StockProviderId) {
11272
+ StockProviderId["Magnific"] = "magnific";
11273
+ })(StockProviderId || (StockProviderId = {}));
11274
+ /** The section families a stock photo may stand in for a picture in. */
11275
+ const STOCK_FAMILIES = new Set(['hero', 'feature', 'content', 'story', 'cta', 'contact', 'header']);
11276
+ /** The empty picture slots of a composed site a stock photo may fill, in page and section order. */
11277
+ function stockSlots(plan, composed) {
11278
+ const empty = new Set(composed.placeholders.filter(p => p.kind === PlaceholderKind.Image).map(p => p.nodeId));
11279
+ const out = [];
11280
+ composed.pages.forEach((page, p) => {
11281
+ page.layout.forEach((root, s) => {
11282
+ const pattern = plan.pages[p]?.sections[s]?.pattern;
11283
+ const family = patternById(pattern)?.family;
11284
+ if (!pattern || !family || !STOCK_FAMILIES.has(family))
11285
+ return;
11286
+ walkSerialized([root], n => {
11287
+ if (n.type === 'Image' && empty.has(n.id) && !n.bindings.length)
11288
+ out.push({ nodeId: n.id, owner: page.key, pattern });
11289
+ });
11290
+ });
11291
+ });
11292
+ return out;
11293
+ }
11294
+ /** Which photo goes where: the library's photos in order, one per slot, never twice; none when the type says no stock. */
11295
+ function planStockPhotos(slots, photos, stockAllowed) {
11296
+ if (!stockAllowed)
11297
+ return [];
11298
+ return slots.slice(0, photos.length).map((slot, i) => ({ ...slot, photo: photos[i] }));
11299
+ }
11300
+ /** A placed photo's copy in the studio's storage: the key the build writes it under. */
11301
+ function stockCopyKey(photo) {
11302
+ return `media/stock/${photo.key.replace(/[^a-zA-Z0-9._-]+/g, '-')}`;
11303
+ }
11304
+ /** Put the studio's copy of each photo into its slot (src + the provider's alt). Returns the nodes changed. */
11305
+ function applyStockPhotos(trees, placed) {
11306
+ const byId = new Map(placed.map(p => [p.nodeId, p]));
11307
+ let changed = 0;
11308
+ walkSerialized(trees, n => {
11309
+ const p = byId.get(n.id);
11310
+ if (!p || n.type !== 'Image')
11311
+ return;
11312
+ n.props['src'] = p.url;
11313
+ n.props['alt'] = p.alt;
11314
+ changed++;
11315
+ });
11316
+ return changed;
11317
+ }
11318
+ /** The credit lines a set of photos needs — one per distinct line (a free Magnific photo: "Designed by Magnific"). */
11319
+ function stockCreditLines(photos) {
11320
+ const seen = new Map();
11321
+ for (const p of photos)
11322
+ if (!seen.has(p.credit.line))
11323
+ seen.set(p.credit.line, p.credit.lineUrl);
11324
+ return [...seen.entries()].map(([line, url]) => ({ line, url }));
11325
+ }
11326
+ /** The footer node id of a credit line (stable — a build that runs twice finds it). */
11327
+ const stockCreditNodeId = (index) => `stock-credit-${index + 1}`;
11328
+ /**
11329
+ * The credit line, as the builder would create a Link (a plain node the owner may move or restyle): the licence's words,
11330
+ * linking where it asks, opening in a new tab. Placed at the end of the footer — the place the licence names for a site.
11331
+ */
11332
+ function stockCreditNode(index, credit) {
11333
+ return createSerializedNode(stockCreditNodeId(index), 'Link', { label: credit.line, linkType: 'external', url: credit.url, target: '_blank' }, 'link');
11334
+ }
11335
+ /** Append the credit lines to the footer root (once each). Returns how many it added. */
11336
+ function addStockCredits(footer, credits) {
11337
+ footer.children ??= [];
11338
+ let added = 0;
11339
+ credits.forEach((credit, i) => {
11340
+ const id = stockCreditNodeId(i);
11341
+ if (footer.children.some(c => c.id === id))
11342
+ return;
11343
+ footer.children.push(stockCreditNode(i, credit));
11344
+ added++;
11345
+ });
11346
+ return added;
11347
+ }
11348
+
10125
11349
  /**
10126
11350
  * @vectoriox/iox-ui/generation-core — the shared, FRAMEWORK-FREE site-plan core (design-system plan
10127
11351
  * §6b, W1): the Site Plan shape + its validator, the pattern library and the Studio template (both
@@ -10138,5 +11362,5 @@ var AiCallStatus;
10138
11362
  * Generated bundle index. Do not edit.
10139
11363
  */
10140
11364
 
10141
- export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_NEEDS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiGenerationStage, AiKeyMode, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CREDITS_PER_USD, CardBindTarget, ComposeError, ComposeIssueCode, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, FACT_KEYS, FINAL_GENERATION_STAGES, GlobalSlot, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, PreflightRefusalCode, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, costUsd, createSerializedNode, creditsOfUsd, describePlanData, entranceAction, entranceInteraction, estimateStepCredits, failedBuildStep, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, pathAt, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, phoneMenuPages, planFromTemplate, planNavShape, releasedLayoutDefaults, removeBarItem, requiredShapeKeys, routeParams, sampleImageRef, sectionEntrances, setStyleProps, shapeKeys, siteNavPages, staggerDelay, validateSitePlan, walkSerialized };
11365
+ export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiGenerationStage, AiKeyMode, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CREDITS_PER_USD, CardBindTarget, CheckSeverity, ComposeError, ComposeIssueCode, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, FACT_KEYS, FINAL_GENERATION_STAGES, FINDING_SEVERITY, GlobalSlot, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PLAN_EDITS_ANSWER_SHAPE, PLAN_EDITS_SCHEMA_NAME, PLAN_EDIT_SHAPE, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanChangeAction, PlanChangeKind, PlanDataSourceMode, PlanDataSourceType, PlanEditIssueCode, PlanEditOp, PlanEditsOutcome, PlanWordIssueCode, PreflightRefusalCode, RelativeOp, SAMPLE_IMAGE_REF, SITE_CHECKS, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_PATTERNS, SITE_PLAN_SCHEMA_NAME, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STOCK_FAMILIES, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SitePlanWireKey, SiteTemplateError, SiteTemplateId, StepState, StockProviderId, addBarItem, addStockCredits, altSourceScopes, applyPlanEdits, applyStockPhotos, assertStyleSlots, assertValidSitePlan, auditSite, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, contentTypeFromWire, contentTypeJsonSchema, costUsd, createSerializedNode, creditsOfUsd, dataSourceJsonSchema, describePlanData, describeShapeKey, entranceAction, entranceInteraction, estimateStepCredits, failedBuildStep, fieldJsonSchema, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, jsonAnyOf, jsonBoolean, jsonClosedObject, jsonInteger, jsonListOf, jsonOneOf, jsonSchemaKeywords, jsonSchemaObjects, jsonShapedObject, jsonText, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, pageJsonSchema, parseSampleImageRef, pathAt, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, phoneMenuPages, planEditsFromWire, planEditsJsonSchema, planFromTemplate, planNavShape, planStockPhotos, planWordIssues, publishVerdict, releasedLayoutDefaults, removeBarItem, requiredAcknowledgements, requiredShapeKeys, routeMatches, routeParams, sampleImageRef, sectionEntrances, sectionFromWire, sectionJsonSchema, sectionPatternIds, setStyleProps, shapeKeys, siteNavPages, sitePlanFromWire, sitePlanJsonSchema, sitePlanToWire, staggerDelay, stockCopyKey, stockCreditLines, stockCreditNode, stockCreditNodeId, stockSlots, validatePlanEdits, validateSitePlan, walkSerialized, wordPattern, writtenTexts };
10142
11366
  //# sourceMappingURL=vectoriox-iox-ui-generation-core.mjs.map