@danielsimonjr/mathts-workbook 0.1.8 → 0.2.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.
@@ -0,0 +1,1239 @@
1
+ // src/fs-atomic.ts
2
+ import { writeFileSync, renameSync, unlinkSync } from "fs";
3
+ var counter = 0;
4
+ function writeFileAtomic(file, content) {
5
+ const tmp = `${file}.${process.pid}.${counter++}.tmp`;
6
+ writeFileSync(tmp, content, { encoding: "utf-8", flag: "wx" });
7
+ try {
8
+ renameSync(tmp, file);
9
+ } catch (error) {
10
+ try {
11
+ unlinkSync(tmp);
12
+ } catch {
13
+ }
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ // src/yaml-safe.ts
19
+ import { parse as parseYaml } from "yaml";
20
+ var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
21
+ function parseYamlHardened(content) {
22
+ return parseYaml(content, { schema: "core", merge: false, logLevel: "silent" });
23
+ }
24
+ function isPlainObject(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function findPollutionKeys(value, seen = /* @__PURE__ */ new WeakSet(), found = []) {
28
+ if (!isPlainObject(value) && !Array.isArray(value)) return found;
29
+ if (seen.has(value)) return found;
30
+ seen.add(value);
31
+ if (Array.isArray(value)) {
32
+ for (const item of value) findPollutionKeys(item, seen, found);
33
+ return found;
34
+ }
35
+ for (const key of Object.getOwnPropertyNames(value)) {
36
+ if (POLLUTION_KEYS.has(key)) {
37
+ found.push(key);
38
+ continue;
39
+ }
40
+ findPollutionKeys(value[key], seen, found);
41
+ }
42
+ return found;
43
+ }
44
+ function assertNoPollution(value) {
45
+ const keys = findPollutionKeys(value);
46
+ if (keys.length > 0) {
47
+ throw new Error(`Disallowed key "${keys[0]}" (prototype pollution)`);
48
+ }
49
+ }
50
+
51
+ // src/graph.ts
52
+ function buildDependencyGraph(cells) {
53
+ const nodes = /* @__PURE__ */ new Map();
54
+ for (const cell of cells) {
55
+ nodes.set(cell.id, {
56
+ id: cell.id,
57
+ dependencies: cell.dependsOn ?? [],
58
+ dependents: []
59
+ });
60
+ }
61
+ for (const cell of cells) {
62
+ for (const depId of cell.dependsOn ?? []) {
63
+ const depNode = nodes.get(depId);
64
+ if (depNode) {
65
+ depNode.dependents.push(cell.id);
66
+ }
67
+ }
68
+ }
69
+ const executionOrder = topologicalSort(nodes);
70
+ return { nodes, executionOrder };
71
+ }
72
+ function topologicalSort(nodes) {
73
+ const visited = /* @__PURE__ */ new Set();
74
+ const result = [];
75
+ function visit(id) {
76
+ if (visited.has(id)) return;
77
+ visited.add(id);
78
+ const node = nodes.get(id);
79
+ if (node) {
80
+ for (const depId of node.dependencies) {
81
+ visit(depId);
82
+ }
83
+ }
84
+ result.push(id);
85
+ }
86
+ for (const id of nodes.keys()) {
87
+ visit(id);
88
+ }
89
+ return result;
90
+ }
91
+ function getDependents(graph, cellId) {
92
+ const result = [];
93
+ const visited = /* @__PURE__ */ new Set();
94
+ function collect(id) {
95
+ const node = graph.nodes.get(id);
96
+ if (!node) return;
97
+ for (const depId of node.dependents) {
98
+ if (!visited.has(depId)) {
99
+ visited.add(depId);
100
+ result.push(depId);
101
+ collect(depId);
102
+ }
103
+ }
104
+ }
105
+ collect(cellId);
106
+ return result;
107
+ }
108
+ function getAncestors(graph, id) {
109
+ const result = /* @__PURE__ */ new Set();
110
+ function visit(nodeId) {
111
+ if (result.has(nodeId)) return;
112
+ const node = graph.nodes.get(nodeId);
113
+ if (!node) return;
114
+ result.add(nodeId);
115
+ for (const dep of node.dependencies) visit(dep);
116
+ }
117
+ visit(id);
118
+ return graph.executionOrder.filter((nodeId) => result.has(nodeId));
119
+ }
120
+ function toMermaid(graph) {
121
+ const lines = ["graph TD"];
122
+ for (const id of graph.nodes.keys()) {
123
+ lines.push(` ${id}["${id}"]`);
124
+ }
125
+ for (const [id, node] of graph.nodes) {
126
+ for (const dep of node.dependencies) {
127
+ lines.push(` ${dep} --> ${id}`);
128
+ }
129
+ }
130
+ return lines.join("\n");
131
+ }
132
+ function toDOT(graph) {
133
+ const lines = ["digraph deps {"];
134
+ for (const id of graph.nodes.keys()) {
135
+ lines.push(` ${id} [label="${id}"];`);
136
+ }
137
+ for (const [id, node] of graph.nodes) {
138
+ for (const dep of node.dependencies) {
139
+ lines.push(` ${dep} -> ${id};`);
140
+ }
141
+ }
142
+ lines.push("}");
143
+ return lines.join("\n");
144
+ }
145
+ function detectCycles(graph) {
146
+ const cycles = [];
147
+ const visited = /* @__PURE__ */ new Set();
148
+ const stack = /* @__PURE__ */ new Set();
149
+ function dfs(id, path) {
150
+ if (stack.has(id)) {
151
+ const cycleStart = path.indexOf(id);
152
+ cycles.push(path.slice(cycleStart));
153
+ return true;
154
+ }
155
+ if (visited.has(id)) return false;
156
+ visited.add(id);
157
+ stack.add(id);
158
+ const node = graph.nodes.get(id);
159
+ if (node) {
160
+ for (const depId of node.dependencies) {
161
+ dfs(depId, [...path, id]);
162
+ }
163
+ }
164
+ stack.delete(id);
165
+ return false;
166
+ }
167
+ for (const id of graph.nodes.keys()) {
168
+ if (!visited.has(id)) {
169
+ dfs(id, []);
170
+ }
171
+ }
172
+ return cycles;
173
+ }
174
+
175
+ // src/parser.ts
176
+ import { stringify as stringifyYaml } from "yaml";
177
+ var CELL_TYPE_KEYS = [
178
+ "markdown",
179
+ "code",
180
+ "tensor",
181
+ "equation",
182
+ "visualization",
183
+ "data",
184
+ "test",
185
+ "export"
186
+ ];
187
+ var RESERVED_CELL_KEYS = ["id", "depends_on", "language", "format", "output", "error"];
188
+ var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
189
+ function isValidIdentifier(id) {
190
+ return typeof id === "string" && IDENTIFIER_RE.test(id);
191
+ }
192
+ var SUPPORTED_CELL_TYPES = ["code", "markdown", "data", "test", "equation", "visualization"];
193
+ var EXECUTION_MODES = ["reactive", "sequential", "manual"];
194
+ function detectCellType(cell) {
195
+ for (const key of CELL_TYPE_KEYS) {
196
+ if (key in cell) {
197
+ return key;
198
+ }
199
+ }
200
+ return "code";
201
+ }
202
+ function isPlainObject2(value) {
203
+ return typeof value === "object" && value !== null && !Array.isArray(value);
204
+ }
205
+ function toContent(raw) {
206
+ if (raw === null || raw === void 0) return "";
207
+ if (typeof raw === "string") return raw;
208
+ return stringifyYaml(raw).trim();
209
+ }
210
+ function normalizeRuntime(raw) {
211
+ const rt = isPlainObject2(raw) ? raw : {};
212
+ const runtime = {
213
+ engine: rt.engine === "custom" ? "custom" : "mathts",
214
+ execution: typeof rt.execution === "string" && EXECUTION_MODES.includes(rt.execution) ? rt.execution : "reactive"
215
+ };
216
+ if (typeof rt.timeout === "number") runtime.timeout = rt.timeout;
217
+ return runtime;
218
+ }
219
+ function mapCell(raw, index, errors) {
220
+ if (!isPlainObject2(raw)) {
221
+ errors.push(`Cell at index ${index}: must be a mapping`);
222
+ return { id: `#${index}`, type: "code", content: "" };
223
+ }
224
+ const rawId = raw.id;
225
+ let id;
226
+ if (rawId === void 0 || rawId === null) {
227
+ errors.push(`Cell at index ${index}: missing "id"`);
228
+ id = `#${index}`;
229
+ } else if (typeof rawId !== "string" || !IDENTIFIER_RE.test(rawId)) {
230
+ errors.push(
231
+ `Cell "${String(rawId)}": id must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`
232
+ );
233
+ id = String(rawId);
234
+ } else {
235
+ id = rawId;
236
+ }
237
+ const presentTypeKeys = CELL_TYPE_KEYS.filter(
238
+ (k) => Object.prototype.hasOwnProperty.call(raw, k)
239
+ );
240
+ if (presentTypeKeys.length === 0) {
241
+ errors.push(
242
+ `Cell "${id}": no recognized type key (expected one of ${CELL_TYPE_KEYS.join(", ")})`
243
+ );
244
+ } else if (presentTypeKeys.length > 1) {
245
+ errors.push(`Cell "${id}": multiple type keys (${presentTypeKeys.join(", ")})`);
246
+ }
247
+ const type = presentTypeKeys[0] ?? "code";
248
+ const content = presentTypeKeys[0] ? toContent(raw[presentTypeKeys[0]]) : "";
249
+ if (presentTypeKeys[0] && !SUPPORTED_CELL_TYPES.includes(type)) {
250
+ errors.push(
251
+ `Cell "${id}": type '${type}' is reserved but not yet supported (supported: ${SUPPORTED_CELL_TYPES.join(", ")})`
252
+ );
253
+ }
254
+ let dependsOn;
255
+ if (raw.depends_on !== void 0) {
256
+ if (!Array.isArray(raw.depends_on)) {
257
+ errors.push(`Cell "${id}": depends_on must be a list`);
258
+ } else {
259
+ dependsOn = raw.depends_on.map((d) => String(d));
260
+ }
261
+ }
262
+ const metadata = {};
263
+ for (const key of Object.keys(raw)) {
264
+ if (RESERVED_CELL_KEYS.includes(key) || CELL_TYPE_KEYS.includes(key)) continue;
265
+ metadata[key] = raw[key];
266
+ }
267
+ const cell = { id, type, content };
268
+ if (dependsOn) cell.dependsOn = dependsOn;
269
+ if (Object.keys(metadata).length > 0) cell.metadata = metadata;
270
+ if (raw.output !== void 0) cell.output = raw.output;
271
+ if (raw.error !== void 0) cell.error = String(raw.error);
272
+ return cell;
273
+ }
274
+ function parseWorkbook(content) {
275
+ try {
276
+ if (!content.trim()) {
277
+ return { success: false, errors: ["Empty workbook content"] };
278
+ }
279
+ const errors = [];
280
+ const warnings = [];
281
+ const doc = parseYamlHardened(content);
282
+ if (!isPlainObject2(doc)) {
283
+ return { success: false, errors: ["Workbook must be a YAML mapping"] };
284
+ }
285
+ for (const key of findPollutionKeys(doc)) {
286
+ errors.push(`Disallowed key "${key}" (prototype pollution)`);
287
+ }
288
+ const metadata = isPlainObject2(doc.metadata) ? doc.metadata : {};
289
+ const workbook = {
290
+ version: doc.version != null ? String(doc.version) : "1.0",
291
+ metadata,
292
+ runtime: normalizeRuntime(doc.runtime),
293
+ cells: []
294
+ };
295
+ if (doc.cells === void 0) {
296
+ warnings.push("Workbook has no cells");
297
+ } else if (!Array.isArray(doc.cells)) {
298
+ errors.push('Workbook "cells" must be a list');
299
+ } else {
300
+ const idSet = /* @__PURE__ */ new Set();
301
+ const cells = doc.cells.map((raw, i) => {
302
+ const cell = mapCell(raw, i, errors);
303
+ if (IDENTIFIER_RE.test(cell.id)) {
304
+ if (idSet.has(cell.id)) {
305
+ errors.push(`Duplicate cell id: "${cell.id}"`);
306
+ } else {
307
+ idSet.add(cell.id);
308
+ }
309
+ }
310
+ return cell;
311
+ });
312
+ for (const cell of cells) {
313
+ for (const dep of cell.dependsOn ?? []) {
314
+ if (!idSet.has(dep)) {
315
+ errors.push(`Cell "${cell.id}": depends_on references unknown cell "${dep}"`);
316
+ }
317
+ }
318
+ }
319
+ workbook.cells = cells;
320
+ }
321
+ const success = errors.length === 0;
322
+ return {
323
+ success,
324
+ workbook: success ? workbook : void 0,
325
+ errors: errors.length > 0 ? errors : void 0,
326
+ warnings: warnings.length > 0 ? warnings : void 0
327
+ };
328
+ } catch (error) {
329
+ return {
330
+ success: false,
331
+ errors: [`Parse error: ${error instanceof Error ? error.message : String(error)}`]
332
+ };
333
+ }
334
+ }
335
+ function serializeCell(cell) {
336
+ const out = { [cell.type]: cell.content, id: cell.id };
337
+ if (cell.dependsOn && cell.dependsOn.length > 0) out.depends_on = cell.dependsOn;
338
+ if (cell.metadata) {
339
+ for (const [key, value] of Object.entries(cell.metadata)) {
340
+ if (CELL_TYPE_KEYS.includes(key) || RESERVED_CELL_KEYS.includes(key)) continue;
341
+ out[key] = value;
342
+ }
343
+ }
344
+ if (cell.output !== void 0) out.output = cell.output;
345
+ if (cell.error !== void 0) out.error = String(cell.error);
346
+ return out;
347
+ }
348
+ function serializeWorkbook(workbook) {
349
+ if (!workbook || !Array.isArray(workbook.cells)) {
350
+ throw new Error('serializeWorkbook: invalid workbook (missing "cells" array)');
351
+ }
352
+ const runtime = {
353
+ engine: workbook.runtime?.engine ?? "mathts",
354
+ execution: workbook.runtime?.execution ?? "reactive"
355
+ };
356
+ if (workbook.runtime?.timeout !== void 0) runtime.timeout = workbook.runtime.timeout;
357
+ const doc = {
358
+ version: String(workbook.version ?? "1.0"),
359
+ metadata: workbook.metadata ?? {},
360
+ runtime,
361
+ cells: workbook.cells.map(serializeCell)
362
+ };
363
+ return stringifyYaml(doc, { lineWidth: 0 });
364
+ }
365
+ function stripOutputs(workbook) {
366
+ return {
367
+ ...workbook,
368
+ cells: workbook.cells.map((cell) => ({
369
+ ...cell,
370
+ output: void 0,
371
+ error: void 0
372
+ }))
373
+ };
374
+ }
375
+ function importWorkbook(input) {
376
+ let obj;
377
+ try {
378
+ obj = parseYamlHardened(input);
379
+ } catch (error) {
380
+ return { success: false, errors: [`Invalid JSON/YAML: ${error instanceof Error ? error.message : String(error)}`] };
381
+ }
382
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
383
+ return { success: false, errors: ['Input must be an object with a "cells" array'] };
384
+ }
385
+ const root = obj;
386
+ if (!Array.isArray(root.cells)) {
387
+ return { success: false, errors: ['Input must have a "cells" array'] };
388
+ }
389
+ const errors = [];
390
+ const cells = [];
391
+ root.cells.forEach((raw, index) => {
392
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
393
+ errors.push(`Cell at index ${index}: must be an object`);
394
+ return;
395
+ }
396
+ const c = raw;
397
+ const id = typeof c.id === "string" ? c.id : "";
398
+ const type = typeof c.type === "string" ? c.type : "";
399
+ const content = typeof c.content === "string" ? c.content : c.content == null ? "" : String(c.content);
400
+ const depsRaw = Array.isArray(c.dependsOn) ? c.dependsOn : Array.isArray(c.depends_on) ? c.depends_on : void 0;
401
+ const dependsOn = depsRaw?.map((d) => String(d));
402
+ const cell = { id, type, content };
403
+ if (dependsOn && dependsOn.length > 0) cell.dependsOn = dependsOn;
404
+ cells.push(cell);
405
+ });
406
+ if (errors.length > 0) return { success: false, errors };
407
+ const metadata = root.metadata && typeof root.metadata === "object" && !Array.isArray(root.metadata) ? root.metadata : {};
408
+ const runtime = root.runtime && typeof root.runtime === "object" && !Array.isArray(root.runtime) ? root.runtime : { engine: "mathts", execution: "reactive" };
409
+ const version = typeof root.version === "string" ? root.version : "1.0";
410
+ const workbook = { version, metadata, runtime, cells };
411
+ const parsed = parseWorkbook(serializeWorkbook(workbook));
412
+ if (!parsed.success || !parsed.workbook) return parsed;
413
+ const cycles = detectCycles(buildDependencyGraph(parsed.workbook.cells));
414
+ if (cycles.length > 0) {
415
+ return { success: false, errors: cycles.map((c) => `Dependency cycle: ${c.join(" -> ")}`) };
416
+ }
417
+ return parsed;
418
+ }
419
+
420
+ // src/executor.ts
421
+ import { evaluate } from "@danielsimonjr/mathts-functions";
422
+ var WorkbookExecutor = class {
423
+ workbook;
424
+ graph;
425
+ outputs = /* @__PURE__ */ new Map();
426
+ handlers = [];
427
+ constructor(workbook) {
428
+ this.workbook = workbook;
429
+ this.graph = buildDependencyGraph(workbook.cells);
430
+ }
431
+ /**
432
+ * Subscribe to execution events
433
+ */
434
+ on(handler) {
435
+ this.handlers.push(handler);
436
+ return () => {
437
+ const index = this.handlers.indexOf(handler);
438
+ if (index >= 0) {
439
+ this.handlers.splice(index, 1);
440
+ }
441
+ };
442
+ }
443
+ /**
444
+ * Emit an event to all handlers
445
+ */
446
+ emit(event) {
447
+ for (const handler of this.handlers) {
448
+ handler(event);
449
+ }
450
+ }
451
+ /**
452
+ * Run all cells in order
453
+ */
454
+ async runAll() {
455
+ for (const cellId of this.graph.executionOrder) {
456
+ await this.runCell(cellId);
457
+ }
458
+ this.emit({
459
+ type: "workbook:complete",
460
+ timestamp: Date.now()
461
+ });
462
+ }
463
+ /**
464
+ * Run a specific cell
465
+ */
466
+ async runCell(cellId) {
467
+ const cell = this.workbook.cells.find((c) => c.id === cellId);
468
+ if (!cell) {
469
+ throw new Error(`Cell not found: ${cellId}`);
470
+ }
471
+ this.emit({
472
+ type: "cell:start",
473
+ cellId,
474
+ timestamp: Date.now()
475
+ });
476
+ try {
477
+ const output = await this.executeCell(cell);
478
+ this.outputs.set(cellId, output);
479
+ this.emit({
480
+ type: "cell:success",
481
+ cellId,
482
+ output,
483
+ timestamp: Date.now()
484
+ });
485
+ if (this.workbook.runtime.execution === "reactive") {
486
+ const dependents = getDependents(this.graph, cellId);
487
+ for (const depId of dependents) {
488
+ this.emit({
489
+ type: "cell:stale",
490
+ cellId: depId,
491
+ timestamp: Date.now()
492
+ });
493
+ }
494
+ }
495
+ return output;
496
+ } catch (error) {
497
+ const errorMessage = error instanceof Error ? error.message : String(error);
498
+ this.emit({
499
+ type: "cell:error",
500
+ cellId,
501
+ error: errorMessage,
502
+ timestamp: Date.now()
503
+ });
504
+ throw error;
505
+ }
506
+ }
507
+ /**
508
+ * Execute a single cell
509
+ */
510
+ async executeCell(cell) {
511
+ switch (cell.type) {
512
+ case "code":
513
+ return this.executeCode(cell);
514
+ case "test":
515
+ return this.executeTest(cell);
516
+ case "markdown":
517
+ case "equation":
518
+ case "visualization":
519
+ return cell.content;
520
+ // Display-only cells: pass content through, no eval
521
+ case "data":
522
+ return this.executeData(cell);
523
+ default:
524
+ throw new Error(`Unsupported cell type: ${cell.type}`);
525
+ }
526
+ }
527
+ /**
528
+ * Build the evaluation scope from a cell's DIRECT dependency outputs.
529
+ *
530
+ * Dependency outputs are injected as named variables keyed by the
531
+ * dependency's cell id, so a cell can reference an earlier cell's result by
532
+ * that id. Injection is non-transitive: only ids in `cell.dependsOn` are
533
+ * injected (a dependency's own dependencies are not). An output of
534
+ * `undefined` (cell never ran) is skipped, so the reference fails loudly.
535
+ */
536
+ buildScope(cell) {
537
+ const scope = {};
538
+ if (cell.dependsOn) {
539
+ for (const depId of cell.dependsOn) {
540
+ if (this.outputs.has(depId)) {
541
+ scope[depId] = this.outputs.get(depId);
542
+ }
543
+ }
544
+ }
545
+ return scope;
546
+ }
547
+ /**
548
+ * Execute a code cell by evaluating its content as a MathTS expression.
549
+ *
550
+ * Evaluation goes through the MathTS expression engine — property and
551
+ * method access route through the expression sandbox, so this does not
552
+ * have the arbitrary-code-execution exposure of the `Function` constructor.
553
+ */
554
+ async executeCode(cell) {
555
+ return evaluate(cell.content, this.buildScope(cell));
556
+ }
557
+ /**
558
+ * Execute a test cell: evaluate its assertion expression in the dependency
559
+ * scope. The result must be a boolean — `runReport` classifies `true` as a
560
+ * pass and `false` as a fail. A non-boolean result is a usage error (a test
561
+ * that doesn't assert), surfaced via throw like any other evaluation error.
562
+ */
563
+ async executeTest(cell) {
564
+ const result = evaluate(cell.content, this.buildScope(cell));
565
+ if (typeof result !== "boolean") {
566
+ throw new Error(`test cell must evaluate to a boolean, got ${typeof result}`);
567
+ }
568
+ return result;
569
+ }
570
+ /**
571
+ * Execute a data cell — parse its content as YAML (a superset of JSON) using
572
+ * the same hardened options + prototype-pollution guard as the document
573
+ * parser, so both YAML entry points are consistent.
574
+ */
575
+ async executeData(cell) {
576
+ const value = parseYamlHardened(cell.content);
577
+ assertNoPollution(value);
578
+ return value;
579
+ }
580
+ /**
581
+ * Run every cell in dependency order and return a structured report.
582
+ *
583
+ * Unlike {@link runAll} (the event-stream API, which throws on the first cell
584
+ * error), `runReport` is the headless/report API: it is **continue-on-error**
585
+ * (one failing cell does not abort the rest) and never throws on a cell
586
+ * failure. A dependency cycle is refused up front. Test cells are classified
587
+ * as `pass`/`fail`/`error`; all other cells as `success`/`error`.
588
+ */
589
+ /**
590
+ * Seed the output cache (for incremental session runs): non-stale cells'
591
+ * outputs are provided so downstream cells can read them as scope without
592
+ * re-execution. Replaces any existing cached outputs.
593
+ */
594
+ seedOutputs(entries) {
595
+ this.outputs = new Map(entries);
596
+ }
597
+ async runReport(options = {}) {
598
+ const cells = [];
599
+ const cycles = detectCycles(this.graph);
600
+ if (cycles.length > 0) {
601
+ const description = cycles.map((cycle) => cycle.join(" -> ")).join("; ");
602
+ cells.push({
603
+ id: "(workbook)",
604
+ type: "code",
605
+ status: "error",
606
+ error: `Dependency cycle detected: ${description}`
607
+ });
608
+ return { cells, ok: false };
609
+ }
610
+ let order = this.graph.executionOrder;
611
+ if (options.only !== void 0) {
612
+ const allowed = new Set(getAncestors(this.graph, options.only));
613
+ order = order.filter((id) => allowed.has(id));
614
+ }
615
+ for (const cellId of order) {
616
+ if (options.runIds && !options.runIds.has(cellId)) continue;
617
+ const cell = this.workbook.cells.find((c) => c.id === cellId);
618
+ if (!cell) continue;
619
+ try {
620
+ const output = await this.runCell(cellId);
621
+ if (cell.type === "test") {
622
+ const passed = output === true;
623
+ cells.push({
624
+ id: cell.id,
625
+ type: cell.type,
626
+ status: passed ? "pass" : "fail",
627
+ output,
628
+ error: passed ? void 0 : `Assertion failed in cell '${cell.id}'`
629
+ });
630
+ } else {
631
+ cells.push({ id: cell.id, type: cell.type, status: "success", output });
632
+ }
633
+ } catch (error) {
634
+ cells.push({
635
+ id: cell.id,
636
+ type: cell.type,
637
+ status: "error",
638
+ error: error instanceof Error ? error.message : String(error)
639
+ });
640
+ }
641
+ }
642
+ const ok = cells.every((c) => c.status === "success" || c.status === "pass");
643
+ return { cells, ok };
644
+ }
645
+ /**
646
+ * Get output from a previous cell
647
+ */
648
+ getOutput(cellId) {
649
+ return this.outputs.get(cellId);
650
+ }
651
+ };
652
+ function createExecutor(workbook) {
653
+ return new WorkbookExecutor(workbook);
654
+ }
655
+
656
+ // src/edit.ts
657
+ function cloneWorkbook(wb) {
658
+ return {
659
+ ...wb,
660
+ metadata: { ...wb.metadata },
661
+ runtime: { ...wb.runtime },
662
+ cells: wb.cells.map((c) => ({
663
+ ...c,
664
+ ...c.dependsOn ? { dependsOn: [...c.dependsOn] } : {},
665
+ ...c.metadata ? { metadata: { ...c.metadata } } : {}
666
+ }))
667
+ };
668
+ }
669
+ function assertSupportedType(type) {
670
+ if (!SUPPORTED_CELL_TYPES.includes(type)) {
671
+ throw new Error(`Unsupported cell type '${type}' (supported: ${SUPPORTED_CELL_TYPES.join(", ")})`);
672
+ }
673
+ }
674
+ function indexOfCell(wb, id) {
675
+ const i = wb.cells.findIndex((c) => c.id === id);
676
+ if (i < 0) throw new Error(`No such cell: '${id}'`);
677
+ return i;
678
+ }
679
+ function validateDeps(wb, dependsOn, selfId) {
680
+ const ids = new Set(wb.cells.map((c) => c.id));
681
+ for (const dep of dependsOn) {
682
+ if (dep === selfId) throw new Error(`Cell '${selfId}' cannot depend on itself`);
683
+ if (!ids.has(dep)) {
684
+ throw new Error(`depends_on references unknown cell '${dep}'`);
685
+ }
686
+ }
687
+ return dependsOn;
688
+ }
689
+ function assertNoNewCycle(before, after) {
690
+ if (detectCycles(buildDependencyGraph(before.cells)).length > 0) return;
691
+ const cycles = detectCycles(buildDependencyGraph(after.cells));
692
+ if (cycles.length > 0) {
693
+ throw new Error(
694
+ `Operation would create a dependency cycle: ${cycles.map((c) => c.join(" -> ")).join("; ")}`
695
+ );
696
+ }
697
+ }
698
+ function clearResult(cell) {
699
+ delete cell.output;
700
+ delete cell.error;
701
+ }
702
+ function resolveInsertIndex(wb, position) {
703
+ if (!position) return wb.cells.length;
704
+ if (position.at !== void 0) {
705
+ if (!Number.isInteger(position.at) || position.at < 0 || position.at > wb.cells.length) {
706
+ throw new Error(`--at ${position.at} out of range (0..${wb.cells.length})`);
707
+ }
708
+ return position.at;
709
+ }
710
+ if (position.before !== void 0) {
711
+ const i = wb.cells.findIndex((c) => c.id === position.before);
712
+ if (i < 0) throw new Error(`--before: no such cell '${position.before}'`);
713
+ return i;
714
+ }
715
+ if (position.after !== void 0) {
716
+ const i = wb.cells.findIndex((c) => c.id === position.after);
717
+ if (i < 0) throw new Error(`--after: no such cell '${position.after}'`);
718
+ return i + 1;
719
+ }
720
+ return wb.cells.length;
721
+ }
722
+ function addCell(wb, spec, position) {
723
+ if (!isValidIdentifier(spec.id)) {
724
+ throw new Error(`Invalid cell id '${spec.id}': must be an identifier ([A-Za-z_][A-Za-z0-9_]*)`);
725
+ }
726
+ if (wb.cells.some((c) => c.id === spec.id)) {
727
+ throw new Error(`Duplicate cell id '${spec.id}'`);
728
+ }
729
+ assertSupportedType(spec.type);
730
+ const next = cloneWorkbook(wb);
731
+ const cell = { id: spec.id, type: spec.type, content: spec.content ?? "" };
732
+ const deps = spec.dependsOn ? validateDeps(next, spec.dependsOn, spec.id) : void 0;
733
+ if (deps && deps.length > 0) cell.dependsOn = deps;
734
+ next.cells.splice(resolveInsertIndex(next, position), 0, cell);
735
+ assertNoNewCycle(wb, next);
736
+ return next;
737
+ }
738
+ function editCell(wb, id, changes) {
739
+ const next = cloneWorkbook(wb);
740
+ const cell = next.cells[indexOfCell(next, id)];
741
+ let touched = false;
742
+ if (changes.type !== void 0) {
743
+ assertSupportedType(changes.type);
744
+ cell.type = changes.type;
745
+ touched = true;
746
+ }
747
+ if (changes.content !== void 0) {
748
+ cell.content = changes.content;
749
+ touched = true;
750
+ }
751
+ if (changes.dependsOn !== void 0) {
752
+ const deps = validateDeps(next, changes.dependsOn, id);
753
+ if (deps.length > 0) cell.dependsOn = deps;
754
+ else delete cell.dependsOn;
755
+ touched = true;
756
+ }
757
+ if (touched) clearResult(cell);
758
+ if (changes.dependsOn !== void 0) assertNoNewCycle(wb, next);
759
+ return next;
760
+ }
761
+ function removeCell(wb, id, options) {
762
+ const next = cloneWorkbook(wb);
763
+ indexOfCell(next, id);
764
+ const dependents = next.cells.filter((c) => (c.dependsOn ?? []).includes(id)).map((c) => c.id);
765
+ if (dependents.length > 0 && !options?.force) {
766
+ throw new Error(`Cell '${id}' has dependents: ${dependents.join(", ")} (use --force to remove and detach)`);
767
+ }
768
+ const changedCells = [];
769
+ for (const c of next.cells) {
770
+ if (c.dependsOn?.includes(id)) {
771
+ c.dependsOn = c.dependsOn.filter((d) => d !== id);
772
+ if (c.dependsOn.length === 0) delete c.dependsOn;
773
+ clearResult(c);
774
+ changedCells.push(c.id);
775
+ }
776
+ }
777
+ next.cells = next.cells.filter((c) => c.id !== id);
778
+ return { workbook: next, changedCells };
779
+ }
780
+ function moveCell(wb, id, position) {
781
+ if (position.before === id || position.after === id) return cloneWorkbook(wb);
782
+ const next = cloneWorkbook(wb);
783
+ const from = indexOfCell(next, id);
784
+ const [cell] = next.cells.splice(from, 1);
785
+ next.cells.splice(resolveInsertIndex(next, position), 0, cell);
786
+ return next;
787
+ }
788
+ function renameCell(wb, oldId, newId) {
789
+ if (oldId === newId) return cloneWorkbook(wb);
790
+ const next = cloneWorkbook(wb);
791
+ const cell = next.cells[indexOfCell(next, oldId)];
792
+ if (!isValidIdentifier(newId)) {
793
+ throw new Error(`Invalid cell id '${newId}': must be an identifier ([A-Za-z_][A-Za-z0-9_]*)`);
794
+ }
795
+ if (next.cells.some((c) => c.id === newId)) {
796
+ throw new Error(`Duplicate cell id '${newId}'`);
797
+ }
798
+ cell.id = newId;
799
+ const reference = new RegExp(`\\b${oldId}\\b`, "g");
800
+ for (const c of next.cells) {
801
+ if (c.dependsOn?.includes(oldId)) {
802
+ c.dependsOn = c.dependsOn.map((d) => d === oldId ? newId : d);
803
+ c.content = c.content.replace(reference, newId);
804
+ }
805
+ }
806
+ return next;
807
+ }
808
+ function setMetadata(wb, changes) {
809
+ const next = cloneWorkbook(wb);
810
+ if (changes.title !== void 0) next.metadata.title = changes.title;
811
+ if (changes.author !== void 0) next.metadata.author = changes.author;
812
+ if (changes.description !== void 0) next.metadata.description = changes.description;
813
+ if (changes.tags !== void 0) next.metadata.tags = changes.tags;
814
+ return next;
815
+ }
816
+
817
+ // src/session.ts
818
+ import { readFileSync } from "fs";
819
+ function canonicalCell(cell) {
820
+ const meta = cell.metadata ?? {};
821
+ const sortedMeta = {};
822
+ for (const key of Object.keys(meta).sort()) sortedMeta[key] = meta[key];
823
+ return JSON.stringify([cell.type, cell.content, cell.dependsOn ?? [], sortedMeta]);
824
+ }
825
+ var Session = class {
826
+ workbook = null;
827
+ path = null;
828
+ dirty = false;
829
+ cache = /* @__PURE__ */ new Map();
830
+ stale = /* @__PURE__ */ new Set();
831
+ require() {
832
+ if (!this.workbook) throw new Error("No workbook open");
833
+ return this.workbook;
834
+ }
835
+ /** A cell has a usable, up-to-date result (the basis for "not stale"). */
836
+ hasValidResult(id) {
837
+ const r = this.cache.get(id);
838
+ return r !== void 0 && (r.status === "success" || r.status === "pass");
839
+ }
840
+ /** Load a workbook file into the session. Refuses to discard unsaved edits. */
841
+ open(path, options) {
842
+ if (this.dirty && !options?.force) {
843
+ throw new Error("Workbook has unsaved changes (open with force to discard)");
844
+ }
845
+ const content = readFileSync(path, "utf-8");
846
+ const parsed = parseWorkbook(content);
847
+ if (!parsed.success) {
848
+ throw new Error(`Parse errors: ${(parsed.errors ?? []).join("; ")}`);
849
+ }
850
+ this.workbook = parsed.workbook;
851
+ this.path = path;
852
+ this.cache.clear();
853
+ this.stale = new Set(this.workbook.cells.map((c) => c.id));
854
+ this.dirty = false;
855
+ }
856
+ /** Replace the workbook and recompute the stale set by diffing old vs new. */
857
+ commit(next) {
858
+ const old = this.require();
859
+ const oldCanonical = new Map(old.cells.map((c) => [c.id, canonicalCell(c)]));
860
+ const changed = /* @__PURE__ */ new Set();
861
+ for (const cell of next.cells) {
862
+ const prev = oldCanonical.get(cell.id);
863
+ if (prev === void 0 || prev !== canonicalCell(cell)) changed.add(cell.id);
864
+ }
865
+ const graph = buildDependencyGraph(next.cells);
866
+ const affected = new Set(changed);
867
+ for (const id of changed) {
868
+ for (const dep of getDependents(graph, id)) affected.add(dep);
869
+ }
870
+ const survivingIds = new Set(next.cells.map((c) => c.id));
871
+ for (const id of [...this.cache.keys()]) {
872
+ if (!survivingIds.has(id) || affected.has(id)) this.cache.delete(id);
873
+ }
874
+ this.stale = new Set(next.cells.map((c) => c.id).filter((id) => !this.hasValidResult(id)));
875
+ this.workbook = next;
876
+ this.dirty = true;
877
+ }
878
+ // --- Edits (each is atomic: the pure op throws before any state change) ---
879
+ addCell(spec, position) {
880
+ this.commit(addCell(this.require(), spec, position));
881
+ }
882
+ editCell(id, changes) {
883
+ this.commit(editCell(this.require(), id, changes));
884
+ }
885
+ removeCell(id, options) {
886
+ const result = removeCell(this.require(), id, options);
887
+ this.commit(result.workbook);
888
+ return result.changedCells;
889
+ }
890
+ moveCell(id, position) {
891
+ this.commit(moveCell(this.require(), id, position));
892
+ }
893
+ renameCell(oldId, newId) {
894
+ this.commit(renameCell(this.require(), oldId, newId));
895
+ }
896
+ setMetadata(changes) {
897
+ this.commit(setMetadata(this.require(), changes));
898
+ }
899
+ /**
900
+ * Incremental run: execute only stale cells in the target set (`only` + its
901
+ * ancestors, or the whole workbook), reusing cached outputs for the rest.
902
+ * Returns the merged report over the target plus the streamed events.
903
+ */
904
+ async run(only) {
905
+ const wb = this.require();
906
+ const graph = buildDependencyGraph(wb.cells);
907
+ const target = only ? new Set(getAncestors(graph, only)) : new Set(wb.cells.map((c) => c.id));
908
+ const toRun = new Set([...this.stale].filter((id) => target.has(id)));
909
+ const executor = new WorkbookExecutor(wb);
910
+ const seed = /* @__PURE__ */ new Map();
911
+ for (const [id, res] of this.cache) {
912
+ if (res.status === "success" || res.status === "pass") {
913
+ seed.set(id, res.output);
914
+ }
915
+ }
916
+ executor.seedOutputs(seed);
917
+ const events = [];
918
+ executor.on((e) => events.push({ type: e.type, cellId: e.cellId, error: e.error }));
919
+ const report = await executor.runReport({ only, runIds: toRun });
920
+ if (!report.ok && report.cells.some((c) => c.id === "(workbook)")) {
921
+ return { result: report, events };
922
+ }
923
+ for (const cr of report.cells) {
924
+ this.cache.set(cr.id, cr);
925
+ if (cr.status === "success" || cr.status === "pass") this.stale.delete(cr.id);
926
+ }
927
+ const ordered = graph.executionOrder.filter((id) => target.has(id));
928
+ const cells = ordered.map((id) => this.cache.get(id)).filter((c) => c !== void 0);
929
+ const ok = cells.every((c) => c.status === "success" || c.status === "pass");
930
+ return { result: { cells, ok }, events };
931
+ }
932
+ /** The set of cells awaiting (re-)execution. */
933
+ staleIds() {
934
+ return [...this.stale];
935
+ }
936
+ /** Serialize the current workbook and write it atomically. */
937
+ save(path) {
938
+ const wb = this.require();
939
+ const target = path ?? this.path;
940
+ if (!target) throw new Error("No path to save to");
941
+ writeFileAtomic(target, serializeWorkbook(wb));
942
+ this.path = target;
943
+ this.dirty = false;
944
+ return target;
945
+ }
946
+ };
947
+
948
+ // src/contract.ts
949
+ var SCHEMA_VERSION = { major: 1, minor: 0 };
950
+ var VERSION = "0.1.0";
951
+ var COMMAND_NAMES = [
952
+ "run",
953
+ "describe",
954
+ "validate",
955
+ "graph",
956
+ "strip",
957
+ "new",
958
+ "capabilities",
959
+ "templates",
960
+ "cell",
961
+ "serve",
962
+ "functions",
963
+ "meta",
964
+ "import",
965
+ "export"
966
+ ];
967
+
968
+ // src/introspect.ts
969
+ import * as mathFunctions from "@danielsimonjr/mathts-functions";
970
+ var functionsCache;
971
+ function listFunctions() {
972
+ if (functionsCache) return functionsCache;
973
+ const functions = [];
974
+ const constants = [];
975
+ for (const [name, value] of Object.entries(mathFunctions)) {
976
+ if (typeof value === "function") functions.push(name);
977
+ else if (value !== null && typeof value !== "object") constants.push(name);
978
+ }
979
+ functionsCache = { functions: functions.sort(), constants: constants.sort() };
980
+ return functionsCache;
981
+ }
982
+ var CELL_SCHEMAS = {
983
+ code: "MathTS expression; the cell output is its evaluated value",
984
+ markdown: "Markdown text (rendered to HTML on export)",
985
+ equation: "MathTS expression syntax, rendered to MathML on export (display-only)",
986
+ test: "Boolean MathTS expression; true = pass, false = fail",
987
+ data: "A YAML/JSON literal value (e.g. a numeric array)",
988
+ visualization: "YAML chart spec: { type: line|scatter|bar, title?, x: {label?, data}, y: {label?, data} }; data = a dependency cell id (numeric array) or an inline array"
989
+ };
990
+ function capabilitiesInfo() {
991
+ return {
992
+ name: "mtsw",
993
+ version: VERSION,
994
+ schemaVersion: SCHEMA_VERSION,
995
+ cellTypes: {
996
+ supported: [...SUPPORTED_CELL_TYPES],
997
+ deferred: ["tensor", "export"]
998
+ },
999
+ cellSchemas: CELL_SCHEMAS,
1000
+ commands: [...COMMAND_NAMES],
1001
+ features: {
1002
+ json: true,
1003
+ write: true,
1004
+ runCell: true,
1005
+ editCell: true,
1006
+ serve: true,
1007
+ incremental: true,
1008
+ functions: true,
1009
+ import: true,
1010
+ export: true
1011
+ }
1012
+ };
1013
+ }
1014
+
1015
+ // src/doc.ts
1016
+ function describeData(wb) {
1017
+ const cells = (wb?.cells ?? []).map((c) => ({
1018
+ id: c.id,
1019
+ type: c.type,
1020
+ content: c.content,
1021
+ dependsOn: c.dependsOn ?? [],
1022
+ metadata: c.metadata ?? {},
1023
+ output: c.output ?? null,
1024
+ error: c.error ?? null
1025
+ }));
1026
+ const graph = wb ? buildDependencyGraph(wb.cells) : void 0;
1027
+ const edges = graph ? [...graph.nodes].flatMap(([id, node]) => node.dependencies.map((dep) => ({ from: dep, to: id }))) : [];
1028
+ const cycles = graph ? detectCycles(graph) : [];
1029
+ return {
1030
+ version: wb?.version ?? "1.0",
1031
+ metadata: wb?.metadata ?? {},
1032
+ runtime: wb?.runtime ?? { engine: "mathts", execution: "reactive" },
1033
+ cells,
1034
+ graph: { edges, cycles }
1035
+ };
1036
+ }
1037
+
1038
+ // src/rpc.ts
1039
+ var CODE = {
1040
+ PARSE: -32700,
1041
+ INVALID_REQUEST: -32600,
1042
+ METHOD_NOT_FOUND: -32601,
1043
+ INVALID_PARAMS: -32602,
1044
+ INTERNAL: -32603
1045
+ };
1046
+ function str(value) {
1047
+ return typeof value === "string" ? value : void 0;
1048
+ }
1049
+ async function handleRequest(session, request) {
1050
+ const id = request && request.id !== void 0 ? request.id : null;
1051
+ const ok = (result, events = [], shutdown = false) => ({
1052
+ response: { jsonrpc: "2.0", id, result },
1053
+ events,
1054
+ shutdown
1055
+ });
1056
+ const fail = (code, message) => ({
1057
+ response: { jsonrpc: "2.0", id, error: { code, message } },
1058
+ events: [],
1059
+ shutdown: false
1060
+ });
1061
+ if (!request || typeof request.method !== "string") {
1062
+ return fail(CODE.INVALID_REQUEST, "Invalid request: missing method");
1063
+ }
1064
+ const p = request.params ?? {};
1065
+ const doc = () => describeData(session.workbook ?? void 0);
1066
+ try {
1067
+ switch (request.method) {
1068
+ case "open": {
1069
+ const path = str(p.path);
1070
+ if (path === void 0) return fail(CODE.INVALID_PARAMS, "open requires params.path");
1071
+ session.open(path, { force: p.force === true });
1072
+ return ok({ path: session.path, ...doc() });
1073
+ }
1074
+ case "describe":
1075
+ return ok(doc());
1076
+ case "validate": {
1077
+ const d = doc();
1078
+ const problems = d.graph.cycles.map((c) => `Dependency cycle: ${c.join(" -> ")}`);
1079
+ return ok({ ok: problems.length === 0, problems, cellCount: d.cells.length });
1080
+ }
1081
+ case "graph":
1082
+ return ok(doc().graph);
1083
+ case "run": {
1084
+ const { result, events } = await session.run(str(p.only));
1085
+ const evts = events.map((e) => ({
1086
+ jsonrpc: "2.0",
1087
+ method: "cell/event",
1088
+ params: { type: e.type, cellId: e.cellId, error: e.error }
1089
+ }));
1090
+ return ok(result, evts);
1091
+ }
1092
+ case "cell/add":
1093
+ session.addCell(
1094
+ {
1095
+ id: str(p.id) ?? "",
1096
+ type: str(p.type) ?? "",
1097
+ content: str(p.content),
1098
+ dependsOn: Array.isArray(p.dependsOn) ? p.dependsOn : void 0
1099
+ },
1100
+ p.position
1101
+ );
1102
+ return ok(doc());
1103
+ case "cell/edit":
1104
+ session.editCell(str(p.id) ?? "", {
1105
+ content: str(p.content),
1106
+ type: str(p.type),
1107
+ dependsOn: Array.isArray(p.dependsOn) ? p.dependsOn : void 0
1108
+ });
1109
+ return ok(doc());
1110
+ case "cell/rm": {
1111
+ const changedCells = session.removeCell(str(p.id) ?? "", { force: p.force === true });
1112
+ return ok({ changedCells, ...doc() });
1113
+ }
1114
+ case "cell/move":
1115
+ session.moveCell(str(p.id) ?? "", p.position ?? {});
1116
+ return ok(doc());
1117
+ case "cell/rename":
1118
+ session.renameCell(str(p.oldId) ?? "", str(p.newId) ?? "");
1119
+ return ok(doc());
1120
+ case "meta/get":
1121
+ return ok(session.workbook?.metadata ?? {});
1122
+ case "meta/set":
1123
+ session.setMetadata({
1124
+ title: str(p.title),
1125
+ author: str(p.author),
1126
+ description: str(p.description),
1127
+ tags: Array.isArray(p.tags) ? p.tags : void 0
1128
+ });
1129
+ return ok(session.workbook?.metadata ?? {});
1130
+ case "save":
1131
+ return ok({ path: session.save(str(p.path)) });
1132
+ case "capabilities":
1133
+ return ok(capabilitiesInfo());
1134
+ case "functions":
1135
+ return ok(listFunctions());
1136
+ case "shutdown":
1137
+ return ok({ ok: true }, [], true);
1138
+ default:
1139
+ return fail(CODE.METHOD_NOT_FOUND, `Method not found: ${request.method}`);
1140
+ }
1141
+ } catch (error) {
1142
+ return fail(CODE.INTERNAL, error instanceof Error ? error.message : String(error));
1143
+ }
1144
+ }
1145
+
1146
+ // src/formatter.ts
1147
+ function bigintReplacer(_key, val) {
1148
+ return typeof val === "bigint" ? `${val}n` : val;
1149
+ }
1150
+ function safeStringify(value) {
1151
+ try {
1152
+ const json = JSON.stringify(value, bigintReplacer);
1153
+ if (json !== void 0) return json;
1154
+ } catch {
1155
+ }
1156
+ try {
1157
+ const seen = /* @__PURE__ */ new WeakSet();
1158
+ const json = JSON.stringify(value, (_key, val) => {
1159
+ if (typeof val === "bigint") return `${val}n`;
1160
+ if (val !== null && typeof val === "object") {
1161
+ if (seen.has(val)) return "[Circular]";
1162
+ seen.add(val);
1163
+ }
1164
+ return val;
1165
+ });
1166
+ return json ?? "(no result)";
1167
+ } catch {
1168
+ try {
1169
+ return String(value);
1170
+ } catch {
1171
+ return "[unserializable]";
1172
+ }
1173
+ }
1174
+ }
1175
+ function hasCustomToString(value) {
1176
+ const toString = value.toString;
1177
+ return typeof toString === "function" && toString !== Object.prototype.toString && toString !== Array.prototype.toString;
1178
+ }
1179
+ function formatResult(value) {
1180
+ if (value === null || value === void 0) return "(no result)";
1181
+ switch (typeof value) {
1182
+ case "string":
1183
+ return value;
1184
+ case "number":
1185
+ case "boolean":
1186
+ return String(value);
1187
+ case "bigint":
1188
+ return `${value}n`;
1189
+ case "function":
1190
+ return "(function)";
1191
+ case "symbol":
1192
+ return value.toString();
1193
+ case "object": {
1194
+ if (!Array.isArray(value) && hasCustomToString(value)) {
1195
+ try {
1196
+ const text = value.toString();
1197
+ if (text && text !== "[object Object]") return text;
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ return safeStringify(value);
1202
+ }
1203
+ default:
1204
+ return safeStringify(value);
1205
+ }
1206
+ }
1207
+
1208
+ export {
1209
+ writeFileAtomic,
1210
+ parseYamlHardened,
1211
+ buildDependencyGraph,
1212
+ topologicalSort,
1213
+ getDependents,
1214
+ getAncestors,
1215
+ toMermaid,
1216
+ toDOT,
1217
+ detectCycles,
1218
+ detectCellType,
1219
+ parseWorkbook,
1220
+ serializeWorkbook,
1221
+ stripOutputs,
1222
+ importWorkbook,
1223
+ WorkbookExecutor,
1224
+ createExecutor,
1225
+ addCell,
1226
+ editCell,
1227
+ removeCell,
1228
+ moveCell,
1229
+ renameCell,
1230
+ setMetadata,
1231
+ Session,
1232
+ SCHEMA_VERSION,
1233
+ VERSION,
1234
+ listFunctions,
1235
+ capabilitiesInfo,
1236
+ describeData,
1237
+ handleRequest,
1238
+ formatResult
1239
+ };