@elytracms/studio 0.0.81 → 0.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/redirect-revalidate.test.ts +130 -0
- package/convex/revalidate.ts +9 -7
- package/convex/triggers.ts +33 -3
- package/dist/components/ai/ai-chat-panel.js +26 -4
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +27 -0
- package/dist/components/ai/proposal-card.js +31 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.d.ts +55 -0
- package/dist/components/ai/proposal-issues.js +69 -0
- package/dist/components/ai/proposal-issues.js.map +1 -0
- package/dist/components/ai/translate-document-dialog.js +38 -3
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +1 -1
- package/dist/components/app-views/studio-config.js +1 -1
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +3 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +8 -5
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +18 -3
- package/dist/components/cms/cell-edit.js +27 -9
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +14 -1
- package/dist/components/cms/cms-store.js +93 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +1 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +7 -12
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +12 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/field-control.js +20 -7
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +36 -0
- package/dist/components/cms/gate-validation.js +17 -0
- package/dist/components/cms/gate-validation.js.map +1 -0
- package/dist/components/cms/inline-field-errors.d.ts +8 -0
- package/dist/components/cms/inline-field-errors.js +52 -0
- package/dist/components/cms/inline-field-errors.js.map +1 -0
- package/dist/components/cms/issues-pane.js +8 -4
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/slug-collision-report.d.ts +41 -0
- package/dist/components/cms/slug-collision-report.js +52 -0
- package/dist/components/cms/slug-collision-report.js.map +1 -0
- package/dist/components/deployment-setup-gate.d.ts +24 -0
- package/dist/components/deployment-setup-gate.js +29 -4
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/issue-severity.d.ts +73 -0
- package/dist/components/issue-severity.js +56 -0
- package/dist/components/issue-severity.js.map +1 -0
- package/dist/components/nav-issues.d.ts +68 -0
- package/dist/components/nav-issues.js +78 -0
- package/dist/components/nav-issues.js.map +1 -0
- package/dist/components/operation-error.js +3 -3
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/section-nav.js +5 -38
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +6 -7
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +32 -26
- package/dist/lib/ai/chat-context.js +81 -9
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +42 -0
- package/dist/lib/ai/constraints.js +259 -0
- package/dist/lib/ai/constraints.js.map +1 -0
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/proposal-validation.d.ts +26 -0
- package/dist/lib/ai/proposal-validation.js +17 -0
- package/dist/lib/ai/proposal-validation.js.map +1 -0
- package/dist/lib/ai/shape.js +60 -13
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -4
- package/dist/lib/ai/tools.js +43 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +35 -1
- package/dist/lib/ai/translate-document.js +16 -1
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +11 -6
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +8 -6
- package/dist/lib/ai/update-fields.js +9 -22
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +5 -0
- package/dist/lib/clipboard/paste-issues.js +16 -6
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +37 -0
- package/dist/lib/cms/document-gate.js +40 -0
- package/dist/lib/cms/document-gate.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +5 -0
- package/dist/lib/cms-views/view-model.js +10 -4
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.js +1 -3
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +51 -16
- package/dist/lib/i18n/i18n.js +119 -43
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +8 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +68 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +59 -0
- package/dist/lib/i18n/messages.en.js +80 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/i18n/types.d.ts +15 -9
- package/dist/lib/i18n/types.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +19 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +10 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +44 -9
- package/dist/lib/persona/labels.js +143 -60
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/webhooks/dispatcher.d.ts +5 -2
- package/dist/lib/webhooks/dispatcher.js.map +1 -1
- package/dist/lib/webhooks/tags.js +150 -17
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +3 -1
- package/dist/lib/workspace/config-issues.js +3 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +15 -8
- package/dist/lib/workspace/index.js +17 -18
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +27 -0
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +13 -6
- package/dist/previews/manifest-format.js +9 -2
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/root.d.ts +36 -1
- package/dist/router/root.js +54 -7
- package/dist/router/root.js.map +1 -1
- package/package.json +5 -2
package/dist/lib/ai/types.d.ts
CHANGED
|
@@ -193,12 +193,17 @@ export interface ApplyContentInput {
|
|
|
193
193
|
existing?: FieldValues;
|
|
194
194
|
/** Target locale for localized fields (their values land under `localized[locale]`). */
|
|
195
195
|
locale?: string;
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
/**
|
|
197
|
+
* THE validation option bag (EC-497) — the studio session's
|
|
198
|
+
* `gateValidateOptions(...)`, passed WHOLE rather than reassembled from
|
|
199
|
+
* individual keys here. EC-473 had already established that the assistant must
|
|
200
|
+
* judge its output by the editor's rules; this makes that structural: there is
|
|
201
|
+
* one bag, and a surface cannot quietly take a subset of it. `localeConfig` is
|
|
202
|
+
* overridden per generation (a create targets ONE locale), which is the single
|
|
203
|
+
* documented deviation and is written at the override, not implied by an
|
|
204
|
+
* omission.
|
|
205
|
+
*/
|
|
206
|
+
validateOptions?: ValidateDocumentOptions;
|
|
202
207
|
/** Max repair attempts after the first generation (default {@link DEFAULT_MAX_REPAIR_ATTEMPTS}). */
|
|
203
208
|
maxRepairAttempts?: number;
|
|
204
209
|
}
|
package/dist/lib/ai/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/ai/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — headless AI platform seam types (EC-331, phase 1).\n *\n * Pure types only: this module imports nothing at runtime (every import is\n * `import type`), so referencing `StudioAiConfig` from the studio-config type does\n * NOT pull the `ai` / `@ai-sdk/*` runtime onto the critical path. The runtime lives\n * in the sibling modules (`config`, `anthropic-provider`, `fake-provider`,\n * `knowledge`, `shape`, `apply-content`, `route`, `provenance`), reached only when\n * the `ai` block is configured and the AI subsystem actually boots. The Anthropic\n * provider additionally loads `ai`/`@ai-sdk/anthropic` via dynamic `import()`, so\n * even importing this barrel never eagerly loads the SDK.\n */\nimport type {\n CollectionDef,\n CollectionRegistry,\n CmsValidationIssue,\n FieldValues,\n JsonSchema,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\n// Type-only on purpose (erased at compile; `verbatimModuleSyntax`): referencing the\n// SDK's `LanguageModel` here creates NO runtime edge — pinned in `bundle.test.ts`.\nimport type { LanguageModel } from 'ai'\n\n// --- Provider seam ----------------------------------------------------------------\n\n/**\n * The capabilities the engine requires of a provider. Both must be `true` — the\n * capability gate (`resolveAiConfig`) fails loud otherwise (tool-calling +\n * structured output are the non-negotiable floor for schema-guaranteed authoring).\n */\nexport interface AiProviderCapabilities {\n /** The provider can call tools (function/tool calling). */\n toolCalling: boolean\n /** The provider can return schema-constrained structured output. */\n structuredOutput: boolean\n}\n\n/** A request for one schema-constrained structured object — the engine's core primitive. */\nexport interface GenerateStructuredRequest {\n /** System prompt: rules, injection stance, closed-world constraints. */\n system?: string\n /** The task prompt plus assembled context (knowledge, existing values, prior issues). */\n prompt: string\n /** The target shape as a JSON Schema (draft 2020-12 subset) the output must satisfy. */\n schema: JsonSchema\n /** A short name for the schema (some providers surface it as the tool/output name). */\n schemaName?: string\n /** A one-line description of what the schema represents. */\n schemaDescription?: string\n}\n\n/** Token usage a provider may report (best-effort; recorded for soft spend counters). */\nexport interface AiUsage {\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface GenerateStructuredResult {\n /** The structured object the provider produced (best-effort match to the request schema). */\n object: unknown\n usage?: AiUsage\n}\n\n/**\n * A provider adapter over a tool-calling, structured-output model. Anthropic (or\n * the Vercel AI Gateway) in production, {@link FakeProvider} (deterministic,\n * keyless) in tests/playground — all implement this identical surface so the whole\n * loop runs without a key.\n */\nexport interface AiProvider {\n /** Stable provider id (e.g. `anthropic`, `fake`); recorded in provenance. */\n readonly id: string\n /** The model id the provider targets, when fixed; recorded in provenance. */\n readonly model?: string\n /** The provider's capabilities — validated by the capability gate at config load. */\n capabilities(): AiProviderCapabilities\n /** Produce a single schema-constrained structured object. */\n generateStructured(request: GenerateStructuredRequest): Promise<GenerateStructuredResult>\n /**\n * The AI-SDK `LanguageModel` behind this provider — the seam the route's native\n * chat branch consumes (EC-334: `streamText` over UIMessages, tools declared\n * without `execute` so they forward to the client). Optional: a pure structured\n * proxy (the client-side route provider) exposes none — the route answers 501\n * for chat requests then.\n */\n languageModel?(): Promise<LanguageModel>\n}\n\n// --- Config seam ------------------------------------------------------------------\n\n/** Soft, advisory limits for the route counter and the engine repair loop. */\nexport interface AiLimits {\n /** Soft per-project ceiling on route requests per minute (advisory; route-enforced). */\n maxRequestsPerMinute?: number\n /** Cap on repair attempts after the first generation (engine default when omitted). */\n maxRepairAttempts?: number\n}\n\n/**\n * The `ai` block of `defineStudio({ ai })` (EC-331). Studio-only live code — like\n * `fieldRenderers` / `i18n`, it never reaches the delivery bundle. The block's\n * ABSENCE means the feature is fully absent (no AI UI, no AI deps on the critical\n * path); `enabled: false` is a temporary kill-switch that keeps the config.\n */\nexport interface StudioAiConfig {\n /**\n * Temporary kill-switch. Presence of the whole `ai` block enables the feature;\n * `enabled: false` disables it without removing the config. Omitted ⇒ enabled.\n */\n enabled?: boolean\n /** The provider adapter (live code): Anthropic in production, FakeProvider in tests/playground. */\n provider: AiProvider\n /** Project knowledge sources assembled into the model's context. */\n knowledge?: KnowledgeSource[]\n /** Soft rate / repair limits. */\n limits?: AiLimits\n}\n\n// --- Knowledge seam ---------------------------------------------------------------\n\n/** An inline knowledge literal (markdown prose versioned with the repo). */\nexport interface KnowledgeItem {\n title?: string\n content: string\n}\n\n/**\n * A `fromCollection('<id>')` source: studio-editable knowledge whose *content* is\n * prose held in the DB (data), while the mechanism stays code. Resolved by a\n * caller-supplied collection reader (starter-kit / studio wires it to operations).\n */\nexport interface KnowledgeCollectionSource {\n readonly kind: 'collection'\n /** Collection id whose documents supply editable knowledge prose. */\n readonly collectionId: string\n /** Field carrying the prose; when omitted the reader picks the collection's title/first text field. */\n readonly field?: string\n}\n\n/** An async (or sync) function producing knowledge items at resolve time. */\nexport type KnowledgeFn = () =>\n | KnowledgeItem\n | KnowledgeItem[]\n | Promise<KnowledgeItem | KnowledgeItem[]>\n\n/** A knowledge source: inline literal, async function, or `fromCollection(...)` sugar. */\nexport type KnowledgeSource = KnowledgeItem | KnowledgeFn | KnowledgeCollectionSource\n\n/** The assembled knowledge context — a flat, deterministic list of resolved items. */\nexport interface ResolvedKnowledge {\n items: KnowledgeItem[]\n}\n\n/** Reads knowledge prose out of a `fromCollection` source (wired to the operations layer). */\nexport type KnowledgeCollectionReader = (\n source: KnowledgeCollectionSource,\n) => KnowledgeItem[] | Promise<KnowledgeItem[]>\n\n// --- Engine seam ------------------------------------------------------------------\n\n/**\n * A relation candidate id (from a `searchDocuments`/tool result). Relation fields may\n * ONLY use ids present here — the target shape emits them as an enum, so a\n * hallucinated id is not even representable as valid structured output.\n */\nexport interface RelationCandidate {\n id: string\n /** Human label (title) for the model to choose against. */\n label?: string\n}\n\n/** Relation candidate ids keyed by field name. A field absent here is omitted from the shape. */\nexport type RelationCandidates = Record<string, RelationCandidate[]>\n\n/**\n * An asset candidate id (from a `listAssets` tool result). Asset/image fields may\n * ONLY use ids present here — the target shape emits them as an enum, so a\n * hallucinated (or invented / \"to-be-uploaded\") asset id is not even representable\n * as valid structured output. From the EXISTING media library ONLY (EC-381): the\n * assistant never generates or uploads an asset; empty beats wrong.\n */\nexport interface AssetCandidate {\n id: string\n /** Human label (title / filename) for the model to choose against. */\n label?: string\n}\n\n/** Asset candidate ids keyed by field name. A field absent here is omitted from the shape. */\nexport type AssetCandidates = Record<string, AssetCandidate[]>\n\n/** The typed draft-values patch the engine emits — applied by the draft-write path. */\nexport interface ContentPatch {\n collection: string\n id: string\n /** Non-localized field values. */\n values: FieldValues\n /** Per-locale field values (localized fields, under the target locale). */\n localized?: Record<string, FieldValues>\n}\n\n/** Origin marker the draft-write path records so an AI-authored draft is attributable. */\nexport interface AiProvenance {\n origin: 'assistant'\n /** Provider id (e.g. `anthropic`, `fake`). */\n provider: string\n /** Model id, when the provider fixes one. */\n model?: string\n /** ISO timestamp when the content was generated. */\n at: string\n}\n\nexport interface ApplyContentInput {\n provider: AiProvider\n registry: CollectionRegistry\n collection: CollectionDef\n /** Document id the patch targets (an existing doc for updates, or a fresh id for creation). */\n documentId: string\n /** The author's instruction / prompt. */\n instruction: string\n /** Assembled knowledge context (from `resolveKnowledge`). */\n knowledge?: ResolvedKnowledge\n /** Component manifests — context for `allow`-leashed richText/blocks fields. */\n components?: readonly ComponentManifest[]\n /** Relation candidate ids by field — the ONLY ids a relation field may use. */\n candidates?: RelationCandidates\n /**\n * Asset candidate ids by field — the ONLY ids an asset/image field may use\n * (EC-381). Emitted as an enum; an asset field with none is omitted from the\n * shape (so a create proposal simply leaves it empty — empty beats wrong).\n */\n assetCandidates?: AssetCandidates\n /** Existing draft values (updates): the model output merges over these before validation. */\n existing?: FieldValues\n /** Target locale for localized fields (their values land under `localized[locale]`). */\n locale?: string\n /** Injected rich-text embed leash (studio-wired); enforced when present. */\n validateRichTextEmbeds?: ValidateDocumentOptions['validateRichTextEmbeds']\n /** Injected blocks composition leash (studio-wired); enforced when present. */\n validateComposition?: ValidateDocumentOptions['validateComposition']\n /** Document store for relation/asset existence checks (optional). */\n store?: ValidateDocumentOptions['store']\n /** Max repair attempts after the first generation (default {@link DEFAULT_MAX_REPAIR_ATTEMPTS}). */\n maxRepairAttempts?: number\n}\n\n/** The engine's outcome: a valid, leash-compliant patch, or the blocking issues. */\nexport type ApplyContentResult =\n | {\n ok: true\n patch: ContentPatch\n provenance: AiProvenance\n /** Total generation attempts made (1 = happy path, >1 = repaired). */\n attempts: number\n /** Non-error issues surfaced by the accepted document (e.g. warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n ok: false\n /** The error-severity issues from the final failed attempt. */\n issues: CmsValidationIssue[]\n attempts: number\n provenance: AiProvenance\n }\n\n// --- Route seam -------------------------------------------------------------------\n\n/** A validated studio session, from the host-wired `getSession`. Opaque beyond `userId`. */\nexport interface AiSession {\n /** Authenticated principal id. */\n userId: string\n [key: string]: unknown\n}\n\nexport interface AiRouteHandlerOptions {\n /** The provider (holds the host-env key). The route is a thin proxy over it. */\n provider: AiProvider\n /**\n * Server-side session check (reject anonymous callers). Returns a session for an\n * authenticated caller, `null` otherwise. Wired by the host to its own auth — the\n * route consumes it, never re-implements it.\n */\n getSession: (request: Request) => AiSession | null | Promise<AiSession | null>\n /** Soft per-project rate limit (advisory spend guard). */\n limits?: AiLimits\n /** Derive the per-project rate-bucket key from a request (default: one global bucket). */\n projectKey?: (request: Request) => string\n}\n\n/** The mounted route handler: a framework-agnostic Web `Request → Response`. */\nexport type AiRouteHandler = (request: Request) => Promise<Response>\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/ai/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — headless AI platform seam types (EC-331, phase 1).\n *\n * Pure types only: this module imports nothing at runtime (every import is\n * `import type`), so referencing `StudioAiConfig` from the studio-config type does\n * NOT pull the `ai` / `@ai-sdk/*` runtime onto the critical path. The runtime lives\n * in the sibling modules (`config`, `anthropic-provider`, `fake-provider`,\n * `knowledge`, `shape`, `apply-content`, `route`, `provenance`), reached only when\n * the `ai` block is configured and the AI subsystem actually boots. The Anthropic\n * provider additionally loads `ai`/`@ai-sdk/anthropic` via dynamic `import()`, so\n * even importing this barrel never eagerly loads the SDK.\n */\nimport type {\n CollectionDef,\n CollectionRegistry,\n CmsValidationIssue,\n FieldValues,\n JsonSchema,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\n// Type-only on purpose (erased at compile; `verbatimModuleSyntax`): referencing the\n// SDK's `LanguageModel` here creates NO runtime edge — pinned in `bundle.test.ts`.\nimport type { LanguageModel } from 'ai'\n\n// --- Provider seam ----------------------------------------------------------------\n\n/**\n * The capabilities the engine requires of a provider. Both must be `true` — the\n * capability gate (`resolveAiConfig`) fails loud otherwise (tool-calling +\n * structured output are the non-negotiable floor for schema-guaranteed authoring).\n */\nexport interface AiProviderCapabilities {\n /** The provider can call tools (function/tool calling). */\n toolCalling: boolean\n /** The provider can return schema-constrained structured output. */\n structuredOutput: boolean\n}\n\n/** A request for one schema-constrained structured object — the engine's core primitive. */\nexport interface GenerateStructuredRequest {\n /** System prompt: rules, injection stance, closed-world constraints. */\n system?: string\n /** The task prompt plus assembled context (knowledge, existing values, prior issues). */\n prompt: string\n /** The target shape as a JSON Schema (draft 2020-12 subset) the output must satisfy. */\n schema: JsonSchema\n /** A short name for the schema (some providers surface it as the tool/output name). */\n schemaName?: string\n /** A one-line description of what the schema represents. */\n schemaDescription?: string\n}\n\n/** Token usage a provider may report (best-effort; recorded for soft spend counters). */\nexport interface AiUsage {\n inputTokens?: number\n outputTokens?: number\n}\n\nexport interface GenerateStructuredResult {\n /** The structured object the provider produced (best-effort match to the request schema). */\n object: unknown\n usage?: AiUsage\n}\n\n/**\n * A provider adapter over a tool-calling, structured-output model. Anthropic (or\n * the Vercel AI Gateway) in production, {@link FakeProvider} (deterministic,\n * keyless) in tests/playground — all implement this identical surface so the whole\n * loop runs without a key.\n */\nexport interface AiProvider {\n /** Stable provider id (e.g. `anthropic`, `fake`); recorded in provenance. */\n readonly id: string\n /** The model id the provider targets, when fixed; recorded in provenance. */\n readonly model?: string\n /** The provider's capabilities — validated by the capability gate at config load. */\n capabilities(): AiProviderCapabilities\n /** Produce a single schema-constrained structured object. */\n generateStructured(request: GenerateStructuredRequest): Promise<GenerateStructuredResult>\n /**\n * The AI-SDK `LanguageModel` behind this provider — the seam the route's native\n * chat branch consumes (EC-334: `streamText` over UIMessages, tools declared\n * without `execute` so they forward to the client). Optional: a pure structured\n * proxy (the client-side route provider) exposes none — the route answers 501\n * for chat requests then.\n */\n languageModel?(): Promise<LanguageModel>\n}\n\n// --- Config seam ------------------------------------------------------------------\n\n/** Soft, advisory limits for the route counter and the engine repair loop. */\nexport interface AiLimits {\n /** Soft per-project ceiling on route requests per minute (advisory; route-enforced). */\n maxRequestsPerMinute?: number\n /** Cap on repair attempts after the first generation (engine default when omitted). */\n maxRepairAttempts?: number\n}\n\n/**\n * The `ai` block of `defineStudio({ ai })` (EC-331). Studio-only live code — like\n * `fieldRenderers` / `i18n`, it never reaches the delivery bundle. The block's\n * ABSENCE means the feature is fully absent (no AI UI, no AI deps on the critical\n * path); `enabled: false` is a temporary kill-switch that keeps the config.\n */\nexport interface StudioAiConfig {\n /**\n * Temporary kill-switch. Presence of the whole `ai` block enables the feature;\n * `enabled: false` disables it without removing the config. Omitted ⇒ enabled.\n */\n enabled?: boolean\n /** The provider adapter (live code): Anthropic in production, FakeProvider in tests/playground. */\n provider: AiProvider\n /** Project knowledge sources assembled into the model's context. */\n knowledge?: KnowledgeSource[]\n /** Soft rate / repair limits. */\n limits?: AiLimits\n}\n\n// --- Knowledge seam ---------------------------------------------------------------\n\n/** An inline knowledge literal (markdown prose versioned with the repo). */\nexport interface KnowledgeItem {\n title?: string\n content: string\n}\n\n/**\n * A `fromCollection('<id>')` source: studio-editable knowledge whose *content* is\n * prose held in the DB (data), while the mechanism stays code. Resolved by a\n * caller-supplied collection reader (starter-kit / studio wires it to operations).\n */\nexport interface KnowledgeCollectionSource {\n readonly kind: 'collection'\n /** Collection id whose documents supply editable knowledge prose. */\n readonly collectionId: string\n /** Field carrying the prose; when omitted the reader picks the collection's title/first text field. */\n readonly field?: string\n}\n\n/** An async (or sync) function producing knowledge items at resolve time. */\nexport type KnowledgeFn = () =>\n | KnowledgeItem\n | KnowledgeItem[]\n | Promise<KnowledgeItem | KnowledgeItem[]>\n\n/** A knowledge source: inline literal, async function, or `fromCollection(...)` sugar. */\nexport type KnowledgeSource = KnowledgeItem | KnowledgeFn | KnowledgeCollectionSource\n\n/** The assembled knowledge context — a flat, deterministic list of resolved items. */\nexport interface ResolvedKnowledge {\n items: KnowledgeItem[]\n}\n\n/** Reads knowledge prose out of a `fromCollection` source (wired to the operations layer). */\nexport type KnowledgeCollectionReader = (\n source: KnowledgeCollectionSource,\n) => KnowledgeItem[] | Promise<KnowledgeItem[]>\n\n// --- Engine seam ------------------------------------------------------------------\n\n/**\n * A relation candidate id (from a `searchDocuments`/tool result). Relation fields may\n * ONLY use ids present here — the target shape emits them as an enum, so a\n * hallucinated id is not even representable as valid structured output.\n */\nexport interface RelationCandidate {\n id: string\n /** Human label (title) for the model to choose against. */\n label?: string\n}\n\n/** Relation candidate ids keyed by field name. A field absent here is omitted from the shape. */\nexport type RelationCandidates = Record<string, RelationCandidate[]>\n\n/**\n * An asset candidate id (from a `listAssets` tool result). Asset/image fields may\n * ONLY use ids present here — the target shape emits them as an enum, so a\n * hallucinated (or invented / \"to-be-uploaded\") asset id is not even representable\n * as valid structured output. From the EXISTING media library ONLY (EC-381): the\n * assistant never generates or uploads an asset; empty beats wrong.\n */\nexport interface AssetCandidate {\n id: string\n /** Human label (title / filename) for the model to choose against. */\n label?: string\n}\n\n/** Asset candidate ids keyed by field name. A field absent here is omitted from the shape. */\nexport type AssetCandidates = Record<string, AssetCandidate[]>\n\n/** The typed draft-values patch the engine emits — applied by the draft-write path. */\nexport interface ContentPatch {\n collection: string\n id: string\n /** Non-localized field values. */\n values: FieldValues\n /** Per-locale field values (localized fields, under the target locale). */\n localized?: Record<string, FieldValues>\n}\n\n/** Origin marker the draft-write path records so an AI-authored draft is attributable. */\nexport interface AiProvenance {\n origin: 'assistant'\n /** Provider id (e.g. `anthropic`, `fake`). */\n provider: string\n /** Model id, when the provider fixes one. */\n model?: string\n /** ISO timestamp when the content was generated. */\n at: string\n}\n\nexport interface ApplyContentInput {\n provider: AiProvider\n registry: CollectionRegistry\n collection: CollectionDef\n /** Document id the patch targets (an existing doc for updates, or a fresh id for creation). */\n documentId: string\n /** The author's instruction / prompt. */\n instruction: string\n /** Assembled knowledge context (from `resolveKnowledge`). */\n knowledge?: ResolvedKnowledge\n /** Component manifests — context for `allow`-leashed richText/blocks fields. */\n components?: readonly ComponentManifest[]\n /** Relation candidate ids by field — the ONLY ids a relation field may use. */\n candidates?: RelationCandidates\n /**\n * Asset candidate ids by field — the ONLY ids an asset/image field may use\n * (EC-381). Emitted as an enum; an asset field with none is omitted from the\n * shape (so a create proposal simply leaves it empty — empty beats wrong).\n */\n assetCandidates?: AssetCandidates\n /** Existing draft values (updates): the model output merges over these before validation. */\n existing?: FieldValues\n /** Target locale for localized fields (their values land under `localized[locale]`). */\n locale?: string\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, passed WHOLE rather than reassembled from\n * individual keys here. EC-473 had already established that the assistant must\n * judge its output by the editor's rules; this makes that structural: there is\n * one bag, and a surface cannot quietly take a subset of it. `localeConfig` is\n * overridden per generation (a create targets ONE locale), which is the single\n * documented deviation and is written at the override, not implied by an\n * omission.\n */\n validateOptions?: ValidateDocumentOptions\n /** Max repair attempts after the first generation (default {@link DEFAULT_MAX_REPAIR_ATTEMPTS}). */\n maxRepairAttempts?: number\n}\n\n/** The engine's outcome: a valid, leash-compliant patch, or the blocking issues. */\nexport type ApplyContentResult =\n | {\n ok: true\n patch: ContentPatch\n provenance: AiProvenance\n /** Total generation attempts made (1 = happy path, >1 = repaired). */\n attempts: number\n /** Non-error issues surfaced by the accepted document (e.g. warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n ok: false\n /** The error-severity issues from the final failed attempt. */\n issues: CmsValidationIssue[]\n attempts: number\n provenance: AiProvenance\n }\n\n// --- Route seam -------------------------------------------------------------------\n\n/** A validated studio session, from the host-wired `getSession`. Opaque beyond `userId`. */\nexport interface AiSession {\n /** Authenticated principal id. */\n userId: string\n [key: string]: unknown\n}\n\nexport interface AiRouteHandlerOptions {\n /** The provider (holds the host-env key). The route is a thin proxy over it. */\n provider: AiProvider\n /**\n * Server-side session check (reject anonymous callers). Returns a session for an\n * authenticated caller, `null` otherwise. Wired by the host to its own auth — the\n * route consumes it, never re-implements it.\n */\n getSession: (request: Request) => AiSession | null | Promise<AiSession | null>\n /** Soft per-project rate limit (advisory spend guard). */\n limits?: AiLimits\n /** Derive the per-project rate-bucket key from a request (default: one global bucket). */\n projectKey?: (request: Request) => string\n}\n\n/** The mounted route handler: a framework-agnostic Web `Request → Response`. */\nexport type AiRouteHandler = (request: Request) => Promise<Response>\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, FieldValues, ValidateDocumentOptions
|
|
1
|
+
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, FieldValues, ValidateDocumentOptions } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { AssetCandidates, ContentPatch, RelationCandidates } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* One field change of an `updateFields` proposal, addressed with the engine's
|
|
@@ -26,15 +26,17 @@ export interface BuildUpdateProposalInput {
|
|
|
26
26
|
changes: readonly UpdateFieldChange[];
|
|
27
27
|
/** The editor's working locale — the default bucket for localized fields. */
|
|
28
28
|
locale?: string;
|
|
29
|
-
localeConfig?: LocaleConfig;
|
|
30
29
|
/** Relation candidates (searched ids) keyed by field name — the id leash. */
|
|
31
30
|
candidates?: RelationCandidates;
|
|
32
31
|
/** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */
|
|
33
32
|
assetCandidates?: AssetCandidates;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
/**
|
|
34
|
+
* THE validation option bag (EC-497) — the studio session's
|
|
35
|
+
* `gateValidateOptions(...)`, whole. It also carries the project's
|
|
36
|
+
* `localeConfig`, which is what the unknown-locale targeting check reads: one
|
|
37
|
+
* source for "which locales exist", not two that can disagree.
|
|
38
|
+
*/
|
|
39
|
+
validateOptions?: ValidateDocumentOptions;
|
|
38
40
|
}
|
|
39
41
|
/** The `before` snapshot the review diff renders against. */
|
|
40
42
|
export interface UpdateBeforeSnapshot {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { assetNode, cmsIssue, isFieldLocalized, readAssetId, readDocumentRef, referenceNode, validateDocument, } from '@elytracms/core/cms-core';
|
|
38
38
|
import { blockRootsOf, staticProp } from '@elytracms/core/project-graph';
|
|
39
39
|
import { diffDocuments } from '@elytracms/core/content';
|
|
40
|
+
import { introducedErrors, warningsOf } from './proposal-validation.js';
|
|
40
41
|
import { richTextValueFromModelInput } from './rich-text-input.js';
|
|
41
42
|
function isNonEmptyString(value) {
|
|
42
43
|
return typeof value === 'string' && value.length > 0;
|
|
@@ -215,9 +216,9 @@ function applyOneChange(input, doc, change) {
|
|
|
215
216
|
return issue('unknown-locale', `Field "${change.field}" is not localized — omit "locale".`);
|
|
216
217
|
}
|
|
217
218
|
if (change.locale !== undefined &&
|
|
218
|
-
input.localeConfig &&
|
|
219
|
-
!input.localeConfig.locales.includes(change.locale)) {
|
|
220
|
-
return issue('unknown-locale', `Unknown locale "${change.locale}" — this project has: ${input.localeConfig
|
|
219
|
+
input.validateOptions?.localeConfig &&
|
|
220
|
+
!input.validateOptions.localeConfig.locales.includes(change.locale)) {
|
|
221
|
+
return issue('unknown-locale', `Unknown locale "${change.locale}" — this project has: ${input.validateOptions?.localeConfig?.locales.join(', ')}.`);
|
|
221
222
|
}
|
|
222
223
|
const locale = localized ? change.locale ?? input.locale : undefined;
|
|
223
224
|
if (localized && locale === undefined) {
|
|
@@ -231,10 +232,6 @@ function applyOneChange(input, doc, change) {
|
|
|
231
232
|
return mapped;
|
|
232
233
|
return { doc: writeFieldValue(doc, change.field, locale, mapped.value) };
|
|
233
234
|
}
|
|
234
|
-
/** Identity for "is this error pre-existing?" — code + path + message. */
|
|
235
|
-
function issueKey(issue) {
|
|
236
|
-
return `${issue.code}|${issue.path.join('.')}|${issue.message}`;
|
|
237
|
-
}
|
|
238
235
|
/**
|
|
239
236
|
* Build the reviewable update proposal: apply every change over a copy of the
|
|
240
237
|
* document (loud targeting), validate the merged result with the injected
|
|
@@ -254,21 +251,11 @@ export function buildUpdateProposal(input) {
|
|
|
254
251
|
}
|
|
255
252
|
if (targetingIssues.length > 0)
|
|
256
253
|
return { ok: false, issues: targetingIssues };
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
...(input.validateComposition !== undefined
|
|
261
|
-
? { validateComposition: input.validateComposition }
|
|
262
|
-
: {}),
|
|
263
|
-
...(input.validateRichTextEmbeds !== undefined
|
|
264
|
-
? { validateRichTextEmbeds: input.validateRichTextEmbeds }
|
|
265
|
-
: {}),
|
|
266
|
-
};
|
|
267
|
-
const preExisting = new Set(validateDocument(registry, document, validateOptions)
|
|
268
|
-
.filter((issue) => issue.severity === 'error')
|
|
269
|
-
.map(issueKey));
|
|
254
|
+
// EC-497: THE bag, whole — no per-call-site reassembly.
|
|
255
|
+
const validateOptions = input.validateOptions ?? {};
|
|
256
|
+
const before = validateDocument(registry, document, validateOptions);
|
|
270
257
|
const issues = validateDocument(registry, next, validateOptions);
|
|
271
|
-
const introduced =
|
|
258
|
+
const introduced = introducedErrors(before, issues);
|
|
272
259
|
if (introduced.length > 0)
|
|
273
260
|
return { ok: false, issues: introduced };
|
|
274
261
|
const diff = diffDocuments(collection, document, next);
|
|
@@ -290,7 +277,7 @@ export function buildUpdateProposal(input) {
|
|
|
290
277
|
...(document.localized ? { localized: document.localized } : {}),
|
|
291
278
|
},
|
|
292
279
|
changedFields: diff.changes.map((change) => change.locale !== undefined ? `${change.field} (${change.locale})` : change.field),
|
|
293
|
-
issues: issues
|
|
280
|
+
issues: warningsOf(issues),
|
|
294
281
|
};
|
|
295
282
|
}
|
|
296
283
|
//# sourceMappingURL=update-fields.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-fields.js","sourceRoot":"","sources":["../../../src/lib/ai/update-fields.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow\n * counterpart of `applyContentWithShape`, minus the generation step).\n *\n * The MODEL authors the field changes directly in the `updateFields` tool input\n * (it has the conversation + `getDocument` output as context); this module turns\n * those changes into a reviewable proposal with the SAME engine discipline the\n * create flow has:\n *\n * - **merge semantics**: changes apply per field over the EXISTING document —\n * never a whole-document rewrite; the patch is the merged document, the diff\n * (`diffDocuments`) is what the editor reviews (changed fields only).\n * - **targeting is addressed with the engine's existing conventions** — field\n * name + optional locale bucket (the diff / `RestoreScope` shape) and, inside\n * a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,\n * root blocks by stable node id). No new path grammar.\n * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND\n * asset ids (EC-381) must come from searched candidates (`searchDocuments` /\n * `listAssets`) or already be on the document; every targeting failure is an\n * explicit issue — never a silent drop (unlike the create shape, which makes bad\n * ids structurally unrepresentable, an update change is imperative — so it FAILS\n * instead of being filtered).\n * - **validation parity**: the merged document runs through `validateDocument`\n * with the studio's injected leashes. Only errors the change INTRODUCES block\n * the proposal — pre-existing brokenness on the document is not the\n * assistant's doing and must not force a whole-document repair (the editor's\n * own save path tolerates it the same way; errors gate publish, not drafts).\n *\n * Locale-awareness: a localized field's change lands in the change's `locale`,\n * defaulting to the editor's working locale (`input.locale`). Non-localized\n * fields reject a `locale` loudly.\n *\n * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires\n * it to `getDocument` + the candidate store and hands the result to the review\n * sink.\n */\nimport {\n assetNode,\n cmsIssue,\n isFieldLocalized,\n readAssetId,\n readDocumentRef,\n referenceNode,\n validateDocument,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n FieldDef,\n FieldValues,\n ValidateDocumentOptions,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { blockRootsOf, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { richTextValueFromModelInput } from './rich-text-input'\nimport type { AssetCandidates, ContentPatch, RelationCandidates } from './types'\n\n/**\n * One field change of an `updateFields` proposal, addressed with the engine's\n * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a\n * localized field), and for a `blocks` field optionally `blockId` + `prop` to\n * change one component prop of one root block. `value` is the new value\n * (`null`/omitted clears the field or prop).\n */\nexport interface UpdateFieldChange {\n field: string\n /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */\n locale?: string\n /** For a `blocks` field: the root block's stable node id (from `getDocument`). */\n blockId?: string\n /** With `blockId`: the component prop to change. */\n prop?: string\n /** The new value; `null` / omitted clears the field or prop. */\n value?: unknown\n}\n\nexport interface BuildUpdateProposalInput {\n registry: CollectionRegistry\n collection: CollectionDef\n /** The CURRENT document — the proposal's \"before\" side. Never mutated. */\n document: CmsDocument\n changes: readonly UpdateFieldChange[]\n /** The editor's working locale — the default bucket for localized fields. */\n locale?: string\n localeConfig?: LocaleConfig\n /** Relation candidates (searched ids) keyed by field name — the id leash. */\n candidates?: RelationCandidates\n /** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */\n assetCandidates?: AssetCandidates\n /** Injected studio leashes — same discipline as the create engine. */\n validateRichTextEmbeds?: ValidateDocumentOptions['validateRichTextEmbeds']\n validateComposition?: ValidateDocumentOptions['validateComposition']\n store?: ValidateDocumentOptions['store']\n}\n\n/** The `before` snapshot the review diff renders against. */\nexport interface UpdateBeforeSnapshot {\n values: FieldValues\n localized?: Record<string, FieldValues>\n}\n\nexport type BuildUpdateProposalResult =\n | {\n ok: true\n kind: 'proposal'\n /** The MERGED document as a patch (full values; the diff shows changed fields only). */\n patch: ContentPatch\n before: UpdateBeforeSnapshot\n /** Human-readable descriptors of the changed fields (e.g. `title (de)`). */\n changedFields: string[]\n /** Non-error issues on the accepted result (warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n /** Every change resolved to the current values — nothing to propose. */\n ok: true\n kind: 'no-change'\n }\n | { ok: false; issues: CmsValidationIssue[] }\n\ntype ChangeIssue = { issue: CmsValidationIssue }\ntype ChangeOutcome = { doc: CmsDocument } | ChangeIssue\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0\n}\n\n/**\n * The present/absent-key field write — the same semantics as the editor's\n * single-sourced `setFieldValue` (`lib/locale/fallback`): a resolved `locale`\n * writes the `localized[locale]` bucket, clearing DELETES the key. Local here\n * (not imported) because this module takes the locale as an already-resolved\n * argument per change; the semantics are pinned by tests on both sides.\n */\nfunction writeFieldValue(\n doc: CmsDocument,\n fieldName: string,\n locale: string | undefined,\n value: unknown,\n): CmsDocument {\n if (locale !== undefined) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n/** Relation ids CURRENTLY on the document for a field (any bucket) — re-usable without a search. */\nfunction currentRelationIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const ref = readDocumentRef(entry)\n if (ref) ids.add(ref.id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */\nfunction currentAssetIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const id = readAssetId(entry)\n if (id) ids.add(id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Map a change's raw value into the stored value shape (envelopes; loud leash). */\nfunction mapUpdateValue(\n field: FieldDef,\n change: UpdateFieldChange,\n document: CmsDocument,\n input: BuildUpdateProposalInput,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): { value: unknown } | ChangeIssue {\n const raw = change.value\n if (raw === undefined || raw === null) return { value: undefined } // clear\n switch (field.type) {\n case 'relation': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Relation \"${field.name}\" takes document id string(s) from searchDocuments results.`,\n )\n }\n // The id leash (same stance as the create enum, but LOUD): only searched\n // candidate ids or ids already on the document are usable.\n const allowed = new Set([\n ...(input.candidates?.[field.name] ?? []).map((c) => c.id),\n ...currentRelationIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-relation-target',\n `Relation \"${field.name}\" may only use ids from searchDocuments results — search \"${field.target}\" first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single reference — pass one id.`)\n }\n const refs = ids.map((id) => referenceNode({ collection: field.target, id }))\n return { value: field.multiple ? refs : refs[0] }\n }\n case 'asset': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Asset field \"${field.name}\" takes asset id string(s) from listAssets results.`,\n )\n }\n // EC-381 — the asset id leash (same stance as relations, but LOUD): only\n // searched library candidate ids or ids already on the document are usable;\n // the assistant never invents or uploads an asset.\n const allowed = new Set([\n ...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),\n ...currentAssetIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-asset',\n `Asset field \"${field.name}\" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single asset — pass one id.`)\n }\n const nodes = ids.map((id) => assetNode(id))\n return { value: field.multiple ? nodes : nodes[0] }\n }\n case 'richText': {\n // EC-379: a string value is MARKDOWN, converted through the engine's strict\n // subset converter; out-of-subset constructs fail LOUD (the model retries\n // in-conversation). A `{ version, doc }` envelope object stays accepted for\n // robustness (getDocument may have shown one) — see rich-text-input.ts.\n const converted = richTextValueFromModelInput(raw)\n if (!converted.ok) {\n return issue('invalid-field-value', `Field \"${field.name}\": ${converted.problems.join(' ')}`)\n }\n return { value: converted.value }\n }\n default:\n // text / number / boolean / date / select / object / blocks: as-is\n // (the merged document's validateDocument run is the shape leash).\n return { value: raw }\n }\n}\n\n/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */\nfunction applyBlockPropChange(\n doc: CmsDocument,\n field: FieldDef,\n change: UpdateFieldChange,\n locale: string | undefined,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): ChangeOutcome {\n if (field.type !== 'blocks') {\n return issue(\n 'invalid-field-value',\n `\"${change.field}\" is not a blocks field — omit \"blockId\"/\"prop\" (set the field value directly).`,\n )\n }\n if (!isNonEmptyString(change.blockId)) {\n return issue(\n 'invalid-field-value',\n `Changing a block on \"${change.field}\" needs \"blockId\" — block ids come from getDocument.`,\n )\n }\n if (!isNonEmptyString(change.prop)) {\n return issue(\n 'invalid-field-value',\n `Changing block \"${change.blockId}\" needs \"prop\" — the component property to change.`,\n )\n }\n const bucket = locale !== undefined ? doc.localized?.[locale] : doc.values\n const original = bucket?.[change.field]\n const blocks = blockRootsOf(original)\n const index = blocks.findIndex((block) => block.id === change.blockId)\n if (index < 0) {\n return issue(\n 'invalid-field-value',\n `Block \"${change.blockId}\" was not found on \"${change.field}\" — read the document first; block ids come from getDocument.`,\n )\n }\n const block = blocks[index]!\n const props = { ...(block.props ?? {}) }\n if (change.value === undefined || change.value === null) delete props[change.prop]\n else props[change.prop] = staticProp(change.value as JsonValue)\n const nextBlock: ComponentNode = { ...block, props }\n const nextBlocks = blocks.map((b, i) => (i === index ? nextBlock : b))\n // Preserve the stored shape: an array for a `multiple` composition, a bare node otherwise.\n const value = Array.isArray(original) ? nextBlocks : nextBlocks[0]\n return { doc: writeFieldValue(doc, change.field, locale, value) }\n}\n\n/** Apply ONE change to a (copy of the) document, or report the targeting issue. */\nfunction applyOneChange(\n input: BuildUpdateProposalInput,\n doc: CmsDocument,\n change: UpdateFieldChange,\n): ChangeOutcome {\n const { collection } = input\n const issue = (\n code: Parameters<typeof cmsIssue>[0]['code'],\n message: string,\n ): ChangeIssue => ({\n issue: cmsIssue({\n code,\n message,\n path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],\n collectionId: collection.id,\n documentId: doc.id,\n meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },\n }),\n })\n\n const field = collection.fields.find((f) => f.name === change.field)\n if (!field) {\n return issue(\n 'invalid-field-value',\n `Unknown field \"${change.field}\" on collection \"${collection.id}\" — use a field name from the schema.`,\n )\n }\n if (field.ai?.exclude) {\n return issue('invalid-field-value', `Field \"${change.field}\" must not be edited by the assistant.`)\n }\n\n const localized = isFieldLocalized(collection, change.field)\n if (!localized && change.locale !== undefined) {\n return issue('unknown-locale', `Field \"${change.field}\" is not localized — omit \"locale\".`)\n }\n if (\n change.locale !== undefined &&\n input.localeConfig &&\n !input.localeConfig.locales.includes(change.locale)\n ) {\n return issue(\n 'unknown-locale',\n `Unknown locale \"${change.locale}\" — this project has: ${input.localeConfig.locales.join(', ')}.`,\n )\n }\n const locale = localized ? change.locale ?? input.locale : undefined\n if (localized && locale === undefined) {\n return issue('missing-localized-value', `Field \"${change.field}\" is localized — provide \"locale\".`)\n }\n\n if (change.blockId !== undefined || change.prop !== undefined) {\n return applyBlockPropChange(doc, field, change, locale, issue)\n }\n\n const mapped = mapUpdateValue(field, change, input.document, input, issue)\n if ('issue' in mapped) return mapped\n return { doc: writeFieldValue(doc, change.field, locale, mapped.value) }\n}\n\n/** Identity for \"is this error pre-existing?\" — code + path + message. */\nfunction issueKey(issue: CmsValidationIssue): string {\n return `${issue.code}|${issue.path.join('.')}|${issue.message}`\n}\n\n/**\n * Build the reviewable update proposal: apply every change over a copy of the\n * document (loud targeting), validate the merged result with the injected\n * leashes (only errors the change INTRODUCES block), and report the semantic\n * diff. `no-change` when the changes resolve to the current values.\n */\nexport function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult {\n const { registry, collection, document } = input\n\n let next: CmsDocument = structuredClone(document)\n const targetingIssues: CmsValidationIssue[] = []\n for (const change of input.changes) {\n const outcome = applyOneChange(input, next, change)\n if ('issue' in outcome) targetingIssues.push(outcome.issue)\n else next = outcome.doc\n }\n if (targetingIssues.length > 0) return { ok: false, issues: targetingIssues }\n\n const validateOptions: ValidateDocumentOptions = {\n ...(input.store !== undefined ? { store: input.store } : {}),\n ...(input.localeConfig !== undefined ? { localeConfig: input.localeConfig } : {}),\n ...(input.validateComposition !== undefined\n ? { validateComposition: input.validateComposition }\n : {}),\n ...(input.validateRichTextEmbeds !== undefined\n ? { validateRichTextEmbeds: input.validateRichTextEmbeds }\n : {}),\n }\n const preExisting = new Set(\n validateDocument(registry, document, validateOptions)\n .filter((issue) => issue.severity === 'error')\n .map(issueKey),\n )\n const issues = validateDocument(registry, next, validateOptions)\n const introduced = issues.filter(\n (issue) => issue.severity === 'error' && !preExisting.has(issueKey(issue)),\n )\n if (introduced.length > 0) return { ok: false, issues: introduced }\n\n const diff = diffDocuments(collection, document, next)\n if (diff.changes.length === 0) return { ok: true, kind: 'no-change' }\n\n return {\n ok: true,\n kind: 'proposal',\n patch: {\n collection: collection.id,\n id: document.id,\n values: next.values,\n ...(next.localized && Object.keys(next.localized).length > 0\n ? { localized: next.localized }\n : {}),\n },\n before: {\n values: document.values,\n ...(document.localized ? { localized: document.localized } : {}),\n },\n changedFields: diff.changes.map((change) =>\n change.locale !== undefined ? `${change.field} (${change.locale})` : change.field,\n ),\n issues: issues.filter((issue) => issue.severity !== 'error'),\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"update-fields.js","sourceRoot":"","sources":["../../../src/lib/ai/update-fields.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow\n * counterpart of `applyContentWithShape`, minus the generation step).\n *\n * The MODEL authors the field changes directly in the `updateFields` tool input\n * (it has the conversation + `getDocument` output as context); this module turns\n * those changes into a reviewable proposal with the SAME engine discipline the\n * create flow has:\n *\n * - **merge semantics**: changes apply per field over the EXISTING document —\n * never a whole-document rewrite; the patch is the merged document, the diff\n * (`diffDocuments`) is what the editor reviews (changed fields only).\n * - **targeting is addressed with the engine's existing conventions** — field\n * name + optional locale bucket (the diff / `RestoreScope` shape) and, inside\n * a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,\n * root blocks by stable node id). No new path grammar.\n * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND\n * asset ids (EC-381) must come from searched candidates (`searchDocuments` /\n * `listAssets`) or already be on the document; every targeting failure is an\n * explicit issue — never a silent drop (unlike the create shape, which makes bad\n * ids structurally unrepresentable, an update change is imperative — so it FAILS\n * instead of being filtered).\n * - **validation parity**: the merged document runs through `validateDocument`\n * with the studio's injected leashes. Only errors the change INTRODUCES block\n * the proposal — pre-existing brokenness on the document is not the\n * assistant's doing and must not force a whole-document repair (the editor's\n * own save path tolerates it the same way; errors gate publish, not drafts).\n *\n * Locale-awareness: a localized field's change lands in the change's `locale`,\n * defaulting to the editor's working locale (`input.locale`). Non-localized\n * fields reject a `locale` loudly.\n *\n * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires\n * it to `getDocument` + the candidate store and hands the result to the review\n * sink.\n */\nimport {\n assetNode,\n cmsIssue,\n isFieldLocalized,\n readAssetId,\n readDocumentRef,\n referenceNode,\n validateDocument,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n FieldDef,\n FieldValues,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\nimport { blockRootsOf, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { introducedErrors, warningsOf } from './proposal-validation'\nimport { richTextValueFromModelInput } from './rich-text-input'\nimport type { AssetCandidates, ContentPatch, RelationCandidates } from './types'\n\n/**\n * One field change of an `updateFields` proposal, addressed with the engine's\n * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a\n * localized field), and for a `blocks` field optionally `blockId` + `prop` to\n * change one component prop of one root block. `value` is the new value\n * (`null`/omitted clears the field or prop).\n */\nexport interface UpdateFieldChange {\n field: string\n /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */\n locale?: string\n /** For a `blocks` field: the root block's stable node id (from `getDocument`). */\n blockId?: string\n /** With `blockId`: the component prop to change. */\n prop?: string\n /** The new value; `null` / omitted clears the field or prop. */\n value?: unknown\n}\n\nexport interface BuildUpdateProposalInput {\n registry: CollectionRegistry\n collection: CollectionDef\n /** The CURRENT document — the proposal's \"before\" side. Never mutated. */\n document: CmsDocument\n changes: readonly UpdateFieldChange[]\n /** The editor's working locale — the default bucket for localized fields. */\n locale?: string\n /** Relation candidates (searched ids) keyed by field name — the id leash. */\n candidates?: RelationCandidates\n /** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */\n assetCandidates?: AssetCandidates\n /**\n * THE validation option bag (EC-497) — the studio session's\n * `gateValidateOptions(...)`, whole. It also carries the project's\n * `localeConfig`, which is what the unknown-locale targeting check reads: one\n * source for \"which locales exist\", not two that can disagree.\n */\n validateOptions?: ValidateDocumentOptions\n}\n\n/** The `before` snapshot the review diff renders against. */\nexport interface UpdateBeforeSnapshot {\n values: FieldValues\n localized?: Record<string, FieldValues>\n}\n\nexport type BuildUpdateProposalResult =\n | {\n ok: true\n kind: 'proposal'\n /** The MERGED document as a patch (full values; the diff shows changed fields only). */\n patch: ContentPatch\n before: UpdateBeforeSnapshot\n /** Human-readable descriptors of the changed fields (e.g. `title (de)`). */\n changedFields: string[]\n /** Non-error issues on the accepted result (warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n /** Every change resolved to the current values — nothing to propose. */\n ok: true\n kind: 'no-change'\n }\n | { ok: false; issues: CmsValidationIssue[] }\n\ntype ChangeIssue = { issue: CmsValidationIssue }\ntype ChangeOutcome = { doc: CmsDocument } | ChangeIssue\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0\n}\n\n/**\n * The present/absent-key field write — the same semantics as the editor's\n * single-sourced `setFieldValue` (`lib/locale/fallback`): a resolved `locale`\n * writes the `localized[locale]` bucket, clearing DELETES the key. Local here\n * (not imported) because this module takes the locale as an already-resolved\n * argument per change; the semantics are pinned by tests on both sides.\n */\nfunction writeFieldValue(\n doc: CmsDocument,\n fieldName: string,\n locale: string | undefined,\n value: unknown,\n): CmsDocument {\n if (locale !== undefined) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n/** Relation ids CURRENTLY on the document for a field (any bucket) — re-usable without a search. */\nfunction currentRelationIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const ref = readDocumentRef(entry)\n if (ref) ids.add(ref.id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */\nfunction currentAssetIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const id = readAssetId(entry)\n if (id) ids.add(id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Map a change's raw value into the stored value shape (envelopes; loud leash). */\nfunction mapUpdateValue(\n field: FieldDef,\n change: UpdateFieldChange,\n document: CmsDocument,\n input: BuildUpdateProposalInput,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): { value: unknown } | ChangeIssue {\n const raw = change.value\n if (raw === undefined || raw === null) return { value: undefined } // clear\n switch (field.type) {\n case 'relation': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Relation \"${field.name}\" takes document id string(s) from searchDocuments results.`,\n )\n }\n // The id leash (same stance as the create enum, but LOUD): only searched\n // candidate ids or ids already on the document are usable.\n const allowed = new Set([\n ...(input.candidates?.[field.name] ?? []).map((c) => c.id),\n ...currentRelationIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-relation-target',\n `Relation \"${field.name}\" may only use ids from searchDocuments results — search \"${field.target}\" first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single reference — pass one id.`)\n }\n const refs = ids.map((id) => referenceNode({ collection: field.target, id }))\n return { value: field.multiple ? refs : refs[0] }\n }\n case 'asset': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Asset field \"${field.name}\" takes asset id string(s) from listAssets results.`,\n )\n }\n // EC-381 — the asset id leash (same stance as relations, but LOUD): only\n // searched library candidate ids or ids already on the document are usable;\n // the assistant never invents or uploads an asset.\n const allowed = new Set([\n ...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),\n ...currentAssetIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-asset',\n `Asset field \"${field.name}\" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single asset — pass one id.`)\n }\n const nodes = ids.map((id) => assetNode(id))\n return { value: field.multiple ? nodes : nodes[0] }\n }\n case 'richText': {\n // EC-379: a string value is MARKDOWN, converted through the engine's strict\n // subset converter; out-of-subset constructs fail LOUD (the model retries\n // in-conversation). A `{ version, doc }` envelope object stays accepted for\n // robustness (getDocument may have shown one) — see rich-text-input.ts.\n const converted = richTextValueFromModelInput(raw)\n if (!converted.ok) {\n return issue('invalid-field-value', `Field \"${field.name}\": ${converted.problems.join(' ')}`)\n }\n return { value: converted.value }\n }\n default:\n // text / number / boolean / date / select / object / blocks: as-is\n // (the merged document's validateDocument run is the shape leash).\n return { value: raw }\n }\n}\n\n/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */\nfunction applyBlockPropChange(\n doc: CmsDocument,\n field: FieldDef,\n change: UpdateFieldChange,\n locale: string | undefined,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): ChangeOutcome {\n if (field.type !== 'blocks') {\n return issue(\n 'invalid-field-value',\n `\"${change.field}\" is not a blocks field — omit \"blockId\"/\"prop\" (set the field value directly).`,\n )\n }\n if (!isNonEmptyString(change.blockId)) {\n return issue(\n 'invalid-field-value',\n `Changing a block on \"${change.field}\" needs \"blockId\" — block ids come from getDocument.`,\n )\n }\n if (!isNonEmptyString(change.prop)) {\n return issue(\n 'invalid-field-value',\n `Changing block \"${change.blockId}\" needs \"prop\" — the component property to change.`,\n )\n }\n const bucket = locale !== undefined ? doc.localized?.[locale] : doc.values\n const original = bucket?.[change.field]\n const blocks = blockRootsOf(original)\n const index = blocks.findIndex((block) => block.id === change.blockId)\n if (index < 0) {\n return issue(\n 'invalid-field-value',\n `Block \"${change.blockId}\" was not found on \"${change.field}\" — read the document first; block ids come from getDocument.`,\n )\n }\n const block = blocks[index]!\n const props = { ...(block.props ?? {}) }\n if (change.value === undefined || change.value === null) delete props[change.prop]\n else props[change.prop] = staticProp(change.value as JsonValue)\n const nextBlock: ComponentNode = { ...block, props }\n const nextBlocks = blocks.map((b, i) => (i === index ? nextBlock : b))\n // Preserve the stored shape: an array for a `multiple` composition, a bare node otherwise.\n const value = Array.isArray(original) ? nextBlocks : nextBlocks[0]\n return { doc: writeFieldValue(doc, change.field, locale, value) }\n}\n\n/** Apply ONE change to a (copy of the) document, or report the targeting issue. */\nfunction applyOneChange(\n input: BuildUpdateProposalInput,\n doc: CmsDocument,\n change: UpdateFieldChange,\n): ChangeOutcome {\n const { collection } = input\n const issue = (\n code: Parameters<typeof cmsIssue>[0]['code'],\n message: string,\n ): ChangeIssue => ({\n issue: cmsIssue({\n code,\n message,\n path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],\n collectionId: collection.id,\n documentId: doc.id,\n meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },\n }),\n })\n\n const field = collection.fields.find((f) => f.name === change.field)\n if (!field) {\n return issue(\n 'invalid-field-value',\n `Unknown field \"${change.field}\" on collection \"${collection.id}\" — use a field name from the schema.`,\n )\n }\n if (field.ai?.exclude) {\n return issue('invalid-field-value', `Field \"${change.field}\" must not be edited by the assistant.`)\n }\n\n const localized = isFieldLocalized(collection, change.field)\n if (!localized && change.locale !== undefined) {\n return issue('unknown-locale', `Field \"${change.field}\" is not localized — omit \"locale\".`)\n }\n if (\n change.locale !== undefined &&\n input.validateOptions?.localeConfig &&\n !input.validateOptions.localeConfig.locales.includes(change.locale)\n ) {\n return issue(\n 'unknown-locale',\n `Unknown locale \"${change.locale}\" — this project has: ${input.validateOptions?.localeConfig?.locales.join(', ')}.`,\n )\n }\n const locale = localized ? change.locale ?? input.locale : undefined\n if (localized && locale === undefined) {\n return issue('missing-localized-value', `Field \"${change.field}\" is localized — provide \"locale\".`)\n }\n\n if (change.blockId !== undefined || change.prop !== undefined) {\n return applyBlockPropChange(doc, field, change, locale, issue)\n }\n\n const mapped = mapUpdateValue(field, change, input.document, input, issue)\n if ('issue' in mapped) return mapped\n return { doc: writeFieldValue(doc, change.field, locale, mapped.value) }\n}\n\n\n/**\n * Build the reviewable update proposal: apply every change over a copy of the\n * document (loud targeting), validate the merged result with the injected\n * leashes (only errors the change INTRODUCES block), and report the semantic\n * diff. `no-change` when the changes resolve to the current values.\n */\nexport function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult {\n const { registry, collection, document } = input\n\n let next: CmsDocument = structuredClone(document)\n const targetingIssues: CmsValidationIssue[] = []\n for (const change of input.changes) {\n const outcome = applyOneChange(input, next, change)\n if ('issue' in outcome) targetingIssues.push(outcome.issue)\n else next = outcome.doc\n }\n if (targetingIssues.length > 0) return { ok: false, issues: targetingIssues }\n\n // EC-497: THE bag, whole — no per-call-site reassembly.\n const validateOptions: ValidateDocumentOptions = input.validateOptions ?? {}\n const before = validateDocument(registry, document, validateOptions)\n const issues = validateDocument(registry, next, validateOptions)\n const introduced = introducedErrors(before, issues)\n if (introduced.length > 0) return { ok: false, issues: introduced }\n\n const diff = diffDocuments(collection, document, next)\n if (diff.changes.length === 0) return { ok: true, kind: 'no-change' }\n\n return {\n ok: true,\n kind: 'proposal',\n patch: {\n collection: collection.id,\n id: document.id,\n values: next.values,\n ...(next.localized && Object.keys(next.localized).length > 0\n ? { localized: next.localized }\n : {}),\n },\n before: {\n values: document.values,\n ...(document.localized ? { localized: document.localized } : {}),\n },\n changedFields: diff.changes.map((change) =>\n change.locale !== undefined ? `${change.field} (${change.locale})` : change.field,\n ),\n issues: warningsOf(issues),\n }\n}\n"]}
|
|
@@ -7,6 +7,11 @@ import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
|
7
7
|
* project can resolve), and every mismatch becomes a visible, node-addressed
|
|
8
8
|
* issue the notice deep-links. The renderer's existing fallbacks keep the
|
|
9
9
|
* pasted nodes visible meanwhile.
|
|
10
|
+
*
|
|
11
|
+
* EC-494: this diff runs STUDIO-side (it needs the target registry and the session's
|
|
12
|
+
* data sources), so it holds the active locale and can call `translate` directly —
|
|
13
|
+
* no message ref has to cross a boundary, unlike the engine-built clipboard errors
|
|
14
|
+
* in `@elytracms/core/clipboard`.
|
|
10
15
|
*/
|
|
11
16
|
export interface PasteIssue {
|
|
12
17
|
kind: 'missing-component' | 'unresolved-binding';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
1
2
|
function walk(node, visit) {
|
|
2
3
|
visit(node);
|
|
3
4
|
for (const children of Object.values(node.slots ?? {})) {
|
|
@@ -32,7 +33,9 @@ export function diffPastedSubtree(args) {
|
|
|
32
33
|
if (!unresolvedBindings.has(key)) {
|
|
33
34
|
unresolvedBindings.set(key, {
|
|
34
35
|
kind: 'unresolved-binding',
|
|
35
|
-
message:
|
|
36
|
+
message: translate('builder.clipboard.issue.unresolvedBinding', {
|
|
37
|
+
sourceId: binding.sourceId,
|
|
38
|
+
}),
|
|
36
39
|
nodeId: node.id,
|
|
37
40
|
});
|
|
38
41
|
}
|
|
@@ -42,7 +45,7 @@ export function diffPastedSubtree(args) {
|
|
|
42
45
|
...[...unresolvedBindings.values()],
|
|
43
46
|
...[...missingComponents.entries()].map(([componentId, nodeId]) => ({
|
|
44
47
|
kind: 'missing-component',
|
|
45
|
-
message:
|
|
48
|
+
message: translate('builder.clipboard.issue.missingComponent', { componentId }),
|
|
46
49
|
nodeId,
|
|
47
50
|
})),
|
|
48
51
|
];
|
|
@@ -56,12 +59,19 @@ export function diffPastedSubtree(args) {
|
|
|
56
59
|
export function pasteSummaryMessage(diff) {
|
|
57
60
|
const parts = [];
|
|
58
61
|
const bindings = diff.issues.filter((i) => i.kind === 'unresolved-binding').length;
|
|
59
|
-
if (bindings > 0)
|
|
60
|
-
parts.push(
|
|
62
|
+
if (bindings > 0) {
|
|
63
|
+
parts.push(translate(bindings === 1
|
|
64
|
+
? 'builder.clipboard.summary.bindingUnresolvedOne'
|
|
65
|
+
: 'builder.clipboard.summary.bindingsUnresolved', { count: bindings }));
|
|
66
|
+
}
|
|
61
67
|
if (diff.missingComponents.length > 0) {
|
|
62
68
|
const n = diff.missingComponents.length;
|
|
63
|
-
parts.push(
|
|
69
|
+
parts.push(translate(n === 1
|
|
70
|
+
? 'builder.clipboard.summary.componentMissingOne'
|
|
71
|
+
: 'builder.clipboard.summary.componentsMissing', { count: n }));
|
|
64
72
|
}
|
|
65
|
-
return parts.length > 0
|
|
73
|
+
return parts.length > 0
|
|
74
|
+
? translate('builder.clipboard.summary.pastedWith', { parts: parts.join(', ') })
|
|
75
|
+
: translate('builder.clipboard.summary.pasted');
|
|
66
76
|
}
|
|
67
77
|
//# sourceMappingURL=paste-issues.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paste-issues.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-issues.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"paste-issues.js","sourceRoot":"","sources":["../../../src/lib/clipboard/paste-issues.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BindingReference, ComponentNode } from '@elytracms/core/project-graph'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Permissive-paste validation (EC-175): pasting NEVER refuses a structurally\n * valid fragment. Instead, the inserted subtree is diffed against the TARGET\n * project's registry and data sources at paste time (cross-project semantics:\n * the fragment's source project is irrelevant — what matters is what *this*\n * project can resolve), and every mismatch becomes a visible, node-addressed\n * issue the notice deep-links. The renderer's existing fallbacks keep the\n * pasted nodes visible meanwhile.\n *\n * EC-494: this diff runs STUDIO-side (it needs the target registry and the session's\n * data sources), so it holds the active locale and can call `translate` directly —\n * no message ref has to cross a boundary, unlike the engine-built clipboard errors\n * in `@elytracms/core/clipboard`.\n */\n\nexport interface PasteIssue {\n kind: 'missing-component' | 'unresolved-binding'\n message: string\n /** The inserted node carrying the problem — deep-link target. */\n nodeId: string\n}\n\nexport interface PasteDiff {\n /** Distinct component ids absent from the target registry. */\n missingComponents: string[]\n /** Distinct binding source ids the target cannot resolve. */\n unresolvedBindings: string[]\n issues: PasteIssue[]\n}\n\nfunction walk(node: ComponentNode, visit: (node: ComponentNode) => void): void {\n visit(node)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) walk(child, visit)\n }\n}\n\n/**\n * Diff an inserted subtree (with its final, session-assigned node ids)\n * against the target project's registry + known data sources.\n */\nexport function diffPastedSubtree(args: {\n node: ComponentNode\n hasComponent: (componentId: string) => boolean\n knownSourceIds: ReadonlySet<string>\n}): PasteDiff {\n const missingComponents = new Map<string, string>() // componentId -> first nodeId\n const unresolvedBindings = new Map<string, PasteIssue>() // nodeId+sourceId -> issue\n const unresolvedSourceIds = new Set<string>()\n walk(args.node, (node) => {\n if (!args.hasComponent(node.componentId) && !missingComponents.has(node.componentId)) {\n missingComponents.set(node.componentId, node.id)\n }\n const bindings: BindingReference[] = []\n for (const value of Object.values(node.props ?? {})) {\n if (value.kind === 'binding') bindings.push(value.binding)\n }\n if (node.condition) bindings.push(node.condition.source)\n for (const binding of bindings) {\n if (args.knownSourceIds.has(binding.sourceId)) continue\n unresolvedSourceIds.add(binding.sourceId)\n const key = `${node.id} ${binding.sourceId}`\n if (!unresolvedBindings.has(key)) {\n unresolvedBindings.set(key, {\n kind: 'unresolved-binding',\n message: translate('builder.clipboard.issue.unresolvedBinding', {\n sourceId: binding.sourceId,\n }),\n nodeId: node.id,\n })\n }\n }\n })\n const issues: PasteIssue[] = [\n ...[...unresolvedBindings.values()],\n ...[...missingComponents.entries()].map(\n ([componentId, nodeId]): PasteIssue => ({\n kind: 'missing-component',\n message: translate('builder.clipboard.issue.missingComponent', { componentId }),\n nodeId,\n }),\n ),\n ]\n return {\n missingComponents: [...missingComponents.keys()],\n unresolvedBindings: [...unresolvedSourceIds],\n issues,\n }\n}\n\n/** “Pasted — 2 bindings unresolved, 1 component missing”, or just “Pasted”. */\nexport function pasteSummaryMessage(diff: PasteDiff): string {\n const parts: string[] = []\n const bindings = diff.issues.filter((i) => i.kind === 'unresolved-binding').length\n if (bindings > 0) {\n parts.push(\n translate(\n bindings === 1\n ? 'builder.clipboard.summary.bindingUnresolvedOne'\n : 'builder.clipboard.summary.bindingsUnresolved',\n { count: bindings },\n ),\n )\n }\n if (diff.missingComponents.length > 0) {\n const n = diff.missingComponents.length\n parts.push(\n translate(\n n === 1\n ? 'builder.clipboard.summary.componentMissingOne'\n : 'builder.clipboard.summary.componentsMissing',\n { count: n },\n ),\n )\n }\n return parts.length > 0\n ? translate('builder.clipboard.summary.pastedWith', { parts: parts.join(', ') })\n : translate('builder.clipboard.summary.pasted')\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CmsDocument, CmsValidationIssue } from '@elytracms/core/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* The publish-gate seam (EC-497) — a framework-free module store, the same shape
|
|
4
|
+
* as the current-document seam (`lib/ai-current-document`).
|
|
5
|
+
*
|
|
6
|
+
* `createOperations` is a module singleton built long before (and independently
|
|
7
|
+
* of) any React tree, while gate-strength validation needs the live CMS session:
|
|
8
|
+
* its document store, its asset oracle, its published-ness pointer. So the CMS
|
|
9
|
+
* session PUBLISHES a validator here while mounted, and
|
|
10
|
+
* `createStudioOperations` hands this module's reader to the operations layer
|
|
11
|
+
* once, at bootstrap.
|
|
12
|
+
*
|
|
13
|
+
* The reader deliberately returns `undefined` when nothing is registered rather
|
|
14
|
+
* than an empty issue list: `undefined` means "no session context here" and puts
|
|
15
|
+
* the operations gate on its documented adapter-record fallback, whereas `[]`
|
|
16
|
+
* would silently claim the document is clean. A gate that cannot see must say so,
|
|
17
|
+
* not pass.
|
|
18
|
+
*
|
|
19
|
+
* This module imports nothing but types, so hanging the seam off it never drags
|
|
20
|
+
* the CMS session (or the component registry behind the composition validators)
|
|
21
|
+
* into the operations bootstrap's module graph.
|
|
22
|
+
*/
|
|
23
|
+
export type SessionDocumentValidator = (document: CmsDocument) => readonly CmsValidationIssue[];
|
|
24
|
+
/** Publish the session's canonical document validator (`null` clears the stack). */
|
|
25
|
+
export declare function setSessionDocumentValidator(next: SessionDocumentValidator | null): void;
|
|
26
|
+
/**
|
|
27
|
+
* Remove this validator — the unmount race guard. Removing an INNER session
|
|
28
|
+
* leaves the outer one in place; removing one that already left is a no-op (a
|
|
29
|
+
* remount registers the new validator before the old effect's cleanup runs).
|
|
30
|
+
*/
|
|
31
|
+
export declare function clearSessionDocumentValidator(owned: SessionDocumentValidator): void;
|
|
32
|
+
/**
|
|
33
|
+
* The reader handed to `createOperations`. `undefined` = no session ⇒ the gate
|
|
34
|
+
* falls back; see {@link SessionDocumentValidator} and the operations layer's
|
|
35
|
+
* `DocumentValidator`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sessionValidateDocument(document: CmsDocument): readonly CmsValidationIssue[] | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registered sessions, innermost LAST — a stack, not a single slot, because more
|
|
3
|
+
* than one `CmsProvider` can be mounted at a time: the "Use in document…"
|
|
4
|
+
* transfer dialog mounts its own over the app's
|
|
5
|
+
* (`components/transfer-dialog.tsx:92`). With a single slot, closing that dialog
|
|
6
|
+
* would clear the seam and silently drop the publish gate back to its fallback
|
|
7
|
+
* for the rest of the app's life — a regression invisible until something
|
|
8
|
+
* published. Popping restores the outer session instead.
|
|
9
|
+
*/
|
|
10
|
+
const registered = [];
|
|
11
|
+
/** Publish the session's canonical document validator (`null` clears the stack). */
|
|
12
|
+
export function setSessionDocumentValidator(next) {
|
|
13
|
+
if (next === null) {
|
|
14
|
+
registered.length = 0;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const at = registered.indexOf(next);
|
|
18
|
+
if (at >= 0)
|
|
19
|
+
registered.splice(at, 1);
|
|
20
|
+
registered.push(next);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Remove this validator — the unmount race guard. Removing an INNER session
|
|
24
|
+
* leaves the outer one in place; removing one that already left is a no-op (a
|
|
25
|
+
* remount registers the new validator before the old effect's cleanup runs).
|
|
26
|
+
*/
|
|
27
|
+
export function clearSessionDocumentValidator(owned) {
|
|
28
|
+
const at = registered.indexOf(owned);
|
|
29
|
+
if (at >= 0)
|
|
30
|
+
registered.splice(at, 1);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The reader handed to `createOperations`. `undefined` = no session ⇒ the gate
|
|
34
|
+
* falls back; see {@link SessionDocumentValidator} and the operations layer's
|
|
35
|
+
* `DocumentValidator`.
|
|
36
|
+
*/
|
|
37
|
+
export function sessionValidateDocument(document) {
|
|
38
|
+
return registered.at(-1)?.(document);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=document-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-gate.js","sourceRoot":"","sources":["../../../src/lib/cms/document-gate.ts"],"names":[],"mappings":"AAyBA;;;;;;;;GAQG;AACH,MAAM,UAAU,GAA+B,EAAE,CAAA;AAEjD,oFAAoF;AACpF,MAAM,UAAU,2BAA2B,CAAC,IAAqC;IAC/E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,UAAU,CAAC,MAAM,GAAG,CAAC,CAAA;QACrB,OAAM;IACR,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IACrC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAA+B;IAC3E,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACpC,IAAI,EAAE,IAAI,CAAC;QAAE,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,QAAqB;IAErB,OAAO,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAA;AACtC,CAAC","sourcesContent":["import type { CmsDocument, CmsValidationIssue } from '@elytracms/core/cms-core'\n\n/**\n * The publish-gate seam (EC-497) — a framework-free module store, the same shape\n * as the current-document seam (`lib/ai-current-document`).\n *\n * `createOperations` is a module singleton built long before (and independently\n * of) any React tree, while gate-strength validation needs the live CMS session:\n * its document store, its asset oracle, its published-ness pointer. So the CMS\n * session PUBLISHES a validator here while mounted, and\n * `createStudioOperations` hands this module's reader to the operations layer\n * once, at bootstrap.\n *\n * The reader deliberately returns `undefined` when nothing is registered rather\n * than an empty issue list: `undefined` means \"no session context here\" and puts\n * the operations gate on its documented adapter-record fallback, whereas `[]`\n * would silently claim the document is clean. A gate that cannot see must say so,\n * not pass.\n *\n * This module imports nothing but types, so hanging the seam off it never drags\n * the CMS session (or the component registry behind the composition validators)\n * into the operations bootstrap's module graph.\n */\nexport type SessionDocumentValidator = (document: CmsDocument) => readonly CmsValidationIssue[]\n\n/**\n * Registered sessions, innermost LAST — a stack, not a single slot, because more\n * than one `CmsProvider` can be mounted at a time: the \"Use in document…\"\n * transfer dialog mounts its own over the app's\n * (`components/transfer-dialog.tsx:92`). With a single slot, closing that dialog\n * would clear the seam and silently drop the publish gate back to its fallback\n * for the rest of the app's life — a regression invisible until something\n * published. Popping restores the outer session instead.\n */\nconst registered: SessionDocumentValidator[] = []\n\n/** Publish the session's canonical document validator (`null` clears the stack). */\nexport function setSessionDocumentValidator(next: SessionDocumentValidator | null): void {\n if (next === null) {\n registered.length = 0\n return\n }\n const at = registered.indexOf(next)\n if (at >= 0) registered.splice(at, 1)\n registered.push(next)\n}\n\n/**\n * Remove this validator — the unmount race guard. Removing an INNER session\n * leaves the outer one in place; removing one that already left is a no-op (a\n * remount registers the new validator before the old effect's cleanup runs).\n */\nexport function clearSessionDocumentValidator(owned: SessionDocumentValidator): void {\n const at = registered.indexOf(owned)\n if (at >= 0) registered.splice(at, 1)\n}\n\n/**\n * The reader handed to `createOperations`. `undefined` = no session ⇒ the gate\n * falls back; see {@link SessionDocumentValidator} and the operations layer's\n * `DocumentValidator`.\n */\nexport function sessionValidateDocument(\n document: CmsDocument,\n): readonly CmsValidationIssue[] | undefined {\n return registered.at(-1)?.(document)\n}\n"]}
|
|
@@ -218,6 +218,11 @@ export interface ClauseIssue {
|
|
|
218
218
|
* operator/value shape); state clauses check the `eq`/`in` subset and the
|
|
219
219
|
* draft/published value set. Invalid clauses (e.g. a persisted view whose
|
|
220
220
|
* field is no longer filterable) are reported — never silently applied.
|
|
221
|
+
*
|
|
222
|
+
* EC-494 — the two `$state` sentences are authored HERE, studio-side, so they call
|
|
223
|
+
* `translate` directly; the schema-field ones come from `core/content`, below the
|
|
224
|
+
* catalog in the DAG, and are resolved from the `messageRef` that error now carries.
|
|
225
|
+
* Both halves of one line therefore render in the same language.
|
|
221
226
|
*/
|
|
222
227
|
export declare function validateClauses(collection: CollectionDef, clauses: readonly FilterClause[]): ClauseIssue[];
|
|
223
228
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { documentDefaultLocale, localizedStringSchema, resolveField, resolveLabel, titleFieldOf, } from '@elytracms/core/cms-core';
|
|
3
|
-
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
3
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
import { contentQueryErrorMessage } from '@elytracms/studio/lib/persona/labels';
|
|
4
5
|
import { FILTER_OPERATORS, compareListEntries, filterScalarOf, filterableFields, matchesFieldValue, validateWhere, } from '@elytracms/core/content';
|
|
5
6
|
/**
|
|
6
7
|
* The pure table-view model for the CMS collection table (EC-169).
|
|
@@ -303,18 +304,23 @@ export function scalarListToText(value) {
|
|
|
303
304
|
* operator/value shape); state clauses check the `eq`/`in` subset and the
|
|
304
305
|
* draft/published value set. Invalid clauses (e.g. a persisted view whose
|
|
305
306
|
* field is no longer filterable) are reported — never silently applied.
|
|
307
|
+
*
|
|
308
|
+
* EC-494 — the two `$state` sentences are authored HERE, studio-side, so they call
|
|
309
|
+
* `translate` directly; the schema-field ones come from `core/content`, below the
|
|
310
|
+
* catalog in the DAG, and are resolved from the `messageRef` that error now carries.
|
|
311
|
+
* Both halves of one line therefore render in the same language.
|
|
306
312
|
*/
|
|
307
313
|
export function validateClauses(collection, clauses) {
|
|
308
314
|
const issues = [];
|
|
309
315
|
clauses.forEach((clause, index) => {
|
|
310
316
|
if (clause.field === STATE_FIELD) {
|
|
311
317
|
if (clause.op !== 'eq' && clause.op !== 'in') {
|
|
312
|
-
issues.push({ index, message: '
|
|
318
|
+
issues.push({ index, message: translate('cms.filter.issue.stateOperator') });
|
|
313
319
|
return;
|
|
314
320
|
}
|
|
315
321
|
const values = Array.isArray(clause.value) ? clause.value : [clause.value];
|
|
316
322
|
if (!values.every((value) => STATE_VALUES.includes(value))) {
|
|
317
|
-
issues.push({ index, message: '
|
|
323
|
+
issues.push({ index, message: translate('cms.filter.issue.stateValue') });
|
|
318
324
|
}
|
|
319
325
|
return;
|
|
320
326
|
}
|
|
@@ -323,7 +329,7 @@ export function validateClauses(collection, clauses) {
|
|
|
323
329
|
};
|
|
324
330
|
const errors = validateWhere(collection, where);
|
|
325
331
|
if (errors.length > 0)
|
|
326
|
-
issues.push({ index, message: errors[0]
|
|
332
|
+
issues.push({ index, message: contentQueryErrorMessage(errors[0]) });
|
|
327
333
|
});
|
|
328
334
|
return issues;
|
|
329
335
|
}
|