@elytracms/studio 0.0.95 → 0.0.97

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 (104) hide show
  1. package/convex/cms.ts +272 -16
  2. package/convex/delivery-folded-drift.test.ts +104 -0
  3. package/convex/delivery.ts +24 -8
  4. package/convex/depth-codec-lookalike.test.ts +192 -0
  5. package/convex/fixture-accounts.test.ts +305 -0
  6. package/convex/fixtureAccounts.ts +298 -0
  7. package/convex/incarnation.test.ts +149 -0
  8. package/convex/ingest.test.ts +145 -3
  9. package/convex/ingest.ts +123 -17
  10. package/convex/receipt-races.test.ts +184 -0
  11. package/convex/records-zod-copy.test.ts +76 -0
  12. package/convex/schema.ts +20 -1
  13. package/convex/strict-delete.test.ts +129 -0
  14. package/convex/strict-reference-race.test.ts +139 -0
  15. package/dist/components/ai/ai-chat-panel.js +15 -2
  16. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  17. package/dist/components/ai/translate-document-action.d.ts +6 -0
  18. package/dist/components/ai/translate-document-action.js.map +1 -1
  19. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  20. package/dist/components/ai/translate-document-dialog.js +5 -5
  21. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  22. package/dist/components/assets/asset-detail.js +5 -2
  23. package/dist/components/assets/asset-detail.js.map +1 -1
  24. package/dist/components/assets/asset-locale.d.ts +7 -0
  25. package/dist/components/assets/asset-locale.js +16 -2
  26. package/dist/components/assets/asset-locale.js.map +1 -1
  27. package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
  28. package/dist/components/assets/asset-meta-drift-notice.js +22 -0
  29. package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
  30. package/dist/components/assets/asset-meta-label.d.ts +2 -1
  31. package/dist/components/assets/asset-meta-label.js +4 -2
  32. package/dist/components/assets/asset-meta-label.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +33 -13
  34. package/dist/components/assets/asset-metadata.js +46 -22
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.d.ts +6 -2
  37. package/dist/components/assets/asset-search.js +15 -5
  38. package/dist/components/assets/asset-search.js.map +1 -1
  39. package/dist/components/assets/assets-collection-view-source.d.ts +5 -3
  40. package/dist/components/assets/assets-collection-view-source.js +7 -5
  41. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  42. package/dist/components/builder/canvas-rebase.d.ts +36 -12
  43. package/dist/components/builder/canvas-rebase.js +13 -3
  44. package/dist/components/builder/canvas-rebase.js.map +1 -1
  45. package/dist/components/builder/canvas-render-context.js +5 -4
  46. package/dist/components/builder/canvas-render-context.js.map +1 -1
  47. package/dist/components/builder/composition-field-editor.d.ts +12 -6
  48. package/dist/components/builder/composition-field-editor.js +6 -5
  49. package/dist/components/builder/composition-field-editor.js.map +1 -1
  50. package/dist/components/cms/asset-picker-core.d.ts +2 -2
  51. package/dist/components/cms/asset-picker-core.js +2 -2
  52. package/dist/components/cms/asset-picker-core.js.map +1 -1
  53. package/dist/components/cms/asset-picker.js +3 -1
  54. package/dist/components/cms/asset-picker.js.map +1 -1
  55. package/dist/components/cms/canvas-commit.d.ts +6 -0
  56. package/dist/components/cms/canvas-commit.js +5 -1
  57. package/dist/components/cms/canvas-commit.js.map +1 -1
  58. package/dist/components/cms/cms-store.d.ts +24 -8
  59. package/dist/components/cms/cms-store.js +21 -14
  60. package/dist/components/cms/cms-store.js.map +1 -1
  61. package/dist/components/cms/collection-table.js +4 -11
  62. package/dist/components/cms/collection-table.js.map +1 -1
  63. package/dist/components/cms/detail-editor.js +2 -2
  64. package/dist/components/cms/detail-editor.js.map +1 -1
  65. package/dist/components/cms/draft-commit.d.ts +16 -3
  66. package/dist/components/cms/draft-commit.js +3 -2
  67. package/dist/components/cms/draft-commit.js.map +1 -1
  68. package/dist/components/cms/use-document-draft.d.ts +107 -10
  69. package/dist/components/cms/use-document-draft.js +157 -42
  70. package/dist/components/cms/use-document-draft.js.map +1 -1
  71. package/dist/components/collection-view/cms-collection-view-source.js +6 -4
  72. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  73. package/dist/components/components/transfer-dialog.js +6 -4
  74. package/dist/components/components/transfer-dialog.js.map +1 -1
  75. package/dist/lib/ai/chat-state.d.ts +7 -0
  76. package/dist/lib/ai/chat-state.js.map +1 -1
  77. package/dist/lib/ai/tools.d.ts +8 -0
  78. package/dist/lib/ai/tools.js +4 -0
  79. package/dist/lib/ai/tools.js.map +1 -1
  80. package/dist/lib/cms-views/view-model.d.ts +15 -8
  81. package/dist/lib/cms-views/view-model.js +19 -5
  82. package/dist/lib/cms-views/view-model.js.map +1 -1
  83. package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
  84. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
  85. package/dist/lib/collection-view/collection-view-source.d.ts +18 -7
  86. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  87. package/dist/lib/data/collection-content.js +5 -0
  88. package/dist/lib/data/collection-content.js.map +1 -1
  89. package/dist/lib/engine-version.d.ts +1 -1
  90. package/dist/lib/engine-version.js +1 -1
  91. package/dist/lib/engine-version.js.map +1 -1
  92. package/dist/lib/i18n/messages.de.js +15 -1
  93. package/dist/lib/i18n/messages.de.js.map +1 -1
  94. package/dist/lib/i18n/messages.en.d.ts +13 -0
  95. package/dist/lib/i18n/messages.en.js +16 -1
  96. package/dist/lib/i18n/messages.en.js.map +1 -1
  97. package/dist/lib/operations/cms-persistence.d.ts +119 -35
  98. package/dist/lib/operations/cms-persistence.js +441 -91
  99. package/dist/lib/operations/cms-persistence.js.map +1 -1
  100. package/dist/lib/persistence/convex-adapter.js +80 -8
  101. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  102. package/dist/lib/persona/labels.js +2 -0
  103. package/dist/lib/persona/labels.js.map +1 -1
  104. package/package.json +4 -3
@@ -2,41 +2,81 @@ import type { CmsDocument, CollectionDef, DocumentPatch, DocumentRef, PropFieldL
2
2
  import type { CmsDocumentRecord } from '@elytracms/core/persistence';
3
3
  import type { OperationError, OperationsClient } from './operations-package.js';
4
4
  /**
5
- * CMS session → operations persistence (EC-141 → EC-595 part A2): the PATCH QUEUE.
5
+ * CMS session → operations persistence (EC-141 → EC-595 part A2 → EC-647): the PATCH QUEUE.
6
6
  *
7
7
  * The CMS session keeps documents as optimistic, synchronous state; every write is
8
8
  * mirrored onto the operations layer through the revisioned aggregate's two verbs
9
9
  * (EC-595, ratified 2026-10-24): `documents.patch` for editing, `documents.replace`
10
- * for whole-document writers (create, import). There is no document diffing anywhere:
11
- * every writer EMITS the patches it made at its source (the form's field path, the
12
- * canvas's node patches, the AI proposal's field changes, bulk set-field, transfer) and
13
- * hands them to {@link CmsPersister.commit}.
10
+ * for whole-document writers (create, import, the slot alias). There is no document
11
+ * diffing anywhere: every writer EMITS the patches it made at its source (the form's
12
+ * field path, the canvas's node patches, the AI proposal's field changes, bulk
13
+ * set-field, transfer) and hands them to {@link CmsPersister.commit}.
14
14
  *
15
- * **Per document, the persister keeps** the server row it last adopted (`base` — its
16
- * revision is what a patch is made against), the committed-but-unacknowledged patches
17
- * (each with the revision the user's view was at when it was made), and the refused
18
- * paths awaiting a decision. It derives the session's view of the document from them:
15
+ * **Per document, the persister keeps** the server row it last adopted (`base`), the
16
+ * committed-but-unacknowledged writes (each with the revision it was AUTHORED against),
17
+ * the refused paths awaiting a decision, and the writes it made itself (the own-write
18
+ * ledger). It derives the session's view of the document from them:
19
19
  *
20
20
  * display = base.document ⊕ unresolved conflicts (mine) ⊕ in-flight ⊕ queued
21
21
  *
22
22
  * by re-applying the patches with THE merge (`applyDocumentPatches`, `since: []`) — the
23
- * same pure function both stores run. So local edits win on their paths until they are
24
- * flushed (EC-595 point 7), and a server row adopted underneath them (a write result,
25
- * a subscription push, a conflict's `current`) shows everywhere else at once.
23
+ * same pure function both stores run (a queued whole-document write resets the view to
24
+ * its snapshot at its place in the order). So local edits win on their paths until they
25
+ * are flushed (EC-595 point 7), and a server row adopted underneath them (a write
26
+ * result, a subscription push, a conflict's `current`) shows everywhere else at once.
26
27
  *
27
- * **Base revisions are per patch** (not per call): a patch is checked against the
28
- * changes in `(its base, current]`, so a colleague's change that arrived (by subscribe)
29
- * AFTER the user started typing in a field is still detected on that field. A patch
30
- * whose path our own successful write just covered moves its base to that write's
31
- * revision — the server guarantees no other session touched the path in between, so
32
- * a second edit of a field during the first one's flight never conflicts with itself.
33
- * Consecutive runs of equal base are sent in commit order, one `documents.patch` call
34
- * per run.
28
+ * **A value and the revision it was authored against travel together (EC-647).** Every
29
+ * view this persister shows is registered with the revision it is at
30
+ * ({@link CmsPersister.revisionOfView} — the shown document object IS the token), and a
31
+ * writer hands each patch over WITH the revision of the view it was authored on
32
+ * (`bases`, required). The persister never substitutes a newer revision for it — not at
33
+ * commit (the old `bases ?? tracked revision` default), not at send (the old
34
+ * `queued.base ?? entry.base.revision`), not in a whole-document `replace` (which read
35
+ * the tracked revision at SEND time — Codex F10), and not for an untracked document
36
+ * (which sent at whatever revision a later read returned). A write that cannot name the
37
+ * revision it was authored against is refused, visibly — it never borrows one.
38
+ *
39
+ * - **A view's token is per position (EC-667, Codex F18).** A view that shows a pending
40
+ * write is a COMPOSITE: no single underlying revision names it. Each shown view is
41
+ * registered with its provenance at display time — the base's revision, or under a
42
+ * pending whole-document write its snapshot's authoring revision, and over that each
43
+ * own unacknowledged write shown (queued, in flight, an open conflict's "mine") with
44
+ * the base it was authored at. `revisionOfView(view, path)` is the revision the content
45
+ * at `path` was authored against (the earliest of the writes covering it);
46
+ * `revisionOfView(view)` — a whole-document writer — the EARLIEST of all. The record is
47
+ * immutable, so it outlives the pending write: an edit started on a view that showed a
48
+ * snapshot keeps the snapshot's revision after the snapshot is refused or fails
49
+ * (before EC-667 the composite was named by the base's revision — a colleague's change
50
+ * adopted underneath a pending snapshot lent the edit its revision, and the edit,
51
+ * authored on the old content, overwrote it).
52
+ * - **A position under one of this session's own unacknowledged writes** (queued, in
53
+ * flight, or an open conflict's "mine") was authored against THAT write's base: the
54
+ * view showed mine there, not a colleague's change that may sit underneath it. The
55
+ * later write takes the earlier base, so a colleague's hidden change is still refused
56
+ * on it (same-field supersession has always kept the earlier base for this reason).
57
+ * - **The own-write ledger.** A base may be LIFTED, at send time, across revisions this
58
+ * persister wrote — and only those: across a run of consecutive own revisions right
59
+ * after it (nothing foreign happened in between), or to an own write's revision when
60
+ * the patch only touches positions that write covered and the write was sent at or
61
+ * below the patch's base (the store refused every foreign change of those positions in
62
+ * between, or the write would not have applied). Without it, the session's second
63
+ * edit of a position, made at the revision its first was authored against, would be
64
+ * refused by its own first write.
65
+ * - **The provisional view** (`undefined` authoring revision): a document whose view
66
+ * has no revision yet — our own create in flight, or a document the session never
67
+ * tracked (a staged transfer). Writes on it are based on the revision our create
68
+ * landed at, or — untracked — on a read of the document that EQUALS the view they were
69
+ * authored on; anything else is refused as a conflict per position.
70
+ *
71
+ * Consecutive runs of equal (lifted) base are sent in commit order, one
72
+ * `documents.patch` call per run; a queued whole-document write is sent in its place in
73
+ * that order.
35
74
  *
36
75
  * **A refused path is a conflict ON THAT FIELD** ({@link DocumentConflict}): the
37
76
  * session shows "changed by X — take theirs / keep mine"; until the user decides, the
38
77
  * field keeps showing mine (and further edits of it replace mine without being sent);
39
- * keep-mine re-sends at the current base, take-theirs drops mine. Never a
78
+ * keep-mine re-sends at the current base — the ONE deliberate "at the current base"
79
+ * write: the user chose it after seeing theirs; take-theirs drops mine. Never a
40
80
  * whole-document dialog, never a silent overwrite.
41
81
  */
42
82
  /** One refused patch awaiting the user's decision (EC-595 binding point 5). */
@@ -67,7 +107,7 @@ export type CommitOutcome = {
67
107
  readonly status: 'refused';
68
108
  readonly conflicts: readonly DocumentConflict[];
69
109
  }
70
- /** A transport / authorization failure (also reported through `onError`). */
110
+ /** A transport / authorization failure, or a write that named no authoring revision (also reported through `onError`). */
71
111
  | {
72
112
  readonly status: 'failed';
73
113
  readonly error: OperationError;
@@ -75,29 +115,58 @@ export type CommitOutcome = {
75
115
  export interface CmsPersister {
76
116
  /** Adopt a server row as this session's base for its document (hydration, a create result). */
77
117
  track(row: TrackedRow): void;
78
- /** The revision the session's view of a document is at (`undefined` = untracked). */
118
+ /**
119
+ * The revision of the server row the session last adopted for a document (`undefined` =
120
+ * untracked). A document deleted since still answers its removed lifetime's last
121
+ * revision (EC-673): a publish conditioned on it is refused, never sent unconditionally
122
+ * into a re-creation this session never showed — so whether to create or replace is
123
+ * never decided on it alone ({@link importWrite} decides on the row the session shows).
124
+ */
79
125
  revisionOf(ref: DocumentRef): number | undefined;
80
126
  /** The session's view of a tracked document (base ⊕ local), or `undefined`. */
81
127
  documentOf(ref: DocumentRef): CmsDocument | undefined;
82
128
  /**
83
- * Commit patches a writer made (at the session's current view) and flush them. The
84
- * session's view updates synchronously (via `onDocument`); the patches persist in
85
- * order through `documents.patch`. `view` — the session's current document, the
86
- * provisional base of a document this session holds no revision for (a staged
87
- * transfer); its revision is read before the first send. `bases` — per patch, the
88
- * revision the user's view was at when they STARTED that edit (an editor field typed
89
- * across a colleague's change); absent → the revision the view is at now.
129
+ * EC-647 — the revision a document value this persister SHOWED is at: every view it
130
+ * emits (`onDocument`, {@link documentOf}) and every row it tracks is registered with
131
+ * its revision, so a writer holding the value it displayed holds its revision too —
132
+ * the pair cannot drift apart. `undefined` for a value the persister never showed (a
133
+ * copy, a fixture), or a provisional view (our own create in flight, an untracked
134
+ * document).
135
+ *
136
+ * EC-667 — per position: `path` given, the revision the content the view shows AT
137
+ * `path` was authored against (an editor's first keystroke there); without it, the
138
+ * EARLIEST authoring revision of any position the view shows — the token of a
139
+ * whole-document writer (a replace, an import). A view showing a pending write is never
140
+ * named by the base's revision alone (see the module doc).
90
141
  */
91
- commit(ref: DocumentRef, patches: readonly DocumentPatch[], view?: CmsDocument, bases?: readonly (number | undefined)[]): Promise<CommitOutcome>;
142
+ revisionOfView(view: CmsDocument, path?: ValuePath): number | undefined;
143
+ /**
144
+ * Commit patches a writer made and flush them. The session's view updates
145
+ * synchronously (via `onDocument`); the patches persist in order through
146
+ * `documents.patch`. `bases` — REQUIRED, one per patch: the revision of the view the
147
+ * writer authored that patch against (an editor field: the revision its draft was at
148
+ * when the user STARTED that edit; the canvas: the revision of the value its delta is
149
+ * measured from; a table row: {@link revisionOfView} of the row it showed; an AI
150
+ * proposal: the revision of the read it was built from). `undefined` entries mean the
151
+ * view was provisional (see the module doc) — on a document this session tracks and
152
+ * did not create, such a patch is refused. `view` — the session's current document,
153
+ * the provisional view of a document this session holds no revision for (a staged
154
+ * transfer).
155
+ */
156
+ commit(ref: DocumentRef, patches: readonly DocumentPatch[], bases: readonly (number | undefined)[], view?: CmsDocument): Promise<CommitOutcome>;
92
157
  /** Adopt a server row one of the session's OWN direct writes returned (publish, restore). */
93
158
  adoptRecord(record: CmsDocumentRecord): void;
94
159
  /** Create a document (insert-if-absent: `documents.replace` `{ ifAbsent: true }`). */
95
160
  create(document: CmsDocument): void;
96
161
  /**
97
- * Write a whole document at the session's tracked revision (or insert-if-absent when
98
- * untracked): an import. A move since then is refused as a conflict, never overwritten.
162
+ * Write a whole document at the revision it was AUTHORED against (EC-647 — the
163
+ * snapshot and its revision travel together; Codex F10): an import, the slot alias. A
164
+ * move since then — by anyone but this session's own writes — is refused as a
165
+ * conflict, never overwritten. `revision` `undefined`: the snapshot was authored on a
166
+ * provisional view (our own pending create; insert-if-absent when untracked). Sent in
167
+ * its place among the session's queued patches.
99
168
  */
100
- replace(document: CmsDocument): void;
169
+ replace(document: CmsDocument, revision: number | undefined): void;
101
170
  deleteDocument(ref: DocumentRef): void;
102
171
  unpublishDocument(ref: DocumentRef): void;
103
172
  /**
@@ -125,7 +194,8 @@ export interface CmsPersisterOptions {
125
194
  onError: (error: OperationError) => void;
126
195
  /**
127
196
  * The session's view of a document changed (an optimistic commit, an adopted server
128
- * row, a resolved conflict) — `undefined` when the document is gone. Synchronous.
197
+ * row, a resolved conflict) — `undefined` when the document is gone. Synchronous. The
198
+ * document object is registered with its revision ({@link CmsPersister.revisionOfView}).
129
199
  */
130
200
  onDocument?: (ref: DocumentRef, document: CmsDocument | undefined) => void;
131
201
  /**
@@ -140,4 +210,18 @@ export interface CmsPersisterOptions {
140
210
  }
141
211
  /** What the session hydrated for a document: its value and the revision it is at. */
142
212
  export type TrackedRow = Pick<CmsDocumentRecord, 'document' | 'revision'>;
213
+ /**
214
+ * An import's whole-document write (EC-595 A2 → EC-647 → EC-673): a row the session SHOWS
215
+ * and holds a revision for is REPLACED at the revision of the row it showed (the one the
216
+ * import was judged against); any other row is CREATED (insert-if-absent).
217
+ *
218
+ * EC-673: the decision is the SHOWN row's, never the tracked revision alone. A document
219
+ * deleted earlier in the session is not shown, but its entry can still answer a
220
+ * revision — the editor's Delete and a colleague's delete keep the deleted lifetime's
221
+ * base, and the list's bulk Delete never tells the persister at all. Deciding on that
222
+ * revision sent the import down `replace`: queued on a deleted entry and skipped, or
223
+ * refused as unauthored with the deleted lifetime's values shown again. A replace with no
224
+ * shown row could never be right anyway — it names no revision it was authored against.
225
+ */
226
+ export declare function importWrite(persister: Pick<CmsPersister, 'revisionOf' | 'revisionOfView' | 'replace' | 'create'>, document: CmsDocument, shown: CmsDocument | undefined): void;
143
227
  export declare function createCmsPersister(options: CmsPersisterOptions): CmsPersister;