@mantaq/inspect 0.1.0 → 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.
package/README.md CHANGED
@@ -10,7 +10,7 @@ It finds every exported actor, or function that builds one, from the file's type
10
10
 
11
11
  ## Three ways in
12
12
 
13
- **CLI**: `mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--no-open]`. `--json` writes the viz document instead of the page. Seed: `MANTAQ_SEED` (default from `@mantaq/pbt`). Same seed → same output.
13
+ **CLI**: `mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--config FILE] [--mode MODE] [--no-open] [--check]`. `--config` loads the files with that Vite config (env, aliases, plugins) and `--mode` picks its mode; without `--config` no project config is read. `--check` exits 1 if a warning was printed or fuzzing found a handler error (printed as `error:`), so CI can gate on design errors. `--json` writes the viz document instead of the page. Seed: `MANTAQ_SEED` (default from `@mantaq/pbt`). Same seed → same output.
14
14
 
15
15
  **Vite plugin**: a live page on your dev server that redraws, keeping your walk, when you save.
16
16
 
@@ -47,6 +47,7 @@ const json = serializeViz(vizDocument([machine]));
47
47
 
48
48
  - **Exports.** An actor, or a function that returns one (or `{ actor }`). Functions are called with no arguments, with a `VirtualClock` when the first parameter is a clock, or with `{ clock }` when it is an options object with a `clock`. Functions with other required parameters are skipped. Discovery reads types only; nothing is called to find out.
49
49
  - **A fresh actor per call**, to fuzz. A file that only exports one actor instance is drawn, not fuzzed. When a file has factories, its actor instances are skipped.
50
+ - **Exported factories.** Export a factory that takes ports and a clock, not only a built instance. See [Machines in an application](https://anderscan.github.io/mantaq/guides/machines-in-an-app/).
50
51
  - **A `VirtualClock`**, to fuzz. An actor on the real clock is drawn, with a warning.
51
52
 
52
53
  Every part that cannot run is skipped with a warning, never guessed.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as vizDocument, r as inspectFile } from "./load-C4rGWmGU.mjs";
2
+ import { l as vizDocument, r as inspectFile } from "./load-DOwsxTyL.mjs";
3
3
  import { t as htmlPage } from "./page-rr9IQ-4k.mjs";
4
4
  import { serializeViz } from "@mantaq/viz";
5
5
  import { Either } from "@mantaq/utils";
@@ -12,15 +12,24 @@ import { parseArgs } from "node:util";
12
12
  /**
13
13
  * mantaq-inspect: draw the machines a file exports.
14
14
  *
15
- * mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--no-open]
15
+ * mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--config FILE]
16
+ * [--mode MODE] [--no-open] [--check]
16
17
  *
17
18
  * Finds every exported actor or actor factory from the file's types, reads
18
19
  * handler explanations and sample payloads from the source, fuzzes each
19
20
  * machine that runs on a VirtualClock, and writes one self-contained HTML page
20
21
  * (default `mantaq-viz.html`) that opens without a server. `--json` writes the
21
- * viz document instead. Seed: MANTAQ_SEED (default from @mantaq/pbt).
22
+ * viz document instead. `--config` loads the files with that Vite config (env,
23
+ * aliases, plugins) and `--mode` picks its mode. `--check` exits 1 if any warning was printed or
24
+ * fuzzing found a handler error (each printed as `error:`). Seed: MANTAQ_SEED (default from @mantaq/pbt).
22
25
  */
23
- const USAGE = "usage: mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--no-open]\n";
26
+ const USAGE = "usage: mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--config FILE] [--mode MODE] [--no-open] [--check]\n";
27
+ function loadOptions(config, mode) {
28
+ return {
29
+ ...config === void 0 ? {} : { config: resolve(config) },
30
+ ...mode === void 0 ? {} : { mode }
31
+ };
32
+ }
24
33
  function parse() {
25
34
  const parsed = Either.from(() => parseArgs({
26
35
  allowPositionals: true,
@@ -28,8 +37,11 @@ function parse() {
28
37
  out: { type: "string" },
29
38
  json: { type: "boolean" },
30
39
  iterations: { type: "string" },
40
+ config: { type: "string" },
41
+ mode: { type: "string" },
31
42
  "no-fuzz": { type: "boolean" },
32
- "no-open": { type: "boolean" }
43
+ "no-open": { type: "boolean" },
44
+ check: { type: "boolean" }
33
45
  }
34
46
  }));
35
47
  if (Either.isLeft(parsed)) return Either.left(String(parsed[0]));
@@ -49,6 +61,8 @@ function parse() {
49
61
  out: resolve(values.out ?? (json ? "mantaq-viz.json" : "mantaq-viz.html")),
50
62
  json,
51
63
  fuzz,
64
+ check: values.check === true,
65
+ load: loadOptions(values.config, values.mode),
52
66
  open: values["no-open"] !== true && !json && process.stdout.isTTY === true
53
67
  });
54
68
  }
@@ -59,6 +73,11 @@ function openInBrowser(path) {
59
73
  detached: true
60
74
  }).on("error", () => {}).unref();
61
75
  }
76
+ function checkFails(results, machines) {
77
+ const errors = machines.flatMap((machine) => (machine.fuzz?.errors ?? []).map((error) => `${machine.id}: ${error.from} -${error.event}-> ${error.message}`));
78
+ for (const error of errors) process.stderr.write(`error: ${error}\n`);
79
+ return errors.length > 0 || results.some((result) => result.warnings.length > 0);
80
+ }
62
81
  async function main() {
63
82
  const options = parse();
64
83
  if (Either.isLeft(options)) {
@@ -66,13 +85,16 @@ async function main() {
66
85
  process.exitCode = 1;
67
86
  return;
68
87
  }
69
- const { files, out, json, fuzz, open } = options[1];
88
+ const { files, out, json, fuzz, open, check, load } = options[1];
70
89
  const results = [];
71
- for (const file of files) results.push(await inspectFile(file, { fuzz }));
90
+ for (const file of files) results.push(await inspectFile(file, { fuzz }, load));
72
91
  for (const warning of results.flatMap((result) => result.warnings)) process.stderr.write(`warning: ${warning}\n`);
92
+ const failed = results.some((result) => result.failed);
93
+ if (failed) process.exitCode = 1;
73
94
  const machines = results.flatMap((result) => result.machines);
95
+ if (check && checkFails(results, machines)) process.exitCode = 1;
74
96
  if (machines.length === 0) {
75
- process.stderr.write("no machines found\n");
97
+ if (!failed) process.stderr.write("no machines found\n");
76
98
  process.exitCode = 1;
77
99
  return;
78
100
  }
@@ -0,0 +1,7 @@
1
+ import { t as FuzzTarget } from "./run-BLt0mwXe.mjs";
2
+ import { VizDriver } from "@mantaq/viz";
3
+ /**
4
+ * A running actor for the viewer. Steps go through the same function the
5
+ * fuzzer replays with, so live and fuzzed behaviour cannot drift. No node APIs.
6
+ */
7
+ export declare function createDriver(target: FuzzTarget): VizDriver;
package/dist/drive.mjs ADDED
@@ -0,0 +1,92 @@
1
+ import { a as messageOf, n as contextText, o as parsed, r as eventCatalog, t as applyStep } from "./run-B5huYj_r.mjs";
2
+ // src/drive.ts
3
+ function json(text) {
4
+ return parsed(text);
5
+ }
6
+ /**
7
+ * A running actor for the viewer. Steps go through the same function the
8
+ * fuzzer replays with, so live and fuzzed behaviour cannot drift. No node APIs.
9
+ */
10
+ function createDriver(target) {
11
+ let session = start(target);
12
+ function start(from) {
13
+ const { actor, clock } = from.create();
14
+ const internal = new Set(eventCatalog(actor).internal);
15
+ const seen = { last: null };
16
+ const steps = [];
17
+ actor.on("transition", { fn: ({ event, from: before, to, transitioned, effects }) => {
18
+ seen.last = {
19
+ from: before,
20
+ event: event.type,
21
+ to,
22
+ outcome: transitioned ? "transition" : "stay",
23
+ effects,
24
+ context: json(contextText(actor)),
25
+ error: null
26
+ };
27
+ } });
28
+ actor.on("error", { fn: (info) => {
29
+ const error = messageOf(info.error);
30
+ seen.last = {
31
+ from: info.state.name,
32
+ event: info.event.type,
33
+ to: actor.state.name,
34
+ outcome: "stay",
35
+ effects: [],
36
+ context: json(contextText(actor)),
37
+ error
38
+ };
39
+ } });
40
+ return {
41
+ actor,
42
+ clock,
43
+ internal,
44
+ seen,
45
+ steps
46
+ };
47
+ }
48
+ function view() {
49
+ const { actor, clock, steps, seen } = session;
50
+ return {
51
+ state: actor.state.name,
52
+ steps: [...steps],
53
+ last: seen.last,
54
+ timers: clock.pendingTimers().map((timer) => ({
55
+ id: String(timer.id),
56
+ ms: timer.ms,
57
+ deadline: timer.deadline,
58
+ eventName: timer.eventName ?? null
59
+ })),
60
+ context: json(contextText(actor))
61
+ };
62
+ }
63
+ return {
64
+ reset() {
65
+ session.actor.dispose();
66
+ session = start(target);
67
+ return view();
68
+ },
69
+ step(step) {
70
+ const { actor, clock, internal, seen, steps } = session;
71
+ seen.last = null;
72
+ const before = actor.state.name;
73
+ applyStep(actor, {
74
+ step,
75
+ clock,
76
+ internal
77
+ });
78
+ steps.push(step);
79
+ if (seen.last === null && step.kind === "event") seen.last = {
80
+ from: before,
81
+ event: step.event,
82
+ to: before,
83
+ outcome: "ignored",
84
+ effects: [],
85
+ context: json(contextText(actor)),
86
+ error: null
87
+ };
88
+ return view();
89
+ }
90
+ };
91
+ }
92
+ export { createDriver };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,9 @@
1
+ import { a as eventCatalog, i as Trace, n as Observation, o as execute, r as RunError, t as FuzzTarget } from "./run-BLt0mwXe.mjs";
1
2
  import { a as ModuleOptions, c as machineId, i as InspectedModule, l as vizDocument, n as InspectTarget, o as inspect, r as Inspected, s as inspectModule, t as FuzzSettings, u as AnalysisSource } from "./inspect-DvQtAcwv.mjs";
2
3
  import { VizComparison, VizDocument, VizExplanation, VizFuzzError, VizJson, VizMachine, VizStep } from "@mantaq/viz";
3
4
  import { History } from "@mantaq/traversal";
4
5
  import { Either } from "@mantaq/utils";
5
- import { AnyActor, VirtualClock } from "@mantaq/core";
6
+ import { AnyActor } from "@mantaq/core";
6
7
  import { Plugin } from "vite";
7
8
  interface VizInput {
8
9
  id: string;
@@ -23,63 +24,6 @@ interface LiteralPool {
23
24
  numbers: number[];
24
25
  strings: string[];
25
26
  }
26
- /** A machine the fuzzer can drive: a fresh actor plus the virtual clock it runs on. */
27
- interface FuzzTarget {
28
- id: string;
29
- create(): {
30
- actor: AnyActor<unknown>;
31
- clock: VirtualClock;
32
- };
33
- /**
34
- * Sample payloads per event type. Payload types are erased at runtime, so
35
- * the fuzzer only mutates shapes it was given. Events without samples are
36
- * sent without a payload.
37
- */
38
- payloads?: Readonly<Record<string, ReadonlyArray<VizJson>>>;
39
- }
40
- interface Observation {
41
- from: string;
42
- event: string;
43
- to: string;
44
- transitioned: boolean;
45
- /** Names of the effects that ran on entry into `to`. */
46
- effects: string[];
47
- /** Index of the step that caused it. */
48
- step: number;
49
- /** Payload the handler received (for branch-distance guidance). */
50
- payload: unknown;
51
- /** JSON copy of the actor context right after the handler ran; null if not JSON. */
52
- context: unknown;
53
- }
54
- interface RunError {
55
- from: string;
56
- event: string;
57
- message: string;
58
- /** Index of the step that triggered it. */
59
- step: number;
60
- }
61
- interface Trace {
62
- visited: string[];
63
- observations: Observation[];
64
- errors: RunError[];
65
- /** Indices of steps after which the actor context differed: loop candidates. */
66
- contextSteps: number[];
67
- /**
68
- * Coverage features, sorted: `state:x`, `edge:a>e>b` (state changed or
69
- * re-entered), `stay:a>e` (handled, no change), `error:a>e` (handler threw).
70
- */
71
- features: string[];
72
- }
73
- /** Declared inputs and internal events, each sorted. */
74
- export declare function eventCatalog(actor: AnyActor<unknown>): {
75
- inputs: string[];
76
- internal: string[];
77
- };
78
- /**
79
- * Replay steps against a fresh actor. Synchronous and deterministic: effects
80
- * only run via the virtual clock, and nothing here reads wall time.
81
- */
82
- export declare function execute(target: FuzzTarget, steps: ReadonlyArray<VizStep>): Trace;
83
27
  interface FuzzOptions {
84
28
  seed: number;
85
29
  iterations: number;
@@ -169,12 +113,16 @@ interface HandlerSite {
169
113
  state: string | null;
170
114
  event: string;
171
115
  explanation: VizExplanation;
116
+ /** Leading JSDoc of the registration. null = undocumented. */
117
+ doc: string | null;
172
118
  }
173
119
  /** Explain every handler registration in a file. Left = the file could not be analyzed at all. */
174
120
  export declare function analyzeSource(source: AnalysisSource): Either<string, HandlerSite[]>;
175
121
  /** Everything one type-check of a machine file yields. */
176
122
  interface ModuleAnalysis {
177
123
  sites: HandlerSite[];
124
+ stateDocs: ReadonlyMap<string, string>;
125
+ eventDocs: ReadonlyMap<string, string>;
178
126
  payloads: Record<string, VizJson[]>;
179
127
  machines: MachineExport[];
180
128
  }
@@ -188,8 +136,17 @@ export declare function analyzeModule(source: AnalysisSource): Either<string, Mo
188
136
  export declare function annotateMachine(machine: VizMachine, source: AnalysisSource): VizMachine;
189
137
  /** Options `inspectFile` passes through to `inspectModule`; loading is its job. */
190
138
  type FileOptions = Omit<ModuleOptions, "load">;
139
+ /** An inspected file; `failed` when it could not be loaded at all (its warnings say why). */
140
+ type LoadedFile = InspectedModule & {
141
+ failed: boolean;
142
+ };
143
+ /** Which Vite config and mode load the file; without a config, no project config is read. */
144
+ interface LoadOptions {
145
+ config?: string;
146
+ mode?: string;
147
+ }
191
148
  /** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
192
- export declare function inspectFile(file: string, options?: FileOptions): Promise<InspectedModule>;
149
+ export declare function inspectFile(file: string, options?: FileOptions, load?: LoadOptions): Promise<LoadedFile>;
193
150
  /** A self-contained HTML page for `document`: open it from disk, attach it, commit it. */
194
151
  export declare function htmlPage(document: VizDocument, title: string): Promise<string>;
195
- export { type AnalysisSource, type CorpusEntry, type FileOptions, type FuzzOptions, type FuzzResult, type FuzzSettings, type FuzzTarget, type HandlerSite, type InspectTarget, type Inspected, type InspectedModule, type LiteralPool, type MachineExport, type ModuleAnalysis, type ModuleOptions, type Observation, type RunError, type Trace, type VizInput, inspect, inspectModule, machineId, vizDocument };
152
+ export { type AnalysisSource, type CorpusEntry, type FileOptions, type FuzzOptions, type FuzzResult, type FuzzSettings, type FuzzTarget, type HandlerSite, type InspectTarget, type Inspected, type InspectedModule, type LiteralPool, type MachineExport, type ModuleAnalysis, type ModuleOptions, type Observation, type RunError, type Trace, type VizInput, eventCatalog, execute, inspect, inspectModule, machineId, vizDocument };
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
- import { C as toVizMachine, S as toViz, _ as withTestCoverage, a as inspect, b as eventCatalog, c as vizDocument, d as annotateMachine, f as branchDistance, g as fuzzedMachine, h as literalPool, l as analyzeModule, m as guidance, o as inspectModule, p as branchDistanceGuide, r as inspectFile, s as machineId, u as analyzeSource, v as fuzz, x as execute, y as shrink } from "./load-C4rGWmGU.mjs";
1
+ import { S as toVizMachine, _ as fuzzedMachine, a as inspect, b as shrink, d as analyzeSource, f as annotateMachine, g as literalPool, h as guidance, l as vizDocument, m as branchDistanceGuide, o as inspectModule, p as branchDistance, r as inspectFile, s as machineId, u as analyzeModule, v as withTestCoverage, x as toViz, y as fuzz } from "./load-DOwsxTyL.mjs";
2
+ import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
2
3
  import { t as htmlPage } from "./page-rr9IQ-4k.mjs";
3
4
  export { analyzeModule, analyzeSource, annotateMachine, branchDistance, branchDistanceGuide, eventCatalog, execute, fuzz, fuzzedMachine, guidance, htmlPage, inspect, inspectFile, inspectModule, literalPool, machineId, shrink, toViz, toVizMachine, vizDocument, withTestCoverage };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,106 @@
1
+ import { createDriver } from "./drive.mjs";
2
+ import { UPDATE_EVENT, parseViz } from "@mantaq/viz";
3
+ import { Either } from "@mantaq/utils";
4
+ import { targets } from "virtual:mantaq/targets";
5
+ import { mountViewer, showIssues } from "@mantaq/viz/viewer";
6
+ import { linkOptions } from "@mantaq/viz/page";
7
+ // src/live-page.ts
8
+ /**
9
+ * The live page of the Vite plugin: the served document, with each machine
10
+ * that has a target running for real behind the viewer. The walk lives in the
11
+ * address bar (`?machine=&step=`), so a redraw after a save replays it on the
12
+ * new code.
13
+ */
14
+ /** A document holding only `machine`, so the viewer shows the machine its driver runs. */
15
+ function only(viz, machine) {
16
+ const chosen = viz.machines.find((each) => each.id === machine) ?? viz.machines[0];
17
+ return {
18
+ ...viz,
19
+ machines: chosen === void 0 ? [] : [chosen]
20
+ };
21
+ }
22
+ /** Mount the document at `text`; machines with a target run live, the rest stay static walks. */
23
+ function mountDriven(root, context) {
24
+ const parsed = parseViz(context.text);
25
+ if (Either.isLeft(parsed)) {
26
+ showIssues(root, {
27
+ source: context.source,
28
+ issues: parsed[0]
29
+ });
30
+ return;
31
+ }
32
+ const options = linkOptions();
33
+ const viz = parsed[1];
34
+ const shown = only(viz, options.machine ?? null);
35
+ const id = shown.machines[0]?.id ?? "";
36
+ const target = context.targets[id];
37
+ const create = target !== void 0 && typeof Reflect.get(target().clock, "advance") === "function" ? target : void 0;
38
+ const picker = viz.machines.length > 1;
39
+ const select = (machine) => {
40
+ const next = new URLSearchParams(location.search);
41
+ next.set("machine", machine);
42
+ next.delete("step");
43
+ history.replaceState(null, "", `?${next.toString()}`);
44
+ mountDriven(root, context);
45
+ };
46
+ const doc = root.ownerDocument;
47
+ const bar = doc.createElement("nav");
48
+ bar.className = "machines";
49
+ if (picker) for (const machine of viz.machines) {
50
+ const button = doc.createElement("button");
51
+ button.type = "button";
52
+ button.textContent = machine.id;
53
+ button.disabled = machine.id === id;
54
+ button.addEventListener("click", () => select(machine.id));
55
+ bar.append(button);
56
+ }
57
+ const viewer = doc.createElement("div");
58
+ root.replaceChildren(...picker ? [bar] : [], viewer);
59
+ mountViewer(viewer, shown, {
60
+ ...options,
61
+ ...create === void 0 ? {} : { driver: createDriver({
62
+ id,
63
+ create
64
+ }) }
65
+ });
66
+ }
67
+ /** Fetch the document from `source` and mount it; call again to redraw, keeping the walk. */
68
+ async function mountLiveDriven(root, context) {
69
+ const response = await fetch(context.source).catch(() => null);
70
+ if (response === null || !response.ok) {
71
+ const status = response === null ? "network error" : `HTTP ${response.status}`;
72
+ showIssues(root, {
73
+ source: context.source,
74
+ issues: [{
75
+ path: "$",
76
+ message: status
77
+ }]
78
+ });
79
+ return;
80
+ }
81
+ mountDriven(root, {
82
+ ...context,
83
+ text: await response.text()
84
+ });
85
+ }
86
+ // src/live-entry.ts
87
+ /**
88
+ * Entry of the page `@mantaq/inspect/vite` serves. Redraws when the dev
89
+ * server reports a change, and when the machine code itself hot-updates.
90
+ */
91
+ const root = document.querySelector("#app");
92
+ const source = root?.dataset["src"];
93
+ const current = { targets };
94
+ if (root && source !== void 0) {
95
+ const draw = () => void mountLiveDriven(root, {
96
+ source,
97
+ targets: current.targets
98
+ });
99
+ draw();
100
+ import.meta.hot?.on(UPDATE_EVENT, draw);
101
+ import.meta.hot?.accept("virtual:mantaq/targets", (module) => {
102
+ if (module !== void 0) current.targets = module["targets"];
103
+ draw();
104
+ });
105
+ }
106
+ export {};
@@ -1,13 +1,14 @@
1
+ import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
1
2
  import { ANY_STATE, VIZ_SCHEMA_VERSION, coveredByAny, isAnyState, routingWarnings, unhandledSends } from "@mantaq/viz";
2
3
  import { INITIAL_NODE_ID, buildGraph, parseInternalEventIds } from "@mantaq/traversal";
3
4
  import { Either } from "@mantaq/utils";
4
5
  import ts from "typescript";
5
- import { dirname, join, relative, sep } from "node:path";
6
+ import { dirname, join, relative, resolve, sep } from "node:path";
6
7
  import { DEFAULT_SEED } from "@mantaq/pbt";
7
8
  import { VirtualClock } from "@mantaq/core";
8
9
  import { existsSync } from "node:fs";
9
10
  import { fileURLToPath } from "node:url";
10
- import { runnerImport } from "vite";
11
+ import { loadConfigFromFile, loadEnv, runnerImport } from "vite";
11
12
  // src/structure.ts
12
13
  function byId$1(left, right) {
13
14
  if (left.id < right.id) return -1;
@@ -35,7 +36,8 @@ function toState(node, context) {
35
36
  final: node.isFinal,
36
37
  effects: context.effects.find((edge) => edge.source === node.id)?.effects ?? [],
37
38
  coverage: visits ? hits(visits.get(node.id)) : null,
38
- tested: null
39
+ tested: null,
40
+ doc: null
39
41
  };
40
42
  }
41
43
  function edgeKind(edge) {
@@ -91,6 +93,7 @@ function toEdge(edge, transitions) {
91
93
  emits: emitsOf(edge),
92
94
  contexts: [...new Set(edge.contexts)].sort(),
93
95
  explanation: null,
96
+ doc: null,
94
97
  coverage: transitions && kind === "transition" ? hits(edgeHits$1(edge, transitions)) : null,
95
98
  tested: null
96
99
  };
@@ -112,6 +115,7 @@ function toVizMachine(input) {
112
115
  const events = [...new Set(edges.map((edge) => edge.event).filter(Boolean))].filter((eventId) => !eventId.startsWith("effect:")).sort().map((eventId) => ({
113
116
  id: eventId,
114
117
  internal: internalIds.has(eventId),
118
+ doc: null,
115
119
  payloadType: null
116
120
  }));
117
121
  const regions = [...new Set(states.flatMap((state) => state.region ? [state.region] : []))].sort();
@@ -330,120 +334,6 @@ function createRng(seed) {
330
334
  chance: (probability) => next() < probability
331
335
  };
332
336
  }
333
- // src/fuzz/run.ts
334
- const ERROR_STATE = "__error";
335
- function eventTypes(options, key) {
336
- if (typeof options !== "object" || options === null) return [];
337
- const list = Reflect.get(options, key);
338
- if (!Array.isArray(list)) return [];
339
- return list.flatMap((entry) => {
340
- if (typeof entry !== "object" || entry === null) return [];
341
- const type = Reflect.get(entry, "type");
342
- return typeof type === "string" ? [type] : [];
343
- });
344
- }
345
- /** Declared inputs and internal events, each sorted. */
346
- function eventCatalog(actor) {
347
- return {
348
- inputs: eventTypes(actor.options, "inputs").sort(),
349
- internal: eventTypes(actor.options, "internal").sort()
350
- };
351
- }
352
- /** onAny handlers live under this key in the dispatch map (core builder). */
353
- const ANY_KEY = "Any";
354
- function handles(actor, event) {
355
- const transitions = actor.options?.transitions;
356
- return transitions?.[actor.state.name]?.[event] !== void 0 || transitions?.[ANY_KEY]?.[event] !== void 0;
357
- }
358
- function toEvent(step) {
359
- return step.payload === null ? { type: step.event } : {
360
- type: step.event,
361
- payload: step.payload
362
- };
363
- }
364
- function messageOf(error) {
365
- return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
366
- }
367
- function featuresOf(trace) {
368
- const features = /* @__PURE__ */ new Set();
369
- for (const state of trace.visited) features.add(`state:${state}`);
370
- for (const seen of trace.observations) features.add(seen.transitioned ? `edge:${seen.from}>${seen.event}>${seen.to}` : `stay:${seen.from}>${seen.event}`);
371
- for (const error of trace.errors) features.add(`error:${error.from}>${error.event}`);
372
- return [...features].sort();
373
- }
374
- /** JSON text of the context, or null when it is not serializable (functions, cycles). */
375
- function contextText(actor) {
376
- const text = Either.from(() => JSON.stringify(actor.context ?? null));
377
- return Either.isRight(text) ? text[1] : null;
378
- }
379
- function parsed(text) {
380
- return text === null ? null : JSON.parse(text);
381
- }
382
- function dispatch(actor, context) {
383
- const { step } = context;
384
- if (step.kind === "advance") context.clock.advance(step.ms);
385
- else if (context.internal.has(step.event)) {
386
- if (handles(actor, step.event)) actor.inject(toEvent(step));
387
- } else actor.send(toEvent(step));
388
- }
389
- /**
390
- * Replay steps against a fresh actor. Synchronous and deterministic: effects
391
- * only run via the virtual clock, and nothing here reads wall time.
392
- */
393
- function execute(target, steps) {
394
- const { actor, clock } = target.create();
395
- const internal = new Set(eventCatalog(actor).internal);
396
- const visited = [actor.state.name];
397
- const observations = [];
398
- const errors = [];
399
- const contextSteps = [];
400
- let index = 0;
401
- actor.on("transition", { fn: ({ event, from, to, transitioned, effects }) => {
402
- const context = parsed(contextText(actor));
403
- observations.push({
404
- from,
405
- event: event.type,
406
- to,
407
- transitioned,
408
- effects,
409
- step: index,
410
- payload: event.payload,
411
- context
412
- });
413
- if (transitioned) visited.push(to);
414
- } });
415
- actor.on("error", { fn: (info) => {
416
- errors.push({
417
- from: info.state.name,
418
- event: info.event.type,
419
- message: messageOf(info.error),
420
- step: index
421
- });
422
- } });
423
- for (const step of steps) {
424
- if (actor.state.name === ERROR_STATE) break;
425
- const before = contextText(actor);
426
- dispatch(actor, {
427
- step,
428
- clock,
429
- internal
430
- });
431
- if (contextText(actor) !== before) contextSteps.push(index);
432
- index += 1;
433
- }
434
- actor.dispose();
435
- return {
436
- visited,
437
- observations,
438
- errors,
439
- contextSteps,
440
- features: featuresOf({
441
- visited,
442
- observations,
443
- errors
444
- })
445
- };
446
- }
447
337
  // src/fuzz/fuzz.ts
448
338
  function covers(target, steps, wanted) {
449
339
  const reached = new Set(execute(target, steps).features);
@@ -605,6 +495,7 @@ function fuzzEdge(seen, from) {
605
495
  emits: [],
606
496
  contexts: [],
607
497
  explanation: null,
498
+ doc: null,
608
499
  coverage: null,
609
500
  tested: null
610
501
  };
@@ -650,6 +541,7 @@ function fuzzedMachine(target, result) {
650
541
  const events = [...base.events, ...[...new Set(added.map((edge) => edge.event))].filter((id) => !known.has(id)).map((id) => ({
651
542
  id,
652
543
  internal: internal.has(id),
544
+ doc: null,
653
545
  payloadType: null
654
546
  }))].sort(byId);
655
547
  return {
@@ -874,6 +766,33 @@ function machineExports(loaded) {
874
766
  }).sort((left, right) => left.at - right.at).map(({ found }) => found);
875
767
  }
876
768
  // src/ast/program.ts
769
+ /**
770
+ * Prose of the JSDoc block found in the trivia starting at `pos`, or null when
771
+ * there is none. Block tags (`@param`, ...) and everything after them are cut;
772
+ * `{@link x}` reads as `x`.
773
+ */
774
+ function docAt(sourceFile, pos) {
775
+ const block = (ts.getLeadingCommentRanges(sourceFile.text, pos) ?? []).filter((range) => sourceFile.text.startsWith("/**", range.pos)).at(-1);
776
+ if (block === void 0) return null;
777
+ const lines = sourceFile.text.slice(block.pos + 3, block.end - 2).split(/\r?\n/).map((line) => line.replace(/^\s*\* ?/, "").trim());
778
+ const tag = lines.findIndex((line) => line.startsWith("@"));
779
+ const text = (tag === -1 ? lines : lines.slice(0, tag)).join("\n").replace(/\{@link(?:code|plain)?\s+([^}|\s]+)[^}]*\}/g, "$1").trim();
780
+ return text === "" ? null : text;
781
+ }
782
+ function docOf(node) {
783
+ return docAt(node.getSourceFile(), node.getFullStart());
784
+ }
785
+ /**
786
+ * Doc of one `m.on(...)` link. The first link of a chain owns the comment above
787
+ * the statement; later links own the comment just before their `.on`.
788
+ */
789
+ function callDoc(call) {
790
+ const callee = call.expression;
791
+ if (ts.isPropertyAccessExpression(callee) && ts.isCallExpression(callee.expression)) return docAt(call.getSourceFile(), callee.expression.end);
792
+ let top = call;
793
+ while (ts.isPropertyAccessExpression(top.parent) || ts.isCallExpression(top.parent)) top = top.parent;
794
+ return docOf(ts.isExpressionStatement(top.parent) ? top.parent : call);
795
+ }
877
796
  function compilerOptions(source) {
878
797
  const configPath = source.tsconfig ?? ts.findConfigFile(dirname(source.file), (path) => ts.sys.fileExists(path));
879
798
  if (configPath === void 0) return {
@@ -914,7 +833,8 @@ function toRegistration(checker, call) {
914
833
  event: literalProperty(checker, eventRef, "type"),
915
834
  eventRef,
916
835
  handler: property(options, "handler"),
917
- call
836
+ call,
837
+ doc: callDoc(call)
918
838
  };
919
839
  }
920
840
  function locationOf(node, root) {
@@ -926,6 +846,27 @@ function locationOf(node, root) {
926
846
  column: position.character + 1
927
847
  };
928
848
  }
849
+ /** Name literal of `state("x")()` / `event("X")()`, with which builder it came from. */
850
+ function declaredName(init) {
851
+ if (!ts.isCallExpression(init) || !ts.isCallExpression(init.expression)) return null;
852
+ const builder = init.expression.expression;
853
+ const [name] = init.expression.arguments;
854
+ if (!ts.isIdentifier(builder) || name === void 0 || !ts.isStringLiteralLike(name)) return null;
855
+ return {
856
+ builder: builder.text,
857
+ name: name.text
858
+ };
859
+ }
860
+ function collectDeclarationDocs(statement, docs) {
861
+ const doc = docOf(statement);
862
+ if (doc === null) return;
863
+ for (const declaration of statement.declarationList.declarations) {
864
+ const declared = declaration.initializer && declaredName(declaration.initializer);
865
+ if (!declared) continue;
866
+ if (declared.builder === "state") docs.stateDocs.set(declared.name, doc);
867
+ if (declared.builder === "event") docs.eventDocs.set(declared.name, doc);
868
+ }
869
+ }
929
870
  /**
930
871
  * Type-check one file and collect its handler registrations. Any compiler
931
872
  * failure becomes a Left reason; it never throws.
@@ -937,9 +878,15 @@ function loadSource(source) {
937
878
  if (sourceFile === void 0) return `source not found: ${source.file}`;
938
879
  const checker = program.getTypeChecker();
939
880
  const registrations = [];
881
+ const stateDocs = /* @__PURE__ */ new Map();
882
+ const eventDocs = /* @__PURE__ */ new Map();
940
883
  const visit = (node) => {
941
884
  const found = ts.isCallExpression(node) ? toRegistration(checker, node) : null;
942
885
  if (found) registrations.push(found);
886
+ if (ts.isVariableStatement(node)) collectDeclarationDocs(node, {
887
+ stateDocs,
888
+ eventDocs
889
+ });
943
890
  ts.forEachChild(node, visit);
944
891
  };
945
892
  visit(sourceFile);
@@ -947,7 +894,9 @@ function loadSource(source) {
947
894
  checker,
948
895
  sourceFile,
949
896
  root: source.root,
950
- registrations
897
+ registrations,
898
+ stateDocs,
899
+ eventDocs
951
900
  };
952
901
  });
953
902
  if (Either.isLeft(loaded)) return Either.left(`compiler failed: ${String(loaded[0])}`);
@@ -1477,6 +1426,7 @@ function sitesOf(loaded) {
1477
1426
  return [{
1478
1427
  state: registration.state,
1479
1428
  event: registration.event,
1429
+ doc: registration.doc,
1480
1430
  explanation: Either.getOrElse(explanation, { onLeft: (error) => ({
1481
1431
  status: "not_analyzed",
1482
1432
  location: null,
@@ -1490,10 +1440,17 @@ function sitesOf(loaded) {
1490
1440
  function analyzeSource(source) {
1491
1441
  return Either.chain(loadSource(source), { onRight: (loaded) => Either.right(sitesOf(loaded)) });
1492
1442
  }
1443
+ function docsOf(loaded) {
1444
+ return {
1445
+ sites: sitesOf(loaded),
1446
+ stateDocs: loaded.stateDocs,
1447
+ eventDocs: loaded.eventDocs
1448
+ };
1449
+ }
1493
1450
  /** Handlers, payload samples and machine exports of a file, from one TypeScript program. */
1494
1451
  function analyzeModule(source) {
1495
1452
  return Either.chain(loadSource(source), { onRight: (loaded) => Either.right({
1496
- sites: sitesOf(loaded),
1453
+ ...docsOf(loaded),
1497
1454
  payloads: payloadSamples(loaded),
1498
1455
  machines: machineExports(loaded)
1499
1456
  }) });
@@ -1503,15 +1460,24 @@ function localState(id) {
1503
1460
  return id.slice(id.lastIndexOf(".") + 1);
1504
1461
  }
1505
1462
  function explain(edge, context) {
1506
- if (edge.kind === "initial" || edge.kind === "effect") return null;
1463
+ if (edge.kind === "initial" || edge.kind === "effect") return {
1464
+ explanation: null,
1465
+ doc: null
1466
+ };
1507
1467
  const state = localState(edge.from);
1508
1468
  const site = context.sites.find((candidate) => candidate.state === state && candidate.event === edge.event) ?? context.sites.find((candidate) => candidate.state === null && candidate.event === edge.event);
1509
- if (site) return site.explanation;
1469
+ if (site) return {
1470
+ explanation: site.explanation,
1471
+ doc: site.doc
1472
+ };
1510
1473
  return {
1511
- status: "not_analyzed",
1512
- location: null,
1513
- reason: `no m.on/m.onAny registration for (${state}, ${edge.event}) found in ${context.file}`,
1514
- snippet: null
1474
+ explanation: {
1475
+ status: "not_analyzed",
1476
+ location: null,
1477
+ reason: `no m.on/m.onAny registration for (${state}, ${edge.event}) found in ${context.file}`,
1478
+ snippet: null
1479
+ },
1480
+ doc: null
1515
1481
  };
1516
1482
  }
1517
1483
  /**
@@ -1521,7 +1487,7 @@ function explain(edge, context) {
1521
1487
  */
1522
1488
  function annotateMachine(machine, source) {
1523
1489
  return annotateWith(machine, {
1524
- analysis: analyzeSource(source),
1490
+ analysis: Either.chain(loadSource(source), { onRight: (loaded) => Either.right(docsOf(loaded)) }),
1525
1491
  source
1526
1492
  });
1527
1493
  }
@@ -1539,19 +1505,33 @@ function annotateWith(machine, context) {
1539
1505
  } : null;
1540
1506
  return {
1541
1507
  ...edge,
1542
- explanation
1508
+ explanation,
1509
+ doc: null
1543
1510
  };
1544
1511
  }
1545
1512
  return {
1546
1513
  ...edge,
1547
- explanation: explain(edge, {
1548
- sites: analysis[1],
1514
+ ...explain(edge, {
1515
+ sites: analysis[1].sites,
1549
1516
  file
1550
1517
  })
1551
1518
  };
1552
1519
  });
1520
+ if (Either.isLeft(analysis)) return {
1521
+ ...machine,
1522
+ edges
1523
+ };
1524
+ const { stateDocs, eventDocs } = analysis[1];
1553
1525
  return {
1554
1526
  ...machine,
1527
+ states: machine.states.map((state) => ({
1528
+ ...state,
1529
+ doc: stateDocs.get(localState(state.id)) ?? null
1530
+ })),
1531
+ events: machine.events.map((event) => ({
1532
+ ...event,
1533
+ doc: eventDocs.get(event.id) ?? null
1534
+ })),
1555
1535
  edges
1556
1536
  };
1557
1537
  }
@@ -1583,9 +1563,8 @@ function runner(target, payloads) {
1583
1563
  function annotator(target, analysis) {
1584
1564
  const { source } = target;
1585
1565
  if (analysis === null || source === void 0) return (machine) => machine;
1586
- const sites = Either.map(analysis, { onRight: (found) => found.sites });
1587
1566
  return (machine) => annotateWith(machine, {
1588
- analysis: sites,
1567
+ analysis,
1589
1568
  source
1590
1569
  });
1591
1570
  }
@@ -1734,6 +1713,7 @@ function vizDocument(machines) {
1734
1713
  };
1735
1714
  }
1736
1715
  // src/load.ts
1716
+ const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
1737
1717
  const ENTRY = "virtual:mantaq-inspect";
1738
1718
  const RESOLVED = `\0${ENTRY}`;
1739
1719
  /**
@@ -1773,18 +1753,88 @@ function runOf(module) {
1773
1753
  if (typeof run !== "function") return null;
1774
1754
  return async (options) => Reflect.apply(run, void 0, [options]);
1775
1755
  }
1776
- /** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
1777
- async function inspectFile(file, options = {}) {
1778
- const { module } = await runnerImport(entryId(file), {
1779
- configFile: false,
1780
- logLevel: "silent",
1781
- plugins: [inspectEntry()]
1756
+ const ENV_ACCESS = [
1757
+ "import",
1758
+ "meta",
1759
+ "env"
1760
+ ].join(".");
1761
+ /**
1762
+ * Replaces the env object with the project's env: `runnerImport` never reads
1763
+ * env files, so `VITE_*` variables would otherwise be missing. A textual
1764
+ * replace: it also rewrites matches in strings and comments, and a frozen
1765
+ * snapshot replaces Vite's own `import.meta.env`, so assigning to it fails.
1766
+ */
1767
+ function projectEnv(env, mode) {
1768
+ const value = JSON.stringify({
1769
+ ...env,
1770
+ BASE_URL: "/",
1771
+ MODE: mode,
1772
+ DEV: true,
1773
+ PROD: false,
1774
+ SSR: true
1782
1775
  });
1776
+ return {
1777
+ name: "mantaq-inspect-env",
1778
+ enforce: "pre",
1779
+ transform: (code, id) => id.includes("/node_modules/") || !code.includes(ENV_ACCESS) ? null : {
1780
+ code: code.replaceAll(ENV_ACCESS, `(${value})`),
1781
+ map: null
1782
+ }
1783
+ };
1784
+ }
1785
+ /**
1786
+ * The inline config for a load: empty without `--config`; otherwise the
1787
+ * project's Vite config (plugins, aliases, define) plus its env files.
1788
+ */
1789
+ async function inlineConfig({ config, mode }) {
1790
+ const base = {
1791
+ configFile: false,
1792
+ logLevel: "silent"
1793
+ };
1794
+ if (config === void 0) return {
1795
+ ...base,
1796
+ ...mode === void 0 ? {} : { mode }
1797
+ };
1798
+ const chosen = mode ?? "development";
1799
+ const user = (await loadConfigFromFile({
1800
+ command: "serve",
1801
+ mode: chosen
1802
+ }, config))?.config ?? {};
1803
+ const envDir = resolve(dirname(config), user.envDir === false ? "." : user.envDir ?? ".");
1804
+ const env = user.envDir === false ? {} : loadEnv(chosen, envDir, user.envPrefix ?? "VITE_");
1805
+ return {
1806
+ ...user,
1807
+ ...base,
1808
+ mode: chosen,
1809
+ plugins: [...user.plugins ?? [], projectEnv(env, chosen)]
1810
+ };
1811
+ }
1812
+ /** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
1813
+ async function inspectFile(file, options = {}, load = {}) {
1814
+ if (load.config !== void 0 && !existsSync(load.config)) return {
1815
+ machines: [],
1816
+ warnings: [`${file}: Vite config not found: ${load.config}`],
1817
+ failed: true
1818
+ };
1819
+ const loaded = await inlineConfig(load).then((inline) => runnerImport(entryId(file), {
1820
+ ...inline,
1821
+ plugins: [...inline.plugins ?? [], inspectEntry()]
1822
+ })).then((value) => Either.right(value), (error) => Either.left((error instanceof Error ? error.message : String(error)).replace(ANSI, "")));
1823
+ if (Either.isLeft(loaded)) return {
1824
+ machines: [],
1825
+ warnings: [`${file}: ${loaded[0]}`],
1826
+ failed: true
1827
+ };
1828
+ const { module } = loaded[1];
1783
1829
  const run = runOf(module);
1784
1830
  if (run === null) return {
1785
1831
  machines: [],
1786
- warnings: [`${file}: could not load the inspector`]
1832
+ warnings: [`${file}: could not load the inspector`],
1833
+ failed: true
1834
+ };
1835
+ return {
1836
+ ...await run(options),
1837
+ failed: false
1787
1838
  };
1788
- return run(options);
1789
1839
  }
1790
- export { toVizMachine as C, toViz as S, withTestCoverage as _, inspect as a, eventCatalog as b, vizDocument as c, annotateMachine as d, branchDistance as f, fuzzedMachine as g, literalPool as h, runOf as i, analyzeModule as l, guidance as m, inspectEntry as n, inspectModule as o, branchDistanceGuide as p, inspectFile as r, machineId as s, entryId as t, analyzeSource as u, fuzz as v, execute as x, shrink as y };
1840
+ export { toVizMachine as S, fuzzedMachine as _, inspect as a, shrink as b, projectRoot as c, analyzeSource as d, annotateMachine as f, literalPool as g, guidance as h, runOf as i, vizDocument as l, branchDistanceGuide as m, inspectEntry as n, inspectModule as o, branchDistance as p, inspectFile as r, machineId as s, entryId as t, analyzeModule as u, withTestCoverage as v, toViz as x, fuzz as y };
@@ -0,0 +1,117 @@
1
+ import { Either } from "@mantaq/utils";
2
+ // src/fuzz/run.ts
3
+ const ERROR_STATE = "__error";
4
+ function eventTypes(options, key) {
5
+ if (typeof options !== "object" || options === null) return [];
6
+ const list = Reflect.get(options, key);
7
+ if (!Array.isArray(list)) return [];
8
+ return list.flatMap((entry) => {
9
+ if (typeof entry !== "object" || entry === null) return [];
10
+ const type = Reflect.get(entry, "type");
11
+ return typeof type === "string" ? [type] : [];
12
+ });
13
+ }
14
+ /** Declared inputs and internal events, each sorted. */
15
+ function eventCatalog(actor) {
16
+ return {
17
+ inputs: eventTypes(actor.options, "inputs").sort(),
18
+ internal: eventTypes(actor.options, "internal").sort()
19
+ };
20
+ }
21
+ /** onAny handlers live under this key in the dispatch map (core builder). */
22
+ const ANY_KEY = "Any";
23
+ function handles(actor, event) {
24
+ const transitions = actor.options?.transitions;
25
+ return transitions?.[actor.state.name]?.[event] !== void 0 || transitions?.[ANY_KEY]?.[event] !== void 0;
26
+ }
27
+ function toEvent(step) {
28
+ return step.payload === null ? { type: step.event } : {
29
+ type: step.event,
30
+ payload: step.payload
31
+ };
32
+ }
33
+ function messageOf(error) {
34
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
35
+ }
36
+ function featuresOf(trace) {
37
+ const features = /* @__PURE__ */ new Set();
38
+ for (const state of trace.visited) features.add(`state:${state}`);
39
+ for (const seen of trace.observations) features.add(seen.transitioned ? `edge:${seen.from}>${seen.event}>${seen.to}` : `stay:${seen.from}>${seen.event}`);
40
+ for (const error of trace.errors) features.add(`error:${error.from}>${error.event}`);
41
+ return [...features].sort();
42
+ }
43
+ /** JSON text of the context, or null when it is not serializable (functions, cycles). */
44
+ function contextText(actor) {
45
+ const text = Either.from(() => JSON.stringify(actor.context ?? null));
46
+ return Either.isRight(text) ? text[1] : null;
47
+ }
48
+ function parsed(text) {
49
+ return text === null ? null : JSON.parse(text);
50
+ }
51
+ /** The one step function: the fuzzer and the live driver both apply steps through it. */
52
+ function applyStep(actor, context) {
53
+ const { step } = context;
54
+ if (step.kind === "advance") context.clock.advance(step.ms);
55
+ else if (context.internal.has(step.event)) {
56
+ if (handles(actor, step.event)) actor.inject(toEvent(step));
57
+ } else actor.send(toEvent(step));
58
+ }
59
+ /**
60
+ * Replay steps against a fresh actor. Synchronous and deterministic: effects
61
+ * only run via the virtual clock, and nothing here reads wall time.
62
+ */
63
+ function execute(target, steps) {
64
+ const { actor, clock } = target.create();
65
+ const internal = new Set(eventCatalog(actor).internal);
66
+ const visited = [actor.state.name];
67
+ const observations = [];
68
+ const errors = [];
69
+ const contextSteps = [];
70
+ let index = 0;
71
+ actor.on("transition", { fn: ({ event, from, to, transitioned, effects }) => {
72
+ const context = parsed(contextText(actor));
73
+ observations.push({
74
+ from,
75
+ event: event.type,
76
+ to,
77
+ transitioned,
78
+ effects,
79
+ step: index,
80
+ payload: event.payload,
81
+ context
82
+ });
83
+ if (transitioned) visited.push(to);
84
+ } });
85
+ actor.on("error", { fn: (info) => {
86
+ errors.push({
87
+ from: info.state.name,
88
+ event: info.event.type,
89
+ message: messageOf(info.error),
90
+ step: index
91
+ });
92
+ } });
93
+ for (const step of steps) {
94
+ if (actor.state.name === ERROR_STATE) break;
95
+ const before = contextText(actor);
96
+ applyStep(actor, {
97
+ step,
98
+ clock,
99
+ internal
100
+ });
101
+ if (contextText(actor) !== before) contextSteps.push(index);
102
+ index += 1;
103
+ }
104
+ actor.dispose();
105
+ return {
106
+ visited,
107
+ observations,
108
+ errors,
109
+ contextSteps,
110
+ features: featuresOf({
111
+ visited,
112
+ observations,
113
+ errors
114
+ })
115
+ };
116
+ }
117
+ export { messageOf as a, execute as i, contextText as n, parsed as o, eventCatalog as r, applyStep as t };
@@ -0,0 +1,60 @@
1
+ import { VizJson, VizStep } from "@mantaq/viz";
2
+ import { AnyActor, VirtualClock } from "@mantaq/core";
3
+ /** A machine the fuzzer can drive: a fresh actor plus the virtual clock it runs on. */
4
+ interface FuzzTarget {
5
+ id: string;
6
+ create(): {
7
+ actor: AnyActor<unknown>;
8
+ clock: VirtualClock;
9
+ };
10
+ /**
11
+ * Sample payloads per event type. Payload types are erased at runtime, so
12
+ * the fuzzer only mutates shapes it was given. Events without samples are
13
+ * sent without a payload.
14
+ */
15
+ payloads?: Readonly<Record<string, ReadonlyArray<VizJson>>>;
16
+ }
17
+ interface Observation {
18
+ from: string;
19
+ event: string;
20
+ to: string;
21
+ transitioned: boolean;
22
+ /** Names of the effects that ran on entry into `to`. */
23
+ effects: string[];
24
+ /** Index of the step that caused it. */
25
+ step: number;
26
+ /** Payload the handler received (for branch-distance guidance). */
27
+ payload: unknown;
28
+ /** JSON copy of the actor context right after the handler ran; null if not JSON. */
29
+ context: unknown;
30
+ }
31
+ interface RunError {
32
+ from: string;
33
+ event: string;
34
+ message: string;
35
+ /** Index of the step that triggered it. */
36
+ step: number;
37
+ }
38
+ interface Trace {
39
+ visited: string[];
40
+ observations: Observation[];
41
+ errors: RunError[];
42
+ /** Indices of steps after which the actor context differed: loop candidates. */
43
+ contextSteps: number[];
44
+ /**
45
+ * Coverage features, sorted: `state:x`, `edge:a>e>b` (state changed or
46
+ * re-entered), `stay:a>e` (handled, no change), `error:a>e` (handler threw).
47
+ */
48
+ features: string[];
49
+ }
50
+ /** Declared inputs and internal events, each sorted. */
51
+ declare function eventCatalog(actor: AnyActor<unknown>): {
52
+ inputs: string[];
53
+ internal: string[];
54
+ };
55
+ /**
56
+ * Replay steps against a fresh actor. Synchronous and deterministic: effects
57
+ * only run via the virtual clock, and nothing here reads wall time.
58
+ */
59
+ declare function execute(target: FuzzTarget, steps: ReadonlyArray<VizStep>): Trace;
60
+ export { eventCatalog as a, Trace as i, Observation as n, execute as o, RunError as r, FuzzTarget as t };
package/dist/vite.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { c as vizDocument, i as runOf, n as inspectEntry, t as entryId } from "./load-C4rGWmGU.mjs";
1
+ import { c as projectRoot, i as runOf, l as vizDocument, n as inspectEntry, s as machineId, t as entryId, u as analyzeModule } from "./load-DOwsxTyL.mjs";
2
2
  import { UPDATE_EVENT, livePageHtml, serializeViz } from "@mantaq/viz";
3
+ import { Either } from "@mantaq/utils";
3
4
  import { dirname, isAbsolute, resolve } from "node:path";
4
5
  import { globSync, readFileSync } from "node:fs";
5
6
  import { fileURLToPath } from "node:url";
@@ -18,6 +19,42 @@ import { searchForWorkspaceRoot } from "vite";
18
19
  */
19
20
  const STYLESHEETS = ["@mantaq/viz/viewer.css", "@mantaq/viz/page.css"];
20
21
  const DEV_ITERATIONS = 500;
22
+ const TARGETS = "virtual:mantaq/targets";
23
+ const TARGETS_RESOLVED = `\0${TARGETS}`;
24
+ function createExpression(found, index) {
25
+ const callee = `m${index}[${JSON.stringify(found.name)}]`;
26
+ const call = found.call === "clock" ? `${callee}(VirtualClock())` : found.call === "options" ? `${callee}({ clock: VirtualClock() })` : `${callee}()`;
27
+ return found.unwrap === "actor" ? `${call}.actor` : call;
28
+ }
29
+ /**
30
+ * Source of `virtual:mantaq/targets`: one factory per exported actor factory,
31
+ * shaped by its type (`call`, `clock`, `options`). A bare actor export cannot
32
+ * be recreated, so it gets none. Like the server-side inspection, plain actors
33
+ * are skipped when a file has factories.
34
+ */
35
+ function targetsSource(files) {
36
+ const core = fileURLToPath(import.meta.resolve("@mantaq/core"));
37
+ const imports = [];
38
+ const entries = [];
39
+ files.forEach((file, index) => {
40
+ const analysis = analyzeModule({
41
+ file,
42
+ root: projectRoot(file)
43
+ });
44
+ if (Either.isLeft(analysis)) return;
45
+ const factories = analysis[1].machines.filter((found) => found.call !== "instance");
46
+ if (factories.length === 0) return;
47
+ imports.push(`import * as m${index} from ${JSON.stringify(file)};`);
48
+ for (const found of factories) entries.push(` ${JSON.stringify(machineId(found.name))}: () => { const actor = ${createExpression(found, index)}; return { actor, clock: actor.clock }; },`);
49
+ });
50
+ return [
51
+ `import { VirtualClock } from ${JSON.stringify(core)};`,
52
+ ...imports,
53
+ "export const targets = {",
54
+ ...entries,
55
+ "};"
56
+ ].join("\n");
57
+ }
21
58
  function machineFiles(root, patterns) {
22
59
  const files = patterns.flatMap((pattern) => globSync(pattern, { cwd: root }));
23
60
  return [...new Set(files.map((file) => isAbsolute(file) ? file : resolve(root, file)))].sort();
@@ -40,63 +77,83 @@ function mantaqViz(options) {
40
77
  const base = (options.base ?? "/__mantaq/").replace(/\/?$/, "/");
41
78
  const patterns = Array.isArray(options.machines) ? options.machines : [options.machines];
42
79
  const fuzz = options.fuzz ?? { iterations: DEV_ITERATIONS };
43
- const live = fileURLToPath(import.meta.resolve("@mantaq/viz/live"));
44
- return [inspectEntry(), {
45
- name: "mantaq-viz",
46
- apply: "serve",
47
- config: (config) => ({ server: { fs: { allow: [searchForWorkspaceRoot(config.root ?? process.cwd()), dirname(dirname(live))] } } }),
48
- configureServer(server) {
49
- let cached = null;
50
- const build = async () => {
51
- const { text, warnings } = await documentText(server, {
52
- files: machineFiles(server.config.root, patterns),
53
- fuzz
54
- });
55
- for (const warning of warnings) server.config.logger.warn(`[mantaq-viz] ${warning}`);
56
- return text;
57
- };
58
- server.watcher.on("all", (_event, path) => {
59
- if (!/\.[cm]?[jt]sx?$/.test(path)) return;
60
- cached = null;
61
- server.environments.ssr.moduleGraph.invalidateAll();
62
- server.ws.send({
63
- type: "custom",
64
- event: UPDATE_EVENT
80
+ const live = fileURLToPath(import.meta.resolve(import.meta.url.endsWith(".ts") ? "./live-entry.ts" : "./live-entry.mjs"));
81
+ const viz = dirname(dirname(fileURLToPath(import.meta.resolve("@mantaq/viz/live"))));
82
+ let root = process.cwd();
83
+ return [
84
+ inspectEntry(),
85
+ {
86
+ name: "mantaq-viz-targets",
87
+ apply: "serve",
88
+ configResolved: (config) => {
89
+ root = config.root;
90
+ },
91
+ resolveId: (id) => id === TARGETS ? TARGETS_RESOLVED : null,
92
+ load: (id) => id === TARGETS_RESOLVED ? targetsSource(machineFiles(root, patterns)) : null
93
+ },
94
+ {
95
+ name: "mantaq-viz",
96
+ apply: "serve",
97
+ config: (config) => ({ server: { fs: { allow: [
98
+ searchForWorkspaceRoot(config.root ?? process.cwd()),
99
+ dirname(dirname(live)),
100
+ viz
101
+ ] } } }),
102
+ configureServer(server) {
103
+ let cached = null;
104
+ const build = async () => {
105
+ const { text, warnings } = await documentText(server, {
106
+ files: machineFiles(server.config.root, patterns),
107
+ fuzz
108
+ });
109
+ for (const warning of warnings) server.config.logger.warn(`[mantaq-viz] ${warning}`);
110
+ return text;
111
+ };
112
+ server.watcher.on("all", (_event, path) => {
113
+ if (!/\.[cm]?[jt]sx?$/.test(path)) return;
114
+ cached = null;
115
+ const generated = server.moduleGraph.getModuleById(TARGETS_RESOLVED);
116
+ if (generated !== void 0) server.moduleGraph.invalidateModule(generated);
117
+ server.environments.ssr.moduleGraph.invalidateAll();
118
+ server.ws.send({
119
+ type: "custom",
120
+ event: UPDATE_EVENT
121
+ });
65
122
  });
66
- });
67
- server.middlewares.use((request, response, next) => {
68
- const path = request.url?.split("?")[0];
69
- if (path === `${base}viz.json`) {
70
- cached ??= build();
71
- cached.then((text) => {
72
- response.setHeader("content-type", "application/json");
73
- response.end(text);
74
- }, (error) => {
75
- cached = null;
76
- response.statusCode = 500;
77
- response.end(String(error));
123
+ server.middlewares.use((request, response, next) => {
124
+ const path = request.url?.split("?")[0];
125
+ if (path === `${base}viz.json`) {
126
+ cached ??= build();
127
+ cached.then((text) => {
128
+ response.setHeader("content-type", "application/json");
129
+ response.end(text);
130
+ }, (error) => {
131
+ cached = null;
132
+ response.statusCode = 500;
133
+ response.end(String(error));
134
+ });
135
+ return;
136
+ }
137
+ if (path !== base && path !== base.slice(0, -1)) {
138
+ next();
139
+ return;
140
+ }
141
+ const html = livePageHtml({
142
+ title: "mantaq viz",
143
+ source: `${base}viz.json`,
144
+ entry: `/@fs${live}`,
145
+ css: STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n")
146
+ });
147
+ server.transformIndexHtml(request.url ?? base, html).then((page) => {
148
+ response.setHeader("content-type", "text/html");
149
+ response.end(page);
78
150
  });
79
- return;
80
- }
81
- if (path !== base && path !== base.slice(0, -1)) {
82
- next();
83
- return;
84
- }
85
- const html = livePageHtml({
86
- title: "mantaq viz",
87
- source: `${base}viz.json`,
88
- entry: `/@fs${live}`,
89
- css: STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n")
90
151
  });
91
- server.transformIndexHtml(request.url ?? base, html).then((page) => {
92
- response.setHeader("content-type", "text/html");
93
- response.end(page);
152
+ server.httpServer?.once("listening", () => {
153
+ server.config.logger.info(` mantaq viz: ${base}`);
94
154
  });
95
- });
96
- server.httpServer?.once("listening", () => {
97
- server.config.logger.info(` mantaq viz: ${base}`);
98
- });
155
+ }
99
156
  }
100
- }];
157
+ ];
101
158
  }
102
159
  export { mantaqViz };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantaq/inspect",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Turn a Mantaq machine into a viz document: structure, handler explanations (AST), fuzz and test coverage. CLI and Vite plugin included.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,6 +17,8 @@
17
17
  "type": "module",
18
18
  "exports": {
19
19
  ".": "./dist/index.mjs",
20
+ "./drive": "./dist/drive.mjs",
21
+ "./live-entry": "./dist/live-entry.mjs",
20
22
  "./vite": "./dist/vite.mjs",
21
23
  "./package.json": "./package.json"
22
24
  },
@@ -25,11 +27,11 @@
25
27
  },
26
28
  "dependencies": {
27
29
  "typescript": "6.0.3",
28
- "@mantaq/core": "0.5.0",
30
+ "@mantaq/core": "0.5.1",
29
31
  "@mantaq/pbt": "0.0.3",
30
- "@mantaq/traversal": "0.3.0",
32
+ "@mantaq/traversal": "0.3.1",
31
33
  "@mantaq/utils": "0.0.4",
32
- "@mantaq/viz": "0.1.0"
34
+ "@mantaq/viz": "0.2.0"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@types/node": "^26.2.0",
@@ -37,7 +39,7 @@
37
39
  "happy-dom": "^21.0.4",
38
40
  "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
39
41
  "vite-plus": "1.4.0",
40
- "@mantaq/sugar": "2.0.0"
42
+ "@mantaq/sugar": "2.0.1"
41
43
  },
42
44
  "peerDependencies": {
43
45
  "vite": "*"