@danielsimonjr/mathts-workbook 0.1.8 → 0.3.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.
- package/README.md +247 -117
- package/dist/chunk-3KTM2DZC.js +720 -0
- package/dist/chunk-TMVFG3I3.js +581 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +1758 -79
- package/dist/index.d.ts +304 -9
- package/dist/index.js +35 -1
- package/dist/run-worker.d.ts +2 -0
- package/dist/run-worker.js +39 -0
- package/package.json +64 -62
- package/dist/chunk-L7UWFWMV.js +0 -269
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SUPPORTED_CELL_TYPES,
|
|
3
|
+
WorkbookExecutor,
|
|
4
|
+
buildDependencyGraph,
|
|
5
|
+
detectCycles,
|
|
6
|
+
getAncestors,
|
|
7
|
+
getDependents,
|
|
8
|
+
isValidIdentifier,
|
|
9
|
+
parseWorkbook,
|
|
10
|
+
serializeWorkbook
|
|
11
|
+
} from "./chunk-3KTM2DZC.js";
|
|
12
|
+
|
|
13
|
+
// src/fs-atomic.ts
|
|
14
|
+
import { writeFileSync, renameSync, unlinkSync } from "fs";
|
|
15
|
+
var counter = 0;
|
|
16
|
+
function writeFileAtomic(file, content) {
|
|
17
|
+
const tmp = `${file}.${process.pid}.${counter++}.tmp`;
|
|
18
|
+
writeFileSync(tmp, content, { encoding: "utf-8", flag: "wx" });
|
|
19
|
+
try {
|
|
20
|
+
renameSync(tmp, file);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
try {
|
|
23
|
+
unlinkSync(tmp);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/edit.ts
|
|
31
|
+
function cloneWorkbook(wb) {
|
|
32
|
+
return {
|
|
33
|
+
...wb,
|
|
34
|
+
metadata: { ...wb.metadata },
|
|
35
|
+
runtime: { ...wb.runtime },
|
|
36
|
+
cells: wb.cells.map((c) => ({
|
|
37
|
+
...c,
|
|
38
|
+
...c.dependsOn ? { dependsOn: [...c.dependsOn] } : {},
|
|
39
|
+
...c.metadata ? { metadata: { ...c.metadata } } : {}
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function assertSupportedType(type) {
|
|
44
|
+
if (!SUPPORTED_CELL_TYPES.includes(type)) {
|
|
45
|
+
throw new Error(`Unsupported cell type '${type}' (supported: ${SUPPORTED_CELL_TYPES.join(", ")})`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function indexOfCell(wb, id) {
|
|
49
|
+
const i = wb.cells.findIndex((c) => c.id === id);
|
|
50
|
+
if (i < 0) throw new Error(`No such cell: '${id}'`);
|
|
51
|
+
return i;
|
|
52
|
+
}
|
|
53
|
+
function validateDeps(wb, dependsOn, selfId) {
|
|
54
|
+
const ids = new Set(wb.cells.map((c) => c.id));
|
|
55
|
+
for (const dep of dependsOn) {
|
|
56
|
+
if (dep === selfId) throw new Error(`Cell '${selfId}' cannot depend on itself`);
|
|
57
|
+
if (!ids.has(dep)) {
|
|
58
|
+
throw new Error(`depends_on references unknown cell '${dep}'`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return dependsOn;
|
|
62
|
+
}
|
|
63
|
+
function assertNoNewCycle(before, after) {
|
|
64
|
+
if (detectCycles(buildDependencyGraph(before.cells)).length > 0) return;
|
|
65
|
+
const cycles = detectCycles(buildDependencyGraph(after.cells));
|
|
66
|
+
if (cycles.length > 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Operation would create a dependency cycle: ${cycles.map((c) => c.join(" -> ")).join("; ")}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function clearResult(cell) {
|
|
73
|
+
delete cell.output;
|
|
74
|
+
delete cell.error;
|
|
75
|
+
}
|
|
76
|
+
function resolveInsertIndex(wb, position) {
|
|
77
|
+
if (!position) return wb.cells.length;
|
|
78
|
+
if (position.at !== void 0) {
|
|
79
|
+
if (!Number.isInteger(position.at) || position.at < 0 || position.at > wb.cells.length) {
|
|
80
|
+
throw new Error(`--at ${position.at} out of range (0..${wb.cells.length})`);
|
|
81
|
+
}
|
|
82
|
+
return position.at;
|
|
83
|
+
}
|
|
84
|
+
if (position.before !== void 0) {
|
|
85
|
+
const i = wb.cells.findIndex((c) => c.id === position.before);
|
|
86
|
+
if (i < 0) throw new Error(`--before: no such cell '${position.before}'`);
|
|
87
|
+
return i;
|
|
88
|
+
}
|
|
89
|
+
if (position.after !== void 0) {
|
|
90
|
+
const i = wb.cells.findIndex((c) => c.id === position.after);
|
|
91
|
+
if (i < 0) throw new Error(`--after: no such cell '${position.after}'`);
|
|
92
|
+
return i + 1;
|
|
93
|
+
}
|
|
94
|
+
return wb.cells.length;
|
|
95
|
+
}
|
|
96
|
+
function addCell(wb, spec, position) {
|
|
97
|
+
if (!isValidIdentifier(spec.id)) {
|
|
98
|
+
throw new Error(`Invalid cell id '${spec.id}': must be an identifier ([A-Za-z_][A-Za-z0-9_]*)`);
|
|
99
|
+
}
|
|
100
|
+
if (wb.cells.some((c) => c.id === spec.id)) {
|
|
101
|
+
throw new Error(`Duplicate cell id '${spec.id}'`);
|
|
102
|
+
}
|
|
103
|
+
assertSupportedType(spec.type);
|
|
104
|
+
const next = cloneWorkbook(wb);
|
|
105
|
+
const cell = { id: spec.id, type: spec.type, content: spec.content ?? "" };
|
|
106
|
+
const deps = spec.dependsOn ? validateDeps(next, spec.dependsOn, spec.id) : void 0;
|
|
107
|
+
if (deps && deps.length > 0) cell.dependsOn = deps;
|
|
108
|
+
next.cells.splice(resolveInsertIndex(next, position), 0, cell);
|
|
109
|
+
assertNoNewCycle(wb, next);
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
function editCell(wb, id, changes) {
|
|
113
|
+
const next = cloneWorkbook(wb);
|
|
114
|
+
const cell = next.cells[indexOfCell(next, id)];
|
|
115
|
+
let touched = false;
|
|
116
|
+
if (changes.type !== void 0) {
|
|
117
|
+
assertSupportedType(changes.type);
|
|
118
|
+
cell.type = changes.type;
|
|
119
|
+
touched = true;
|
|
120
|
+
}
|
|
121
|
+
if (changes.content !== void 0) {
|
|
122
|
+
cell.content = changes.content;
|
|
123
|
+
touched = true;
|
|
124
|
+
}
|
|
125
|
+
if (changes.dependsOn !== void 0) {
|
|
126
|
+
const deps = validateDeps(next, changes.dependsOn, id);
|
|
127
|
+
if (deps.length > 0) cell.dependsOn = deps;
|
|
128
|
+
else delete cell.dependsOn;
|
|
129
|
+
touched = true;
|
|
130
|
+
}
|
|
131
|
+
if (touched) clearResult(cell);
|
|
132
|
+
if (changes.dependsOn !== void 0) assertNoNewCycle(wb, next);
|
|
133
|
+
return next;
|
|
134
|
+
}
|
|
135
|
+
function removeCell(wb, id, options) {
|
|
136
|
+
const next = cloneWorkbook(wb);
|
|
137
|
+
indexOfCell(next, id);
|
|
138
|
+
const dependents = next.cells.filter((c) => (c.dependsOn ?? []).includes(id)).map((c) => c.id);
|
|
139
|
+
if (dependents.length > 0 && !options?.force) {
|
|
140
|
+
throw new Error(`Cell '${id}' has dependents: ${dependents.join(", ")} (use --force to remove and detach)`);
|
|
141
|
+
}
|
|
142
|
+
const changedCells = [];
|
|
143
|
+
for (const c of next.cells) {
|
|
144
|
+
if (c.dependsOn?.includes(id)) {
|
|
145
|
+
c.dependsOn = c.dependsOn.filter((d) => d !== id);
|
|
146
|
+
if (c.dependsOn.length === 0) delete c.dependsOn;
|
|
147
|
+
clearResult(c);
|
|
148
|
+
changedCells.push(c.id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
next.cells = next.cells.filter((c) => c.id !== id);
|
|
152
|
+
return { workbook: next, changedCells };
|
|
153
|
+
}
|
|
154
|
+
function moveCell(wb, id, position) {
|
|
155
|
+
if (position.before === id || position.after === id) return cloneWorkbook(wb);
|
|
156
|
+
const next = cloneWorkbook(wb);
|
|
157
|
+
const from = indexOfCell(next, id);
|
|
158
|
+
const [cell] = next.cells.splice(from, 1);
|
|
159
|
+
next.cells.splice(resolveInsertIndex(next, position), 0, cell);
|
|
160
|
+
return next;
|
|
161
|
+
}
|
|
162
|
+
function renameCell(wb, oldId, newId) {
|
|
163
|
+
if (oldId === newId) return cloneWorkbook(wb);
|
|
164
|
+
const next = cloneWorkbook(wb);
|
|
165
|
+
const cell = next.cells[indexOfCell(next, oldId)];
|
|
166
|
+
if (!isValidIdentifier(newId)) {
|
|
167
|
+
throw new Error(`Invalid cell id '${newId}': must be an identifier ([A-Za-z_][A-Za-z0-9_]*)`);
|
|
168
|
+
}
|
|
169
|
+
if (next.cells.some((c) => c.id === newId)) {
|
|
170
|
+
throw new Error(`Duplicate cell id '${newId}'`);
|
|
171
|
+
}
|
|
172
|
+
cell.id = newId;
|
|
173
|
+
const reference = new RegExp(`\\b${oldId}\\b`, "g");
|
|
174
|
+
for (const c of next.cells) {
|
|
175
|
+
if (c.dependsOn?.includes(oldId)) {
|
|
176
|
+
c.dependsOn = c.dependsOn.map((d) => d === oldId ? newId : d);
|
|
177
|
+
c.content = c.content.replace(reference, newId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
function setMetadata(wb, changes) {
|
|
183
|
+
const next = cloneWorkbook(wb);
|
|
184
|
+
if (changes.title !== void 0) next.metadata.title = changes.title;
|
|
185
|
+
if (changes.author !== void 0) next.metadata.author = changes.author;
|
|
186
|
+
if (changes.description !== void 0) next.metadata.description = changes.description;
|
|
187
|
+
if (changes.tags !== void 0) next.metadata.tags = changes.tags;
|
|
188
|
+
return next;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/session.ts
|
|
192
|
+
import { readFileSync } from "fs";
|
|
193
|
+
function canonicalCell(cell) {
|
|
194
|
+
const meta = cell.metadata ?? {};
|
|
195
|
+
const sortedMeta = {};
|
|
196
|
+
for (const key of Object.keys(meta).sort()) sortedMeta[key] = meta[key];
|
|
197
|
+
return JSON.stringify([cell.type, cell.content, cell.dependsOn ?? [], sortedMeta]);
|
|
198
|
+
}
|
|
199
|
+
var Session = class {
|
|
200
|
+
workbook = null;
|
|
201
|
+
path = null;
|
|
202
|
+
dirty = false;
|
|
203
|
+
cache = /* @__PURE__ */ new Map();
|
|
204
|
+
stale = /* @__PURE__ */ new Set();
|
|
205
|
+
require() {
|
|
206
|
+
if (!this.workbook) throw new Error("No workbook open");
|
|
207
|
+
return this.workbook;
|
|
208
|
+
}
|
|
209
|
+
/** A cell has a usable, up-to-date result (the basis for "not stale"). */
|
|
210
|
+
hasValidResult(id) {
|
|
211
|
+
const r = this.cache.get(id);
|
|
212
|
+
return r !== void 0 && (r.status === "success" || r.status === "pass");
|
|
213
|
+
}
|
|
214
|
+
/** Load a workbook file into the session. Refuses to discard unsaved edits. */
|
|
215
|
+
open(path, options) {
|
|
216
|
+
if (this.dirty && !options?.force) {
|
|
217
|
+
throw new Error("Workbook has unsaved changes (open with force to discard)");
|
|
218
|
+
}
|
|
219
|
+
const content = readFileSync(path, "utf-8");
|
|
220
|
+
const parsed = parseWorkbook(content);
|
|
221
|
+
if (!parsed.success) {
|
|
222
|
+
throw new Error(`Parse errors: ${(parsed.errors ?? []).join("; ")}`);
|
|
223
|
+
}
|
|
224
|
+
this.workbook = parsed.workbook;
|
|
225
|
+
this.path = path;
|
|
226
|
+
this.cache.clear();
|
|
227
|
+
this.stale = new Set(this.workbook.cells.map((c) => c.id));
|
|
228
|
+
this.dirty = false;
|
|
229
|
+
}
|
|
230
|
+
/** Replace the workbook and recompute the stale set by diffing old vs new. */
|
|
231
|
+
commit(next) {
|
|
232
|
+
const old = this.require();
|
|
233
|
+
const oldCanonical = new Map(old.cells.map((c) => [c.id, canonicalCell(c)]));
|
|
234
|
+
const changed = /* @__PURE__ */ new Set();
|
|
235
|
+
for (const cell of next.cells) {
|
|
236
|
+
const prev = oldCanonical.get(cell.id);
|
|
237
|
+
if (prev === void 0 || prev !== canonicalCell(cell)) changed.add(cell.id);
|
|
238
|
+
}
|
|
239
|
+
const graph = buildDependencyGraph(next.cells);
|
|
240
|
+
const affected = new Set(changed);
|
|
241
|
+
for (const id of changed) {
|
|
242
|
+
for (const dep of getDependents(graph, id)) affected.add(dep);
|
|
243
|
+
}
|
|
244
|
+
const survivingIds = new Set(next.cells.map((c) => c.id));
|
|
245
|
+
for (const id of [...this.cache.keys()]) {
|
|
246
|
+
if (!survivingIds.has(id) || affected.has(id)) this.cache.delete(id);
|
|
247
|
+
}
|
|
248
|
+
this.stale = new Set(next.cells.map((c) => c.id).filter((id) => !this.hasValidResult(id)));
|
|
249
|
+
this.workbook = next;
|
|
250
|
+
this.dirty = true;
|
|
251
|
+
}
|
|
252
|
+
// --- Edits (each is atomic: the pure op throws before any state change) ---
|
|
253
|
+
addCell(spec, position) {
|
|
254
|
+
this.commit(addCell(this.require(), spec, position));
|
|
255
|
+
}
|
|
256
|
+
editCell(id, changes) {
|
|
257
|
+
this.commit(editCell(this.require(), id, changes));
|
|
258
|
+
}
|
|
259
|
+
removeCell(id, options) {
|
|
260
|
+
const result = removeCell(this.require(), id, options);
|
|
261
|
+
this.commit(result.workbook);
|
|
262
|
+
return result.changedCells;
|
|
263
|
+
}
|
|
264
|
+
moveCell(id, position) {
|
|
265
|
+
this.commit(moveCell(this.require(), id, position));
|
|
266
|
+
}
|
|
267
|
+
renameCell(oldId, newId) {
|
|
268
|
+
this.commit(renameCell(this.require(), oldId, newId));
|
|
269
|
+
}
|
|
270
|
+
setMetadata(changes) {
|
|
271
|
+
this.commit(setMetadata(this.require(), changes));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Incremental run: execute only stale cells in the target set (`only` + its
|
|
275
|
+
* ancestors, or the whole workbook), reusing cached outputs for the rest.
|
|
276
|
+
* Returns the merged report over the target plus the streamed events.
|
|
277
|
+
*/
|
|
278
|
+
async run(only) {
|
|
279
|
+
const wb = this.require();
|
|
280
|
+
const graph = buildDependencyGraph(wb.cells);
|
|
281
|
+
const target = only ? new Set(getAncestors(graph, only)) : new Set(wb.cells.map((c) => c.id));
|
|
282
|
+
const toRun = new Set([...this.stale].filter((id) => target.has(id)));
|
|
283
|
+
const executor = new WorkbookExecutor(wb);
|
|
284
|
+
const seed = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const [id, res] of this.cache) {
|
|
286
|
+
if (res.status === "success" || res.status === "pass") {
|
|
287
|
+
seed.set(id, res.output);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
executor.seedOutputs(seed);
|
|
291
|
+
const events = [];
|
|
292
|
+
executor.on((e) => events.push({ type: e.type, cellId: e.cellId, error: e.error }));
|
|
293
|
+
const report = await executor.runReport({ only, runIds: toRun });
|
|
294
|
+
if (!report.ok && report.cells.some((c) => c.id === "(workbook)")) {
|
|
295
|
+
return { result: report, events };
|
|
296
|
+
}
|
|
297
|
+
for (const cr of report.cells) {
|
|
298
|
+
this.cache.set(cr.id, cr);
|
|
299
|
+
if (cr.status === "success" || cr.status === "pass") this.stale.delete(cr.id);
|
|
300
|
+
}
|
|
301
|
+
const ordered = graph.executionOrder.filter((id) => target.has(id));
|
|
302
|
+
const cells = ordered.map((id) => this.cache.get(id)).filter((c) => c !== void 0);
|
|
303
|
+
const ok = cells.every((c) => c.status === "success" || c.status === "pass");
|
|
304
|
+
return { result: { cells, ok }, events };
|
|
305
|
+
}
|
|
306
|
+
/** The set of cells awaiting (re-)execution. */
|
|
307
|
+
staleIds() {
|
|
308
|
+
return [...this.stale];
|
|
309
|
+
}
|
|
310
|
+
/** Serialize the current workbook and write it atomically. */
|
|
311
|
+
save(path) {
|
|
312
|
+
const wb = this.require();
|
|
313
|
+
const target = path ?? this.path;
|
|
314
|
+
if (!target) throw new Error("No path to save to");
|
|
315
|
+
writeFileAtomic(target, serializeWorkbook(wb));
|
|
316
|
+
this.path = target;
|
|
317
|
+
this.dirty = false;
|
|
318
|
+
return target;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/contract.ts
|
|
323
|
+
var SCHEMA_VERSION = { major: 1, minor: 0 };
|
|
324
|
+
var VERSION = "0.1.0";
|
|
325
|
+
var COMMAND_NAMES = [
|
|
326
|
+
"run",
|
|
327
|
+
"describe",
|
|
328
|
+
"validate",
|
|
329
|
+
"graph",
|
|
330
|
+
"strip",
|
|
331
|
+
"new",
|
|
332
|
+
"capabilities",
|
|
333
|
+
"templates",
|
|
334
|
+
"cell",
|
|
335
|
+
"serve",
|
|
336
|
+
"functions",
|
|
337
|
+
"meta",
|
|
338
|
+
"import",
|
|
339
|
+
"export"
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
// src/introspect.ts
|
|
343
|
+
import * as mathFunctions from "@danielsimonjr/mathts-functions";
|
|
344
|
+
var functionsCache;
|
|
345
|
+
function listFunctions() {
|
|
346
|
+
if (functionsCache) return functionsCache;
|
|
347
|
+
const functions = [];
|
|
348
|
+
const constants = [];
|
|
349
|
+
for (const [name, value] of Object.entries(mathFunctions)) {
|
|
350
|
+
if (typeof value === "function") functions.push(name);
|
|
351
|
+
else if (value !== null && typeof value !== "object") constants.push(name);
|
|
352
|
+
}
|
|
353
|
+
functionsCache = { functions: functions.sort(), constants: constants.sort() };
|
|
354
|
+
return functionsCache;
|
|
355
|
+
}
|
|
356
|
+
var CELL_SCHEMAS = {
|
|
357
|
+
code: "MathTS expression; the cell output is its evaluated value",
|
|
358
|
+
markdown: "Markdown text (rendered to HTML on export)",
|
|
359
|
+
equation: "MathTS expression syntax, rendered to MathML on export (display-only)",
|
|
360
|
+
test: "Boolean MathTS expression; true = pass, false = fail",
|
|
361
|
+
data: "A YAML/JSON literal value (e.g. a numeric array)",
|
|
362
|
+
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"
|
|
363
|
+
};
|
|
364
|
+
function capabilitiesInfo() {
|
|
365
|
+
return {
|
|
366
|
+
name: "mtsw",
|
|
367
|
+
version: VERSION,
|
|
368
|
+
schemaVersion: SCHEMA_VERSION,
|
|
369
|
+
cellTypes: {
|
|
370
|
+
supported: [...SUPPORTED_CELL_TYPES],
|
|
371
|
+
deferred: ["tensor", "export"]
|
|
372
|
+
},
|
|
373
|
+
cellSchemas: CELL_SCHEMAS,
|
|
374
|
+
commands: [...COMMAND_NAMES],
|
|
375
|
+
features: {
|
|
376
|
+
json: true,
|
|
377
|
+
write: true,
|
|
378
|
+
runCell: true,
|
|
379
|
+
editCell: true,
|
|
380
|
+
serve: true,
|
|
381
|
+
incremental: true,
|
|
382
|
+
functions: true,
|
|
383
|
+
import: true,
|
|
384
|
+
export: true
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/doc.ts
|
|
390
|
+
function describeData(wb) {
|
|
391
|
+
const cells = (wb?.cells ?? []).map((c) => ({
|
|
392
|
+
id: c.id,
|
|
393
|
+
type: c.type,
|
|
394
|
+
content: c.content,
|
|
395
|
+
dependsOn: c.dependsOn ?? [],
|
|
396
|
+
metadata: c.metadata ?? {},
|
|
397
|
+
output: c.output ?? null,
|
|
398
|
+
error: c.error ?? null
|
|
399
|
+
}));
|
|
400
|
+
const graph = wb ? buildDependencyGraph(wb.cells) : void 0;
|
|
401
|
+
const edges = graph ? [...graph.nodes].flatMap(([id, node]) => node.dependencies.map((dep) => ({ from: dep, to: id }))) : [];
|
|
402
|
+
const cycles = graph ? detectCycles(graph) : [];
|
|
403
|
+
return {
|
|
404
|
+
version: wb?.version ?? "1.0",
|
|
405
|
+
metadata: wb?.metadata ?? {},
|
|
406
|
+
runtime: wb?.runtime ?? { engine: "mathts", execution: "reactive" },
|
|
407
|
+
cells,
|
|
408
|
+
graph: { edges, cycles }
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/rpc.ts
|
|
413
|
+
var CODE = {
|
|
414
|
+
PARSE: -32700,
|
|
415
|
+
INVALID_REQUEST: -32600,
|
|
416
|
+
METHOD_NOT_FOUND: -32601,
|
|
417
|
+
INVALID_PARAMS: -32602,
|
|
418
|
+
INTERNAL: -32603
|
|
419
|
+
};
|
|
420
|
+
function str(value) {
|
|
421
|
+
return typeof value === "string" ? value : void 0;
|
|
422
|
+
}
|
|
423
|
+
async function handleRequest(session, request) {
|
|
424
|
+
const id = request && request.id !== void 0 ? request.id : null;
|
|
425
|
+
const ok = (result, events = [], shutdown = false) => ({
|
|
426
|
+
response: { jsonrpc: "2.0", id, result },
|
|
427
|
+
events,
|
|
428
|
+
shutdown
|
|
429
|
+
});
|
|
430
|
+
const fail = (code, message) => ({
|
|
431
|
+
response: { jsonrpc: "2.0", id, error: { code, message } },
|
|
432
|
+
events: [],
|
|
433
|
+
shutdown: false
|
|
434
|
+
});
|
|
435
|
+
if (!request || typeof request.method !== "string") {
|
|
436
|
+
return fail(CODE.INVALID_REQUEST, "Invalid request: missing method");
|
|
437
|
+
}
|
|
438
|
+
const p = request.params ?? {};
|
|
439
|
+
const doc = () => describeData(session.workbook ?? void 0);
|
|
440
|
+
try {
|
|
441
|
+
switch (request.method) {
|
|
442
|
+
case "open": {
|
|
443
|
+
const path = str(p.path);
|
|
444
|
+
if (path === void 0) return fail(CODE.INVALID_PARAMS, "open requires params.path");
|
|
445
|
+
session.open(path, { force: p.force === true });
|
|
446
|
+
return ok({ path: session.path, ...doc() });
|
|
447
|
+
}
|
|
448
|
+
case "describe":
|
|
449
|
+
return ok(doc());
|
|
450
|
+
case "validate": {
|
|
451
|
+
const d = doc();
|
|
452
|
+
const problems = d.graph.cycles.map((c) => `Dependency cycle: ${c.join(" -> ")}`);
|
|
453
|
+
return ok({ ok: problems.length === 0, problems, cellCount: d.cells.length });
|
|
454
|
+
}
|
|
455
|
+
case "graph":
|
|
456
|
+
return ok(doc().graph);
|
|
457
|
+
case "run": {
|
|
458
|
+
const { result, events } = await session.run(str(p.only));
|
|
459
|
+
const evts = events.map((e) => ({
|
|
460
|
+
jsonrpc: "2.0",
|
|
461
|
+
method: "cell/event",
|
|
462
|
+
params: { type: e.type, cellId: e.cellId, error: e.error }
|
|
463
|
+
}));
|
|
464
|
+
return ok(result, evts);
|
|
465
|
+
}
|
|
466
|
+
case "cell/add":
|
|
467
|
+
session.addCell(
|
|
468
|
+
{
|
|
469
|
+
id: str(p.id) ?? "",
|
|
470
|
+
type: str(p.type) ?? "",
|
|
471
|
+
content: str(p.content),
|
|
472
|
+
dependsOn: Array.isArray(p.dependsOn) ? p.dependsOn : void 0
|
|
473
|
+
},
|
|
474
|
+
p.position
|
|
475
|
+
);
|
|
476
|
+
return ok(doc());
|
|
477
|
+
case "cell/edit":
|
|
478
|
+
session.editCell(str(p.id) ?? "", {
|
|
479
|
+
content: str(p.content),
|
|
480
|
+
type: str(p.type),
|
|
481
|
+
dependsOn: Array.isArray(p.dependsOn) ? p.dependsOn : void 0
|
|
482
|
+
});
|
|
483
|
+
return ok(doc());
|
|
484
|
+
case "cell/rm": {
|
|
485
|
+
const changedCells = session.removeCell(str(p.id) ?? "", { force: p.force === true });
|
|
486
|
+
return ok({ changedCells, ...doc() });
|
|
487
|
+
}
|
|
488
|
+
case "cell/move":
|
|
489
|
+
session.moveCell(str(p.id) ?? "", p.position ?? {});
|
|
490
|
+
return ok(doc());
|
|
491
|
+
case "cell/rename":
|
|
492
|
+
session.renameCell(str(p.oldId) ?? "", str(p.newId) ?? "");
|
|
493
|
+
return ok(doc());
|
|
494
|
+
case "meta/get":
|
|
495
|
+
return ok(session.workbook?.metadata ?? {});
|
|
496
|
+
case "meta/set":
|
|
497
|
+
session.setMetadata({
|
|
498
|
+
title: str(p.title),
|
|
499
|
+
author: str(p.author),
|
|
500
|
+
description: str(p.description),
|
|
501
|
+
tags: Array.isArray(p.tags) ? p.tags : void 0
|
|
502
|
+
});
|
|
503
|
+
return ok(session.workbook?.metadata ?? {});
|
|
504
|
+
case "save":
|
|
505
|
+
return ok({ path: session.save(str(p.path)) });
|
|
506
|
+
case "capabilities":
|
|
507
|
+
return ok(capabilitiesInfo());
|
|
508
|
+
case "functions":
|
|
509
|
+
return ok(listFunctions());
|
|
510
|
+
case "shutdown":
|
|
511
|
+
return ok({ ok: true }, [], true);
|
|
512
|
+
default:
|
|
513
|
+
return fail(CODE.METHOD_NOT_FOUND, `Method not found: ${request.method}`);
|
|
514
|
+
}
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return fail(CODE.INTERNAL, error instanceof Error ? error.message : String(error));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/timeout-runner.ts
|
|
521
|
+
import { Worker } from "worker_threads";
|
|
522
|
+
var WorkbookTimeoutError = class extends Error {
|
|
523
|
+
timeoutMs;
|
|
524
|
+
constructor(timeoutMs) {
|
|
525
|
+
super(`workbook execution exceeded ${timeoutMs}ms and was terminated`);
|
|
526
|
+
this.name = "WorkbookTimeoutError";
|
|
527
|
+
this.timeoutMs = timeoutMs;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
function runWorkbookWithTimeout(source, options) {
|
|
531
|
+
const { timeoutMs } = options;
|
|
532
|
+
const workerUrl = new URL("./run-worker.js", import.meta.url);
|
|
533
|
+
return new Promise((resolve, reject) => {
|
|
534
|
+
const worker = new Worker(workerUrl, { workerData: { source } });
|
|
535
|
+
let settled = false;
|
|
536
|
+
const finish = (fn) => {
|
|
537
|
+
if (settled) return;
|
|
538
|
+
settled = true;
|
|
539
|
+
clearTimeout(timer);
|
|
540
|
+
void worker.terminate().catch(() => {
|
|
541
|
+
});
|
|
542
|
+
fn();
|
|
543
|
+
};
|
|
544
|
+
const timer = setTimeout(() => {
|
|
545
|
+
finish(() => reject(new WorkbookTimeoutError(timeoutMs)));
|
|
546
|
+
}, timeoutMs);
|
|
547
|
+
worker.once("message", (msg) => {
|
|
548
|
+
finish(() => {
|
|
549
|
+
if (msg.ok) resolve(msg.report);
|
|
550
|
+
else reject(new Error(msg.error));
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
worker.once("error", (error) => {
|
|
554
|
+
finish(() => reject(error));
|
|
555
|
+
});
|
|
556
|
+
worker.once("exit", (code) => {
|
|
557
|
+
finish(
|
|
558
|
+
() => reject(new Error(`workbook worker exited with code ${code} before returning a result`))
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export {
|
|
565
|
+
writeFileAtomic,
|
|
566
|
+
addCell,
|
|
567
|
+
editCell,
|
|
568
|
+
removeCell,
|
|
569
|
+
moveCell,
|
|
570
|
+
renameCell,
|
|
571
|
+
setMetadata,
|
|
572
|
+
Session,
|
|
573
|
+
SCHEMA_VERSION,
|
|
574
|
+
VERSION,
|
|
575
|
+
listFunctions,
|
|
576
|
+
capabilitiesInfo,
|
|
577
|
+
describeData,
|
|
578
|
+
handleRequest,
|
|
579
|
+
WorkbookTimeoutError,
|
|
580
|
+
runWorkbookWithTimeout
|
|
581
|
+
};
|
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MathTS Workbook CLI
|
|
4
|
+
*
|
|
5
|
+
* Command handlers are pure functions returning `{ stdout, stderr, exitCode }`
|
|
6
|
+
* (no direct console / process.exit) so they can be unit-tested. The thin
|
|
7
|
+
* `main()` at the bottom wires them to the real streams, and only runs when
|
|
8
|
+
* this file is the process entry point.
|
|
9
|
+
*/
|
|
10
|
+
interface CommandResult {
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
exitCode: number;
|
|
14
|
+
}
|
|
15
|
+
declare function runCommand(args: string[]): Promise<CommandResult>;
|
|
16
|
+
declare function validateCommand(args: string[]): CommandResult;
|
|
17
|
+
declare function describeCommand(args: string[]): CommandResult;
|
|
18
|
+
declare function capabilitiesCommand(args: string[]): CommandResult;
|
|
19
|
+
declare function templatesCommand(args: string[]): CommandResult;
|
|
20
|
+
declare function functionsCommand(args: string[]): CommandResult;
|
|
21
|
+
declare function metaCommand(args: string[]): CommandResult;
|
|
22
|
+
declare function exportCommand(args: string[]): Promise<CommandResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Run the JSON-RPC-over-stdio server loop until `shutdown`, stdin EOF, or a
|
|
25
|
+
* termination signal. NDJSON via readline (chunked-input / CRLF safe). Events
|
|
26
|
+
* for a run are flushed before that run's response (not mid-run in v1).
|
|
27
|
+
*/
|
|
28
|
+
declare function runServer(input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<void>;
|
|
29
|
+
declare function serveCommand(): Promise<CommandResult>;
|
|
30
|
+
declare function graphCommand(args: string[]): CommandResult;
|
|
31
|
+
declare function stripCommand(args: string[]): CommandResult;
|
|
32
|
+
declare function newCommand(args: string[]): CommandResult;
|
|
33
|
+
declare function importCommand(args: string[]): CommandResult;
|
|
34
|
+
declare function cellCommand(args: string[]): CommandResult;
|
|
35
|
+
/**
|
|
36
|
+
* Route argv to a command handler. Pure — returns a CommandResult.
|
|
37
|
+
*/
|
|
38
|
+
declare function dispatch(argv: string[]): Promise<CommandResult>;
|
|
39
|
+
|
|
40
|
+
export { type CommandResult, capabilitiesCommand, cellCommand, describeCommand, dispatch, exportCommand, functionsCommand, graphCommand, importCommand, metaCommand, newCommand, runCommand, runServer, serveCommand, stripCommand, templatesCommand, validateCommand };
|