@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,384 @@
1
+ /** Reviewed state reconciliation; source writes remain the separate sync operation. */
2
+ import { join } from "node:path";
3
+ import {
4
+ assertProjectionIdentity,
5
+ projectProjection,
6
+ withProject,
7
+ } from "./project.ts";
8
+ import { hashSource } from "../source/mod.ts";
9
+ import { entity, equal, get } from "../state/json.ts";
10
+ import {
11
+ assertOutputUnchanged,
12
+ type BentoDoc,
13
+ canonical,
14
+ type Change,
15
+ clone,
16
+ compose,
17
+ type OwnershipMap,
18
+ type ProjectState,
19
+ type ProvenanceMap,
20
+ StateError,
21
+ targetKey,
22
+ validateDoc,
23
+ } from "../state/mod.ts";
24
+ export type ReconcileAction = "keep-source" | "keep-bento" | "detach";
25
+ export interface ReconcileChoice {
26
+ changeId: string;
27
+ action: ReconcileAction;
28
+ }
29
+ export interface ReconcileProjection {
30
+ document: BentoDoc;
31
+ provenance: ProvenanceMap;
32
+ ownership?: OwnershipMap;
33
+ sourceHash: string;
34
+ diagnostics?: unknown[];
35
+ }
36
+ export type ReconcileProjector = (
37
+ source: string,
38
+ state: ProjectState,
39
+ ) => Promise<ReconcileProjection>;
40
+ export interface ReconcileItem {
41
+ change: Change;
42
+ actions: ReconcileAction[];
43
+ }
44
+ export interface ReconcilePlan {
45
+ version: 1;
46
+ source: string;
47
+ sourceHash: string;
48
+ stateRevision: number;
49
+ stateHash: string;
50
+ projectionHash: string;
51
+ changes: ReconcileItem[];
52
+ document: BentoDoc;
53
+ planHash: string;
54
+ }
55
+ export interface ReconcileResult {
56
+ state: ProjectState;
57
+ resolved: ReconcileChoice[];
58
+ remaining: Change[];
59
+ renderNeeded: true;
60
+ sourceUnchanged: true;
61
+ }
62
+ function fail(code: string, message: string, details?: unknown): never {
63
+ throw new StateError(code, message, details);
64
+ }
65
+ const defaultProjector: ReconcileProjector = (source, state) =>
66
+ projectProjection(source, state);
67
+ function owned(projection: ReconcileProjection, change: Change): boolean {
68
+ const path = change.path.join("/");
69
+ return projection.ownership?.[targetKey(change.target)]?.some((p) =>
70
+ p === path || path.startsWith(p + "/")
71
+ ) ?? false;
72
+ }
73
+ function actionsFor(
74
+ change: Change,
75
+ projection: ReconcileProjection,
76
+ ): ReconcileAction[] {
77
+ const choices: ReconcileAction[] = ["keep-source"];
78
+ const source = entity(projection.document, change.target);
79
+ if (change.kind === "field" && source) {
80
+ if (change.origin === "computed" && !owned(projection, change)) {
81
+ choices.push("detach");
82
+ } else if (change.origin === "authored" || owned(projection, change)) {
83
+ choices.push("keep-bento");
84
+ }
85
+ } else if (
86
+ change.origin === "authored" &&
87
+ ["delete-slide", "delete-element", "slide-order"].includes(change.kind)
88
+ ) choices.push("keep-bento");
89
+ return choices;
90
+ }
91
+ async function noPendingApproval(stateDir: string) {
92
+ try {
93
+ await Deno.lstat(join(stateDir, "sync-approved.json"));
94
+ } catch (e) {
95
+ if (e instanceof Deno.errors.NotFound) return;
96
+ throw e;
97
+ }
98
+ fail(
99
+ "SYNC_APPROVAL_PENDING",
100
+ "Finish rendering the approved source synchronization before resolving unrelated state changes.",
101
+ );
102
+ }
103
+ async function projectionFor(
104
+ source: string,
105
+ state: ProjectState,
106
+ projector: ReconcileProjector,
107
+ ): Promise<ReconcileProjection> {
108
+ const before = hashSource(await Deno.readTextFile(source)),
109
+ projection = await projector(source, clone(state));
110
+ validateDoc(projection.document);
111
+ if (projection.document.docId !== state.docId) {
112
+ fail(
113
+ "DOC_ID_CHANGED",
114
+ "Conflict inspection must preserve project identity.",
115
+ );
116
+ }
117
+ if (
118
+ projection.sourceHash !== before ||
119
+ hashSource(await Deno.readTextFile(source)) !== before
120
+ ) {
121
+ fail(
122
+ "SOURCE_CHANGED",
123
+ "QMD changed during conflict inspection; execute a fresh preview.",
124
+ );
125
+ }
126
+ assertProjectionIdentity(state, projection.provenance, projection.sourceHash);
127
+ return { ...projection, ownership: projection.ownership ?? state.ownership };
128
+ }
129
+ function buildPlan(
130
+ source: string,
131
+ state: ProjectState,
132
+ projection: ReconcileProjection,
133
+ ): { plan: ReconcilePlan; candidate: ProjectState } {
134
+ const composed = compose(
135
+ state,
136
+ projection.document,
137
+ projection.provenance,
138
+ projection.ownership,
139
+ { strict: false },
140
+ );
141
+ const content = {
142
+ version: 1 as const,
143
+ source,
144
+ sourceHash: projection.sourceHash,
145
+ stateRevision: state.revision,
146
+ stateHash: hashSource(canonical(state)),
147
+ projectionHash: hashSource(
148
+ canonical({
149
+ document: projection.document,
150
+ provenance: projection.provenance,
151
+ ownership: projection.ownership ?? {},
152
+ sourceHash: projection.sourceHash,
153
+ }),
154
+ ),
155
+ changes: composed.changes.map((change) => ({
156
+ change,
157
+ actions: actionsFor(change, projection),
158
+ })),
159
+ document: composed.document,
160
+ };
161
+ return {
162
+ plan: { ...content, planHash: hashSource(canonical(content)) },
163
+ candidate: composed.state,
164
+ };
165
+ }
166
+ /** Executes a fresh projection but never saves project state or writes QMD/HTML. */
167
+ export async function previewReconcile(
168
+ source: string,
169
+ projector: ReconcileProjector = defaultProjector,
170
+ ): Promise<ReconcilePlan> {
171
+ return await withProject(source, async (paths, store) => {
172
+ const state = await store.load();
173
+ if (!state) {
174
+ fail("NO_BASELINE", "Render the project before inspecting divergence.");
175
+ }
176
+ await noPendingApproval(paths.stateDir);
177
+ return buildPlan(
178
+ paths.source,
179
+ state,
180
+ await projectionFor(paths.source, state, projector),
181
+ ).plan;
182
+ });
183
+ }
184
+ function resolveCandidate(
185
+ candidate: ProjectState,
186
+ projection: ReconcileProjection,
187
+ choices: ReconcileChoice[],
188
+ plan: ReconcilePlan,
189
+ ): ProjectState {
190
+ const state = clone(candidate);
191
+ if (
192
+ !choices.length ||
193
+ new Set(choices.map((c) => c.changeId)).size !== choices.length
194
+ ) {
195
+ fail(
196
+ "INVALID_RECONCILE_CHOICES",
197
+ "Choose at least one distinct reviewed change.",
198
+ );
199
+ }
200
+ for (const choice of choices) {
201
+ const item = plan.changes.find((i) => i.change.id === choice.changeId);
202
+ if (!item || !item.actions.includes(choice.action)) {
203
+ fail(
204
+ "INVALID_RECONCILE_CHOICE",
205
+ "This resolution is not allowed for the reviewed change.",
206
+ choice,
207
+ );
208
+ }
209
+ const pending = state.pending.find((p) => p.id === choice.changeId);
210
+ if (!pending) {
211
+ fail(
212
+ "OVERLAPPING_RECONCILE_CHOICES",
213
+ "An earlier choice already discarded this dependent change; review a new plan.",
214
+ choice,
215
+ );
216
+ }
217
+ // Source-owned visual properties may be represented by both a conflicting
218
+ // overlay and a pending field; leaving the overlay would recreate the conflict.
219
+ if (pending.kind === "field") {
220
+ state.overrides = state.overrides.map((o) =>
221
+ equal(o.target, pending.target)
222
+ ? {
223
+ ...o,
224
+ fields: o.fields.filter((f) => !equal(f.path, pending.path)),
225
+ }
226
+ : o
227
+ ).filter((o) => o.fields.length);
228
+ }
229
+ if (choice.action === "keep-source") {
230
+ state.pending = state.pending.filter((p) => p.id !== pending.id);
231
+ if (
232
+ pending.kind === "delete-element" && pending.target.elementId &&
233
+ entity(projection.document, pending.target)
234
+ ) {
235
+ // Removing only the deletion lets compose restore pending text and
236
+ // visual overrides; never restore a bare generated-baseline snapshot.
237
+ const slideId = pending.target.slideId!,
238
+ elementId = pending.target.elementId;
239
+ const order = state.elementOrders.find((item) =>
240
+ item.slideId === slideId
241
+ );
242
+ const ids = (order?.ids ?? state.emitted.slides.find((slide) =>
243
+ slide.id === slideId
244
+ )?.elements.map((element) =>
245
+ element.id
246
+ ) ?? [])
247
+ .filter((id) => id !== elementId);
248
+ const original = pending.restoreOrder ??
249
+ projection.document.slides.find((slide) => slide.id === slideId)!
250
+ .elements.map((element) => element.id);
251
+ const position = original.indexOf(elementId);
252
+ const following = original.slice(position + 1).find((id) =>
253
+ ids.includes(id)
254
+ );
255
+ const preceding = original.slice(0, position).reverse().find((id) =>
256
+ ids.includes(id)
257
+ );
258
+ const index = following
259
+ ? ids.indexOf(following)
260
+ : preceding
261
+ ? ids.indexOf(preceding) + 1
262
+ : ids.length;
263
+ ids.splice(index, 0, elementId);
264
+ state.elementOrders = state.elementOrders.filter((item) =>
265
+ item.slideId !== slideId
266
+ );
267
+ state.elementOrders.push({ slideId, ids });
268
+ }
269
+ if (
270
+ pending.kind === "orphan" || pending.kind === "create-slide" ||
271
+ pending.kind === "create-element"
272
+ ) {
273
+ const covers = (target: Change["target"]) =>
274
+ target.slideId === pending.target.slideId &&
275
+ (!pending.target.elementId ||
276
+ target.elementId === pending.target.elementId);
277
+ state.overrides = state.overrides.filter((o) => !covers(o.target));
278
+ state.bentoOnly = state.bentoOnly.filter((o) =>
279
+ !covers({ slideId: o.slideId, elementId: o.element.id })
280
+ );
281
+ state.pending = state.pending.filter((p) => !covers(p.target));
282
+ if (!pending.target.elementId) {
283
+ state.elementOrders = state.elementOrders.filter((o) =>
284
+ o.slideId !== pending.target.slideId
285
+ );
286
+ }
287
+ // Discarding a newly created slide also removes it from a pending order.
288
+ if (
289
+ pending.kind === "create-slide" &&
290
+ !entity(projection.document, pending.target)
291
+ ) {
292
+ for (const p of state.pending) {
293
+ if (p.kind === "slide-order" && p.after.present) {
294
+ p.after.value = (p.after.value as string[]).filter((id) =>
295
+ id !== pending.target.slideId
296
+ );
297
+ }
298
+ }
299
+ }
300
+ }
301
+ } else {
302
+ const current = entity(projection.document, pending.target);
303
+ pending.before = pending.kind === "field"
304
+ ? get(current, pending.path)
305
+ : pending.kind === "slide-order"
306
+ ? { present: true, value: projection.document.slides.map((s) => s.id) }
307
+ : current
308
+ ? { present: true, value: clone(current) }
309
+ : { present: false };
310
+ pending.source = clone(pending.before);
311
+ pending.status = "bento-ahead";
312
+ delete pending.reason;
313
+ pending.origin = choice.action === "detach" ? "detached" : "authored";
314
+ }
315
+ }
316
+ return compose(
317
+ state,
318
+ projection.document,
319
+ projection.provenance,
320
+ projection.ownership,
321
+ { strict: false },
322
+ ).state;
323
+ }
324
+ /** Re-executes authoritative inputs, verifies the complete reviewed plan, and saves only state. */
325
+ export async function applyReconcile(
326
+ source: string,
327
+ plan: ReconcilePlan,
328
+ choices: ReconcileChoice[],
329
+ projector: ReconcileProjector = defaultProjector,
330
+ ): Promise<ReconcileResult> {
331
+ return await withProject(source, async (paths, store) => {
332
+ const current = await store.load();
333
+ if (!current) {
334
+ fail("NO_BASELINE", "Render the project before resolving divergence.");
335
+ }
336
+ await noPendingApproval(paths.stateDir);
337
+ if (
338
+ !plan || plan.version !== 1 || plan.source !== paths.source ||
339
+ plan.stateRevision !== current.revision ||
340
+ plan.stateHash !== hashSource(canonical(current))
341
+ ) {
342
+ fail(
343
+ "STALE_RECONCILE_STATE",
344
+ "Project state changed after preview; inspect a new plan.",
345
+ );
346
+ }
347
+ if (plan.sourceHash !== hashSource(await Deno.readTextFile(paths.source))) {
348
+ fail(
349
+ "STALE_RECONCILE_SOURCE",
350
+ "QMD changed after preview; inspect a new plan.",
351
+ );
352
+ }
353
+ await assertOutputUnchanged(current, paths.output);
354
+ const projection = await projectionFor(paths.source, current, projector),
355
+ fresh = buildPlan(paths.source, current, projection);
356
+ if (!equal(plan, fresh.plan)) {
357
+ fail(
358
+ "STALE_RECONCILE_PLAN",
359
+ "Execution results or the reviewed plan changed. Inspect a new plan before choosing values.",
360
+ );
361
+ }
362
+ const resolved = resolveCandidate(
363
+ fresh.candidate,
364
+ projection,
365
+ choices,
366
+ plan,
367
+ );
368
+ if (hashSource(await Deno.readTextFile(paths.source)) !== plan.sourceHash) {
369
+ fail(
370
+ "STALE_RECONCILE_SOURCE",
371
+ "QMD changed before state save; no reconciliation was applied.",
372
+ );
373
+ }
374
+ await assertOutputUnchanged(current, paths.output);
375
+ const state = await store.save(resolved, current.revision);
376
+ return {
377
+ state,
378
+ resolved: clone(choices),
379
+ remaining: clone(state.pending),
380
+ renderNeeded: true,
381
+ sourceUnchanged: true,
382
+ };
383
+ });
384
+ }