@brianmsm/quarto-bento 0.0.0-stage → 0.1.0

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/bin/qbento-npm.mjs +62 -0
  4. package/distribution/LICENSE +21 -0
  5. package/distribution/PACKAGING.md +59 -0
  6. package/distribution/bin/qbento +5 -0
  7. package/distribution/bin/qbento.cmd +4 -0
  8. package/distribution/manifest.json +720 -0
  9. package/distribution/runtime/LICENSE +21 -0
  10. package/distribution/runtime/_extensions/qbento/_extension.yml +24 -0
  11. package/distribution/runtime/_extensions/qbento/capture.lua +21 -0
  12. package/distribution/runtime/_extensions/qbento/guard.lua +7 -0
  13. package/distribution/runtime/_extensions/qbento/post.ts +2 -0
  14. package/distribution/runtime/_extensions/qbento/pre.ts +2 -0
  15. package/distribution/runtime/_extensions/qbento/producer-activation.js +129 -0
  16. package/distribution/runtime/src/bridge/browser.js +168 -0
  17. package/distribution/runtime/src/bridge/isolation.ts +55 -0
  18. package/distribution/runtime/src/browser/chromium.ts +218 -0
  19. package/distribution/runtime/src/cli.ts +219 -0
  20. package/distribution/runtime/src/core/agent.ts +565 -0
  21. package/distribution/runtime/src/core/annotations.ts +384 -0
  22. package/distribution/runtime/src/core/capture.ts +203 -0
  23. package/distribution/runtime/src/core/component-actions.ts +550 -0
  24. package/distribution/runtime/src/core/component-frames.ts +59 -0
  25. package/distribution/runtime/src/core/component-sync.ts +290 -0
  26. package/distribution/runtime/src/core/design-diagnostics.ts +78 -0
  27. package/distribution/runtime/src/core/design-registry.ts +287 -0
  28. package/distribution/runtime/src/core/graph-relations.ts +354 -0
  29. package/distribution/runtime/src/core/graph-scope.ts +63 -0
  30. package/distribution/runtime/src/core/graph-sync.ts +228 -0
  31. package/distribution/runtime/src/core/io.ts +27 -0
  32. package/distribution/runtime/src/core/kpi-appearance.ts +205 -0
  33. package/distribution/runtime/src/core/layouts.ts +245 -0
  34. package/distribution/runtime/src/core/project.ts +759 -0
  35. package/distribution/runtime/src/core/reconcile.ts +384 -0
  36. package/distribution/runtime/src/core/recovery.ts +1255 -0
  37. package/distribution/runtime/src/core/slide-actions.ts +158 -0
  38. package/distribution/runtime/src/core/source-scope.ts +50 -0
  39. package/distribution/runtime/src/core/sync.ts +2423 -0
  40. package/distribution/runtime/src/core/widgets.ts +300 -0
  41. package/distribution/runtime/src/integration/install.ts +98 -0
  42. package/distribution/runtime/src/integration/invocation.ts +100 -0
  43. package/distribution/runtime/src/integration/quarto.ts +457 -0
  44. package/distribution/runtime/src/render/assets.ts +66 -0
  45. package/distribution/runtime/src/render/capture.lua +8 -0
  46. package/distribution/runtime/src/render/chart.ts +165 -0
  47. package/distribution/runtime/src/render/components.ts +199 -0
  48. package/distribution/runtime/src/render/design.ts +820 -0
  49. package/distribution/runtime/src/render/diagnostics.ts +36 -0
  50. package/distribution/runtime/src/render/graphs.ts +747 -0
  51. package/distribution/runtime/src/render/html.ts +82 -0
  52. package/distribution/runtime/src/render/images.ts +268 -0
  53. package/distribution/runtime/src/render/inline.ts +259 -0
  54. package/distribution/runtime/src/render/kpi.ts +385 -0
  55. package/distribution/runtime/src/render/lists.ts +183 -0
  56. package/distribution/runtime/src/render/math.ts +413 -0
  57. package/distribution/runtime/src/render/measure.ts +185 -0
  58. package/distribution/runtime/src/render/native-layout.ts +174 -0
  59. package/distribution/runtime/src/render/producer-capture.ts +319 -0
  60. package/distribution/runtime/src/render/producer-environment.R +49 -0
  61. package/distribution/runtime/src/render/producer-environment.ts +96 -0
  62. package/distribution/runtime/src/render/producer-profile.R +91 -0
  63. package/distribution/runtime/src/render/producer-site-profile.R +18 -0
  64. package/distribution/runtime/src/render/project.ts +1806 -0
  65. package/distribution/runtime/src/render/quarto.ts +127 -0
  66. package/distribution/runtime/src/render/registry.ts +233 -0
  67. package/distribution/runtime/src/render/resources.ts +292 -0
  68. package/distribution/runtime/src/render/shell.ts +110 -0
  69. package/distribution/runtime/src/render/source-inline.ts +169 -0
  70. package/distribution/runtime/src/render/table.ts +220 -0
  71. package/distribution/runtime/src/render/text-overflow.ts +208 -0
  72. package/distribution/runtime/src/render/types.ts +71 -0
  73. package/distribution/runtime/src/render/validate.ts +572 -0
  74. package/distribution/runtime/src/rich/project.ts +117 -0
  75. package/distribution/runtime/src/rich/table.ts +374 -0
  76. package/distribution/runtime/src/source/annotations.ts +490 -0
  77. package/distribution/runtime/src/source/attributes.ts +415 -0
  78. package/distribution/runtime/src/source/chart.ts +165 -0
  79. package/distribution/runtime/src/source/code-language.ts +277 -0
  80. package/distribution/runtime/src/source/component-insertion.ts +160 -0
  81. package/distribution/runtime/src/source/deletion.ts +278 -0
  82. package/distribution/runtime/src/source/graphs.ts +517 -0
  83. package/distribution/runtime/src/source/identities.ts +256 -0
  84. package/distribution/runtime/src/source/identity-id.ts +6 -0
  85. package/distribution/runtime/src/source/image.ts +50 -0
  86. package/distribution/runtime/src/source/kpi.ts +196 -0
  87. package/distribution/runtime/src/source/mod.ts +876 -0
  88. package/distribution/runtime/src/source/notes.ts +308 -0
  89. package/distribution/runtime/src/source/structure.ts +647 -0
  90. package/distribution/runtime/src/source/table.ts +290 -0
  91. package/distribution/runtime/src/source/text-conversion.ts +146 -0
  92. package/distribution/runtime/src/state/components.ts +281 -0
  93. package/distribution/runtime/src/state/graphs.ts +200 -0
  94. package/distribution/runtime/src/state/json.ts +162 -0
  95. package/distribution/runtime/src/state/merge.ts +1034 -0
  96. package/distribution/runtime/src/state/mod.ts +5 -0
  97. package/distribution/runtime/src/state/store.ts +226 -0
  98. package/distribution/runtime/src/state/types.ts +151 -0
  99. package/distribution/runtime/src/studio/client.js +790 -0
  100. package/distribution/runtime/src/studio/components.js +188 -0
  101. package/distribution/runtime/src/studio/index.html +278 -0
  102. package/distribution/runtime/src/studio/layouts.js +170 -0
  103. package/distribution/runtime/src/studio/registry.js +283 -0
  104. package/distribution/runtime/src/studio/relations.js +169 -0
  105. package/distribution/runtime/src/studio/server.ts +433 -0
  106. package/distribution/runtime/src/widgets/deck.ts +39 -0
  107. package/distribution/runtime/src/widgets/plotly.ts +354 -0
  108. package/distribution/runtime/src/widgets/presenter.ts +367 -0
  109. package/distribution/runtime/src/widgets/snapshot.ts +116 -0
  110. package/distribution/runtime/vendor/bento/FONT_NOTICES.md +49 -0
  111. package/distribution/runtime/vendor/bento/LICENSE +21 -0
  112. package/distribution/runtime/vendor/bento/THIRD_PARTY_NOTICES.md +51 -0
  113. package/distribution/runtime/vendor/bento/font-licenses/fraunces-OFL.txt +93 -0
  114. package/distribution/runtime/vendor/bento/font-licenses/instrument-sans-OFL.txt +93 -0
  115. package/distribution/runtime/vendor/bento/pin.json +69 -0
  116. package/distribution/runtime/vendor/bento/provenance/slides.package-lock.json +1928 -0
  117. package/distribution/runtime/vendor/bento/provenance/slides.package.json +28 -0
  118. package/distribution/runtime/vendor/bento/runtime-dependencies.json +244 -0
  119. package/distribution/runtime/vendor/bento/runtime-licenses.json +4038 -0
  120. package/distribution/runtime/vendor/bento/shell.html +269 -0
  121. package/distribution/runtime/vendor/bento/slides.schema.json +2379 -0
  122. package/package.json +40 -4
@@ -0,0 +1,1034 @@
1
+ import {
2
+ captureComponentSnapshots,
3
+ componentOwns,
4
+ validateComponentCreations,
5
+ validateComponentProjections,
6
+ } from "./components.ts";
7
+ import {
8
+ type BentoDoc,
9
+ type BentoElement,
10
+ type BentoSlide,
11
+ type Change,
12
+ type JsonObject,
13
+ type OwnershipMap,
14
+ type ProjectState,
15
+ type ProvenanceMap,
16
+ StateError,
17
+ type Target,
18
+ type Value,
19
+ } from "./types.ts";
20
+ import {
21
+ assertJson,
22
+ clone,
23
+ entity,
24
+ equal,
25
+ fields,
26
+ get,
27
+ set,
28
+ targetKey,
29
+ validateDoc,
30
+ } from "./json.ts";
31
+ import {
32
+ assertGraphDocument,
33
+ graphReceipt,
34
+ graphRelationshipPath,
35
+ } from "./graphs.ts";
36
+
37
+ const semanticFields = new Set([
38
+ "html",
39
+ "text",
40
+ "content",
41
+ "src",
42
+ "svg",
43
+ "data",
44
+ "option",
45
+ "preset",
46
+ "source",
47
+ "rows",
48
+ "header",
49
+ "columns",
50
+ "cells",
51
+ "code",
52
+ "language",
53
+ "grammarName",
54
+ "latex",
55
+ "notes",
56
+ "title",
57
+ "type",
58
+ "url",
59
+ ]);
60
+ function id(target: Target, path: string[], kind: string): string {
61
+ return JSON.stringify([targetKey(target), kind, path]);
62
+ }
63
+ function origin(state: ProjectState, target: Target) {
64
+ return state.provenance[targetKey(target)]?.origin ?? "authored";
65
+ }
66
+ function semantic(
67
+ state: ProjectState,
68
+ target: Target,
69
+ path: string[],
70
+ ): boolean {
71
+ if (graphRelationshipPath(state.provenance[targetKey(target)], path)) {
72
+ return true;
73
+ }
74
+ if (
75
+ state.ownership[targetKey(target)]?.some((p) =>
76
+ p === path.join("/") || p === path[0] ||
77
+ path.join("/").startsWith(p + "/")
78
+ )
79
+ ) return true;
80
+ return semanticFields.has(path[0]);
81
+ }
82
+ function upsert(state: ProjectState, change: Change): void {
83
+ const old = state.pending.find((p) => p.id === change.id);
84
+ if (old) {
85
+ change.before = old.before;
86
+ if (old.restoreOrder) change.restoreOrder = clone(old.restoreOrder);
87
+ }
88
+ state.pending = state.pending.filter((p) => p.id !== change.id);
89
+ if (!equal(change.before, change.after)) state.pending.push(change);
90
+ }
91
+ function change(
92
+ state: ProjectState,
93
+ target: Target,
94
+ path: string[],
95
+ before: Value,
96
+ after: Value,
97
+ kind: Change["kind"] = "field",
98
+ ): void {
99
+ const previous = state.pending.find((p) => p.id === id(target, path, kind));
100
+ const from = previous?.origin === "detached"
101
+ ? "detached"
102
+ : kind === "field" && !semanticFields.has(path[0])
103
+ ? "authored"
104
+ : origin(state, target);
105
+ upsert(state, {
106
+ id: id(target, path, kind),
107
+ target: clone(target),
108
+ path,
109
+ kind,
110
+ origin: from,
111
+ status: from === "computed" ? "conflict" : "bento-ahead",
112
+ before,
113
+ after,
114
+ ...(from === "computed"
115
+ ? {
116
+ reason:
117
+ "Computed content must be restored, edited at its producer, or explicitly detached.",
118
+ }
119
+ : {}),
120
+ });
121
+ }
122
+ export function createState(
123
+ generated: BentoDoc,
124
+ provenance: ProvenanceMap = {},
125
+ ownership: OwnershipMap = {},
126
+ ): ProjectState {
127
+ validateDoc(generated);
128
+ assertJson(provenance);
129
+ assertJson(ownership);
130
+ return {
131
+ schemaVersion: 1,
132
+ revision: 0,
133
+ docId: generated.docId,
134
+ generatedBaseline: clone(generated),
135
+ emitted: clone(generated),
136
+ provenance: clone(provenance),
137
+ ownership: clone(ownership),
138
+ overrides: [],
139
+ elementOrders: [],
140
+ bentoOnly: [],
141
+ pending: [],
142
+ };
143
+ }
144
+ /** Source graph anchors have one semantic el and a separate visual side.
145
+ * Native drag may remove the entire anchor. Collapse ancestor/leaf journals
146
+ * against the captured snapshot, retaining the original three-way evidence.
147
+ */
148
+ function normalizeGraphEndpoints(
149
+ input: ProjectState,
150
+ state: ProjectState,
151
+ edited: BentoDoc,
152
+ ) {
153
+ for (const [key, provenance] of Object.entries(input.provenance)) {
154
+ const proof = graphReceipt(provenance);
155
+ if (!proof || !["edge", "timeline-link"].includes(proof.role)) continue;
156
+ const [slideId, elementId] = JSON.parse(key) as [string, string];
157
+ const target = { slideId, elementId },
158
+ current = entity(edited, target),
159
+ baseline = entity(input.generatedBaseline, target);
160
+ if (!current || !baseline) continue; // Known deletion is a separate pending action.
161
+ for (const endpoint of ["from", "to"] as const) {
162
+ const semanticPath = (change: Change) =>
163
+ change.kind === "field" && equal(change.target, target) &&
164
+ change.path[0] === endpoint &&
165
+ (change.path.length === 1 ||
166
+ change.path.length === 2 && change.path[1] === "el");
167
+ const old = input.pending.filter(semanticPath),
168
+ next = state.pending.filter(semanticPath);
169
+ if (!old.length && !next.length) continue;
170
+ const leaf = old.find((change) => change.path.length === 2) ??
171
+ next.find((change) => change.path.length === 2),
172
+ parent = old.find((change) => change.path.length === 1) ??
173
+ next.find((change) => change.path.length === 1),
174
+ conflict = old.find((change) => change.status === "conflict"),
175
+ anchor = get(current, [endpoint]),
176
+ sourceAnchor = get(baseline, [endpoint]);
177
+ // A newly captured leaf uses the latest generated baseline. Older
178
+ // ancestor/conflict evidence still describes the original edit base.
179
+ const original = conflict ?? old.find((change) =>
180
+ change.path.length === 2
181
+ ) ??
182
+ old.find((change) => change.path.length === 1) ?? leaf ?? parent!;
183
+ const originalEl = original.path.length === 2
184
+ ? original.before
185
+ : original.before.present && original.before.value &&
186
+ typeof original.before.value === "object" &&
187
+ !Array.isArray(original.before.value)
188
+ ? get(original.before.value, ["el"])
189
+ : { present: false };
190
+ const object = anchor.present && anchor.value &&
191
+ typeof anchor.value === "object" && !Array.isArray(anchor.value);
192
+ const path = object ? [endpoint, "el"] : [endpoint];
193
+ let before = originalEl;
194
+ if (!object) {
195
+ before = clone(
196
+ original.path.length === 1
197
+ ? original.before
198
+ : parent?.before ?? sourceAnchor,
199
+ );
200
+ if (
201
+ before.present && before.value && typeof before.value === "object" &&
202
+ !Array.isArray(before.value)
203
+ ) {
204
+ set(before.value, ["el"], originalEl);
205
+ }
206
+ }
207
+ const after = get(current, path), generated = get(baseline, path);
208
+ state.pending = state.pending.filter((change) => !semanticPath(change));
209
+ if (conflict || !equal(after, generated)) {
210
+ const evidence = clone(conflict ?? leaf ?? parent!);
211
+ evidence.id = id(target, path, "field");
212
+ evidence.path = path;
213
+ evidence.before = before;
214
+ evidence.after = after;
215
+ if (conflict?.source && conflict.path.length !== path.length) {
216
+ evidence.source =
217
+ object && conflict.source.present && conflict.source.value &&
218
+ typeof conflict.source.value === "object" &&
219
+ !Array.isArray(conflict.source.value)
220
+ ? get(conflict.source.value, ["el"])
221
+ : generated;
222
+ }
223
+ state.pending.push(evidence);
224
+ }
225
+ if (object) {
226
+ // Reconstructing an anchor must not leave a stale side overlay that
227
+ // changes the next composed document away from the saved snapshot.
228
+ const sidePath = [endpoint, "side"], side = get(current, sidePath);
229
+ let overlay = state.overrides.find((overlay) =>
230
+ equal(overlay.target, target)
231
+ );
232
+ if (overlay) {
233
+ overlay.fields = overlay.fields.filter((field) =>
234
+ !equal(field.path, sidePath)
235
+ );
236
+ }
237
+ if (!equal(side, get(baseline, sidePath))) {
238
+ if (!overlay) {
239
+ overlay = { target: clone(target), fields: [] };
240
+ state.overrides.push(overlay);
241
+ }
242
+ overlay.fields.push({ path: sidePath, value: side });
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ /** Persist edits relative to the actual last emitted/saved document, never relative to stale source. */
250
+ export function captureEdits(
251
+ input: ProjectState,
252
+ edited: BentoDoc,
253
+ ): ProjectState {
254
+ validateState(input);
255
+ validateDoc(edited);
256
+ assertGraphDocument(edited, input.provenance, {
257
+ capture: true,
258
+ baseline: input.emitted,
259
+ });
260
+ if (input.docId !== edited.docId) {
261
+ throw new StateError(
262
+ "UNRELATED_DOCUMENT",
263
+ "The edited HTML belongs to a different docId.",
264
+ );
265
+ }
266
+ const state = clone(input);
267
+ captureComponentSnapshots(state, edited);
268
+ function capture(
269
+ target: Target,
270
+ before: JsonObject,
271
+ after: JsonObject,
272
+ skip: string[],
273
+ restored = false,
274
+ ) {
275
+ if (
276
+ state.pending.some((p) =>
277
+ (p.kind === "create-slide" && p.target.slideId === target.slideId) ||
278
+ (p.kind === "create-element" && equal(p.target, target))
279
+ )
280
+ ) return;
281
+ if (restored) {
282
+ // Restoring an absent object is a complete snapshot, so a value restored to
283
+ // the source baseline explicitly cancels older hidden pending/visual edits.
284
+ state.pending = state.pending.filter((p) =>
285
+ !(p.kind === "field" && equal(p.target, target) &&
286
+ equal(get(before, p.path), get(after, p.path)))
287
+ );
288
+ state.overrides = state.overrides.filter((o) => !equal(o.target, target));
289
+ }
290
+ for (const field of fields(before, after, skip)) {
291
+ if (semantic(state, target, field.path)) {
292
+ change(
293
+ state,
294
+ target,
295
+ field.path,
296
+ get(entity(state.generatedBaseline, target), field.path),
297
+ field.value,
298
+ );
299
+ } else {
300
+ let overlay = state.overrides.find((o) => equal(o.target, target));
301
+ if (!overlay) {
302
+ overlay = { target: clone(target), fields: [] };
303
+ state.overrides.push(overlay);
304
+ }
305
+ overlay.fields = overlay.fields.filter((f) =>
306
+ !equal(f.path, field.path)
307
+ );
308
+ if (
309
+ !equal(
310
+ get(entity(state.generatedBaseline, target), field.path),
311
+ field.value,
312
+ )
313
+ ) overlay.fields.push(clone(field));
314
+ }
315
+ }
316
+ }
317
+ capture({}, state.emitted, edited, ["docId", "slides", "format", "version"]);
318
+ for (const oldSlide of state.emitted.slides) {
319
+ const slide = edited.slides.find((s) => s.id === oldSlide.id);
320
+ if (!slide) {
321
+ change(
322
+ state,
323
+ { slideId: oldSlide.id },
324
+ [],
325
+ get(entity(state.generatedBaseline, { slideId: oldSlide.id }), []),
326
+ { present: false },
327
+ "delete-slide",
328
+ );
329
+ const deletion = state.pending.find((p) =>
330
+ p.kind === "delete-slide" && p.target.slideId === oldSlide.id
331
+ );
332
+ if (deletion) {
333
+ deletion.restoreOrder ??= state.emitted.slides.map((s) => s.id);
334
+ }
335
+ continue;
336
+ }
337
+ if (
338
+ state.pending.some((p) =>
339
+ p.kind === "create-slide" && p.target.slideId === slide.id
340
+ )
341
+ ) continue;
342
+ capture({ slideId: slide.id }, oldSlide, slide, ["id", "elements"]);
343
+ if (
344
+ !equal(
345
+ oldSlide.elements.map((e) => e.id),
346
+ slide.elements.map((e) => e.id),
347
+ )
348
+ ) {
349
+ state.elementOrders = state.elementOrders.filter((o) =>
350
+ o.slideId !== slide.id
351
+ );
352
+ state.elementOrders.push({
353
+ slideId: slide.id,
354
+ ids: slide.elements.map((e) => e.id),
355
+ });
356
+ }
357
+ for (const oldEl of oldSlide.elements) {
358
+ const el = slide.elements.find((e) => e.id === oldEl.id),
359
+ target = { slideId: slide.id, elementId: oldEl.id };
360
+ const decorative = state.bentoOnly.find((e) =>
361
+ e.slideId === slide.id && e.element.id === oldEl.id
362
+ );
363
+ if (decorative) {
364
+ if (el) decorative.element = clone(el);
365
+ else state.bentoOnly = state.bentoOnly.filter((e) => e !== decorative);
366
+ } else if (!el) {
367
+ change(
368
+ state,
369
+ target,
370
+ [],
371
+ get(entity(state.generatedBaseline, target), []),
372
+ {
373
+ present: false,
374
+ },
375
+ "delete-element",
376
+ );
377
+ const deletion = state.pending.find((p) =>
378
+ p.kind === "delete-element" && equal(p.target, target)
379
+ );
380
+ if (deletion) {
381
+ deletion.restoreOrder ??= oldSlide.elements.map((item) => item.id);
382
+ }
383
+ } else capture(target, oldEl, el, ["id"]);
384
+ }
385
+ for (
386
+ const el of slide.elements.filter((e) =>
387
+ !oldSlide.elements.some((o) => o.id === e.id)
388
+ )
389
+ ) {
390
+ const target = { slideId: slide.id, elementId: el.id };
391
+ const existingSource = entity(state.generatedBaseline, target);
392
+ if (existingSource) {
393
+ capture(target, existingSource, el, ["id"], true);
394
+ continue;
395
+ }
396
+ if (["shape", "line", "arrow", "path"].includes(el.type)) {
397
+ state.bentoOnly.push({ slideId: slide.id, element: clone(el) });
398
+ } else {change(state, target, [], { present: false }, {
399
+ present: true,
400
+ value: el,
401
+ }, "create-element");}
402
+ }
403
+ }
404
+ for (
405
+ const slide of edited.slides.filter((s) =>
406
+ !state.emitted.slides.some((o) => o.id === s.id)
407
+ )
408
+ ) {
409
+ const original = state.generatedBaseline.slides.find((s) =>
410
+ s.id === slide.id
411
+ );
412
+ if (original) {
413
+ capture({ slideId: slide.id }, original, slide, ["id", "elements"], true);
414
+ state.elementOrders = state.elementOrders.filter((o) =>
415
+ o.slideId !== slide.id
416
+ );
417
+ state.elementOrders.push({
418
+ slideId: slide.id,
419
+ ids: slide.elements.map((e) => e.id),
420
+ });
421
+ for (const el of slide.elements) {
422
+ const target = { slideId: slide.id, elementId: el.id };
423
+ const old = original.elements.find((e) => e.id === el.id);
424
+ if (old) capture(target, old, el, ["id"], true);
425
+ else if (["shape", "line", "arrow", "path"].includes(el.type)) {
426
+ state.bentoOnly = state.bentoOnly.filter((o) =>
427
+ !(o.slideId === slide.id && o.element.id === el.id)
428
+ );
429
+ state.bentoOnly.push({ slideId: slide.id, element: clone(el) });
430
+ } else {change(state, target, [], { present: false }, {
431
+ present: true,
432
+ value: el,
433
+ }, "create-element");}
434
+ }
435
+ for (
436
+ const old of original.elements.filter((e) =>
437
+ !slide.elements.some((el) => el.id === e.id)
438
+ )
439
+ ) {
440
+ change(
441
+ state,
442
+ { slideId: slide.id, elementId: old.id },
443
+ [],
444
+ { present: true, value: old },
445
+ { present: false },
446
+ "delete-element",
447
+ );
448
+ }
449
+ state.bentoOnly = state.bentoOnly.filter((o) =>
450
+ o.slideId !== slide.id ||
451
+ slide.elements.some((el) => el.id === o.element.id)
452
+ );
453
+ } else {change(state, { slideId: slide.id }, [], { present: false }, {
454
+ present: true,
455
+ value: slide,
456
+ }, "create-slide");}
457
+ }
458
+ // Refresh pending creations with their latest complete content; a new object has no source entity yet.
459
+ for (const pending of state.pending) {
460
+ if (pending.kind === "create-slide" || pending.kind === "create-element") {
461
+ const current = entity(edited, pending.target);
462
+ if (current) pending.after = { present: true, value: clone(current) };
463
+ }
464
+ }
465
+ state.pending = state.pending.filter((p) =>
466
+ !(p.kind.startsWith("create-") && !entity(edited, p.target) &&
467
+ !(componentOwns(state, p.target) &&
468
+ !edited.slides.some((slide) => slide.id === p.target.slideId))) &&
469
+ !(p.kind.startsWith("delete-") && entity(edited, p.target))
470
+ );
471
+ const beforeOrder = state.emitted.slides.map((s) => s.id),
472
+ afterOrder = edited.slides.map((s) => s.id);
473
+ if (!equal(beforeOrder, afterOrder)) {
474
+ change(
475
+ state,
476
+ {},
477
+ ["slides"],
478
+ { present: true, value: state.generatedBaseline.slides.map((s) => s.id) },
479
+ { present: true, value: afterOrder },
480
+ "slide-order",
481
+ );
482
+ }
483
+ normalizeGraphEndpoints(input, state, edited);
484
+ state.overrides = state.overrides.filter((o) => o.fields.length);
485
+ state.emitted = clone(edited);
486
+ return state;
487
+ }
488
+ /**
489
+ * After an approved source sync and fresh execution, adopt explicitly listed
490
+ * creations as source-backed objects without losing native visual design.
491
+ * IDs and semantic fields must agree; this function never writes or approves QMD.
492
+ * Call compose() with the returned state and the same generated document next.
493
+ */
494
+ export function promoteCreated(
495
+ input: ProjectState,
496
+ generated: BentoDoc,
497
+ approvedChangeIds: string[],
498
+ ): ProjectState {
499
+ validateState(input);
500
+ validateDoc(generated);
501
+ if (generated.docId !== input.docId) {
502
+ throw new StateError(
503
+ "DOC_ID_CHANGED",
504
+ "Promotion must preserve project identity.",
505
+ );
506
+ }
507
+ if (new Set(approvedChangeIds).size !== approvedChangeIds.length) {
508
+ throw new StateError(
509
+ "INVALID_PROMOTION",
510
+ "Creation approval IDs must be unique.",
511
+ );
512
+ }
513
+ const state = clone(input);
514
+ function retainVisual(
515
+ target: Target,
516
+ source: JsonObject,
517
+ edited: JsonObject,
518
+ skip: string[],
519
+ ) {
520
+ for (const field of fields(source, edited, skip)) {
521
+ if (semantic(state, target, field.path)) {
522
+ // Native optional empty notes/title are semantically equivalent to absence.
523
+ const previous = get(source, field.path);
524
+ const empty = (v: Value) => !v.present || v.value === "";
525
+ if (
526
+ ["notes", "title"].includes(field.path[0]) && empty(previous) &&
527
+ empty(field.value)
528
+ ) continue;
529
+ throw new StateError(
530
+ "PROMOTION_SEMANTIC_MISMATCH",
531
+ "Approved creation differs from its executed source; do not consume its pending content.",
532
+ { target, path: field.path, source: previous, edited: field.value },
533
+ );
534
+ }
535
+ let overlay = state.overrides.find((o) => equal(o.target, target));
536
+ if (!overlay) {
537
+ overlay = { target: clone(target), fields: [] };
538
+ state.overrides.push(overlay);
539
+ }
540
+ overlay.fields = overlay.fields.filter((f) => !equal(f.path, field.path));
541
+ overlay.fields.push(clone(field));
542
+ }
543
+ }
544
+ for (const changeId of approvedChangeIds) {
545
+ const pending = state.pending.find((p) => p.id === changeId);
546
+ if (
547
+ !pending || !["create-slide", "create-element"].includes(pending.kind)
548
+ ) {
549
+ throw new StateError(
550
+ "INVALID_PROMOTION",
551
+ "Only explicitly approved pending creations can be promoted.",
552
+ );
553
+ }
554
+ const source = entity(generated, pending.target);
555
+ if (!source) {
556
+ throw new StateError(
557
+ "PROMOTION_SOURCE_MISSING",
558
+ "Fresh execution must contain the approved creation's stable ID.",
559
+ pending.target,
560
+ );
561
+ }
562
+ const edited = pending.after.value as JsonObject;
563
+ if (pending.kind === "create-element") {
564
+ retainVisual(pending.target, source, edited, ["id"]);
565
+ } else {
566
+ const sourceSlide = source as BentoSlide,
567
+ editedSlide = edited as BentoSlide;
568
+ retainVisual(pending.target, sourceSlide, editedSlide, [
569
+ "id",
570
+ "elements",
571
+ ]);
572
+ for (const el of editedSlide.elements) {
573
+ const target = { slideId: editedSlide.id, elementId: el.id };
574
+ const sourceEl = sourceSlide.elements.find((e) => e.id === el.id);
575
+ if (sourceEl) retainVisual(target, sourceEl, el, ["id"]);
576
+ else if (["shape", "line", "arrow", "path"].includes(el.type)) {
577
+ state.bentoOnly = state.bentoOnly.filter((o) =>
578
+ !(o.slideId === editedSlide.id && o.element.id === el.id)
579
+ );
580
+ state.bentoOnly.push({ slideId: editedSlide.id, element: clone(el) });
581
+ } else {throw new StateError(
582
+ "PROMOTION_SOURCE_MISSING",
583
+ "An authored element on the approved slide has no stable source identity.",
584
+ target,
585
+ );}
586
+ }
587
+ state.elementOrders = state.elementOrders.filter((o) =>
588
+ o.slideId !== editedSlide.id
589
+ );
590
+ state.elementOrders.push({
591
+ slideId: editedSlide.id,
592
+ ids: editedSlide.elements.map((e) => e.id),
593
+ });
594
+ }
595
+ state.pending = state.pending.filter((p) => p.id !== changeId);
596
+ }
597
+ validateState(state);
598
+ return state;
599
+ }
600
+ export function compose(
601
+ input: ProjectState,
602
+ generated: BentoDoc,
603
+ provenance = input.provenance,
604
+ ownership = input.ownership,
605
+ options: { strict?: boolean } = {},
606
+ ): { state: ProjectState; document: BentoDoc; changes: Change[] } {
607
+ validateState(input);
608
+ validateDoc(generated);
609
+ assertJson(provenance);
610
+ assertJson(ownership);
611
+ if (generated.docId !== input.docId) {
612
+ throw new StateError(
613
+ "DOC_ID_CHANGED",
614
+ "Regeneration must reuse the saved docId.",
615
+ );
616
+ }
617
+ const state = clone(input), doc = clone(generated);
618
+ state.provenance = clone(provenance);
619
+ state.ownership = clone(ownership);
620
+ const retained: Change[] = [];
621
+ // Slide deletion keeps its original confirmation behavior. Element retirement
622
+ // is approved explicitly by prepareApprovedSync after fresh execution; an
623
+ // independent source removal cannot authorize discarding its retained edits.
624
+ const deleted = state.pending.filter((p) =>
625
+ p.kind === "delete-slide" && !entity(generated, p.target)
626
+ );
627
+ if (
628
+ deleted.some((change) =>
629
+ state.componentCreations?.some((intent) =>
630
+ intent.slideId === change.target.slideId
631
+ ) || state.componentProjections?.some((record) =>
632
+ record.slideId === change.target.slideId
633
+ )
634
+ )
635
+ ) {
636
+ throw new StateError(
637
+ "COMPONENT_PARENT_APPROVAL_REQUIRED",
638
+ "A slide with pending components disappeared from QMD. Recover it or review its exact source deletion before retiring child content.",
639
+ );
640
+ }
641
+ const underDeleted = (target: Target) =>
642
+ deleted.some((p) =>
643
+ p.target.slideId === target.slideId &&
644
+ (!p.target.elementId || p.target.elementId === target.elementId)
645
+ );
646
+ state.overrides = state.overrides.filter((o) => !underDeleted(o.target));
647
+ state.bentoOnly = state.bentoOnly.filter((o) =>
648
+ !underDeleted({ slideId: o.slideId, elementId: o.element.id })
649
+ );
650
+ state.elementOrders = state.elementOrders.filter((o) =>
651
+ !deleted.some((p) =>
652
+ p.kind === "delete-slide" && p.target.slideId === o.slideId
653
+ )
654
+ );
655
+ state.pending = state.pending.filter((p) => !underDeleted(p.target));
656
+ for (const overlay of state.overrides) {
657
+ const obj = entity(doc, overlay.target);
658
+ if (!obj) {
659
+ const previousOrphan = input.pending.find((p) =>
660
+ p.kind === "orphan" && equal(p.target, overlay.target)
661
+ );
662
+ const old = previousOrphan?.after.value ??
663
+ entity(input.emitted, overlay.target) ??
664
+ entity(input.generatedBaseline, overlay.target);
665
+ retained.push({
666
+ id: id(overlay.target, [], "orphan"),
667
+ target: overlay.target,
668
+ path: [],
669
+ kind: "orphan",
670
+ origin: origin(state, overlay.target),
671
+ status: "conflict",
672
+ before: { present: true, value: old ?? null },
673
+ after: { present: true, value: old ?? null },
674
+ source: { present: false },
675
+ reason:
676
+ "Source removed an object with saved visual design. Restore or explicitly discard its design.",
677
+ });
678
+ continue;
679
+ }
680
+ for (const field of overlay.fields) {
681
+ if (semantic(state, overlay.target, field.path)) {
682
+ retained.push({
683
+ id: id(overlay.target, field.path, "field"),
684
+ target: overlay.target,
685
+ path: field.path,
686
+ kind: "field",
687
+ origin: origin(state, overlay.target),
688
+ status: "conflict",
689
+ before: get(
690
+ entity(input.generatedBaseline, overlay.target),
691
+ field.path,
692
+ ),
693
+ after: field.value,
694
+ source: get(obj, field.path),
695
+ reason:
696
+ "A saved visual property is now explicitly owned by QMD. Resolve this ownership change.",
697
+ });
698
+ } else set(obj, field.path, field.value);
699
+ }
700
+ }
701
+ for (const item of state.bentoOnly) {
702
+ const slide = doc.slides.find((s) => s.id === item.slideId);
703
+ if (!slide || slide.elements.some((e) => e.id === item.element.id)) {
704
+ retained.push({
705
+ id: id(
706
+ { slideId: item.slideId, elementId: item.element.id },
707
+ [],
708
+ "orphan",
709
+ ),
710
+ target: { slideId: item.slideId, elementId: item.element.id },
711
+ path: [],
712
+ kind: "orphan",
713
+ origin: "bento-only",
714
+ status: "conflict",
715
+ before: { present: false },
716
+ after: { present: true, value: item.element },
717
+ reason:
718
+ "Decoration has no source slide or its ID now collides with generated content.",
719
+ });
720
+ } else slide.elements.push(clone(item.element));
721
+ }
722
+ for (const pending of state.pending.filter((p) => p.kind !== "orphan")) {
723
+ const p = clone(pending), obj = entity(generated, p.target);
724
+ let source: Value;
725
+ if (p.kind === "slide-order") {
726
+ source = {
727
+ present: true,
728
+ value: generated.slides.map((s) => s.id),
729
+ };
730
+ } else {source = p.kind === "field"
731
+ ? get(obj, p.path)
732
+ : obj
733
+ ? { present: true, value: obj }
734
+ : { present: false };}
735
+ // A matching source independently confirms that particular edit; other pending fields survive.
736
+ const creation = p.kind === "create-slide" || p.kind === "create-element";
737
+ if (
738
+ !creation && p.origin !== "detached" && equal(source, p.after) &&
739
+ p.kind !== "delete-element"
740
+ ) {
741
+ continue;
742
+ }
743
+ p.source = source;
744
+ p.status = p.origin === "detached"
745
+ ? "bento-ahead"
746
+ : p.origin === "computed" || !equal(source, p.before)
747
+ ? "conflict"
748
+ : "bento-ahead";
749
+ if (creation && obj) {
750
+ p.status = "conflict";
751
+ p.reason =
752
+ "A source object now occupies this pending creation ID. Explicitly promote the approved synchronized creation to preserve its design; coincident IDs are not proof of approval.";
753
+ }
754
+ if (p.kind === "delete-element" && !obj) {
755
+ p.status = "conflict";
756
+ p.reason =
757
+ "The source element is absent without verified deletion approval. Retained content and design remain recoverable; review or recover the source before retiring them.";
758
+ }
759
+ if (p.kind === "field" && !obj) {
760
+ p.status = "conflict";
761
+ p.reason = "The source object for this pending edit no longer exists.";
762
+ }
763
+ if (p.status === "conflict" && !p.reason) {
764
+ p.reason =
765
+ "QMD and Bento changed the same field differently since their shared baseline.";
766
+ }
767
+ retained.push(p);
768
+ // Non-strict preview keeps pending values visible. Strict rendering refuses all conflicts below.
769
+ if (p.kind === "field") {
770
+ const target = entity(doc, p.target);
771
+ if (target) set(target, p.path, p.after);
772
+ } else if (p.kind === "create-slide" && !obj) {
773
+ doc.slides.push(clone(p.after.value as BentoSlide));
774
+ } else if (p.kind === "create-element" && !obj) {
775
+ doc.slides.find((s) => s.id === p.target.slideId)?.elements.push(
776
+ clone(p.after.value as BentoElement),
777
+ );
778
+ } else if (p.kind === "delete-slide") {
779
+ doc.slides = doc.slides.filter((s) => s.id !== p.target.slideId);
780
+ } else if (p.kind === "delete-element") {
781
+ const s = doc.slides.find((s) => s.id === p.target.slideId);
782
+ if (s) s.elements = s.elements.filter((e) => e.id !== p.target.elementId);
783
+ }
784
+ }
785
+ for (const order of state.elementOrders) {
786
+ const slide = doc.slides.find((s) => s.id === order.slideId);
787
+ if (slide) {
788
+ const rank = (el: BentoElement) => {
789
+ const i = order.ids.indexOf(el.id);
790
+ return i < 0 ? order.ids.length : i;
791
+ };
792
+ slide.elements.sort((a, b) => rank(a) - rank(b));
793
+ }
794
+ }
795
+ const order = retained.find((p) => p.kind === "slide-order");
796
+ if (order) {
797
+ const ids = order.after.value as string[],
798
+ rank = (s: BentoSlide) => {
799
+ const i = ids.indexOf(s.id);
800
+ return i < 0 ? ids.length : i;
801
+ };
802
+ doc.slides.sort((a, b) => rank(a) - rank(b));
803
+ }
804
+ state.pending = retained.filter((p, i, all) =>
805
+ all.findIndex((q) => q.id === p.id) === i
806
+ );
807
+ const conflicts = state.pending.filter((p) => p.status === "conflict");
808
+ if (options.strict !== false && conflicts.length) {
809
+ throw new StateError(
810
+ "MERGE_CONFLICT",
811
+ `${conflicts.length} unresolved change(s); inspect status/diff and explicitly resolve before rendering.`,
812
+ conflicts,
813
+ );
814
+ }
815
+ validateDoc(doc);
816
+ assertGraphDocument(doc, state.provenance, {
817
+ strict: options.strict,
818
+ baseline: input.emitted,
819
+ pending: state.pending,
820
+ });
821
+ state.generatedBaseline = clone(generated);
822
+ state.emitted = clone(doc);
823
+ return { state, document: doc, changes: clone(state.pending) };
824
+ }
825
+ /** Explicit field resolution. keep-source removes that pending value; detach records the exception. */
826
+ export function resolveChange(
827
+ input: ProjectState,
828
+ changeId: string,
829
+ action: "keep-source" | "detach",
830
+ ): ProjectState {
831
+ validateState(input);
832
+ const state = clone(input), p = state.pending.find((p) => p.id === changeId);
833
+ if (!p) {
834
+ throw new StateError("UNKNOWN_CHANGE", "Pending change was not found.");
835
+ }
836
+ if (action === "detach") {
837
+ if (p.kind !== "field" || p.origin !== "computed") {
838
+ throw new StateError(
839
+ "INVALID_DETACH",
840
+ "Only a computed field can be detached.",
841
+ );
842
+ }
843
+ p.origin = "detached";
844
+ p.status = "bento-ahead";
845
+ p.before = p.source ?? p.before;
846
+ delete p.reason;
847
+ } else {
848
+ state.pending = state.pending.filter((q) => q.id !== changeId);
849
+ if (p.kind === "field") {
850
+ const target = entity(state.emitted, p.target);
851
+ if (target) {
852
+ set(
853
+ target,
854
+ p.path,
855
+ get(entity(state.generatedBaseline, p.target), p.path),
856
+ );
857
+ }
858
+ }
859
+ if (p.kind === "orphan") {
860
+ state.overrides = state.overrides.filter((o) =>
861
+ !equal(o.target, p.target)
862
+ );
863
+ state.bentoOnly = state.bentoOnly.filter((b) =>
864
+ !(b.slideId === p.target.slideId && b.element.id === p.target.elementId)
865
+ );
866
+ }
867
+ }
868
+ return state;
869
+ }
870
+ export function validateState(value: unknown): asserts value is ProjectState {
871
+ assertJson(value);
872
+ const s = value as unknown as ProjectState;
873
+ if (!s || s.schemaVersion !== 1) {
874
+ throw new StateError(
875
+ "UNSUPPORTED_STATE_VERSION",
876
+ "Unsupported project-state version; do not overwrite it.",
877
+ );
878
+ }
879
+ if (!Number.isSafeInteger(s.revision) || s.revision < 0) {
880
+ throw new StateError(
881
+ "INVALID_STATE",
882
+ "State revision must be a nonnegative integer.",
883
+ );
884
+ }
885
+ validateDoc(s.generatedBaseline);
886
+ validateDoc(s.emitted);
887
+ if (s.docId !== s.generatedBaseline.docId || s.docId !== s.emitted.docId) {
888
+ throw new StateError(
889
+ "INVALID_STATE",
890
+ "State docId does not match its document snapshots.",
891
+ );
892
+ }
893
+ if (
894
+ !Array.isArray(s.overrides) || !Array.isArray(s.elementOrders) ||
895
+ !Array.isArray(s.bentoOnly) || !Array.isArray(s.pending) || !s.provenance ||
896
+ !s.ownership
897
+ ) throw new StateError("INVALID_STATE", "State collections are missing.");
898
+ const object = (v: unknown): v is Record<string, unknown> =>
899
+ !!v && typeof v === "object" && !Array.isArray(v);
900
+ const target = (v: unknown) =>
901
+ object(v) &&
902
+ (v.slideId === undefined || typeof v.slideId === "string" && !!v.slideId) &&
903
+ (v.elementId === undefined ||
904
+ typeof v.elementId === "string" && !!v.elementId &&
905
+ typeof v.slideId === "string");
906
+ const path = (v: unknown): v is string[] =>
907
+ Array.isArray(v) &&
908
+ v.every((k) =>
909
+ typeof k === "string" &&
910
+ !["__proto__", "constructor", "prototype"].includes(k)
911
+ );
912
+ const fieldValue = (v: unknown) =>
913
+ object(v) && typeof v.present === "boolean" &&
914
+ (!v.present || Object.hasOwn(v, "value"));
915
+ const fail = (message: string): never => {
916
+ throw new StateError("INVALID_STATE", message);
917
+ };
918
+ if (!object(s.provenance) || !object(s.ownership)) {
919
+ fail("Malformed provenance or ownership map.");
920
+ }
921
+ for (const provenance of Object.values(s.provenance)) {
922
+ if (
923
+ !object(provenance) ||
924
+ !["authored", "computed", "bento-only", "detached"].includes(
925
+ provenance.origin,
926
+ )
927
+ ) fail("Invalid provenance origin.");
928
+ }
929
+ for (const owned of Object.values(s.ownership)) {
930
+ if (!Array.isArray(owned) || !owned.every((v) => typeof v === "string")) {
931
+ fail("Invalid ownership paths.");
932
+ }
933
+ }
934
+ for (const o of s.overrides) {
935
+ if (!object(o) || !target(o.target) || !Array.isArray(o.fields)) {
936
+ fail("Malformed visual override.");
937
+ }
938
+ for (const f of o.fields) {
939
+ if (
940
+ !object(f) || !path(f.path) || !f.path.length || !fieldValue(f.value)
941
+ ) fail("Malformed visual field.");
942
+ }
943
+ }
944
+ for (const order of s.elementOrders) {
945
+ if (
946
+ !object(order) || typeof order.slideId !== "string" ||
947
+ !Array.isArray(order.ids) || !order.ids.every((id) =>
948
+ typeof id === "string"
949
+ ) || new Set(order.ids).size !== order.ids.length
950
+ ) fail("Invalid element order.");
951
+ }
952
+ for (const item of s.bentoOnly) {
953
+ if (!object(item) || typeof item.slideId !== "string" || !item.slideId) {
954
+ fail("Malformed decorative object.");
955
+ }
956
+ validateDoc({
957
+ docId: s.docId,
958
+ slides: [{ id: item.slideId, elements: [item.element] }],
959
+ });
960
+ }
961
+ const changeIds = new Set();
962
+ for (const p of s.pending) {
963
+ if (
964
+ !object(p) || typeof p.id !== "string" || !target(p.target) ||
965
+ !path(p.path) || !fieldValue(p.before) || !fieldValue(p.after) ||
966
+ (p.source !== undefined && !fieldValue(p.source)) ||
967
+ (p.restoreOrder !== undefined &&
968
+ (!["delete-slide", "delete-element"].includes(p.kind) ||
969
+ !Array.isArray(p.restoreOrder) ||
970
+ !p.restoreOrder.every((id) => typeof id === "string" && id) ||
971
+ new Set(p.restoreOrder).size !== p.restoreOrder.length ||
972
+ !p.target.slideId ||
973
+ !p.restoreOrder.includes(
974
+ p.kind === "delete-element"
975
+ ? p.target.elementId!
976
+ : p.target.slideId,
977
+ ))) ||
978
+ ![
979
+ "field",
980
+ "create-slide",
981
+ "create-element",
982
+ "delete-slide",
983
+ "delete-element",
984
+ "slide-order",
985
+ "orphan",
986
+ ].includes(p.kind) ||
987
+ !["authored", "computed", "bento-only", "detached"].includes(p.origin) ||
988
+ !["bento-ahead", "conflict", "bento-only"].includes(p.status) ||
989
+ changeIds.has(p.id)
990
+ ) fail("Malformed or duplicate pending edit.");
991
+ changeIds.add(p.id);
992
+ }
993
+ validateComponentCreations(s.componentCreations, s.docId);
994
+ validateComponentProjections(s.componentProjections, s.componentCreations);
995
+ if (s.widgets) {
996
+ const w = s.widgets,
997
+ digest = (v: unknown) =>
998
+ typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
999
+ if (
1000
+ w.version !== 1 || w.docId !== s.docId ||
1001
+ !/^[a-f0-9-]{36}$/.test(w.generation) ||
1002
+ w.generation !== s.output?.generation || !digest(w.sourceHash) ||
1003
+ !digest(w.runtimeSha256) || !digest(w.licenseSha256) ||
1004
+ !digest(w.presenterSha256) || !Array.isArray(w.entries) ||
1005
+ !w.entries.length || w.entries.length > 8
1006
+ ) fail("Invalid widget generation receipt.");
1007
+ const ids = new Set<string>();
1008
+ for (const [i, entry] of w.entries.entries()) {
1009
+ if (
1010
+ !object(entry) || typeof entry.slideId !== "string" ||
1011
+ typeof entry.elementId !== "string" || ids.has(targetKey(entry)) ||
1012
+ entry.file !== `widget-${i}.json` ||
1013
+ entry.presenter !== `widget-${i}.html` ||
1014
+ !digest(entry.presenterSha256) || !object(entry.receipt) ||
1015
+ entry.receipt.version !== 1 || !digest(entry.receipt.bundleHash) ||
1016
+ !digest(entry.receipt.specHash) ||
1017
+ entry.receipt.runtimeSha256 !== w.runtimeSha256 ||
1018
+ !object(entry.receipt.source) ||
1019
+ entry.receipt.source.sourceHash !== w.sourceHash ||
1020
+ entry.receipt.source.sourceId !== entry.elementId ||
1021
+ entry.receipt.source.slideId !== entry.slideId ||
1022
+ typeof entry.receipt.source.producerId !== "string" ||
1023
+ !digest(entry.receipt.source.chunkHash) ||
1024
+ !digest(entry.receipt.source.outputHash)
1025
+ ) fail("Invalid widget entry receipt.");
1026
+ ids.add(targetKey(entry));
1027
+ }
1028
+ }
1029
+ if (
1030
+ s.output &&
1031
+ (!object(s.output) || typeof s.output.path !== "string" ||
1032
+ !/^[a-f0-9]{64}$/.test(s.output.sha256))
1033
+ ) fail("Invalid emitted output receipt.");
1034
+ }