@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.
- package/convex/asset-meta-patch.test.ts +7 -27
- package/convex/assets.ts +6 -41
- package/convex/cms.ts +2 -29
- package/convex/delivery.ts +2 -2
- package/convex/ingest.test.ts +54 -21
- package/convex/ingest.ts +52 -15
- package/convex/record-functions.ts +15 -82
- package/convex/redirects.ts +11 -116
- package/convex/schema.ts +4 -23
- package/convex/triggers.ts +7 -14
- package/dist/components/ai/ai-chat-panel.js +8 -5
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +43 -0
- package/dist/components/ai/translate-document-dialog.js +104 -36
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -3
- package/dist/components/ai/translate-skipped.js +34 -7
- package/dist/components/ai/translate-skipped.js.map +1 -1
- package/dist/components/assets/asset-detail.js +1 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +2 -9
- package/dist/components/assets/asset-metadata.js +3 -10
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +1 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.js +2 -2
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +2 -3
- package/dist/components/assets/assets-collection-view-source.js +2 -3
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/__fixtures__/reuse-race.d.ts +30 -0
- package/dist/components/builder/__fixtures__/reuse-race.js +77 -0
- package/dist/components/builder/__fixtures__/reuse-race.js.map +1 -0
- package/dist/components/builder/composition-field-editor.js +3 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +2 -1
- package/dist/components/builder/reuse-context.js +5 -20
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +18 -11
- package/dist/components/builder/reuse.js +12 -14
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +1 -3
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/cell.d.ts +2 -2
- package/dist/components/cms/cell.js +8 -18
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/detail-editor.js +2 -1
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/field-control.js +13 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +6 -2
- package/dist/lib/ai/chat-context.js +11 -5
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/knowledge.d.ts +9 -9
- package/dist/lib/ai/knowledge.js +22 -0
- package/dist/lib/ai/knowledge.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -1
- package/dist/lib/ai/tools.js +33 -15
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-apply.d.ts +69 -0
- package/dist/lib/ai/translate-apply.js +36 -0
- package/dist/lib/ai/translate-apply.js.map +1 -0
- package/dist/lib/ai/translate-document.d.ts +101 -7
- package/dist/lib/ai/translate-document.js +189 -54
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/update-fields.js +5 -12
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +93 -1
- package/dist/lib/ai/writer-form.js +389 -37
- package/dist/lib/ai/writer-form.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +1 -2
- package/dist/lib/assets/upload.js +1 -2
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +54 -4
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +44 -1
- package/dist/lib/i18n/messages.en.js +58 -4
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +20 -10
- package/dist/lib/operations/cms-persistence.js +11 -3
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +7 -2
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/package.json +3 -3
- package/convex/asset-alt-migration.test.ts +0 -127
- package/convex/redirect-migration.test.ts +0 -205
- package/convex/version-kind-stamping.test.ts +0 -118
|
@@ -41,11 +41,28 @@
|
|
|
41
41
|
* `writerLockOf` locks (`ai.exclude` or `form.readOnly`, and everything below it) keeps
|
|
42
42
|
* its CURRENT value. Leaving it out of the written value, or echoing it back unchanged,
|
|
43
43
|
* is no write; any other value is reported as a `locked` problem (create drops it, edit
|
|
44
|
-
* refuses — the same two policies as the id leash)
|
|
44
|
+
* refuses — the same two policies as the id leash);
|
|
45
|
+
* - **the locked-content judge** (EC-738) — a write to a WRITABLE position must not erase
|
|
46
|
+
* the locked content below it, which its own walk never visits: a holder omitted below
|
|
47
|
+
* the write's root keeps its current value whole (omission is no write — and the model
|
|
48
|
+
* may not know the holder holds anything, EC-741); an explicit removal (a root `null` /
|
|
49
|
+
* omitted value, a nested `null`, a shorter list, a removed asset entry) of a holder whose
|
|
50
|
+
* current value holds a defined locked value is a `locked` problem, located at the holder,
|
|
51
|
+
* and the holder keeps its current value. A repeater whose items hold one cannot be
|
|
52
|
+
* shortened at all (items are matched by index). Translate reuses the judge
|
|
53
|
+
* ({@link withLockedFrom}, {@link lockedChanges});
|
|
54
|
+
* - **the unreadable value** (EC-748, Codex r9 F44) — a stored value the traversal cannot
|
|
55
|
+
* read (a holder of the other cardinality after a schema edit, a repeater item that is no
|
|
56
|
+
* object, a body that is no v2 sequence, a body read without the prop lookup, …) is
|
|
57
|
+
* judged by its DECLARATION (`declarationMayHold`, failing closed on bodies), never read
|
|
58
|
+
* as empty: the model read WITHHOLDS it when it may hold an excluded value, and a write
|
|
59
|
+
* that would change or clear it is REFUSED when it may hold a locked one — until a non-AI
|
|
60
|
+
* save stores it in its canonical form. A value the MODEL writes in a shape its
|
|
61
|
+
* declaration does not read is refused too (nothing below it could be leashed or judged).
|
|
45
62
|
*
|
|
46
63
|
* Pure and deterministic; no provider, no I/O.
|
|
47
64
|
*/
|
|
48
|
-
import { carryAssetPlacements, childPositions, fieldPath, fieldTypeOf, fieldValuePosition, isSequenceValue, rebuildFromChildren, referenceNode, seedObjectItem, sequenceComponentNodes, walkReferences, walkValuePositions, writerLockOf, } from '@elytracms/core/cms-core';
|
|
65
|
+
import { carryAssetPlacements, childPositions, declarationMayHold, fieldPath, fieldTypeOf, fieldValuePosition, isOpaquePosition, isSequenceValue, readAssetId, rebuildFromChildren, referenceNode, resolveDocumentTitle, seedObjectItem, selfTypeFieldValue, sequenceComponentNodes, walkReferences, walkValuePositions, writerLockOf, } from '@elytracms/core/cms-core';
|
|
49
66
|
import { parseRichTextValue, richTextValueToMarkdownProse } from '@elytracms/core/rich-text';
|
|
50
67
|
import { richTextValueFromModelInput } from './rich-text-input.js';
|
|
51
68
|
export const NO_HELD_REFERENCES = { documents: new Map(), assets: new Set() };
|
|
@@ -87,8 +104,12 @@ export function lockedWriteMessage(subjectText, lock) {
|
|
|
87
104
|
* which the placement carry and the composition leash read. `null` / `undefined` clears.
|
|
88
105
|
*/
|
|
89
106
|
export function fromWriterForm(position, current, ctx) {
|
|
107
|
+
return convertRoot(position, current, ctx, position.field.name);
|
|
108
|
+
}
|
|
109
|
+
/** One write, rooted at `position`; `root` names the top-level field in messages. */
|
|
110
|
+
function convertRoot(position, current, ctx, root) {
|
|
90
111
|
const problems = [];
|
|
91
|
-
const value = convert(position, current, ctx, problems,
|
|
112
|
+
const value = convert(position, current, ctx, problems, root, true);
|
|
92
113
|
return { value, problems };
|
|
93
114
|
}
|
|
94
115
|
/** {@link fromWriterForm} for a top-level field's value. */
|
|
@@ -137,23 +158,87 @@ function childKey(parent, child) {
|
|
|
137
158
|
? `node:${child.component.nodeId}:${child.field.name}`
|
|
138
159
|
: `at:${JSON.stringify(child.at.slice(parent.at.length))}`;
|
|
139
160
|
}
|
|
161
|
+
/** {@link childKey} for a value the traversal cannot read, in its own key space (EC-748). */
|
|
162
|
+
function opaqueChildKey(parent, child) {
|
|
163
|
+
return child.opaque === 'not-a-static-prop' && child.component
|
|
164
|
+
? `opaque:node:${child.component.nodeId}:${child.declaration.name}`
|
|
165
|
+
: `opaque:at:${JSON.stringify(child.at.slice(parent.at.length))}`;
|
|
166
|
+
}
|
|
167
|
+
/** The model-facing name of an unreadable value: its declaration's name, located when nested. */
|
|
168
|
+
function opaqueSubject(position, root) {
|
|
169
|
+
const name = `"${position.declaration.name}"`;
|
|
170
|
+
return position.at.length === 0 ? name : `${name} (at ${label(root, position.at)})`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* What a writer is told about a value it wrote in a shape the declaration does not read
|
|
174
|
+
* (EC-748): nothing below it could be converted, leashed or judged for locks, so it is not
|
|
175
|
+
* written — said in the shape the field does take.
|
|
176
|
+
*/
|
|
177
|
+
function unreadableWriteMessage(position, root) {
|
|
178
|
+
const what = `Field ${opaqueSubject(position, root)}`;
|
|
179
|
+
switch (position.opaque) {
|
|
180
|
+
case 'cardinality':
|
|
181
|
+
return Array.isArray(position.value)
|
|
182
|
+
? `${what} holds one value, not a list — send the value itself.`
|
|
183
|
+
: `${what} holds a list — send a list.`;
|
|
184
|
+
case 'not-an-object':
|
|
185
|
+
return `${what} takes an object of its declared sub-fields.`;
|
|
186
|
+
case 'not-an-asset':
|
|
187
|
+
return `${what} takes asset id string(s) from listAssets results.`;
|
|
188
|
+
case 'not-a-sequence':
|
|
189
|
+
return `${what} takes rich text as markdown, or the stored { version: 2, content } object.`;
|
|
190
|
+
case 'not-a-static-prop':
|
|
191
|
+
return `${what} is a component prop stored as no { value } member; the assistant cannot write it.`;
|
|
192
|
+
case 'no-prop-declarations':
|
|
193
|
+
return `${what} holds components whose props cannot be judged here: the project's components are not available.`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
140
196
|
/** Convert every child position of `pos` (its value already in a readable shape). */
|
|
141
197
|
function convertChildren(pos, current, ctx, problems, root) {
|
|
142
198
|
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
143
199
|
const currentChildren = new Map();
|
|
144
200
|
if (current !== undefined) {
|
|
145
201
|
for (const child of childPositions({ ...pos, value: current }, positionCtx)) {
|
|
146
|
-
|
|
202
|
+
if (isOpaquePosition(child))
|
|
203
|
+
currentChildren.set(opaqueChildKey(pos, child), child.value);
|
|
204
|
+
else
|
|
205
|
+
currentChildren.set(childKey(pos, child), child.value);
|
|
147
206
|
}
|
|
148
207
|
}
|
|
149
|
-
return rebuildFromChildren(pos, positionCtx, (child) => convert(child, currentChildren.get(childKey(pos, child)), ctx, problems, root)
|
|
208
|
+
return rebuildFromChildren(pos, positionCtx, (child) => convert(child, currentChildren.get(childKey(pos, child)), ctx, problems, root, false),
|
|
209
|
+
// EC-748: a value the model wrote in a shape its declaration does not read — nothing
|
|
210
|
+
// below it can be converted, leashed or judged for locks (a list written for a single
|
|
211
|
+
// object would reach a locked sub-field through the canonicaliser's unwrap). The stored
|
|
212
|
+
// value echoed back, unchanged, is no write; anything else is a problem, and not written.
|
|
213
|
+
(unreadable) => {
|
|
214
|
+
const stored = currentChildren.get(opaqueChildKey(pos, unreadable));
|
|
215
|
+
if (stored !== undefined && canonicalJson(stored) === canonicalJson(unreadable.value))
|
|
216
|
+
return unreadable.value;
|
|
217
|
+
problems.push({ at: unreadable.at, code: 'invalid-field-value', message: unreadableWriteMessage(unreadable, root) });
|
|
218
|
+
return undefined;
|
|
219
|
+
});
|
|
150
220
|
}
|
|
151
|
-
|
|
221
|
+
/**
|
|
222
|
+
* `isRoot` — whether `pos` is the write's root (the field, or the prop a node change
|
|
223
|
+
* names): there an omitted value CLEARS (`update-fields`: "`null` / omitted clears");
|
|
224
|
+
* below it an omitted holder of locked content is kept whole (EC-738).
|
|
225
|
+
*/
|
|
226
|
+
function convert(pos, current, ctx, problems, root, isRoot) {
|
|
152
227
|
// EC-716: a locked position keeps its current value — and so, by not being visited,
|
|
153
228
|
// does everything below it (the lock cascades as the form's `readOnly` does).
|
|
154
229
|
const lock = writerLockOf(pos.field);
|
|
155
|
-
if (lock === undefined)
|
|
156
|
-
|
|
230
|
+
if (lock === undefined) {
|
|
231
|
+
const written = convertWritable(pos, current, ctx, problems, root, isRoot);
|
|
232
|
+
// EC-748: a component prop is judged against its canonical form (THE drift issue, at prop
|
|
233
|
+
// depth), and a proposal's write does not re-canonicalise props (update-fields: it would
|
|
234
|
+
// rewrite every untouched node's wrappers) — so a prop the converter WRITES is stored in
|
|
235
|
+
// its canonical form here, by the write lens itself (`writeField` does it for a field): a
|
|
236
|
+
// multi-select's lone option becomes a list, an emptied list or sequence absent. A value
|
|
237
|
+
// kept as it is (`current`) is never rewritten.
|
|
238
|
+
if (pos.kind !== 'prop' || written === current || written === undefined)
|
|
239
|
+
return written;
|
|
240
|
+
return selfTypeFieldValue(pos.field, written, ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {});
|
|
241
|
+
}
|
|
157
242
|
if (pos.value === undefined || isEcho(pos, current, ctx, root))
|
|
158
243
|
return current;
|
|
159
244
|
problems.push({
|
|
@@ -164,6 +249,194 @@ function convert(pos, current, ctx, problems, root) {
|
|
|
164
249
|
});
|
|
165
250
|
return current;
|
|
166
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* {@link convert} for a WRITABLE position, judged for the locked content below it (EC-738).
|
|
254
|
+
* The conversion never visits a locked position its written value no longer holds (a
|
|
255
|
+
* cleared object, a shortened list, a removed asset entry), so the judge compares the locked
|
|
256
|
+
* values under the CURRENT value with those under the converted one.
|
|
257
|
+
*/
|
|
258
|
+
function convertWritable(pos, current, ctx, problems, root, isRoot) {
|
|
259
|
+
if (!isPresent(current))
|
|
260
|
+
return convertValue(pos, current, ctx, problems, root);
|
|
261
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
262
|
+
const before = lockedPositionsUnder({ ...pos, value: current }, positionCtx);
|
|
263
|
+
const held = [...before.values()].filter((entry) => isPresent(entry.value));
|
|
264
|
+
if (held.length === 0)
|
|
265
|
+
return convertValue(pos, current, ctx, problems, root);
|
|
266
|
+
// An omission below the write's root is no write (EC-716's rule for a locked position,
|
|
267
|
+
// applied to the holder of one): the holder keeps its current value, whole.
|
|
268
|
+
if (!isRoot && pos.value === undefined)
|
|
269
|
+
return current;
|
|
270
|
+
const next = convertValue(pos, current, ctx, problems, root);
|
|
271
|
+
const lost = lockedLosses(before, lockedPositionsUnder({ ...pos, value: next }, positionCtx));
|
|
272
|
+
// A repeater's items are matched by INDEX: removing one shifts every later item's
|
|
273
|
+
// writable content onto the locked values of the item before it — invisible to a per-key
|
|
274
|
+
// comparison, so any shortening of a repeater that holds locked content is refused.
|
|
275
|
+
const shortened = pos.field.type === 'object' &&
|
|
276
|
+
pos.field.multiple === true &&
|
|
277
|
+
Array.isArray(current) &&
|
|
278
|
+
(Array.isArray(next) ? next.length : 0) < current.length;
|
|
279
|
+
if (lost.length === 0 && !shortened)
|
|
280
|
+
return next;
|
|
281
|
+
const judged = lost.length > 0 ? lost : held;
|
|
282
|
+
problems.push({
|
|
283
|
+
at: pos.at,
|
|
284
|
+
code: 'invalid-field-value',
|
|
285
|
+
message: judged.some((entry) => entry.opaque !== undefined)
|
|
286
|
+
? unreadableLockedMessage(subject(pos, root), root, judged)
|
|
287
|
+
: lockedRemovalMessage(subject(pos, root), root, judged, shortened),
|
|
288
|
+
locked: true,
|
|
289
|
+
});
|
|
290
|
+
return current;
|
|
291
|
+
}
|
|
292
|
+
// --- The locked-content judge (EC-738) -----------------------------------------------
|
|
293
|
+
/** A value that is there (`undefined` / `null` — nothing to lose; the converter's own rule). */
|
|
294
|
+
function isPresent(value) {
|
|
295
|
+
return value !== undefined && value !== null;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The writer lock an unreadable value's declaration may hold (EC-748): `excluded` when it may
|
|
299
|
+
* hold an `ai.exclude` position, else `read-only` when it may hold a `form.readOnly` one, else
|
|
300
|
+
* none — over the declaration alone (`declarationMayHold`: a body may hold any component, so
|
|
301
|
+
* it may hold either; `excluded` wins, the stricter lock).
|
|
302
|
+
*/
|
|
303
|
+
export function declaredWriterLock(declaration) {
|
|
304
|
+
if (declarationMayHold(declaration, (field) => writerLockOf(field) === 'excluded'))
|
|
305
|
+
return 'excluded';
|
|
306
|
+
if (declarationMayHold(declaration, (field) => writerLockOf(field) === 'read-only'))
|
|
307
|
+
return 'read-only';
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
/** {@link lockedChildKey} for a value the traversal cannot read — its own key space (EC-748). */
|
|
311
|
+
function lockedOpaqueKey(parent, parentKey, child) {
|
|
312
|
+
if (child.opaque === 'not-a-static-prop' && child.component) {
|
|
313
|
+
return `opaque:node:${child.component.nodeId}:${child.declaration.name}`;
|
|
314
|
+
}
|
|
315
|
+
return `${parentKey}/opaque:${JSON.stringify(child.at.slice(parent.at.length))}`;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The key matching a child position across two values of one position: a component prop by
|
|
319
|
+
* its NODE (ids are unique in a document and survive a prose edit that shifts indices), an
|
|
320
|
+
* entry of an asset LIST by its asset id and occurrence (the carry's own matching,
|
|
321
|
+
* `carryAssetPlacements` — a reordered gallery keeps each placement), anything else by its
|
|
322
|
+
* location below the parent (a repeater item by index, a sub-field by name).
|
|
323
|
+
*/
|
|
324
|
+
function lockedChildKey(parent, parentKey, child) {
|
|
325
|
+
if (child.kind === 'prop' && child.component)
|
|
326
|
+
return `node:${child.component.nodeId}:${child.field.name}`;
|
|
327
|
+
const below = child.at.slice(parent.at.length);
|
|
328
|
+
if (parent.field.type === 'asset' && parent.field.multiple !== false && Array.isArray(parent.value)) {
|
|
329
|
+
const index = below[0];
|
|
330
|
+
const entries = parent.value;
|
|
331
|
+
const id = readAssetId(entries[index]);
|
|
332
|
+
const occurrence = entries.slice(0, index).filter((entry) => readAssetId(entry) === id).length;
|
|
333
|
+
return `${parentKey}/asset:${JSON.stringify(id)}#${occurrence}/${JSON.stringify(below.slice(1))}`;
|
|
334
|
+
}
|
|
335
|
+
return `${parentKey}/${JSON.stringify(below)}`;
|
|
336
|
+
}
|
|
337
|
+
/** Every topmost writer-locked position at or below `position`, by {@link lockedChildKey}. */
|
|
338
|
+
function lockedPositionsUnder(position, positionCtx) {
|
|
339
|
+
const out = new Map();
|
|
340
|
+
const visit = (pos, key) => {
|
|
341
|
+
const lock = writerLockOf(pos.field);
|
|
342
|
+
if (lock !== undefined) {
|
|
343
|
+
out.set(key, { value: pos.value, lock, at: pos.at });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
for (const child of childPositions(pos, positionCtx)) {
|
|
347
|
+
if (!isOpaquePosition(child)) {
|
|
348
|
+
visit(child, lockedChildKey(pos, key, child));
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// EC-748: what cannot be read is judged by its declaration — never "nothing locked here".
|
|
352
|
+
const lock = declaredWriterLock(child.declaration);
|
|
353
|
+
if (lock !== undefined)
|
|
354
|
+
out.set(lockedOpaqueKey(pos, key, child), { value: child.value, lock, at: child.at, opaque: child.opaque });
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
visit(position, '');
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
/** Whether `entry` sits in `other` unchanged at `key` (a defined value, the same JSON). */
|
|
361
|
+
function keptAt(other, key, entry) {
|
|
362
|
+
const now = other.get(key);
|
|
363
|
+
return now !== undefined && isPresent(now.value) && canonicalJson(now.value) === canonicalJson(entry.value);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The defined locked values of `before` that `after` does not hold, unchanged, at their key —
|
|
367
|
+
* and (EC-748) every unreadable value that may hold one on EITHER side and is not on the other
|
|
368
|
+
* unchanged: what it holds cannot be compared, so a change to it (or one written where none
|
|
369
|
+
* was) is counted lost — fail closed.
|
|
370
|
+
*/
|
|
371
|
+
function lockedLosses(before, after) {
|
|
372
|
+
const lost = [...before].flatMap(([key, entry]) => (isPresent(entry.value) && !keptAt(after, key, entry) ? [entry] : []));
|
|
373
|
+
const written = [...after].flatMap(([key, entry]) => entry.opaque !== undefined && isPresent(entry.value) && !keptAt(before, key, entry) ? [entry] : []);
|
|
374
|
+
return [...lost, ...written];
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* What a writer is told about a write that would change or clear a value stored in a shape
|
|
378
|
+
* its declaration cannot read, which may hold locked content (EC-748), located at the holder
|
|
379
|
+
* the model wrote. Its content is never named (it cannot be read — and excluded content may
|
|
380
|
+
* not be disclosed, EC-741); the repair is a save outside the assistant.
|
|
381
|
+
*/
|
|
382
|
+
export function unreadableLockedMessage(holder, root, locked) {
|
|
383
|
+
const excluded = locked.some((entry) => entry.lock === 'excluded');
|
|
384
|
+
const where = locked.filter((entry) => entry.opaque !== undefined && entry.at.length > 0).map((entry) => label(root, entry.at));
|
|
385
|
+
const at = !excluded && where.length > 0 ? ` (${where.join(', ')})` : '';
|
|
386
|
+
const what = excluded ? 'content the assistant must not change' : 'read-only content';
|
|
387
|
+
return `Field ${holder} holds a value stored in a shape its declaration cannot read${at} — it was saved under an earlier schema or written past the editor — and it may hold ${what}; the assistant cannot change, replace or clear it. Repair it outside the assistant: save the field again in the editor, in the shape its declaration reads; then the assistant can change its other values.`;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* What a writer is told about a write that would remove locked content (EC-738), located at
|
|
391
|
+
* the HOLDER the model wrote. Read-only content is named (the model reads it); excluded
|
|
392
|
+
* content never is (EC-741: the model never sees it, so naming its path would disclose it).
|
|
393
|
+
*/
|
|
394
|
+
export function lockedRemovalMessage(holder, root, locked, shortened) {
|
|
395
|
+
const excluded = locked.some((entry) => entry.lock === 'excluded');
|
|
396
|
+
const what = excluded
|
|
397
|
+
? 'content the assistant must not change'
|
|
398
|
+
: `read-only content (${locked.map((entry) => label(root, entry.at)).join(', ')}) — no one changes it in the studio, the assistant included`;
|
|
399
|
+
return shortened
|
|
400
|
+
? `Field ${holder} holds ${what}; the assistant cannot remove items from it (items are matched by position, so removing one would delete that content or move it onto another item). Keep every item and change only their other values.`
|
|
401
|
+
: `Field ${holder} holds ${what}; the assistant cannot clear or remove it. Change only its other values.`;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The value at `position` with every writer-locked position taken from `current` by key
|
|
405
|
+
* (EC-738, translate): where `current` holds that position — its value, an absent one
|
|
406
|
+
* included — the current one wins; a position `current` does not hold at all (a new item, a
|
|
407
|
+
* node it lacks) keeps `position`'s own. {@link lockedChanges} then says whether that kept
|
|
408
|
+
* the locked content as it is.
|
|
409
|
+
*/
|
|
410
|
+
export function withLockedFrom(position, current, ctx) {
|
|
411
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
412
|
+
const held = lockedPositionsUnder({ ...position, value: current }, positionCtx);
|
|
413
|
+
const rebuild = (pos, key) => {
|
|
414
|
+
if (writerLockOf(pos.field) !== undefined)
|
|
415
|
+
return held.has(key) ? held.get(key).value : pos.value;
|
|
416
|
+
// EC-748: a value `position` holds unreadably is kept as it is — nothing below it can be
|
|
417
|
+
// matched by key — and {@link lockedChanges} counts it lost unless `current` holds it too.
|
|
418
|
+
return rebuildFromChildren(pos, positionCtx, (child) => rebuild(child, lockedChildKey(pos, key, child)), (unreadable) => unreadable.value);
|
|
419
|
+
};
|
|
420
|
+
return rebuild(position, '');
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The locked values a write from `position.value` (the CURRENT value) to `next` would lose or
|
|
424
|
+
* create, judged both ways (EC-738, translate): `lost` — a defined locked value `next` does
|
|
425
|
+
* not hold unchanged; `created` — a defined locked value `next` holds where the current value
|
|
426
|
+
* holds none. EC-748: a value stored in a shape its declaration cannot read, which may hold a
|
|
427
|
+
* locked value, on EITHER side and not on the other unchanged, is `lost` (fail closed) — so a
|
|
428
|
+
* drifted target keeps its value whole, and a drifted source never copies its locked values
|
|
429
|
+
* across.
|
|
430
|
+
*/
|
|
431
|
+
export function lockedChanges(position, next, ctx) {
|
|
432
|
+
const positionCtx = ctx.lookupManifest ? { lookupManifest: ctx.lookupManifest } : {};
|
|
433
|
+
const before = lockedPositionsUnder(position, positionCtx);
|
|
434
|
+
const after = lockedPositionsUnder({ ...position, value: next }, positionCtx);
|
|
435
|
+
return {
|
|
436
|
+
lost: lockedLosses(before, after),
|
|
437
|
+
created: [...after].flatMap(([key, entry]) => entry.opaque === undefined && isPresent(entry.value) && !isPresent(before.get(key)?.value) ? [entry] : []),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
167
440
|
/**
|
|
168
441
|
* Whether a value written to a locked position leaves it as it is: a clear of an empty
|
|
169
442
|
* position, or the current value echoed back — as stored, in the form the model READ it
|
|
@@ -208,8 +481,13 @@ function convertValue(pos, current, ctx, problems, root) {
|
|
|
208
481
|
switch (fieldTypeOf(field).writer) {
|
|
209
482
|
case 'composition': {
|
|
210
483
|
// 4c: a composition is never written whole — the value stays exactly what it is
|
|
211
|
-
// (re-sending it unchanged is no change); its props change one at a time.
|
|
212
|
-
|
|
484
|
+
// (re-sending it unchanged is no change); its props change one at a time. EC-741:
|
|
485
|
+
// "unchanged" includes the value as the model READ it ({@link toWriterForm}: excluded
|
|
486
|
+
// props absent, rich-text props as markdown) — the rule `isEcho` reads.
|
|
487
|
+
const json = canonicalJson(raw);
|
|
488
|
+
if (json === canonicalJson(current))
|
|
489
|
+
return current;
|
|
490
|
+
if (current !== undefined && json === canonicalJson(toWriterForm({ ...pos, value: current }, ctx)))
|
|
213
491
|
return current;
|
|
214
492
|
return report('invalid-field-value', `Field ${subject(pos, root)} is a page-builder composition: adding, reordering or removing its components is done on the canvas, not by the assistant. Change one component's prop with blockId + prop (the component's id from getDocument).`);
|
|
215
493
|
}
|
|
@@ -266,7 +544,11 @@ function convertValue(pos, current, ctx, problems, root) {
|
|
|
266
544
|
return undefined;
|
|
267
545
|
// EC-558 / 2c: the ONE carry rule — a held placement keeps its per-use sub-fields,
|
|
268
546
|
// a swap carries them to the new image, a new id is an envelope.
|
|
269
|
-
const
|
|
547
|
+
const carriedAsStored = carryAssetPlacements(current, asset.multiple === true ? { multiple: true, ids: kept.map((e) => e.id) } : { multiple: false, id: kept[0].id });
|
|
548
|
+
// EC-748: the carry hands back a single field's stored one-item list as it is (same id);
|
|
549
|
+
// the WRITTEN value takes the declared cardinality — the model wrote one value. (Whether
|
|
550
|
+
// replacing that unreadable stored list is allowed is the lock judge's question.)
|
|
551
|
+
const carried = asset.multiple !== true && Array.isArray(carriedAsStored) ? carriedAsStored[0] : carriedAsStored;
|
|
270
552
|
// The writer's own sub-field values win over the carried ones.
|
|
271
553
|
const withWriter = (entry, index) => {
|
|
272
554
|
const rest = kept[index]?.rest ?? {};
|
|
@@ -348,6 +630,8 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
348
630
|
let targetLock;
|
|
349
631
|
const declared = [];
|
|
350
632
|
let componentId;
|
|
633
|
+
// EC-748: the target prop stored as no static member — declared, but unreadable.
|
|
634
|
+
let unreadableTarget = false;
|
|
351
635
|
const visit = (position, inherited) => {
|
|
352
636
|
const lock = writerLockOf(position.field) ?? inherited;
|
|
353
637
|
if (position.kind === 'prop' && position.component?.nodeId === nodeId) {
|
|
@@ -358,10 +642,29 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
358
642
|
targetLock = lock;
|
|
359
643
|
}
|
|
360
644
|
}
|
|
361
|
-
for (const child of childPositions(position, positionCtx))
|
|
362
|
-
|
|
645
|
+
for (const child of childPositions(position, positionCtx)) {
|
|
646
|
+
if (!isOpaquePosition(child)) {
|
|
647
|
+
visit(child, lock);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
// EC-748: a node inside a value the traversal cannot read is not addressable — it is not
|
|
651
|
+
// found (the write never reaches into an unreadable holder); a prop of the node stored as
|
|
652
|
+
// no static member is declared but cannot be written.
|
|
653
|
+
if (child.opaque === 'not-a-static-prop' && child.component?.nodeId === nodeId) {
|
|
654
|
+
componentId = child.component.componentId;
|
|
655
|
+
declared.push(child.declaration.name);
|
|
656
|
+
if (child.declaration.name === prop)
|
|
657
|
+
unreadableTarget = true;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
363
660
|
};
|
|
364
661
|
visit(root, undefined);
|
|
662
|
+
if (!target && unreadableTarget) {
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
message: `Prop "${prop}" of component "${nodeId}" (${componentId}) is stored as no { value } member (a retired shape); the assistant cannot change it. Repair it outside the assistant: set the prop again in the editor.`,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
365
668
|
if (target && targetLock !== undefined) {
|
|
366
669
|
return {
|
|
367
670
|
ok: false,
|
|
@@ -381,8 +684,10 @@ export function fromWriterFormAtNode(field, displayed, nodeId, prop, raw, ctx, l
|
|
|
381
684
|
};
|
|
382
685
|
}
|
|
383
686
|
const found = target;
|
|
384
|
-
|
|
385
|
-
|
|
687
|
+
// The prop is the write's ROOT (an omitted value clears it); messages locate positions
|
|
688
|
+
// from the top-level field (`body.content[0].props.group.locked`).
|
|
689
|
+
const { value: converted, problems } = convertRoot({ ...found, value: raw }, found.value, ctx, field.name);
|
|
690
|
+
const rebuild = (position) => rebuildFromChildren(position, positionCtx, (child) => (isTarget(child) ? converted : rebuild(child)), (unreadable) => unreadable.value);
|
|
386
691
|
const value = rebuild(root);
|
|
387
692
|
return found.exact
|
|
388
693
|
? { ok: true, value, path: found.path, propValue: converted, exact: true, problems }
|
|
@@ -417,8 +722,11 @@ export function textPositionsOf(position, ctx) {
|
|
|
417
722
|
out.push(current);
|
|
418
723
|
if (current.field.type === 'text')
|
|
419
724
|
return;
|
|
725
|
+
// EC-748: a value the traversal cannot read yields no unit — its text cannot be found
|
|
726
|
+
// (and translate skips a bucket where one may hold locked content: `lockedChanges`).
|
|
420
727
|
for (const child of childPositions(current, positionCtx))
|
|
421
|
-
|
|
728
|
+
if (!isOpaquePosition(child))
|
|
729
|
+
visit(child);
|
|
422
730
|
};
|
|
423
731
|
visit(position);
|
|
424
732
|
return out;
|
|
@@ -438,7 +746,8 @@ export function rebuildTextPositions(position, ctx, replace) {
|
|
|
438
746
|
if (current.field.type === 'text')
|
|
439
747
|
return replace(current);
|
|
440
748
|
const value = isTextPosition(current) ? replace(current) : current.value;
|
|
441
|
-
|
|
749
|
+
// EC-748: an unreadable value comes back as it was (it held no unit).
|
|
750
|
+
return rebuildFromChildren({ ...current, value }, positionCtx, rebuild, (unreadable) => unreadable.value);
|
|
442
751
|
};
|
|
443
752
|
return rebuild(position);
|
|
444
753
|
}
|
|
@@ -451,8 +760,27 @@ export function rebuildTextPositions(position, ctx, replace) {
|
|
|
451
760
|
* stays, so nothing echoed back can silently lose structure), at EVERY position: a
|
|
452
761
|
* top-level field, an object sub-field, a repeater item, a component's rich-text prop.
|
|
453
762
|
* Everything else reads as stored (envelopes are writer forms too).
|
|
763
|
+
*
|
|
764
|
+
* **THE model-read projection** (EC-741): a position core's `writerLockOf` marks
|
|
765
|
+
* `excluded` (`ai: { exclude: true }` — "the model never sees or writes the field") reads
|
|
766
|
+
* as ABSENT, and so does everything below it, at every depth: a sub-field, a repeater
|
|
767
|
+
* item's sub-field, a placement sub-field, a component prop in a composition, a slot or
|
|
768
|
+
* typography content. A `read-only` position stays readable. Every model read crosses this
|
|
769
|
+
* one fold — `getDocument`'s output, the titles the model is shown
|
|
770
|
+
* ({@link modelFacingTitle}) and the echo checks (a value the model read and sent back is
|
|
771
|
+
* no change) — so what the model can see and what it can echo are one declaration. The raw
|
|
772
|
+
* stored document stays local: proposals are always built and validated against it.
|
|
773
|
+
*
|
|
774
|
+
* EC-748 (Codex r9 F44): a stored value the traversal cannot read (a holder of the other
|
|
775
|
+
* cardinality after a schema edit, a repeater item that is no object, a body that is no v2
|
|
776
|
+
* sequence or is read without the prop lookup, …) is WITHHELD — absent where it sits: the
|
|
777
|
+
* field, the list entry, the prop — whenever its declaration may hold an excluded position
|
|
778
|
+
* (`declaredWriterLock`, failing closed on bodies). What cannot be read cannot be projected,
|
|
779
|
+
* so it is never sent whole. One that may hold none reads as stored.
|
|
454
780
|
*/
|
|
455
781
|
export function toWriterForm(position, ctx) {
|
|
782
|
+
if (writerLockOf(position.field) === 'excluded')
|
|
783
|
+
return undefined;
|
|
456
784
|
const { value } = position;
|
|
457
785
|
if (value === undefined || value === null)
|
|
458
786
|
return value;
|
|
@@ -463,7 +791,7 @@ export function toWriterForm(position, ctx) {
|
|
|
463
791
|
if (markdown !== undefined)
|
|
464
792
|
return markdown;
|
|
465
793
|
}
|
|
466
|
-
return rebuildFromChildren(position, positionCtx, (child) => toWriterForm(child, ctx));
|
|
794
|
+
return rebuildFromChildren(position, positionCtx, (child) => toWriterForm(child, ctx), (unreadable) => (declaredWriterLock(unreadable.declaration) === 'excluded' ? undefined : unreadable.value));
|
|
467
795
|
}
|
|
468
796
|
/**
|
|
469
797
|
* The components a document holds whose props a writer may change — every component
|
|
@@ -481,7 +809,9 @@ export function componentIdsOf(collection, document, lookupManifest) {
|
|
|
481
809
|
walkValuePositions(fieldValuePosition(field, value), { lookupManifest }, (position) => {
|
|
482
810
|
if (position.kind === 'prop' && position.component)
|
|
483
811
|
ids.add(position.component.componentId);
|
|
484
|
-
}
|
|
812
|
+
},
|
|
813
|
+
// EC-748: a component inside a value the traversal cannot read has no prop a writer can change.
|
|
814
|
+
() => { });
|
|
485
815
|
}
|
|
486
816
|
};
|
|
487
817
|
visitBucket(document.values);
|
|
@@ -489,28 +819,50 @@ export function componentIdsOf(collection, document, lookupManifest) {
|
|
|
489
819
|
visitBucket(bucket);
|
|
490
820
|
return [...ids];
|
|
491
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* {@link toWriterForm} over one values bag: each declared field in its read form, an
|
|
824
|
+
* excluded one (and an excluded position inside any field) left out. A key the collection
|
|
825
|
+
* does not declare reads as stored (AD-14: the schema is a lens).
|
|
826
|
+
*/
|
|
827
|
+
export function toWriterValues(collection, bag, ctx, locale) {
|
|
828
|
+
let next;
|
|
829
|
+
for (const field of collection.fields) {
|
|
830
|
+
if (!Object.hasOwn(bag, field.name))
|
|
831
|
+
continue;
|
|
832
|
+
const value = bag[field.name];
|
|
833
|
+
const read = toWriterForm(fieldValuePosition(field, value, locale !== undefined ? { locale } : {}), ctx);
|
|
834
|
+
if (read === value)
|
|
835
|
+
continue;
|
|
836
|
+
next ??= { ...bag };
|
|
837
|
+
if (read === undefined)
|
|
838
|
+
delete next[field.name];
|
|
839
|
+
else
|
|
840
|
+
next[field.name] = read;
|
|
841
|
+
}
|
|
842
|
+
return next ?? bag;
|
|
843
|
+
}
|
|
492
844
|
/** {@link toWriterForm} over every field of every bucket of a document. */
|
|
493
845
|
export function toWriterDocument(collection, document, ctx) {
|
|
494
|
-
const
|
|
495
|
-
const mapBucket = (bucket, locale) => {
|
|
496
|
-
let next = bucket;
|
|
497
|
-
for (const [name, value] of Object.entries(bucket)) {
|
|
498
|
-
const field = byName.get(name);
|
|
499
|
-
if (!field)
|
|
500
|
-
continue;
|
|
501
|
-
const read = toWriterForm(fieldValuePosition(field, value, locale !== undefined ? { locale } : {}), ctx);
|
|
502
|
-
if (read === value)
|
|
503
|
-
continue;
|
|
504
|
-
if (next === bucket)
|
|
505
|
-
next = { ...bucket };
|
|
506
|
-
next[name] = read;
|
|
507
|
-
}
|
|
508
|
-
return next;
|
|
509
|
-
};
|
|
510
|
-
const values = mapBucket(document.values);
|
|
846
|
+
const values = toWriterValues(collection, document.values, ctx);
|
|
511
847
|
const localized = document.localized
|
|
512
|
-
? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [
|
|
848
|
+
? Object.fromEntries(Object.entries(document.localized).map(([locale, bucket]) => [
|
|
849
|
+
locale,
|
|
850
|
+
toWriterValues(collection, bucket, ctx, locale),
|
|
851
|
+
]))
|
|
513
852
|
: undefined;
|
|
514
853
|
return { ...document, values, ...(localized ? { localized } : {}) };
|
|
515
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* The title a MODEL is shown for a document (EC-741) — core's `resolveDocumentTitle` over
|
|
857
|
+
* the projected values ({@link toWriterValues}), so an excluded field never reaches the
|
|
858
|
+
* model as a title: neither as the named or first-field fallback, nor through a function
|
|
859
|
+
* `titleField` that reads it (the function sees the field absent). Every title the model
|
|
860
|
+
* reads goes through here: the `updateFields` / `createDocument` outputs, the
|
|
861
|
+
* `searchDocuments` results (their query is matched against this title too, so an
|
|
862
|
+
* excluded value cannot be probed by search) and the system prompt's "currently open"
|
|
863
|
+
* line. `values` is the bag the caller resolves titles from (a locale's merged values).
|
|
864
|
+
*/
|
|
865
|
+
export function modelFacingTitle(collection, values, fallbackId, ctx = {}) {
|
|
866
|
+
return resolveDocumentTitle(collection, toWriterValues(collection, values, ctx), fallbackId);
|
|
867
|
+
}
|
|
516
868
|
//# sourceMappingURL=writer-form.js.map
|