@elytracms/studio 0.0.102 → 0.0.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/convex/ingest.test.ts +251 -23
  2. package/convex/ingest.ts +77 -19
  3. package/dist/components/ai/translate-document-dialog.d.ts +43 -0
  4. package/dist/components/ai/translate-document-dialog.js +104 -36
  5. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  6. package/dist/components/ai/translate-skipped.d.ts +19 -3
  7. package/dist/components/ai/translate-skipped.js +33 -7
  8. package/dist/components/ai/translate-skipped.js.map +1 -1
  9. package/dist/components/builder/canvas-theme-notice.js +4 -1
  10. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  11. package/dist/components/components/components-index.d.ts +0 -7
  12. package/dist/components/components/components-index.js +9 -2
  13. package/dist/components/components/components-index.js.map +1 -1
  14. package/dist/lib/ai/translate-apply.d.ts +69 -0
  15. package/dist/lib/ai/translate-apply.js +36 -0
  16. package/dist/lib/ai/translate-apply.js.map +1 -0
  17. package/dist/lib/ai/translate-document.d.ts +95 -7
  18. package/dist/lib/ai/translate-document.js +195 -57
  19. package/dist/lib/ai/translate-document.js.map +1 -1
  20. package/dist/lib/ai/writer-form.d.ts +33 -2
  21. package/dist/lib/ai/writer-form.js +188 -27
  22. package/dist/lib/ai/writer-form.js.map +1 -1
  23. package/dist/lib/engine-version.d.ts +1 -1
  24. package/dist/lib/engine-version.js +1 -1
  25. package/dist/lib/engine-version.js.map +1 -1
  26. package/dist/lib/i18n/messages.de.js +56 -10
  27. package/dist/lib/i18n/messages.de.js.map +1 -1
  28. package/dist/lib/i18n/messages.en.d.ts +41 -1
  29. package/dist/lib/i18n/messages.en.js +61 -10
  30. package/dist/lib/i18n/messages.en.js.map +1 -1
  31. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { BookOpen, Layers, MousePointerClick, Sparkles } from 'lucide-react';
3
+ import { elytraGuidePath } from '@elytracms/core/studio-core';
3
4
  import { getStudioRegistry, registryHealth } from '../../lib/registry/index.js';
4
5
  import { translate } from '../../lib/i18n/index.js';
5
6
  import { RegistryBrowser } from './registry-browser.js';
@@ -9,9 +10,15 @@ import { Badge, Card, Code, Heading, Panel, PanelHeader, Row, Spacer, Stack, Tex
9
10
  * The Components section landing view (EC-050, overhauled EC-320). Serves BOTH
10
11
  * audiences: editors get a live census of what components exist (totals,
11
12
  * per-category breakdown, health) and a "pick one" nudge;
12
- * devs get pointers to the real in-repo docs for registering and extending
13
+ * devs get pointers to the guide's chapters on registering and extending
13
14
  * components — no code sample to drift, no invented docs.
14
15
  */
16
+ /** The guide chapters the developer panel points at (EC-765: file names in the shipped guide). */
17
+ const COMPONENTS_INDEX_CHAPTERS = {
18
+ authoring: '12-component-authoring.md',
19
+ components: '03-components.md',
20
+ overview: '00-overview.md',
21
+ };
15
22
  export function ComponentsIndex({ projectId }) {
16
23
  const studio = getStudioRegistry();
17
24
  const manifests = studio.registry.list();
@@ -20,7 +27,7 @@ export function ComponentsIndex({ projectId }) {
20
27
  return (_jsxs("div", { className: "mx-auto w-full max-w-4xl p-4 sm:p-6", children: [_jsx("div", { className: "mb-6 h-80 overflow-hidden rounded-lg border border-border md:hidden", children: _jsx(RegistryBrowser, { projectId: projectId }) }), _jsxs(Stack, { gap: "6", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Heading, { size: "display", children: translate('components.index.title') }), _jsx(Text, { tone: "muted", children: translate('components.index.subtitle') })] }), _jsxs("div", { className: "grid gap-3 sm:grid-cols-3", children: [_jsx(StatCard, { label: translate('components.index.stat.total'), value: stats.total }), _jsx(StatCard, { label: translate('components.index.stat.valid'), value: health.valid, tone: "success" }), _jsx(StatCard, { label: translate('components.index.stat.needAttention'), value: health.incompatible + health.invalid, tone: health.incompatible + health.invalid > 0 ? 'warning' : 'muted', hint: translate('components.index.stat.needAttentionHint', {
21
28
  incompatible: health.incompatible,
22
29
  invalid: health.invalid,
23
- }) })] }), stats.categories.length > 0 ? (_jsxs(Panel, { children: [_jsxs(PanelHeader, { children: [_jsx(Layers, { className: "size-4 text-muted-foreground" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate('components.index.byCategory') })] }), _jsx(Row, { gap: "2", wrap: true, className: "p-4", children: stats.categories.map((category) => (_jsxs(Badge, { variant: "outline", children: [_jsx("span", { className: category.key ? 'capitalize' : undefined, children: category.key || translate('components.taxonomy.uncategorized') }), _jsx("span", { className: "font-mono text-muted-foreground", children: category.count })] }, category.key || '__uncategorized__'))) })] })) : null, _jsxs(Card, { className: "flex items-center gap-3 p-4", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground", children: _jsx(MousePointerClick, { className: "size-4" }) }), _jsx(Text, { size: "sm", tone: "muted", children: translate('components.index.pickHint') })] }), _jsxs(Panel, { children: [_jsxs(PanelHeader, { children: [_jsx(BookOpen, { className: "size-4 text-muted-foreground" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate('components.index.guide.title') }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('components.index.guide.badge') })] }), _jsxs(Stack, { gap: "3", className: "p-4", children: [_jsx(Text, { size: "sm", tone: "muted", children: translate('components.index.guide.intro') }), _jsxs(Stack, { gap: "2", children: [_jsx(DocPointer, { path: "docs/component-authoring.md", description: translate('components.index.guide.doc.authoring') }), _jsx(DocPointer, { path: "docs/guide/03-components.md", description: translate('components.index.guide.doc.components') }), _jsx(DocPointer, { path: "docs/guide/00-overview.md", description: translate('components.index.guide.doc.guide') })] }), _jsxs(Text, { size: "xs", tone: "subtle", children: [_jsx(Sparkles, { className: "mr-1 inline size-3", "aria-hidden": true }), translate('components.index.guide.namespaceNote')] })] })] })] })] }));
30
+ }) })] }), stats.categories.length > 0 ? (_jsxs(Panel, { children: [_jsxs(PanelHeader, { children: [_jsx(Layers, { className: "size-4 text-muted-foreground" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate('components.index.byCategory') })] }), _jsx(Row, { gap: "2", wrap: true, className: "p-4", children: stats.categories.map((category) => (_jsxs(Badge, { variant: "outline", children: [_jsx("span", { className: category.key ? 'capitalize' : undefined, children: category.key || translate('components.taxonomy.uncategorized') }), _jsx("span", { className: "font-mono text-muted-foreground", children: category.count })] }, category.key || '__uncategorized__'))) })] })) : null, _jsxs(Card, { className: "flex items-center gap-3 p-4", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground", children: _jsx(MousePointerClick, { className: "size-4" }) }), _jsx(Text, { size: "sm", tone: "muted", children: translate('components.index.pickHint') })] }), _jsxs(Panel, { children: [_jsxs(PanelHeader, { children: [_jsx(BookOpen, { className: "size-4 text-muted-foreground" }), _jsx(Text, { size: "sm", weight: "semibold", children: translate('components.index.guide.title') }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('components.index.guide.badge') })] }), _jsxs(Stack, { gap: "3", className: "p-4", children: [_jsx(Text, { size: "sm", tone: "muted", children: translate('components.index.guide.intro') }), _jsxs(Stack, { gap: "2", children: [_jsx(DocPointer, { path: elytraGuidePath(COMPONENTS_INDEX_CHAPTERS.authoring), description: translate('components.index.guide.doc.authoring') }), _jsx(DocPointer, { path: elytraGuidePath(COMPONENTS_INDEX_CHAPTERS.components), description: translate('components.index.guide.doc.components') }), _jsx(DocPointer, { path: elytraGuidePath(COMPONENTS_INDEX_CHAPTERS.overview), description: translate('components.index.guide.doc.guide') })] }), _jsxs(Text, { size: "xs", tone: "subtle", children: [_jsx(Sparkles, { className: "mr-1 inline size-3", "aria-hidden": true }), translate('components.index.guide.namespaceNote')] })] })] })] })] }));
24
31
  }
25
32
  function DocPointer({ path, description }) {
26
33
  return (_jsxs(Row, { gap: "3", align: "center", className: "rounded-md border border-border p-2.5", children: [_jsx(Code, { tone: "primary", className: "shrink-0", children: path }), _jsx(Text, { size: "xs", tone: "muted", children: description })] }));
@@ -1 +1 @@
1
- {"version":3,"file":"components-index.js","sourceRoot":"","sources":["../../../src/components/components/components-index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { BookOpen, Layers, MousePointerClick, Sparkles } from 'lucide-react'\nimport { getStudioRegistry, registryHealth } from '../../lib/registry'\nimport { translate } from '../../lib/i18n'\nimport { RegistryBrowser } from './registry-browser'\nimport { deriveComponentStats } from './taxonomy'\nimport { Badge, Card, Code, Heading, Panel, PanelHeader, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\n\n/**\n * The Components section landing view (EC-050, overhauled EC-320). Serves BOTH\n * audiences: editors get a live census of what components exist (totals,\n * per-category breakdown, health) and a \"pick one\" nudge;\n * devs get pointers to the real in-repo docs for registering and extending\n * components — no code sample to drift, no invented docs.\n */\nexport function ComponentsIndex({ projectId }: { projectId: string }) {\n const studio = getStudioRegistry()\n const manifests = studio.registry.list()\n const stats = deriveComponentStats(manifests)\n const health = registryHealth(studio)\n\n return (\n <div className=\"mx-auto w-full max-w-4xl p-4 sm:p-6\">\n {/* Mobile-only browser (the sidebar is hidden under md). */}\n <div className=\"mb-6 h-80 overflow-hidden rounded-lg border border-border md:hidden\">\n <RegistryBrowser projectId={projectId} />\n </div>\n\n <Stack gap=\"6\">\n <Stack gap=\"1\">\n <Heading size=\"display\">{translate('components.index.title')}</Heading>\n <Text tone=\"muted\">{translate('components.index.subtitle')}</Text>\n </Stack>\n\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <StatCard label={translate('components.index.stat.total')} value={stats.total} />\n <StatCard\n label={translate('components.index.stat.valid')}\n value={health.valid}\n tone=\"success\"\n />\n <StatCard\n label={translate('components.index.stat.needAttention')}\n value={health.incompatible + health.invalid}\n tone={health.incompatible + health.invalid > 0 ? 'warning' : 'muted'}\n hint={translate('components.index.stat.needAttentionHint', {\n incompatible: health.incompatible,\n invalid: health.invalid,\n })}\n />\n </div>\n\n {/* By category — a cheap census derived from manifest `category`. */}\n {stats.categories.length > 0 ? (\n <Panel>\n <PanelHeader>\n <Layers className=\"size-4 text-muted-foreground\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate('components.index.byCategory')}\n </Text>\n </PanelHeader>\n <Row gap=\"2\" wrap className=\"p-4\">\n {stats.categories.map((category) => (\n <Badge key={category.key || '__uncategorized__'} variant=\"outline\">\n <span className={category.key ? 'capitalize' : undefined}>\n {category.key || translate('components.taxonomy.uncategorized')}\n </span>\n <span className=\"font-mono text-muted-foreground\">{category.count}</span>\n </Badge>\n ))}\n </Row>\n </Panel>\n ) : null}\n\n <Card className=\"flex items-center gap-3 p-4\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n <MousePointerClick className=\"size-4\" />\n </span>\n <Text size=\"sm\" tone=\"muted\">\n {translate('components.index.pickHint')}\n </Text>\n </Card>\n\n {/* Dev guidance — pointers to the REAL docs (no code sample to drift). They live in the\n engine repository's docs/, never in the consumer's repo (EC-723: the rail pointed at a\n HISTORICAL doc and a missing one). */}\n <Panel>\n <PanelHeader>\n <BookOpen className=\"size-4 text-muted-foreground\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate('components.index.guide.title')}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('components.index.guide.badge')}</Badge>\n </PanelHeader>\n <Stack gap=\"3\" className=\"p-4\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('components.index.guide.intro')}\n </Text>\n <Stack gap=\"2\">\n <DocPointer\n path=\"docs/component-authoring.md\"\n description={translate('components.index.guide.doc.authoring')}\n />\n <DocPointer\n path=\"docs/guide/03-components.md\"\n description={translate('components.index.guide.doc.components')}\n />\n <DocPointer\n path=\"docs/guide/00-overview.md\"\n description={translate('components.index.guide.doc.guide')}\n />\n </Stack>\n <Text size=\"xs\" tone=\"subtle\">\n <Sparkles className=\"mr-1 inline size-3\" aria-hidden />\n {translate('components.index.guide.namespaceNote')}\n </Text>\n </Stack>\n </Panel>\n </Stack>\n </div>\n )\n}\n\nfunction DocPointer({ path, description }: { path: string; description: string }) {\n return (\n <Row gap=\"3\" align=\"center\" className=\"rounded-md border border-border p-2.5\">\n <Code tone=\"primary\" className=\"shrink-0\">\n {path}\n </Code>\n <Text size=\"xs\" tone=\"muted\">\n {description}\n </Text>\n </Row>\n )\n}\n\nfunction StatCard({\n label,\n value,\n hint,\n tone = 'default',\n}: {\n label: string\n value: number\n hint?: string\n tone?: 'default' | 'success' | 'warning' | 'muted'\n}) {\n const valueClass =\n tone === 'success'\n ? 'text-success'\n : tone === 'warning'\n ? 'text-warning'\n : tone === 'muted'\n ? 'text-muted-foreground'\n : 'text-foreground'\n return (\n <Card className=\"p-4\">\n <Text size=\"xs\" tone=\"muted\" mono>\n {label.toUpperCase()}\n </Text>\n <div className={`mt-1 text-2xl font-semibold tracking-tight ${valueClass}`}>{value}</div>\n {hint ? (\n <Text size=\"xs\" tone=\"subtle\" className=\"mt-0.5\">\n {hint}\n </Text>\n ) : null}\n </Card>\n )\n}\n"]}
1
+ {"version":3,"file":"components-index.js","sourceRoot":"","sources":["../../../src/components/components/components-index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { BookOpen, Layers, MousePointerClick, Sparkles } from 'lucide-react'\nimport { elytraGuidePath } from '@elytracms/core/studio-core'\nimport { getStudioRegistry, registryHealth } from '../../lib/registry'\nimport { translate } from '../../lib/i18n'\nimport { RegistryBrowser } from './registry-browser'\nimport { deriveComponentStats } from './taxonomy'\nimport { Badge, Card, Code, Heading, Panel, PanelHeader, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\n\n/**\n * The Components section landing view (EC-050, overhauled EC-320). Serves BOTH\n * audiences: editors get a live census of what components exist (totals,\n * per-category breakdown, health) and a \"pick one\" nudge;\n * devs get pointers to the guide's chapters on registering and extending\n * components — no code sample to drift, no invented docs.\n */\n/** The guide chapters the developer panel points at (EC-765: file names in the shipped guide). */\nconst COMPONENTS_INDEX_CHAPTERS = {\n authoring: '12-component-authoring.md',\n components: '03-components.md',\n overview: '00-overview.md',\n} as const\n\nexport function ComponentsIndex({ projectId }: { projectId: string }) {\n const studio = getStudioRegistry()\n const manifests = studio.registry.list()\n const stats = deriveComponentStats(manifests)\n const health = registryHealth(studio)\n\n return (\n <div className=\"mx-auto w-full max-w-4xl p-4 sm:p-6\">\n {/* Mobile-only browser (the sidebar is hidden under md). */}\n <div className=\"mb-6 h-80 overflow-hidden rounded-lg border border-border md:hidden\">\n <RegistryBrowser projectId={projectId} />\n </div>\n\n <Stack gap=\"6\">\n <Stack gap=\"1\">\n <Heading size=\"display\">{translate('components.index.title')}</Heading>\n <Text tone=\"muted\">{translate('components.index.subtitle')}</Text>\n </Stack>\n\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <StatCard label={translate('components.index.stat.total')} value={stats.total} />\n <StatCard\n label={translate('components.index.stat.valid')}\n value={health.valid}\n tone=\"success\"\n />\n <StatCard\n label={translate('components.index.stat.needAttention')}\n value={health.incompatible + health.invalid}\n tone={health.incompatible + health.invalid > 0 ? 'warning' : 'muted'}\n hint={translate('components.index.stat.needAttentionHint', {\n incompatible: health.incompatible,\n invalid: health.invalid,\n })}\n />\n </div>\n\n {/* By category — a cheap census derived from manifest `category`. */}\n {stats.categories.length > 0 ? (\n <Panel>\n <PanelHeader>\n <Layers className=\"size-4 text-muted-foreground\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate('components.index.byCategory')}\n </Text>\n </PanelHeader>\n <Row gap=\"2\" wrap className=\"p-4\">\n {stats.categories.map((category) => (\n <Badge key={category.key || '__uncategorized__'} variant=\"outline\">\n <span className={category.key ? 'capitalize' : undefined}>\n {category.key || translate('components.taxonomy.uncategorized')}\n </span>\n <span className=\"font-mono text-muted-foreground\">{category.count}</span>\n </Badge>\n ))}\n </Row>\n </Panel>\n ) : null}\n\n <Card className=\"flex items-center gap-3 p-4\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n <MousePointerClick className=\"size-4\" />\n </span>\n <Text size=\"sm\" tone=\"muted\">\n {translate('components.index.pickHint')}\n </Text>\n </Card>\n\n {/* Dev guidance — pointers to the REAL docs (no code sample to drift; EC-723: the rail\n pointed at a HISTORICAL doc and a missing one). EC-765: the guide ships inside\n @elytracms/core, so each pointer is the chapter's path in the project's node_modules. */}\n <Panel>\n <PanelHeader>\n <BookOpen className=\"size-4 text-muted-foreground\" />\n <Text size=\"sm\" weight=\"semibold\">\n {translate('components.index.guide.title')}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('components.index.guide.badge')}</Badge>\n </PanelHeader>\n <Stack gap=\"3\" className=\"p-4\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('components.index.guide.intro')}\n </Text>\n <Stack gap=\"2\">\n <DocPointer\n path={elytraGuidePath(COMPONENTS_INDEX_CHAPTERS.authoring)}\n description={translate('components.index.guide.doc.authoring')}\n />\n <DocPointer\n path={elytraGuidePath(COMPONENTS_INDEX_CHAPTERS.components)}\n description={translate('components.index.guide.doc.components')}\n />\n <DocPointer\n path={elytraGuidePath(COMPONENTS_INDEX_CHAPTERS.overview)}\n description={translate('components.index.guide.doc.guide')}\n />\n </Stack>\n <Text size=\"xs\" tone=\"subtle\">\n <Sparkles className=\"mr-1 inline size-3\" aria-hidden />\n {translate('components.index.guide.namespaceNote')}\n </Text>\n </Stack>\n </Panel>\n </Stack>\n </div>\n )\n}\n\nfunction DocPointer({ path, description }: { path: string; description: string }) {\n return (\n <Row gap=\"3\" align=\"center\" className=\"rounded-md border border-border p-2.5\">\n <Code tone=\"primary\" className=\"shrink-0\">\n {path}\n </Code>\n <Text size=\"xs\" tone=\"muted\">\n {description}\n </Text>\n </Row>\n )\n}\n\nfunction StatCard({\n label,\n value,\n hint,\n tone = 'default',\n}: {\n label: string\n value: number\n hint?: string\n tone?: 'default' | 'success' | 'warning' | 'muted'\n}) {\n const valueClass =\n tone === 'success'\n ? 'text-success'\n : tone === 'warning'\n ? 'text-warning'\n : tone === 'muted'\n ? 'text-muted-foreground'\n : 'text-foreground'\n return (\n <Card className=\"p-4\">\n <Text size=\"xs\" tone=\"muted\" mono>\n {label.toUpperCase()}\n </Text>\n <div className={`mt-1 text-2xl font-semibold tracking-tight ${valueClass}`}>{value}</div>\n {hint ? (\n <Text size=\"xs\" tone=\"subtle\" className=\"mt-0.5\">\n {hint}\n </Text>\n ) : null}\n </Card>\n )\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import type { DocumentPatch, ValuePath } from '@elytracms/core/cms-core';
2
+ import type { TranslateDocumentResult, TranslateWrite } from './translate-document.js';
3
+ /**
4
+ * EC-752 — how a translation run's writes land, in order: the pure core of the "Translate
5
+ * document" dialog's apply (the dialog wires it to the session's commit queue and the
6
+ * `documents.snapshot` operation), so the order is pinned by a test, not by a component.
7
+ *
8
+ * Re-translate everything REPLACES values someone made, and the draft records no history of
9
+ * its own (versions are recorded on publish, snapshot and restore only), so an unpublished
10
+ * manual translation it replaced was unrecoverable — the dialog used to snapshot only AFTER
11
+ * its commit, a restore point holding the post-write state. Now:
12
+ *
13
+ * 1. `retranslate-all` with anything to write: a labelled snapshot of the stored draft
14
+ * FIRST — the restore point Versions / Compare's scoped restore brings a field back
15
+ * from. If it cannot be recorded, nothing is written (`snapshot-failed`): a
16
+ * re-translation never replaces what it could not save.
17
+ * 2. The patches, through the caller's commit (the session's queue, at the revision the
18
+ * draft was authored on — a position a colleague changed meanwhile is refused there).
19
+ * 3. The AI-provenance snapshot (best-effort, as before — the draft already landed).
20
+ *
21
+ * `fill-empty` is unchanged: it fills only values that followed the base, so it replaces
22
+ * nothing and takes no restore point.
23
+ */
24
+ /** What the caller's commit reports — structurally the session's `CommitOutcome`. */
25
+ export type TranslateCommitOutcome = {
26
+ readonly status: 'applied';
27
+ } | {
28
+ readonly status: 'refused';
29
+ readonly conflicts: readonly {
30
+ readonly path: ValuePath;
31
+ }[];
32
+ } | {
33
+ readonly status: 'failed';
34
+ };
35
+ export interface TranslateApplyDeps {
36
+ /** Persist the run's patches; `undefined` when the session applied them locally (a fixture session). */
37
+ commit(patches: readonly DocumentPatch[]): Promise<TranslateCommitOutcome | undefined>;
38
+ /** Record the current stored draft as a version (`documents.snapshot`). */
39
+ snapshot(options: {
40
+ readonly label: string;
41
+ readonly origin?: string;
42
+ }): Promise<{
43
+ readonly version: number;
44
+ }>;
45
+ }
46
+ export interface TranslateApplyLabels {
47
+ /** The restore point's label (taken before a re-translation writes). */
48
+ readonly restorePoint: string;
49
+ /** The provenance snapshot's label (taken after the write). */
50
+ readonly provenance: string;
51
+ }
52
+ export type TranslateApplyOutcome = {
53
+ readonly status: 'applied';
54
+ /** The writes that landed — the run's writes minus the ones refused at their revision. */
55
+ readonly writes: readonly TranslateWrite[];
56
+ /** Writes refused because a colleague changed the position meanwhile (shown as conflicts there). */
57
+ readonly refused: readonly TranslateWrite[];
58
+ /** The version recorded before a re-translation wrote — what Versions restores from. */
59
+ readonly restorePoint?: number;
60
+ }
61
+ /** `retranslate-all`: the restore point could not be recorded, so nothing was written. */
62
+ | {
63
+ readonly status: 'snapshot-failed';
64
+ }
65
+ /** The commit failed (transport, authorization): reported by the session too. */
66
+ | {
67
+ readonly status: 'failed';
68
+ };
69
+ export declare function applyTranslation(result: Pick<TranslateDocumentResult, 'plan' | 'writes' | 'patches'>, deps: TranslateApplyDeps, labels: TranslateApplyLabels): Promise<TranslateApplyOutcome>;
@@ -0,0 +1,36 @@
1
+ const pathKey = (path) => JSON.stringify(path ?? null);
2
+ /** The position a field write's patch addresses (`set` / `unset` — the only kinds a translation emits). */
3
+ function patchPath(patch) {
4
+ return 'path' in patch ? patch.path : undefined;
5
+ }
6
+ export async function applyTranslation(result, deps, labels) {
7
+ if (result.writes.length === 0)
8
+ return { status: 'applied', writes: [], refused: [] };
9
+ let restorePoint;
10
+ if (result.plan.mode === 'retranslate-all') {
11
+ try {
12
+ restorePoint = (await deps.snapshot({ label: labels.restorePoint })).version;
13
+ }
14
+ catch {
15
+ return { status: 'snapshot-failed' };
16
+ }
17
+ }
18
+ const outcome = await deps.commit(result.patches);
19
+ if (outcome?.status === 'failed')
20
+ return { status: 'failed' };
21
+ const refusedAt = new Set(outcome?.status === 'refused' ? outcome.conflicts.map((conflict) => pathKey(conflict.path)) : []);
22
+ const refused = result.writes.filter((write) => refusedAt.has(pathKey(patchPath(write.patch))));
23
+ try {
24
+ await deps.snapshot({ label: labels.provenance, origin: 'assistant' });
25
+ }
26
+ catch {
27
+ // ignore — provenance is additive
28
+ }
29
+ return {
30
+ status: 'applied',
31
+ writes: result.writes.filter((write) => !refused.includes(write)),
32
+ refused,
33
+ ...(restorePoint !== undefined ? { restorePoint } : {}),
34
+ };
35
+ }
36
+ //# sourceMappingURL=translate-apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate-apply.js","sourceRoot":"","sources":["../../../src/lib/ai/translate-apply.ts"],"names":[],"mappings":"AA4DA,MAAM,OAAO,GAAG,CAAC,IAA2B,EAAU,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC,CAAA;AAErF,2GAA2G;AAC3G,SAAS,SAAS,CAAC,KAAoB;IACrC,OAAO,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AACjD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAoE,EACpE,IAAwB,EACxB,MAA4B;IAE5B,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;IACrF,IAAI,YAAgC,CAAA;IACpC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QAC3C,IAAI,CAAC;YACH,YAAY,GAAG,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,OAAO,CAAA;QAC9E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAA;QACtC,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACjD,IAAI,OAAO,EAAE,MAAM,KAAK,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;IAC7D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC3H,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/F,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;IACpC,CAAC;IACD,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACjE,OAAO;QACP,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxD,CAAA;AACH,CAAC","sourcesContent":["import type { DocumentPatch, ValuePath } from '@elytracms/core/cms-core'\nimport type { TranslateDocumentResult, TranslateWrite } from './translate-document'\n\n/**\n * EC-752 — how a translation run's writes land, in order: the pure core of the \"Translate\n * document\" dialog's apply (the dialog wires it to the session's commit queue and the\n * `documents.snapshot` operation), so the order is pinned by a test, not by a component.\n *\n * Re-translate everything REPLACES values someone made, and the draft records no history of\n * its own (versions are recorded on publish, snapshot and restore only), so an unpublished\n * manual translation it replaced was unrecoverable — the dialog used to snapshot only AFTER\n * its commit, a restore point holding the post-write state. Now:\n *\n * 1. `retranslate-all` with anything to write: a labelled snapshot of the stored draft\n * FIRST — the restore point Versions / Compare's scoped restore brings a field back\n * from. If it cannot be recorded, nothing is written (`snapshot-failed`): a\n * re-translation never replaces what it could not save.\n * 2. The patches, through the caller's commit (the session's queue, at the revision the\n * draft was authored on — a position a colleague changed meanwhile is refused there).\n * 3. The AI-provenance snapshot (best-effort, as before — the draft already landed).\n *\n * `fill-empty` is unchanged: it fills only values that followed the base, so it replaces\n * nothing and takes no restore point.\n */\n\n/** What the caller's commit reports — structurally the session's `CommitOutcome`. */\nexport type TranslateCommitOutcome =\n | { readonly status: 'applied' }\n | { readonly status: 'refused'; readonly conflicts: readonly { readonly path: ValuePath }[] }\n | { readonly status: 'failed' }\n\nexport interface TranslateApplyDeps {\n /** Persist the run's patches; `undefined` when the session applied them locally (a fixture session). */\n commit(patches: readonly DocumentPatch[]): Promise<TranslateCommitOutcome | undefined>\n /** Record the current stored draft as a version (`documents.snapshot`). */\n snapshot(options: { readonly label: string; readonly origin?: string }): Promise<{ readonly version: number }>\n}\n\nexport interface TranslateApplyLabels {\n /** The restore point's label (taken before a re-translation writes). */\n readonly restorePoint: string\n /** The provenance snapshot's label (taken after the write). */\n readonly provenance: string\n}\n\nexport type TranslateApplyOutcome =\n | {\n readonly status: 'applied'\n /** The writes that landed — the run's writes minus the ones refused at their revision. */\n readonly writes: readonly TranslateWrite[]\n /** Writes refused because a colleague changed the position meanwhile (shown as conflicts there). */\n readonly refused: readonly TranslateWrite[]\n /** The version recorded before a re-translation wrote — what Versions restores from. */\n readonly restorePoint?: number\n }\n /** `retranslate-all`: the restore point could not be recorded, so nothing was written. */\n | { readonly status: 'snapshot-failed' }\n /** The commit failed (transport, authorization): reported by the session too. */\n | { readonly status: 'failed' }\n\nconst pathKey = (path: ValuePath | undefined): string => JSON.stringify(path ?? null)\n\n/** The position a field write's patch addresses (`set` / `unset` — the only kinds a translation emits). */\nfunction patchPath(patch: DocumentPatch): ValuePath | undefined {\n return 'path' in patch ? patch.path : undefined\n}\n\nexport async function applyTranslation(\n result: Pick<TranslateDocumentResult, 'plan' | 'writes' | 'patches'>,\n deps: TranslateApplyDeps,\n labels: TranslateApplyLabels,\n): Promise<TranslateApplyOutcome> {\n if (result.writes.length === 0) return { status: 'applied', writes: [], refused: [] }\n let restorePoint: number | undefined\n if (result.plan.mode === 'retranslate-all') {\n try {\n restorePoint = (await deps.snapshot({ label: labels.restorePoint })).version\n } catch {\n return { status: 'snapshot-failed' }\n }\n }\n const outcome = await deps.commit(result.patches)\n if (outcome?.status === 'failed') return { status: 'failed' }\n const refusedAt = new Set(outcome?.status === 'refused' ? outcome.conflicts.map((conflict) => pathKey(conflict.path)) : [])\n const refused = result.writes.filter((write) => refusedAt.has(pathKey(patchPath(write.patch))))\n try {\n await deps.snapshot({ label: labels.provenance, origin: 'assistant' })\n } catch {\n // ignore — provenance is additive\n }\n return {\n status: 'applied',\n writes: result.writes.filter((write) => !refused.includes(write)),\n refused,\n ...(restorePoint !== undefined ? { restorePoint } : {}),\n }\n}\n"]}
@@ -6,7 +6,10 @@ export declare const DEFAULT_TRANSLATE_BATCH_SIZE = 12;
6
6
  export declare const DEFAULT_TRANSLATE_CONCURRENCY = 6;
7
7
  /** The schema name the batch call carries — how a FakeProvider recognises a translate call. */
8
8
  export declare const TRANSLATE_SCHEMA_NAME = "translate_units";
9
- /** Fill only still-connected locale values (default), or re-translate every localized field. */
9
+ /**
10
+ * Fill only still-connected locale values (default), or re-translate every localized field
11
+ * — replacing a value with its own translation, rebuilt from the base (EC-752).
12
+ */
10
13
  export type TranslateMode = 'fill-empty' | 'retranslate-all';
11
14
  export interface TranslateDocumentInput {
12
15
  provider: AiProvider;
@@ -18,7 +21,12 @@ export interface TranslateDocumentInput {
18
21
  targetLocales?: readonly Locale[];
19
22
  /** Assembled project knowledge (glossary / voice) sent as context with every batch. */
20
23
  knowledge?: ResolvedKnowledge;
21
- /** `fill-empty` (default) or `retranslate-all` (overwrites diverged values — gated in UI). */
24
+ /**
25
+ * `fill-empty` (default) or `retranslate-all` — which REPLACES a value with its own
26
+ * translation, whole or not at all, never a slug the editor set (EC-752). The studio runs
27
+ * it only behind a confirm listing {@link planTranslation}'s replacements, after a version
28
+ * snapshot of the draft.
29
+ */
22
30
  mode?: TranslateMode;
23
31
  batchSize?: number;
24
32
  concurrency?: number;
@@ -52,17 +60,26 @@ export interface TranslateProgress {
52
60
  }
53
61
  /** Why a text position was not translated. */
54
62
  export type TranslateSkipReason =
55
- /** The stored value at the position is not readable as its type (e.g. a pre-convergence rich text). */
63
+ /**
64
+ * The stored value at the position is not readable as its type (e.g. a pre-convergence rich
65
+ * text) — or (EC-752) it is a value the traversal cannot read at all (`OpaquePosition`: a
66
+ * holder stored in a shape its declaration no longer reads) that may hold text.
67
+ */
56
68
  'unreadable'
57
69
  /** The provider returned no (or empty) text for the unit. */
58
70
  | 'not-returned'
59
- /** The translation did not keep the formatting / link / embed tokens; the original was kept. */
71
+ /**
72
+ * The translation did not keep the formatting / link / embed tokens: `fill-empty` keeps
73
+ * the source's block in the fill; `retranslate-all` leaves the whole target value as it was.
74
+ */
60
75
  | 'structure-changed'
61
76
  /**
62
77
  * EC-738: the bucket's locked content (`form.readOnly` / `ai.exclude`) differs in shape
63
78
  * from the source's — writing the translation would lose a locked value the target holds or
64
79
  * add one it does not (a repeater with more or fewer items, a composition with other
65
- * nodes). The whole bucket was left as it is: only code, a seed or an import changes it.
80
+ * nodes) — or (EC-748) either side stores it in a shape the declaration no longer reads.
81
+ * The whole bucket was left as it is: only code, a seed, an import or a save in the editor
82
+ * changes it.
66
83
  */
67
84
  | 'locked';
68
85
  /**
@@ -82,9 +99,72 @@ export interface TranslateSkip {
82
99
  };
83
100
  reason: TranslateSkipReason;
84
101
  }
102
+ /** One `(field, locale)` value — the unit a translation writes, plans and reports (EC-752). */
103
+ export interface TranslateBucket {
104
+ readonly field: string;
105
+ readonly locale: Locale;
106
+ }
107
+ /**
108
+ * What a planned bucket is (EC-752): `fill` — the target FOLLOWS the base (EC-318 connected)
109
+ * and gets its own translation; `replace` — the target holds its own value (an earlier
110
+ * translation, an editor's edit), which a re-translation rebuilds from the base — its text
111
+ * translated, every other value (a number, an image, a select, a text the base leaves empty —
112
+ * EC-759) the base's, its locked positions kept (EC-738). `replace` exists only in
113
+ * `retranslate-all`.
114
+ */
115
+ export type TranslateBucketKind = 'fill' | 'replace';
116
+ export interface TranslatePlanBucket extends TranslateBucket {
117
+ readonly kind: TranslateBucketKind;
118
+ }
119
+ /**
120
+ * EC-752 — what a run will do, judged BEFORE any provider call: the eligibility, the locked
121
+ * judgement (EC-738), the units and the unreadable positions. ONE judge
122
+ * ({@link planTranslation}) gives both the studio's confirm and the run's input, so the list
123
+ * an editor confirms is the list the run works from.
124
+ *
125
+ * It is an UPPER bound ("up to"): what is known only after the provider answered — a unit
126
+ * not returned, a block whose tokens did not survive, a cancel, the introduced-errors gate
127
+ * refusing the run, a colleague's write refused at its revision — can only take a bucket
128
+ * away. {@link TranslateDocumentResult.writes} says what was written.
129
+ */
130
+ export interface TranslatePlan {
131
+ readonly mode: TranslateMode;
132
+ readonly base: Locale;
133
+ /** Every configured locale but the base (or the caller's `targetLocales`). */
134
+ readonly targets: readonly Locale[];
135
+ /** The buckets the run writes when every unit lands — walk order (field, then locale). */
136
+ readonly buckets: readonly TranslatePlanBucket[];
137
+ /**
138
+ * What the run will NOT translate, known now: a bucket left as it is for its locked
139
+ * content (`locked`), an unreadable position (`unreadable` — in `retranslate-all` its whole
140
+ * bucket is left as it was, so it is not in {@link buckets}).
141
+ */
142
+ readonly skipped: readonly TranslateSkip[];
143
+ /** Translatable units — the run's {@link TranslateDocumentResult.total}. */
144
+ readonly total: number;
145
+ }
146
+ /** What {@link planTranslation} reads — the run's own inputs, minus the provider. */
147
+ export type TranslatePlanInput = Pick<TranslateDocumentInput, 'collection' | 'document' | 'localeConfig' | 'targetLocales' | 'mode' | 'validateOptions'>;
148
+ /**
149
+ * One bucket this run WROTE (EC-752): a planned `fill` / `replace` whose every unit landed
150
+ * (`fill-empty`: at least one), or a `slug` derived from the translated source (only where
151
+ * the target's slug followed the base). `patch` is the one the caller persists
152
+ * ({@link TranslateDocumentResult.patches}, same order).
153
+ */
154
+ export interface TranslateWrite extends TranslateBucket {
155
+ readonly kind: TranslateBucketKind | 'slug';
156
+ readonly patch: DocumentPatch;
157
+ }
85
158
  export interface TranslateDocumentResult {
86
- /** The new draft with the translations filled in (base + connected locale values). */
159
+ /** The new draft with the translations in. */
87
160
  document: CmsDocument;
161
+ /** EC-752 — the pre-provider plan this run executed ({@link planTranslation} on its input). */
162
+ plan: TranslatePlan;
163
+ /**
164
+ * EC-752 — what this run wrote, one entry per bucket, in {@link patches}' order: compare
165
+ * with {@link plan} for what was planned but not written.
166
+ */
167
+ writes: readonly TranslateWrite[];
88
168
  /**
89
169
  * EC-595 A2 — the positions this run wrote, as patches (one per `(field, locale)`
90
170
  * bucket it filled — "localized fields patch per bucket"): what the caller persists,
@@ -101,7 +181,7 @@ export interface TranslateDocumentResult {
101
181
  changed: boolean;
102
182
  /** Translatable units filled per target locale. */
103
183
  perLocale: Record<Locale, number>;
104
- /** Slug fields derived per target locale (not model-translated). */
184
+ /** Slug fields derived (not model-translated) — the `slug` entries of {@link writes}. */
105
185
  slugsDerived: number;
106
186
  /** Every text position the run could not translate (EC-593) — empty when all landed. */
107
187
  skipped: readonly TranslateSkip[];
@@ -125,4 +205,12 @@ export interface TranslateDocumentResult {
125
205
  /** Surviving non-error findings on the produced draft — carried onto a proposal. */
126
206
  issues: readonly CmsValidationIssue[];
127
207
  }
208
+ /**
209
+ * EC-752 — THE pre-provider judge: which field × locale values a translation of `input`
210
+ * writes when every unit lands (`fill` / `replace`), and what it already knows it will not
211
+ * translate (`locked`, `unreadable`). Pure: no provider, no store, no clock. The studio's
212
+ * confirm lists it; {@link translateDocument} runs exactly it — one judge, never a second
213
+ * count. See {@link TranslatePlan} for why it is an upper bound.
214
+ */
215
+ export declare function planTranslation(input: TranslatePlanInput): TranslatePlan;
128
216
  export declare function translateDocument(input: TranslateDocumentInput): Promise<TranslateDocumentResult>;