@danielsimonjr/mathts-workbook 0.2.0 → 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/dist/cli.js CHANGED
@@ -4,28 +4,31 @@ import {
4
4
  Session,
5
5
  VERSION,
6
6
  addCell,
7
- buildDependencyGraph,
8
7
  capabilitiesInfo,
9
- createExecutor,
10
8
  describeData,
11
- detectCycles,
12
9
  editCell,
13
- formatResult,
14
10
  handleRequest,
15
- importWorkbook,
16
11
  listFunctions,
17
12
  moveCell,
18
- parseWorkbook,
19
- parseYamlHardened,
20
13
  removeCell,
21
14
  renameCell,
22
- serializeWorkbook,
15
+ runWorkbookWithTimeout,
23
16
  setMetadata,
17
+ writeFileAtomic
18
+ } from "./chunk-TMVFG3I3.js";
19
+ import {
20
+ buildDependencyGraph,
21
+ createExecutor,
22
+ detectCycles,
23
+ formatResult,
24
+ importWorkbook,
25
+ parseWorkbook,
26
+ parseYamlHardened,
27
+ serializeWorkbook,
24
28
  stripOutputs,
25
29
  toDOT,
26
- toMermaid,
27
- writeFileAtomic
28
- } from "./chunk-TS2WDJ7W.js";
30
+ toMermaid
31
+ } from "./chunk-3KTM2DZC.js";
29
32
 
30
33
  // src/cli.ts
31
34
  import { readFileSync, writeFileSync, lstatSync, realpathSync } from "fs";
@@ -463,6 +466,85 @@ function toPDF(doc, outPath, options = {}) {
463
466
  return latexToPdf(toTeX(doc, { parse: parse3, fragment: false }), outPath, renderOpts);
464
467
  }
465
468
 
469
+ // src/ipynb.ts
470
+ function toSourceLines(content) {
471
+ if (content === "") return [];
472
+ const lines = content.split("\n");
473
+ return lines.map((line2, i) => i < lines.length - 1 ? `${line2}
474
+ ` : line2);
475
+ }
476
+ function textData(text) {
477
+ return { "text/plain": toSourceLines(text) };
478
+ }
479
+ function renderCodeCell(cell, counter) {
480
+ const outputs = [];
481
+ let executionCount = null;
482
+ if (cell.error !== void 0) {
483
+ outputs.push({
484
+ output_type: "error",
485
+ ename: "Error",
486
+ evalue: cell.error,
487
+ traceback: [cell.error]
488
+ });
489
+ } else if (cell.type === "chart") {
490
+ if (cell.chartSvg !== void 0) {
491
+ outputs.push({
492
+ output_type: "display_data",
493
+ data: { "image/svg+xml": toSourceLines(cell.chartSvg), "text/plain": ["<chart>"] },
494
+ metadata: {}
495
+ });
496
+ }
497
+ } else if (cell.type === "test" && cell.passed !== void 0) {
498
+ executionCount = counter.n++;
499
+ outputs.push({
500
+ output_type: "execute_result",
501
+ data: textData(cell.passed ? "true" : "false"),
502
+ metadata: {},
503
+ execution_count: executionCount
504
+ });
505
+ } else if (cell.output !== void 0) {
506
+ executionCount = counter.n++;
507
+ outputs.push({
508
+ output_type: "execute_result",
509
+ data: textData(cell.output),
510
+ metadata: {},
511
+ execution_count: executionCount
512
+ });
513
+ }
514
+ return {
515
+ cell_type: "code",
516
+ source: toSourceLines(cell.content),
517
+ metadata: {},
518
+ execution_count: executionCount,
519
+ outputs
520
+ };
521
+ }
522
+ function renderIpynbCell(cell, counter) {
523
+ if (cell.type === "markdown") {
524
+ return { cell_type: "markdown", source: toSourceLines(cell.content), metadata: {} };
525
+ }
526
+ return renderCodeCell(cell, counter);
527
+ }
528
+ function toIpynb(doc) {
529
+ const counter = { n: 1 };
530
+ const cells = doc.cells.map((cell) => renderIpynbCell(cell, counter));
531
+ const mathts = {};
532
+ if (doc.title !== void 0) mathts.title = doc.title;
533
+ if (doc.author !== void 0) mathts.author = doc.author;
534
+ if (doc.description !== void 0) mathts.description = doc.description;
535
+ if (doc.tags !== void 0) mathts.tags = doc.tags;
536
+ const notebook = {
537
+ nbformat: 4,
538
+ nbformat_minor: 5,
539
+ metadata: {
540
+ mathts,
541
+ language_info: { name: "mathts", file_extension: ".mtsw" }
542
+ },
543
+ cells
544
+ };
545
+ return JSON.stringify(notebook, null, 2);
546
+ }
547
+
466
548
  // src/svg.ts
467
549
  import { line, scatter, bar } from "@danielsimonjr/mathts-plot";
468
550
  function coerce(v) {
@@ -511,11 +593,15 @@ var HELP = `
511
593
  mtsw - MathTS Workbook CLI
512
594
 
513
595
  Usage:
514
- mtsw run <file> [-c <id>] [-v] [--json] [--write]
596
+ mtsw run <file> [-c <id>] [-v] [--json] [--write] [--timeout <ms>]
515
597
  Execute a workbook (or one cell + its
516
598
  deps with -c/--cell). -v: events,
517
599
  --json: machine output, --write: persist
518
- outputs back to the file.
600
+ outputs back to the file. --timeout runs
601
+ the whole workbook in a worker thread and
602
+ kills it (WorkbookTimeoutError) if it
603
+ exceeds the budget \u2014 a runaway cell can't
604
+ hang the process. Incompatible with -c/-v.
519
605
  mtsw describe <file> [--json] Structured document model (cells, graph)
520
606
  mtsw validate <file> [--json] Validate structure (ids, deps, cycles)
521
607
  mtsw graph <file> [-f mermaid|dot] Print the dependency graph
@@ -539,10 +625,12 @@ Usage:
539
625
  mtsw functions [--json] List functions/constants cells can call
540
626
  mtsw meta get <file> [--json] Show workbook metadata
541
627
  mtsw meta set <file> [--title s] [--author s] [--description s] [--tags a,b]
542
- mtsw export <file> [--format html|tex|json] [--fragment] [-o out] [--no-run]
628
+ mtsw export <file> [--format html|tex|json|pdf|ipynb] [--fragment] [-o out] [--no-run]
543
629
  Render to a self-contained HTML or LaTeX
544
630
  document (MathML/TikZ equations + charts,
545
- no external deps), or emit the executed
631
+ no external deps), a Jupyter notebook
632
+ (nbformat v4, default extension .ipynb), a
633
+ PDF (requires -o), or emit the executed
546
634
  run report as JSON. --fragment (tex only)
547
635
  omits the preamble for \\input; --no-run is
548
636
  incompatible with --format json.
@@ -584,6 +672,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
584
672
  "--template",
585
673
  "-c",
586
674
  "--cell",
675
+ "--timeout",
587
676
  "--id",
588
677
  "--content",
589
678
  "--content-file",
@@ -714,13 +803,32 @@ ${bullets(problems2)}`,
714
803
  exitCode: 1
715
804
  };
716
805
  }
806
+ const timeoutStr = flagValue(args, "--timeout");
717
807
  const verbose = args.includes("-v") || args.includes("--verbose");
718
- const executor = createExecutor(parsed.workbook);
808
+ let report;
719
809
  const events = [];
720
- if (verbose) {
721
- executor.on((event) => events.push(`[${event.type}] ${event.cellId ?? ""}`.trimEnd()));
810
+ if (timeoutStr !== void 0) {
811
+ const timeoutMs = Number(timeoutStr);
812
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
813
+ return fail([`--timeout must be a positive number of milliseconds (got '${timeoutStr}')`]);
814
+ }
815
+ if (cellId !== void 0 || verbose) {
816
+ return fail([
817
+ "--timeout does not support -c/--cell or -v/--verbose (it runs the whole workbook, in a worker, to completion or termination)"
818
+ ]);
819
+ }
820
+ try {
821
+ report = await runWorkbookWithTimeout(read.content, { timeoutMs });
822
+ } catch (error) {
823
+ return fail([error instanceof Error ? error.message : String(error)], { cells: [] });
824
+ }
825
+ } else {
826
+ const executor = createExecutor(parsed.workbook);
827
+ if (verbose) {
828
+ executor.on((event) => events.push(`[${event.type}] ${event.cellId ?? ""}`.trimEnd()));
829
+ }
830
+ report = await executor.runReport(cellId !== void 0 ? { only: cellId } : {});
722
831
  }
723
- const report = await executor.runReport(cellId !== void 0 ? { only: cellId } : {});
724
832
  const problems = report.ok ? [] : failureList(report.cells);
725
833
  const humanFailures = report.ok ? "" : failureSummary(report.cells);
726
834
  if (args.includes("--write")) {
@@ -1033,13 +1141,13 @@ async function exportCommand(args) {
1033
1141
  const json = args.includes("--json");
1034
1142
  const fail = (problems) => json ? { stdout: jsonEnvelope("export", false, null, problems), stderr: "", exitCode: 1 } : { stdout: "", stderr: problems.join("\n"), exitCode: 1 };
1035
1143
  const format = flagValue(args, "--format") ?? "html";
1036
- if (format !== "html" && format !== "tex" && format !== "json" && format !== "pdf") {
1037
- return fail([`Unknown format '${format}' (supported: html, tex, json, pdf)`]);
1144
+ if (format !== "html" && format !== "tex" && format !== "json" && format !== "pdf" && format !== "ipynb") {
1145
+ return fail([`Unknown format '${format}' (supported: html, tex, json, pdf, ipynb)`]);
1038
1146
  }
1039
1147
  const file = firstPositional(args);
1040
1148
  if (!file)
1041
1149
  return fail([
1042
- "Usage: mtsw export <file> [--format html|tex|json|pdf] [--fragment] [-o out] [--no-run]"
1150
+ "Usage: mtsw export <file> [--format html|tex|json|pdf|ipynb] [--fragment] [-o out] [--no-run]"
1043
1151
  ]);
1044
1152
  const read = readFile(file);
1045
1153
  if (read.error) return fail([read.error]);
@@ -1106,7 +1214,7 @@ async function exportCommand(args) {
1106
1214
  return json ? { stdout: jsonEnvelope("export", true, { path: outPath2 }, []), stderr: "", exitCode: 0 } : { stdout: "", stderr: `Exported ${file} -> ${outPath2}`, exitCode: 0 };
1107
1215
  }
1108
1216
  const fragment = args.includes("--fragment");
1109
- const rendered = format === "tex" ? toTeX(buildRenderDoc(workbook, byId, "tikz"), { parse: parse2, fragment }) : toHTML(buildRenderDoc(workbook, byId), { parse: parse2 });
1217
+ const rendered = format === "tex" ? toTeX(buildRenderDoc(workbook, byId, "tikz"), { parse: parse2, fragment }) : format === "ipynb" ? toIpynb(buildRenderDoc(workbook, byId)) : toHTML(buildRenderDoc(workbook, byId), { parse: parse2 });
1110
1218
  const bytes = Buffer.byteLength(rendered, "utf-8");
1111
1219
  const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
1112
1220
  if (outPath) {
package/dist/index.d.ts CHANGED
@@ -432,4 +432,53 @@ interface HandleResult {
432
432
  }
433
433
  declare function handleRequest(session: Session, request: JsonRpcRequest): Promise<HandleResult>;
434
434
 
435
- export { type Cell, type CellPosition, type CellResult, type CellType, type ExecutionMode, type ParseResult, type RemoveResult, type RunResult, type RuntimeConfig, SCHEMA_VERSION, Session, VERSION, type Workbook, type WorkbookEvent, WorkbookExecutor, type WorkbookMetadata, addCell, buildDependencyGraph, createExecutor, detectCellType, detectCycles, editCell, formatResult, getAncestors, getDependents, handleRequest, moveCell, parseWorkbook, removeCell, renameCell, serializeWorkbook, setMetadata, stripOutputs, toMermaid, topologicalSort };
435
+ /**
436
+ * Shared message/result shapes between `timeout-runner.ts` (the coordinator,
437
+ * running on the caller's thread) and `run-worker.ts` (the `worker_threads`
438
+ * entry point that actually executes the workbook). Kept in its own module so
439
+ * neither side needs to import the other's runtime code — the worker script
440
+ * stays a minimal, independently loadable entry point.
441
+ *
442
+ * Cell outputs are pre-formatted to strings (via `formatResult`) before they
443
+ * cross the worker boundary: `postMessage` uses the structured-clone
444
+ * algorithm, which cannot faithfully reproduce engine class instances
445
+ * (Complex, matrices, BigNumber, …) — it either drops their prototype or
446
+ * throws. Formatting to a string in the worker (the same rendering every
447
+ * other report consumer already uses) sidesteps that entirely.
448
+ */
449
+
450
+ /** A `CellResult` with its output pre-formatted to a string (or absent). */
451
+ interface SerializedCellResult {
452
+ id: string;
453
+ type: CellResult['type'];
454
+ status: CellResult['status'];
455
+ output?: string;
456
+ error?: string;
457
+ }
458
+ /** A `RunResult` whose cells are `SerializedCellResult`s. */
459
+ interface SerializedRunResult {
460
+ cells: SerializedCellResult[];
461
+ ok: boolean;
462
+ }
463
+
464
+ interface RunWorkbookWithTimeoutOptions {
465
+ /** Wall-clock budget in milliseconds. Exceeding it terminates the worker. */
466
+ timeoutMs: number;
467
+ }
468
+ /** Thrown when a workbook run is terminated for exceeding its time budget. */
469
+ declare class WorkbookTimeoutError extends Error {
470
+ readonly timeoutMs: number;
471
+ constructor(timeoutMs: number);
472
+ }
473
+ /**
474
+ * Run a workbook's `.mtsw` source to completion (a continue-on-error report,
475
+ * like `runReport()`) inside a worker thread, killing that worker if it
476
+ * exceeds `timeoutMs`. The worker is ALWAYS terminated before this promise
477
+ * settles — on timeout, on success, and on error — so no handle is left open
478
+ * keeping the process alive.
479
+ *
480
+ * @throws {WorkbookTimeoutError} if the run is terminated for exceeding the budget.
481
+ */
482
+ declare function runWorkbookWithTimeout(source: string, options: RunWorkbookWithTimeoutOptions): Promise<SerializedRunResult>;
483
+
484
+ export { type Cell, type CellPosition, type CellResult, type CellType, type ExecutionMode, type ParseResult, type RemoveResult, type RunResult, type RunWorkbookWithTimeoutOptions, type RuntimeConfig, SCHEMA_VERSION, type SerializedCellResult, type SerializedRunResult, Session, VERSION, type Workbook, type WorkbookEvent, WorkbookExecutor, type WorkbookMetadata, WorkbookTimeoutError, addCell, buildDependencyGraph, createExecutor, detectCellType, detectCycles, editCell, formatResult, getAncestors, getDependents, handleRequest, moveCell, parseWorkbook, removeCell, renameCell, runWorkbookWithTimeout, serializeWorkbook, setMetadata, stripOutputs, toMermaid, topologicalSort };
package/dist/index.js CHANGED
@@ -2,32 +2,37 @@ import {
2
2
  SCHEMA_VERSION,
3
3
  Session,
4
4
  VERSION,
5
- WorkbookExecutor,
5
+ WorkbookTimeoutError,
6
6
  addCell,
7
+ editCell,
8
+ handleRequest,
9
+ moveCell,
10
+ removeCell,
11
+ renameCell,
12
+ runWorkbookWithTimeout,
13
+ setMetadata
14
+ } from "./chunk-TMVFG3I3.js";
15
+ import {
16
+ WorkbookExecutor,
7
17
  buildDependencyGraph,
8
18
  createExecutor,
9
19
  detectCellType,
10
20
  detectCycles,
11
- editCell,
12
21
  formatResult,
13
22
  getAncestors,
14
23
  getDependents,
15
- handleRequest,
16
- moveCell,
17
24
  parseWorkbook,
18
- removeCell,
19
- renameCell,
20
25
  serializeWorkbook,
21
- setMetadata,
22
26
  stripOutputs,
23
27
  toMermaid,
24
28
  topologicalSort
25
- } from "./chunk-TS2WDJ7W.js";
29
+ } from "./chunk-3KTM2DZC.js";
26
30
  export {
27
31
  SCHEMA_VERSION,
28
32
  Session,
29
33
  VERSION,
30
34
  WorkbookExecutor,
35
+ WorkbookTimeoutError,
31
36
  addCell,
32
37
  buildDependencyGraph,
33
38
  createExecutor,
@@ -42,6 +47,7 @@ export {
42
47
  parseWorkbook,
43
48
  removeCell,
44
49
  renameCell,
50
+ runWorkbookWithTimeout,
45
51
  serializeWorkbook,
46
52
  setMetadata,
47
53
  stripOutputs,
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,39 @@
1
+ import {
2
+ createExecutor,
3
+ formatResult,
4
+ parseWorkbook
5
+ } from "./chunk-3KTM2DZC.js";
6
+
7
+ // src/run-worker.ts
8
+ import { parentPort, workerData } from "worker_threads";
9
+ async function main() {
10
+ const port = parentPort;
11
+ if (!port) {
12
+ throw new Error("run-worker.js must be run as a worker_threads Worker");
13
+ }
14
+ let message;
15
+ try {
16
+ const { source } = workerData;
17
+ const parsed = parseWorkbook(source);
18
+ if (!parsed.success || !parsed.workbook) {
19
+ message = {
20
+ ok: false,
21
+ error: `Parse error: ${(parsed.errors ?? []).join("; ") || "invalid workbook"}`
22
+ };
23
+ } else {
24
+ const report = await createExecutor(parsed.workbook).runReport();
25
+ const cells = report.cells.map((c) => ({
26
+ id: c.id,
27
+ type: c.type,
28
+ status: c.status,
29
+ output: c.output === void 0 ? void 0 : formatResult(c.output),
30
+ error: c.error
31
+ }));
32
+ message = { ok: true, report: { ok: report.ok, cells } };
33
+ }
34
+ } catch (error) {
35
+ message = { ok: false, error: error instanceof Error ? error.message : String(error) };
36
+ }
37
+ port.postMessage(message);
38
+ }
39
+ void main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/mathts-workbook",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scientific workbook runtime for MathTS (.mtsw format)",
5
5
  "author": "Daniel Simon Jr.",
6
6
  "license": "MIT",
@@ -22,8 +22,8 @@
22
22
  "README.md"
23
23
  ],
24
24
  "scripts": {
25
- "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
26
- "dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
25
+ "build": "tsup src/index.ts src/cli.ts src/run-worker.ts --format esm --dts --clean",
26
+ "dev": "tsup src/index.ts src/cli.ts src/run-worker.ts --format esm --dts --watch",
27
27
  "test": "vitest run",
28
28
  "test:watch": "vitest",
29
29
  "test:coverage": "vitest run --coverage",
@@ -31,7 +31,7 @@
31
31
  "lint": "eslint src --ext .ts",
32
32
  "lint:fix": "eslint src --ext .ts --fix",
33
33
  "clean": "rm -rf dist",
34
- "build:prod": "tsup src/index.ts src/cli.ts --format esm --dts --clean --minify --treeshake"
34
+ "build:prod": "tsup src/index.ts src/cli.ts src/run-worker.ts --format esm --dts --clean --minify --treeshake"
35
35
  },
36
36
  "dependencies": {
37
37
  "@danielsimonjr/mathts-core": "^0.10.0",