@elytracms/studio 0.0.80 → 0.0.81

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 (97) hide show
  1. package/convex/contract-parity.test.ts +1 -1
  2. package/convex/functions.ts +13 -3
  3. package/convex/http-routes.test.ts +143 -0
  4. package/convex/http.ts +19 -9
  5. package/convex/schema.ts +0 -19
  6. package/convex/users.ts +1 -1
  7. package/dist/components/app-views/default-modules.d.ts +141 -44
  8. package/dist/components/app-views/default-modules.js +229 -65
  9. package/dist/components/app-views/default-modules.js.map +1 -1
  10. package/dist/components/app-views/module.d.ts +38 -0
  11. package/dist/components/app-views/module.js.map +1 -1
  12. package/dist/components/app-views/sidebar.d.ts +5 -2
  13. package/dist/components/app-views/sidebar.js +16 -5
  14. package/dist/components/app-views/sidebar.js.map +1 -1
  15. package/dist/components/app-views/studio-config.d.ts +27 -0
  16. package/dist/components/app-views/studio-config.js.map +1 -1
  17. package/dist/components/builder/canvas-render-context.js +7 -1
  18. package/dist/components/builder/canvas-render-context.js.map +1 -1
  19. package/dist/components/builder/reuse-context.js +7 -1
  20. package/dist/components/builder/reuse-context.js.map +1 -1
  21. package/dist/components/builder/reuse.d.ts +94 -16
  22. package/dist/components/builder/reuse.js +201 -19
  23. package/dist/components/builder/reuse.js.map +1 -1
  24. package/dist/components/cms/bulk-actions.js +3 -1
  25. package/dist/components/cms/bulk-actions.js.map +1 -1
  26. package/dist/components/cms/cell-edit.js +6 -2
  27. package/dist/components/cms/cell-edit.js.map +1 -1
  28. package/dist/components/cms/cms-store.d.ts +40 -13
  29. package/dist/components/cms/cms-store.js +50 -22
  30. package/dist/components/cms/cms-store.js.map +1 -1
  31. package/dist/components/cms/collection-table.js +2 -2
  32. package/dist/components/cms/collection-table.js.map +1 -1
  33. package/dist/components/cms/detail-editor.js +11 -5
  34. package/dist/components/cms/detail-editor.js.map +1 -1
  35. package/dist/components/cms/field-control.js +2 -1
  36. package/dist/components/cms/field-control.js.map +1 -1
  37. package/dist/components/cms/field-renderer.js +8 -1
  38. package/dist/components/cms/field-renderer.js.map +1 -1
  39. package/dist/components/cms/import-export.js +2 -1
  40. package/dist/components/cms/import-export.js.map +1 -1
  41. package/dist/components/cms/issues-pane.d.ts +4 -2
  42. package/dist/components/cms/issues-pane.js +6 -4
  43. package/dist/components/cms/issues-pane.js.map +1 -1
  44. package/dist/components/components/transfer-dialog.js +10 -12
  45. package/dist/components/components/transfer-dialog.js.map +1 -1
  46. package/dist/components/project-list.d.ts +9 -3
  47. package/dist/components/project-list.js +11 -5
  48. package/dist/components/project-list.js.map +1 -1
  49. package/dist/components/project-switcher.d.ts +3 -1
  50. package/dist/components/project-switcher.js +4 -2
  51. package/dist/components/project-switcher.js.map +1 -1
  52. package/dist/components/redirects/redirects-store.d.ts +21 -0
  53. package/dist/components/redirects/redirects-store.js +17 -4
  54. package/dist/components/redirects/redirects-store.js.map +1 -1
  55. package/dist/components/section-nav.js +102 -4
  56. package/dist/components/section-nav.js.map +1 -1
  57. package/dist/customization.d.ts +7 -0
  58. package/dist/customization.js.map +1 -1
  59. package/dist/lib/data/collection-content.d.ts +6 -1
  60. package/dist/lib/data/collection-content.js +50 -12
  61. package/dist/lib/data/collection-content.js.map +1 -1
  62. package/dist/lib/i18n/messages.de.js +132 -0
  63. package/dist/lib/i18n/messages.de.js.map +1 -1
  64. package/dist/lib/i18n/messages.en.d.ts +103 -0
  65. package/dist/lib/i18n/messages.en.js +146 -0
  66. package/dist/lib/i18n/messages.en.js.map +1 -1
  67. package/dist/lib/locale/binding-fallback.d.ts +8 -1
  68. package/dist/lib/locale/binding-fallback.js +8 -1
  69. package/dist/lib/locale/binding-fallback.js.map +1 -1
  70. package/dist/lib/locale/locale-store.d.ts +41 -9
  71. package/dist/lib/locale/locale-store.js +34 -15
  72. package/dist/lib/locale/locale-store.js.map +1 -1
  73. package/dist/lib/locale/locale-switcher.d.ts +3 -1
  74. package/dist/lib/locale/locale-switcher.js +4 -2
  75. package/dist/lib/locale/locale-switcher.js.map +1 -1
  76. package/dist/lib/locale/use-studio-locale.d.ts +9 -4
  77. package/dist/lib/locale/use-studio-locale.js.map +1 -1
  78. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  79. package/dist/lib/persistence/convex-adapter.js +0 -55
  80. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  81. package/dist/lib/persona/labels.d.ts +42 -1
  82. package/dist/lib/persona/labels.js +57 -0
  83. package/dist/lib/persona/labels.js.map +1 -1
  84. package/dist/lib/routes/route-issues.d.ts +9 -0
  85. package/dist/lib/routes/route-issues.js +117 -0
  86. package/dist/lib/routes/route-issues.js.map +1 -0
  87. package/dist/lib/workspace/config-issues.d.ts +54 -0
  88. package/dist/lib/workspace/config-issues.js +70 -0
  89. package/dist/lib/workspace/config-issues.js.map +1 -0
  90. package/dist/lib/workspace/index.d.ts +9 -0
  91. package/dist/lib/workspace/index.js +11 -0
  92. package/dist/lib/workspace/index.js.map +1 -1
  93. package/dist/lib/workspace/switcher-source.d.ts +20 -3
  94. package/dist/lib/workspace/switcher-source.js.map +1 -1
  95. package/package.json +2 -2
  96. package/convex/cliTokens.ts +0 -63
  97. package/convex/sync.ts +0 -297
@@ -1,13 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
3
  import { Blocks, Database, FileCog, Image, LayoutGrid, Puzzle, Settings as SettingsIcon, Signpost, Users, } from 'lucide-react';
4
- import { assetCollection, extendCollectionDef, isSingleton, resolveSingletonDocument, sampleDocuments, } from '@elytracms/core/cms-core';
5
- import { reconcileSeedDocumentLocalization } from '@elytracms/core/persistence';
4
+ import { assetCollection, extendCollectionDef, isSingleton, resolveSingletonDocument, } from '@elytracms/core/cms-core';
6
5
  import { NavigateOnce, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
7
6
  import { Text } from '@elytracms/studio/ui';
8
7
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
9
8
  import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
10
- import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
11
9
  import { resolveDeploymentCollections, resolveDeploymentLocaleConfig } from '@elytracms/studio/lib/workspace';
12
10
  import { cmsDeepLink, componentsDeepLink, usersDeepLink, redirectsDeepLink, assetsDeepLink, groupNode, separatorNode, viewNode, } from './module.js';
13
11
  import { SectionSurface } from '../section-surface.js';
@@ -32,29 +30,95 @@ import { OperationErrorBanner } from '../operation-error.js';
32
30
  function slugify(label) {
33
31
  return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'view';
34
32
  }
35
- /** Derive a stable, deterministic id from the CMS focus (overridable via `config.id`). */
36
- function cmsViewId(config) {
33
+ /**
34
+ * The auto-parent namespace for a ONE-ITEM `collections` group (EC-468).
35
+ *
36
+ * A PREFIX, not a suffix, and deliberately so: every other derived CMS id starts with
37
+ * `cms:`, so the 4th character (`-` here, `:` there) makes the two namespaces disjoint
38
+ * BY CONSTRUCTION — no collection id, whatever it contains, can forge a `cms-group:`
39
+ * id, whereas a suffix marker (`cms:page:group`) would be forged by a collection
40
+ * literally named `page:group` (`collectionDefSchema.id` is `z.string().min(1)` —
41
+ * nothing reserves `:` or `-`).
42
+ */
43
+ const CMS_GROUP_ID_PREFIX = 'cms-group:';
44
+ /**
45
+ * Derive a stable, deterministic id from the CMS focus (overridable via `config.id`).
46
+ *
47
+ * The id is the route segment, the rail's active-state key AND the override key
48
+ * (EC-311), so it must be injective: two focuses deriving one id means
49
+ * `resolveOverrides` drops the later view. The derived shapes:
50
+ *
51
+ * | focus | id |
52
+ * | ---------------------------- | ------------------------- |
53
+ * | none | `cms` |
54
+ * | `collection: 'page'` (leaf) | `cms:page` |
55
+ * | `collections: ['a','b']` | `cms:a-b` |
56
+ * | `collections: ['page']` | `cms-group:page` |
57
+ *
58
+ * EC-468 — why the one-item group is its own namespace: `cms:${join(L)}` of a
59
+ * single-element list IS that element, so the auto-generated PARENT took the very id
60
+ * its own auto-expanded child derives (`cmsCollectionLeaf` → `cmsView({ collection })`).
61
+ * Both landed in the one flat override list `resolveSidebar` feeds `resolveOverrides`,
62
+ * so the child was dropped with a `duplicate-override-id` error and the parent rendered
63
+ * childless — a forced collision no config could avoid. Nothing else changes: the leaf
64
+ * and the multi-collection join keep their exact ids (consumer rails route on them).
65
+ *
66
+ * EC-477 — `-` is a SEPARATOR here, and a collection id may legally contain one, so a
67
+ * collection literally named `a-b` derives the same `cms:a-b` as a `['a','b']` group.
68
+ * That ambiguity is NOT closed by constraining collection ids (a permanent, repo-wide
69
+ * naming ban to fix a two-config coincidence, when hyphenated ids are ordinary — see
70
+ * the task's Done); it is DETECTED where both facts are known, by
71
+ * {@link checkSidebarIdKeyspace}, which raises a `derived-id-collision` the moment a
72
+ * group's join equals a real collection id. The escape hatch is unchanged: an explicit
73
+ * `nav.cms({ id })` always wins, and the derivation below is untouched — consumer rails
74
+ * route on these ids.
75
+ */
76
+ export function cmsViewId(config) {
37
77
  if (config.id)
38
78
  return config.id;
39
79
  if (config.collection)
40
80
  return `cms:${config.collection}`;
41
- if (config.collections && config.collections.length > 0) {
42
- return `cms:${[...config.collections].join('-')}`;
81
+ const collections = config.collections;
82
+ if (collections && collections.length > 0) {
83
+ if (collections.length === 1)
84
+ return `${CMS_GROUP_ID_PREFIX}${collections[0]}`;
85
+ return `cms:${joinCollectionIds(collections)}`;
43
86
  }
44
87
  return 'cms';
45
88
  }
89
+ /** The multi-collection focus join — ONE definition, so the id and the EC-477 check cannot drift. */
90
+ function joinCollectionIds(collections) {
91
+ return [...collections].join('-');
92
+ }
93
+ /**
94
+ * The id provenance a CMS focus carries for diagnostics (EC-477) — set only for the one
95
+ * derived shape that is genuinely ambiguous: a MULTI-collection join. An explicit `id`,
96
+ * a single-collection leaf and the one-item `cms-group:` parent are all unambiguous by
97
+ * construction (EC-468), so they carry nothing and are never diagnosed.
98
+ */
99
+ function cmsDerivedId(config) {
100
+ if (config.id || config.collection)
101
+ return undefined;
102
+ const collections = config.collections;
103
+ if (!collections || collections.length < 2)
104
+ return undefined;
105
+ return { from: 'collection-join', collections: [...collections] };
106
+ }
46
107
  /**
47
108
  * Build a CMS view — the default `cms` view, or a focused instance. Every instance
48
109
  * shares one surface ({@link CmsScreen}); `collections` narrows the sidebar + index
49
110
  * to a subset, `collection` is a single-collection lens that opens straight into
50
111
  * that collection's list. The id is derived from the focus (deterministic, stable
51
- * across rail reorders) unless `id` is given — so two focused views never collide.
112
+ * across rail reorders) unless `id` is given — see {@link cmsViewId} for the derived
113
+ * shapes and the one keyspace ambiguity that survives.
52
114
  */
53
115
  export function cmsView(config = {}) {
54
116
  const single = config.collection;
55
117
  const focus = single ? [single] : config.collections;
118
+ const derivedId = cmsDerivedId(config);
56
119
  return {
57
120
  id: cmsViewId(config),
121
+ ...(derivedId ? { derivedId } : {}),
58
122
  // Lazy label/description (EC-241): the sidebar is built when the consumer's
59
123
  // `studio.config` module loads — BEFORE `createStudio` seeds the i18n config — so
60
124
  // resolving here would bake English. A getter defers resolution to render, when
@@ -255,10 +319,22 @@ export function builderView() {
255
319
  }, replace: true })),
256
320
  };
257
321
  }
258
- /** Build a fully custom view (the `nav.view(...)` escape hatch). */
322
+ /**
323
+ * Build a fully custom view (the `nav.view(...)` escape hatch).
324
+ *
325
+ * With no `id` the id is a SLUG of the label, and that slug carries a
326
+ * {@link DerivedViewId} marker (EC-476): a label is a display string a developer picks
327
+ * freely, so `label: 'CMS'` deriving `cms` is a coincidence, not a request to replace
328
+ * the built-in CMS surface. {@link checkSidebarIdKeyspace} refuses the takeover and
329
+ * says so; an explicit `id` carries no marker and keeps the documented replacement.
330
+ */
259
331
  export function customView(config) {
332
+ const derivedId = config.id
333
+ ? undefined
334
+ : { from: 'label', label: config.label };
260
335
  return {
261
336
  id: config.id ?? slugify(config.label),
337
+ ...(derivedId ? { derivedId } : {}),
262
338
  label: config.label,
263
339
  description: config.description ?? config.label,
264
340
  icon: config.icon ?? LayoutGrid,
@@ -284,6 +360,97 @@ export const DEFAULT_VIEWS = [
284
360
  settingsView(),
285
361
  builderView(),
286
362
  ];
363
+ /**
364
+ * Every id the engine already owns — the replacement targets of the sidebar seam,
365
+ * INCLUDING the hidden `builder` redirect (it is in the keyspace even though the stock
366
+ * rail does not list it, so a derived id can swallow it just the same).
367
+ */
368
+ export const BUILTIN_VIEW_IDS = new Set(DEFAULT_VIEWS.map((view) => view.id));
369
+ /** The extension point these issues are located at — the same literal `resolveSidebar` stamps. */
370
+ const SIDEBAR_POINT = 'sidebar';
371
+ /**
372
+ * The catalog keys for the two keyspace findings (EC-478). This module stays pure —
373
+ * it names a key and its identifier values; `lib/persona/labels.ts` resolves them at
374
+ * the render site, so the rail foot localizes as one list.
375
+ */
376
+ const KEYSPACE_KEYS = {
377
+ builtinCollision: 'customization.issue.derivedIdBuiltinCollision',
378
+ collectionCollision: 'customization.issue.derivedIdCollectionCollision',
379
+ };
380
+ /**
381
+ * EC-476/477 — guard the sidebar id keyspace against ids that were DERIVED rather than
382
+ * declared, and landed somewhere that already means something.
383
+ *
384
+ * The seam's replacement rule is id equality (`resolveOverrides`: an entry whose id is a
385
+ * default's id replaces it). That is exactly right for an id the developer TYPED, and
386
+ * exactly wrong for one a builder computed — and the generic core cannot tell them
387
+ * apart, because by the time an entry reaches it, an id is just a string. So the rule
388
+ * lives HERE, next to the derivations that produce it ({@link slugify},
389
+ * {@link cmsViewId}) and next to {@link DEFAULT_VIEWS}, the set it has to protect.
390
+ *
391
+ * Two derived shapes, two different remedies — because the cost of being wrong differs:
392
+ *
393
+ * - **A slugged LABEL landing on a built-in id (EC-476) is REJECTED** (`error`). Losing
394
+ * a whole built-in surface — the CMS, Assets, the users roster — to a coincidence of
395
+ * spelling is far worse than one custom view not appearing, and the developer's fix is
396
+ * a one-line explicit `id`. The built-in is kept, so nothing disappears.
397
+ * - **A collection JOIN equal to a real collection id (EC-477) is KEPT** (`warning`).
398
+ * Nothing here is a takeover: the group's id is legitimate, it is merely no longer
399
+ * unique to it, so dropping the view would destroy a rail item the developer
400
+ * explicitly declared in order to report a naming clash. Reporting is the whole fix.
401
+ *
402
+ * Pure and deterministic — the house "extract the core, test the core" shape.
403
+ * `builtinIds` and `collections` are parameters so the rule is testable against a
404
+ * synthetic keyspace, not just the live one.
405
+ */
406
+ export function checkSidebarIdKeyspace(views, collections = [], builtinIds = BUILTIN_VIEW_IDS) {
407
+ const collectionIds = new Set(collections.map((collection) => collection.id));
408
+ const kept = [];
409
+ const issues = [];
410
+ for (const view of views) {
411
+ const derived = view.derivedId;
412
+ // EC-476 — an auto-derived id must never claim a built-in surface.
413
+ if (derived?.from === 'label' && builtinIds.has(view.id)) {
414
+ issues.push({
415
+ code: 'derived-id-collision',
416
+ severity: 'error',
417
+ message: `View "${derived.label}" for "${SIDEBAR_POINT}" derives the id "${view.id}" from its label, which is the built-in "${view.id}" view — the built-in is kept and this view is ignored. Pass an explicit id to replace a built-in on purpose.`,
418
+ messageRef: {
419
+ key: KEYSPACE_KEYS.builtinCollision,
420
+ vars: { label: derived.label, point: SIDEBAR_POINT, id: view.id },
421
+ },
422
+ point: SIDEBAR_POINT,
423
+ id: view.id,
424
+ });
425
+ continue;
426
+ }
427
+ // EC-477 — a multi-collection join that a single collection could equally have produced.
428
+ if (derived?.from === 'collection-join' && collectionIds.has(joinCollectionIds(derived.collections))) {
429
+ issues.push({
430
+ code: 'derived-id-collision',
431
+ severity: 'warning',
432
+ message: `View id "${view.id}" for "${SIDEBAR_POINT}" is ambiguous: it joins the collections ${quotedIds(derived.collections)} with "-", and a collection is literally named "${joinCollectionIds(derived.collections)}" — its own lens derives the same id. Pass an explicit id to this group.`,
433
+ messageRef: {
434
+ key: KEYSPACE_KEYS.collectionCollision,
435
+ vars: {
436
+ id: view.id,
437
+ point: SIDEBAR_POINT,
438
+ collections: quotedIds(derived.collections),
439
+ joined: joinCollectionIds(derived.collections),
440
+ },
441
+ },
442
+ point: SIDEBAR_POINT,
443
+ id: view.id,
444
+ });
445
+ }
446
+ kept.push(view);
447
+ }
448
+ return { views: kept, issues };
449
+ }
450
+ /** `"a", "b"` — a readable id list for an issue message (the EC-467 issue-copy shape). */
451
+ function quotedIds(ids) {
452
+ return ids.map((id) => `"${id}"`).join(', ');
453
+ }
287
454
  /**
288
455
  * The `nav` namespace — the single typed builder for `studio.config.tsx`'s section
289
456
  * rail. Each call returns a {@link SidebarViewNode} (or a group / separator) that
@@ -313,6 +480,8 @@ export const nav = {
313
480
  * The CMS view. `nav.cms()` / `nav.cms({ collections })` is a PARENT that nests its
314
481
  * collections in the rail (each a focused `cms:<id>` lens); `nav.cms({ collection })`
315
482
  * is a single-collection LEAF; `nav.cms({ children })` supplies explicit children.
483
+ * A one-item `collections` group is a parent whose id is `cms-group:<id>`, distinct
484
+ * from its single child by construction (EC-468 — see {@link cmsViewId}).
316
485
  */
317
486
  cms: (config = {}) => {
318
487
  const view = withNavPermission(cmsView(config), config);
@@ -360,59 +529,47 @@ function ComponentsScreen({ projectId, componentId, state, }) {
360
529
  return (_jsxs("div", { className: "flex h-full min-h-0", children: [_jsx("aside", { className: "hidden h-full min-h-0 w-[300px] shrink-0 border-r border-border md:block", children: _jsx(RegistryBrowser, { projectId: projectId, activeComponentId: componentId ?? undefined }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: componentId === null ? (_jsx(ComponentsIndex, { projectId: projectId })) : (_jsx(ComponentDetail, { projectId: projectId, componentId: componentId })) })] }));
361
530
  }
362
531
  /**
363
- * The CMS schema source for a project mount (bug fix, playground-collections):
364
- * a hydrated session wins (Convex), then a kit's imported schema (EC-119), then —
365
- * in playground mode only — the active project's config-declared collections
366
- * (AD-11, `resolveDeploymentCollections`). Without this last fallback, a non-kit
367
- * playground project has no hydrated session (`null`, by design — see
368
- * `project-session.tsx`) and no kit, so `CmsProvider` fell through to the core's
369
- * hardcoded `sampleFilterableCollections` fixture instead of the project's own
370
- * on-disk schema. Convex mode's first-run `null` is untouched — it still seeds
371
- * from the fixture there, as before. `playground` defaults to the live
372
- * {@link isPlaygroundMode} but is overridable so this stays unit testable
373
- * without stubbing the env (same seam as `resolveBackendConfig`).
532
+ * The CMS schema source for a project mount: a hydrated session wins (Convex),
533
+ * then a kit's imported schema (EC-119), then the active project's
534
+ * config-declared collections (AD-11, `resolveDeploymentCollections`).
535
+ *
536
+ * EC-479 — every rung is the PROJECT's own schema and there is no rung after
537
+ * them. The config rung used to be gated on playground mode, because outside it
538
+ * the `undefined` return let `CmsProvider` fall through to `cms-core`'s
539
+ * `sampleFilterableCollections` fixture — the engine's demo schema, shown as the
540
+ * project's, offering `filterable` fields the project's real backend would
541
+ * reject. With that fallback retired the gate has nothing left to protect: the
542
+ * Convex adapter is itself constructed with `resolveDeploymentCollections()`
543
+ * (`lib/persistence/index.ts`), so config collections are what connected mode
544
+ * serves too. Dropping it also DISSOLVES the EC-467 two-source divergence — the
545
+ * section rail reads `resolveDeploymentCollections()` directly, and now so does
546
+ * the store's last rung, so the two can no longer disagree.
374
547
  */
375
- export function resolveCmsInitialCollections(cms, kit, playground = isPlaygroundMode()) {
376
- return cms?.collections ?? kit?.collections ?? (playground ? resolveDeploymentCollections() : undefined);
548
+ export function resolveCmsInitialCollections(cms, kit) {
549
+ return cms?.collections ?? kit?.collections ?? resolveDeploymentCollections();
377
550
  }
378
551
  /**
379
- * The CMS documents source for a project mount (EC-424) — the document-side
380
- * twin of {@link resolveCmsInitialCollections}, same ladder: a hydrated session
381
- * wins (Convex), then a kit's imported documents, then — in playground mode
382
- * only, and only when a config project actually resolved collections — the
383
- * engine's own `sampleDocuments` fixture RECONCILED against them.
552
+ * The CMS documents source for a project mount — the document-side twin of
553
+ * {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then a
554
+ * kit's imported documents, then **nothing**.
384
555
  *
385
- * Without this, a config project whose OWN schema diverges from the shared
386
- * fixture's localization (e.g. this repo's demo declares `post.slug` localized,
387
- * EC-315, while the engine's `postCollection` fixture deliberately does not)
388
- * left `CmsProvider` falling straight through to the un-reconciled
389
- * `sampleDocuments` default — the CMS surfaces' `initialCollections` already
390
- * reflected the config schema (via the fallback above), but `initialDocuments`
391
- * did not, so every seeded post showed EC-390's shape-aware validation's
392
- * `plain-value` warning + `missing-localized-value` error, standing noise a
393
- * project never actually wrote.
556
+ * EC-479 — the third rung used to be the engine's own `sampleDocuments` fixture,
557
+ * reconciled against the project's schema (EC-424). Reconciling it was already an
558
+ * admission that the content was foreign: EC-424 existed only because the demo
559
+ * declares `post.slug` localized while the shared fixture stores it plain, so the
560
+ * seeded posts carried standing validation noise no project had written. The
561
+ * honest fix is not a better reconciliation but no foreign content at all — a
562
+ * studio whose config did not arrive now shows an empty CMS and a visible issue
563
+ * at the rail, not six documents about Ada Lovelace wearing the project's schema.
394
564
  *
395
- * This is the STATIC-baseline twin of `reconcileSeedDocumentLocalization`
396
- * (`@elytracms/core/persistence`, wired into the seeded persistence adapter for
397
- * Convex-mode/delivery reads): no adapter round-trip, so the EC-138/EC-320
398
- * synchronous-first-render contract (`project-session.tsx`: "CMS surfaces keep
399
- * their fixture baseline") is untouched. `collections` defaults to the SAME
400
- * resolution `resolveCmsInitialCollections` returns, so the two stay in lockstep
401
- * by construction; only reconciles when that resolved to a non-empty config
402
- * schema (env-only playground, with nothing config-declared, is unaffected).
565
+ * The playground keeps its world through the ordinary path, not through here: its
566
+ * seeded in-memory adapter IS a persisted backend behind the operations seam
567
+ * (AD-17), so `CmsSessionGate` hydrates it and the first rung answers. What that
568
+ * adapter seeds is `persistence/seed.ts`'s business — EC-475 (project-defined seed
569
+ * content) owns it.
403
570
  */
404
- export function resolveCmsInitialDocuments(cms, kit, collections = resolveCmsInitialCollections(cms, kit)) {
405
- if (cms)
406
- return cms.documents;
407
- if (kit)
408
- return kit.documents;
409
- if (!collections || collections.length === 0)
410
- return undefined;
411
- const collectionsById = new Map(collections.map((collection) => [collection.id, collection]));
412
- return sampleDocuments.map((doc) => {
413
- const collection = collectionsById.get(doc.collection);
414
- return collection ? reconcileSeedDocumentLocalization(doc, collection) : doc;
415
- });
571
+ export function resolveCmsInitialDocuments(cms, kit) {
572
+ return cms?.documents ?? kit?.documents;
416
573
  }
417
574
  /**
418
575
  * The CMS locale config for a project mount (EC-357 finding #3). Locales are
@@ -421,18 +578,28 @@ export function resolveCmsInitialDocuments(cms, kit, collections = resolveCmsIni
421
578
  * wiring only applied the project's locale settings on a HYDRATED session, so a
422
579
  * playground boot — or a connected FIRST RUN with nothing persisted yet — fell
423
580
  * back to a kit's locales or the core's sample `en/de` fixture and the switcher
424
- * showed `EN·/DE` against a `locales: ['de']` config. Without a config project
425
- * (env-only wiring) the previous ladder is unchanged: hydrated project settings,
426
- * then the kit's locales, then the sample fixture (via `CmsProvider`'s default).
581
+ * showed `EN·/DE` against a `locales: ['de']` config.
582
+ *
583
+ * EC-479/EC-483 — the ladder is now TOTAL, and every rung is the project's own:
584
+ * config-declared locales, then (hydrated) its persisted settings, then a kit's,
585
+ * then its settings regardless. A `Project` only ever exists for a config-declared
586
+ * project (`studio-context.tsx`: `configProjects()`), so `project.settings` is
587
+ * config-derived too — which is what lets the CMS session require a locale instead
588
+ * of borrowing the engine's `en/de` fixture. The default locale is the literal
589
+ * storage key for every localized value, so a guessed one is a data migration.
427
590
  * `configLocales` is a parameter purely so the ladder is unit-testable (same seam
428
591
  * pattern as {@link resolveCmsInitialCollections}).
429
592
  */
430
593
  export function resolveCmsInitialLocaleConfig(cms, kit, project, configLocales = resolveDeploymentLocaleConfig()) {
594
+ const projectSettings = {
595
+ default: project.settings.defaultLocale,
596
+ locales: [...project.settings.locales],
597
+ };
431
598
  if (configLocales)
432
599
  return configLocales;
433
600
  if (cms)
434
- return { default: project.settings.defaultLocale, locales: [...project.settings.locales] };
435
- return kit?.localeConfig;
601
+ return projectSettings;
602
+ return kit?.localeConfig ?? projectSettings;
436
603
  }
437
604
  /**
438
605
  * CMS section: hydrates the persisted (or kit-seeded) collections + documents, then
@@ -450,10 +617,7 @@ function CmsScreen({ project, collectionId, documentId, state, field, focusColle
450
617
  // A project created from a starter kit (EC-119) opens the CMS on the kit's schemas + content.
451
618
  const kit = getInstantiatedKit(projectId);
452
619
  return (_jsx(CmsSessionGate, { children: (cms) => {
453
- // EC-424: resolved once so `initialDocuments`' reconciliation (below)
454
- // stays in lockstep with the SAME collections `initialCollections` uses.
455
- const initialCollections = resolveCmsInitialCollections(cms, kit);
456
- return (_jsx(CmsProvider, { projectId: projectId, initialCollections: initialCollections, initialDocuments: resolveCmsInitialDocuments(cms, kit, initialCollections), initialPublishedKeys: cms?.publishedKeys, initialTimestamps: cms?.timestamps, initialPublishTimestamps: cms?.publishTimestamps, initialChangedKeys: cms?.changedKeys, initialLocaleConfig: resolveCmsInitialLocaleConfig(cms, kit, project),
620
+ return (_jsx(CmsProvider, { projectId: projectId, initialCollections: resolveCmsInitialCollections(cms, kit), initialDocuments: resolveCmsInitialDocuments(cms, kit), initialPublishedKeys: cms?.publishedKeys, initialTimestamps: cms?.timestamps, initialPublishTimestamps: cms?.publishTimestamps, initialChangedKeys: cms?.changedKeys, initialLocaleConfig: resolveCmsInitialLocaleConfig(cms, kit, project),
457
621
  // EC-461: a hydrated mount (cms non-null — both modes now) seeds the
458
622
  // CURRENT persisted state, so staged docs are superseded by it; only
459
623
  // a static kit/fixture seed lets staged entries win.