@elytracms/studio 0.0.101 → 0.0.103

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 (90) hide show
  1. package/convex/asset-meta-patch.test.ts +7 -27
  2. package/convex/assets.ts +6 -41
  3. package/convex/cms.ts +2 -29
  4. package/convex/delivery.ts +2 -2
  5. package/convex/ingest.test.ts +54 -21
  6. package/convex/ingest.ts +52 -15
  7. package/convex/record-functions.ts +15 -82
  8. package/convex/redirects.ts +11 -116
  9. package/convex/schema.ts +4 -23
  10. package/convex/triggers.ts +7 -14
  11. package/dist/components/ai/ai-chat-panel.js +8 -5
  12. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  13. package/dist/components/ai/translate-document-dialog.d.ts +43 -0
  14. package/dist/components/ai/translate-document-dialog.js +104 -36
  15. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  16. package/dist/components/ai/translate-skipped.d.ts +19 -3
  17. package/dist/components/ai/translate-skipped.js +34 -7
  18. package/dist/components/ai/translate-skipped.js.map +1 -1
  19. package/dist/components/assets/asset-detail.js +1 -2
  20. package/dist/components/assets/asset-detail.js.map +1 -1
  21. package/dist/components/assets/asset-metadata.d.ts +2 -9
  22. package/dist/components/assets/asset-metadata.js +3 -10
  23. package/dist/components/assets/asset-metadata.js.map +1 -1
  24. package/dist/components/assets/asset-search.js +1 -1
  25. package/dist/components/assets/asset-search.js.map +1 -1
  26. package/dist/components/assets/asset-selection.js +2 -2
  27. package/dist/components/assets/asset-selection.js.map +1 -1
  28. package/dist/components/assets/assets-collection-view-source.d.ts +2 -3
  29. package/dist/components/assets/assets-collection-view-source.js +2 -3
  30. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  31. package/dist/components/builder/__fixtures__/reuse-race.d.ts +30 -0
  32. package/dist/components/builder/__fixtures__/reuse-race.js +77 -0
  33. package/dist/components/builder/__fixtures__/reuse-race.js.map +1 -0
  34. package/dist/components/builder/composition-field-editor.js +3 -3
  35. package/dist/components/builder/composition-field-editor.js.map +1 -1
  36. package/dist/components/builder/reuse-context.d.ts +2 -1
  37. package/dist/components/builder/reuse-context.js +5 -20
  38. package/dist/components/builder/reuse-context.js.map +1 -1
  39. package/dist/components/builder/reuse.d.ts +18 -11
  40. package/dist/components/builder/reuse.js +12 -14
  41. package/dist/components/builder/reuse.js.map +1 -1
  42. package/dist/components/cms/asset-field.js +1 -3
  43. package/dist/components/cms/asset-field.js.map +1 -1
  44. package/dist/components/cms/cell.d.ts +2 -2
  45. package/dist/components/cms/cell.js +8 -18
  46. package/dist/components/cms/cell.js.map +1 -1
  47. package/dist/components/cms/detail-editor.js +2 -1
  48. package/dist/components/cms/detail-editor.js.map +1 -1
  49. package/dist/components/cms/field-control.js +13 -1
  50. package/dist/components/cms/field-control.js.map +1 -1
  51. package/dist/lib/ai/chat-context.d.ts +6 -2
  52. package/dist/lib/ai/chat-context.js +11 -5
  53. package/dist/lib/ai/chat-context.js.map +1 -1
  54. package/dist/lib/ai/knowledge.d.ts +9 -9
  55. package/dist/lib/ai/knowledge.js +22 -0
  56. package/dist/lib/ai/knowledge.js.map +1 -1
  57. package/dist/lib/ai/tools.d.ts +8 -1
  58. package/dist/lib/ai/tools.js +33 -15
  59. package/dist/lib/ai/tools.js.map +1 -1
  60. package/dist/lib/ai/translate-apply.d.ts +69 -0
  61. package/dist/lib/ai/translate-apply.js +36 -0
  62. package/dist/lib/ai/translate-apply.js.map +1 -0
  63. package/dist/lib/ai/translate-document.d.ts +101 -7
  64. package/dist/lib/ai/translate-document.js +189 -54
  65. package/dist/lib/ai/translate-document.js.map +1 -1
  66. package/dist/lib/ai/update-fields.js +5 -12
  67. package/dist/lib/ai/update-fields.js.map +1 -1
  68. package/dist/lib/ai/writer-form.d.ts +93 -1
  69. package/dist/lib/ai/writer-form.js +389 -37
  70. package/dist/lib/ai/writer-form.js.map +1 -1
  71. package/dist/lib/assets/upload.d.ts +1 -2
  72. package/dist/lib/assets/upload.js +1 -2
  73. package/dist/lib/assets/upload.js.map +1 -1
  74. package/dist/lib/engine-version.d.ts +1 -1
  75. package/dist/lib/engine-version.js +1 -1
  76. package/dist/lib/engine-version.js.map +1 -1
  77. package/dist/lib/i18n/messages.de.js +54 -4
  78. package/dist/lib/i18n/messages.de.js.map +1 -1
  79. package/dist/lib/i18n/messages.en.d.ts +44 -1
  80. package/dist/lib/i18n/messages.en.js +58 -4
  81. package/dist/lib/i18n/messages.en.js.map +1 -1
  82. package/dist/lib/operations/cms-persistence.d.ts +20 -10
  83. package/dist/lib/operations/cms-persistence.js +11 -3
  84. package/dist/lib/operations/cms-persistence.js.map +1 -1
  85. package/dist/lib/persistence/convex-adapter.js +7 -2
  86. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  87. package/package.json +3 -3
  88. package/convex/asset-alt-migration.test.ts +0 -127
  89. package/convex/redirect-migration.test.ts +0 -205
  90. package/convex/version-kind-stamping.test.ts +0 -118
@@ -3,7 +3,6 @@ import type { GenericId } from 'convex/values'
3
3
  import type { RegisteredMutation, RegisteredQuery } from 'convex/server'
4
4
  import {
5
5
  PersistenceValidationError,
6
- matchesRecordQuery,
7
6
  orderRecords,
8
7
  recordSlots,
9
8
  recordWireRow,
@@ -65,33 +64,6 @@ type RecordDoc = {
65
64
  updatedAt: string
66
65
  }
67
66
 
68
- /**
69
- * TOLERANT READ (marked; EC-616's redirect move, the EC-587 precedent): a kind whose rows
70
- * used to live in a table of their own declares where, so a deployment whose functions
71
- * moved but whose migration has not run yet serves exactly what it served before — no
72
- * gap. Reads union the legacy rows a record does not shadow; a save of a legacy-only id
73
- * lifts it (the record is created from the legacy columns, the legacy row deleted, one
74
- * transaction); a remove deletes both. Writes NEVER go to the legacy table. Removed with
75
- * the legacy table once its migration is reported run on every deployment (a gated item).
76
- */
77
- export interface LegacyRecordSource {
78
- /** The legacy row for one id: its columns and stamps, and a handle to delete it. */
79
- find(ctx: QueryCtx | MutationCtx, recordId: string): Promise<LegacyRecord | null>
80
- /** Every legacy row (the tables are small editorial sets: read whole). */
81
- list(ctx: QueryCtx | MutationCtx): Promise<LegacyRecord[]>
82
- /** Delete one legacy row (after its record exists). */
83
- remove(ctx: MutationCtx, record: LegacyRecord): Promise<void>
84
- }
85
-
86
- export interface LegacyRecord {
87
- recordId: string
88
- columns: RecordColumns
89
- createdAt: string
90
- updatedAt: string
91
- /** The legacy row's own id, opaque to the factory. */
92
- handle: unknown
93
- }
94
-
95
67
  /** A `PersistenceValidationError` from the shared rule, as the `ConvexError` the client maps back. */
96
68
  function asConvexValidation(error: unknown): unknown {
97
69
  if (!(error instanceof PersistenceValidationError)) return error
@@ -161,47 +133,26 @@ function indexedDoc(doc: RecordDoc): IndexedRecord & { doc: RecordDoc } {
161
133
  }
162
134
 
163
135
  /**
164
- * The wire rows of one kind for a query — the records, plus (while a legacy source is
165
- * declared) the legacy rows no record shadows, matched and ordered by the SAME pure
166
- * functions the in-memory store uses. The one list reader: the kind's `list` and any
167
- * server-side reader of the kind (the delivery snapshot) go through it.
136
+ * The wire rows of one kind for a query, matched and ordered by the store's query. The one
137
+ * list reader: the kind's `list` and any server-side reader of the kind (the delivery
138
+ * snapshot) go through it.
168
139
  */
169
140
  export async function listRecordWire(
170
141
  ctx: QueryCtx | MutationCtx,
171
142
  kind: AnyRecordKind,
172
143
  resolved: ResolvedRecordQuery,
173
- legacy?: LegacyRecordSource,
174
144
  ): Promise<RecordWireRow[]> {
175
- if (!legacy) return (await queryRecordDocs(ctx, kind, resolved)).map((doc) => recordDocWire(kind, doc))
176
- // TOLERANT READ (marked): the union is read unbounded and ordered once, here.
177
- const { limit: _limit, ...unbounded } = resolved
178
- const docs = await queryRecordDocs(ctx, kind, { ...unbounded, order: 'asc' })
179
- const shadowed = new Set(docs.map((doc) => doc.recordId))
180
- const legacyRows = (await legacy.list(ctx))
181
- .filter((row) => !shadowed.has(row.recordId))
182
- .map((row) => ({
183
- id: row.recordId,
184
- slots: recordSlots(kind, row.columns, row.createdAt),
185
- columns: row.columns,
186
- wire: recordWireRow(kind, row.recordId, row.columns, row.createdAt, row.updatedAt),
187
- }))
188
- .filter((row) => matchesRecordQuery(row, resolved))
189
- // Legacy rows predate every record, so they lead in insertion order.
190
- const union = [...legacyRows, ...docs.map((doc) => ({ ...indexedDoc(doc), wire: recordDocWire(kind, doc) }))]
191
- return orderRecords(kind, union, resolved).map((row) => row.wire)
145
+ return (await queryRecordDocs(ctx, kind, resolved)).map((doc) => recordDocWire(kind, doc))
192
146
  }
193
147
 
194
- /** One record's wire row by id — the record, else (while declared) its legacy row. */
148
+ /** One record's wire row by id. */
195
149
  export async function getRecordWire(
196
150
  ctx: QueryCtx | MutationCtx,
197
151
  kind: AnyRecordKind,
198
152
  recordId: string,
199
- legacy?: LegacyRecordSource,
200
153
  ): Promise<RecordWireRow | null> {
201
154
  const doc = await findRecordDoc(ctx, kind, recordId)
202
- if (doc) return recordDocWire(kind, doc)
203
- const old = legacy ? await legacy.find(ctx, recordId) : null // TOLERANT READ (marked)
204
- return old ? recordWireRow(kind, old.recordId, old.columns, old.createdAt, old.updatedAt) : null
155
+ return doc ? recordDocWire(kind, doc) : null
205
156
  }
206
157
 
207
158
  /**
@@ -214,14 +165,10 @@ export async function saveRecord(
214
165
  recordId: string,
215
166
  write: Readonly<Record<string, unknown>>,
216
167
  stamps: { createdAt: string; updatedAt: string },
217
- legacy?: LegacyRecordSource,
218
168
  ): Promise<RecordWireRow> {
219
169
  const existing = await findRecordDoc(ctx, kind, recordId)
220
- // TOLERANT READ (marked): a legacy-only record is edited from its legacy columns.
221
- const lifted = !existing && legacy ? await legacy.find(ctx, recordId) : null
222
- const stored = existing?.data ?? lifted?.columns
223
- const columns = rethrowAsConvex(() => resolveRecordSave(kind, recordId, stored, write))
224
- const createdAt = existing?.createdAt ?? lifted?.createdAt ?? stamps.createdAt
170
+ const columns = rethrowAsConvex(() => resolveRecordSave(kind, recordId, existing?.data, write))
171
+ const createdAt = existing?.createdAt ?? stamps.createdAt
225
172
  const slots = recordSlots(kind, columns, createdAt)
226
173
  if (existing) {
227
174
  // `key1`/`key2` are always written: an edit that empties a key unsets its slot.
@@ -243,7 +190,6 @@ export async function saveRecord(
243
190
  createdAt,
244
191
  updatedAt: stamps.updatedAt,
245
192
  })
246
- if (lifted && legacy) await legacy.remove(ctx, lifted)
247
193
  }
248
194
  return recordWireRow(kind, recordId, columns, createdAt, stamps.updatedAt)
249
195
  }
@@ -256,14 +202,8 @@ export interface RecordFunctions {
256
202
  remove: RegisteredMutation<'public', Record<string, unknown>, Promise<void>>
257
203
  }
258
204
 
259
- export interface RecordFunctionsOptions {
260
- /** TOLERANT READ (marked) — see {@link LegacyRecordSource}. Only a kind that moved declares one. */
261
- legacy?: LegacyRecordSource
262
- }
263
-
264
205
  /** The public CRUD functions of one record kind — see the module header. */
265
- export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFunctionsOptions = {}): RecordFunctions {
266
- const { legacy } = options
206
+ export function defineRecordFunctions(kind: AnyRecordKind): RecordFunctions {
267
207
  const idArg = { [kind.idKey]: v.string() }
268
208
  const columnArgs = Object.fromEntries(
269
209
  [...Object.keys(kind.fields), 'meta', 'metaPatch'].map((column) => [column, v.optional(v.any())]),
@@ -274,14 +214,10 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
274
214
  handler: async (ctx, args: Record<string, unknown>) => {
275
215
  await requireCapability(ctx, kind.access.write)
276
216
  const { [kind.idKey]: recordId, createdAt, updatedAt, ...write } = args
277
- return saveRecord(
278
- ctx,
279
- kind,
280
- recordId as string,
281
- write,
282
- { createdAt: createdAt as string, updatedAt: updatedAt as string },
283
- legacy,
284
- )
217
+ return saveRecord(ctx, kind, recordId as string, write, {
218
+ createdAt: createdAt as string,
219
+ updatedAt: updatedAt as string,
220
+ })
285
221
  },
286
222
  })
287
223
 
@@ -289,7 +225,7 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
289
225
  args: idArg,
290
226
  handler: async (ctx, args: Record<string, unknown>) => {
291
227
  await requireCapability(ctx, kind.access.read)
292
- return getRecordWire(ctx, kind, args[kind.idKey] as string, legacy)
228
+ return getRecordWire(ctx, kind, args[kind.idKey] as string)
293
229
  },
294
230
  })
295
231
 
@@ -304,7 +240,7 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
304
240
  handler: async (ctx, args: RecordQuery) => {
305
241
  await requireCapability(ctx, kind.access.read)
306
242
  const resolved = rethrowAsConvex(() => resolveRecordQuery(kind, args))
307
- return listRecordWire(ctx, kind, resolved, legacy)
243
+ return listRecordWire(ctx, kind, resolved)
308
244
  },
309
245
  })
310
246
 
@@ -315,9 +251,6 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
315
251
  const recordId = args[kind.idKey] as string
316
252
  const doc = await findRecordDoc(ctx, kind, recordId)
317
253
  if (doc) await ctx.db.delete(doc._id)
318
- // TOLERANT READ (marked): a legacy row of the same id goes too, or it would resurface.
319
- const old = legacy ? await legacy.find(ctx, recordId) : null
320
- if (old && legacy) await legacy.remove(ctx, old)
321
254
  },
322
255
  })
323
256
 
@@ -1,18 +1,7 @@
1
- import { internalMutationGeneric } from 'convex/server'
2
- import type { MutationBuilder } from 'convex/server'
3
- import {
4
- PersistenceValidationError,
5
- recordColumnsOf,
6
- recordSlots,
7
- redirectRecordKind,
8
- resolveRecordQuery,
9
- resolveRecordSave,
10
- } from '@elytracms/core/persistence'
1
+ import { recordEntryFromWire, redirectRecordKind, resolveRecordQuery } from '@elytracms/core/persistence'
11
2
  import type { RedirectEntry } from '@elytracms/core/persistence'
12
- import type { DataModel, MutationCtx, QueryCtx } from './functions'
13
- import { defineRecordFunctions, findRecordDoc, listRecordWire } from './record-functions'
14
- import type { LegacyRecord, LegacyRecordSource } from './record-functions'
15
- import { recordEntryFromWire } from '@elytracms/core/persistence'
3
+ import type { QueryCtx } from './functions'
4
+ import { defineRecordFunctions, listRecordWire } from './record-functions'
16
5
 
17
6
  /**
18
7
  * Editorial redirects persistence (EC-303) — since EC-616 the redirect RECORD KIND over
@@ -22,112 +11,18 @@ import { recordEntryFromWire } from '@elytracms/core/persistence'
22
11
  * `export * from '@elytracms/studio/convex/redirects'` line are unchanged. Content-gated
23
12
  * (a viewer reads, an editor writes), NOT admin-only — redirects are content.
24
13
  *
25
- * ## The move from the `redirects` table (stored data — both production consumers hold real redirects)
26
- *
27
- * The EC-587 precedent, with no gap:
28
- *
29
- * - **writes go to `records`** (kind `redirect`); nothing writes the legacy table again;
30
- * - **reads union the legacy rows no record shadows** — the TOLERANT READ below, marked:
31
- * a deployment with these functions pushed and the migration NOT yet run serves the
32
- * same redirects it served before (`list`, `get`, the delivery snapshot);
33
- * - a save of a legacy-only redirect lifts it (record from its columns, legacy row
34
- * deleted, one transaction); a remove deletes both;
35
- * - **`redirects:migrateLegacy`** moves every legacy row in one idempotent internal
36
- * mutation and reports counts — run per deployment (both consumers + the prod harness:
37
- * owner-run); the legacy table and this tolerant read are dropped together once it is
38
- * reported run everywhere (a gated item, like EC-587's alt column).
14
+ * The legacy `redirects` table, its tolerant read and its one-time move
15
+ * (`redirects:migrateLegacy`) are deleted (EC-708), after every deployment's scan read 0
16
+ * legacy rows: `records` is the one home.
39
17
  */
40
-
41
- type LegacyRedirectDoc = DataModel['redirects']['document']
42
-
43
- /** TOLERANT READ (marked) — the legacy `redirects` table as the kind's legacy source. */
44
- const legacyRedirects: LegacyRecordSource = {
45
- find: async (ctx, recordId) => {
46
- const row = await ctx.db
47
- .query('redirects')
48
- .withIndex('by_redirect', (q) => q.eq('redirectId', recordId))
49
- .first()
50
- return row ? legacyRecord(row) : null
51
- },
52
- list: async (ctx) => (await ctx.db.query('redirects').withIndex('by_redirect').collect()).map(legacyRecord),
53
- remove: async (ctx, record) => {
54
- await ctx.db.delete(record.handle as LegacyRedirectDoc['_id'])
55
- },
56
- }
57
-
58
- function legacyRecord(row: LegacyRedirectDoc): LegacyRecord {
59
- return {
60
- recordId: row.redirectId,
61
- columns: recordColumnsOf(redirectRecordKind, row),
62
- createdAt: row.createdAt,
63
- updatedAt: row.updatedAt,
64
- handle: row._id,
65
- }
66
- }
67
-
68
- export const { save, get, list, remove } = defineRecordFunctions(redirectRecordKind, { legacy: legacyRedirects })
18
+ export const { save, get, list, remove } = defineRecordFunctions(redirectRecordKind)
69
19
 
70
20
  /**
71
- * Every stored redirect of this deployment (records + the tolerant legacy read), ordered
72
- * by `from` — the ONE server-side reader; the delivery snapshot projects it through
73
- * `readLiveRedirects` (EC-590 / EC-627 / EC-628: the one live-redirect rule).
21
+ * Every stored redirect of this deployment, ordered by `from` — the ONE server-side
22
+ * reader; the delivery snapshot projects it through `readLiveRedirects` (EC-590 / EC-627
23
+ * / EC-628: the one live-redirect rule).
74
24
  */
75
25
  export async function loadRedirectEntries(ctx: QueryCtx, projectId: string): Promise<RedirectEntry[]> {
76
- const rows = await listRecordWire(ctx, redirectRecordKind, resolveRecordQuery(redirectRecordKind), legacyRedirects)
26
+ const rows = await listRecordWire(ctx, redirectRecordKind, resolveRecordQuery(redirectRecordKind))
77
27
  return rows.map((row) => recordEntryFromWire(redirectRecordKind, row, projectId))
78
28
  }
79
-
80
- /**
81
- * EC-616 — move every legacy `redirects` row into `records` (kind `redirect`), the
82
- * redirect move's migration. Per row: a record of the same id already exists (a save
83
- * lifted it, or an earlier run) → the legacy row is dropped as shadowed; otherwise the
84
- * row's columns are written as a record CREATE through the one rule (`resolveRecordSave`
85
- * — a legacy row the current schema refuses is REPORTED and left where it is, still
86
- * served by the tolerant read, never dropped silently) and the legacy row deleted.
87
- * Idempotent: a second run scans nothing.
88
- *
89
- * Internal: an admin runs it from the CLI, once per deployment —
90
- * `npx convex run redirects:migrateLegacy` (no args). Built on the RAW internal-mutation
91
- * builder, deliberately outside the write-chokepoint triggers: the served redirect set is
92
- * identical before and after by construction (the tolerant read), so the move needs no
93
- * revalidation — through the triggers it would schedule two sweeps per row (an insert
94
- * into `records`, a delete from `redirects`). One transaction: Convex bounds it at 8192
95
- * written rows, far above any editorial redirect set (a production site's import holds 192).
96
- */
97
- export const migrateLegacy = (internalMutationGeneric as MutationBuilder<DataModel, 'internal'>)({
98
- args: {},
99
- handler: async (ctx) => {
100
- const legacy = await ctx.db.query('redirects').withIndex('by_redirect').collect()
101
- const moved: string[] = []
102
- const shadowed: string[] = []
103
- const refused: Array<{ redirectId: string; message: string }> = []
104
- for (const row of legacy) {
105
- const record = legacyRecord(row)
106
- if (await findRecordDoc(ctx as unknown as MutationCtx, redirectRecordKind, record.recordId)) {
107
- await ctx.db.delete(row._id)
108
- shadowed.push(record.recordId)
109
- continue
110
- }
111
- let columns: Record<string, unknown>
112
- try {
113
- columns = resolveRecordSave(redirectRecordKind, record.recordId, undefined, record.columns)
114
- } catch (error) {
115
- if (!(error instanceof PersistenceValidationError)) throw error
116
- refused.push({ redirectId: record.recordId, message: error.message })
117
- continue
118
- }
119
- const slots = recordSlots(redirectRecordKind, columns, record.createdAt)
120
- await ctx.db.insert('records', {
121
- kind: redirectRecordKind.kind,
122
- recordId: record.recordId,
123
- data: columns,
124
- at: slots.at,
125
- createdAt: record.createdAt,
126
- updatedAt: record.updatedAt,
127
- })
128
- await ctx.db.delete(row._id)
129
- moved.push(record.recordId)
130
- }
131
- return { scanned: legacy.length, moved: moved.length, shadowed: shadowed.length, refused, movedIds: moved }
132
- },
133
- })
package/convex/schema.ts CHANGED
@@ -239,10 +239,10 @@ export default defineSchema({
239
239
  origin: v.optional(v.string()),
240
240
  // EC-585: which mutation recorded the row — 'publish' (`publishDocument`, the only
241
241
  // kind `publishDocumentVersion` pins) or 'snapshot' (`saveDocumentVersion`: a restore
242
- // point). Optional ONLY for rows recorded before EC-585 (read through
243
- // `isPinnableVersion`, which keeps a kind-less `label: 'published'` row pinnable). A
244
- // top-level scalar beside the folded snapshot: it adds no nesting depth.
245
- kind: v.optional(v.union(v.literal('publish'), v.literal('snapshot'))),
242
+ // point). Required (EC-708: every deployment stamped and read 0 kind-less rows first;
243
+ // the push refuses a row without it). A top-level scalar beside the folded snapshot: it
244
+ // adds no nesting depth.
245
+ kind: v.union(v.literal('publish'), v.literal('snapshot')),
246
246
  createdAt: v.string(),
247
247
  })
248
248
  .index('by_doc', ['collection', 'docId'])
@@ -259,7 +259,6 @@ export default defineSchema({
259
259
  byteSize: v.number(),
260
260
  width: v.optional(v.number()),
261
261
  height: v.optional(v.number()),
262
- alt: v.optional(v.string()),
263
262
  /** Editable display title (EC-152; persisted for parity with in-memory — EC-153). */
264
263
  title: v.optional(v.string()),
265
264
  storageKey: v.string(),
@@ -306,24 +305,6 @@ export default defineSchema({
306
305
  .index('by_storage', ['storageKey'])
307
306
  .index('by_thumb_storage', ['thumb.storageKey']),
308
307
 
309
- /**
310
- * Editorial redirects (EC-303) — 301/302 forwards an editor owns, stored as
311
- * DATA (not config) so they change dynamically without a deploy. The delivery
312
- * snapshot projects enabled entries onto the routing `RedirectRecord` the
313
- * catch-all resolves; static/infra redirects stay native (next.config/vercel).
314
- */
315
- redirects: defineTable({
316
- redirectId: v.string(),
317
- from: v.string(),
318
- to: v.string(),
319
- permanent: v.boolean(),
320
- enabled: v.boolean(),
321
- // Schema-driven additive-field bag (EC-311 Tier 2) — see the `assets.meta` bag.
322
- meta: v.optional(v.any()),
323
- createdAt: v.string(),
324
- updatedAt: v.string(),
325
- }).index('by_redirect', ['redirectId']),
326
-
327
308
  /**
328
309
  * The generic RECORD STORE (EC-616) — one row per record of a DECLARED record kind
329
310
  * (`defineRecordKind` in `@elytracms/core/persistence`): the engine's own kinds and
@@ -34,8 +34,9 @@ import { scheduleRevalidate } from './revalidate'
34
34
  *
35
35
  * - `cmsDocuments` — only when the live pin (`publishedVersion`) moves
36
36
  * (`publish` / `unpublish`), or a pinned row is deleted (`delete`);
37
- * - `records` of the redirect kind (EC-616; EC-510 before it on the `redirects` table) —
38
- * every write (`redirects`; the row IS the live state); no other record kind;
37
+ * - `records` of the redirect kind (EC-616; EC-510 before it on the legacy `redirects`
38
+ * table, deleted by EC-708) — every write (`redirects`; the row IS the live state); no
39
+ * other record kind;
39
40
  * - `assets` (EC-579) — every edit and delete (`asset`); a create fires nothing.
40
41
  *
41
42
  * The snapshot reads one more table, `documentVersions`, but only the version a
@@ -127,22 +128,14 @@ triggers.register('cmsDocuments', async (ctx, change) => {
127
128
  * no-op, and a redirect save is an explicit editorial action, not a keystroke
128
129
  * (EC-501's cost argument does not transfer).
129
130
  */
130
- triggers.register('redirects', async (ctx) => {
131
- await scheduleRevalidate(ctx, { kind: 'redirects' })
132
- })
133
-
134
131
  /**
135
132
  * The generic record store (EC-616). Of all record kinds, ONLY the engine's redirect
136
133
  * kind is projected onto the live wire (`delivery.ts` reads it through
137
134
  * `loadRedirectEntries`), so only a write to a `redirect` row revalidates — with exactly
138
- * the event the legacy `redirects` table fired above (EC-510: every write, the receiver
139
- * answers with the sweep). Every other kind — an addon's submissions, analytics rows —
140
- * is never on the wire and fires nothing; a kind that starts reaching the snapshot
141
- * belongs in this check.
142
- *
143
- * Since the move, a redirect write is a `records` write; the legacy table's trigger
144
- * above still fires for the transitional deletes (a save lifting a legacy-only redirect
145
- * deletes its legacy row: two sweeps for that one save, until the migration ran).
135
+ * the event the legacy `redirects` table fired (EC-510: every write, the receiver answers
136
+ * with the sweep). Every other kind — an addon's submissions, analytics rows — is never
137
+ * on the wire and fires nothing; a kind that starts reaching the snapshot belongs in this
138
+ * check.
146
139
  */
147
140
  triggers.register('records', async (ctx, change) => {
148
141
  const kind = (change.newDoc ?? change.oldDoc)?.kind
@@ -3,13 +3,13 @@ import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
3
3
  import { MessageSquarePlus, X } from 'lucide-react';
4
4
  import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
5
5
  import { useChat } from '@ai-sdk/react';
6
- import { createCollectionRegistry, resolveDocumentTitle, } from '@elytracms/core/cms-core';
6
+ import { createCollectionRegistry } from '@elytracms/core/cms-core';
7
7
  import { Button, Heading, Row, Spacer, Text } from '@elytracms/studio/ui';
8
8
  import { translate } from '../../lib/i18n/index.js';
9
9
  import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
10
10
  import { resolveDeploymentAi } from '../../lib/workspace/index.js';
11
11
  import { getStudioOperations } from '../../lib/operations/index.js';
12
- import { DEFAULT_MAX_STEPS, buildChatContext, chatReviewReducer, clearConversation, createChatCandidateStore, createChatToolExecutor, guardAiRouteFetch, initialChatReviewState, introducedErrors, isAiRouteNotMountedError, renderCurrentDocumentSection, resolveAiConfig, resolveChatTarget, resolveKnowledge, withStepCap, } from '../../lib/ai/index.js';
12
+ import { DEFAULT_MAX_STEPS, buildChatContext, chatReviewReducer, clearConversation, createChatCandidateStore, createChatToolExecutor, guardAiRouteFetch, initialChatReviewState, introducedErrors, isAiRouteNotMountedError, knowledgeFieldOf, modelFacingTitle, renderCurrentDocumentSection, resolveAiConfig, resolveChatTarget, resolveKnowledge, withStepCap, } from '../../lib/ai/index.js';
13
13
  import { getAiCurrentDocument } from '../../lib/ai-current-document.js';
14
14
  import { useCms } from '../cms/cms-store.js';
15
15
  import { AssistantTurn, ChatErrorNote, Conversation, ConversationEmpty, PromptInput, ToolStatus, UserTurn, WorkingIndicator, describeToolPart, messageText, segmentMessageParts, } from './chat-ui.js';
@@ -84,11 +84,14 @@ export function AiChatPanel() {
84
84
  if (!result.ok)
85
85
  return [];
86
86
  const target = cms.getCollection(collection);
87
+ // EC-741: the title the model reads — and the query is matched against — comes
88
+ // from the projected values, so an excluded field is neither shown nor probeable.
89
+ const lookupManifest = cms.validateOptions.lookupManifest;
87
90
  return result.value
88
91
  .map((rec) => ({
89
92
  id: rec.document.id,
90
93
  title: target
91
- ? resolveDocumentTitle(target, localeValues(rec.document), rec.document.id)
94
+ ? modelFacingTitle(target, localeValues(rec.document), rec.document.id, lookupManifest ? { lookupManifest } : {})
92
95
  : rec.document.id,
93
96
  }))
94
97
  .filter((r) => matches(query, r.title, r.id));
@@ -142,8 +145,8 @@ export function AiChatPanel() {
142
145
  const result = await operations.documents.list({ projectId, collection: source.collectionId });
143
146
  if (!result.ok)
144
147
  return [];
145
- const target = cms.getCollection(source.collectionId);
146
- const field = source.field ?? target?.fields.find((f) => f.type === 'text' || f.type === 'richText')?.name;
148
+ // EC-741: the default prose field is never an `ai.exclude` one.
149
+ const field = knowledgeFieldOf(source, cms.getCollection(source.collectionId));
147
150
  return result.value
148
151
  .map((rec) => {
149
152
  const values = localeValues(rec.document);