@elytracms/studio 0.0.79 → 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 (107) 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/date-input-core.d.ts +21 -0
  34. package/dist/components/cms/date-input-core.js +20 -0
  35. package/dist/components/cms/date-input-core.js.map +1 -1
  36. package/dist/components/cms/date-input.d.ts +9 -1
  37. package/dist/components/cms/date-input.js +2 -2
  38. package/dist/components/cms/date-input.js.map +1 -1
  39. package/dist/components/cms/detail-editor.js +11 -5
  40. package/dist/components/cms/detail-editor.js.map +1 -1
  41. package/dist/components/cms/editable-cell.js +2 -1
  42. package/dist/components/cms/editable-cell.js.map +1 -1
  43. package/dist/components/cms/field-control.js +4 -2
  44. package/dist/components/cms/field-control.js.map +1 -1
  45. package/dist/components/cms/field-renderer.js +8 -1
  46. package/dist/components/cms/field-renderer.js.map +1 -1
  47. package/dist/components/cms/import-export.js +2 -1
  48. package/dist/components/cms/import-export.js.map +1 -1
  49. package/dist/components/cms/issues-pane.d.ts +4 -2
  50. package/dist/components/cms/issues-pane.js +6 -4
  51. package/dist/components/cms/issues-pane.js.map +1 -1
  52. package/dist/components/cms/scalar-value-editor.js +2 -1
  53. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  54. package/dist/components/components/transfer-dialog.js +10 -12
  55. package/dist/components/components/transfer-dialog.js.map +1 -1
  56. package/dist/components/project-list.d.ts +9 -3
  57. package/dist/components/project-list.js +11 -5
  58. package/dist/components/project-list.js.map +1 -1
  59. package/dist/components/project-switcher.d.ts +3 -1
  60. package/dist/components/project-switcher.js +4 -2
  61. package/dist/components/project-switcher.js.map +1 -1
  62. package/dist/components/redirects/redirects-store.d.ts +21 -0
  63. package/dist/components/redirects/redirects-store.js +17 -4
  64. package/dist/components/redirects/redirects-store.js.map +1 -1
  65. package/dist/components/section-nav.js +102 -4
  66. package/dist/components/section-nav.js.map +1 -1
  67. package/dist/customization.d.ts +7 -0
  68. package/dist/customization.js.map +1 -1
  69. package/dist/lib/data/collection-content.d.ts +6 -1
  70. package/dist/lib/data/collection-content.js +50 -12
  71. package/dist/lib/data/collection-content.js.map +1 -1
  72. package/dist/lib/i18n/messages.de.js +132 -0
  73. package/dist/lib/i18n/messages.de.js.map +1 -1
  74. package/dist/lib/i18n/messages.en.d.ts +103 -0
  75. package/dist/lib/i18n/messages.en.js +146 -0
  76. package/dist/lib/i18n/messages.en.js.map +1 -1
  77. package/dist/lib/locale/binding-fallback.d.ts +8 -1
  78. package/dist/lib/locale/binding-fallback.js +8 -1
  79. package/dist/lib/locale/binding-fallback.js.map +1 -1
  80. package/dist/lib/locale/locale-store.d.ts +41 -9
  81. package/dist/lib/locale/locale-store.js +34 -15
  82. package/dist/lib/locale/locale-store.js.map +1 -1
  83. package/dist/lib/locale/locale-switcher.d.ts +3 -1
  84. package/dist/lib/locale/locale-switcher.js +4 -2
  85. package/dist/lib/locale/locale-switcher.js.map +1 -1
  86. package/dist/lib/locale/use-studio-locale.d.ts +9 -4
  87. package/dist/lib/locale/use-studio-locale.js.map +1 -1
  88. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  89. package/dist/lib/persistence/convex-adapter.js +0 -55
  90. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  91. package/dist/lib/persona/labels.d.ts +42 -1
  92. package/dist/lib/persona/labels.js +57 -0
  93. package/dist/lib/persona/labels.js.map +1 -1
  94. package/dist/lib/routes/route-issues.d.ts +9 -0
  95. package/dist/lib/routes/route-issues.js +117 -0
  96. package/dist/lib/routes/route-issues.js.map +1 -0
  97. package/dist/lib/workspace/config-issues.d.ts +54 -0
  98. package/dist/lib/workspace/config-issues.js +70 -0
  99. package/dist/lib/workspace/config-issues.js.map +1 -0
  100. package/dist/lib/workspace/index.d.ts +9 -0
  101. package/dist/lib/workspace/index.js +11 -0
  102. package/dist/lib/workspace/index.js.map +1 -1
  103. package/dist/lib/workspace/switcher-source.d.ts +20 -3
  104. package/dist/lib/workspace/switcher-source.js.map +1 -1
  105. package/package.json +2 -2
  106. package/convex/cliTokens.ts +0 -63
  107. package/convex/sync.ts +0 -297
@@ -225,7 +225,7 @@ describe('guard capability gates through the real functions (EC-150/EC-240c, Con
225
225
  await expect(
226
226
  viewer.cms.upsertDocument(PROJECT_ID, { collection: 'post', id: 'x', values: {} }),
227
227
  ).rejects.toThrow(/unauthorized/) // content.write denied
228
- await expect(viewer.cliTokens.listTokens()).rejects.toThrow(/unauthorized/) // project.administer denied
228
+ await expect(viewer.users.listUsers(PROJECT_ID)).rejects.toThrow(/unauthorized/) // project.administer denied
229
229
 
230
230
  // An authenticated NON-member is rejected even on reads …
231
231
  await expect(forSubject('user_stranger').cms.listDocuments(PROJECT_ID)).rejects.toThrow(
@@ -52,8 +52,18 @@ export const internalMutation = customMutation(
52
52
  internalMutationGeneric as MutationBuilder<DataModel, 'internal'>,
53
53
  customCtx(triggers.wrapDB),
54
54
  )
55
- /** Server-only reads (e.g. the EC-157 HTTP sync endpoints) — not callable by clients. */
55
+ /**
56
+ * Server-only reads — not callable by clients. The mirror of `internalMutation`,
57
+ * kept as the symmetric half of the codegen-replacement builder set and as the
58
+ * builder a consumer reaches for when adding its own internal query.
59
+ * EC-481: the engine itself no longer declares one (its only user was the retired
60
+ * registry-sync snapshot).
61
+ */
56
62
  export const internalQuery = internalQueryGeneric as QueryBuilder<DataModel, 'internal'>
57
- /** HTTP action builder (EC-252) — used by the package's `http.ts`/`sync.ts`. Not
58
- * DataModel-bound: an action reaches data only through `ctx.runQuery`/`runMutation`. */
63
+ /**
64
+ * HTTP action builder (EC-252). Not DataModel-bound: an action reaches data only
65
+ * through `ctx.runQuery`/`runMutation`.
66
+ * EC-481: the engine declares no HTTP action of its own any more (`http.ts` only
67
+ * mounts the consumer's auth routes); this stays for consumer-defined actions.
68
+ */
59
69
  export const httpAction = httpActionGeneric
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { httpRouter } from 'convex/server'
3
+ import type { HttpRouter, PublicHttpAction } from 'convex/server'
4
+ import { defineStudioHttp } from './http'
5
+
6
+ /**
7
+ * EC-481 — the studio's HTTP route table, pinned.
8
+ *
9
+ * `defineStudioHttp` used to add `GET /api/projects/*` (the EC-157 registry-sync
10
+ * endpoint). It was Bearer-authenticated, reachable on every deployment, and — after
11
+ * EC-470 deleted the `builder` CLI — had no client at all. Removing it is only half
12
+ * the job: the failure mode this file exists to prevent is a future edit quietly
13
+ * mounting a route again, or re-claiming that very broad prefix. Neither would break
14
+ * anything, which is exactly why nothing would catch it.
15
+ *
16
+ * The `auth` seam is injected (`{ addHttpRoutes }`), so these assertions isolate what
17
+ * the ENGINE contributes from what Convex Auth contributes — the real question. Every
18
+ * detector below is paired with a positive control (`AGENTS.md` §1.5): a recorder that
19
+ * observes nothing is indistinguishable from a router that registers nothing.
20
+ */
21
+
22
+ /** An `auth` double that registers nothing — isolates the engine's own contribution. */
23
+ const silentAuth = { addHttpRoutes: () => {} }
24
+
25
+ /** A stand-in handler; the router only stores it. */
26
+ const handler = (() => {}) as unknown as PublicHttpAction
27
+
28
+ /** The route table as stable `"<METHOD> <path>"` strings, sorted. */
29
+ function routeTable(http: HttpRouter): string[] {
30
+ return http
31
+ .getRoutes()
32
+ .map(([path, method]) => `${method} ${path}`)
33
+ .sort()
34
+ }
35
+
36
+ describe('defineStudioHttp — the engine registers no HTTP routes of its own (EC-481)', () => {
37
+ it('contributes an EMPTY route table beyond what the injected auth registers', () => {
38
+ expect(routeTable(defineStudioHttp({ auth: silentAuth }))).toEqual([])
39
+ })
40
+
41
+ it('control: the recorder DOES see routes, so the empty result above is real', () => {
42
+ const auth = {
43
+ addHttpRoutes: (http: HttpRouter) => {
44
+ http.route({ path: '/.well-known/jwks.json', method: 'GET', handler })
45
+ http.route({ pathPrefix: '/api/auth/callback/', method: 'POST', handler })
46
+ },
47
+ }
48
+ expect(routeTable(defineStudioHttp({ auth }))).toEqual([
49
+ 'GET /.well-known/jwks.json',
50
+ 'POST /api/auth/callback/*',
51
+ ])
52
+ })
53
+
54
+ it('passes the auth routes through untouched — nothing is added, dropped, or reordered', () => {
55
+ const registered: string[] = []
56
+ const auth = {
57
+ addHttpRoutes: (http: HttpRouter) => {
58
+ for (const path of ['/.well-known/openid-configuration', '/.well-known/jwks.json']) {
59
+ http.route({ path, method: 'GET', handler })
60
+ registered.push(`GET ${path}`)
61
+ }
62
+ },
63
+ }
64
+ expect(routeTable(defineStudioHttp({ auth }))).toEqual([...registered].sort())
65
+ })
66
+ })
67
+
68
+ describe('the /api/projects/ prefix is released (EC-481)', () => {
69
+ it('routes NOTHING under /api/projects/ — the request falls through to Convex 404', () => {
70
+ const http = defineStudioHttp({ auth: silentAuth })
71
+ // `lookup` is exactly what Convex's `runRequest` consults; `null` is the branch
72
+ // that answers `No HttpAction routed for <path>`.
73
+ expect(http.lookup('/api/projects/demo/registry-sync/snapshot', 'GET')).toBeNull()
74
+ expect(http.lookup('/api/projects/', 'GET')).toBeNull()
75
+ expect(http.lookup('/api/projects/anything-at-all', 'GET')).toBeNull()
76
+ })
77
+
78
+ it('control: the same lookup DOES resolve when something claims the prefix', () => {
79
+ const auth = {
80
+ addHttpRoutes: (http: HttpRouter) => {
81
+ http.route({ pathPrefix: '/api/projects/', method: 'GET', handler })
82
+ },
83
+ }
84
+ const http = defineStudioHttp({ auth })
85
+ expect(http.lookup('/api/projects/demo/registry-sync/snapshot', 'GET')).not.toBeNull()
86
+ })
87
+
88
+ it('no route the engine mounts may claim a prefix that swallows /api/projects/', () => {
89
+ const claims = routeTable(defineStudioHttp({ auth: silentAuth })).filter((route) =>
90
+ route.includes('/api/projects'),
91
+ )
92
+ expect(claims).toEqual([])
93
+ })
94
+ })
95
+
96
+ describe('auth routes are unaffected by the removal (EC-481)', () => {
97
+ it('leaves the /api/auth/* prefixes matching exactly as before', () => {
98
+ // The removed route was `/api/projects/`, which is neither a prefix of nor
99
+ // prefixed by any auth path — so Convex's longest-prefix-wins ordering cannot
100
+ // have changed for them. Asserted rather than argued.
101
+ const auth = {
102
+ addHttpRoutes: (http: HttpRouter) => {
103
+ http.route({ pathPrefix: '/api/auth/signin/', method: 'GET', handler })
104
+ http.route({ pathPrefix: '/api/auth/callback/', method: 'GET', handler })
105
+ },
106
+ }
107
+ const http = defineStudioHttp({ auth })
108
+ expect(http.lookup('/api/auth/signin/github', 'GET')?.[2]).toBe('/api/auth/signin/*')
109
+ expect(http.lookup('/api/auth/callback/github', 'GET')?.[2]).toBe('/api/auth/callback/*')
110
+ // …and the released prefix still resolves to nothing alongside them.
111
+ expect(http.lookup('/api/projects/demo', 'GET')).toBeNull()
112
+ })
113
+ })
114
+
115
+ describe('the registry-sync and CLI-token modules are gone, not merely unrouted (EC-481/EC-485)', () => {
116
+ // `import.meta.glob` is the module-enumeration pattern this directory already uses
117
+ // (`contract-parity.test.ts`). A literal `import('./sync')` cannot be used here: TS
118
+ // fails to *typecheck* a specifier that no longer resolves, so the assertion could
119
+ // never compile — the glob asks the same question without naming a missing module.
120
+ const convexModules = Object.keys(import.meta.glob('./*.ts'))
121
+
122
+ it('ships no `sync` module — a route can only come back from a handler that exists', () => {
123
+ expect(convexModules).not.toContain('./sync.ts')
124
+ })
125
+
126
+ it('ships no `cliTokens` module — the whole CLI-token path was retired (EC-485)', () => {
127
+ // The registry-sync endpoint was its only verifier (EC-481); with that gone the
128
+ // issuance path was inert and `cliTokens:byHash` was a public, unauthenticated
129
+ // query with an expired justification. Nothing addresses these functions any
130
+ // more — `ConvexCliTokenRepository` went with them, in the same change, because
131
+ // it named them by STRING and a half-rip would typecheck and fail at runtime.
132
+ expect(convexModules).not.toContain('./cliTokens.ts')
133
+ })
134
+
135
+ it('control: the glob DOES see this directory, so the absences above are real', () => {
136
+ expect(convexModules).toContain('./http.ts')
137
+ expect(convexModules).toContain('./users.ts')
138
+ })
139
+
140
+ it('control: `httpRouter()` itself is unchanged — an empty router really is empty', () => {
141
+ expect(routeTable(httpRouter())).toEqual([])
142
+ })
143
+ })
package/convex/http.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { httpRouter } from 'convex/server'
2
2
  import type { HttpRouter } from 'convex/server'
3
- import { registrySync } from './sync'
4
3
 
5
4
  /** The slice of the Convex Auth object the studio's HTTP routes need. */
6
5
  interface AuthHttpRoutes {
@@ -8,23 +7,34 @@ interface AuthHttpRoutes {
8
7
  }
9
8
 
10
9
  /**
11
- * EC-252 — the studio's HTTP routes as a factory. `http`/`sync` are pure engine
12
- * infra, so they ship in the package; only the consumer's `auth` (provider choice)
13
- * is injected. The consumer's `convex/http.ts` is a two-liner:
10
+ * EC-252 — the studio's HTTP routes as a factory. Pure engine infra ships in the
11
+ * package; only the consumer's `auth` (provider choice) is injected. The consumer's
12
+ * `convex/http.ts` is a two-liner:
14
13
  *
15
14
  * ```ts
16
15
  * import { auth } from './auth'
17
16
  * export default defineStudioHttp({ auth })
18
17
  * ```
19
18
  *
20
- * Registers Convex Auth's JWT discovery endpoints (`/.well-known/*`, EC-150) plus the
21
- * EC-157 Bearer-token registry-sync endpoint. Convex routes by exact path or prefix
22
- * only, so the dynamic `/api/projects/:projectId/registry-sync/snapshot` segment is
23
- * parsed inside the action (`sync.ts`).
19
+ * ## The engine registers NO HTTP routes of its own (EC-481)
20
+ *
21
+ * Everything on this router comes from Convex Auth's `addHttpRoutes` — the JWT
22
+ * discovery endpoints (`/.well-known/*`, EC-150) and, only when a consumer supplies
23
+ * an OAuth provider, `/api/auth/signin/*` + `/api/auth/callback/*`.
24
+ *
25
+ * The engine used to add one more: `GET /api/projects/*`, the EC-157 Bearer-token
26
+ * registry-sync endpoint backing `builder typegen`/`builder check`. EC-470 deleted
27
+ * that CLI and with it the endpoint's only client; the handler had in any case
28
+ * answered with config-owned placeholders since EC-287 retired the project graph
29
+ * (`project: null`, `collections: []`). EC-481 removed the route and its handler
30
+ * together rather than leave a Bearer-authenticated surface serving stub data on
31
+ * every deployment. `http-routes.test.ts` pins that shut.
32
+ *
33
+ * A future engine-owned HTTP surface is fine — but it needs a real client and a
34
+ * narrower prefix than `/api/projects/`.
24
35
  */
25
36
  export function defineStudioHttp({ auth }: { auth: AuthHttpRoutes }): HttpRouter {
26
37
  const http = httpRouter()
27
38
  auth.addHttpRoutes(http)
28
- http.route({ pathPrefix: '/api/projects/', method: 'GET', handler: registrySync })
29
39
  return http
30
40
  }
package/convex/schema.ts CHANGED
@@ -109,25 +109,6 @@ export default defineSchema({
109
109
  at: v.string(),
110
110
  }).index('by_time', ['at']),
111
111
 
112
- /**
113
- * Revocable CLI sync tokens (EC-150, consumed by the EC-147 push CLI).
114
- * Only the SHA-256 hash of a token is ever stored; revoked tokens keep
115
- * their row (with `revokedAt`) for auditability.
116
- *
117
- * Single-tenant (EC-182/EC-183): a deployment hosts one project, so a token
118
- * carries no per-project scope — `projectId` is gone (it was `optional`
119
- * through EC-182; this drops the field now that both deployments are migrated).
120
- */
121
- cliTokens: defineTable({
122
- tokenId: v.string(),
123
- label: v.string(),
124
- tokenHash: v.string(),
125
- createdAt: v.string(),
126
- revokedAt: v.optional(v.string()),
127
- })
128
- .index('by_token', ['tokenId'])
129
- .index('by_hash', ['tokenHash']),
130
-
131
112
  /**
132
113
  * CMS documents (EC-043). The stored `document` is the continuous working
133
114
  * **draft**; `publishedVersion` pins which `documentVersions` row is live
package/convex/users.ts CHANGED
@@ -42,7 +42,7 @@ export const me = query({
42
42
  * onto `@elytracms/core/persistence` `UserRecord`s.
43
43
  *
44
44
  * Admin-only, server-side: the list exposes every member's email + role — an
45
- * administration concern (like `cliTokens:list`), not a content read. UI gating
45
+ * administration concern (like `invites:list`), not a content read. UI gating
46
46
  * alone is never trusted.
47
47
  */
48
48
 
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { LucideIcon } from 'lucide-react';
3
3
  import type { CmsDocument, CollectionDef, CollectionFieldOverride, CollectionFormMeta, LocaleConfig } from '@elytracms/core/cms-core';
4
- import type { Project, SectionId } from '@elytracms/core/studio-core';
4
+ import type { CustomizationIssue, Project, SectionId } from '@elytracms/core/studio-core';
5
5
  import type { InstantiatedKit } from '@elytracms/core/starter-kits';
6
6
  import type { HydratedCms } from '@elytracms/studio/lib/operations';
7
7
  import { type AppViewContext, type AppViewDeepLink, type AppViewModule, type NavPermission, type SidebarViewNode } from './module.js';
@@ -28,6 +28,11 @@ export interface CmsViewConfig {
28
28
  * Scope the rail auto-expansion (EC-293) to these collection ids, in order. The
29
29
  * parent expands into one focused-lens child per id; the parent index shows the
30
30
  * same subset. Omitted → every collection. Ignored for a single-collection lens.
31
+ *
32
+ * A ONE-ITEM array is legal and degrades to a working (if redundant) parent+child
33
+ * pair — the parent takes the `cms-group:<id>` id so it cannot collide with its own
34
+ * child (EC-468). For a single collection with no group heading, prefer the
35
+ * {@link CmsViewConfig.collection} leaf form.
31
36
  */
32
37
  collections?: readonly string[];
33
38
  /** Single-collection lens: a LEAF rail item that opens straight into this collection. */
@@ -103,12 +108,46 @@ export interface CustomViewConfig {
103
108
  /** OPTIONAL, default-open nav-permission predicate (EC-240 / AD-18). See {@link BuiltinViewConfig.visible}. */
104
109
  visible?: NavPermission;
105
110
  }
111
+ /**
112
+ * Derive a stable, deterministic id from the CMS focus (overridable via `config.id`).
113
+ *
114
+ * The id is the route segment, the rail's active-state key AND the override key
115
+ * (EC-311), so it must be injective: two focuses deriving one id means
116
+ * `resolveOverrides` drops the later view. The derived shapes:
117
+ *
118
+ * | focus | id |
119
+ * | ---------------------------- | ------------------------- |
120
+ * | none | `cms` |
121
+ * | `collection: 'page'` (leaf) | `cms:page` |
122
+ * | `collections: ['a','b']` | `cms:a-b` |
123
+ * | `collections: ['page']` | `cms-group:page` |
124
+ *
125
+ * EC-468 — why the one-item group is its own namespace: `cms:${join(L)}` of a
126
+ * single-element list IS that element, so the auto-generated PARENT took the very id
127
+ * its own auto-expanded child derives (`cmsCollectionLeaf` → `cmsView({ collection })`).
128
+ * Both landed in the one flat override list `resolveSidebar` feeds `resolveOverrides`,
129
+ * so the child was dropped with a `duplicate-override-id` error and the parent rendered
130
+ * childless — a forced collision no config could avoid. Nothing else changes: the leaf
131
+ * and the multi-collection join keep their exact ids (consumer rails route on them).
132
+ *
133
+ * EC-477 — `-` is a SEPARATOR here, and a collection id may legally contain one, so a
134
+ * collection literally named `a-b` derives the same `cms:a-b` as a `['a','b']` group.
135
+ * That ambiguity is NOT closed by constraining collection ids (a permanent, repo-wide
136
+ * naming ban to fix a two-config coincidence, when hyphenated ids are ordinary — see
137
+ * the task's Done); it is DETECTED where both facts are known, by
138
+ * {@link checkSidebarIdKeyspace}, which raises a `derived-id-collision` the moment a
139
+ * group's join equals a real collection id. The escape hatch is unchanged: an explicit
140
+ * `nav.cms({ id })` always wins, and the derivation below is untouched — consumer rails
141
+ * route on these ids.
142
+ */
143
+ export declare function cmsViewId(config: CmsViewConfig): string;
106
144
  /**
107
145
  * Build a CMS view — the default `cms` view, or a focused instance. Every instance
108
146
  * shares one surface ({@link CmsScreen}); `collections` narrows the sidebar + index
109
147
  * to a subset, `collection` is a single-collection lens that opens straight into
110
148
  * that collection's list. The id is derived from the focus (deterministic, stable
111
- * across rail reorders) unless `id` is given — so two focused views never collide.
149
+ * across rail reorders) unless `id` is given — see {@link cmsViewId} for the derived
150
+ * shapes and the one keyspace ambiguity that survives.
112
151
  */
113
152
  export declare function cmsView(config?: CmsViewConfig): AppViewModule;
114
153
  /** The components registry browser view. */
@@ -155,7 +194,15 @@ export declare function extensionsView(config?: BuiltinViewConfig): AppViewModul
155
194
  export declare function settingsView(config?: BuiltinViewConfig): AppViewModule;
156
195
  /** The legacy `builder` redirect — a hidden, routable view (so `/builder?page=` still lands). */
157
196
  export declare function builderView(): AppViewModule;
158
- /** Build a fully custom view (the `nav.view(...)` escape hatch). */
197
+ /**
198
+ * Build a fully custom view (the `nav.view(...)` escape hatch).
199
+ *
200
+ * With no `id` the id is a SLUG of the label, and that slug carries a
201
+ * {@link DerivedViewId} marker (EC-476): a label is a display string a developer picks
202
+ * freely, so `label: 'CMS'` deriving `cms` is a coincidence, not a request to replace
203
+ * the built-in CMS surface. {@link checkSidebarIdKeyspace} refuses the takeover and
204
+ * says so; an explicit `id` carries no marker and keeps the documented replacement.
205
+ */
159
206
  export declare function customView(config: CustomViewConfig): AppViewModule;
160
207
  /**
161
208
  * The studio's built-in views in canonical rail order (components · CMS · routes ·
@@ -165,11 +212,57 @@ export declare function customView(config: CustomViewConfig): AppViewModule;
165
212
  * when a consumer's rail omits one.
166
213
  */
167
214
  export declare const DEFAULT_VIEWS: readonly AppViewModule[];
215
+ /**
216
+ * Every id the engine already owns — the replacement targets of the sidebar seam,
217
+ * INCLUDING the hidden `builder` redirect (it is in the keyspace even though the stock
218
+ * rail does not list it, so a derived id can swallow it just the same).
219
+ */
220
+ export declare const BUILTIN_VIEW_IDS: ReadonlySet<string>;
221
+ /** The outcome of the keyspace check: the entries that may still act as overrides, plus the findings. */
222
+ export interface SidebarIdKeyspace {
223
+ /**
224
+ * The views to resolve as overrides — an ACCIDENTAL built-in takeover removed, so the
225
+ * engine keeps its own block (the EC-236 contract for a rejected entry). Everything
226
+ * else passes through untouched, including the merely-ambiguous EC-477 case.
227
+ */
228
+ views: AppViewModule[];
229
+ /** What was found — visible at the rail foot, never silent. */
230
+ issues: CustomizationIssue[];
231
+ }
232
+ /**
233
+ * EC-476/477 — guard the sidebar id keyspace against ids that were DERIVED rather than
234
+ * declared, and landed somewhere that already means something.
235
+ *
236
+ * The seam's replacement rule is id equality (`resolveOverrides`: an entry whose id is a
237
+ * default's id replaces it). That is exactly right for an id the developer TYPED, and
238
+ * exactly wrong for one a builder computed — and the generic core cannot tell them
239
+ * apart, because by the time an entry reaches it, an id is just a string. So the rule
240
+ * lives HERE, next to the derivations that produce it ({@link slugify},
241
+ * {@link cmsViewId}) and next to {@link DEFAULT_VIEWS}, the set it has to protect.
242
+ *
243
+ * Two derived shapes, two different remedies — because the cost of being wrong differs:
244
+ *
245
+ * - **A slugged LABEL landing on a built-in id (EC-476) is REJECTED** (`error`). Losing
246
+ * a whole built-in surface — the CMS, Assets, the users roster — to a coincidence of
247
+ * spelling is far worse than one custom view not appearing, and the developer's fix is
248
+ * a one-line explicit `id`. The built-in is kept, so nothing disappears.
249
+ * - **A collection JOIN equal to a real collection id (EC-477) is KEPT** (`warning`).
250
+ * Nothing here is a takeover: the group's id is legitimate, it is merely no longer
251
+ * unique to it, so dropping the view would destroy a rail item the developer
252
+ * explicitly declared in order to report a naming clash. Reporting is the whole fix.
253
+ *
254
+ * Pure and deterministic — the house "extract the core, test the core" shape.
255
+ * `builtinIds` and `collections` are parameters so the rule is testable against a
256
+ * synthetic keyspace, not just the live one.
257
+ */
258
+ export declare function checkSidebarIdKeyspace(views: readonly AppViewModule[], collections?: readonly CollectionDef[], builtinIds?: ReadonlySet<string>): SidebarIdKeyspace;
168
259
  export declare const nav: {
169
260
  /**
170
261
  * The CMS view. `nav.cms()` / `nav.cms({ collections })` is a PARENT that nests its
171
262
  * collections in the rail (each a focused `cms:<id>` lens); `nav.cms({ collection })`
172
263
  * is a single-collection LEAF; `nav.cms({ children })` supplies explicit children.
264
+ * A one-item `collections` group is a parent whose id is `cms-group:<id>`, distinct
265
+ * from its single child by construction (EC-468 — see {@link cmsViewId}).
173
266
  */
174
267
  readonly cms: (config?: CmsViewConfig) => SidebarViewNode;
175
268
  /** The component registry browser. */
@@ -193,46 +286,44 @@ export declare const nav: {
193
286
  readonly separator: () => import("./module.js").SidebarSeparatorNode;
194
287
  };
195
288
  /**
196
- * The CMS schema source for a project mount (bug fix, playground-collections):
197
- * a hydrated session wins (Convex), then a kit's imported schema (EC-119), then —
198
- * in playground mode only — the active project's config-declared collections
199
- * (AD-11, `resolveDeploymentCollections`). Without this last fallback, a non-kit
200
- * playground project has no hydrated session (`null`, by design — see
201
- * `project-session.tsx`) and no kit, so `CmsProvider` fell through to the core's
202
- * hardcoded `sampleFilterableCollections` fixture instead of the project's own
203
- * on-disk schema. Convex mode's first-run `null` is untouched — it still seeds
204
- * from the fixture there, as before. `playground` defaults to the live
205
- * {@link isPlaygroundMode} but is overridable so this stays unit testable
206
- * without stubbing the env (same seam as `resolveBackendConfig`).
207
- */
208
- export declare function resolveCmsInitialCollections(cms: HydratedCms | null, kit: InstantiatedKit | undefined, playground?: boolean): CollectionDef[] | undefined;
209
- /**
210
- * The CMS documents source for a project mount (EC-424) — the document-side
211
- * twin of {@link resolveCmsInitialCollections}, same ladder: a hydrated session
212
- * wins (Convex), then a kit's imported documents, then — in playground mode
213
- * only, and only when a config project actually resolved collections — the
214
- * engine's own `sampleDocuments` fixture RECONCILED against them.
289
+ * The CMS schema source for a project mount: a hydrated session wins (Convex),
290
+ * then a kit's imported schema (EC-119), then the active project's
291
+ * config-declared collections (AD-11, `resolveDeploymentCollections`).
292
+ *
293
+ * EC-479 — every rung is the PROJECT's own schema and there is no rung after
294
+ * them. The config rung used to be gated on playground mode, because outside it
295
+ * the `undefined` return let `CmsProvider` fall through to `cms-core`'s
296
+ * `sampleFilterableCollections` fixture — the engine's demo schema, shown as the
297
+ * project's, offering `filterable` fields the project's real backend would
298
+ * reject. With that fallback retired the gate has nothing left to protect: the
299
+ * Convex adapter is itself constructed with `resolveDeploymentCollections()`
300
+ * (`lib/persistence/index.ts`), so config collections are what connected mode
301
+ * serves too. Dropping it also DISSOLVES the EC-467 two-source divergence — the
302
+ * section rail reads `resolveDeploymentCollections()` directly, and now so does
303
+ * the store's last rung, so the two can no longer disagree.
304
+ */
305
+ export declare function resolveCmsInitialCollections(cms: HydratedCms | null, kit: InstantiatedKit | undefined): CollectionDef[] | undefined;
306
+ /**
307
+ * The CMS documents source for a project mount — the document-side twin of
308
+ * {@link resolveCmsInitialCollections}: a hydrated session wins (Convex), then a
309
+ * kit's imported documents, then **nothing**.
215
310
  *
216
- * Without this, a config project whose OWN schema diverges from the shared
217
- * fixture's localization (e.g. this repo's demo declares `post.slug` localized,
218
- * EC-315, while the engine's `postCollection` fixture deliberately does not)
219
- * left `CmsProvider` falling straight through to the un-reconciled
220
- * `sampleDocuments` default — the CMS surfaces' `initialCollections` already
221
- * reflected the config schema (via the fallback above), but `initialDocuments`
222
- * did not, so every seeded post showed EC-390's shape-aware validation's
223
- * `plain-value` warning + `missing-localized-value` error, standing noise a
224
- * project never actually wrote.
311
+ * EC-479 — the third rung used to be the engine's own `sampleDocuments` fixture,
312
+ * reconciled against the project's schema (EC-424). Reconciling it was already an
313
+ * admission that the content was foreign: EC-424 existed only because the demo
314
+ * declares `post.slug` localized while the shared fixture stores it plain, so the
315
+ * seeded posts carried standing validation noise no project had written. The
316
+ * honest fix is not a better reconciliation but no foreign content at all — a
317
+ * studio whose config did not arrive now shows an empty CMS and a visible issue
318
+ * at the rail, not six documents about Ada Lovelace wearing the project's schema.
225
319
  *
226
- * This is the STATIC-baseline twin of `reconcileSeedDocumentLocalization`
227
- * (`@elytracms/core/persistence`, wired into the seeded persistence adapter for
228
- * Convex-mode/delivery reads): no adapter round-trip, so the EC-138/EC-320
229
- * synchronous-first-render contract (`project-session.tsx`: "CMS surfaces keep
230
- * their fixture baseline") is untouched. `collections` defaults to the SAME
231
- * resolution `resolveCmsInitialCollections` returns, so the two stay in lockstep
232
- * by construction; only reconciles when that resolved to a non-empty config
233
- * schema (env-only playground, with nothing config-declared, is unaffected).
320
+ * The playground keeps its world through the ordinary path, not through here: its
321
+ * seeded in-memory adapter IS a persisted backend behind the operations seam
322
+ * (AD-17), so `CmsSessionGate` hydrates it and the first rung answers. What that
323
+ * adapter seeds is `persistence/seed.ts`'s business — EC-475 (project-defined seed
324
+ * content) owns it.
234
325
  */
235
- export declare function resolveCmsInitialDocuments(cms: HydratedCms | null, kit: InstantiatedKit | undefined, collections?: CollectionDef[] | undefined): CmsDocument[] | undefined;
326
+ export declare function resolveCmsInitialDocuments(cms: HydratedCms | null, kit: InstantiatedKit | undefined): CmsDocument[] | undefined;
236
327
  /**
237
328
  * The CMS locale config for a project mount (EC-357 finding #3). Locales are
238
329
  * structure and structure is code: when the active `elytra.config.ts` project
@@ -240,10 +331,16 @@ export declare function resolveCmsInitialDocuments(cms: HydratedCms | null, kit:
240
331
  * wiring only applied the project's locale settings on a HYDRATED session, so a
241
332
  * playground boot — or a connected FIRST RUN with nothing persisted yet — fell
242
333
  * back to a kit's locales or the core's sample `en/de` fixture and the switcher
243
- * showed `EN·/DE` against a `locales: ['de']` config. Without a config project
244
- * (env-only wiring) the previous ladder is unchanged: hydrated project settings,
245
- * then the kit's locales, then the sample fixture (via `CmsProvider`'s default).
334
+ * showed `EN·/DE` against a `locales: ['de']` config.
335
+ *
336
+ * EC-479/EC-483 — the ladder is now TOTAL, and every rung is the project's own:
337
+ * config-declared locales, then (hydrated) its persisted settings, then a kit's,
338
+ * then its settings regardless. A `Project` only ever exists for a config-declared
339
+ * project (`studio-context.tsx`: `configProjects()`), so `project.settings` is
340
+ * config-derived too — which is what lets the CMS session require a locale instead
341
+ * of borrowing the engine's `en/de` fixture. The default locale is the literal
342
+ * storage key for every localized value, so a guessed one is a data migration.
246
343
  * `configLocales` is a parameter purely so the ladder is unit-testable (same seam
247
344
  * pattern as {@link resolveCmsInitialCollections}).
248
345
  */
249
- export declare function resolveCmsInitialLocaleConfig(cms: HydratedCms | null, kit: InstantiatedKit | undefined, project: Project, configLocales?: LocaleConfig | undefined): LocaleConfig | undefined;
346
+ export declare function resolveCmsInitialLocaleConfig(cms: HydratedCms | null, kit: InstantiatedKit | undefined, project: Project, configLocales?: LocaleConfig | undefined): LocaleConfig;