@elytracms/studio 0.0.57 → 0.0.58
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/dist/components/ai/ai-chat-panel.js +44 -6
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/chat-ui.d.ts +1 -1
- package/dist/components/ai/chat-ui.js +11 -2
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +30 -7
- package/dist/components/ai/proposal-card.js +81 -21
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/cms/detail-editor.js +12 -0
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +12 -1
- package/dist/components/cms/document-diff-view.js +119 -42
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/rich-text-preview.d.ts +15 -0
- package/dist/components/cms/rich-text-preview.js +81 -0
- package/dist/components/cms/rich-text-preview.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +26 -0
- package/dist/components/cms/version-compare-text.js +60 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/lib/ai/apply-content.js +52 -10
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +19 -3
- package/dist/lib/ai/chat-context.js +33 -4
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +6 -3
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +60 -5
- package/dist/lib/ai/fake-provider.js +74 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- 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/rich-text-input.d.ts +27 -0
- package/dist/lib/ai/rich-text-input.js +25 -0
- package/dist/lib/ai/rich-text-input.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +12 -1
- package/dist/lib/ai/shape.js +46 -20
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +88 -13
- package/dist/lib/ai/tools.js +253 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +20 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +68 -0
- package/dist/lib/ai/update-fields.js +296 -0
- package/dist/lib/ai/update-fields.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +53 -0
- package/dist/lib/ai-current-document.js +54 -0
- package/dist/lib/ai-current-document.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/i18n/messages.en.js +10 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +2 -2
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/** 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 /** 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 /** 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"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, FieldValues, ValidateDocumentOptions, LocaleConfig } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { AssetCandidates, ContentPatch, RelationCandidates } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* One field change of an `updateFields` proposal, addressed with the engine's
|
|
5
|
+
* existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a
|
|
6
|
+
* localized field), and for a `blocks` field optionally `blockId` + `prop` to
|
|
7
|
+
* change one component prop of one root block. `value` is the new value
|
|
8
|
+
* (`null`/omitted clears the field or prop).
|
|
9
|
+
*/
|
|
10
|
+
export interface UpdateFieldChange {
|
|
11
|
+
field: string;
|
|
12
|
+
/** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */
|
|
13
|
+
locale?: string;
|
|
14
|
+
/** For a `blocks` field: the root block's stable node id (from `getDocument`). */
|
|
15
|
+
blockId?: string;
|
|
16
|
+
/** With `blockId`: the component prop to change. */
|
|
17
|
+
prop?: string;
|
|
18
|
+
/** The new value; `null` / omitted clears the field or prop. */
|
|
19
|
+
value?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface BuildUpdateProposalInput {
|
|
22
|
+
registry: CollectionRegistry;
|
|
23
|
+
collection: CollectionDef;
|
|
24
|
+
/** The CURRENT document — the proposal's "before" side. Never mutated. */
|
|
25
|
+
document: CmsDocument;
|
|
26
|
+
changes: readonly UpdateFieldChange[];
|
|
27
|
+
/** The editor's working locale — the default bucket for localized fields. */
|
|
28
|
+
locale?: string;
|
|
29
|
+
localeConfig?: LocaleConfig;
|
|
30
|
+
/** Relation candidates (searched ids) keyed by field name — the id leash. */
|
|
31
|
+
candidates?: RelationCandidates;
|
|
32
|
+
/** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */
|
|
33
|
+
assetCandidates?: AssetCandidates;
|
|
34
|
+
/** Injected studio leashes — same discipline as the create engine. */
|
|
35
|
+
validateRichTextEmbeds?: ValidateDocumentOptions['validateRichTextEmbeds'];
|
|
36
|
+
validateComposition?: ValidateDocumentOptions['validateComposition'];
|
|
37
|
+
store?: ValidateDocumentOptions['store'];
|
|
38
|
+
}
|
|
39
|
+
/** The `before` snapshot the review diff renders against. */
|
|
40
|
+
export interface UpdateBeforeSnapshot {
|
|
41
|
+
values: FieldValues;
|
|
42
|
+
localized?: Record<string, FieldValues>;
|
|
43
|
+
}
|
|
44
|
+
export type BuildUpdateProposalResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
kind: 'proposal';
|
|
47
|
+
/** The MERGED document as a patch (full values; the diff shows changed fields only). */
|
|
48
|
+
patch: ContentPatch;
|
|
49
|
+
before: UpdateBeforeSnapshot;
|
|
50
|
+
/** Human-readable descriptors of the changed fields (e.g. `title (de)`). */
|
|
51
|
+
changedFields: string[];
|
|
52
|
+
/** Non-error issues on the accepted result (warnings). */
|
|
53
|
+
issues: CmsValidationIssue[];
|
|
54
|
+
} | {
|
|
55
|
+
/** Every change resolved to the current values — nothing to propose. */
|
|
56
|
+
ok: true;
|
|
57
|
+
kind: 'no-change';
|
|
58
|
+
} | {
|
|
59
|
+
ok: false;
|
|
60
|
+
issues: CmsValidationIssue[];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Build the reviewable update proposal: apply every change over a copy of the
|
|
64
|
+
* document (loud targeting), validate the merged result with the injected
|
|
65
|
+
* leashes (only errors the change INTRODUCES block), and report the semantic
|
|
66
|
+
* diff. `no-change` when the changes resolve to the current values.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow
|
|
3
|
+
* counterpart of `applyContentWithShape`, minus the generation step).
|
|
4
|
+
*
|
|
5
|
+
* The MODEL authors the field changes directly in the `updateFields` tool input
|
|
6
|
+
* (it has the conversation + `getDocument` output as context); this module turns
|
|
7
|
+
* those changes into a reviewable proposal with the SAME engine discipline the
|
|
8
|
+
* create flow has:
|
|
9
|
+
*
|
|
10
|
+
* - **merge semantics**: changes apply per field over the EXISTING document —
|
|
11
|
+
* never a whole-document rewrite; the patch is the merged document, the diff
|
|
12
|
+
* (`diffDocuments`) is what the editor reviews (changed fields only).
|
|
13
|
+
* - **targeting is addressed with the engine's existing conventions** — field
|
|
14
|
+
* name + optional locale bucket (the diff / `RestoreScope` shape) and, inside
|
|
15
|
+
* a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,
|
|
16
|
+
* root blocks by stable node id). No new path grammar.
|
|
17
|
+
* - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND
|
|
18
|
+
* asset ids (EC-381) must come from searched candidates (`searchDocuments` /
|
|
19
|
+
* `listAssets`) or already be on the document; every targeting failure is an
|
|
20
|
+
* explicit issue — never a silent drop (unlike the create shape, which makes bad
|
|
21
|
+
* ids structurally unrepresentable, an update change is imperative — so it FAILS
|
|
22
|
+
* instead of being filtered).
|
|
23
|
+
* - **validation parity**: the merged document runs through `validateDocument`
|
|
24
|
+
* with the studio's injected leashes. Only errors the change INTRODUCES block
|
|
25
|
+
* the proposal — pre-existing brokenness on the document is not the
|
|
26
|
+
* assistant's doing and must not force a whole-document repair (the editor's
|
|
27
|
+
* own save path tolerates it the same way; errors gate publish, not drafts).
|
|
28
|
+
*
|
|
29
|
+
* Locale-awareness: a localized field's change lands in the change's `locale`,
|
|
30
|
+
* defaulting to the editor's working locale (`input.locale`). Non-localized
|
|
31
|
+
* fields reject a `locale` loudly.
|
|
32
|
+
*
|
|
33
|
+
* Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires
|
|
34
|
+
* it to `getDocument` + the candidate store and hands the result to the review
|
|
35
|
+
* sink.
|
|
36
|
+
*/
|
|
37
|
+
import { assetNode, cmsIssue, isFieldLocalized, readAssetId, readDocumentRef, referenceNode, validateDocument, } from '@elytracms/core/cms-core';
|
|
38
|
+
import { blockRootsOf, staticProp } from '@elytracms/core/project-graph';
|
|
39
|
+
import { diffDocuments } from '@elytracms/core/content';
|
|
40
|
+
import { richTextValueFromModelInput } from './rich-text-input.js';
|
|
41
|
+
function isNonEmptyString(value) {
|
|
42
|
+
return typeof value === 'string' && value.length > 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The present/absent-key field write — the same semantics as the editor's
|
|
46
|
+
* single-sourced `setFieldValue` (`lib/locale/fallback`): a resolved `locale`
|
|
47
|
+
* writes the `localized[locale]` bucket, clearing DELETES the key. Local here
|
|
48
|
+
* (not imported) because this module takes the locale as an already-resolved
|
|
49
|
+
* argument per change; the semantics are pinned by tests on both sides.
|
|
50
|
+
*/
|
|
51
|
+
function writeFieldValue(doc, fieldName, locale, value) {
|
|
52
|
+
if (locale !== undefined) {
|
|
53
|
+
const perLocale = { ...(doc.localized?.[locale] ?? {}) };
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
delete perLocale[fieldName];
|
|
56
|
+
else
|
|
57
|
+
perLocale[fieldName] = value;
|
|
58
|
+
return { ...doc, localized: { ...doc.localized, [locale]: perLocale } };
|
|
59
|
+
}
|
|
60
|
+
const values = { ...doc.values };
|
|
61
|
+
if (value === undefined)
|
|
62
|
+
delete values[fieldName];
|
|
63
|
+
else
|
|
64
|
+
values[fieldName] = value;
|
|
65
|
+
return { ...doc, values };
|
|
66
|
+
}
|
|
67
|
+
/** Relation ids CURRENTLY on the document for a field (any bucket) — re-usable without a search. */
|
|
68
|
+
function currentRelationIds(doc, fieldName) {
|
|
69
|
+
const ids = new Set();
|
|
70
|
+
const collect = (value) => {
|
|
71
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
72
|
+
const ref = readDocumentRef(entry);
|
|
73
|
+
if (ref)
|
|
74
|
+
ids.add(ref.id);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
collect(doc.values[fieldName]);
|
|
78
|
+
for (const bucket of Object.values(doc.localized ?? {}))
|
|
79
|
+
collect(bucket[fieldName]);
|
|
80
|
+
return ids;
|
|
81
|
+
}
|
|
82
|
+
/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */
|
|
83
|
+
function currentAssetIds(doc, fieldName) {
|
|
84
|
+
const ids = new Set();
|
|
85
|
+
const collect = (value) => {
|
|
86
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
87
|
+
const id = readAssetId(entry);
|
|
88
|
+
if (id)
|
|
89
|
+
ids.add(id);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
collect(doc.values[fieldName]);
|
|
93
|
+
for (const bucket of Object.values(doc.localized ?? {}))
|
|
94
|
+
collect(bucket[fieldName]);
|
|
95
|
+
return ids;
|
|
96
|
+
}
|
|
97
|
+
/** Map a change's raw value into the stored value shape (envelopes; loud leash). */
|
|
98
|
+
function mapUpdateValue(field, change, document, input, issue) {
|
|
99
|
+
const raw = change.value;
|
|
100
|
+
if (raw === undefined || raw === null)
|
|
101
|
+
return { value: undefined }; // clear
|
|
102
|
+
switch (field.type) {
|
|
103
|
+
case 'relation': {
|
|
104
|
+
const ids = Array.isArray(raw) ? raw : [raw];
|
|
105
|
+
if (!ids.every(isNonEmptyString)) {
|
|
106
|
+
return issue('invalid-field-value', `Relation "${field.name}" takes document id string(s) from searchDocuments results.`);
|
|
107
|
+
}
|
|
108
|
+
// The id leash (same stance as the create enum, but LOUD): only searched
|
|
109
|
+
// candidate ids or ids already on the document are usable.
|
|
110
|
+
const allowed = new Set([
|
|
111
|
+
...(input.candidates?.[field.name] ?? []).map((c) => c.id),
|
|
112
|
+
...currentRelationIds(document, field.name),
|
|
113
|
+
]);
|
|
114
|
+
const unknown = ids.filter((id) => !allowed.has(id));
|
|
115
|
+
if (unknown.length > 0) {
|
|
116
|
+
return issue('unknown-relation-target', `Relation "${field.name}" may only use ids from searchDocuments results — search "${field.target}" first (got: ${unknown.join(', ')}).`);
|
|
117
|
+
}
|
|
118
|
+
if (!field.multiple && ids.length > 1) {
|
|
119
|
+
return issue('invalid-field-value', `Field "${field.name}" holds a single reference — pass one id.`);
|
|
120
|
+
}
|
|
121
|
+
const refs = ids.map((id) => referenceNode({ collection: field.target, id }));
|
|
122
|
+
return { value: field.multiple ? refs : refs[0] };
|
|
123
|
+
}
|
|
124
|
+
case 'asset': {
|
|
125
|
+
const ids = Array.isArray(raw) ? raw : [raw];
|
|
126
|
+
if (!ids.every(isNonEmptyString)) {
|
|
127
|
+
return issue('invalid-field-value', `Asset field "${field.name}" takes asset id string(s) from listAssets results.`);
|
|
128
|
+
}
|
|
129
|
+
// EC-381 — the asset id leash (same stance as relations, but LOUD): only
|
|
130
|
+
// searched library candidate ids or ids already on the document are usable;
|
|
131
|
+
// the assistant never invents or uploads an asset.
|
|
132
|
+
const allowed = new Set([
|
|
133
|
+
...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),
|
|
134
|
+
...currentAssetIds(document, field.name),
|
|
135
|
+
]);
|
|
136
|
+
const unknown = ids.filter((id) => !allowed.has(id));
|
|
137
|
+
if (unknown.length > 0) {
|
|
138
|
+
return issue('unknown-asset', `Asset field "${field.name}" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`);
|
|
139
|
+
}
|
|
140
|
+
if (!field.multiple && ids.length > 1) {
|
|
141
|
+
return issue('invalid-field-value', `Field "${field.name}" holds a single asset — pass one id.`);
|
|
142
|
+
}
|
|
143
|
+
const nodes = ids.map((id) => assetNode(id));
|
|
144
|
+
return { value: field.multiple ? nodes : nodes[0] };
|
|
145
|
+
}
|
|
146
|
+
case 'richText': {
|
|
147
|
+
// EC-379: a string value is MARKDOWN, converted through the engine's strict
|
|
148
|
+
// subset converter; out-of-subset constructs fail LOUD (the model retries
|
|
149
|
+
// in-conversation). A `{ version, doc }` envelope object stays accepted for
|
|
150
|
+
// robustness (getDocument may have shown one) — see rich-text-input.ts.
|
|
151
|
+
const converted = richTextValueFromModelInput(raw);
|
|
152
|
+
if (!converted.ok) {
|
|
153
|
+
return issue('invalid-field-value', `Field "${field.name}": ${converted.problems.join(' ')}`);
|
|
154
|
+
}
|
|
155
|
+
return { value: converted.value };
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
// text / number / boolean / date / select / object / blocks: as-is
|
|
159
|
+
// (the merged document's validateDocument run is the shape leash).
|
|
160
|
+
return { value: raw };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */
|
|
164
|
+
function applyBlockPropChange(doc, field, change, locale, issue) {
|
|
165
|
+
if (field.type !== 'blocks') {
|
|
166
|
+
return issue('invalid-field-value', `"${change.field}" is not a blocks field — omit "blockId"/"prop" (set the field value directly).`);
|
|
167
|
+
}
|
|
168
|
+
if (!isNonEmptyString(change.blockId)) {
|
|
169
|
+
return issue('invalid-field-value', `Changing a block on "${change.field}" needs "blockId" — block ids come from getDocument.`);
|
|
170
|
+
}
|
|
171
|
+
if (!isNonEmptyString(change.prop)) {
|
|
172
|
+
return issue('invalid-field-value', `Changing block "${change.blockId}" needs "prop" — the component property to change.`);
|
|
173
|
+
}
|
|
174
|
+
const bucket = locale !== undefined ? doc.localized?.[locale] : doc.values;
|
|
175
|
+
const original = bucket?.[change.field];
|
|
176
|
+
const blocks = blockRootsOf(original);
|
|
177
|
+
const index = blocks.findIndex((block) => block.id === change.blockId);
|
|
178
|
+
if (index < 0) {
|
|
179
|
+
return issue('invalid-field-value', `Block "${change.blockId}" was not found on "${change.field}" — read the document first; block ids come from getDocument.`);
|
|
180
|
+
}
|
|
181
|
+
const block = blocks[index];
|
|
182
|
+
const props = { ...(block.props ?? {}) };
|
|
183
|
+
if (change.value === undefined || change.value === null)
|
|
184
|
+
delete props[change.prop];
|
|
185
|
+
else
|
|
186
|
+
props[change.prop] = staticProp(change.value);
|
|
187
|
+
const nextBlock = { ...block, props };
|
|
188
|
+
const nextBlocks = blocks.map((b, i) => (i === index ? nextBlock : b));
|
|
189
|
+
// Preserve the stored shape: an array for a `multiple` composition, a bare node otherwise.
|
|
190
|
+
const value = Array.isArray(original) ? nextBlocks : nextBlocks[0];
|
|
191
|
+
return { doc: writeFieldValue(doc, change.field, locale, value) };
|
|
192
|
+
}
|
|
193
|
+
/** Apply ONE change to a (copy of the) document, or report the targeting issue. */
|
|
194
|
+
function applyOneChange(input, doc, change) {
|
|
195
|
+
const { collection } = input;
|
|
196
|
+
const issue = (code, message) => ({
|
|
197
|
+
issue: cmsIssue({
|
|
198
|
+
code,
|
|
199
|
+
message,
|
|
200
|
+
path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],
|
|
201
|
+
collectionId: collection.id,
|
|
202
|
+
documentId: doc.id,
|
|
203
|
+
meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
const field = collection.fields.find((f) => f.name === change.field);
|
|
207
|
+
if (!field) {
|
|
208
|
+
return issue('invalid-field-value', `Unknown field "${change.field}" on collection "${collection.id}" — use a field name from the schema.`);
|
|
209
|
+
}
|
|
210
|
+
if (field.ai?.exclude) {
|
|
211
|
+
return issue('invalid-field-value', `Field "${change.field}" must not be edited by the assistant.`);
|
|
212
|
+
}
|
|
213
|
+
const localized = isFieldLocalized(collection, change.field);
|
|
214
|
+
if (!localized && change.locale !== undefined) {
|
|
215
|
+
return issue('unknown-locale', `Field "${change.field}" is not localized — omit "locale".`);
|
|
216
|
+
}
|
|
217
|
+
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.locales.join(', ')}.`);
|
|
221
|
+
}
|
|
222
|
+
const locale = localized ? change.locale ?? input.locale : undefined;
|
|
223
|
+
if (localized && locale === undefined) {
|
|
224
|
+
return issue('missing-localized-value', `Field "${change.field}" is localized — provide "locale".`);
|
|
225
|
+
}
|
|
226
|
+
if (change.blockId !== undefined || change.prop !== undefined) {
|
|
227
|
+
return applyBlockPropChange(doc, field, change, locale, issue);
|
|
228
|
+
}
|
|
229
|
+
const mapped = mapUpdateValue(field, change, input.document, input, issue);
|
|
230
|
+
if ('issue' in mapped)
|
|
231
|
+
return mapped;
|
|
232
|
+
return { doc: writeFieldValue(doc, change.field, locale, mapped.value) };
|
|
233
|
+
}
|
|
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
|
+
/**
|
|
239
|
+
* Build the reviewable update proposal: apply every change over a copy of the
|
|
240
|
+
* document (loud targeting), validate the merged result with the injected
|
|
241
|
+
* leashes (only errors the change INTRODUCES block), and report the semantic
|
|
242
|
+
* diff. `no-change` when the changes resolve to the current values.
|
|
243
|
+
*/
|
|
244
|
+
export function buildUpdateProposal(input) {
|
|
245
|
+
const { registry, collection, document } = input;
|
|
246
|
+
let next = structuredClone(document);
|
|
247
|
+
const targetingIssues = [];
|
|
248
|
+
for (const change of input.changes) {
|
|
249
|
+
const outcome = applyOneChange(input, next, change);
|
|
250
|
+
if ('issue' in outcome)
|
|
251
|
+
targetingIssues.push(outcome.issue);
|
|
252
|
+
else
|
|
253
|
+
next = outcome.doc;
|
|
254
|
+
}
|
|
255
|
+
if (targetingIssues.length > 0)
|
|
256
|
+
return { ok: false, issues: targetingIssues };
|
|
257
|
+
const validateOptions = {
|
|
258
|
+
...(input.store !== undefined ? { store: input.store } : {}),
|
|
259
|
+
...(input.localeConfig !== undefined ? { localeConfig: input.localeConfig } : {}),
|
|
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));
|
|
270
|
+
const issues = validateDocument(registry, next, validateOptions);
|
|
271
|
+
const introduced = issues.filter((issue) => issue.severity === 'error' && !preExisting.has(issueKey(issue)));
|
|
272
|
+
if (introduced.length > 0)
|
|
273
|
+
return { ok: false, issues: introduced };
|
|
274
|
+
const diff = diffDocuments(collection, document, next);
|
|
275
|
+
if (diff.changes.length === 0)
|
|
276
|
+
return { ok: true, kind: 'no-change' };
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
kind: 'proposal',
|
|
280
|
+
patch: {
|
|
281
|
+
collection: collection.id,
|
|
282
|
+
id: document.id,
|
|
283
|
+
values: next.values,
|
|
284
|
+
...(next.localized && Object.keys(next.localized).length > 0
|
|
285
|
+
? { localized: next.localized }
|
|
286
|
+
: {}),
|
|
287
|
+
},
|
|
288
|
+
before: {
|
|
289
|
+
values: document.values,
|
|
290
|
+
...(document.localized ? { localized: document.localized } : {}),
|
|
291
|
+
},
|
|
292
|
+
changedFields: diff.changes.map((change) => change.locale !== undefined ? `${change.field} (${change.locale})` : change.field),
|
|
293
|
+
issues: issues.filter((issue) => issue.severity !== 'error'),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=update-fields.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CmsDocument } from '@elytracms/core/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* The current-document seam (EC-378) — a framework-free module store, like the
|
|
4
|
+
* AI panel's open state (`lib/ai-panel`). The document editor (`EditorBody`)
|
|
5
|
+
* publishes WHICH document is open; the AI chat panel — a disjoint React
|
|
6
|
+
* subtree — reads it so "this document" / "that element" resolve without
|
|
7
|
+
* interrogating the user: the coordinates go into the model's context
|
|
8
|
+
* (advisory) AND into the tool executor's `currentDocument` thunk (the
|
|
9
|
+
* deterministic fallback for `getDocument` / `updateFields`).
|
|
10
|
+
*
|
|
11
|
+
* `adoptDraft` is the narrow write-back half of the seam: after the panel
|
|
12
|
+
* applies an UPDATE proposal to the open document, it must replace the
|
|
13
|
+
* editor's draft buffer (the editor clones its document once at mount —
|
|
14
|
+
* without adoption, its autosave would write the stale buffer back and
|
|
15
|
+
* silently revert the applied change). It is exactly the editor's own
|
|
16
|
+
* `replaceDraft` (the same channel the translate dialog uses via `onApplied`).
|
|
17
|
+
*
|
|
18
|
+
* This module imports NO AI runtime — publishing the tap from the always-
|
|
19
|
+
* mounted editor never pulls the AI chunk (the heavy panel stays behind its
|
|
20
|
+
* lazy boundary).
|
|
21
|
+
*/
|
|
22
|
+
export interface AiCurrentDocument {
|
|
23
|
+
/** Collection id — the coordinate tools target. */
|
|
24
|
+
collection: string;
|
|
25
|
+
/** Document id — the coordinate tools target. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Resolved display title (advisory — for the model's context block). */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Replace the open editor's draft buffer with an externally applied update. */
|
|
30
|
+
adoptDraft: (next: CmsDocument) => void;
|
|
31
|
+
}
|
|
32
|
+
/** The document currently open in the editor, or null. */
|
|
33
|
+
export declare function getAiCurrentDocument(): AiCurrentDocument | null;
|
|
34
|
+
/** Publish (or clear with `null`) the open document. */
|
|
35
|
+
export declare function setAiCurrentDocument(next: AiCurrentDocument | null): void;
|
|
36
|
+
/** Clear the store ONLY if it still holds the given document (unmount race guard). */
|
|
37
|
+
export declare function clearAiCurrentDocument(ref: {
|
|
38
|
+
collection: string;
|
|
39
|
+
id: string;
|
|
40
|
+
}): void;
|
|
41
|
+
/** React binding — a surface that wants to REACT to the open document. */
|
|
42
|
+
export declare function useAiCurrentDocument(): AiCurrentDocument | null;
|
|
43
|
+
/**
|
|
44
|
+
* The editor-side tap: publish the open document while mounted, clear on
|
|
45
|
+
* unmount. `adoptDraft` is kept fresh through a ref so the published value only
|
|
46
|
+
* changes when the coordinates/title do (no churn per keystroke elsewhere).
|
|
47
|
+
*/
|
|
48
|
+
export declare function useAiCurrentDocumentTap(input: {
|
|
49
|
+
collection: string;
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
adoptDraft: (next: CmsDocument) => void;
|
|
53
|
+
}): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
let current = null;
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
function emit() {
|
|
5
|
+
for (const listener of listeners)
|
|
6
|
+
listener();
|
|
7
|
+
}
|
|
8
|
+
/** The document currently open in the editor, or null. */
|
|
9
|
+
export function getAiCurrentDocument() {
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
/** Publish (or clear with `null`) the open document. */
|
|
13
|
+
export function setAiCurrentDocument(next) {
|
|
14
|
+
if (next === current)
|
|
15
|
+
return;
|
|
16
|
+
current = next;
|
|
17
|
+
emit();
|
|
18
|
+
}
|
|
19
|
+
/** Clear the store ONLY if it still holds the given document (unmount race guard). */
|
|
20
|
+
export function clearAiCurrentDocument(ref) {
|
|
21
|
+
if (current && current.collection === ref.collection && current.id === ref.id) {
|
|
22
|
+
setAiCurrentDocument(null);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function subscribe(listener) {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return () => {
|
|
28
|
+
listeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** React binding — a surface that wants to REACT to the open document. */
|
|
32
|
+
export function useAiCurrentDocument() {
|
|
33
|
+
return useSyncExternalStore(subscribe, getAiCurrentDocument, getAiCurrentDocument);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The editor-side tap: publish the open document while mounted, clear on
|
|
37
|
+
* unmount. `adoptDraft` is kept fresh through a ref so the published value only
|
|
38
|
+
* changes when the coordinates/title do (no churn per keystroke elsewhere).
|
|
39
|
+
*/
|
|
40
|
+
export function useAiCurrentDocumentTap(input) {
|
|
41
|
+
const adoptRef = useRef(input.adoptDraft);
|
|
42
|
+
adoptRef.current = input.adoptDraft;
|
|
43
|
+
const { collection, id, title } = input;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setAiCurrentDocument({
|
|
46
|
+
collection,
|
|
47
|
+
id,
|
|
48
|
+
title,
|
|
49
|
+
adoptDraft: (next) => adoptRef.current(next),
|
|
50
|
+
});
|
|
51
|
+
return () => clearAiCurrentDocument({ collection, id });
|
|
52
|
+
}, [collection, id, title]);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ai-current-document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-current-document.js","sourceRoot":"","sources":["../../src/lib/ai-current-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAkC/D,IAAI,OAAO,GAA6B,IAAI,CAAA;AAC5C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,oBAAoB,CAAC,IAA8B;IACjE,IAAI,IAAI,KAAK,OAAO;QAAE,OAAM;IAC5B,OAAO,GAAG,IAAI,CAAA;IACd,IAAI,EAAE,CAAA;AACR,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,sBAAsB,CAAC,GAAuC;IAC5E,IAAI,OAAO,IAAI,OAAO,CAAC,UAAU,KAAK,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC;QAC9E,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAoB;IACrC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB;IAClC,OAAO,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,oBAAoB,CAAC,CAAA;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAKvC;IACC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IACzC,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC,UAAU,CAAA;IACnC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IACvC,SAAS,CAAC,GAAG,EAAE;QACb,oBAAoB,CAAC;YACnB,UAAU;YACV,EAAE;YACF,KAAK;YACL,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC;SAC7C,CAAC,CAAA;QACF,OAAO,GAAG,EAAE,CAAC,sBAAsB,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;IACzD,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAA;AAC7B,CAAC","sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\n\n/**\n * The current-document seam (EC-378) — a framework-free module store, like the\n * AI panel's open state (`lib/ai-panel`). The document editor (`EditorBody`)\n * publishes WHICH document is open; the AI chat panel — a disjoint React\n * subtree — reads it so \"this document\" / \"that element\" resolve without\n * interrogating the user: the coordinates go into the model's context\n * (advisory) AND into the tool executor's `currentDocument` thunk (the\n * deterministic fallback for `getDocument` / `updateFields`).\n *\n * `adoptDraft` is the narrow write-back half of the seam: after the panel\n * applies an UPDATE proposal to the open document, it must replace the\n * editor's draft buffer (the editor clones its document once at mount —\n * without adoption, its autosave would write the stale buffer back and\n * silently revert the applied change). It is exactly the editor's own\n * `replaceDraft` (the same channel the translate dialog uses via `onApplied`).\n *\n * This module imports NO AI runtime — publishing the tap from the always-\n * mounted editor never pulls the AI chunk (the heavy panel stays behind its\n * lazy boundary).\n */\nexport interface AiCurrentDocument {\n /** Collection id — the coordinate tools target. */\n collection: string\n /** Document id — the coordinate tools target. */\n id: string\n /** Resolved display title (advisory — for the model's context block). */\n title: string\n /** Replace the open editor's draft buffer with an externally applied update. */\n adoptDraft: (next: CmsDocument) => void\n}\n\nlet current: AiCurrentDocument | null = null\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** The document currently open in the editor, or null. */\nexport function getAiCurrentDocument(): AiCurrentDocument | null {\n return current\n}\n\n/** Publish (or clear with `null`) the open document. */\nexport function setAiCurrentDocument(next: AiCurrentDocument | null): void {\n if (next === current) return\n current = next\n emit()\n}\n\n/** Clear the store ONLY if it still holds the given document (unmount race guard). */\nexport function clearAiCurrentDocument(ref: { collection: string; id: string }): void {\n if (current && current.collection === ref.collection && current.id === ref.id) {\n setAiCurrentDocument(null)\n }\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** React binding — a surface that wants to REACT to the open document. */\nexport function useAiCurrentDocument(): AiCurrentDocument | null {\n return useSyncExternalStore(subscribe, getAiCurrentDocument, getAiCurrentDocument)\n}\n\n/**\n * The editor-side tap: publish the open document while mounted, clear on\n * unmount. `adoptDraft` is kept fresh through a ref so the published value only\n * changes when the coordinates/title do (no churn per keystroke elsewhere).\n */\nexport function useAiCurrentDocumentTap(input: {\n collection: string\n id: string\n title: string\n adoptDraft: (next: CmsDocument) => void\n}): void {\n const adoptRef = useRef(input.adoptDraft)\n adoptRef.current = input.adoptDraft\n const { collection, id, title } = input\n useEffect(() => {\n setAiCurrentDocument({\n collection,\n id,\n title,\n adoptDraft: (next) => adoptRef.current(next),\n })\n return () => clearAiCurrentDocument({ collection, id })\n }, [collection, id, title])\n}\n"]}
|