@danielsimonjr/mathts-workbook 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -19,6 +19,7 @@ mtsw run example.mtsw
19
19
  mtsw run example.mtsw -v # also print the execution event stream
20
20
  mtsw run example.mtsw --json # machine-readable envelope on stdout
21
21
  mtsw run example.mtsw -c gauss # run one cell + its transitive deps (stateless)
22
+ mtsw run example.mtsw --timeout 5000 # run in a worker; killed (exit 1) if it exceeds 5s
22
23
 
23
24
  # Describe the structured document model (cells, outputs, dependency graph).
24
25
  mtsw describe example.mtsw --json
@@ -73,6 +74,9 @@ mtsw export example.mtsw -o example.html # one offline file; stdout if no -o
73
74
  mtsw export example.mtsw --no-run # render cached outputs without executing
74
75
  mtsw export example.mtsw --json # envelope: { data: { path, bytes } }
75
76
 
77
+ # Other export formats: --format tex|pdf|json|ipynb (html is the default).
78
+ mtsw export example.mtsw --format ipynb -o example.ipynb # Jupyter notebook (nbformat v4)
79
+
76
80
  # Persistent session for a GUI/tooling: JSON-RPC 2.0 over stdio (NDJSON).
77
81
  mtsw serve
78
82
  # -> {"jsonrpc":"2.0","id":1,"method":"open","params":{"path":"example.mtsw"}}
@@ -94,9 +98,30 @@ dependencies**. Equation cells contain MathTS expression syntax (e.g.
94
98
  outputs); a whole-run failure such as a dependency cycle fails loudly rather than
95
99
  emitting a misleading document.
96
100
 
101
+ **`export --format ipynb` (Jupyter notebook).** Renders the same run report to a
102
+ structurally conformant **nbformat v4** JSON document: `markdown` cells map to
103
+ notebook markdown cells; every other cell type (`code`, `equation`, `test`, `data`,
104
+ `visualization`) maps to a notebook code cell, with a computed result becoming an
105
+ `execute_result` output (`data['text/plain']`), an error becoming an `error` output,
106
+ and a chart becoming a `display_data` output (inline SVG). Shares the same
107
+ run-then-render pipeline as `--format html`/`tex` (including `--no-run`, `--json`,
108
+ `-o`).
109
+
110
+ **`run --timeout <ms>` (kill-able worker-thread run).** By default, cell execution runs
111
+ in-process with no time budget — a runaway cell (e.g. an unbounded computation) hangs
112
+ the process. `--timeout` runs the whole workbook in a `worker_threads` Worker and
113
+ forcibly terminates it if it exceeds the budget, exiting 1 with a clear
114
+ `workbook execution exceeded <ms>ms and was terminated` message; termination kills the
115
+ worker outright, so it interrupts even a synchronous, CPU-bound runaway. It always runs
116
+ the entire workbook to completion-or-termination, so it's incompatible with `-c`/`-v`.
117
+ The same primitive is available programmatically as `runWorkbookWithTimeout(source, {
118
+ timeoutMs })` (throws `WorkbookTimeoutError` on timeout); cell outputs come back
119
+ pre-formatted to strings (via `formatResult`), since engine class instances (Complex,
120
+ matrices, …) don't survive the worker's `postMessage`.
121
+
97
122
  **`serve` (persistent session).** One long-lived process holds the workbook in memory with a per-cell result cache and a **stale set**: a `cell.*` edit marks that cell and its transitive dependents stale, and a `run` re-executes **only** the stale cells (reusing cached outputs for the rest) — the incremental latency win a GUI needs. Requests are processed strictly in order; `run` streams `cell/event` notifications (flushed before that run's response in v1, not mid-run). Edits stay in memory until `save`. Single-document per process; concurrent writers are last-write-wins.
98
123
 
99
- **Editing notes.** Cell edits are validity-preserving: an op that would create a duplicate/invalid id, a missing dependency, or a **dependency cycle** is rejected and the file is left byte-for-byte unchanged. Editing a cell clears its (now-stale) persisted output; `--at N` is the cell's final 0-based index; `--force` detaches dependents (clearing their outputs) but does not rewrite cell *content*, so a dependent that still references the removed id by name will error at run. Concurrent editors are last-write-wins (an optimistic-lock guard arrives with the `serve` session).
124
+ **Editing notes.** Cell edits are validity-preserving: an op that would create a duplicate/invalid id, a missing dependency, or a **dependency cycle** is rejected and the file is left byte-for-byte unchanged. Editing a cell clears its (now-stale) persisted output; `--at N` is the cell's final 0-based index; `--force` detaches dependents (clearing their outputs) but does not rewrite cell _content_, so a dependent that still references the removed id by name will error at run. Concurrent editors are last-write-wins (an optimistic-lock guard arrives with the `serve` session).
100
125
 
101
126
  Diagnostics and errors are written to stderr; results (including `--json`) go to stdout, so the exit code can be used in scripts independently of the output.
102
127
 
@@ -199,13 +224,13 @@ A `test` cell's expression must evaluate to a **boolean**: `true` passes, `false
199
224
 
200
225
  ## Cell types (v1)
201
226
 
202
- | Type | Status | Description |
203
- | ---------- | ------ | ------------------------------------------------------------ |
204
- | `markdown` | ✅ | Documentation (passed through verbatim) |
205
- | `code` | ✅ | MathTS expression script; last value is the result |
206
- | `data` | ✅ | Structured YAML, parsed (hardened) into a value |
207
- | `test` | ✅ | Boolean assertion (`true` = pass) |
208
- | `tensor` / `equation` / `visualization` / `export` | ⏳ | Reserved; not executed in v1 (reported as unsupported) |
227
+ | Type | Status | Description |
228
+ | -------------------------------------------------- | ------ | ------------------------------------------------------ |
229
+ | `markdown` | ✅ | Documentation (passed through verbatim) |
230
+ | `code` | ✅ | MathTS expression script; last value is the result |
231
+ | `data` | ✅ | Structured YAML, parsed (hardened) into a value |
232
+ | `test` | ✅ | Boolean assertion (`true` = pass) |
233
+ | `tensor` / `equation` / `visualization` / `export` | ⏳ | Reserved; not executed in v1 (reported as unsupported) |
209
234
 
210
235
  ## Execution modes
211
236
 
@@ -1,20 +1,3 @@
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
1
  // src/yaml-safe.ts
19
2
  import { parse as parseYaml } from "yaml";
20
3
  var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
@@ -653,496 +636,6 @@ function createExecutor(workbook) {
653
636
  return new WorkbookExecutor(workbook);
654
637
  }
655
638
 
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
639
  // src/formatter.ts
1147
640
  function bigintReplacer(_key, val) {
1148
641
  return typeof val === "bigint" ? `${val}n` : val;
@@ -1206,7 +699,6 @@ function formatResult(value) {
1206
699
  }
1207
700
 
1208
701
  export {
1209
- writeFileAtomic,
1210
702
  parseYamlHardened,
1211
703
  buildDependencyGraph,
1212
704
  topologicalSort,
@@ -1215,6 +707,8 @@ export {
1215
707
  toMermaid,
1216
708
  toDOT,
1217
709
  detectCycles,
710
+ isValidIdentifier,
711
+ SUPPORTED_CELL_TYPES,
1218
712
  detectCellType,
1219
713
  parseWorkbook,
1220
714
  serializeWorkbook,
@@ -1222,18 +716,5 @@ export {
1222
716
  importWorkbook,
1223
717
  WorkbookExecutor,
1224
718
  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
719
  formatResult
1239
720
  };