@elytracms/studio 0.0.84 → 0.0.86

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.
Files changed (73) hide show
  1. package/convex/delivery.ts +21 -0
  2. package/convex/engine-version.test.ts +31 -0
  3. package/dist/components/ai/ai-chat-panel.js +13 -2
  4. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  5. package/dist/components/builder/blocks-model.d.ts +5 -4
  6. package/dist/components/builder/blocks-model.js +5 -4
  7. package/dist/components/builder/blocks-model.js.map +1 -1
  8. package/dist/components/builder/clipboard-actions.d.ts +6 -0
  9. package/dist/components/builder/clipboard-actions.js +15 -1
  10. package/dist/components/builder/clipboard-actions.js.map +1 -1
  11. package/dist/components/builder/composition-field-editor.d.ts +13 -0
  12. package/dist/components/builder/composition-field-editor.js +19 -4
  13. package/dist/components/builder/composition-field-editor.js.map +1 -1
  14. package/dist/components/builder/insert-context.js +6 -4
  15. package/dist/components/builder/insert-context.js.map +1 -1
  16. package/dist/components/cms/cell-edit.d.ts +7 -0
  17. package/dist/components/cms/cell-edit.js +9 -1
  18. package/dist/components/cms/cell-edit.js.map +1 -1
  19. package/dist/components/cms/collection-table.js +11 -0
  20. package/dist/components/cms/collection-table.js.map +1 -1
  21. package/dist/components/cms/document-url-path.d.ts +7 -4
  22. package/dist/components/cms/document-url-path.js +15 -1
  23. package/dist/components/cms/document-url-path.js.map +1 -1
  24. package/dist/components/cms/field-control.js +4 -0
  25. package/dist/components/cms/field-control.js.map +1 -1
  26. package/dist/components/cms/inline-field-errors.d.ts +36 -0
  27. package/dist/components/cms/inline-field-errors.js +13 -5
  28. package/dist/components/cms/inline-field-errors.js.map +1 -1
  29. package/dist/components/cms/rich-text/insert.d.ts +6 -3
  30. package/dist/components/cms/rich-text/insert.js +9 -6
  31. package/dist/components/cms/rich-text/insert.js.map +1 -1
  32. package/dist/components/cms/use-live-url.d.ts +18 -6
  33. package/dist/components/cms/use-live-url.js +10 -0
  34. package/dist/components/cms/use-live-url.js.map +1 -1
  35. package/dist/components/components/transfer.d.ts +6 -5
  36. package/dist/components/components/transfer.js +7 -6
  37. package/dist/components/components/transfer.js.map +1 -1
  38. package/dist/components/deployment-setup-gate.js +9 -3
  39. package/dist/components/deployment-setup-gate.js.map +1 -1
  40. package/dist/lib/ai/constraints.js +54 -71
  41. package/dist/lib/ai/constraints.js.map +1 -1
  42. package/dist/lib/ai/index.d.ts +1 -0
  43. package/dist/lib/ai/index.js +1 -0
  44. package/dist/lib/ai/index.js.map +1 -1
  45. package/dist/lib/ai/route-fetch.d.ts +58 -0
  46. package/dist/lib/ai/route-fetch.js +98 -0
  47. package/dist/lib/ai/route-fetch.js.map +1 -0
  48. package/dist/lib/ai/route-provider.d.ts +5 -22
  49. package/dist/lib/ai/route-provider.js +23 -1
  50. package/dist/lib/ai/route-provider.js.map +1 -1
  51. package/dist/lib/clipboard/paste-target.d.ts +23 -1
  52. package/dist/lib/clipboard/paste-target.js +12 -11
  53. package/dist/lib/clipboard/paste-target.js.map +1 -1
  54. package/dist/lib/cms/document-gate.d.ts +19 -9
  55. package/dist/lib/cms/document-gate.js +25 -4
  56. package/dist/lib/cms/document-gate.js.map +1 -1
  57. package/dist/lib/engine-version.d.ts +16 -0
  58. package/dist/lib/engine-version.js +17 -0
  59. package/dist/lib/engine-version.js.map +1 -0
  60. package/dist/lib/i18n/messages.de.js +4 -0
  61. package/dist/lib/i18n/messages.de.js.map +1 -1
  62. package/dist/lib/i18n/messages.en.d.ts +4 -0
  63. package/dist/lib/i18n/messages.en.js +7 -0
  64. package/dist/lib/i18n/messages.en.js.map +1 -1
  65. package/dist/lib/operations/studio-operations.js +4 -2
  66. package/dist/lib/operations/studio-operations.js.map +1 -1
  67. package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
  68. package/dist/lib/persistence/deployment-readiness.js +4 -0
  69. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  70. package/dist/previews/manifest-format.d.ts +7 -7
  71. package/dist/previews/manifest-format.js +0 -21
  72. package/dist/previews/manifest-format.js.map +1 -1
  73. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"transfer.js","sourceRoot":"","sources":["../../../src/components/components/transfer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAExF,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAE1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAoBnD;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CACnC,WAAqC;IAErC,MAAM,OAAO,GAA6B,EAAE,CAAA;IAC5C,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QACnD,IAAI,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;IAChD,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAqB,EAAE,WAAmB;IAC3E,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AACtF,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,cAAc,CAAC,IAAmB,EAAE,aAAuC;IAClF,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACxF,OAAO,aAAa,CAAC,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAAA;AAC/D,CAAC;AAcD;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,UAAU,EACV,WAAW,EACX,IAAI,EACJ,WAAW,EACX,GAAG,GACqB;IACxB,MAAM,EAAE,GAAG,cAAc,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;IAC1D,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAA;IACvC,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IACzC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IACvE,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAA;AAClD,CAAC;AAED,kFAAkF;AAClF,SAAS,eAAe,CACtB,UAAyB,EACzB,QAAqB,EACrB,WAA2B,EAC3B,MAAc;IAEd,OAAO,gBAAgB,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,CAAC;QACnD,CAAC,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC;QAClD,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;AACvC,CAAC;AAUD;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAoB;IACzF,IAAI,WAAW,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IACrC,OAAO,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAA;AAC9F,CAAC;AAaD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,UAAU,EACV,QAAQ,EACR,WAAW,EACX,IAAI,EACJ,MAAM,GACqB;IAC3B,MAAM,aAAa,GAAG,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAC9F,IAAI,CAAC,WAAW,CAAC,QAAQ,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CACb,8CAA8C,WAAW,CAAC,IAAI,mEAAmE,CAClI,CAAA;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,EAAE,aAAa,CAAC,CAAA;IACpD,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAChF,OAAO,cAAc,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { isBlocksField, isFieldLocalized, seedDefaults } from '@elytracms/core/cms-core'\nimport type { BlocksFieldDef, CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core'\nimport { blockRootsOf, maxNodeIdSeq } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { makeNodeIdCounter } from '../builder/builder-store'\nimport { applyCellValue } from '../cms/cell-edit'\nimport { mintDocumentId } from '../cms/cms-store'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — the \"design guide → real content\" bridge. A\n * component combination the user has arranged in the Components view (a\n * manifest + edited prop values, captured as a {@link ComponentNode}) is\n * transferred into a document's blocks/composition field — either a NEW DRAFT\n * document created for it ({@link buildBlockDocument}) or an EXISTING\n * document's field, appended alongside whatever it already holds\n * ({@link appendToBlockDocument}). Pure so id-minting/embedding/appending are\n * unit-testable; the React dialog owns the picker, persistence, and\n * navigation.\n */\n\nexport interface CollectionBlocksTarget {\n collection: CollectionDef\n /** The (first) blocks field the node lands in. */\n field: BlocksFieldDef\n}\n\n/**\n * Collections that can receive a transferred combination — those declaring a\n * `blocks` (composition) field. The first blocks field wins (a collection with a\n * single canvas is the norm; a second is an unusual edge we don't disambiguate).\n * Callers additionally gate each target through {@link isComponentAllowed} —\n * this only reports which collections HAVE a blocks field, not which ones\n * accept a given component.\n */\nexport function collectionsWithBlocks(\n collections: readonly CollectionDef[],\n): CollectionBlocksTarget[] {\n const targets: CollectionBlocksTarget[] = []\n for (const collection of collections) {\n const field = collection.fields.find(isBlocksField)\n if (field) targets.push({ collection, field })\n }\n return targets\n}\n\n/**\n * Is `componentId` permitted as a top-level block of `field`? Mirrors\n * `validateCompositionValue`'s (`@elytracms/core/project-graph`) top-level\n * `allow` rule EXACTLY: an absent or empty list permits any registered\n * component; a non-empty list is the closed vocabulary. A target this\n * predicate accepts can never fail that validator's `forbidden-component`\n * check on save — the transfer dialog uses this to hide targets the\n * component isn't allowed into (new AND existing document targets alike).\n */\nexport function isComponentAllowed(field: BlocksFieldDef, componentId: string): boolean {\n return !field.allow || field.allow.length === 0 || field.allow.includes(componentId)\n}\n\n/**\n * Mint a fresh `node-<n>` id for `node`, guaranteed not to collide with any id\n * already present anywhere in `existingRoots`. Reuses the SAME mint the\n * canvas reducer and clipboard-paste path share (`maxNodeIdSeq` +\n * `makeNodeIdCounter`, `builder-store.tsx`) rather than a fixed literal: a\n * brand-new document (`existingRoots` empty) mints `node-1` — matching the\n * transfer's prior behavior — but appending into a document whose blocks\n * already hold `node-1..N` mints past that, never re-using an id already in\n * the tree (the EC-320 follow-up collision hazard).\n */\nfunction embedFreshNode(node: ComponentNode, existingRoots: readonly ComponentNode[]): ComponentNode {\n const maxSeq = existingRoots.reduce((max, root) => Math.max(max, maxNodeIdSeq(root)), 0)\n return embedNodeFrom(node, makeNodeIdCounter(maxSeq).genId())\n}\n\nexport interface BuildBlockDocumentInput {\n collection: CollectionDef\n /** The blocks field the node lands in (from {@link collectionsWithBlocks}). */\n blocksField: BlocksFieldDef\n /** The component node — a manifest id + the edited static props (+ slots). */\n node: ComponentNode\n /** Every doc id already in the target collection (collision guard, EC-316). */\n existingIds: ReadonlySet<string>\n /** The session id counter (mutated by {@link mintDocumentId}). */\n seq: { current: number }\n}\n\n/**\n * Build the draft document that embeds `node` in the collection's blocks field.\n * Reuses the CMS create machinery: {@link mintDocumentId} for a collision-free id\n * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a\n * normal `+ New` would be). A `multiple` blocks field stores an array; a single\n * (`multiple: false`) blocks field stores the lone node — matching how the\n * composition editor persists and `blockRootsOf` reads it back.\n */\nexport function buildBlockDocument({\n collection,\n blocksField,\n node,\n existingIds,\n seq,\n}: BuildBlockDocumentInput): CmsDocument {\n const id = mintDocumentId(collection.id, seq, existingIds)\n const values = seedDefaults(collection)\n const embedded = embedFreshNode(node, [])\n values[blocksField.name] = blocksField.multiple ? [embedded] : embedded\n return { collection: collection.id, id, values }\n}\n\n/** The blocks field's current value on `document`, respecting locale (EC-018). */\nfunction readBlocksValue(\n collection: CollectionDef,\n document: CmsDocument,\n blocksField: BlocksFieldDef,\n locale: Locale,\n): unknown {\n return isFieldLocalized(collection, blocksField.name)\n ? document.localized?.[locale]?.[blocksField.name]\n : document.values[blocksField.name]\n}\n\nexport interface CanAppendToInput {\n collection: CollectionDef\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Can `node` be appended to `document`'s blocks field without destroying\n * existing content? Always true for a `multiple` field (an ordered list has\n * room for one more); a single-root (`multiple: false`) field only while it\n * is empty — occupied, there is no second root to hold it without\n * overwriting the existing block, so the transfer dialog excludes that\n * document from its offered targets rather than risk data loss.\n */\nexport function canAppendTo({ collection, document, blocksField, locale }: CanAppendToInput): boolean {\n if (blocksField.multiple) return true\n return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length === 0\n}\n\nexport interface AppendToBlockDocumentInput {\n collection: CollectionDef\n /** The document to append into (the transfer dialog's \"insert into existing document\"). */\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The component node to append — not yet re-ided (fresh id minted here). */\n node: ComponentNode\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Append `node` into an EXISTING document's blocks field (EC-320 follow-up,\n * 0.0.44 — \"insert into existing document\"), leaving everything else about\n * the document untouched: other fields, other locale bundles, and the\n * field's own existing content, which the embed always lands AFTER.\n *\n * Reads the current roots via `blockRootsOf` (`@elytracms/core/project-graph`\n * — the same reader the composition editor, table cell, and reuse-context\n * use), so this reads the field back exactly like every other consumer,\n * undefined-safe for a never-set field. Locale-aware writes go through\n * {@link applyCellValue} (the same locale-write helper inline cell edits use,\n * `cell-edit.ts`) so a localized blocks field's OTHER locale bundles are\n * never touched, only the edited locale's.\n *\n * Node-id collision guard: `node` is re-embedded under a fresh id minted past\n * every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`\n * — a document whose blocks already hold that id would otherwise collide.\n *\n * Throws if `blocksField` is a single-root (`multiple: false`) field that\n * already holds a root — a defensive invariant guard, not a user-facing\n * validation path: the caller must pre-filter candidates with\n * {@link canAppendTo} so this is never reached in that state.\n */\nexport function appendToBlockDocument({\n collection,\n document,\n blocksField,\n node,\n locale,\n}: AppendToBlockDocumentInput): CmsDocument {\n const existingRoots = blockRootsOf(readBlocksValue(collection, document, blocksField, locale))\n if (!blocksField.multiple && existingRoots.length > 0) {\n throw new Error(\n `Cannot append to single-root blocks field \"${blocksField.name}\": already holds a component (caller must gate with canAppendTo).`,\n )\n }\n const embedded = embedFreshNode(node, existingRoots)\n const nextValue = blocksField.multiple ? [...existingRoots, embedded] : embedded\n return applyCellValue(collection, document, blocksField.name, locale, nextValue)\n}\n"]}
1
+ {"version":3,"file":"transfer.js","sourceRoot":"","sources":["../../../src/components/components/transfer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isBlocksField, isFieldLocalized, seedDefaults } from '@elytracms/core/cms-core'\nimport type { BlocksFieldDef, CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core'\nimport { blockRootsOf, maxNodeIdSeq } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { makeNodeIdCounter } from '../builder/builder-store'\nimport { applyCellValue } from '../cms/cell-edit'\nimport { mintDocumentId } from '../cms/cms-store'\nimport { embedNodeFrom } from './component-preview'\n\n/**\n * EC-320 (+ 0.0.44 follow-up) — the \"design guide → real content\" bridge. A\n * component combination the user has arranged in the Components view (a\n * manifest + edited prop values, captured as a {@link ComponentNode}) is\n * transferred into a document's blocks/composition field — either a NEW DRAFT\n * document created for it ({@link buildBlockDocument}) or an EXISTING\n * document's field, appended alongside whatever it already holds\n * ({@link appendToBlockDocument}). Pure so id-minting/embedding/appending are\n * unit-testable; the React dialog owns the picker, persistence, and\n * navigation.\n */\n\nexport interface CollectionBlocksTarget {\n collection: CollectionDef\n /** The (first) blocks field the node lands in. */\n field: BlocksFieldDef\n}\n\n/**\n * Collections that can receive a transferred combination — those declaring a\n * `blocks` (composition) field. The first blocks field wins (a collection with a\n * single canvas is the norm; a second is an unusual edge we don't disambiguate).\n * Callers additionally gate each target through {@link isComponentAllowed} —\n * this only reports which collections HAVE a blocks field, not which ones\n * accept a given component.\n */\nexport function collectionsWithBlocks(\n collections: readonly CollectionDef[],\n): CollectionBlocksTarget[] {\n const targets: CollectionBlocksTarget[] = []\n for (const collection of collections) {\n const field = collection.fields.find(isBlocksField)\n if (field) targets.push({ collection, field })\n }\n return targets\n}\n\n/**\n * Is `componentId` permitted as a top-level block of `field`? Mirrors\n * `validateCompositionValue`'s (`@elytracms/core/project-graph`) top-level\n * `allow` rule EXACTLY (EC-544): an absent list permits any registered\n * component; a list is the closed vocabulary (an empty one permits nothing).\n * A target this predicate accepts can never fail that validator's\n * `forbidden-component` check on save — the transfer dialog uses this to hide\n * targets the component isn't allowed into (new AND existing document targets\n * alike).\n */\nexport function isComponentAllowed(field: BlocksFieldDef, componentId: string): boolean {\n return !field.allow || field.allow.includes(componentId)\n}\n\n/**\n * Mint a fresh `node-<n>` id for `node`, guaranteed not to collide with any id\n * already present anywhere in `existingRoots`. Reuses the SAME mint the\n * canvas reducer and clipboard-paste path share (`maxNodeIdSeq` +\n * `makeNodeIdCounter`, `builder-store.tsx`) rather than a fixed literal: a\n * brand-new document (`existingRoots` empty) mints `node-1` — matching the\n * transfer's prior behavior — but appending into a document whose blocks\n * already hold `node-1..N` mints past that, never re-using an id already in\n * the tree (the EC-320 follow-up collision hazard).\n */\nfunction embedFreshNode(node: ComponentNode, existingRoots: readonly ComponentNode[]): ComponentNode {\n const maxSeq = existingRoots.reduce((max, root) => Math.max(max, maxNodeIdSeq(root)), 0)\n return embedNodeFrom(node, makeNodeIdCounter(maxSeq).genId())\n}\n\nexport interface BuildBlockDocumentInput {\n collection: CollectionDef\n /** The blocks field the node lands in (from {@link collectionsWithBlocks}). */\n blocksField: BlocksFieldDef\n /** The component node — a manifest id + the edited static props (+ slots). */\n node: ComponentNode\n /** Every doc id already in the target collection (collision guard, EC-316). */\n existingIds: ReadonlySet<string>\n /** The session id counter (mutated by {@link mintDocumentId}). */\n seq: { current: number }\n}\n\n/**\n * Build the draft document that embeds `node` in the collection's blocks field.\n * Reuses the CMS create machinery: {@link mintDocumentId} for a collision-free id\n * (EC-316) and {@link seedDefaults} so declared field defaults are present (as a\n * normal `+ New` would be). A `multiple` blocks field stores an array; a single\n * (`multiple: false`) blocks field stores the lone node — matching how the\n * composition editor persists and `blockRootsOf` reads it back.\n */\nexport function buildBlockDocument({\n collection,\n blocksField,\n node,\n existingIds,\n seq,\n}: BuildBlockDocumentInput): CmsDocument {\n const id = mintDocumentId(collection.id, seq, existingIds)\n const values = seedDefaults(collection)\n const embedded = embedFreshNode(node, [])\n values[blocksField.name] = blocksField.multiple ? [embedded] : embedded\n return { collection: collection.id, id, values }\n}\n\n/** The blocks field's current value on `document`, respecting locale (EC-018). */\nfunction readBlocksValue(\n collection: CollectionDef,\n document: CmsDocument,\n blocksField: BlocksFieldDef,\n locale: Locale,\n): unknown {\n return isFieldLocalized(collection, blocksField.name)\n ? document.localized?.[locale]?.[blocksField.name]\n : document.values[blocksField.name]\n}\n\nexport interface CanAppendToInput {\n collection: CollectionDef\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Can `node` be appended to `document`'s blocks field without destroying\n * existing content? Always true for a `multiple` field (an ordered list has\n * room for one more); a single-root (`multiple: false`) field only while it\n * is empty — occupied, there is no second root to hold it without\n * overwriting the existing block, so the transfer dialog excludes that\n * document from its offered targets rather than risk data loss.\n */\nexport function canAppendTo({ collection, document, blocksField, locale }: CanAppendToInput): boolean {\n if (blocksField.multiple) return true\n return blockRootsOf(readBlocksValue(collection, document, blocksField, locale)).length === 0\n}\n\nexport interface AppendToBlockDocumentInput {\n collection: CollectionDef\n /** The document to append into (the transfer dialog's \"insert into existing document\"). */\n document: CmsDocument\n blocksField: BlocksFieldDef\n /** The component node to append — not yet re-ided (fresh id minted here). */\n node: ComponentNode\n /** The locale being edited (EC-176 ambient active locale) for a localized field. */\n locale: Locale\n}\n\n/**\n * Append `node` into an EXISTING document's blocks field (EC-320 follow-up,\n * 0.0.44 — \"insert into existing document\"), leaving everything else about\n * the document untouched: other fields, other locale bundles, and the\n * field's own existing content, which the embed always lands AFTER.\n *\n * Reads the current roots via `blockRootsOf` (`@elytracms/core/project-graph`\n * — the same reader the composition editor, table cell, and reuse-context\n * use), so this reads the field back exactly like every other consumer,\n * undefined-safe for a never-set field. Locale-aware writes go through\n * {@link applyCellValue} (the same locale-write helper inline cell edits use,\n * `cell-edit.ts`) so a localized blocks field's OTHER locale bundles are\n * never touched, only the edited locale's.\n *\n * Node-id collision guard: `node` is re-embedded under a fresh id minted past\n * every existing root's ids ({@link embedFreshNode}), never a fixed `node-1`\n * — a document whose blocks already hold that id would otherwise collide.\n *\n * Throws if `blocksField` is a single-root (`multiple: false`) field that\n * already holds a root — a defensive invariant guard, not a user-facing\n * validation path: the caller must pre-filter candidates with\n * {@link canAppendTo} so this is never reached in that state.\n */\nexport function appendToBlockDocument({\n collection,\n document,\n blocksField,\n node,\n locale,\n}: AppendToBlockDocumentInput): CmsDocument {\n const existingRoots = blockRootsOf(readBlocksValue(collection, document, blocksField, locale))\n if (!blocksField.multiple && existingRoots.length > 0) {\n throw new Error(\n `Cannot append to single-root blocks field \"${blocksField.name}\": already holds a component (caller must gate with canAppendTo).`,\n )\n }\n const embedded = embedFreshNode(node, existingRoots)\n const nextValue = blocksField.multiple ? [...existingRoots, embedded] : embedded\n return applyCellValue(collection, document, blocksField.name, locale, nextValue)\n}\n"]}
@@ -5,6 +5,7 @@ import { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } f
5
5
  import { resolveBackendConfig, selectPersistenceAdapter, studioDeploymentReadiness, } from '@elytracms/studio/lib/persistence';
6
6
  import { getWorkspaceState, resolveDeploymentAi, resolveDeploymentCollections, resolveDeploymentUrl, } from '@elytracms/studio/lib/workspace';
7
7
  import { translate, useHydrated, useT } from '@elytracms/studio/lib/i18n';
8
+ import { probeDeployedFunctionsVersion } from '@elytracms/core/export-sync/deployment-ready';
8
9
  /**
9
10
  * Deployment-setup boot gate (EC-258) — the single readiness surface that
10
11
  * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)
@@ -44,7 +45,7 @@ export function DeploymentSetupGate({ children }) {
44
45
  return;
45
46
  }
46
47
  const effectiveUrl = resolveDeploymentUrl(config.url);
47
- const resolve = (reachable) => setState({
48
+ const resolve = (reachable, functionsVersion) => setState({
48
49
  phase: 'resolved',
49
50
  report: studioDeploymentReadiness({
50
51
  backend: config,
@@ -52,14 +53,19 @@ export function DeploymentSetupGate({ children }) {
52
53
  workspace: getWorkspaceState(),
53
54
  collections: resolveDeploymentCollections(),
54
55
  reachable,
56
+ functionsVersion,
55
57
  aiRouteConfigured,
56
58
  }),
57
59
  });
58
60
  setState({ phase: 'checking' });
61
+ // EC-552: the version handshake probes in parallel with the reachability check —
62
+ // same deployment URL, one extra public query. The probe never rejects (network
63
+ // failures resolve to an honest `unknown`), so awaiting it in both paths is safe.
64
+ const versionProbe = probeDeployedFunctionsVersion(effectiveUrl);
59
65
  void selectPersistenceAdapter()
60
66
  .then((adapter) => adapter.status())
61
- .then((status) => resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }))
62
- .catch((error) => resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }));
67
+ .then(async (status) => resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }, await versionProbe))
68
+ .catch(async (error) => resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }, await versionProbe));
63
69
  }, [config, staticReport, aiRouteConfigured]);
64
70
  useEffect(() => {
65
71
  if (config.kind === 'convex')
@@ -1 +1 @@
1
- {"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Loader2 } from 'lucide-react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '@elytracms/studio/lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentAi,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '@elytracms/studio/lib/workspace'\nimport { translate, useHydrated, useT } from '@elytracms/studio/lib/i18n'\nimport type {\n DeploymentReadinessReport,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n // EC-399: a configured (and not disabled) `defineStudio({ ai })` block means the\n // AI route exists — its provider-key expectation joins the deployment-env checks.\n const aiRouteConfigured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return Boolean(ai && ai.enabled !== false)\n }, [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n aiRouteConfigured,\n }),\n [config, aiRouteConfigured],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (reachable: { available: boolean; detail?: string }) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n aiRouteConfigured,\n }),\n })\n setState({ phase: 'checking' })\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then((status) =>\n resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }),\n )\n .catch((error: unknown) =>\n resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }),\n )\n }, [config, staticReport, aiRouteConfigured])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment-, deployment-env-, or app-level\n * precondition. The per-user bucket (\"not a member\") is not a boot blocker — it is\n * surfaced post-auth (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n * Deployment-env checks are `unknown` in the browser today (nothing gathered — EC-399),\n * so they never block in practice; `warn` findings never block anywhere.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) =>\n c.status === 'fail' &&\n (c.bucket === 'deployment' || c.bucket === 'deployment-env' || c.bucket === 'app'),\n )\n}\n\n/**\n * Deterministic boot state while the backend probe is in flight — and, in a\n * CONNECTED build, the body of the prerendered shell itself.\n *\n * **EC-525.** EC-523 removed the last localized string from the shell and proved it\n * against the built artefact — but against the *playground* build, which is the demo's\n * default and the one nobody deploys. Every real studio is connected, so\n * `config.kind === 'convex'` seeds `{ phase: 'checking' }` and *this* is what a\n * production cold load actually bakes: `Checking deployment…`, in the build machine's\n * locale, shown to every visitor whatever theirs is. The class was not closed until\n * both builds satisfied it.\n *\n * Same shape as `SessionLoading` (`router/root.tsx`), for the same reasons, which are\n * written out there: a build-time prerender has no request and therefore no locale, so\n * the shell shows a spinner and an EMPTY `role=\"status\"` region, and `useHydrated`\n * fills it in the visitor's real language once hydration lands. No `aria-label` — that\n * bakes the wrong-language string somewhere a screen-reader user cannot see it flip —\n * and no `aria-busy`, which would suppress the announcement this shape exists to make.\n *\n * Exported so the connected shell's pre-hydration markup is pinnable with\n * `renderToStaticMarkup` — same reason `SessionLoading` is. The built artefact is the\n * real proof; the pin is what fails when someone puts a string back.\n */\nexport function DeploymentChecking() {\n const hydrated = useHydrated()\n const t = useT()\n return (\n <BootScreenFrame>\n <div role=\"status\">\n <Loader2 className=\"size-6 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n {hydrated ? <span className=\"sr-only\">{t('shell.deploymentSetup.checking')}</span> : null}\n </div>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n const envChecks = report.checks.filter((c) => c.bucket === 'deployment-env')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {envChecks.length > 0 ? <ExpectedEnvSection checks={envChecks} /> : null}\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\n/**\n * The expected-env facts (EC-399) — the SAME contract `elytra doctor` checks, so the\n * studio and CLI tell one story. Presence only: each row is a var name + set/missing/\n * unknown status text; values never exist in the report. In the browser the presence\n * sources aren't readable pre-auth, so rows typically read `unknown` and point at\n * `elytra doctor` — which is exactly the honest answer.\n */\nfunction ExpectedEnvSection({ checks }: { checks: ReadinessCheck[] }) {\n return (\n <Stack gap=\"2\">\n <Text size=\"sm\" className=\"font-medium\">\n {translate('shell.deploymentSetup.envSection')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.deploymentSetup.envSectionHint')}\n </Text>\n <Stack gap=\"1\" role=\"list\" aria-label={translate('shell.deploymentSetup.envSection')}>\n {checks.map((check) => (\n <EnvCheckRow key={check.id} check={check} />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nconst ENV_STATUS_GLYPH: Record<ReadinessCheck['status'], string> = {\n ok: '✓',\n warn: '!',\n fail: '✗',\n unknown: '?',\n}\n\nfunction EnvCheckRow({ check }: { check: ReadinessCheck }) {\n const failing = check.status === 'fail'\n return (\n <div\n role=\"listitem\"\n className={`rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`}\n >\n <Text size=\"xs\" mono className={failing ? 'text-destructive' : undefined}>\n {ENV_STATUS_GLYPH[check.status]} [{check.id}]\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {check.detail}\n </Text>\n {check.fix && (check.status === 'fail' || check.status === 'warn') ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
1
+ {"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Loader2 } from 'lucide-react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '@elytracms/studio/lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentAi,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '@elytracms/studio/lib/workspace'\nimport { translate, useHydrated, useT } from '@elytracms/studio/lib/i18n'\nimport { probeDeployedFunctionsVersion } from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeploymentReadinessReport,\n FunctionsVersionProbe,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n // EC-399: a configured (and not disabled) `defineStudio({ ai })` block means the\n // AI route exists — its provider-key expectation joins the deployment-env checks.\n const aiRouteConfigured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return Boolean(ai && ai.enabled !== false)\n }, [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n aiRouteConfigured,\n }),\n [config, aiRouteConfigured],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (\n reachable: { available: boolean; detail?: string },\n functionsVersion: FunctionsVersionProbe,\n ) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n functionsVersion,\n aiRouteConfigured,\n }),\n })\n setState({ phase: 'checking' })\n // EC-552: the version handshake probes in parallel with the reachability check —\n // same deployment URL, one extra public query. The probe never rejects (network\n // failures resolve to an honest `unknown`), so awaiting it in both paths is safe.\n const versionProbe = probeDeployedFunctionsVersion(effectiveUrl)\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then(async (status) =>\n resolve(\n { available: status.available, ...(status.detail ? { detail: status.detail } : {}) },\n await versionProbe,\n ),\n )\n .catch(async (error: unknown) =>\n resolve(\n { available: false, detail: error instanceof Error ? error.message : String(error) },\n await versionProbe,\n ),\n )\n }, [config, staticReport, aiRouteConfigured])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment-, deployment-env-, or app-level\n * precondition. The per-user bucket (\"not a member\") is not a boot blocker — it is\n * surfaced post-auth (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n * Deployment-env checks are `unknown` in the browser today (nothing gathered — EC-399),\n * so they never block in practice; `warn` findings never block anywhere.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) =>\n c.status === 'fail' &&\n (c.bucket === 'deployment' || c.bucket === 'deployment-env' || c.bucket === 'app'),\n )\n}\n\n/**\n * Deterministic boot state while the backend probe is in flight — and, in a\n * CONNECTED build, the body of the prerendered shell itself.\n *\n * **EC-525.** EC-523 removed the last localized string from the shell and proved it\n * against the built artefact — but against the *playground* build, which is the demo's\n * default and the one nobody deploys. Every real studio is connected, so\n * `config.kind === 'convex'` seeds `{ phase: 'checking' }` and *this* is what a\n * production cold load actually bakes: `Checking deployment…`, in the build machine's\n * locale, shown to every visitor whatever theirs is. The class was not closed until\n * both builds satisfied it.\n *\n * Same shape as `SessionLoading` (`router/root.tsx`), for the same reasons, which are\n * written out there: a build-time prerender has no request and therefore no locale, so\n * the shell shows a spinner and an EMPTY `role=\"status\"` region, and `useHydrated`\n * fills it in the visitor's real language once hydration lands. No `aria-label` — that\n * bakes the wrong-language string somewhere a screen-reader user cannot see it flip —\n * and no `aria-busy`, which would suppress the announcement this shape exists to make.\n *\n * Exported so the connected shell's pre-hydration markup is pinnable with\n * `renderToStaticMarkup` — same reason `SessionLoading` is. The built artefact is the\n * real proof; the pin is what fails when someone puts a string back.\n */\nexport function DeploymentChecking() {\n const hydrated = useHydrated()\n const t = useT()\n return (\n <BootScreenFrame>\n <div role=\"status\">\n <Loader2 className=\"size-6 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n {hydrated ? <span className=\"sr-only\">{t('shell.deploymentSetup.checking')}</span> : null}\n </div>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n const envChecks = report.checks.filter((c) => c.bucket === 'deployment-env')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {envChecks.length > 0 ? <ExpectedEnvSection checks={envChecks} /> : null}\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\n/**\n * The expected-env facts (EC-399) — the SAME contract `elytra doctor` checks, so the\n * studio and CLI tell one story. Presence only: each row is a var name + set/missing/\n * unknown status text; values never exist in the report. In the browser the presence\n * sources aren't readable pre-auth, so rows typically read `unknown` and point at\n * `elytra doctor` — which is exactly the honest answer.\n */\nfunction ExpectedEnvSection({ checks }: { checks: ReadinessCheck[] }) {\n return (\n <Stack gap=\"2\">\n <Text size=\"sm\" className=\"font-medium\">\n {translate('shell.deploymentSetup.envSection')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.deploymentSetup.envSectionHint')}\n </Text>\n <Stack gap=\"1\" role=\"list\" aria-label={translate('shell.deploymentSetup.envSection')}>\n {checks.map((check) => (\n <EnvCheckRow key={check.id} check={check} />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nconst ENV_STATUS_GLYPH: Record<ReadinessCheck['status'], string> = {\n ok: '✓',\n warn: '!',\n fail: '✗',\n unknown: '?',\n}\n\nfunction EnvCheckRow({ check }: { check: ReadinessCheck }) {\n const failing = check.status === 'fail'\n return (\n <div\n role=\"listitem\"\n className={`rounded-md border p-2 ${failing ? 'border-destructive/30 bg-destructive/5' : 'border-border bg-muted/30'}`}\n >\n <Text size=\"xs\" mono className={failing ? 'text-destructive' : undefined}>\n {ENV_STATUS_GLYPH[check.status]} [{check.id}]\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {check.detail}\n </Text>\n {check.fix && (check.status === 'fail' || check.status === 'warn') ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * The engine has ONE constraint projection: `fieldJsonSchema` merges a field's
5
5
  * declarative rules (`validation.min`/`max`/`pattern`) with the fragment derived
6
- * from a schema-form `validation.validate` (`validation.schema`). This module turns
6
+ * from a schema-form `validation.validate` (EC-522: nested `items`/`properties`
7
+ * bounds included, fitted to the field's type at config resolve). This module turns
7
8
  * that projection into what the MODEL should see — which is not the same artifact a
8
9
  * JSON-Schema consumer wants:
9
10
  *
@@ -110,29 +111,13 @@ export function stripRedundantPatternsDeep(schema) {
110
111
  export function toModelFacingSchema(schema) {
111
112
  return mapSchemaDeep(schema, (node) => narrowDateUnion(stripRedundantPattern(node)));
112
113
  }
113
- /** The constraint keywords this module renders as prose, for the "is anything left?" scan. */
114
- const RENDERED_KEYWORDS = [
115
- 'minLength',
116
- 'maxLength',
117
- 'minimum',
118
- 'maximum',
119
- 'minItems',
120
- 'maxItems',
121
- 'formatMinimum',
122
- 'formatMaximum',
123
- 'format',
124
- 'pattern',
125
- 'enum',
126
- 'const',
127
- 'multipleOf',
128
- 'exclusiveMinimum',
129
- 'exclusiveMaximum',
130
- 'uniqueItems',
131
- ];
132
114
  /**
133
115
  * Human-readable phrases for the constraint keywords of ONE schema node, in a stable
134
116
  * order — the form a model reads best. Node-level only: nested `items`/`properties`
135
- * are the caller's business (see {@link describeFieldConstraints}).
117
+ * are {@link describeSchemaDeep}'s business. Total over the projection's numeric,
118
+ * string and array vocabulary; `enum`/`const` are deliberately not phrased — they
119
+ * ride the target SHAPE as JSON Schema, and a `select`'s options or a relation's
120
+ * `collection` const would otherwise repeat on every catalog line.
136
121
  */
137
122
  function describeSchemaConstraints(schema) {
138
123
  const out = [];
@@ -155,12 +140,24 @@ function describeSchemaConstraints(schema) {
155
140
  out.push(`at least ${minimum}`);
156
141
  else if (maximum !== undefined)
157
142
  out.push(`at most ${maximum}`);
143
+ // EC-522: the projection carried these already; the prose never said them.
144
+ const exclusiveMinimum = num(schema.exclusiveMinimum);
145
+ const exclusiveMaximum = num(schema.exclusiveMaximum);
146
+ if (exclusiveMinimum !== undefined)
147
+ out.push(`more than ${exclusiveMinimum}`);
148
+ if (exclusiveMaximum !== undefined)
149
+ out.push(`less than ${exclusiveMaximum}`);
150
+ const multipleOf = num(schema.multipleOf);
151
+ if (multipleOf !== undefined)
152
+ out.push(`a multiple of ${multipleOf}`);
158
153
  const minItems = num(schema.minItems);
159
154
  const maxItems = num(schema.maxItems);
160
155
  if (minItems !== undefined)
161
156
  out.push(`at least ${minItems} items`);
162
157
  if (maxItems !== undefined)
163
158
  out.push(`at most ${maxItems} items`);
159
+ if (schema.uniqueItems === true)
160
+ out.push('no duplicate items');
164
161
  // EC-473: date bounds finally reach a consumer. `formatMinimum`/`formatMaximum`
165
162
  // is the projection's machine form; this is the form a model acts on.
166
163
  const from = text(schema.formatMinimum);
@@ -182,47 +179,41 @@ function describeSchemaConstraints(schema) {
182
179
  out.push(`matching /${pattern}/`);
183
180
  return out;
184
181
  }
185
- /** Does this fragment carry a constraint keyword BELOW its own top level? */
186
- function hasNestedConstraint(schema) {
187
- const scan = (node) => {
188
- if (typeof node !== 'object' || node === null)
189
- return false;
190
- if (Array.isArray(node))
191
- return node.some(scan);
192
- const record = node;
193
- if (RENDERED_KEYWORDS.some((keyword) => record[keyword] !== undefined))
194
- return true;
195
- return Object.values(record).some(scan);
196
- };
197
- return scan(schema.items) || scan(schema.properties);
182
+ function isSchemaNode(value) {
183
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
198
184
  }
199
185
  /**
200
- * The PER-ITEM constraint fragment of a `multiple` field, if the author declared one
201
- * (EC-505).
186
+ * The phrases of one projected schema node AND everything below it (EC-522): a
187
+ * `multiple` field's per-item bounds become an `each item: …` clause, an object's
188
+ * subfield bounds a `<name>: …` clause, recursively. The projection is the ONLY
189
+ * input — since EC-522 the merge carries nested `items`/`properties` constraints
190
+ * itself, so this module never looks past `fieldJsonSchema` (EC-505's direct read
191
+ * of the derived fragment's `items` is gone, and with it the second place that
192
+ * understood fragment structure).
202
193
  *
203
- * A schema in the `validate` slot addresses the field's WHOLE value — that is not a
204
- * choice this module makes, it is what the executor does: `validateDocument` calls
205
- * the validator once with the resolved field value, which for a `multiple` field is
206
- * the ARRAY (`validate-document.ts:1099`). The projection agrees with it, so a
207
- * per-ITEM schema written directly (`z.string().min(3)` on a multi-valued field) is
208
- * rejected loudly on the first save rather than silently mis-bounding anything.
209
- *
210
- * The per-item form is therefore `z.array(z.string().min(3))` — and THAT is what
211
- * needed fixing: the merged projection contributes CONSTRAINT keywords only, so the
212
- * derived fragment's nested `items` never reached the model even though the executor
213
- * enforces it. This reads that fragment directly, which is the one place this module
214
- * looks past `fieldJsonSchema` — see the EC-505 task file for the merge-side proposal.
194
+ * Which side a schema addresses is still the executor's call, not this module's:
195
+ * `validateDocument` hands the validator the field's WHOLE value — a `multiple`
196
+ * field's array — so the per-item form is `z.array(z.string().min(3))`, and a
197
+ * bare `z.string().min(3)` there is a config error at load (EC-522), never a
198
+ * silently mis-applied bound.
215
199
  */
216
- function perItemFragment(field) {
217
- if (!('multiple' in field) || field.multiple !== true)
218
- return undefined;
219
- const derived = field.validation?.schema;
220
- if (typeof derived !== 'object' || derived === null || Array.isArray(derived))
221
- return undefined;
222
- const items = derived.items;
223
- if (typeof items !== 'object' || items === null || Array.isArray(items))
224
- return undefined;
225
- return items;
200
+ function describeSchemaDeep(schema) {
201
+ const out = describeSchemaConstraints(schema);
202
+ if (isSchemaNode(schema.items)) {
203
+ const itemParts = describeSchemaDeep(schema.items);
204
+ if (itemParts.length > 0)
205
+ out.push(`each item: ${itemParts.join(', ')}`);
206
+ }
207
+ if (isSchemaNode(schema.properties)) {
208
+ for (const [name, sub] of Object.entries(schema.properties)) {
209
+ if (!isSchemaNode(sub))
210
+ continue;
211
+ const subParts = describeSchemaDeep(sub);
212
+ if (subParts.length > 0)
213
+ out.push(`${name}: ${subParts.join(', ')}`);
214
+ }
215
+ }
216
+ return out;
226
217
  }
227
218
  /**
228
219
  * Human-readable constraint phrases for one field, in a stable order — the form a
@@ -233,21 +224,13 @@ export function describeFieldConstraints(field) {
233
224
  const out = [];
234
225
  if (field.validation?.required === true)
235
226
  out.push('required');
236
- out.push(...describeSchemaConstraints(fieldJsonSchema(field)));
237
- let incomplete = field.validation?.schemaComplete === false;
238
- const items = perItemFragment(field);
239
- if (items !== undefined) {
240
- const itemParts = describeSchemaConstraints(items);
241
- if (itemParts.length > 0)
242
- out.push(`each item: ${itemParts.join(', ')}`);
243
- // A nested rule this vocabulary cannot phrase (a constraint inside an OBJECT
244
- // item, say) is declared partial rather than dropped in silence.
245
- if (hasNestedConstraint(items))
246
- incomplete = true;
247
- }
227
+ out.push(...describeSchemaDeep(fieldJsonSchema(field)));
248
228
  if (field.validation?.unique === true)
249
229
  out.push('unique across the collection');
250
- if (incomplete)
230
+ // The one thing the projection cannot say about itself: that it is partial (an
231
+ // opaque vendor, a `.refine()`, or — EC-522 — a rule at a position the field's
232
+ // shape has no counterpart for). Set at config resolve, read here, nowhere else.
233
+ if (field.validation?.schemaComplete === false)
251
234
  out.push(INCOMPLETE_CONSTRAINTS_NOTE);
252
235
  return out;
253
236
  }
@@ -1 +1 @@
1
- {"version":3,"file":"constraints.js","sourceRoot":"","sources":["../../../src/lib/ai/constraints.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model-facing constraint rendering (EC-473, tuned by EC-505).\n *\n * The engine has ONE constraint projection: `fieldJsonSchema` merges a field's\n * declarative rules (`validation.min`/`max`/`pattern`) with the fragment derived\n * from a schema-form `validation.validate` (`validation.schema`). This module turns\n * that projection into what the MODEL should see — which is not the same artifact a\n * JSON-Schema consumer wants:\n *\n * - **a regex is a poor instruction for a model.** `z.string().email()` emits a\n * ~200-character pattern and `z.iso.date()` a leap-year monster (measured,\n * EC-474 §3); `format` + bounds says the same thing in four tokens. So on the AI\n * path a `pattern` is dropped wherever a `format` is present. The JSON Schema\n * OUTPUT artifact keeps it — different consumer, different needs.\n * - **a union `type` is a poor instruction too** (EC-505). A `date` field accepts\n * an ISO string OR an epoch number *at rest*, so the general projection honestly\n * says `type: ['string','number']`. The model does not need that latitude: it is\n * asked to AUTHOR a date, EC-472 ratified that a date is an ISO instant, and the\n * declared bounds this same projection carries (`formatMinimum`/`formatMaximum`)\n * are ISO strings that mean nothing against an epoch. Some structured-output\n * providers also reject a union `type` outright. So the model-facing shape says\n * `type: 'string'`; accepting a number stays an input-parsing detail of the\n * validator, not a shape the model is taught.\n * - **incompleteness must be stated.** When `validation.schemaComplete` is `false`\n * the projection is a partial view of the rule, and the model is told so rather\n * than being handed a half-truth as the whole rule. (An `ai.hint` is exactly the\n * right place to say what the dropped refinement wants — the note nudges toward\n * writing one.)\n */\nimport { fieldJsonSchema } from '@elytracms/core/cms-core'\nimport type { FieldDef, JsonSchema } from '@elytracms/core/cms-core'\n\n/** The note appended when a field's constraint projection is knowingly partial. */\nexport const INCOMPLETE_CONSTRAINTS_NOTE =\n 'further validation rules apply that are not expressible here — keep the value conventional and plausible'\n\n/**\n * The prose form of `format: 'date-time'` (EC-505). The JSON-Schema keyword rides\n * on the target shape for the provider's validator; the CATALOG has no JSON Schema\n * at all — it is prose only, and the chat update path authors date values through\n * it. So the format is said in words too, or the only channel that path has says\n * nothing about the one format the engine accepts.\n *\n * Kept to three words deliberately: measured at 6 tokens, against 20 for the same\n * note with a worked example — and it rides every date field of every turn.\n */\nexport const ISO_DATE_NOTE = 'ISO 8601 date'\n\n/**\n * Drop `pattern` wherever `format` is present (EC-473/EC-474 req 6). Returns the\n * same object when there is nothing to strip, so callers can rely on identity.\n */\nexport function stripRedundantPattern(schema: JsonSchema): JsonSchema {\n if (schema.format === undefined || schema.pattern === undefined) return schema\n const { pattern: _pattern, ...rest } = schema\n return rest\n}\n\n/**\n * Narrow a date field's `['string','number']` union to the ISO string the model\n * should author (EC-505 — see the module note). Keyed on the exact combination the\n * date branch of the projection emits (`format: 'date-time'` + an array `type`\n * containing `string`), so nothing else is touched. Identity-preserving.\n */\nexport function narrowDateUnion(schema: JsonSchema): JsonSchema {\n if (schema.format !== 'date-time') return schema\n const type = schema.type\n if (!Array.isArray(type) || !type.includes('string')) return schema\n return { ...schema, type: 'string' }\n}\n\n/**\n * Apply a node transform through a schema tree (`items` + `properties`), preserving\n * object identity wherever nothing changed — `buildTargetShape` relies on that to\n * avoid copying a property it did not touch.\n */\nfunction mapSchemaDeep(schema: JsonSchema, transform: (node: JsonSchema) => JsonSchema): JsonSchema {\n let out = transform(schema)\n const items = out.items\n if (typeof items === 'object' && items !== null && !Array.isArray(items)) {\n const next = mapSchemaDeep(items as JsonSchema, transform)\n if (next !== items) out = { ...out, items: next }\n }\n const properties = out.properties\n if (typeof properties === 'object' && properties !== null && !Array.isArray(properties)) {\n let changed = false\n const nextProps: Record<string, JsonSchema> = {}\n for (const [key, value] of Object.entries(properties as Record<string, unknown>)) {\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const next = mapSchemaDeep(value as JsonSchema, transform)\n nextProps[key] = next\n if (next !== value) changed = true\n } else {\n nextProps[key] = value as JsonSchema\n }\n }\n if (changed) out = { ...out, properties: nextProps }\n }\n return out\n}\n\n/** Recursively strip redundant patterns through `items` / `properties`. */\nexport function stripRedundantPatternsDeep(schema: JsonSchema): JsonSchema {\n return mapSchemaDeep(schema, stripRedundantPattern)\n}\n\n/**\n * The whole model-facing rewrite of a projected schema (EC-505): redundant patterns\n * stripped and date unions narrowed, everywhere in the tree — an `object` field's\n * date SUBfield gets the same treatment as a top-level one. Identity-preserving.\n */\nexport function toModelFacingSchema(schema: JsonSchema): JsonSchema {\n return mapSchemaDeep(schema, (node) => narrowDateUnion(stripRedundantPattern(node)))\n}\n\n/** The constraint keywords this module renders as prose, for the \"is anything left?\" scan. */\nconst RENDERED_KEYWORDS = [\n 'minLength',\n 'maxLength',\n 'minimum',\n 'maximum',\n 'minItems',\n 'maxItems',\n 'formatMinimum',\n 'formatMaximum',\n 'format',\n 'pattern',\n 'enum',\n 'const',\n 'multipleOf',\n 'exclusiveMinimum',\n 'exclusiveMaximum',\n 'uniqueItems',\n] as const\n\n/**\n * Human-readable phrases for the constraint keywords of ONE schema node, in a stable\n * order — the form a model reads best. Node-level only: nested `items`/`properties`\n * are the caller's business (see {@link describeFieldConstraints}).\n */\nfunction describeSchemaConstraints(schema: JsonSchema): string[] {\n const out: string[] = []\n const num = (value: unknown): number | undefined =>\n typeof value === 'number' ? value : undefined\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' ? value : undefined\n\n const minLength = num(schema.minLength)\n const maxLength = num(schema.maxLength)\n if (minLength !== undefined && maxLength !== undefined) {\n out.push(`${minLength}–${maxLength} characters`)\n } else if (minLength !== undefined) out.push(`at least ${minLength} characters`)\n else if (maxLength !== undefined) out.push(`at most ${maxLength} characters`)\n\n const minimum = num(schema.minimum)\n const maximum = num(schema.maximum)\n if (minimum !== undefined && maximum !== undefined) out.push(`between ${minimum} and ${maximum}`)\n else if (minimum !== undefined) out.push(`at least ${minimum}`)\n else if (maximum !== undefined) out.push(`at most ${maximum}`)\n\n const minItems = num(schema.minItems)\n const maxItems = num(schema.maxItems)\n if (minItems !== undefined) out.push(`at least ${minItems} items`)\n if (maxItems !== undefined) out.push(`at most ${maxItems} items`)\n\n // EC-473: date bounds finally reach a consumer. `formatMinimum`/`formatMaximum`\n // is the projection's machine form; this is the form a model acts on.\n const from = text(schema.formatMinimum)\n const to = text(schema.formatMaximum)\n if (from !== undefined) out.push(`not earlier than ${from}`)\n if (to !== undefined) out.push(`not later than ${to}`)\n\n const format = text(schema.format)\n // EC-505: `date-time` is said in words rather than skipped — the catalog carries\n // no JSON Schema, so this is the only place the update path learns the format.\n if (format === 'date-time') out.push(ISO_DATE_NOTE)\n else if (format !== undefined) out.push(`format: ${format}`)\n // Only a pattern that is NOT explained by a format is worth the tokens.\n const pattern = text(schema.pattern)\n if (pattern !== undefined && format === undefined) out.push(`matching /${pattern}/`)\n\n return out\n}\n\n/** Does this fragment carry a constraint keyword BELOW its own top level? */\nfunction hasNestedConstraint(schema: JsonSchema): boolean {\n const scan = (node: unknown): boolean => {\n if (typeof node !== 'object' || node === null) return false\n if (Array.isArray(node)) return node.some(scan)\n const record = node as Record<string, unknown>\n if (RENDERED_KEYWORDS.some((keyword) => record[keyword] !== undefined)) return true\n return Object.values(record).some(scan)\n }\n return scan(schema.items) || scan(schema.properties)\n}\n\n/**\n * The PER-ITEM constraint fragment of a `multiple` field, if the author declared one\n * (EC-505).\n *\n * A schema in the `validate` slot addresses the field's WHOLE value — that is not a\n * choice this module makes, it is what the executor does: `validateDocument` calls\n * the validator once with the resolved field value, which for a `multiple` field is\n * the ARRAY (`validate-document.ts:1099`). The projection agrees with it, so a\n * per-ITEM schema written directly (`z.string().min(3)` on a multi-valued field) is\n * rejected loudly on the first save rather than silently mis-bounding anything.\n *\n * The per-item form is therefore `z.array(z.string().min(3))` — and THAT is what\n * needed fixing: the merged projection contributes CONSTRAINT keywords only, so the\n * derived fragment's nested `items` never reached the model even though the executor\n * enforces it. This reads that fragment directly, which is the one place this module\n * looks past `fieldJsonSchema` — see the EC-505 task file for the merge-side proposal.\n */\nfunction perItemFragment(field: FieldDef): JsonSchema | undefined {\n if (!('multiple' in field) || field.multiple !== true) return undefined\n const derived = field.validation?.schema\n if (typeof derived !== 'object' || derived === null || Array.isArray(derived)) return undefined\n const items = (derived as JsonSchema).items\n if (typeof items !== 'object' || items === null || Array.isArray(items)) return undefined\n return items as JsonSchema\n}\n\n/**\n * Human-readable constraint phrases for one field, in a stable order — the form a\n * model reads best (and the form the collections catalog carries, where there is no\n * JSON Schema to hand the model at all). Empty when the field constrains nothing.\n */\nexport function describeFieldConstraints(field: FieldDef): string[] {\n const out: string[] = []\n if (field.validation?.required === true) out.push('required')\n out.push(...describeSchemaConstraints(fieldJsonSchema(field)))\n\n let incomplete = field.validation?.schemaComplete === false\n const items = perItemFragment(field)\n if (items !== undefined) {\n const itemParts = describeSchemaConstraints(items)\n if (itemParts.length > 0) out.push(`each item: ${itemParts.join(', ')}`)\n // A nested rule this vocabulary cannot phrase (a constraint inside an OBJECT\n // item, say) is declared partial rather than dropped in silence.\n if (hasNestedConstraint(items)) incomplete = true\n }\n\n if (field.validation?.unique === true) out.push('unique across the collection')\n if (incomplete) out.push(INCOMPLETE_CONSTRAINTS_NOTE)\n return out\n}\n\n/** The constraint phrases as one clause (`min 2 characters; required`), or `undefined`. */\nexport function constraintSentence(field: FieldDef): string | undefined {\n const parts = describeFieldConstraints(field)\n return parts.length > 0 ? parts.join('; ') : undefined\n}\n"]}
1
+ {"version":3,"file":"constraints.js","sourceRoot":"","sources":["../../../src/lib/ai/constraints.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model-facing constraint rendering (EC-473, tuned by EC-505).\n *\n * The engine has ONE constraint projection: `fieldJsonSchema` merges a field's\n * declarative rules (`validation.min`/`max`/`pattern`) with the fragment derived\n * from a schema-form `validation.validate` (EC-522: nested `items`/`properties`\n * bounds included, fitted to the field's type at config resolve). This module turns\n * that projection into what the MODEL should see — which is not the same artifact a\n * JSON-Schema consumer wants:\n *\n * - **a regex is a poor instruction for a model.** `z.string().email()` emits a\n * ~200-character pattern and `z.iso.date()` a leap-year monster (measured,\n * EC-474 §3); `format` + bounds says the same thing in four tokens. So on the AI\n * path a `pattern` is dropped wherever a `format` is present. The JSON Schema\n * OUTPUT artifact keeps it — different consumer, different needs.\n * - **a union `type` is a poor instruction too** (EC-505). A `date` field accepts\n * an ISO string OR an epoch number *at rest*, so the general projection honestly\n * says `type: ['string','number']`. The model does not need that latitude: it is\n * asked to AUTHOR a date, EC-472 ratified that a date is an ISO instant, and the\n * declared bounds this same projection carries (`formatMinimum`/`formatMaximum`)\n * are ISO strings that mean nothing against an epoch. Some structured-output\n * providers also reject a union `type` outright. So the model-facing shape says\n * `type: 'string'`; accepting a number stays an input-parsing detail of the\n * validator, not a shape the model is taught.\n * - **incompleteness must be stated.** When `validation.schemaComplete` is `false`\n * the projection is a partial view of the rule, and the model is told so rather\n * than being handed a half-truth as the whole rule. (An `ai.hint` is exactly the\n * right place to say what the dropped refinement wants — the note nudges toward\n * writing one.)\n */\nimport { fieldJsonSchema } from '@elytracms/core/cms-core'\nimport type { FieldDef, JsonSchema } from '@elytracms/core/cms-core'\n\n/** The note appended when a field's constraint projection is knowingly partial. */\nexport const INCOMPLETE_CONSTRAINTS_NOTE =\n 'further validation rules apply that are not expressible here — keep the value conventional and plausible'\n\n/**\n * The prose form of `format: 'date-time'` (EC-505). The JSON-Schema keyword rides\n * on the target shape for the provider's validator; the CATALOG has no JSON Schema\n * at all — it is prose only, and the chat update path authors date values through\n * it. So the format is said in words too, or the only channel that path has says\n * nothing about the one format the engine accepts.\n *\n * Kept to three words deliberately: measured at 6 tokens, against 20 for the same\n * note with a worked example — and it rides every date field of every turn.\n */\nexport const ISO_DATE_NOTE = 'ISO 8601 date'\n\n/**\n * Drop `pattern` wherever `format` is present (EC-473/EC-474 req 6). Returns the\n * same object when there is nothing to strip, so callers can rely on identity.\n */\nexport function stripRedundantPattern(schema: JsonSchema): JsonSchema {\n if (schema.format === undefined || schema.pattern === undefined) return schema\n const { pattern: _pattern, ...rest } = schema\n return rest\n}\n\n/**\n * Narrow a date field's `['string','number']` union to the ISO string the model\n * should author (EC-505 — see the module note). Keyed on the exact combination the\n * date branch of the projection emits (`format: 'date-time'` + an array `type`\n * containing `string`), so nothing else is touched. Identity-preserving.\n */\nexport function narrowDateUnion(schema: JsonSchema): JsonSchema {\n if (schema.format !== 'date-time') return schema\n const type = schema.type\n if (!Array.isArray(type) || !type.includes('string')) return schema\n return { ...schema, type: 'string' }\n}\n\n/**\n * Apply a node transform through a schema tree (`items` + `properties`), preserving\n * object identity wherever nothing changed — `buildTargetShape` relies on that to\n * avoid copying a property it did not touch.\n */\nfunction mapSchemaDeep(schema: JsonSchema, transform: (node: JsonSchema) => JsonSchema): JsonSchema {\n let out = transform(schema)\n const items = out.items\n if (typeof items === 'object' && items !== null && !Array.isArray(items)) {\n const next = mapSchemaDeep(items as JsonSchema, transform)\n if (next !== items) out = { ...out, items: next }\n }\n const properties = out.properties\n if (typeof properties === 'object' && properties !== null && !Array.isArray(properties)) {\n let changed = false\n const nextProps: Record<string, JsonSchema> = {}\n for (const [key, value] of Object.entries(properties as Record<string, unknown>)) {\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const next = mapSchemaDeep(value as JsonSchema, transform)\n nextProps[key] = next\n if (next !== value) changed = true\n } else {\n nextProps[key] = value as JsonSchema\n }\n }\n if (changed) out = { ...out, properties: nextProps }\n }\n return out\n}\n\n/** Recursively strip redundant patterns through `items` / `properties`. */\nexport function stripRedundantPatternsDeep(schema: JsonSchema): JsonSchema {\n return mapSchemaDeep(schema, stripRedundantPattern)\n}\n\n/**\n * The whole model-facing rewrite of a projected schema (EC-505): redundant patterns\n * stripped and date unions narrowed, everywhere in the tree — an `object` field's\n * date SUBfield gets the same treatment as a top-level one. Identity-preserving.\n */\nexport function toModelFacingSchema(schema: JsonSchema): JsonSchema {\n return mapSchemaDeep(schema, (node) => narrowDateUnion(stripRedundantPattern(node)))\n}\n\n/**\n * Human-readable phrases for the constraint keywords of ONE schema node, in a stable\n * order — the form a model reads best. Node-level only: nested `items`/`properties`\n * are {@link describeSchemaDeep}'s business. Total over the projection's numeric,\n * string and array vocabulary; `enum`/`const` are deliberately not phrased — they\n * ride the target SHAPE as JSON Schema, and a `select`'s options or a relation's\n * `collection` const would otherwise repeat on every catalog line.\n */\nfunction describeSchemaConstraints(schema: JsonSchema): string[] {\n const out: string[] = []\n const num = (value: unknown): number | undefined =>\n typeof value === 'number' ? value : undefined\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' ? value : undefined\n\n const minLength = num(schema.minLength)\n const maxLength = num(schema.maxLength)\n if (minLength !== undefined && maxLength !== undefined) {\n out.push(`${minLength}–${maxLength} characters`)\n } else if (minLength !== undefined) out.push(`at least ${minLength} characters`)\n else if (maxLength !== undefined) out.push(`at most ${maxLength} characters`)\n\n const minimum = num(schema.minimum)\n const maximum = num(schema.maximum)\n if (minimum !== undefined && maximum !== undefined) out.push(`between ${minimum} and ${maximum}`)\n else if (minimum !== undefined) out.push(`at least ${minimum}`)\n else if (maximum !== undefined) out.push(`at most ${maximum}`)\n\n // EC-522: the projection carried these already; the prose never said them.\n const exclusiveMinimum = num(schema.exclusiveMinimum)\n const exclusiveMaximum = num(schema.exclusiveMaximum)\n if (exclusiveMinimum !== undefined) out.push(`more than ${exclusiveMinimum}`)\n if (exclusiveMaximum !== undefined) out.push(`less than ${exclusiveMaximum}`)\n const multipleOf = num(schema.multipleOf)\n if (multipleOf !== undefined) out.push(`a multiple of ${multipleOf}`)\n\n const minItems = num(schema.minItems)\n const maxItems = num(schema.maxItems)\n if (minItems !== undefined) out.push(`at least ${minItems} items`)\n if (maxItems !== undefined) out.push(`at most ${maxItems} items`)\n if (schema.uniqueItems === true) out.push('no duplicate items')\n\n // EC-473: date bounds finally reach a consumer. `formatMinimum`/`formatMaximum`\n // is the projection's machine form; this is the form a model acts on.\n const from = text(schema.formatMinimum)\n const to = text(schema.formatMaximum)\n if (from !== undefined) out.push(`not earlier than ${from}`)\n if (to !== undefined) out.push(`not later than ${to}`)\n\n const format = text(schema.format)\n // EC-505: `date-time` is said in words rather than skipped — the catalog carries\n // no JSON Schema, so this is the only place the update path learns the format.\n if (format === 'date-time') out.push(ISO_DATE_NOTE)\n else if (format !== undefined) out.push(`format: ${format}`)\n // Only a pattern that is NOT explained by a format is worth the tokens.\n const pattern = text(schema.pattern)\n if (pattern !== undefined && format === undefined) out.push(`matching /${pattern}/`)\n\n return out\n}\n\nfunction isSchemaNode(value: unknown): value is JsonSchema {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The phrases of one projected schema node AND everything below it (EC-522): a\n * `multiple` field's per-item bounds become an `each item: …` clause, an object's\n * subfield bounds a `<name>: …` clause, recursively. The projection is the ONLY\n * input — since EC-522 the merge carries nested `items`/`properties` constraints\n * itself, so this module never looks past `fieldJsonSchema` (EC-505's direct read\n * of the derived fragment's `items` is gone, and with it the second place that\n * understood fragment structure).\n *\n * Which side a schema addresses is still the executor's call, not this module's:\n * `validateDocument` hands the validator the field's WHOLE value — a `multiple`\n * field's array — so the per-item form is `z.array(z.string().min(3))`, and a\n * bare `z.string().min(3)` there is a config error at load (EC-522), never a\n * silently mis-applied bound.\n */\nfunction describeSchemaDeep(schema: JsonSchema): string[] {\n const out = describeSchemaConstraints(schema)\n if (isSchemaNode(schema.items)) {\n const itemParts = describeSchemaDeep(schema.items)\n if (itemParts.length > 0) out.push(`each item: ${itemParts.join(', ')}`)\n }\n if (isSchemaNode(schema.properties)) {\n for (const [name, sub] of Object.entries(schema.properties)) {\n if (!isSchemaNode(sub)) continue\n const subParts = describeSchemaDeep(sub)\n if (subParts.length > 0) out.push(`${name}: ${subParts.join(', ')}`)\n }\n }\n return out\n}\n\n/**\n * Human-readable constraint phrases for one field, in a stable order — the form a\n * model reads best (and the form the collections catalog carries, where there is no\n * JSON Schema to hand the model at all). Empty when the field constrains nothing.\n */\nexport function describeFieldConstraints(field: FieldDef): string[] {\n const out: string[] = []\n if (field.validation?.required === true) out.push('required')\n out.push(...describeSchemaDeep(fieldJsonSchema(field)))\n if (field.validation?.unique === true) out.push('unique across the collection')\n // The one thing the projection cannot say about itself: that it is partial (an\n // opaque vendor, a `.refine()`, or — EC-522 — a rule at a position the field's\n // shape has no counterpart for). Set at config resolve, read here, nowhere else.\n if (field.validation?.schemaComplete === false) out.push(INCOMPLETE_CONSTRAINTS_NOTE)\n return out\n}\n\n/** The constraint phrases as one clause (`min 2 characters; required`), or `undefined`. */\nexport function constraintSentence(field: FieldDef): string | undefined {\n const parts = describeFieldConstraints(field)\n return parts.length > 0 ? parts.join('; ') : undefined\n}\n"]}
@@ -33,6 +33,7 @@ export * from './anthropic-provider.js';
33
33
  export * from './gateway-provider.js';
34
34
  export * from './fake-provider.js';
35
35
  export * from './route.js';
36
+ export * from './route-fetch.js';
36
37
  export * from './route-provider.js';
37
38
  export * from './convex-session.js';
38
39
  export * from './convex-session-headers.js';
@@ -33,6 +33,7 @@ export * from './anthropic-provider.js';
33
33
  export * from './gateway-provider.js';
34
34
  export * from './fake-provider.js';
35
35
  export * from './route.js';
36
+ export * from './route-fetch.js';
36
37
  export * from './route-provider.js';
37
38
  export * from './convex-session.js';
38
39
  export * from './convex-session-headers.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './constraints'\nexport * from './proposal-validation'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './constraints'\nexport * from './proposal-validation'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-fetch'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The AI route's response-shape guard (EC-514 rider): ONE wrapping `fetch` shared by
3
+ * both client call sites of the mounted route — the panel's chat transport
4
+ * (`useChat` + `DefaultChatTransport`, which accepts a `fetch` option) and the
5
+ * structured proxy in `createRouteProvider` — so "the route is not mounted at this
6
+ * path" is a typed, nameable failure at both, instead of whatever each transport
7
+ * makes of a page of HTML.
8
+ *
9
+ * Why this exists (measured, EC-514): with the EC-503 dev degrade active (studio
10
+ * `dist` missing → the Vite dev mount is skipped) a `POST /api/ai` falls through to
11
+ * the SPA shell — `200 text/html`. The chat transport treats a 200 as success and
12
+ * parses the HTML as an event stream; the HTML carries no `data:` lines, so the
13
+ * stream ends EMPTY and the panel shows the user's turn followed by nothing — no
14
+ * error note, no assistant turn. A silent failure, the exact state AGENTS.md §4
15
+ * forbids. The structured proxy's path is no better by construction: a 200 is
16
+ * `response.ok`, so it calls `response.json()` on `<!DOCTYPE …` — a parse error
17
+ * that names neither the path nor the cause.
18
+ *
19
+ * The contract this guard enforces is the route's OWN: `createAiRouteHandler` only
20
+ * ever answers `application/json` (every non-stream branch, `route.ts`
21
+ * `jsonResponse`) or the SDK's UI message stream (`text/event-stream`,
22
+ * `createUIMessageStreamResponse`). Anything else — an HTML shell, a host's 404
23
+ * page, a proxy's error page — was not produced by the route, so the request did
24
+ * not reach a mounted route at that path. Positive detection (what the route DOES
25
+ * answer) rather than a blocklist of page types, so a new kind of "something else"
26
+ * cannot slip past as success.
27
+ *
28
+ * Bundle discipline: imports nothing from `ai` / `@ai-sdk/*` (pinned in
29
+ * `bundle.test.ts`) — it is a plain `fetch` decorator.
30
+ */
31
+ /** The bare media type of a `content-type` header (`text/html; charset=utf-8` → `text/html`). */
32
+ export declare function mediaTypeOf(contentType: string | null | undefined): string | null;
33
+ /** True when a response's content type is one the AI route itself produces. */
34
+ export declare function isAiRouteAnswer(contentType: string | null | undefined): boolean;
35
+ /**
36
+ * Thrown by {@link guardAiRouteFetch} when the answer at `endpoint` was not produced
37
+ * by the AI route — no route is mounted there (or something in front of it
38
+ * answered). Carries what a developer needs to act: the path, the status, and the
39
+ * content type that came back. The `message` is self-describing on purpose: the
40
+ * tool executor (`tools.ts`) surfaces `error.message` verbatim as a tool step's
41
+ * error text, and a bare "unavailable" there would hide the cause again.
42
+ */
43
+ export declare class AiRouteNotMountedError extends Error {
44
+ readonly endpoint: string;
45
+ readonly status: number;
46
+ readonly contentType: string | null;
47
+ readonly name = "AiRouteNotMountedError";
48
+ constructor(endpoint: string, status: number, contentType: string | null);
49
+ }
50
+ /** Type guard for {@link AiRouteNotMountedError} (works across bundle copies — checks the name). */
51
+ export declare function isAiRouteNotMountedError(error: unknown): error is AiRouteNotMountedError;
52
+ /**
53
+ * Wrap a `fetch` so a response the AI route could not have produced throws
54
+ * {@link AiRouteNotMountedError} instead of being handed to a JSON/stream parser.
55
+ * Everything else — including the route's own error answers (401/429/502 JSON) —
56
+ * passes through unchanged for the existing handlers.
57
+ */
58
+ export declare function guardAiRouteFetch(fetchImpl?: typeof fetch): typeof fetch;