@elytracms/studio 0.0.91 → 0.0.93
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/asset-alt-migration.test.ts +127 -0
- package/convex/asset-replace.test.ts +168 -0
- package/convex/asset-revalidate.test.ts +6 -6
- package/convex/assets.ts +41 -5
- package/convex/cms.ts +537 -177
- package/convex/contract-parity.test.ts +173 -19
- package/convex/delivery.ts +26 -13
- package/convex/document-revalidate.test.ts +17 -16
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/presence-cost.test.ts +89 -0
- package/convex/rail-write-parity.test.ts +17 -14
- package/convex/referenceMaintenance.ts +15 -15
- package/convex/schema.ts +70 -5
- package/convex/triggers.ts +21 -8
- package/convex/write-limits.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +33 -22
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +8 -2
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/ai/translate-document-dialog.js +14 -8
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -0
- package/dist/components/ai/translate-skipped.js +46 -0
- package/dist/components/ai/translate-skipped.js.map +1 -0
- package/dist/components/app-views/default-modules.d.ts +23 -6
- package/dist/components/app-views/default-modules.js +33 -14
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-detail.js +18 -8
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +15 -4
- package/dist/components/assets/asset-metadata.js +15 -2
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +4 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +0 -8
- package/dist/components/assets/asset-selection.js +4 -23
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
- package/dist/components/assets/assets-collection-view-source.js +9 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +14 -0
- package/dist/components/assets/assets-store.js +23 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/replace-file.d.ts +22 -0
- package/dist/components/assets/replace-file.js +59 -0
- package/dist/components/assets/replace-file.js.map +1 -0
- package/dist/components/builder/blocks-model.d.ts +14 -26
- package/dist/components/builder/blocks-model.js +35 -41
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +7 -3
- package/dist/components/builder/blocks-panel.js +6 -5
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +126 -30
- package/dist/components/builder/builder-store.js +169 -67
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-body.d.ts +30 -0
- package/dist/components/builder/canvas-body.js +38 -0
- package/dist/components/builder/canvas-body.js.map +1 -0
- package/dist/components/builder/canvas-chrome-css.js +7 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -7
- package/dist/components/builder/canvas-dnd.js +5 -5
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +30 -22
- package/dist/components/builder/canvas-render-context.js +44 -44
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-runs.d.ts +45 -0
- package/dist/components/builder/canvas-runs.js +51 -0
- package/dist/components/builder/canvas-runs.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +5 -4
- package/dist/components/builder/canvas.js +34 -6
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +15 -0
- package/dist/components/builder/clipboard-actions.js +50 -13
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +10 -16
- package/dist/components/builder/composition-field-editor.js +30 -40
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/composition-patches.d.ts +7 -0
- package/dist/components/builder/composition-patches.js +256 -0
- package/dist/components/builder/composition-patches.js.map +1 -0
- package/dist/components/builder/delete-key.d.ts +5 -0
- package/dist/components/builder/delete-key.js +4 -0
- package/dist/components/builder/delete-key.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +17 -25
- package/dist/components/builder/insert-context.js +45 -92
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -3
- package/dist/components/builder/page-tree.js +3 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.d.ts +18 -0
- package/dist/components/builder/placement-refusal.js +46 -0
- package/dist/components/builder/placement-refusal.js.map +1 -0
- package/dist/components/builder/property-inspector.js +4 -0
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +65 -26
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -8
- package/dist/components/builder/reuse.js +71 -23
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/run-inspector.d.ts +11 -0
- package/dist/components/builder/run-inspector.js +33 -0
- package/dist/components/builder/run-inspector.js.map +1 -0
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +182 -58
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +16 -0
- package/dist/components/cms/asset-picker-core.js +20 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +12 -18
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +4 -20
- package/dist/components/cms/bulk-actions.js +3 -24
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +5 -5
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +87 -45
- package/dist/components/cms/cms-store.js +347 -203
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +24 -0
- package/dist/components/cms/commit-policy.js +37 -0
- package/dist/components/cms/commit-policy.js.map +1 -0
- package/dist/components/cms/detail-editor.js +83 -137
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +12 -7
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +70 -0
- package/dist/components/cms/draft-commit.js +82 -0
- package/dist/components/cms/draft-commit.js.map +1 -0
- package/dist/components/cms/duplicate-document.d.ts +11 -13
- package/dist/components/cms/duplicate-document.js +26 -34
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/field-anchors.d.ts +7 -4
- package/dist/components/cms/field-anchors.js +9 -7
- package/dist/components/cms/field-anchors.js.map +1 -1
- package/dist/components/cms/field-collaboration.d.ts +47 -0
- package/dist/components/cms/field-collaboration.js +87 -0
- package/dist/components/cms/field-collaboration.js.map +1 -0
- package/dist/components/cms/field-control-lib.d.ts +22 -1
- package/dist/components/cms/field-control-lib.js +33 -4
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +17 -14
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -22
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/object-array-field.js +4 -4
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +7 -2
- package/dist/components/cms/prop-fields.js +13 -6
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +10 -4
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +20 -9
- package/dist/components/cms/rich-text/insert.js +25 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +96 -0
- package/dist/components/cms/use-document-draft.js +283 -0
- package/dist/components/cms/use-document-draft.js.map +1 -0
- package/dist/components/cms/use-document-versions.js +2 -2
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +6 -12
- package/dist/components/cms/use-live-url.js +16 -15
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +13 -18
- package/dist/components/cms/use-publish-controller.js +24 -41
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +2 -3
- package/dist/components/cms/use-published-perspective.js +4 -3
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +24 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +28 -23
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +31 -21
- package/dist/components/components/transfer.js +49 -35
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +6 -3
- package/dist/components/project-session.js +14 -8
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-view-source.js +2 -1
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/section-nav.js +1 -1
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +56 -116
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +8 -0
- package/dist/lib/ai/chat-context.js +37 -6
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +14 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +20 -5
- package/dist/lib/ai/constraints.js +125 -33
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +16 -3
- package/dist/lib/ai/fake-provider.js +22 -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/leash.d.ts +34 -0
- package/dist/lib/ai/leash.js +28 -0
- package/dist/lib/ai/leash.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +1 -1
- package/dist/lib/ai/provenance.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +8 -8
- package/dist/lib/ai/rich-text-input.js +2 -1
- package/dist/lib/ai/rich-text-input.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +27 -4
- package/dist/lib/ai/rich-text-translate.js +28 -10
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +13 -13
- package/dist/lib/ai/shape.js +69 -93
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +36 -41
- package/dist/lib/ai/tools.js +85 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +39 -5
- package/dist/lib/ai/translate-document.js +160 -51
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +34 -23
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +21 -12
- package/dist/lib/ai/update-fields.js +118 -177
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +116 -0
- package/dist/lib/ai/writer-form.js +429 -0
- package/dist/lib/ai/writer-form.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +11 -11
- package/dist/lib/ai-current-document.js +4 -4
- package/dist/lib/ai-current-document.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +31 -2
- package/dist/lib/assets/upload.js +61 -6
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
- package/dist/lib/clipboard/os-clipboard.js +8 -4
- package/dist/lib/clipboard/os-clipboard.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +8 -7
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +15 -0
- package/dist/lib/cms/field-patch.js +25 -0
- package/dist/lib/cms/field-patch.js.map +1 -0
- package/dist/lib/cms/published-documents.d.ts +1 -1
- package/dist/lib/cms/published-documents.js +1 -1
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
- package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +4 -13
- package/dist/lib/data/collection-content.js +11 -5
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +25 -31
- package/dist/lib/dnd/drop-rules.js +53 -59
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +72 -15
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +61 -12
- package/dist/lib/i18n/messages.en.js +76 -16
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
- package/dist/lib/operations/__fixtures__/write-document.js +16 -0
- package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +124 -31
- package/dist/lib/operations/cms-persistence.js +447 -68
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
- package/dist/lib/operations/config-manifest-lookup.js +56 -0
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
- package/dist/lib/operations/project-runtime-config.d.ts +39 -0
- package/dist/lib/operations/project-runtime-config.js +54 -0
- package/dist/lib/operations/project-runtime-config.js.map +1 -0
- package/dist/lib/operations/publish-with-revision.d.ts +64 -0
- package/dist/lib/operations/publish-with-revision.js +66 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -0
- package/dist/lib/operations/runtime-validator.d.ts +32 -0
- package/dist/lib/operations/runtime-validator.js +18 -0
- package/dist/lib/operations/runtime-validator.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +31 -14
- package/dist/lib/operations/session-hydration.js +12 -16
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +59 -2
- package/dist/lib/operations/studio-operations.js +76 -14
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +13 -5
- package/dist/lib/persistence/convex-adapter.js +189 -74
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/convex-client.d.ts +65 -5
- package/dist/lib/persistence/convex-client.js +106 -3
- package/dist/lib/persistence/convex-client.js.map +1 -1
- package/dist/lib/persistence/index.js +17 -18
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +4 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/impact.d.ts +21 -15
- package/dist/lib/references/impact.js +13 -26
- package/dist/lib/references/impact.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +9 -0
- package/dist/lib/references/references-store.js +24 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +6 -6
- package/dist/lib/routes/route-issues.js +26 -80
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/config-issues.js +2 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +31 -1
- package/dist/lib/workspace/index.js +64 -17
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +3 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +2 -2
- package/dist/lib/workspace/workspace-source.js +2 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +3 -3
- package/dist/components/cms/cms-composition-validators.d.ts +0 -16
- package/dist/components/cms/cms-composition-validators.js +0 -133
- package/dist/components/cms/cms-composition-validators.js.map +0 -1
- package/dist/components/cms/publish-clean.d.ts +0 -28
- package/dist/components/cms/publish-clean.js +0 -38
- package/dist/components/cms/publish-clean.js.map +0 -1
- package/dist/components/cms/use-save-mode.d.ts +0 -35
- package/dist/components/cms/use-save-mode.js +0 -54
- package/dist/components/cms/use-save-mode.js.map +0 -1
- package/dist/lib/assets/asset-placements.d.ts +0 -28
- package/dist/lib/assets/asset-placements.js +0 -83
- package/dist/lib/assets/asset-placements.js.map +0 -1
- package/dist/lib/cms/document-gate.d.ts +0 -47
- package/dist/lib/cms/document-gate.js +0 -61
- package/dist/lib/cms/document-gate.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/fake-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * FakeProvider (EC-331): a deterministic, fixture-driven {@link AiProvider} that\n * implements the same interface as the Anthropic provider, so playground / demo /\n * tests run the WHOLE loop (config gate → shape → generate → validate → repair →\n * patch) without a key. Determinism is a project principle: the same fixtures always\n * yield the same result. Imports no AI SDK at runtime — the `LanguageModelV4` spec\n * below is implemented STRUCTURALLY from type-only imports (erased at compile;\n * pinned in `bundle.test.ts`).\n *\n * For the repair loop, pass `objects: [invalidDoc, validDoc]` — the provider returns\n * them in call order (the last is reused once the script is exhausted). Or pass\n * `respond` to compute the object from the request.\n *\n * EC-334 adds {@link createFakeChatModel}: a scripted fake `LanguageModel` served by\n * the ROUTE's native chat branch, so the keyless playground / CI drive the FULL\n * `streamText` → SSE wire → client-tool loop. It is a STATELESS prompt-inspecting\n * responder (state read from the prompt, the FakeProvider trick): a prompt without a\n * tool result emits a tool call; a prompt carrying one streams a text answer.\n */\n// Type-only (erased): the fake implements the SDK's v4 model spec structurally.\nimport type { LanguageModel } from 'ai'\nimport type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest } from './types'\n\n// --- Fake chat model (EC-334) -------------------------------------------------------\n\n/** The SDK's v4 language-model spec, extracted structurally from the type-only union. */\nexport type FakeLanguageModelV4 = Extract<LanguageModel, { specificationVersion: 'v4' }>\n\n/** The call options a fake chat model receives (`LanguageModelV4CallOptions`). */\nexport type FakeChatModelCallOptions = Parameters<FakeLanguageModelV4['doGenerate']>[0]\n\n/** One provider-spec stream part the fake emits (`LanguageModelV4StreamPart`). */\nexport type FakeChatModelStreamPart =\n Awaited<ReturnType<FakeLanguageModelV4['doStream']>>['stream'] extends ReadableStream<infer PART>\n ? PART\n : never\n\ntype FakeFinishPart = Extract<FakeChatModelStreamPart, { type: 'finish' }>\ntype FakeGenerateResult = Awaited<ReturnType<FakeLanguageModelV4['doGenerate']>>\n\n/** Zero-usage report (the fake spends nothing). Shape = `LanguageModelV4Usage`. */\nconst FAKE_USAGE: FakeFinishPart['usage'] = {\n inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },\n outputTokens: { total: 0, text: 0, reasoning: 0 },\n}\n\n/** Default preamble streamed before the scripted tool call. */\nexport const FAKE_CHAT_PREAMBLE = 'Let me look that up. '\n/** Default final answer streamed once the prompt carries a tool result. */\nexport const FAKE_CHAT_ANSWER = 'Found it — the tool result is in, and this is the answer.'\n\nexport interface FakeChatModelOptions {\n /** Model id reported to the SDK (default `fake-chat-model`). */\n modelId?: string\n /** Text streamed before the tool call on the first (tool-result-free) turn. */\n preambleText?: string\n /** The tool the first turn requests (default `searchDocuments`). */\n toolName?: string\n /** The tool-call input (default `{ collection: 'post' }`). */\n toolInput?: Record<string, unknown>\n /** The answer streamed word-by-word once the prompt carries a tool result. */\n answerText?: string\n /**\n * Full override: script the exact provider-spec stream parts for a call\n * (zero-based call index). The default prompt-inspecting behavior is skipped.\n */\n script?: (options: FakeChatModelCallOptions, callIndex: number) => FakeChatModelStreamPart[]\n}\n\n/** A fake chat model that also records the calls it received, for assertions. */\nexport type FakeChatModel = FakeLanguageModelV4 & {\n /** Every `doStream` / `doGenerate` call options received, in order. */\n readonly calls: FakeChatModelCallOptions[]\n}\n\n/** Does any prompt message carry a tool RESULT part? (The fake's only \"state\".) */\nfunction promptHasToolResult(prompt: FakeChatModelCallOptions['prompt']): boolean {\n return prompt.some(\n (message) =>\n Array.isArray(message.content) &&\n message.content.some(\n (part) => (part as { type?: unknown }).type === 'tool-result',\n ),\n )\n}\n\n/** Index of the LAST user message in the prompt (`-1` when none). */\nfunction lastUserIndex(prompt: FakeChatModelCallOptions['prompt']): number {\n for (let i = prompt.length - 1; i >= 0; i -= 1) {\n if (prompt[i]?.role === 'user') return i\n }\n return -1\n}\n\n/** The last user message's text (the fake's \"instruction\" source). */\nfunction lastUserText(prompt: FakeChatModelCallOptions['prompt']): string {\n const index = lastUserIndex(prompt)\n const message = prompt[index]\n if (!message || !Array.isArray(message.content)) return ''\n return message.content\n .filter((part): part is { type: 'text'; text: string } => (part as { type?: unknown }).type === 'text')\n .map((part) => part.text)\n .join(' ')\n .trim()\n}\n\n/**\n * Tool names with a RESULT in the CURRENT user turn (prompt suffix after the\n * last user message) — the stateless stage detector for scripted flows.\n */\nfunction toolResultNamesInCurrentTurn(prompt: FakeChatModelCallOptions['prompt']): Set<string> {\n const names = new Set<string>()\n for (const message of prompt.slice(lastUserIndex(prompt) + 1)) {\n if (!Array.isArray(message.content)) continue\n for (const part of message.content) {\n const candidate = part as { type?: unknown; toolName?: unknown }\n if (candidate.type === 'tool-result' && typeof candidate.toolName === 'string') {\n names.add(candidate.toolName)\n }\n }\n }\n return names\n}\n\n/** Provider-spec parts for one text-preamble + tool-call turn (finish `tool-calls`). */\nfunction toolTurnParts(\n preamble: string,\n toolName: string,\n input: Record<string, unknown>,\n callId: string,\n): FakeChatModelStreamPart[] {\n const encoded = JSON.stringify(input)\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n { type: 'text-delta', id: 'text-1', delta: preamble },\n { type: 'text-end', id: 'text-1' },\n { type: 'tool-input-start', id: callId, toolName },\n { type: 'tool-input-delta', id: callId, delta: encoded },\n { type: 'tool-input-end', id: callId },\n { type: 'tool-call', toolCallId: callId, toolName, input: encoded },\n {\n type: 'finish',\n usage: FAKE_USAGE,\n finishReason: { unified: 'tool-calls', raw: 'fake-tool-calls' },\n },\n ]\n}\n\n/** Provider-spec parts for a word-by-word streamed text answer (finish `stop`). */\nfunction answerTurnParts(answer: string): FakeChatModelStreamPart[] {\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n ...wordDeltas(answer).map(\n (delta): FakeChatModelStreamPart => ({ type: 'text-delta', id: 'text-1', delta }),\n ),\n { type: 'text-end', id: 'text-1' },\n { type: 'finish', usage: FAKE_USAGE, finishReason: { unified: 'stop', raw: 'fake-stop' } },\n ]\n}\n\n/** Split text into word deltas (trailing spaces preserved) for visible streaming. */\nfunction wordDeltas(text: string): string[] {\n const words = text.split(' ')\n return words.map((word, index) => (index < words.length - 1 ? `${word} ` : word))\n}\n\n/**\n * Build a scripted, stateless fake `LanguageModel` (the `LanguageModelV4` spec,\n * implemented structurally — zero SDK runtime import). Default behavior:\n * - prompt WITHOUT a tool result → preamble text, then a `tool-call` for\n * {@link FakeChatModelOptions.toolName} (finish reason `tool-calls`);\n * - prompt WITH a tool result → the answer text streamed word-by-word (`stop`).\n * Chunk shapes follow the v4 spec: `finishReason` is `{ unified, raw }`, tool chunks\n * are `tool-input-start/delta/end` + `tool-call`.\n */\nexport function createFakeChatModel(options: FakeChatModelOptions = {}): FakeChatModel {\n const preamble = options.preambleText ?? FAKE_CHAT_PREAMBLE\n const toolName = options.toolName ?? 'searchDocuments'\n const toolInput = options.toolInput ?? { collection: 'post' }\n const answer = options.answerText ?? FAKE_CHAT_ANSWER\n const calls: FakeChatModelCallOptions[] = []\n let callIndex = 0\n\n function partsFor(callOptions: FakeChatModelCallOptions): FakeChatModelStreamPart[] {\n if (options.script) return options.script(callOptions, callIndex)\n if (!promptHasToolResult(callOptions.prompt)) {\n return toolTurnParts(preamble, toolName, toolInput, 'call-1')\n }\n return answerTurnParts(answer)\n }\n\n /** Fold the scripted stream parts into a non-streaming `doGenerate` result. */\n function materialize(parts: FakeChatModelStreamPart[]): FakeGenerateResult {\n const content: FakeGenerateResult['content'] = []\n const textById = new Map<string, string>()\n let finishReason: FakeFinishPart['finishReason'] = { unified: 'stop', raw: 'fake-stop' }\n for (const part of parts) {\n if (part.type === 'text-delta') {\n textById.set(part.id, (textById.get(part.id) ?? '') + part.delta)\n } else if (part.type === 'tool-call') {\n content.push(part)\n } else if (part.type === 'finish') {\n finishReason = part.finishReason\n }\n }\n for (const text of textById.values()) content.unshift({ type: 'text', text })\n return { content, finishReason, usage: FAKE_USAGE, warnings: [] }\n }\n\n return {\n specificationVersion: 'v4',\n provider: 'fake',\n modelId: options.modelId ?? 'fake-chat-model',\n supportedUrls: {},\n calls,\n doGenerate(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve(materialize(parts))\n },\n doStream(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve({\n stream: new ReadableStream<FakeChatModelStreamPart>({\n start(controller) {\n for (const part of parts) controller.enqueue(part)\n controller.close()\n },\n }),\n })\n },\n }\n}\n\n// --- Fake create-flow chat model (EC-334 slice 2) -------------------------------------\n\nexport interface FakeCreateFlowChatModelOptions {\n /** Model id reported to the SDK (default `fake-create-flow-model`). */\n modelId?: string\n /** The collection the first stage searches (a relation target; default `author`). */\n searchCollection?: string\n /**\n * EC-381: when set, the create flow inserts a `listAssets` stage after the\n * relation search and before `createDocument`, so a collection with an image\n * field gets it filled from a REAL library asset (the enum leash). Optional\n * search text passed to `listAssets` (omit to list all).\n */\n searchAssets?: boolean\n /** Optional query passed to the scripted `listAssets` stage (EC-381). */\n assetQuery?: string\n /** The collection the terminal stage creates in (default `post`). */\n createCollection?: string\n /** The final answer streamed word-by-word after the proposal lands. */\n answerText?: string\n /**\n * EC-378: the field the scripted UPDATE flow changes (default `title`). A user\n * turn matching {@link FakeCreateFlowChatModelOptions.updatePattern} routes to\n * the update flow instead of the create flow.\n */\n updateField?: string\n /** Routes a user turn to the update flow (default {@link FAKE_UPDATE_PATTERN}). */\n updatePattern?: RegExp\n /** The final answer of the scripted update flow. */\n updateAnswerText?: string\n}\n\n/** Default final answer of the scripted create flow. */\nexport const FAKE_CREATE_FLOW_ANSWER =\n 'Done — review the proposal below and apply it as a draft.'\n\n/** Default final answer of the scripted update flow (EC-378). */\nexport const FAKE_UPDATE_FLOW_ANSWER =\n 'Done — review the proposed change below and apply it as a draft.'\n\n/** Default edit-request detector of the scripted assistant (EC-378). */\nexport const FAKE_UPDATE_PATTERN = /\\b(change|update|edit|remove|replace|rewrite|fix|set)\\b/i\n\n/**\n * The scripted update flow's new value for the target field: the text after the\n * FIRST colon when present (`Set the title to: New Title` → `New Title`), else\n * the whole user text — deterministic either way. First (not last) colon so a\n * markdown body value may itself contain colons (EC-379).\n */\nexport function fakeUpdateValueFrom(userText: string): string {\n const colon = userText.indexOf(':')\n const value = colon >= 0 ? userText.slice(colon + 1) : userText\n return value.trim() || 'Updated by the assistant'\n}\n\n/**\n * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the\n * <field> to: …` names it (so the playground can drive a markdown-authored\n * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything\n * else falls back to the configured default field.\n */\nexport function fakeUpdateFieldFrom(userText: string, fallback: string): string {\n const match = /\\b(?:set|change|update)\\s+the\\s+([A-Za-z]\\w*)\\s+to\\s*:/i.exec(userText)\n return match?.[1] ?? fallback\n}\n\n/**\n * A scripted, STATELESS fake chat model that drives the FULL create flow — and,\n * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the\n * native loop (EC-334): keyless playground / CI exercise the whole client-tool\n * round-trip deterministically. Stage is read from the CURRENT user turn's tool\n * results (prompt suffix after the last user message), so every follow-up turn\n * starts a fresh flow — multi-turn works without model state.\n *\n * Create flow (default):\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`\n * 3. create result present → the answer text, word by word.\n *\n * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`\n * 3. listAssets done, no create → `createDocument({ collection, instruction })`\n * 4. create result present → the answer text, word by word.\n *\n * Update flow (user text matches `updatePattern`, e.g. \"Change the title to: X\"):\n * 1. no tool result yet → `getDocument({})` — target omitted on purpose:\n * the executor resolves the document OPEN in the\n * editor (the EC-378 current-document seam)\n * 2. document read, no update → `updateFields({ changes: [{ field:\n * fakeUpdateFieldFrom(<text>, updateField),\n * value: fakeUpdateValueFrom(<last user text>) }] })`\n * — \"Set the body to: ## Neu\" drives a markdown\n * rich-text update deterministically (EC-379)\n * 3. update result present → the update answer, word by word.\n */\nexport function createFakeCreateFlowChatModel(\n options: FakeCreateFlowChatModelOptions = {},\n): FakeChatModel {\n const searchCollection = options.searchCollection ?? 'author'\n const createCollection = options.createCollection ?? 'post'\n const searchAssets = options.searchAssets ?? false\n const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER\n const updateField = options.updateField ?? 'title'\n const updatePattern = options.updatePattern ?? FAKE_UPDATE_PATTERN\n const updateAnswer = options.updateAnswerText ?? FAKE_UPDATE_FLOW_ANSWER\n return createFakeChatModel({\n modelId: options.modelId ?? 'fake-create-flow-model',\n script: (callOptions) => {\n const done = toolResultNamesInCurrentTurn(callOptions.prompt)\n const userText = lastUserText(callOptions.prompt)\n if (updatePattern.test(userText)) {\n if (done.has('updateFields')) return answerTurnParts(updateAnswer)\n if (done.has('getDocument')) {\n return toolTurnParts(\n 'Proposing the change. ',\n 'updateFields',\n {\n changes: [\n {\n field: fakeUpdateFieldFrom(userText, updateField),\n value: fakeUpdateValueFrom(userText),\n },\n ],\n },\n 'call-update-1',\n )\n }\n return toolTurnParts('Reading the document. ', 'getDocument', {}, 'call-get-1')\n }\n if (done.has('createDocument')) return answerTurnParts(answer)\n if (done.has('searchDocuments')) {\n // EC-381 — search the media library before creating (a topically fitting,\n // REAL asset fills the image field via the enum leash).\n if (searchAssets && !done.has('listAssets')) {\n return toolTurnParts(\n 'Looking for a fitting image. ',\n 'listAssets',\n { ...(options.assetQuery ? { query: options.assetQuery } : {}) },\n 'call-assets-1',\n )\n }\n const instruction = userText || 'Write a short document.'\n return toolTurnParts(\n 'Drafting a proposal. ',\n 'createDocument',\n { collection: createCollection, instruction },\n 'call-create-1',\n )\n }\n return toolTurnParts(\n FAKE_CHAT_PREAMBLE,\n 'searchDocuments',\n { collection: searchCollection },\n 'call-search-1',\n )\n },\n })\n}\n\n// --- Fake provider ------------------------------------------------------------------\n\nexport interface FakeProviderOptions {\n /** Provider id (default `fake`). */\n id?: string\n /** Model id recorded in provenance (default `fake-model`). */\n model?: string\n /** Reported capabilities (default: both true). Set to test the capability gate. */\n capabilities?: AiProviderCapabilities\n /** Scripted objects returned in call order; the last entry is reused when exhausted. */\n objects?: unknown[]\n /** Compute the object from the request + zero-based call index (overrides `objects`). */\n respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown\n /** The scripted chat model `languageModel()` serves (default {@link createFakeChatModel}). */\n chatModel?: FakeChatModel\n}\n\n/** A FakeProvider that also records the requests it received, for assertions. */\nexport interface FakeProvider extends AiProvider {\n /** Every `generateStructured` request received, in order. */\n readonly calls: GenerateStructuredRequest[]\n /** The scripted fake chat model behind `languageModel()` (records its calls). */\n readonly chatModel: FakeChatModel\n}\n\nexport function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {\n const calls: GenerateStructuredRequest[] = []\n const objects = options.objects ?? []\n const chatModel = options.chatModel ?? createFakeChatModel()\n let index = 0\n\n return {\n id: options.id ?? 'fake',\n model: options.model ?? 'fake-model',\n calls,\n chatModel,\n capabilities: (): AiProviderCapabilities =>\n options.capabilities ?? { toolCalling: true, structuredOutput: true },\n\n languageModel: () => Promise.resolve(chatModel),\n\n generateStructured(request) {\n calls.push(request)\n const callIndex = index\n index += 1\n const object = options.respond\n ? options.respond(request, callIndex)\n : objects.length > 0\n ? objects[Math.min(callIndex, objects.length - 1)]\n : {}\n return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } })\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fake-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/fake-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * FakeProvider (EC-331): a deterministic, fixture-driven {@link AiProvider} that\n * implements the same interface as the Anthropic provider, so playground / demo /\n * tests run the WHOLE loop (config gate → shape → generate → validate → repair →\n * patch) without a key. Determinism is a project principle: the same fixtures always\n * yield the same result. Imports no AI SDK at runtime — the `LanguageModelV4` spec\n * below is implemented STRUCTURALLY from type-only imports (erased at compile;\n * pinned in `bundle.test.ts`).\n *\n * For the repair loop, pass `objects: [invalidDoc, validDoc]` — the provider returns\n * them in call order (the last is reused once the script is exhausted). Or pass\n * `respond` to compute the object from the request.\n *\n * EC-334 adds {@link createFakeChatModel}: a scripted fake `LanguageModel` served by\n * the ROUTE's native chat branch, so the keyless playground / CI drive the FULL\n * `streamText` → SSE wire → client-tool loop. It is a STATELESS prompt-inspecting\n * responder (state read from the prompt, the FakeProvider trick): a prompt without a\n * tool result emits a tool call; a prompt carrying one streams a text answer.\n */\n// Type-only (erased): the fake implements the SDK's v4 model spec structurally.\nimport type { LanguageModel } from 'ai'\nimport type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest } from './types'\n\n// --- Fake chat model (EC-334) -------------------------------------------------------\n\n/** The SDK's v4 language-model spec, extracted structurally from the type-only union. */\nexport type FakeLanguageModelV4 = Extract<LanguageModel, { specificationVersion: 'v4' }>\n\n/** The call options a fake chat model receives (`LanguageModelV4CallOptions`). */\nexport type FakeChatModelCallOptions = Parameters<FakeLanguageModelV4['doGenerate']>[0]\n\n/** One provider-spec stream part the fake emits (`LanguageModelV4StreamPart`). */\nexport type FakeChatModelStreamPart =\n Awaited<ReturnType<FakeLanguageModelV4['doStream']>>['stream'] extends ReadableStream<infer PART>\n ? PART\n : never\n\ntype FakeFinishPart = Extract<FakeChatModelStreamPart, { type: 'finish' }>\ntype FakeGenerateResult = Awaited<ReturnType<FakeLanguageModelV4['doGenerate']>>\n\n/** Zero-usage report (the fake spends nothing). Shape = `LanguageModelV4Usage`. */\nconst FAKE_USAGE: FakeFinishPart['usage'] = {\n inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },\n outputTokens: { total: 0, text: 0, reasoning: 0 },\n}\n\n/** Default preamble streamed before the scripted tool call. */\nexport const FAKE_CHAT_PREAMBLE = 'Let me look that up. '\n/** Default final answer streamed once the prompt carries a tool result. */\nexport const FAKE_CHAT_ANSWER = 'Found it — the tool result is in, and this is the answer.'\n\nexport interface FakeChatModelOptions {\n /** Model id reported to the SDK (default `fake-chat-model`). */\n modelId?: string\n /** Text streamed before the tool call on the first (tool-result-free) turn. */\n preambleText?: string\n /** The tool the first turn requests (default `searchDocuments`). */\n toolName?: string\n /** The tool-call input (default `{ collection: 'post' }`). */\n toolInput?: Record<string, unknown>\n /** The answer streamed word-by-word once the prompt carries a tool result. */\n answerText?: string\n /**\n * Full override: script the exact provider-spec stream parts for a call\n * (zero-based call index). The default prompt-inspecting behavior is skipped.\n */\n script?: (options: FakeChatModelCallOptions, callIndex: number) => FakeChatModelStreamPart[]\n}\n\n/** A fake chat model that also records the calls it received, for assertions. */\nexport type FakeChatModel = FakeLanguageModelV4 & {\n /** Every `doStream` / `doGenerate` call options received, in order. */\n readonly calls: FakeChatModelCallOptions[]\n}\n\n/** Does any prompt message carry a tool RESULT part? (The fake's only \"state\".) */\nfunction promptHasToolResult(prompt: FakeChatModelCallOptions['prompt']): boolean {\n return prompt.some(\n (message) =>\n Array.isArray(message.content) &&\n message.content.some(\n (part) => (part as { type?: unknown }).type === 'tool-result',\n ),\n )\n}\n\n/** Index of the LAST user message in the prompt (`-1` when none). */\nfunction lastUserIndex(prompt: FakeChatModelCallOptions['prompt']): number {\n for (let i = prompt.length - 1; i >= 0; i -= 1) {\n if (prompt[i]?.role === 'user') return i\n }\n return -1\n}\n\n/** The last user message's text (the fake's \"instruction\" source). */\nfunction lastUserText(prompt: FakeChatModelCallOptions['prompt']): string {\n const index = lastUserIndex(prompt)\n const message = prompt[index]\n if (!message || !Array.isArray(message.content)) return ''\n return message.content\n .filter((part): part is { type: 'text'; text: string } => (part as { type?: unknown }).type === 'text')\n .map((part) => part.text)\n .join(' ')\n .trim()\n}\n\n/**\n * Tool names with a RESULT in the CURRENT user turn (prompt suffix after the\n * last user message) — the stateless stage detector for scripted flows.\n */\nfunction toolResultNamesInCurrentTurn(prompt: FakeChatModelCallOptions['prompt']): Set<string> {\n const names = new Set<string>()\n for (const message of prompt.slice(lastUserIndex(prompt) + 1)) {\n if (!Array.isArray(message.content)) continue\n for (const part of message.content) {\n const candidate = part as { type?: unknown; toolName?: unknown }\n if (candidate.type === 'tool-result' && typeof candidate.toolName === 'string') {\n names.add(candidate.toolName)\n }\n }\n }\n return names\n}\n\n/** Provider-spec parts for one text-preamble + tool-call turn (finish `tool-calls`). */\nfunction toolTurnParts(\n preamble: string,\n toolName: string,\n input: Record<string, unknown>,\n callId: string,\n): FakeChatModelStreamPart[] {\n const encoded = JSON.stringify(input)\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n { type: 'text-delta', id: 'text-1', delta: preamble },\n { type: 'text-end', id: 'text-1' },\n { type: 'tool-input-start', id: callId, toolName },\n { type: 'tool-input-delta', id: callId, delta: encoded },\n { type: 'tool-input-end', id: callId },\n { type: 'tool-call', toolCallId: callId, toolName, input: encoded },\n {\n type: 'finish',\n usage: FAKE_USAGE,\n finishReason: { unified: 'tool-calls', raw: 'fake-tool-calls' },\n },\n ]\n}\n\n/** Provider-spec parts for a word-by-word streamed text answer (finish `stop`). */\nfunction answerTurnParts(answer: string): FakeChatModelStreamPart[] {\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n ...wordDeltas(answer).map(\n (delta): FakeChatModelStreamPart => ({ type: 'text-delta', id: 'text-1', delta }),\n ),\n { type: 'text-end', id: 'text-1' },\n { type: 'finish', usage: FAKE_USAGE, finishReason: { unified: 'stop', raw: 'fake-stop' } },\n ]\n}\n\n/** Split text into word deltas (trailing spaces preserved) for visible streaming. */\nfunction wordDeltas(text: string): string[] {\n const words = text.split(' ')\n return words.map((word, index) => (index < words.length - 1 ? `${word} ` : word))\n}\n\n/**\n * Build a scripted, stateless fake `LanguageModel` (the `LanguageModelV4` spec,\n * implemented structurally — zero SDK runtime import). Default behavior:\n * - prompt WITHOUT a tool result → preamble text, then a `tool-call` for\n * {@link FakeChatModelOptions.toolName} (finish reason `tool-calls`);\n * - prompt WITH a tool result → the answer text streamed word-by-word (`stop`).\n * Chunk shapes follow the v4 spec: `finishReason` is `{ unified, raw }`, tool chunks\n * are `tool-input-start/delta/end` + `tool-call`.\n */\nexport function createFakeChatModel(options: FakeChatModelOptions = {}): FakeChatModel {\n const preamble = options.preambleText ?? FAKE_CHAT_PREAMBLE\n const toolName = options.toolName ?? 'searchDocuments'\n const toolInput = options.toolInput ?? { collection: 'post' }\n const answer = options.answerText ?? FAKE_CHAT_ANSWER\n const calls: FakeChatModelCallOptions[] = []\n let callIndex = 0\n\n function partsFor(callOptions: FakeChatModelCallOptions): FakeChatModelStreamPart[] {\n if (options.script) return options.script(callOptions, callIndex)\n if (!promptHasToolResult(callOptions.prompt)) {\n return toolTurnParts(preamble, toolName, toolInput, 'call-1')\n }\n return answerTurnParts(answer)\n }\n\n /** Fold the scripted stream parts into a non-streaming `doGenerate` result. */\n function materialize(parts: FakeChatModelStreamPart[]): FakeGenerateResult {\n const content: FakeGenerateResult['content'] = []\n const textById = new Map<string, string>()\n let finishReason: FakeFinishPart['finishReason'] = { unified: 'stop', raw: 'fake-stop' }\n for (const part of parts) {\n if (part.type === 'text-delta') {\n textById.set(part.id, (textById.get(part.id) ?? '') + part.delta)\n } else if (part.type === 'tool-call') {\n content.push(part)\n } else if (part.type === 'finish') {\n finishReason = part.finishReason\n }\n }\n for (const text of textById.values()) content.unshift({ type: 'text', text })\n return { content, finishReason, usage: FAKE_USAGE, warnings: [] }\n }\n\n return {\n specificationVersion: 'v4',\n provider: 'fake',\n modelId: options.modelId ?? 'fake-chat-model',\n supportedUrls: {},\n calls,\n doGenerate(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve(materialize(parts))\n },\n doStream(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve({\n stream: new ReadableStream<FakeChatModelStreamPart>({\n start(controller) {\n for (const part of parts) controller.enqueue(part)\n controller.close()\n },\n }),\n })\n },\n }\n}\n\n// --- Fake create-flow chat model (EC-334 slice 2) -------------------------------------\n\nexport interface FakeCreateFlowChatModelOptions {\n /** Model id reported to the SDK (default `fake-create-flow-model`). */\n modelId?: string\n /** The collection the first stage searches (a relation target; default `author`). */\n searchCollection?: string\n /**\n * EC-381: when set, the create flow inserts a `listAssets` stage after the\n * relation search and before `createDocument`, so a collection with an image\n * field gets it filled from a REAL library asset (the enum leash). Optional\n * search text passed to `listAssets` (omit to list all).\n */\n searchAssets?: boolean\n /** Optional query passed to the scripted `listAssets` stage (EC-381). */\n assetQuery?: string\n /** The collection the terminal stage creates in (default `post`). */\n createCollection?: string\n /** The final answer streamed word-by-word after the proposal lands. */\n answerText?: string\n /**\n * EC-378: the field the scripted UPDATE flow changes (default `title`). A user\n * turn matching {@link FakeCreateFlowChatModelOptions.updatePattern} routes to\n * the update flow instead of the create flow.\n */\n updateField?: string\n /** Routes a user turn to the update flow (default {@link FAKE_UPDATE_PATTERN}). */\n updatePattern?: RegExp\n /** The final answer of the scripted update flow. */\n updateAnswerText?: string\n}\n\n/** Default final answer of the scripted create flow. */\nexport const FAKE_CREATE_FLOW_ANSWER =\n 'Done — review the proposal below and apply it as a draft.'\n\n/** Default final answer of the scripted update flow (EC-378). */\nexport const FAKE_UPDATE_FLOW_ANSWER =\n 'Done — review the proposed change below and apply it as a draft.'\n\n/** Default edit-request detector of the scripted assistant (EC-378). */\nexport const FAKE_UPDATE_PATTERN = /\\b(change|update|edit|remove|replace|rewrite|fix|set)\\b/i\n\n/**\n * The scripted update flow's new value for the target field: the text after the\n * FIRST colon when present (`Set the title to: New Title` → `New Title`), else\n * the whole user text — deterministic either way. First (not last) colon so a\n * markdown body value may itself contain colons (EC-379).\n */\nexport function fakeUpdateValueFrom(userText: string): string {\n const colon = userText.indexOf(':')\n const value = colon >= 0 ? userText.slice(colon + 1) : userText\n return value.trim() || 'Updated by the assistant'\n}\n\n/**\n * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the\n * <field> to: …` names it (so the playground can drive a markdown-authored\n * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything\n * else falls back to the configured default field.\n */\nexport function fakeUpdateFieldFrom(userText: string, fallback: string): string {\n return fakeUpdateTargetFrom(userText, fallback).field\n}\n\n/**\n * The scripted update flow's full TARGET (EC-593): a field, or one component prop\n * inside it — `set the <field> component <id> <prop> to: …` names a component by its\n * node id at any depth (`Set the body component p-home-quote-note value to: Hi`), so the\n * keyless playground can drive a component-prop edit through the one converter.\n */\nexport function fakeUpdateTargetFrom(\n userText: string,\n fallback: string,\n): { field: string; blockId?: string; prop?: string } {\n const match =\n /\\b(?:set|change|update)\\s+the\\s+([A-Za-z]\\w*)(?:\\s+component\\s+([\\w-]+)\\s+([A-Za-z]\\w*))?\\s+to\\s*:/i.exec(\n userText,\n )\n if (!match) return { field: fallback }\n return {\n field: match[1]!,\n ...(match[2] !== undefined && match[3] !== undefined ? { blockId: match[2], prop: match[3] } : {}),\n }\n}\n\n/**\n * A scripted, STATELESS fake chat model that drives the FULL create flow — and,\n * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the\n * native loop (EC-334): keyless playground / CI exercise the whole client-tool\n * round-trip deterministically. Stage is read from the CURRENT user turn's tool\n * results (prompt suffix after the last user message), so every follow-up turn\n * starts a fresh flow — multi-turn works without model state.\n *\n * Create flow (default):\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`\n * 3. create result present → the answer text, word by word.\n *\n * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`\n * 3. listAssets done, no create → `createDocument({ collection, instruction })`\n * 4. create result present → the answer text, word by word.\n *\n * Update flow (user text matches `updatePattern`, e.g. \"Change the title to: X\"):\n * 1. no tool result yet → `getDocument({})` — target omitted on purpose:\n * the executor resolves the document OPEN in the\n * editor (the EC-378 current-document seam)\n * 2. document read, no update → `updateFields({ changes: [{\n * ...fakeUpdateTargetFrom(<text>, updateField),\n * value: fakeUpdateValueFrom(<last user text>) }] })`\n * — \"Set the body to: ## Neu\" drives a markdown\n * rich-text update deterministically (EC-379);\n * \"Set the body component <id> <prop> to: X\" one\n * component prop at any depth (EC-593)\n * 3. update result present → the update answer, word by word.\n */\nexport function createFakeCreateFlowChatModel(\n options: FakeCreateFlowChatModelOptions = {},\n): FakeChatModel {\n const searchCollection = options.searchCollection ?? 'author'\n const createCollection = options.createCollection ?? 'post'\n const searchAssets = options.searchAssets ?? false\n const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER\n const updateField = options.updateField ?? 'title'\n const updatePattern = options.updatePattern ?? FAKE_UPDATE_PATTERN\n const updateAnswer = options.updateAnswerText ?? FAKE_UPDATE_FLOW_ANSWER\n return createFakeChatModel({\n modelId: options.modelId ?? 'fake-create-flow-model',\n script: (callOptions) => {\n const done = toolResultNamesInCurrentTurn(callOptions.prompt)\n const userText = lastUserText(callOptions.prompt)\n if (updatePattern.test(userText)) {\n if (done.has('updateFields')) return answerTurnParts(updateAnswer)\n if (done.has('getDocument')) {\n return toolTurnParts(\n 'Proposing the change. ',\n 'updateFields',\n {\n changes: [\n {\n ...fakeUpdateTargetFrom(userText, updateField),\n value: fakeUpdateValueFrom(userText),\n },\n ],\n },\n 'call-update-1',\n )\n }\n return toolTurnParts('Reading the document. ', 'getDocument', {}, 'call-get-1')\n }\n if (done.has('createDocument')) return answerTurnParts(answer)\n if (done.has('searchDocuments')) {\n // EC-381 — search the media library before creating (a topically fitting,\n // REAL asset fills the image field via the enum leash).\n if (searchAssets && !done.has('listAssets')) {\n return toolTurnParts(\n 'Looking for a fitting image. ',\n 'listAssets',\n { ...(options.assetQuery ? { query: options.assetQuery } : {}) },\n 'call-assets-1',\n )\n }\n const instruction = userText || 'Write a short document.'\n return toolTurnParts(\n 'Drafting a proposal. ',\n 'createDocument',\n { collection: createCollection, instruction },\n 'call-create-1',\n )\n }\n return toolTurnParts(\n FAKE_CHAT_PREAMBLE,\n 'searchDocuments',\n { collection: searchCollection },\n 'call-search-1',\n )\n },\n })\n}\n\n// --- Fake provider ------------------------------------------------------------------\n\nexport interface FakeProviderOptions {\n /** Provider id (default `fake`). */\n id?: string\n /** Model id recorded in provenance (default `fake-model`). */\n model?: string\n /** Reported capabilities (default: both true). Set to test the capability gate. */\n capabilities?: AiProviderCapabilities\n /** Scripted objects returned in call order; the last entry is reused when exhausted. */\n objects?: unknown[]\n /** Compute the object from the request + zero-based call index (overrides `objects`). */\n respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown\n /** The scripted chat model `languageModel()` serves (default {@link createFakeChatModel}). */\n chatModel?: FakeChatModel\n}\n\n/** A FakeProvider that also records the requests it received, for assertions. */\nexport interface FakeProvider extends AiProvider {\n /** Every `generateStructured` request received, in order. */\n readonly calls: GenerateStructuredRequest[]\n /** The scripted fake chat model behind `languageModel()` (records its calls). */\n readonly chatModel: FakeChatModel\n}\n\nexport function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {\n const calls: GenerateStructuredRequest[] = []\n const objects = options.objects ?? []\n const chatModel = options.chatModel ?? createFakeChatModel()\n let index = 0\n\n return {\n id: options.id ?? 'fake',\n model: options.model ?? 'fake-model',\n calls,\n chatModel,\n capabilities: (): AiProviderCapabilities =>\n options.capabilities ?? { toolCalling: true, structuredOutput: true },\n\n languageModel: () => Promise.resolve(chatModel),\n\n generateStructured(request) {\n calls.push(request)\n const callIndex = index\n index += 1\n const object = options.respond\n ? options.respond(request, callIndex)\n : objects.length > 0\n ? objects[Math.min(callIndex, objects.length - 1)]\n : {}\n return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } })\n },\n }\n}\n"]}
|
package/dist/lib/ai/index.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export * from './knowledge.js';
|
|
|
26
26
|
export * from './rich-text-input.js';
|
|
27
27
|
export * from './constraints.js';
|
|
28
28
|
export * from './proposal-validation.js';
|
|
29
|
+
export * from './leash.js';
|
|
30
|
+
export * from './writer-form.js';
|
|
29
31
|
export * from './shape.js';
|
|
30
32
|
export * from './apply-content.js';
|
|
31
33
|
export * from './update-fields.js';
|
package/dist/lib/ai/index.js
CHANGED
|
@@ -26,6 +26,8 @@ export * from './knowledge.js';
|
|
|
26
26
|
export * from './rich-text-input.js';
|
|
27
27
|
export * from './constraints.js';
|
|
28
28
|
export * from './proposal-validation.js';
|
|
29
|
+
export * from './leash.js';
|
|
30
|
+
export * from './writer-form.js';
|
|
29
31
|
export * from './shape.js';
|
|
30
32
|
export * from './apply-content.js';
|
|
31
33
|
export * from './update-fields.js';
|
package/dist/lib/ai/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './constraints'\nexport * from './proposal-validation'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-fetch'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,SAAS,CAAA;AACvB,cAAc,eAAe,CAAA;AAC7B,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './constraints'\nexport * from './proposal-validation'\nexport * from './leash'\nexport * from './writer-form'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-fetch'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AssetFieldDef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { AssetCandidate, RelationCandidate, WriterLeash } from './types.js';
|
|
3
|
+
/** One searched document (a `searchDocuments` result). */
|
|
4
|
+
export interface SearchedDocument {
|
|
5
|
+
id: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
}
|
|
8
|
+
/** One searched library asset (a `listAssets` result) — with the two facts `accept` is judged on. */
|
|
9
|
+
export interface SearchedAsset {
|
|
10
|
+
id: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
contentType: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
}
|
|
15
|
+
/** What a conversation searched: documents by collection, and one flat library pool. */
|
|
16
|
+
export interface SearchResults {
|
|
17
|
+
readonly documents?: Readonly<Record<string, readonly SearchedDocument[]>>;
|
|
18
|
+
readonly assets?: readonly SearchedAsset[];
|
|
19
|
+
}
|
|
20
|
+
/** The relation candidates a set of searched documents offers. */
|
|
21
|
+
export declare function relationCandidatesOf(documents: Iterable<SearchedDocument>): RelationCandidate[];
|
|
22
|
+
/**
|
|
23
|
+
* The asset candidates an asset POSITION may hold (EC-588 A4, at every position since
|
|
24
|
+
* EC-593): the searched pool filtered by that position's own `accept` — an image field
|
|
25
|
+
* is never offered a PDF, and neither is an image prop of a component.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assetCandidatesOf(field: AssetFieldDef, pool: Iterable<SearchedAsset>): AssetCandidate[];
|
|
28
|
+
/**
|
|
29
|
+
* A {@link WriterLeash} over plain search results — what the chat's per-conversation
|
|
30
|
+
* store is, built once (tests and any non-chat caller construct one the same way).
|
|
31
|
+
*/
|
|
32
|
+
export declare function writerLeash(results: SearchResults): WriterLeash;
|
|
33
|
+
/** Nothing searched: every relation / asset position is empty (empty beats wrong). */
|
|
34
|
+
export declare const NO_SEARCHED_IDS: WriterLeash;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assetMatchesAccept } from '@elytracms/core/cms-core';
|
|
2
|
+
/** The relation candidates a set of searched documents offers. */
|
|
3
|
+
export function relationCandidatesOf(documents) {
|
|
4
|
+
return [...documents].map((r) => ({ id: r.id, ...(r.title ? { label: r.title } : {}) }));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The asset candidates an asset POSITION may hold (EC-588 A4, at every position since
|
|
8
|
+
* EC-593): the searched pool filtered by that position's own `accept` — an image field
|
|
9
|
+
* is never offered a PDF, and neither is an image prop of a component.
|
|
10
|
+
*/
|
|
11
|
+
export function assetCandidatesOf(field, pool) {
|
|
12
|
+
return [...pool]
|
|
13
|
+
.filter((asset) => assetMatchesAccept(asset, field.accept))
|
|
14
|
+
.map((a) => ({ id: a.id, ...(a.title ? { label: a.title } : {}) }));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A {@link WriterLeash} over plain search results — what the chat's per-conversation
|
|
18
|
+
* store is, built once (tests and any non-chat caller construct one the same way).
|
|
19
|
+
*/
|
|
20
|
+
export function writerLeash(results) {
|
|
21
|
+
return {
|
|
22
|
+
relationCandidates: (target) => relationCandidatesOf(results.documents?.[target] ?? []),
|
|
23
|
+
assetCandidates: (field) => assetCandidatesOf(field, results.assets ?? []),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Nothing searched: every relation / asset position is empty (empty beats wrong). */
|
|
27
|
+
export const NO_SEARCHED_IDS = writerLeash({});
|
|
28
|
+
//# sourceMappingURL=leash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leash.js","sourceRoot":"","sources":["../../../src/lib/ai/leash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAwB7D,kEAAkE;AAClE,MAAM,UAAU,oBAAoB,CAAC,SAAqC;IACxE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;AAC1F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAoB,EAAE,IAA6B;IACnF,OAAO,CAAC,GAAG,IAAI,CAAC;SACb,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;SAC1D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;AACvE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAAsB;IAChD,OAAO;QACL,kBAAkB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,oBAAoB,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACvF,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;KAC3E,CAAA;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,CAAC,MAAM,eAAe,GAAgB,WAAW,CAAC,EAAE,CAAC,CAAA","sourcesContent":["import { assetMatchesAccept } from '@elytracms/core/cms-core'\nimport type { AssetFieldDef } from '@elytracms/core/cms-core'\nimport type { AssetCandidate, RelationCandidate, WriterLeash } from './types'\n\n/** One searched document (a `searchDocuments` result). */\nexport interface SearchedDocument {\n id: string\n title?: string\n}\n\n/** One searched library asset (a `listAssets` result) — with the two facts `accept` is judged on. */\nexport interface SearchedAsset {\n id: string\n title?: string\n contentType: string\n filename: string\n}\n\n/** What a conversation searched: documents by collection, and one flat library pool. */\nexport interface SearchResults {\n readonly documents?: Readonly<Record<string, readonly SearchedDocument[]>>\n readonly assets?: readonly SearchedAsset[]\n}\n\n/** The relation candidates a set of searched documents offers. */\nexport function relationCandidatesOf(documents: Iterable<SearchedDocument>): RelationCandidate[] {\n return [...documents].map((r) => ({ id: r.id, ...(r.title ? { label: r.title } : {}) }))\n}\n\n/**\n * The asset candidates an asset POSITION may hold (EC-588 A4, at every position since\n * EC-593): the searched pool filtered by that position's own `accept` — an image field\n * is never offered a PDF, and neither is an image prop of a component.\n */\nexport function assetCandidatesOf(field: AssetFieldDef, pool: Iterable<SearchedAsset>): AssetCandidate[] {\n return [...pool]\n .filter((asset) => assetMatchesAccept(asset, field.accept))\n .map((a) => ({ id: a.id, ...(a.title ? { label: a.title } : {}) }))\n}\n\n/**\n * A {@link WriterLeash} over plain search results — what the chat's per-conversation\n * store is, built once (tests and any non-chat caller construct one the same way).\n */\nexport function writerLeash(results: SearchResults): WriterLeash {\n return {\n relationCandidates: (target) => relationCandidatesOf(results.documents?.[target] ?? []),\n assetCandidates: (field) => assetCandidatesOf(field, results.assets ?? []),\n }\n}\n\n/** Nothing searched: every relation / asset position is empty (empty beats wrong). */\nexport const NO_SEARCHED_IDS: WriterLeash = writerLeash({})\n"]}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* only the retired project-`VersionRecord` has `origin: 'edit' | 'import' |
|
|
8
8
|
* 'rollback'`, and the active per-document `DocumentVersionRecord` carries only
|
|
9
9
|
* `label`/`createdAt`). Phase 1 defines the seam minimally: the engine attaches
|
|
10
|
-
* this marker to its result; wiring it into `documents.
|
|
10
|
+
* this marker to its result; wiring it into `documents.patch` /
|
|
11
11
|
* `DocumentVersionRecord` (and any UI) is deferred to EC-199. No behaviour here.
|
|
12
12
|
*/
|
|
13
13
|
import type { AiProvenance, AiProvider } from './types.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provenance.js","sourceRoot":"","sources":["../../../src/lib/ai/provenance.ts"],"names":[],"mappings":"AAcA,8DAA8D;AAC9D,MAAM,CAAC,MAAM,SAAS,GAAG,WAAoB,CAAA;AAE7C;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,QAAoB,EAAE,KAAa,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;IACtF,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,GAAG,CAAC,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClE,EAAE;KACH,CAAA;AACH,CAAC","sourcesContent":["/**\n * Provenance seam (EC-331). AI-authored content carries an origin marker the\n * draft-write path records so an assistant-authored draft is attributable in\n * history — distinct from a human edit.\n *\n * There is no document-level or draft-level origin field in the store yet (today\n * only the retired project-`VersionRecord` has `origin: 'edit' | 'import' |\n * 'rollback'`, and the active per-document `DocumentVersionRecord` carries only\n * `label`/`createdAt`). Phase 1 defines the seam minimally: the engine attaches\n * this marker to its result; wiring it into `documents.
|
|
1
|
+
{"version":3,"file":"provenance.js","sourceRoot":"","sources":["../../../src/lib/ai/provenance.ts"],"names":[],"mappings":"AAcA,8DAA8D;AAC9D,MAAM,CAAC,MAAM,SAAS,GAAG,WAAoB,CAAA;AAE7C;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,QAAoB,EAAE,KAAa,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;IACtF,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,GAAG,CAAC,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClE,EAAE;KACH,CAAA;AACH,CAAC","sourcesContent":["/**\n * Provenance seam (EC-331). AI-authored content carries an origin marker the\n * draft-write path records so an assistant-authored draft is attributable in\n * history — distinct from a human edit.\n *\n * There is no document-level or draft-level origin field in the store yet (today\n * only the retired project-`VersionRecord` has `origin: 'edit' | 'import' |\n * 'rollback'`, and the active per-document `DocumentVersionRecord` carries only\n * `label`/`createdAt`). Phase 1 defines the seam minimally: the engine attaches\n * this marker to its result; wiring it into `documents.patch` /\n * `DocumentVersionRecord` (and any UI) is deferred to EC-199. No behaviour here.\n */\nimport type { AiProvenance, AiProvider } from './types'\n\n/** The origin discriminator for assistant-authored writes. */\nexport const AI_ORIGIN = 'assistant' as const\n\n/**\n * Build the provenance marker for an engine run. `at` defaults to now (ISO); a\n * fixed clock can be injected for deterministic tests.\n */\nexport function aiProvenance(provider: AiProvider, at: string = new Date().toISOString()): AiProvenance {\n return {\n origin: AI_ORIGIN,\n provider: provider.id,\n ...(provider.model !== undefined ? { model: provider.model } : {}),\n at,\n }\n}\n"]}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import type { RichTextValue } from '@elytracms/core/rich-text';
|
|
2
2
|
/**
|
|
3
|
-
* The model-input policy for rich-text
|
|
4
|
-
*
|
|
5
|
-
* (`buildUpdateProposal`):
|
|
3
|
+
* The model-input policy for rich-text values (EC-379), at every position the AI
|
|
4
|
+
* writes one — read by THE writer-form converter (`writer-form.ts`, EC-593), which
|
|
5
|
+
* create (`applyContentWithShape`) and edit (`buildUpdateProposal`) both call:
|
|
6
6
|
*
|
|
7
7
|
* - a **string is ALWAYS markdown** (never JSON-sniffed — deterministic): it is
|
|
8
8
|
* converted through the engine's strict subset converter, and out-of-subset
|
|
9
9
|
* constructs come back as explicit problems the caller feeds to the model for
|
|
10
10
|
* repair;
|
|
11
|
-
* - a `{ version,
|
|
12
|
-
* (the model sees
|
|
13
|
-
*
|
|
14
|
-
* schema-validated here and leash-validated downstream by
|
|
15
|
-
* + the injected `validateRichTextEmbeds`;
|
|
11
|
+
* - a stored `{ version: 2, content }` **value is still accepted** for robustness
|
|
12
|
+
* (the model sees one in `getDocument` output whenever a value is not losslessly
|
|
13
|
+
* expressible as markdown — embedded components, internal links — and may echo it
|
|
14
|
+
* back) — it is schema-validated here and leash-validated downstream by
|
|
15
|
+
* `validateDocument` + the injected `validateRichTextEmbeds`;
|
|
16
16
|
* - anything else is a problem. The target SHAPE only ever advertises the
|
|
17
17
|
* markdown string form (`shape.ts`), so markdown is what the model is
|
|
18
18
|
* steered to author.
|
|
@@ -13,7 +13,8 @@ export function richTextValueFromModelInput(raw) {
|
|
|
13
13
|
return {
|
|
14
14
|
ok: false,
|
|
15
15
|
problems: [
|
|
16
|
-
|
|
16
|
+
// SHAPES #16 (EC-593): the storage value this names is the one the engine reads.
|
|
17
|
+
`Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}); the provided object is not a valid stored { version: 2, content } value either.`,
|
|
17
18
|
],
|
|
18
19
|
};
|
|
19
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-text-input.js","sourceRoot":"","sources":["../../../src/lib/ai/rich-text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,2BAA2B,CAAA;AAyBlC,MAAM,UAAU,2BAA2B,CAAC,GAAY;IACtD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,4BAA4B,CAAC,GAAG,CAAC,CAAA;QACnD,IAAI,SAAS,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAA;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAA;IAChF,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;QACvD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE;gBACR,oDAAoD,0BAA0B,
|
|
1
|
+
{"version":3,"file":"rich-text-input.js","sourceRoot":"","sources":["../../../src/lib/ai/rich-text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,2BAA2B,CAAA;AAyBlC,MAAM,UAAU,2BAA2B,CAAC,GAAY;IACtD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,4BAA4B,CAAC,GAAG,CAAC,CAAA;QACnD,IAAI,SAAS,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAA;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAA;IAChF,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;QACvD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE;gBACR,iFAAiF;gBACjF,oDAAoD,0BAA0B,oFAAoF;aACnK;SACF,CAAA;IACH,CAAC;IACD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,QAAQ,EAAE,CAAC,oDAAoD,0BAA0B,IAAI,CAAC;KAC/F,CAAA;AACH,CAAC","sourcesContent":["import {\n MARKDOWN_PROSE_DESCRIPTION,\n markdownProseToRichTextValue,\n parseRichTextValue,\n} from '@elytracms/core/rich-text'\nimport type { RichTextValue } from '@elytracms/core/rich-text'\n\n/**\n * The model-input policy for rich-text values (EC-379), at every position the AI\n * writes one — read by THE writer-form converter (`writer-form.ts`, EC-593), which\n * create (`applyContentWithShape`) and edit (`buildUpdateProposal`) both call:\n *\n * - a **string is ALWAYS markdown** (never JSON-sniffed — deterministic): it is\n * converted through the engine's strict subset converter, and out-of-subset\n * constructs come back as explicit problems the caller feeds to the model for\n * repair;\n * - a stored `{ version: 2, content }` **value is still accepted** for robustness\n * (the model sees one in `getDocument` output whenever a value is not losslessly\n * expressible as markdown — embedded components, internal links — and may echo it\n * back) — it is schema-validated here and leash-validated downstream by\n * `validateDocument` + the injected `validateRichTextEmbeds`;\n * - anything else is a problem. The target SHAPE only ever advertises the\n * markdown string form (`shape.ts`), so markdown is what the model is\n * steered to author.\n */\nexport type RichTextInputResult =\n | { ok: true; value: RichTextValue }\n | { ok: false; problems: string[] }\n\nexport function richTextValueFromModelInput(raw: unknown): RichTextInputResult {\n if (typeof raw === 'string') {\n const converted = markdownProseToRichTextValue(raw)\n if (converted.ok) return { ok: true, value: converted.value }\n return { ok: false, problems: converted.issues.map((issue) => issue.message) }\n }\n if (typeof raw === 'object' && raw !== null) {\n const parsed = parseRichTextValue(raw)\n if (parsed.ok) return { ok: true, value: parsed.value }\n return {\n ok: false,\n problems: [\n // SHAPES #16 (EC-593): the storage value this names is the one the engine reads.\n `Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}); the provided object is not a valid stored { version: 2, content } value either.`,\n ],\n }\n }\n return {\n ok: false,\n problems: [`Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}).`],\n }\n}\n"]}
|
|
@@ -21,19 +21,42 @@ export interface RichTextPlan {
|
|
|
21
21
|
blocks: Map<string, BlockTables>;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Walk a rich-text value
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Walk a rich-text value — or any `{ version: 2, content }` sequence, a `blocks` value's
|
|
25
|
+
* typography included — and extract its translatable blocks. Deterministic; never
|
|
26
|
+
* throws. A value that is not a readable sequence yields no units, a plan whose rebuild
|
|
27
|
+
* is a no-op, and `readable: false` — the caller REPORTS it (EC-593: translate never
|
|
28
|
+
* skips silently). Code blocks are skipped (code is not prose). Component nodes are not
|
|
29
|
+
* text here: their props are positions of the value traversal, translated by the caller.
|
|
30
|
+
*
|
|
31
|
+
* The walk reads the STORED value once the parse proved it readable, so every node the
|
|
32
|
+
* rebuild does not touch — a component node, an atom — comes back byte-for-byte as
|
|
33
|
+
* stored (a parse is a validation, not a copy to write from).
|
|
27
34
|
*/
|
|
28
35
|
export declare function extractRichTextUnits(raw: unknown): {
|
|
29
36
|
units: RichTextBlockUnit[];
|
|
30
37
|
plan: RichTextPlan;
|
|
38
|
+
readable: boolean;
|
|
31
39
|
};
|
|
40
|
+
/** {@link rebuildRichTextReport}'s outcome: the value, and which blocks did and did not apply. */
|
|
41
|
+
export interface RichTextRebuild {
|
|
42
|
+
value: RichTextValue;
|
|
43
|
+
/** Block ids whose translation was applied. */
|
|
44
|
+
applied: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Block ids whose translation came back but did not decode token-faithfully — the
|
|
47
|
+
* ORIGINAL block was kept (never a broken tree), so it is untranslated: EC-593 makes
|
|
48
|
+
* the caller report it rather than count it as done.
|
|
49
|
+
*/
|
|
50
|
+
rejected: string[];
|
|
51
|
+
}
|
|
32
52
|
/**
|
|
33
53
|
* Rebuild a rich-text value from a plan + a per-block translation map. A block absent
|
|
34
54
|
* from `translations` (skipped/aborted) or whose translated tokens don't parse keeps its
|
|
35
|
-
* ORIGINAL content — never a broken structure. Returns a fresh `{ version,
|
|
55
|
+
* ORIGINAL content — never a broken structure. Returns a fresh `{ version: 2, content }`
|
|
56
|
+
* value, and the blocks it applied and rejected.
|
|
36
57
|
*/
|
|
58
|
+
export declare function rebuildRichTextReport(plan: RichTextPlan, translations: ReadonlyMap<string, string>): RichTextRebuild;
|
|
59
|
+
/** {@link rebuildRichTextReport}'s value alone. */
|
|
37
60
|
export declare function rebuildRichText(plan: RichTextPlan, translations: ReadonlyMap<string, string>): RichTextValue;
|
|
38
61
|
/**
|
|
39
62
|
* Apply `fn` to only the human-readable text segments of an encoded block (the runs
|
|
@@ -86,18 +86,26 @@ function hasTranslatableText(encoded) {
|
|
|
86
86
|
return encoded.replace(TOKEN_RE, '').trim().length > 0;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* Walk a rich-text value
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* Walk a rich-text value — or any `{ version: 2, content }` sequence, a `blocks` value's
|
|
90
|
+
* typography included — and extract its translatable blocks. Deterministic; never
|
|
91
|
+
* throws. A value that is not a readable sequence yields no units, a plan whose rebuild
|
|
92
|
+
* is a no-op, and `readable: false` — the caller REPORTS it (EC-593: translate never
|
|
93
|
+
* skips silently). Code blocks are skipped (code is not prose). Component nodes are not
|
|
94
|
+
* text here: their props are positions of the value traversal, translated by the caller.
|
|
95
|
+
*
|
|
96
|
+
* The walk reads the STORED value once the parse proved it readable, so every node the
|
|
97
|
+
* rebuild does not touch — a component node, an atom — comes back byte-for-byte as
|
|
98
|
+
* stored (a parse is a validation, not a copy to write from).
|
|
92
99
|
*/
|
|
93
100
|
export function extractRichTextUnits(raw) {
|
|
94
101
|
const parsed = parseRichTextValue(raw);
|
|
95
102
|
const blocks = new Map();
|
|
96
103
|
const units = [];
|
|
97
104
|
if (!parsed.ok) {
|
|
98
|
-
return { units, plan: { value: { version: 2, content: [] }, blocks } };
|
|
105
|
+
return { units, plan: { value: { version: 2, content: [] }, blocks }, readable: false };
|
|
99
106
|
}
|
|
100
|
-
|
|
107
|
+
// A readable value whose `content` the parse defaulted (absent) has nothing stored to keep.
|
|
108
|
+
const value = Array.isArray(raw.content) ? raw : parsed.value;
|
|
101
109
|
const walk = (node, path) => {
|
|
102
110
|
if (node.type === 'codeBlock')
|
|
103
111
|
return;
|
|
@@ -113,7 +121,7 @@ export function extractRichTextUnits(raw) {
|
|
|
113
121
|
node.content?.forEach((child, i) => walk(child, [...path, i]));
|
|
114
122
|
};
|
|
115
123
|
value.content.forEach((node, i) => walk(node, [i]));
|
|
116
|
-
return { units, plan: { value, blocks } };
|
|
124
|
+
return { units, plan: { value, blocks }, readable: true };
|
|
117
125
|
}
|
|
118
126
|
// --- Reconstruction ---------------------------------------------------------------
|
|
119
127
|
/**
|
|
@@ -191,21 +199,31 @@ function replaceAtPath(nodes, path, content) {
|
|
|
191
199
|
/**
|
|
192
200
|
* Rebuild a rich-text value from a plan + a per-block translation map. A block absent
|
|
193
201
|
* from `translations` (skipped/aborted) or whose translated tokens don't parse keeps its
|
|
194
|
-
* ORIGINAL content — never a broken structure. Returns a fresh `{ version,
|
|
202
|
+
* ORIGINAL content — never a broken structure. Returns a fresh `{ version: 2, content }`
|
|
203
|
+
* value, and the blocks it applied and rejected.
|
|
195
204
|
*/
|
|
196
|
-
export function
|
|
205
|
+
export function rebuildRichTextReport(plan, translations) {
|
|
197
206
|
let content = plan.value.content.map((n) => n);
|
|
207
|
+
const applied = [];
|
|
208
|
+
const rejected = [];
|
|
198
209
|
for (const [id, tables] of plan.blocks) {
|
|
199
210
|
const translated = translations.get(id);
|
|
200
211
|
if (translated === undefined)
|
|
201
212
|
continue;
|
|
202
213
|
const decoded = decodeBlock(translated, tables);
|
|
203
|
-
if (!decoded)
|
|
214
|
+
if (!decoded) {
|
|
215
|
+
rejected.push(id);
|
|
204
216
|
continue;
|
|
217
|
+
}
|
|
218
|
+
applied.push(id);
|
|
205
219
|
content = replaceAtPath(content, tables.path, decoded);
|
|
206
220
|
}
|
|
207
221
|
const doc = { type: 'doc', content };
|
|
208
|
-
return createRichTextValue(doc);
|
|
222
|
+
return { value: createRichTextValue(doc), applied, rejected };
|
|
223
|
+
}
|
|
224
|
+
/** {@link rebuildRichTextReport}'s value alone. */
|
|
225
|
+
export function rebuildRichText(plan, translations) {
|
|
226
|
+
return rebuildRichTextReport(plan, translations).value;
|
|
209
227
|
}
|
|
210
228
|
// --- Deterministic segment transform (FakeProvider + tests) -----------------------
|
|
211
229
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-text-translate.js","sourceRoot":"","sources":["../../../src/lib/ai/rich-text-translate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Structure-preserving rich-text translation (EC-332).\n *\n * The load-bearing invariant: **the model never emits node structure.** Each\n * translatable block's inline content is serialized to a single plain string in which\n * every text run is wrapped in an opaque placeholder token (`[[s{k}]]…[[/s{k}]]`) and\n * every non-text inline atom (image, embed, componentEmbed, hardBreak, mention, any\n * unknown inline node) is a self-closing token (`[[a{k}]]`). The model translates ONLY\n * the human-readable text between tokens; on reconstruction each token is mapped back to\n * the ORIGINAL stored mark stack / atom node — byte-for-byte — so marks, links, internal\n * `attrs.ref` references, and embeds survive by construction. Only `text` leaves change.\n *\n * This ports the Sanity precedent's `blockToMarkdown` / `markdownToChildren` moves to\n * Elytra's ProseMirror storage, but uses opaque placeholder tokens rather than literal\n * markdown: Elytra marks (`strike`/`underline`/`link` + its `attrs.ref` envelope) do not\n * round-trip through markdown losslessly, and reference-index integrity (EC-286) requires\n * the `ref` bytes to be preserved exactly — which token-indexed restoration guarantees.\n *\n * Robust against a misbehaving translator: if a translated block's tokens don't parse\n * back to a coherent structure, the ORIGINAL block is kept unchanged (never a broken\n * tree). Determinism: given a deterministic segment transform the output is identical\n * every run. See `rich-text-translate.test.ts` for the round-trip property tests.\n */\nimport { createRichTextValue, parseRichTextValue } from '@elytracms/core/rich-text'\nimport type {\n RichTextDoc,\n RichTextMark,\n RichTextNode,\n RichTextValue,\n} from '@elytracms/core/rich-text'\n\n// --- Token vocabulary -------------------------------------------------------------\n\nconst spanOpen = (k: number): string => `[[s${k}]]`\nconst spanClose = (k: number): string => `[[/s${k}]]`\nconst atomToken = (k: number): string => `[[a${k}]]`\n\n/** Matches every placeholder token: open/close spans and atoms. */\nconst TOKEN_RE = /\\[\\[(\\/?)([sa])(\\d+)\\]\\]/g\n\n/** True when a string contains any placeholder token (non-global; no shared state). */\nfunction containsToken(value: string): boolean {\n return /\\[\\[\\/?[sa]\\d+\\]\\]/.test(value)\n}\n\n/** Key-order-insensitive canonical JSON for de-duplicating identical mark stacks. */\nfunction canonicalJson(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`)\n return `{${entries.join(',')}}`\n}\n\n// --- Block-unit model -------------------------------------------------------------\n\n/** The per-block side tables the rebuild uses to restore marks + atoms verbatim. */\ninterface BlockTables {\n /** Path (content-index chain) to the text-bearing node within the doc. */\n path: number[]\n /** De-duplicated mark stacks; a span token's index selects one. */\n markStacks: RichTextMark[][]\n /** Inline atom nodes (image/embed/componentEmbed/hardBreak/…), preserved verbatim. */\n atoms: RichTextNode[]\n}\n\n/** One translatable rich-text block: an id stable within the field + its encoded text. */\nexport interface RichTextBlockUnit {\n /** Stable within the field (the block's content-index path, dot-joined). */\n id: string\n /** The encoded translatable string (text runs + placeholder tokens). */\n text: string\n}\n\n/** The extraction plan: the original value + every block's restoration tables. */\nexport interface RichTextPlan {\n value: RichTextValue\n blocks: Map<string, BlockTables>\n}\n\n/** True when a node is a text leaf (`{ type: 'text', text }`). */\nfunction isTextNode(node: RichTextNode): boolean {\n return node.type === 'text' && typeof node.text === 'string'\n}\n\n/** True when a node's direct children include a text leaf (⇒ it is an inline/text block). */\nfunction hasDirectText(node: RichTextNode): boolean {\n return Array.isArray(node.content) && node.content.some(isTextNode)\n}\n\n/** Encode one text-bearing block's inline content into a token string + tables. */\nfunction encodeBlock(node: RichTextNode): { encoded: string; markStacks: RichTextMark[][]; atoms: RichTextNode[] } {\n const markStacks: RichTextMark[][] = []\n const markIndex = new Map<string, number>()\n const atoms: RichTextNode[] = []\n const stackKey = (marks: RichTextMark[] | undefined): number => {\n const key = canonicalJson(marks ?? [])\n const existing = markIndex.get(key)\n if (existing !== undefined) return existing\n const idx = markStacks.length\n markStacks.push(marks ? marks.map((m) => ({ ...m })) : [])\n markIndex.set(key, idx)\n return idx\n }\n const parts: string[] = []\n for (const child of node.content ?? []) {\n if (isTextNode(child)) {\n const k = stackKey(child.marks)\n parts.push(`${spanOpen(k)}${child.text ?? ''}${spanClose(k)}`)\n } else {\n const k = atoms.length\n atoms.push(child)\n parts.push(atomToken(k))\n }\n }\n return { encoded: parts.join(''), markStacks, atoms }\n}\n\n/** True when an encoded block holds at least one non-whitespace translatable segment. */\nfunction hasTranslatableText(encoded: string): boolean {\n return encoded.replace(TOKEN_RE, '').trim().length > 0\n}\n\n/**\n * Walk a rich-text value and extract its translatable blocks. Deterministic; never\n * throws. A value that is not parseable rich text yields no units (and a plan whose\n * rebuild is a no-op). Code blocks are skipped (code is not prose).\n */\nexport function extractRichTextUnits(raw: unknown): { units: RichTextBlockUnit[]; plan: RichTextPlan } {\n const parsed = parseRichTextValue(raw)\n const blocks = new Map<string, BlockTables>()\n const units: RichTextBlockUnit[] = []\n if (!parsed.ok) {\n return { units, plan: { value: { version: 2, content: [] }, blocks } }\n }\n const value = parsed.value\n\n const walk = (node: RichTextNode, path: number[]): void => {\n if (node.type === 'codeBlock') return\n if (hasDirectText(node)) {\n const { encoded, markStacks, atoms } = encodeBlock(node)\n if (hasTranslatableText(encoded)) {\n const id = path.join('.')\n blocks.set(id, { path, markStacks, atoms })\n units.push({ id, text: encoded })\n }\n return\n }\n node.content?.forEach((child, i) => walk(child, [...path, i]))\n }\n\n (value.content as RichTextNode[]).forEach((node, i) => walk(node, [i]))\n return { units, plan: { value, blocks } }\n}\n\n// --- Reconstruction ---------------------------------------------------------------\n\n/**\n * Parse an encoded (translated) block string back into inline nodes, or `null` on any\n * structural mismatch. Strict on purpose: the block is only rewritten when the translated\n * string is token-faithful — every atom token present exactly once, every span opened and\n * closed, and no non-whitespace text stranded outside a span (which would mean a dropped\n * formatting/reference token). Anything else keeps the ORIGINAL block, so an internal\n * link, image ref, or embed can never be silently lost to a garbled translation.\n */\nfunction decodeBlock(translated: string, tables: BlockTables): RichTextNode[] | null {\n const out: RichTextNode[] = []\n const atomsUsed = new Set<number>()\n let cursor = 0\n const pushText = (text: string, marks: RichTextMark[]): void => {\n if (text.length === 0) return\n out.push(marks.length > 0 ? { type: 'text', text, marks: marks.map((m) => ({ ...m })) } : { type: 'text', text })\n }\n const re = new RegExp(TOKEN_RE.source, 'g')\n let match: RegExpExecArray | null\n while ((match = re.exec(translated)) !== null) {\n const [token, closing, kind, digits] = match\n const index = Number(digits)\n const before = translated.slice(cursor, match.index)\n // Only whitespace may sit between/around tokens (a translator's added spacing);\n // stranded words mean a formatting/reference token was dropped → reject.\n if (before.trim().length > 0) return null\n if (kind === 'a') {\n const atom = tables.atoms[index]\n if (!atom || atomsUsed.has(index)) return null\n atomsUsed.add(index)\n out.push(atom)\n cursor = match.index + token.length\n continue\n }\n // kind === 's'\n if (closing === '/') return null // an unmatched close — garbled structure\n const marks = tables.markStacks[index]\n if (!marks) return null\n const close = spanClose(index)\n const closeAt = translated.indexOf(close, re.lastIndex)\n if (closeAt === -1) return null\n const inner = translated.slice(re.lastIndex, closeAt)\n // Inner must not itself contain tokens (spans never nest in our encoding).\n if (containsToken(inner)) return null\n pushText(inner, marks)\n cursor = closeAt + close.length\n re.lastIndex = cursor\n }\n if (translated.slice(cursor).trim().length > 0) return null\n // Every atom the block carried (image/embed/ref) must survive the translation.\n if (atomsUsed.size !== tables.atoms.length) return null\n return out\n}\n\n/** Deep-clone a node array and replace the inline content at `path` with `content`. */\nfunction replaceAtPath(nodes: RichTextNode[], path: number[], content: RichTextNode[]): RichTextNode[] {\n const [head, ...rest] = path\n return nodes.map((node, i) => {\n if (i !== head) return node\n if (rest.length === 0) return { ...node, content }\n return { ...node, content: replaceAtPath(node.content ?? [], rest, content) }\n })\n}\n\n/**\n * Rebuild a rich-text value from a plan + a per-block translation map. A block absent\n * from `translations` (skipped/aborted) or whose translated tokens don't parse keeps its\n * ORIGINAL content — never a broken structure. Returns a fresh `{ version, doc }` value.\n */\nexport function rebuildRichText(\n plan: RichTextPlan,\n translations: ReadonlyMap<string, string>,\n): RichTextValue {\n let content = (plan.value.content as RichTextNode[]).map((n) => n)\n for (const [id, tables] of plan.blocks) {\n const translated = translations.get(id)\n if (translated === undefined) continue\n const decoded = decodeBlock(translated, tables)\n if (!decoded) continue\n content = replaceAtPath(content, tables.path, decoded)\n }\n const doc: RichTextDoc = { type: 'doc', content }\n return createRichTextValue(doc)\n}\n\n// --- Deterministic segment transform (FakeProvider + tests) -----------------------\n\n/**\n * Apply `fn` to only the human-readable text segments of an encoded block (the runs\n * between placeholder tokens), leaving every token exactly in place. This is how the\n * deterministic FakeProvider \"translates\" a block and how the property tests apply a\n * real text change without ever touching structure. Pure.\n */\nexport function mapEncodedSegments(encoded: string, fn: (segment: string) => string): string {\n const re = new RegExp(TOKEN_RE.source, 'g')\n let out = ''\n let cursor = 0\n let match: RegExpExecArray | null\n while ((match = re.exec(encoded)) !== null) {\n out += fn(encoded.slice(cursor, match.index)) + match[0]\n cursor = match.index + match[0].length\n }\n out += fn(encoded.slice(cursor))\n return out\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rich-text-translate.js","sourceRoot":"","sources":["../../../src/lib/ai/rich-text-translate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Structure-preserving rich-text translation (EC-332).\n *\n * The load-bearing invariant: **the model never emits node structure.** Each\n * translatable block's inline content is serialized to a single plain string in which\n * every text run is wrapped in an opaque placeholder token (`[[s{k}]]…[[/s{k}]]`) and\n * every non-text inline atom (image, embed, componentEmbed, hardBreak, mention, any\n * unknown inline node) is a self-closing token (`[[a{k}]]`). The model translates ONLY\n * the human-readable text between tokens; on reconstruction each token is mapped back to\n * the ORIGINAL stored mark stack / atom node — byte-for-byte — so marks, links, internal\n * `attrs.ref` references, and embeds survive by construction. Only `text` leaves change.\n *\n * This ports the Sanity precedent's `blockToMarkdown` / `markdownToChildren` moves to\n * Elytra's ProseMirror storage, but uses opaque placeholder tokens rather than literal\n * markdown: Elytra marks (`strike`/`underline`/`link` + its `attrs.ref` envelope) do not\n * round-trip through markdown losslessly, and reference-index integrity (EC-286) requires\n * the `ref` bytes to be preserved exactly — which token-indexed restoration guarantees.\n *\n * Robust against a misbehaving translator: if a translated block's tokens don't parse\n * back to a coherent structure, the ORIGINAL block is kept unchanged (never a broken\n * tree). Determinism: given a deterministic segment transform the output is identical\n * every run. See `rich-text-translate.test.ts` for the round-trip property tests.\n */\nimport { createRichTextValue, parseRichTextValue } from '@elytracms/core/rich-text'\nimport type {\n RichTextDoc,\n RichTextMark,\n RichTextNode,\n RichTextValue,\n} from '@elytracms/core/rich-text'\n\n// --- Token vocabulary -------------------------------------------------------------\n\nconst spanOpen = (k: number): string => `[[s${k}]]`\nconst spanClose = (k: number): string => `[[/s${k}]]`\nconst atomToken = (k: number): string => `[[a${k}]]`\n\n/** Matches every placeholder token: open/close spans and atoms. */\nconst TOKEN_RE = /\\[\\[(\\/?)([sa])(\\d+)\\]\\]/g\n\n/** True when a string contains any placeholder token (non-global; no shared state). */\nfunction containsToken(value: string): boolean {\n return /\\[\\[\\/?[sa]\\d+\\]\\]/.test(value)\n}\n\n/** Key-order-insensitive canonical JSON for de-duplicating identical mark stacks. */\nfunction canonicalJson(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`)\n return `{${entries.join(',')}}`\n}\n\n// --- Block-unit model -------------------------------------------------------------\n\n/** The per-block side tables the rebuild uses to restore marks + atoms verbatim. */\ninterface BlockTables {\n /** Path (content-index chain) to the text-bearing node within the doc. */\n path: number[]\n /** De-duplicated mark stacks; a span token's index selects one. */\n markStacks: RichTextMark[][]\n /** Inline atom nodes (image/embed/componentEmbed/hardBreak/…), preserved verbatim. */\n atoms: RichTextNode[]\n}\n\n/** One translatable rich-text block: an id stable within the field + its encoded text. */\nexport interface RichTextBlockUnit {\n /** Stable within the field (the block's content-index path, dot-joined). */\n id: string\n /** The encoded translatable string (text runs + placeholder tokens). */\n text: string\n}\n\n/** The extraction plan: the original value + every block's restoration tables. */\nexport interface RichTextPlan {\n value: RichTextValue\n blocks: Map<string, BlockTables>\n}\n\n/** True when a node is a text leaf (`{ type: 'text', text }`). */\nfunction isTextNode(node: RichTextNode): boolean {\n return node.type === 'text' && typeof node.text === 'string'\n}\n\n/** True when a node's direct children include a text leaf (⇒ it is an inline/text block). */\nfunction hasDirectText(node: RichTextNode): boolean {\n return Array.isArray(node.content) && node.content.some(isTextNode)\n}\n\n/** Encode one text-bearing block's inline content into a token string + tables. */\nfunction encodeBlock(node: RichTextNode): { encoded: string; markStacks: RichTextMark[][]; atoms: RichTextNode[] } {\n const markStacks: RichTextMark[][] = []\n const markIndex = new Map<string, number>()\n const atoms: RichTextNode[] = []\n const stackKey = (marks: RichTextMark[] | undefined): number => {\n const key = canonicalJson(marks ?? [])\n const existing = markIndex.get(key)\n if (existing !== undefined) return existing\n const idx = markStacks.length\n markStacks.push(marks ? marks.map((m) => ({ ...m })) : [])\n markIndex.set(key, idx)\n return idx\n }\n const parts: string[] = []\n for (const child of node.content ?? []) {\n if (isTextNode(child)) {\n const k = stackKey(child.marks)\n parts.push(`${spanOpen(k)}${child.text ?? ''}${spanClose(k)}`)\n } else {\n const k = atoms.length\n atoms.push(child)\n parts.push(atomToken(k))\n }\n }\n return { encoded: parts.join(''), markStacks, atoms }\n}\n\n/** True when an encoded block holds at least one non-whitespace translatable segment. */\nfunction hasTranslatableText(encoded: string): boolean {\n return encoded.replace(TOKEN_RE, '').trim().length > 0\n}\n\n/**\n * Walk a rich-text value — or any `{ version: 2, content }` sequence, a `blocks` value's\n * typography included — and extract its translatable blocks. Deterministic; never\n * throws. A value that is not a readable sequence yields no units, a plan whose rebuild\n * is a no-op, and `readable: false` — the caller REPORTS it (EC-593: translate never\n * skips silently). Code blocks are skipped (code is not prose). Component nodes are not\n * text here: their props are positions of the value traversal, translated by the caller.\n *\n * The walk reads the STORED value once the parse proved it readable, so every node the\n * rebuild does not touch — a component node, an atom — comes back byte-for-byte as\n * stored (a parse is a validation, not a copy to write from).\n */\nexport function extractRichTextUnits(raw: unknown): {\n units: RichTextBlockUnit[]\n plan: RichTextPlan\n readable: boolean\n} {\n const parsed = parseRichTextValue(raw)\n const blocks = new Map<string, BlockTables>()\n const units: RichTextBlockUnit[] = []\n if (!parsed.ok) {\n return { units, plan: { value: { version: 2, content: [] }, blocks }, readable: false }\n }\n // A readable value whose `content` the parse defaulted (absent) has nothing stored to keep.\n const value = Array.isArray((raw as { content?: unknown }).content) ? (raw as RichTextValue) : parsed.value\n\n const walk = (node: RichTextNode, path: number[]): void => {\n if (node.type === 'codeBlock') return\n if (hasDirectText(node)) {\n const { encoded, markStacks, atoms } = encodeBlock(node)\n if (hasTranslatableText(encoded)) {\n const id = path.join('.')\n blocks.set(id, { path, markStacks, atoms })\n units.push({ id, text: encoded })\n }\n return\n }\n node.content?.forEach((child, i) => walk(child, [...path, i]))\n }\n\n (value.content as RichTextNode[]).forEach((node, i) => walk(node, [i]))\n return { units, plan: { value, blocks }, readable: true }\n}\n\n// --- Reconstruction ---------------------------------------------------------------\n\n/**\n * Parse an encoded (translated) block string back into inline nodes, or `null` on any\n * structural mismatch. Strict on purpose: the block is only rewritten when the translated\n * string is token-faithful — every atom token present exactly once, every span opened and\n * closed, and no non-whitespace text stranded outside a span (which would mean a dropped\n * formatting/reference token). Anything else keeps the ORIGINAL block, so an internal\n * link, image ref, or embed can never be silently lost to a garbled translation.\n */\nfunction decodeBlock(translated: string, tables: BlockTables): RichTextNode[] | null {\n const out: RichTextNode[] = []\n const atomsUsed = new Set<number>()\n let cursor = 0\n const pushText = (text: string, marks: RichTextMark[]): void => {\n if (text.length === 0) return\n out.push(marks.length > 0 ? { type: 'text', text, marks: marks.map((m) => ({ ...m })) } : { type: 'text', text })\n }\n const re = new RegExp(TOKEN_RE.source, 'g')\n let match: RegExpExecArray | null\n while ((match = re.exec(translated)) !== null) {\n const [token, closing, kind, digits] = match\n const index = Number(digits)\n const before = translated.slice(cursor, match.index)\n // Only whitespace may sit between/around tokens (a translator's added spacing);\n // stranded words mean a formatting/reference token was dropped → reject.\n if (before.trim().length > 0) return null\n if (kind === 'a') {\n const atom = tables.atoms[index]\n if (!atom || atomsUsed.has(index)) return null\n atomsUsed.add(index)\n out.push(atom)\n cursor = match.index + token.length\n continue\n }\n // kind === 's'\n if (closing === '/') return null // an unmatched close — garbled structure\n const marks = tables.markStacks[index]\n if (!marks) return null\n const close = spanClose(index)\n const closeAt = translated.indexOf(close, re.lastIndex)\n if (closeAt === -1) return null\n const inner = translated.slice(re.lastIndex, closeAt)\n // Inner must not itself contain tokens (spans never nest in our encoding).\n if (containsToken(inner)) return null\n pushText(inner, marks)\n cursor = closeAt + close.length\n re.lastIndex = cursor\n }\n if (translated.slice(cursor).trim().length > 0) return null\n // Every atom the block carried (image/embed/ref) must survive the translation.\n if (atomsUsed.size !== tables.atoms.length) return null\n return out\n}\n\n/** Deep-clone a node array and replace the inline content at `path` with `content`. */\nfunction replaceAtPath(nodes: RichTextNode[], path: number[], content: RichTextNode[]): RichTextNode[] {\n const [head, ...rest] = path\n return nodes.map((node, i) => {\n if (i !== head) return node\n if (rest.length === 0) return { ...node, content }\n return { ...node, content: replaceAtPath(node.content ?? [], rest, content) }\n })\n}\n\n/** {@link rebuildRichTextReport}'s outcome: the value, and which blocks did and did not apply. */\nexport interface RichTextRebuild {\n value: RichTextValue\n /** Block ids whose translation was applied. */\n applied: string[]\n /**\n * Block ids whose translation came back but did not decode token-faithfully — the\n * ORIGINAL block was kept (never a broken tree), so it is untranslated: EC-593 makes\n * the caller report it rather than count it as done.\n */\n rejected: string[]\n}\n\n/**\n * Rebuild a rich-text value from a plan + a per-block translation map. A block absent\n * from `translations` (skipped/aborted) or whose translated tokens don't parse keeps its\n * ORIGINAL content — never a broken structure. Returns a fresh `{ version: 2, content }`\n * value, and the blocks it applied and rejected.\n */\nexport function rebuildRichTextReport(\n plan: RichTextPlan,\n translations: ReadonlyMap<string, string>,\n): RichTextRebuild {\n let content = (plan.value.content as RichTextNode[]).map((n) => n)\n const applied: string[] = []\n const rejected: string[] = []\n for (const [id, tables] of plan.blocks) {\n const translated = translations.get(id)\n if (translated === undefined) continue\n const decoded = decodeBlock(translated, tables)\n if (!decoded) {\n rejected.push(id)\n continue\n }\n applied.push(id)\n content = replaceAtPath(content, tables.path, decoded)\n }\n const doc: RichTextDoc = { type: 'doc', content }\n return { value: createRichTextValue(doc), applied, rejected }\n}\n\n/** {@link rebuildRichTextReport}'s value alone. */\nexport function rebuildRichText(\n plan: RichTextPlan,\n translations: ReadonlyMap<string, string>,\n): RichTextValue {\n return rebuildRichTextReport(plan, translations).value\n}\n\n// --- Deterministic segment transform (FakeProvider + tests) -----------------------\n\n/**\n * Apply `fn` to only the human-readable text segments of an encoded block (the runs\n * between placeholder tokens), leaving every token exactly in place. This is how the\n * deterministic FakeProvider \"translates\" a block and how the property tests apply a\n * real text change without ever touching structure. Pure.\n */\nexport function mapEncodedSegments(encoded: string, fn: (segment: string) => string): string {\n const re = new RegExp(TOKEN_RE.source, 'g')\n let out = ''\n let cursor = 0\n let match: RegExpExecArray | null\n while ((match = re.exec(encoded)) !== null) {\n out += fn(encoded.slice(cursor, match.index)) + match[0]\n cursor = match.index + match[0].length\n }\n out += fn(encoded.slice(cursor))\n return out\n}\n"]}
|