@elytracms/studio 0.0.103 → 0.0.105

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 (34) hide show
  1. package/convex/ingest.test.ts +208 -13
  2. package/convex/ingest.ts +56 -35
  3. package/dist/components/app-views/module.d.ts +4 -0
  4. package/dist/components/app-views/module.js.map +1 -1
  5. package/dist/components/builder/canvas-theme-notice.js +4 -1
  6. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  7. package/dist/components/cms/document-diff-view.js +43 -7
  8. package/dist/components/cms/document-diff-view.js.map +1 -1
  9. package/dist/components/components/components-index.d.ts +0 -7
  10. package/dist/components/components/components-index.js +9 -2
  11. package/dist/components/components/components-index.js.map +1 -1
  12. package/dist/components/redirects/redirects-schema.d.ts +31 -0
  13. package/dist/components/redirects/redirects-schema.js +12 -6
  14. package/dist/components/redirects/redirects-schema.js.map +1 -1
  15. package/dist/lib/ai/translate-document.d.ts +7 -3
  16. package/dist/lib/ai/translate-document.js +63 -48
  17. package/dist/lib/ai/translate-document.js.map +1 -1
  18. package/dist/lib/ai/writer-form.d.ts +48 -5
  19. package/dist/lib/ai/writer-form.js +118 -64
  20. package/dist/lib/ai/writer-form.js.map +1 -1
  21. package/dist/lib/engine-version.d.ts +1 -1
  22. package/dist/lib/engine-version.js +1 -1
  23. package/dist/lib/engine-version.js.map +1 -1
  24. package/dist/lib/i18n/messages.de.js +25 -5
  25. package/dist/lib/i18n/messages.de.js.map +1 -1
  26. package/dist/lib/i18n/messages.en.d.ts +21 -1
  27. package/dist/lib/i18n/messages.en.js +29 -6
  28. package/dist/lib/i18n/messages.en.js.map +1 -1
  29. package/dist/lib/persona/labels.js +1 -0
  30. package/dist/lib/persona/labels.js.map +1 -1
  31. package/dist/lib/workspace/index.d.ts +9 -1
  32. package/dist/lib/workspace/index.js +13 -2
  33. package/dist/lib/workspace/index.js.map +1 -1
  34. package/package.json +3 -3
@@ -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"]}
@@ -6,5 +6,36 @@ import type { CollectionDef } from '@elytracms/core/cms-core';
6
6
  * {@link CollectionViewSource}. The rail's semantics (content-gated, no
7
7
  * publish/versions/locales, create + remove) come from the source's
8
8
  * capabilities, never the schema `kind`. The title identity is the `from` path.
9
+ *
10
+ * The lens as AUTHORED — exported so its test runs it through the engine's unknown-key gate
11
+ * (`unknownCollectionKeyFindings`, EC-625), the one a project's own collections pass. The parse
12
+ * below strips a key the schema does not have: each field once declared `form.help` (the key is
13
+ * `description`), and the redirect form showed no help at all (wave-0106, measured connected).
9
14
  */
15
+ export declare const redirectsCollectionInput: {
16
+ id: string;
17
+ kind: string;
18
+ titleField: string;
19
+ form: {
20
+ label: string | Record<string, string>;
21
+ labelSingular: string | Record<string, string>;
22
+ };
23
+ fields: ({
24
+ name: string;
25
+ type: string;
26
+ filterable: boolean;
27
+ form: {
28
+ label: string | Record<string, string>;
29
+ description: string | Record<string, string>;
30
+ };
31
+ } | {
32
+ name: string;
33
+ type: string;
34
+ form: {
35
+ label: string | Record<string, string>;
36
+ description: string | Record<string, string>;
37
+ };
38
+ filterable?: undefined;
39
+ })[];
40
+ };
10
41
  export declare const redirectsCollectionDef: CollectionDef;
@@ -7,8 +7,13 @@ import { msgLabel } from '../../lib/i18n/index.js';
7
7
  * {@link CollectionViewSource}. The rail's semantics (content-gated, no
8
8
  * publish/versions/locales, create + remove) come from the source's
9
9
  * capabilities, never the schema `kind`. The title identity is the `from` path.
10
+ *
11
+ * The lens as AUTHORED — exported so its test runs it through the engine's unknown-key gate
12
+ * (`unknownCollectionKeyFindings`, EC-625), the one a project's own collections pass. The parse
13
+ * below strips a key the schema does not have: each field once declared `form.help` (the key is
14
+ * `description`), and the redirect form showed no help at all (wave-0106, measured connected).
10
15
  */
11
- export const redirectsCollectionDef = collectionDefSchema.parse({
16
+ export const redirectsCollectionInput = {
12
17
  id: 'redirect',
13
18
  // Redirects aren't a CMS `kind`; 'document' just satisfies the lens schema.
14
19
  kind: 'document',
@@ -23,25 +28,26 @@ export const redirectsCollectionDef = collectionDefSchema.parse({
23
28
  name: 'from',
24
29
  type: 'text',
25
30
  filterable: true,
26
- form: { label: msgLabel('redirects.field.from.label'), help: 'The source path to forward, e.g. /old-page.' },
31
+ form: { label: msgLabel('redirects.field.from.label'), description: msgLabel('redirects.field.from.description') },
27
32
  },
28
33
  {
29
34
  name: 'to',
30
35
  type: 'text',
31
36
  filterable: true,
32
- form: { label: msgLabel('redirects.field.to.label'), help: 'The destination path or URL, e.g. /new-page.' },
37
+ form: { label: msgLabel('redirects.field.to.label'), description: msgLabel('redirects.field.to.description') },
33
38
  },
34
39
  {
35
40
  name: 'permanent',
36
41
  type: 'boolean',
37
- form: { label: msgLabel('redirects.field.permanent.label'), help: 'On → 301 permanent; off → 302 temporary.' },
42
+ form: { label: msgLabel('redirects.field.permanent.label'), description: msgLabel('redirects.field.permanent.description') },
38
43
  },
39
44
  {
40
45
  name: 'enabled',
41
46
  type: 'boolean',
42
47
  filterable: true,
43
- form: { label: msgLabel('redirects.field.enabled.label'), help: 'Only enabled redirects are served on the live site.' },
48
+ form: { label: msgLabel('redirects.field.enabled.label'), description: msgLabel('redirects.field.enabled.description') },
44
49
  },
45
50
  ],
46
- });
51
+ };
52
+ export const redirectsCollectionDef = collectionDefSchema.parse(redirectsCollectionInput);
47
53
  //# sourceMappingURL=redirects-schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirects-schema.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAkB,mBAAmB,CAAC,KAAK,CAAC;IAC7E,EAAE,EAAE,UAAU;IACd,4EAA4E;IAC5E,IAAI,EAAE,UAAU;IAChB,UAAU,EAAE,MAAM;IAClB,qFAAqF;IACrF,mFAAmF;IACnF,yEAAyE;IACzE,2DAA2D;IAC3D,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,8BAA8B,CAAC,EAAE;IAC1G,MAAM,EAAE;QACN;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,MAAM;YACZ,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,4BAA4B,CAAC,EAAE,IAAI,EAAE,6CAA6C,EAAE;SAC7G;QACD;YACE,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,MAAM;YACZ,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE,IAAI,EAAE,8CAA8C,EAAE;SAC5G;QACD;YACE,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,iCAAiC,CAAC,EAAE,IAAI,EAAE,0CAA0C,EAAE;SAC/G;QACD;YACE,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,+BAA+B,CAAC,EAAE,IAAI,EAAE,qDAAqD,EAAE;SACxH;KACF;CACF,CAAC,CAAA","sourcesContent":["import { collectionDefSchema } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { msgLabel } from '../../lib/i18n'\n\n/**\n * The built-in **redirects** field lens (EC-303). Redirects are a separate\n * editorial rail — NOT a `cmsDocuments` collection — so this `CollectionDef`\n * exists only to drive the portable list/edit surfaces over the redirects\n * {@link CollectionViewSource}. The rail's semantics (content-gated, no\n * publish/versions/locales, create + remove) come from the source's\n * capabilities, never the schema `kind`. The title identity is the `from` path.\n */\nexport const redirectsCollectionDef: CollectionDef = collectionDefSchema.parse({\n id: 'redirect',\n // Redirects aren't a CMS `kind`; 'document' just satisfies the lens schema.\n kind: 'document',\n titleField: 'from',\n // EC-241: the built-in rail's labels resolve through the message catalog (msgLabel),\n // so a consumer's translation catalog localizes them — no per-rail EC-311 override\n // needed just to translate. EC-311 field overrides still win for genuine\n // customization (renaming, readOnly, control, validation).\n form: { label: msgLabel('redirects.rail.label'), labelSingular: msgLabel('redirects.rail.labelSingular') },\n fields: [\n {\n name: 'from',\n type: 'text',\n filterable: true,\n form: { label: msgLabel('redirects.field.from.label'), help: 'The source path to forward, e.g. /old-page.' },\n },\n {\n name: 'to',\n type: 'text',\n filterable: true,\n form: { label: msgLabel('redirects.field.to.label'), help: 'The destination path or URL, e.g. /new-page.' },\n },\n {\n name: 'permanent',\n type: 'boolean',\n form: { label: msgLabel('redirects.field.permanent.label'), help: 'On → 301 permanent; off → 302 temporary.' },\n },\n {\n name: 'enabled',\n type: 'boolean',\n filterable: true,\n form: { label: msgLabel('redirects.field.enabled.label'), help: 'Only enabled redirects are served on the live site.' },\n },\n ],\n})\n"]}
1
+ {"version":3,"file":"redirects-schema.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,EAAE,EAAE,UAAU;IACd,4EAA4E;IAC5E,IAAI,EAAE,UAAU;IAChB,UAAU,EAAE,MAAM;IAClB,qFAAqF;IACrF,mFAAmF;IACnF,yEAAyE;IACzE,2DAA2D;IAC3D,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,8BAA8B,CAAC,EAAE;IAC1G,MAAM,EAAE;QACN;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,MAAM;YACZ,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,4BAA4B,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,kCAAkC,CAAC,EAAE;SACnH;QACD;YACE,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,MAAM;YACZ,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,gCAAgC,CAAC,EAAE;SAC/G;QACD;YACE,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,iCAAiC,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,uCAAuC,CAAC,EAAE;SAC7H;QACD;YACE,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,IAAI;YAChB,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,+BAA+B,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,qCAAqC,CAAC,EAAE;SACzH;KACF;CACF,CAAA;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAkB,mBAAmB,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAA","sourcesContent":["import { collectionDefSchema } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { msgLabel } from '../../lib/i18n'\n\n/**\n * The built-in **redirects** field lens (EC-303). Redirects are a separate\n * editorial rail — NOT a `cmsDocuments` collection — so this `CollectionDef`\n * exists only to drive the portable list/edit surfaces over the redirects\n * {@link CollectionViewSource}. The rail's semantics (content-gated, no\n * publish/versions/locales, create + remove) come from the source's\n * capabilities, never the schema `kind`. The title identity is the `from` path.\n *\n * The lens as AUTHORED — exported so its test runs it through the engine's unknown-key gate\n * (`unknownCollectionKeyFindings`, EC-625), the one a project's own collections pass. The parse\n * below strips a key the schema does not have: each field once declared `form.help` (the key is\n * `description`), and the redirect form showed no help at all (wave-0106, measured connected).\n */\nexport const redirectsCollectionInput = {\n id: 'redirect',\n // Redirects aren't a CMS `kind`; 'document' just satisfies the lens schema.\n kind: 'document',\n titleField: 'from',\n // EC-241: the built-in rail's labels resolve through the message catalog (msgLabel),\n // so a consumer's translation catalog localizes them — no per-rail EC-311 override\n // needed just to translate. EC-311 field overrides still win for genuine\n // customization (renaming, readOnly, control, validation).\n form: { label: msgLabel('redirects.rail.label'), labelSingular: msgLabel('redirects.rail.labelSingular') },\n fields: [\n {\n name: 'from',\n type: 'text',\n filterable: true,\n form: { label: msgLabel('redirects.field.from.label'), description: msgLabel('redirects.field.from.description') },\n },\n {\n name: 'to',\n type: 'text',\n filterable: true,\n form: { label: msgLabel('redirects.field.to.label'), description: msgLabel('redirects.field.to.description') },\n },\n {\n name: 'permanent',\n type: 'boolean',\n form: { label: msgLabel('redirects.field.permanent.label'), description: msgLabel('redirects.field.permanent.description') },\n },\n {\n name: 'enabled',\n type: 'boolean',\n filterable: true,\n form: { label: msgLabel('redirects.field.enabled.label'), description: msgLabel('redirects.field.enabled.description') },\n },\n ],\n}\n\nexport const redirectsCollectionDef: CollectionDef = collectionDefSchema.parse(redirectsCollectionInput)\n"]}
@@ -63,7 +63,10 @@ export type TranslateSkipReason =
63
63
  /**
64
64
  * The stored value at the position is not readable as its type (e.g. a pre-convergence rich
65
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.
66
+ * holder stored in a shape its declaration no longer reads) that may hold text — or (EC-774)
67
+ * it sits in a component whose node id its sequence holds more than once: the position has
68
+ * no identity of its own (a write addresses a prop by its node id within one sequence, so it
69
+ * would land in the twin too), and both twins are reported.
67
70
  */
68
71
  'unreadable'
69
72
  /** The provider returned no (or empty) text for the unit. */
@@ -108,8 +111,9 @@ export interface TranslateBucket {
108
111
  * What a planned bucket is (EC-752): `fill` — the target FOLLOWS the base (EC-318 connected)
109
112
  * and gets its own translation; `replace` — the target holds its own value (an earlier
110
113
  * 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) the base's, its locked
112
- * positions kept (EC-738). `replace` exists only in `retranslate-all`.
114
+ * translated, every other value (a number, an image, a select, a text the base leaves empty —
115
+ * EC-759) the base's, its locked positions kept (EC-738). `replace` exists only in
116
+ * `retranslate-all`.
113
117
  */
114
118
  export type TranslateBucketKind = 'fill' | 'replace';
115
119
  export interface TranslatePlanBucket extends TranslateBucket {
@@ -21,9 +21,12 @@
21
21
  * - `fill-empty`: a field/locale is translated only when it is CONNECTED (no per-locale
22
22
  * key yet) and the base value is non-empty — so writing the translation DIVERGES it,
23
23
  * which is exactly what makes a re-run an idempotent no-op. `retranslate-all`: also when
24
- * it holds its own value — and then a field/locale is written only WHOLE, when every unit
25
- * of it landed (a dropped unit, a rejected block, a cancel or an unreadable position
26
- * leaves the target's value as it was, reported); its locked content is kept (EC-738);
24
+ * it holds its own value — and then a field/locale is written only WHOLE, when every
25
+ * PLANNED unit of it landed (a dropped unit, a rejected block, a cancel or an unreadable
26
+ * position leaves the target's value as it was, reported); its locked content is kept
27
+ * (EC-738). A text position the base leaves empty is no unit and no part of that judgement
28
+ * (EC-759): the rebuilt value takes the base's state there, as it does for a number — and
29
+ * so does a text the engine reads as a URL (EC-766: `isUrlLike` — a URL is not prose);
27
30
  * - EVERY text position of the base value is a unit (EC-593, ratified 4e): the walk is
28
31
  * core's ONE value-position traversal, so a `text` position is one unit and a
29
32
  * `richText` / `blocks` sequence one unit per translatable block (structure preserved
@@ -50,7 +53,7 @@
50
53
  * translates with cross-block context. Bounded concurrency (default 6) keeps a realistic
51
54
  * document well under the route's per-minute rate limit.
52
55
  */
53
- import { autoDeriveSlug, childPositions, createCollectionRegistry, declarationMayHold, fieldValuePosition, isFieldLocalized, isOpaquePosition, isWriterLocked, resolveField, slugFieldOf, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
56
+ import { autoDeriveSlug, childPositions, createCollectionRegistry, declarationMayHold, fieldValuePosition, isFieldLocalized, isOpaquePosition, isUrlLike, isWriterLocked, resolveField, slugFieldOf, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
54
57
  import { introducedErrors, warningsOf } from './proposal-validation.js';
55
58
  import { fieldWritePatch } from '../cms/field-patch.js';
56
59
  import { fieldConnection, hasLocaleVariant } from '../locale/index.js';
@@ -64,6 +67,9 @@ export const DEFAULT_TRANSLATE_BATCH_SIZE = 12;
64
67
  export const DEFAULT_TRANSLATE_CONCURRENCY = 6;
65
68
  /** The schema name the batch call carries — how a FakeProvider recognises a translate call. */
66
69
  export const TRANSLATE_SCHEMA_NAME = 'translate_units';
70
+ function plannedAt(locale, field, key) {
71
+ return JSON.stringify([locale, field, key]);
72
+ }
67
73
  // --- Field classification ---------------------------------------------------------
68
74
  /**
69
75
  * True when a field is the collection's SLUG ROLE (EC-590 — `slugField`, default a text
@@ -81,24 +87,6 @@ function slugSourceField(field, collection) {
81
87
  return typeof collection.titleField === 'string' ? collection.titleField : 'title';
82
88
  return null; // a function source — fall back to "any translation happened this run"
83
89
  }
84
- /**
85
- * A position's stable key within its field value: a component prop (or anything below
86
- * one) by its node id — ids survive a translation that moves an inline embed — and
87
- * every other position by its location. `''` for the field itself.
88
- */
89
- function positionKey(position) {
90
- const { at, component } = position;
91
- if (component) {
92
- const below = at.slice(at.lastIndexOf('props') + 1);
93
- return `${component.nodeId}/${below.join('/')}`;
94
- }
95
- return at.length === 0 ? '' : `/${at.join('/')}`;
96
- }
97
- /** `<locale>|<field>` plus the position key when it is below the field. */
98
- function positionId(locale, field, position) {
99
- const key = positionKey(position);
100
- return key === '' ? `${locale}|${field}` : `${locale}|${field}|${key}`;
101
- }
102
90
  /** A declaration whose value is text a writer may translate (a unit, or a sequence of them). */
103
91
  function isTranslatableDeclaration(field) {
104
92
  return (field.type === 'text' || field.type === 'richText' || field.type === 'blocks') && !isWriterLocked(field);
@@ -213,6 +201,8 @@ async function translateBatch(provider, locale, units, knowledge) {
213
201
  schemaName: TRANSLATE_SCHEMA_NAME,
214
202
  schemaDescription: `Translate content units into ${localeName(locale)}.`,
215
203
  });
204
+ // EC-774: a reply maps back through THIS batch's own ids — an id it was not sent (another
205
+ // batch's, a location, an invention) is ignored.
216
206
  const out = new Map();
217
207
  const known = new Set(units.map((u) => u.id));
218
208
  const raw = result.object?.units;
@@ -301,16 +291,26 @@ function planRun(input) {
301
291
  const bucketSkips = [];
302
292
  const unreadable = (site) => bucketSkips.push(skipOf(field.name, locale, site, 'unreadable'));
303
293
  // EC-593: every text position of the base value — the converter module's fold
304
- // over core's ONE value-position traversal.
294
+ // over core's ONE value-position traversal, each with its position key (EC-774).
305
295
  for (const position of textPositionsOf(root, positionCtx)) {
306
- const { value } = position;
296
+ const { value, key } = position;
307
297
  if (value === undefined || value === null)
308
298
  continue;
309
299
  if (position.field.type === 'text') {
310
300
  if (typeof value !== 'string')
311
301
  unreadable(position);
312
- else if (value.trim().length > 0) {
313
- bucketUnits.push({ kind: 'text', id: positionId(locale, field.name, position), locale, field: field.name, text: value });
302
+ // EC-766: a URL is not prose. A value the engine's one URL-shape declaration reads
303
+ // as a URL (`isUrlLike` — the renderer's own predicate) is no unit, as an empty text
304
+ // is none (EC-759): the rebuild carries the base's value, and a bucket holding
305
+ // nothing else is not planned. `mailto:`, `tel:`, `#…` and bare domains are no URLs
306
+ // by that rule and are still sent.
307
+ else if (value.trim().length > 0 && !isUrlLike(value.trim())) {
308
+ // EC-774: text with no key of its own (under a node its sequence holds twice)
309
+ // could only be written into its twin too — refused, reported, never sent.
310
+ if (key === undefined)
311
+ unreadable(position);
312
+ else
313
+ bucketUnits.push({ kind: 'text', at: plannedAt(locale, field.name, key), locale, field: field.name, text: value });
314
314
  }
315
315
  continue;
316
316
  }
@@ -321,18 +321,14 @@ function planRun(input) {
321
321
  }
322
322
  if (extracted.units.length === 0)
323
323
  continue;
324
- const planId = positionId(locale, field.name, position);
325
- bucketPlans.set(planId, extracted.plan);
324
+ if (key === undefined) {
325
+ unreadable(position); // EC-774, as for a text position above
326
+ continue;
327
+ }
328
+ const at = plannedAt(locale, field.name, key);
329
+ bucketPlans.set(at, extracted.plan);
326
330
  for (const block of extracted.units) {
327
- bucketUnits.push({
328
- kind: 'richText',
329
- id: `${planId}|${block.id}`,
330
- locale,
331
- field: field.name,
332
- plan: planId,
333
- blockId: block.id,
334
- text: block.text,
335
- });
331
+ bucketUnits.push({ kind: 'richText', locale, field: field.name, plan: at, blockId: block.id, text: block.text });
336
332
  }
337
333
  }
338
334
  // EC-752 (8b): text inside a value the traversal cannot read is no unit — reported, not
@@ -349,9 +345,11 @@ function planRun(input) {
349
345
  continue;
350
346
  if (bucketUnits.length === 0)
351
347
  continue;
352
- units.push(...bucketUnits);
353
- for (const [id, plan] of bucketPlans)
354
- richPlans.set(id, plan);
348
+ // EC-774: each planned unit's wire id — the run's opaque index, in plan order.
349
+ for (const unit of bucketUnits)
350
+ units.push({ ...unit, id: `u${units.length + 1}` });
351
+ for (const [at, plan] of bucketPlans)
352
+ richPlans.set(at, plan);
355
353
  pending.push({ field, locale, base: conn.baseValue, current, kind: conn.connected ? 'fill' : 'replace' });
356
354
  }
357
355
  }
@@ -437,10 +435,16 @@ export async function translateDocument(input) {
437
435
  perLocale[locale] = (perLocale[locale] ?? 0) + count;
438
436
  translated += count;
439
437
  };
438
+ // The plan's units by the position they translate — the rebuild's ONE reader of what was
439
+ // planned (EC-759): a text unit by its position, a rich-text position's blocks by its plan.
440
+ // EC-774: the position is named by its key, the same one the plan walked by.
441
+ const textUnitAt = new Map();
440
442
  const blocksOf = new Map();
441
443
  for (const unit of units) {
442
- if (unit.kind !== 'richText')
444
+ if (unit.kind === 'text') {
445
+ textUnitAt.set(unit.at, unit);
443
446
  continue;
447
+ }
444
448
  const list = blocksOf.get(unit.plan) ?? new Map();
445
449
  list.set(unit.blockId, unit);
446
450
  blocksOf.set(unit.plan, list);
@@ -453,19 +457,30 @@ export async function translateDocument(input) {
453
457
  ];
454
458
  for (const { field, locale, base: baseValue, current, kind } of pending) {
455
459
  let applied = 0;
456
- // EC-752 rule 2: whether every unit of the bucket landed — a unit never sent (a cancel),
457
- // not returned, or a block whose tokens did not survive makes it incomplete.
460
+ // EC-752 rule 2: whether every PLANNED unit of the bucket landed — a planned unit never
461
+ // sent (a cancel), not returned, or a block whose tokens did not survive makes it
462
+ // incomplete. A position the plan made no unit of is no part of it (EC-759).
458
463
  let complete = true;
459
464
  // EC-593: the base value with each translated text position replaced — the
460
465
  // converter module's writing fold; every other value is carried from the base.
461
466
  const value = rebuildTextPositions(fieldValuePosition(field, baseValue, { locale: base }), positionCtx, (position) => {
462
- const id = positionId(locale, field.name, position);
467
+ const at = plannedAt(locale, field.name, position.key);
463
468
  if (position.field.type === 'text') {
464
- if (!attempted.has(id)) {
469
+ // EC-759 (Codex r10 F48): the PLANNED unit at this position, read as the rich-text
470
+ // branch reads its plan. None — the base leaves the position empty (absent, `null`,
471
+ // `''`, whitespace), or its value is unreadable and reported by the plan — means
472
+ // the base's value is carried, as every non-text value is, and completeness is
473
+ // untouched. Judging "every text position" by the units whose batch ran read a
474
+ // position never planned as a cancel, and the whole-bucket rule then dropped a
475
+ // listed replacement with no skip.
476
+ const unit = textUnitAt.get(at);
477
+ if (!unit)
478
+ return position.value;
479
+ if (!attempted.has(unit.id)) {
465
480
  complete = false;
466
481
  return position.value;
467
482
  }
468
- const text = returned(id);
483
+ const text = returned(unit.id);
469
484
  if (text === undefined) {
470
485
  skip(field.name, locale, position, 'not-returned');
471
486
  complete = false;
@@ -474,11 +489,11 @@ export async function translateDocument(input) {
474
489
  applied += 1;
475
490
  return text;
476
491
  }
477
- const plan = plans.get(id);
492
+ const plan = plans.get(at);
478
493
  if (!plan)
479
494
  return position.value;
480
495
  const blockTranslations = new Map();
481
- for (const [blockId, unit] of blocksOf.get(id) ?? []) {
496
+ for (const [blockId, unit] of blocksOf.get(at) ?? []) {
482
497
  if (!attempted.has(unit.id)) {
483
498
  complete = false;
484
499
  continue;