@brianmsm/quarto-bento 0.0.0-stage → 0.1.1

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 +93 -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 +232 -0
  20. package/distribution/runtime/src/core/agent.ts +546 -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 +851 -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 +2557 -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 +892 -0
  100. package/distribution/runtime/src/studio/components.js +188 -0
  101. package/distribution/runtime/src/studio/index.html +283 -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 +461 -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,5 @@
1
+ export * from "./types.ts";
2
+ export { canonical, clone, equal, targetKey, validateDoc } from "./json.ts";
3
+ export * from "./merge.ts";
4
+ export * from "./store.ts";
5
+ export * from "./components.ts";
@@ -0,0 +1,226 @@
1
+ import { canonical, clone } from "./json.ts";
2
+ import { validateState } from "./merge.ts";
3
+ import { type BentoDoc, type ProjectState, StateError } from "./types.ts";
4
+ import { validateDoc } from "./json.ts";
5
+
6
+ export async function sha256(data: string | Uint8Array): Promise<string> {
7
+ const bytes = typeof data === "string"
8
+ ? new TextEncoder().encode(data)
9
+ : data;
10
+ return Array.from(
11
+ new Uint8Array(
12
+ await crypto.subtle.digest("SHA-256", bytes as BufferSource),
13
+ ),
14
+ ).map((n) => n.toString(16).padStart(2, "0")).join("");
15
+ }
16
+ async function readFile(path: string): Promise<string | undefined> {
17
+ try {
18
+ const info = await Deno.lstat(path);
19
+ if (!info.isFile || info.isSymlink) {
20
+ throw new StateError(
21
+ "UNSAFE_STATE_PATH",
22
+ `Expected a regular file at ${path}.`,
23
+ );
24
+ }
25
+ return await Deno.readTextFile(path);
26
+ } catch (error) {
27
+ if (error instanceof Deno.errors.NotFound) return undefined;
28
+ throw error;
29
+ }
30
+ }
31
+ async function durableWrite(path: string, content: string) {
32
+ const file = await Deno.open(path, {
33
+ write: true,
34
+ createNew: true,
35
+ mode: 0o600,
36
+ });
37
+ try {
38
+ const bytes = new TextEncoder().encode(content);
39
+ let offset = 0;
40
+ while (offset < bytes.length) {
41
+ offset += await file.write(bytes.subarray(offset));
42
+ }
43
+ await file.sync();
44
+ } finally {
45
+ file.close();
46
+ }
47
+ }
48
+ export class StateStore {
49
+ readonly path: string;
50
+ constructor(readonly directory: string) {
51
+ this.path = directory.replace(/\/$/, "") + "/state.json";
52
+ }
53
+ async load(): Promise<ProjectState | undefined> {
54
+ const content = await readFile(this.path);
55
+ if (content === undefined) return undefined;
56
+ return await this.decode(content);
57
+ }
58
+ private async decode(content: string): Promise<ProjectState> {
59
+ let envelope;
60
+ try {
61
+ envelope = JSON.parse(content);
62
+ } catch {
63
+ throw new StateError(
64
+ "CORRUPT_STATE",
65
+ "Project state is invalid JSON. Preserve it and inspect state.previous.json for recovery.",
66
+ );
67
+ }
68
+ if (
69
+ envelope?.format !== "qbento-state-envelope/1" ||
70
+ typeof envelope.sha256 !== "string"
71
+ ) {
72
+ throw new StateError(
73
+ "UNSUPPORTED_STATE_VERSION",
74
+ "Unsupported persistence envelope.",
75
+ );
76
+ }
77
+ validateState(envelope.state);
78
+ if (await sha256(canonical(envelope.state)) !== envelope.sha256) {
79
+ throw new StateError(
80
+ "CORRUPT_STATE",
81
+ "Project state checksum differs. Preserve it and explicitly recover from state.previous.json.",
82
+ );
83
+ }
84
+ return envelope.state;
85
+ }
86
+ /** Recoverable replacement, mutually exclusive writer, and compare-and-swap revision check. */
87
+ async save(
88
+ input: ProjectState,
89
+ expectedRevision = input.revision,
90
+ ): Promise<ProjectState> {
91
+ validateState(input);
92
+ await Deno.mkdir(this.directory, { recursive: true });
93
+ const info = await Deno.lstat(this.directory);
94
+ if (info.isSymlink || !info.isDirectory) {
95
+ throw new StateError(
96
+ "UNSAFE_STATE_PATH",
97
+ "Project state directory must be a real directory.",
98
+ );
99
+ }
100
+ const lock = this.directory + "/write.lock";
101
+ try {
102
+ await Deno.mkdir(lock);
103
+ } catch (error) {
104
+ if (error instanceof Deno.errors.AlreadyExists) {
105
+ throw new StateError(
106
+ "STATE_LOCKED",
107
+ "Another writer or interrupted transaction owns write.lock. Inspect the owner and recover explicitly; locks are never stolen.",
108
+ );
109
+ }
110
+ throw error;
111
+ }
112
+ const temp = this.path + ".tmp-" + crypto.randomUUID(),
113
+ backupTemp = this.path + ".previous-" + crypto.randomUUID();
114
+ try {
115
+ await Deno.writeTextFile(
116
+ lock + "/owner.json",
117
+ JSON.stringify({ pid: Deno.pid, started: new Date().toISOString() }),
118
+ { createNew: true },
119
+ );
120
+ const oldText = await readFile(this.path),
121
+ old = oldText === undefined ? undefined : await this.decode(oldText);
122
+ if (
123
+ (old?.revision ?? 0) !== expectedRevision ||
124
+ input.revision !== expectedRevision
125
+ ) {
126
+ throw new StateError(
127
+ "STALE_REVISION",
128
+ `State changed since revision ${expectedRevision}; reload before saving.`,
129
+ { actual: old?.revision ?? 0 },
130
+ );
131
+ }
132
+ if (old && old.docId !== input.docId) {
133
+ throw new StateError(
134
+ "DOC_ID_CHANGED",
135
+ "Cannot replace a project with a different document identity.",
136
+ );
137
+ }
138
+ const state = clone(input);
139
+ state.revision = expectedRevision + 1;
140
+ const content = JSON.stringify(
141
+ {
142
+ format: "qbento-state-envelope/1",
143
+ sha256: await sha256(canonical(state)),
144
+ state,
145
+ },
146
+ null,
147
+ 2,
148
+ ) + "\n";
149
+ await durableWrite(temp, content);
150
+ if (oldText !== undefined) {
151
+ await durableWrite(backupTemp, oldText);
152
+ await Deno.rename(backupTemp, this.directory + "/state.previous.json");
153
+ }
154
+ await Deno.rename(temp, this.path);
155
+ // fsync the directory on Unix; Windows directory handles do not support this operation.
156
+ if (Deno.build.os !== "windows") {
157
+ const dir = await Deno.open(this.directory, { read: true });
158
+ try {
159
+ await dir.sync();
160
+ } finally {
161
+ dir.close();
162
+ }
163
+ }
164
+ return state;
165
+ } finally {
166
+ await Promise.all([temp, backupTemp].map(async (path) => {
167
+ try {
168
+ await Deno.remove(path);
169
+ } catch (error) {
170
+ if (!(error instanceof Deno.errors.NotFound)) throw error;
171
+ }
172
+ }));
173
+ await Deno.remove(lock, { recursive: true });
174
+ }
175
+ }
176
+ /** Read, validate and return the backup; restoring it is a separate deliberate operation. */
177
+ async inspectRecovery(): Promise<ProjectState | undefined> {
178
+ const text = await readFile(this.directory + "/state.previous.json");
179
+ return text === undefined ? undefined : await this.decode(text);
180
+ }
181
+ }
182
+ /** Call before touching the output, including before Quarto may overwrite it. Missing HTML is rebuildable. */
183
+ export async function assertOutputUnchanged(
184
+ state: ProjectState | undefined,
185
+ path: string,
186
+ ): Promise<void> {
187
+ let bytes: Uint8Array;
188
+ try {
189
+ bytes = await Deno.readFile(path);
190
+ } catch (error) {
191
+ if (error instanceof Deno.errors.NotFound) return;
192
+ throw error;
193
+ }
194
+ if (!state?.output || state.output.path !== path) {
195
+ throw new StateError(
196
+ "UNTRACKED_OUTPUT",
197
+ `Existing output ${path} has no captured baseline. Capture it explicitly or choose a different output.`,
198
+ );
199
+ }
200
+ if (await sha256(bytes) !== state.output.sha256) {
201
+ throw new StateError(
202
+ "UNCAPTURED_HTML_EDITS",
203
+ `Output ${path} changed since last emission/capture. Capture its native Bento edits before regenerating.`,
204
+ );
205
+ }
206
+ }
207
+ /** Record only HTML whose write/capture the caller has completed successfully. */
208
+ export async function markEmitted(
209
+ input: ProjectState,
210
+ document: BentoDoc,
211
+ path: string,
212
+ html: string | Uint8Array,
213
+ ): Promise<ProjectState> {
214
+ validateState(input);
215
+ validateDoc(document);
216
+ if (document.docId !== input.docId) {
217
+ throw new StateError(
218
+ "DOC_ID_CHANGED",
219
+ "Output identity differs from project.",
220
+ );
221
+ }
222
+ const state = clone(input);
223
+ state.emitted = clone(document);
224
+ state.output = { ...input.output, path, sha256: await sha256(html) };
225
+ return state;
226
+ }
@@ -0,0 +1,151 @@
1
+ /** Versioned JSON-only persistence contract; no upstream runtime dependency. */
2
+ export type Json = null | boolean | number | string | Json[] | {
3
+ [key: string]: Json;
4
+ };
5
+ export interface JsonObject {
6
+ [key: string]: Json;
7
+ }
8
+ export interface BentoElement extends JsonObject {
9
+ id: string;
10
+ type: string;
11
+ }
12
+ export interface BentoSlide extends JsonObject {
13
+ id: string;
14
+ elements: BentoElement[];
15
+ }
16
+ export interface BentoDoc extends JsonObject {
17
+ docId: string;
18
+ slides: BentoSlide[];
19
+ }
20
+ export interface Target {
21
+ slideId?: string;
22
+ elementId?: string;
23
+ }
24
+ export interface Value {
25
+ present: boolean;
26
+ value?: Json;
27
+ }
28
+ export interface Field {
29
+ path: string[];
30
+ value: Value;
31
+ }
32
+ export type Origin = "authored" | "computed" | "bento-only" | "detached";
33
+ export interface Provenance {
34
+ origin: Origin;
35
+ semanticId?: string;
36
+ producer?: string;
37
+ source?: JsonObject;
38
+ }
39
+ /** Key with targetKey({slideId, elementId}); '/'-separated paths also accepted in ownership. */
40
+ export type ProvenanceMap = Record<string, Provenance>;
41
+ export type OwnershipMap = Record<string, string[]>;
42
+ export interface Override {
43
+ target: Target;
44
+ fields: Field[];
45
+ }
46
+ export interface Change {
47
+ id: string;
48
+ target: Target;
49
+ path: string[];
50
+ kind:
51
+ | "field"
52
+ | "create-slide"
53
+ | "create-element"
54
+ | "delete-slide"
55
+ | "delete-element"
56
+ | "slide-order"
57
+ | "orphan";
58
+ origin: Origin;
59
+ status: "bento-ahead" | "conflict" | "bento-only";
60
+ before: Value;
61
+ after: Value;
62
+ /** Observed slide or element order before a pending deletion, for reversible restoration. */
63
+ restoreOrder?: string[];
64
+ source?: Value;
65
+ reason?: string;
66
+ }
67
+ export interface PublicationReceipt {
68
+ version: 1;
69
+ generation: string;
70
+ docId: string;
71
+ publishedDocSha256: string;
72
+ }
73
+ export interface OutputReceipt {
74
+ path: string;
75
+ sha256: string;
76
+ generation?: string;
77
+ publishedDocSha256?: string;
78
+ captureConsumed?: boolean;
79
+ }
80
+ export interface WidgetPublication {
81
+ version: 1;
82
+ docId: string;
83
+ generation: string;
84
+ sourceHash: string;
85
+ runtimeSha256: string;
86
+ licenseSha256: string;
87
+ presenterSha256: string;
88
+ entries: {
89
+ slideId: string;
90
+ elementId: string;
91
+ file: string;
92
+ presenter: string;
93
+ presenterSha256: string;
94
+ receipt: JsonObject;
95
+ }[];
96
+ }
97
+ /** Immutable measured creation frame; native decoration geometry is a separate overlay. */
98
+ export interface ComponentFrame {
99
+ x: number;
100
+ y: number;
101
+ w: number;
102
+ h: number;
103
+ }
104
+ /** Approved source component bootstrap, retained independently from its native background. */
105
+ export interface ComponentProjection {
106
+ version: 1;
107
+ id: string;
108
+ kind: "card" | "kpi";
109
+ slideId: string;
110
+ frame: ComponentFrame;
111
+ }
112
+ /** Explicit authored component intent; snapshots keep removed members recoverable. */
113
+ export interface ComponentCreation {
114
+ version: 1;
115
+ id: string;
116
+ kind: "card" | "kpi";
117
+ slideId: string;
118
+ backgroundId: string;
119
+ backgroundSnapshot: BentoElement;
120
+ projectionFrame: ComponentFrame;
121
+ restoreOrder: string[];
122
+ members: {
123
+ role: "title" | "body" | "label" | "value" | "detail";
124
+ sourceId: string;
125
+ nativeId: string;
126
+ snapshot: BentoElement;
127
+ }[];
128
+ }
129
+ export interface ProjectState {
130
+ schemaVersion: 1;
131
+ revision: number;
132
+ docId: string;
133
+ generatedBaseline: BentoDoc;
134
+ emitted: BentoDoc;
135
+ provenance: ProvenanceMap;
136
+ ownership: OwnershipMap;
137
+ overrides: Override[];
138
+ elementOrders: { slideId: string; ids: string[] }[];
139
+ bentoOnly: { slideId: string; element: BentoElement }[];
140
+ pending: Change[];
141
+ componentCreations?: ComponentCreation[];
142
+ componentProjections?: ComponentProjection[];
143
+ output?: OutputReceipt;
144
+ widgets?: WidgetPublication;
145
+ }
146
+ export class StateError extends Error {
147
+ constructor(public code: string, message: string, public details?: unknown) {
148
+ super(message);
149
+ this.name = "StateError";
150
+ }
151
+ }