@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,354 @@
1
+ /** Explicit source diagram relationships; saving never writes QMD or executes it. */
2
+ import { projectPaths, readProject, saveProject } from "./project.ts";
3
+ import { nativeGraphSlide } from "./graph-scope.ts";
4
+ import { canonical, clone, set, targetKey } from "../state/json.ts";
5
+ import { assertGraphDocument, graphReceipt } from "../state/graphs.ts";
6
+ import {
7
+ type JsonObject,
8
+ type ProjectState,
9
+ StateError,
10
+ } from "../state/types.ts";
11
+ import { hashSource, scanSource } from "../source/mod.ts";
12
+ import {
13
+ graphEdgeId,
14
+ graphNodeId,
15
+ sourceGraphs,
16
+ timelineLinkId,
17
+ } from "../source/graphs.ts";
18
+
19
+ export interface RelationGraph {
20
+ slideId: string;
21
+ slideLabel: string;
22
+ componentId: string;
23
+ kind: "diagram" | "timeline";
24
+ nodes: { id: string; label: string }[];
25
+ edges: {
26
+ id: string;
27
+ label: string;
28
+ fromNodeId: string;
29
+ toNodeId: string;
30
+ detached: boolean;
31
+ }[];
32
+ }
33
+ export interface GraphRelations {
34
+ docId: string;
35
+ revision: number;
36
+ sourceHash: string;
37
+ graphs: RelationGraph[];
38
+ }
39
+ export interface GraphRelationRequest {
40
+ expectedDocId: string;
41
+ expectedRevision: number;
42
+ expectedSourceHash: string;
43
+ slideId: string;
44
+ componentId: string;
45
+ edgeId: string;
46
+ fromNodeId: string;
47
+ toNodeId: string;
48
+ }
49
+ function fail(code: string, message: string): never {
50
+ throw new StateError(code, message);
51
+ }
52
+ function catalog(state: ProjectState, text: string): GraphRelations {
53
+ const source = scanSource(text);
54
+ for (const proof of Object.values(state.provenance)) {
55
+ if (graphReceipt(proof) && proof.source?.hash !== source.hash) {
56
+ fail(
57
+ "STALE_SOURCE",
58
+ "QMD cambió desde la última proyección. Recalcula o revisa sus diferencias; tus cambios pendientes siguen guardados.",
59
+ );
60
+ }
61
+ }
62
+ const graphs = sourceGraphs(source).map((graph): RelationGraph => {
63
+ const slideId = nativeGraphSlide(source, state, graph),
64
+ slide = state.emitted.slides.find((slide) => slide.id === slideId),
65
+ baseline = state.generatedBaseline.slides.find((slide) =>
66
+ slide.id === slideId
67
+ );
68
+ if (!slide || !baseline) {
69
+ fail(
70
+ "GRAPH_PENDING_DELETION",
71
+ "Restaura la diapositiva del gráfico antes de cambiar sus relaciones.",
72
+ );
73
+ }
74
+ const originalIds = graph.nodes.map((node) => node.block.id);
75
+ const member = (
76
+ nativeId: string,
77
+ sourceId: string,
78
+ role: string,
79
+ labelId?: string,
80
+ ) => {
81
+ const provenance =
82
+ state.provenance[targetKey({ slideId, elementId: nativeId })],
83
+ proof = graphReceipt(provenance),
84
+ native = slide.elements.find((element) => element.id === nativeId),
85
+ generated = baseline.elements.find((element) =>
86
+ element.id === nativeId
87
+ );
88
+ if (
89
+ !proof || provenance.origin !== "authored" ||
90
+ proof.componentId !== graph.wrapper.id ||
91
+ proof.sourceId !== sourceId || proof.role !== role ||
92
+ proof.kind !== graph.kind ||
93
+ proof.labelId !== labelId ||
94
+ canonical(proof.nodeIds) !== canonical(originalIds) ||
95
+ provenance.source?.hash !== source.hash || !generated
96
+ ) {
97
+ fail(
98
+ "UNMAPPED_GRAPH_SOURCE",
99
+ "Este gráfico necesita una proyección fuente verificada antes de editar relaciones.",
100
+ );
101
+ }
102
+ if (
103
+ !native ||
104
+ (labelId && !slide.elements.some((element) => element.id === labelId))
105
+ ) {
106
+ fail(
107
+ "GRAPH_PENDING_DELETION",
108
+ "Restaura los nodos, etiquetas y conectores pendientes de eliminación antes de editar relaciones.",
109
+ );
110
+ }
111
+ if (
112
+ native.type !== "shape" ||
113
+ (role === "node" || role === "milestone"
114
+ ? ["line", "path"].includes(String(native.shape))
115
+ : native.shape !== "line") ||
116
+ (labelId &&
117
+ slide.elements.find((element) => element.id === labelId)?.type !==
118
+ "text")
119
+ ) {
120
+ fail(
121
+ "GRAPH_PENDING_STRUCTURE",
122
+ "Restaura el tipo nativo de los nodos, textos y conectores antes de editar relaciones.",
123
+ );
124
+ }
125
+ return { native, generated, proof };
126
+ };
127
+ const nodes = graph.nodes.map((node) => {
128
+ member(
129
+ graphNodeId(node.block.id),
130
+ node.block.id,
131
+ graph.kind === "diagram" ? "node" : "milestone",
132
+ node.label!.id,
133
+ );
134
+ return { id: node.block.id, label: node.label!.content.trim() };
135
+ });
136
+ const links = graph.kind === "diagram"
137
+ ? graph.edges.map((edge) => ({
138
+ id: edge.block.id,
139
+ nativeId: graphEdgeId(edge.block.id),
140
+ sourceId: edge.block.id,
141
+ from: edge.from,
142
+ to: edge.to,
143
+ labelId: edge.label?.id,
144
+ label: edge.label?.content.trim() ?? "",
145
+ }))
146
+ : nodes.slice(0, -1).map((node, index) => ({
147
+ id: timelineLinkId(graph.wrapper.id, node.id, nodes[index + 1].id),
148
+ nativeId: timelineLinkId(
149
+ graph.wrapper.id,
150
+ node.id,
151
+ nodes[index + 1].id,
152
+ ),
153
+ sourceId: graph.wrapper.id,
154
+ from: node.id,
155
+ to: nodes[index + 1].id,
156
+ labelId: undefined,
157
+ label: "",
158
+ }));
159
+ const edges = links.map((link) => {
160
+ const { native, generated, proof } = member(
161
+ link.nativeId,
162
+ link.sourceId,
163
+ graph.kind === "diagram" ? "edge" : "timeline-link",
164
+ link.labelId,
165
+ );
166
+ const sourceNode = (endpoint: "from" | "to") => {
167
+ if (
168
+ proof[endpoint] !== link[endpoint] ||
169
+ (generated[endpoint] as JsonObject | undefined)?.el !==
170
+ graphNodeId(link[endpoint])
171
+ ) {
172
+ fail(
173
+ "UNMAPPED_GRAPH_SOURCE",
174
+ "La relación original no coincide con su proyección verificada.",
175
+ );
176
+ }
177
+ const id = (native[endpoint] as JsonObject | undefined)?.el;
178
+ if (id === undefined) return { id: link[endpoint], detached: true };
179
+ const node = nodes.find((node) => graphNodeId(node.id) === id);
180
+ if (!node) {
181
+ fail(
182
+ "GRAPH_ENDPOINT_SCOPE",
183
+ "La relación apunta fuera de los nodos originales del mismo gráfico.",
184
+ );
185
+ }
186
+ return { id: node.id, detached: false };
187
+ };
188
+ const from = sourceNode("from"), to = sourceNode("to");
189
+ return {
190
+ id: link.id,
191
+ label: link.label,
192
+ fromNodeId: from.id,
193
+ toNodeId: to.id,
194
+ detached: from.detached || to.detached,
195
+ };
196
+ });
197
+ return {
198
+ slideId,
199
+ slideLabel: source.blocks.find((block) =>
200
+ block.id === graph.wrapper.slideId && block.kind === "heading"
201
+ )?.content.trim() ?? slideId,
202
+ componentId: graph.wrapper.id,
203
+ kind: graph.kind,
204
+ nodes,
205
+ edges,
206
+ };
207
+ });
208
+ assertGraphDocument(state.emitted, state.provenance, {
209
+ capture: true,
210
+ baseline: state.generatedBaseline,
211
+ });
212
+ return {
213
+ docId: state.docId,
214
+ revision: state.revision,
215
+ sourceHash: source.hash,
216
+ graphs,
217
+ };
218
+ }
219
+
220
+ /** Read only; selectors come from exact source roles and verified saved native members. */
221
+ export async function readGraphRelations(
222
+ source: string,
223
+ ): Promise<GraphRelations> {
224
+ return catalog(
225
+ await readProject(source),
226
+ await Deno.readTextFile(projectPaths(source).source),
227
+ );
228
+ }
229
+ /** One bounded atomic pair update, including explicit reconstruction after native detachment. */
230
+ export async function saveGraphRelation(
231
+ source: string,
232
+ request: GraphRelationRequest,
233
+ ) {
234
+ if (
235
+ !request || typeof request !== "object" || Array.isArray(request) ||
236
+ Object.keys(request).sort().join(",") !==
237
+ "componentId,edgeId,expectedDocId,expectedRevision,expectedSourceHash,fromNodeId,slideId,toNodeId" ||
238
+ !Number.isSafeInteger(request.expectedRevision) ||
239
+ request.expectedRevision < 0 ||
240
+ ![
241
+ request.expectedDocId,
242
+ request.slideId,
243
+ request.componentId,
244
+ request.edgeId,
245
+ request.fromNodeId,
246
+ request.toNodeId,
247
+ ].every((value) => typeof value === "string" && value.length > 0) ||
248
+ typeof request.expectedSourceHash !== "string" ||
249
+ !/^[a-f0-9]{64}$/.test(request.expectedSourceHash)
250
+ ) {
251
+ fail(
252
+ "INVALID_GRAPH_RELATION",
253
+ "Selecciona una relación con identidad, revisión y fuente verificadas.",
254
+ );
255
+ }
256
+ const state = await readProject(source);
257
+ if (state.docId !== request.expectedDocId) {
258
+ fail(
259
+ "UNRELATED_DOCUMENT",
260
+ "La relación pertenece a otro documento; recarga el proyecto.",
261
+ );
262
+ }
263
+ if (state.revision !== request.expectedRevision) {
264
+ fail(
265
+ "STALE_REVISION",
266
+ "Otro guardado cambió el proyecto; recarga las relaciones antes de guardar.",
267
+ );
268
+ }
269
+ const text = await Deno.readTextFile(projectPaths(source).source);
270
+ if (hashSource(text) !== request.expectedSourceHash) {
271
+ fail(
272
+ "STALE_SOURCE",
273
+ "QMD cambió después de abrir las relaciones; recarga y revisa antes de guardar.",
274
+ );
275
+ }
276
+ const snapshot = catalog(state, text),
277
+ graph = snapshot.graphs.find((graph) =>
278
+ graph.slideId === request.slideId &&
279
+ graph.componentId === request.componentId
280
+ );
281
+ if (!graph) {
282
+ fail(
283
+ "GRAPH_ENDPOINT_SCOPE",
284
+ "Selecciona un diagrama QMD existente en su diapositiva original.",
285
+ );
286
+ }
287
+ if (graph.kind !== "diagram") {
288
+ fail(
289
+ "UNSUPPORTED_TIMELINE_RELATION",
290
+ "La secuencia deriva del orden de hitos en QMD; sus relaciones son de solo lectura.",
291
+ );
292
+ }
293
+ if (
294
+ !graph.edges.some((edge) => edge.id === request.edgeId) ||
295
+ !graph.nodes.some((node) => node.id === request.fromNodeId) ||
296
+ !graph.nodes.some((node) => node.id === request.toNodeId) ||
297
+ request.fromNodeId === request.toNodeId
298
+ ) {
299
+ fail(
300
+ "GRAPH_ENDPOINT_SCOPE",
301
+ "Selecciona una arista y dos nodos distintos del mismo diagrama.",
302
+ );
303
+ }
304
+ if (
305
+ state.pending.some((change) =>
306
+ change.kind === "field" && change.target.slideId === graph.slideId &&
307
+ change.target.elementId === graphEdgeId(request.edgeId) &&
308
+ ["from", "to"].includes(change.path[0]) &&
309
+ (change.path.length === 1 || change.path[1] === "el") &&
310
+ change.status === "conflict"
311
+ )
312
+ ) {
313
+ fail(
314
+ "GRAPH_RELATION_CONFLICT",
315
+ "Esta relación tiene un conflicto con QMD. Resuélvelo desde Comparar con QMD antes de elegir nuevos nodos; las demás ediciones siguen guardadas.",
316
+ );
317
+ }
318
+ const doc = clone(state.emitted),
319
+ edge = doc.slides.find((slide) => slide.id === graph.slideId)!.elements
320
+ .find((element) => element.id === graphEdgeId(request.edgeId))!,
321
+ baseline = state.generatedBaseline.slides.find((slide) =>
322
+ slide.id === graph.slideId
323
+ )!.elements.find((element) => element.id === edge.id)!;
324
+ for (const endpoint of ["from", "to"] as const) {
325
+ const current = edge[endpoint];
326
+ const present = current && typeof current === "object" &&
327
+ !Array.isArray(current);
328
+ const anchor = clone(present ? current : baseline[endpoint] as JsonObject);
329
+ if (!present) {
330
+ const overlay = state.overrides.find((overlay) =>
331
+ overlay.target.slideId === graph.slideId &&
332
+ overlay.target.elementId === edge.id
333
+ );
334
+ for (const field of overlay?.fields ?? []) {
335
+ if (
336
+ field.path.length === 2 && field.path[0] === endpoint &&
337
+ field.path[1] === "side"
338
+ ) set(anchor, ["side"], field.value);
339
+ }
340
+ }
341
+ edge[endpoint] = {
342
+ ...anchor,
343
+ el: graphNodeId(
344
+ endpoint === "from" ? request.fromNodeId : request.toNodeId,
345
+ ),
346
+ };
347
+ }
348
+ return await saveProject(
349
+ source,
350
+ doc,
351
+ request.expectedRevision,
352
+ request.expectedSourceHash,
353
+ );
354
+ }
@@ -0,0 +1,63 @@
1
+ /** Weak source heading scope is an executed receipt, never a slug/position guess. */
2
+ import type { SourceDocument } from "../source/mod.ts";
3
+ import type { SourceGraph } from "../source/graphs.ts";
4
+ import { graphReceipt } from "../state/graphs.ts";
5
+ import { type ProjectState, StateError } from "../state/types.ts";
6
+ import { verifiedSourceSlide } from "./source-scope.ts";
7
+
8
+ function fail(): never {
9
+ throw new StateError(
10
+ "UNMAPPED_GRAPH_SOURCE",
11
+ "A graph under an unpinned heading needs its unique exact executed heading and native scope receipts. Recalculate and review the original source before editing relationships.",
12
+ );
13
+ }
14
+ export function nativeGraphSlide(
15
+ source: SourceDocument,
16
+ state: ProjectState,
17
+ graph: SourceGraph,
18
+ ): string {
19
+ const slides = source.slides.filter((slide) =>
20
+ slide.id === graph.wrapper.slideId
21
+ );
22
+ if (slides.length !== 1) fail();
23
+ const slide = slides[0];
24
+ const members = Object.entries(state.provenance).filter(([, provenance]) => {
25
+ const proof = graphReceipt(provenance);
26
+ return proof?.componentId === graph.wrapper.id;
27
+ });
28
+ if (!members.length) fail();
29
+ if (slide.stable) {
30
+ // Older explicit-ID receipts have no alias fields; any fields retained by
31
+ // current projections must still agree with their exact native scope.
32
+ for (const [key, provenance] of members) {
33
+ const [nativeSlideId] = JSON.parse(key) as [string],
34
+ proof = graphReceipt(provenance)!;
35
+ if (
36
+ nativeSlideId !== slide.id ||
37
+ proof.nativeSlideId !== undefined && proof.nativeSlideId !== slide.id ||
38
+ proof.sourceSlideId !== undefined && proof.sourceSlideId !== slide.id
39
+ ) fail();
40
+ }
41
+ return slide.id;
42
+ }
43
+ const scope = graphReceipt(members[0][1])!.nativeSlideId;
44
+ if (typeof scope !== "string" || !scope) fail();
45
+ for (const [key, provenance] of members) {
46
+ const [nativeSlideId] = JSON.parse(key) as [string],
47
+ proof = graphReceipt(provenance)!;
48
+ if (
49
+ nativeSlideId !== scope || proof.nativeSlideId !== scope ||
50
+ proof.sourceSlideId !== slide.id ||
51
+ proof.kind !== graph.kind || provenance.origin !== "authored" ||
52
+ provenance.source?.hash !== source.hash
53
+ ) fail();
54
+ }
55
+ try {
56
+ return verifiedSourceSlide(source, state, slide.id, scope);
57
+ } catch (error) {
58
+ if (error instanceof StateError && error.code === "UNMAPPED_SOURCE_SLIDE") {
59
+ fail();
60
+ }
61
+ throw error;
62
+ }
63
+ }
@@ -0,0 +1,228 @@
1
+ /** Source-specific relationship synchronization and prospective measured graph gates. */
2
+ import { canonical, entity, get, set, targetKey } from "../state/json.ts";
3
+ import { graphReceipt, graphRelationshipPath } from "../state/graphs.ts";
4
+ import {
5
+ type Change,
6
+ type JsonObject,
7
+ type ProjectState,
8
+ StateError,
9
+ } from "../state/types.ts";
10
+ import {
11
+ type GraphEndpointRequest,
12
+ graphFlowSignature,
13
+ graphNodeId,
14
+ sourceGraph,
15
+ sourceGraphs,
16
+ } from "../source/graphs.ts";
17
+ import { scanSource } from "../source/mod.ts";
18
+ import { type Frame } from "../render/design.ts";
19
+ import {
20
+ assertGraphTypography,
21
+ originalGraphNode,
22
+ projectGraph,
23
+ } from "../render/graphs.ts";
24
+ import type { BentoDoc as RenderDoc } from "../render/types.ts";
25
+ import { nativeGraphSlide } from "./graph-scope.ts";
26
+
27
+ function fail(code: string, message: string): never {
28
+ throw new StateError(code, message);
29
+ }
30
+ /** Neither native group IDs nor generic connector objects establish semantic membership. */
31
+ export function graphSyncRequest(
32
+ text: string,
33
+ state: ProjectState,
34
+ change: Change,
35
+ ): GraphEndpointRequest | undefined {
36
+ const source = state.provenance[targetKey(change.target)];
37
+ if (!graphRelationshipPath(source, change.path)) return undefined;
38
+ const proof = graphReceipt(source)!;
39
+ if (proof.role === "timeline-link") {
40
+ fail(
41
+ "UNSUPPORTED_TIMELINE_RELATION",
42
+ "Timeline relationships derive from lexical QMD order. Restore native endpoints or reorder the original milestone blocks.",
43
+ );
44
+ }
45
+ if (
46
+ proof.role !== "edge" || proof.kind !== "diagram" ||
47
+ change.origin !== "authored" ||
48
+ change.target.elementId !== proof.sourceId + "/qb-edge" ||
49
+ !change.target.slideId
50
+ ) {
51
+ fail(
52
+ "UNMAPPED_GRAPH_SOURCE",
53
+ "Relationship edits require the original source diagram edge receipt.",
54
+ );
55
+ }
56
+ const endpoint = change.path[0] as "from" | "to";
57
+ let nativeId: unknown = change.after.value;
58
+ if (
59
+ change.path.length === 1 && nativeId && typeof nativeId === "object" &&
60
+ !Array.isArray(nativeId)
61
+ ) nativeId = (nativeId as JsonObject).el;
62
+ if (!change.after.present || typeof nativeId !== "string") {
63
+ fail(
64
+ "GRAPH_ENDPOINT_REQUIRED",
65
+ "A source edge endpoint needs an explicit node; removing/detaching endpoints cannot rewrite a faithful source edge.",
66
+ );
67
+ }
68
+ const saved = get(entity(state.emitted, change.target), [endpoint, "el"]);
69
+ if (!saved.present || saved.value !== nativeId) {
70
+ fail(
71
+ "GRAPH_RELATION_SNAPSHOT",
72
+ "The pending relationship no longer matches the saved native connector. Reopen Relations or compare with QMD before approving a source patch; all edits remain saved.",
73
+ );
74
+ }
75
+ const doc = scanSource(text), graph = sourceGraph(doc, proof.componentId);
76
+ if (
77
+ nativeGraphSlide(doc, state, graph) !== change.target.slideId ||
78
+ canonical(graph.nodes.map((node) => node.block.id).sort()) !==
79
+ canonical([...proof.nodeIds].sort())
80
+ ) {
81
+ fail(
82
+ "SOURCE_CONFLICT",
83
+ "The original graph membership or slide scope changed; render and review the graph before synchronizing relationships.",
84
+ );
85
+ }
86
+ const edge = graph.edges.find((edge) => edge.block.id === proof.sourceId);
87
+ const expected = proof[endpoint];
88
+ if (!edge || typeof expected !== "string" || edge[endpoint] !== expected) {
89
+ fail(
90
+ "SOURCE_CONFLICT",
91
+ "QMD independently changed this source edge endpoint; compare and resolve before synchronization.",
92
+ );
93
+ }
94
+ const node = graph.nodes.find((node) =>
95
+ graphNodeId(node.block.id) === nativeId
96
+ );
97
+ const nativeProof = node &&
98
+ graphReceipt(
99
+ state
100
+ .provenance[
101
+ targetKey({ slideId: change.target.slideId, elementId: nativeId })
102
+ ],
103
+ );
104
+ if (
105
+ !node || nativeProof?.role !== "node" ||
106
+ nativeProof.componentId !== proof.componentId ||
107
+ nativeProof.sourceId !== node.block.id ||
108
+ !state.emitted.slides.find((slide) => slide.id === change.target.slideId)
109
+ ?.elements.some((element) => element.id === nativeId)
110
+ ) {
111
+ fail(
112
+ "GRAPH_ENDPOINT_SCOPE",
113
+ "Choose an existing direct native source node shape from the same diagram.",
114
+ );
115
+ }
116
+ const baseline = get(entity(state.generatedBaseline, change.target), [
117
+ endpoint,
118
+ "el",
119
+ ]);
120
+ if (!baseline.present || baseline.value !== graphNodeId(expected)) {
121
+ fail(
122
+ "UNMAPPED_GRAPH_SOURCE",
123
+ "The saved source edge receipt no longer agrees with its generated endpoint baseline.",
124
+ );
125
+ }
126
+ return {
127
+ componentId: proof.componentId,
128
+ edgeId: proof.sourceId,
129
+ endpoint,
130
+ expected,
131
+ nodeId: node.block.id,
132
+ };
133
+ }
134
+ function checkedBase(proof: JsonObject): Frame {
135
+ const value = proof.base;
136
+ if (
137
+ !value || typeof value !== "object" || Array.isArray(value) ||
138
+ !["x", "y", "w", "h"].every((key) =>
139
+ typeof value[key] === "number" && Number.isFinite(value[key])
140
+ )
141
+ ) {
142
+ fail(
143
+ "UNMAPPED_GRAPH_SOURCE",
144
+ "A graph requires its fresh executed automatic-layout base.",
145
+ );
146
+ }
147
+ return value as unknown as Frame;
148
+ }
149
+ /** Combined proposed QMD plus retained native design, before a review can approve any writes. */
150
+ export async function preflightGraphSync(
151
+ state: ProjectState,
152
+ current: string,
153
+ result: string,
154
+ ): Promise<void> {
155
+ const currentDoc = scanSource(current), prospective = scanSource(result);
156
+ const theme = state.generatedBaseline.theme as RenderDoc["theme"];
157
+ for (const graph of sourceGraphs(prospective)) {
158
+ const receipts = Object.values(state.provenance).map(graphReceipt).filter((
159
+ proof,
160
+ ) => proof?.componentId === graph.wrapper.id);
161
+ const proof = receipts[0];
162
+ if (!proof) {
163
+ fail(
164
+ "UNMAPPED_GRAPH_SOURCE",
165
+ "Prospective graphs require an existing fresh source graph receipt; author and render new components first.",
166
+ );
167
+ }
168
+ const original = sourceGraph(currentDoc, graph.wrapper.id);
169
+ const slideId = nativeGraphSlide(currentDoc, state, original);
170
+ if (
171
+ graph.wrapper.slideId !== original.wrapper.slideId &&
172
+ graph.wrapper.slideId !== slideId
173
+ ) {
174
+ fail(
175
+ "SOURCE_CONFLICT",
176
+ "The proposed graph moved outside its verified original heading scope.",
177
+ );
178
+ }
179
+ const explicitFrame = original.declaration.properties.y !== undefined &&
180
+ original.declaration.properties.h !== undefined &&
181
+ graph.declaration.properties.y !== undefined &&
182
+ graph.declaration.properties.h !== undefined;
183
+ if (
184
+ !explicitFrame &&
185
+ (proof.prefix !== graphFlowSignature(currentDoc, original) ||
186
+ proof.prefix !== graphFlowSignature(prospective, graph))
187
+ ) {
188
+ fail(
189
+ "GRAPH_FLOW_PREFLIGHT",
190
+ "A source change alters the unverified automatic flow before this graph. Declare source y and h for an independent frame, or restore/review the preceding structure before applying this combined sync.",
191
+ );
192
+ }
193
+ const projected = await projectGraph(
194
+ prospective,
195
+ await originalGraphNode(graph),
196
+ checkedBase(proof),
197
+ { theme },
198
+ );
199
+ // Validate actual retained geometry/typography too. Semantic pending fields
200
+ // are applied by the chosen prospective source, never frozen as overrides.
201
+ for (const element of projected.elements) {
202
+ const overlay = state.overrides.find((overlay) =>
203
+ overlay.target.slideId === slideId &&
204
+ overlay.target.elementId === element.id
205
+ );
206
+ for (const field of overlay?.fields ?? []) {
207
+ const target = element as unknown as JsonObject;
208
+ set(target, field.path, field.value);
209
+ }
210
+ }
211
+ const document = {
212
+ ...state.emitted,
213
+ slides: [{ id: slideId, elements: projected.elements }],
214
+ };
215
+ const provenance = Object.fromEntries(
216
+ Object.entries(projected.origins).map(([elementId, origin]) => [
217
+ targetKey({ slideId, elementId }),
218
+ {
219
+ origin: origin.origin,
220
+ source: {
221
+ ...(origin.graph ? { graph: origin.graph as JsonObject } : {}),
222
+ },
223
+ },
224
+ ]),
225
+ );
226
+ await assertGraphTypography(document as unknown as RenderDoc, provenance);
227
+ }
228
+ }
@@ -0,0 +1,27 @@
1
+ /** File durability helpers for recoverable single-project transactions. */
2
+ export async function syncDirectory(path: string) {
3
+ if (Deno.build.os === "windows") return;
4
+ const directory = await Deno.open(path, { read: true });
5
+ try {
6
+ await directory.sync();
7
+ } finally {
8
+ directory.close();
9
+ }
10
+ }
11
+ export async function writeDurable(path: string, text: string) {
12
+ const file = await Deno.open(path, {
13
+ createNew: true,
14
+ write: true,
15
+ mode: 0o600,
16
+ });
17
+ try {
18
+ const bytes = new TextEncoder().encode(text);
19
+ let offset = 0;
20
+ while (offset < bytes.length) {
21
+ offset += await file.write(bytes.subarray(offset));
22
+ }
23
+ await file.sync();
24
+ } finally {
25
+ file.close();
26
+ }
27
+ }