machina-inspect 3.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +103 -83
  2. package/package.json +7 -7
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # machina-inspect
2
2
 
3
- Static analysis for [machina](https://machina-js.org) FSM configs. Catches structural bugs — unreachable states, infinite `_onEnter` loops, missing handlers — without running the machine.
3
+ Static analysis for [machina](https://machina-js.org) FSM configs. machina-inspect finds structural bugs like unreachable states, infinite `_onEnter` loops, and missing handlers, without running the FSM.
4
4
 
5
- Parses your FSM config (or a live instance) into a directed graph IR, then runs checks against it. The graph is a first-class export, so other tools (diagram generators, linters, [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina)) can build on the same representation.
5
+ This library parses your FSM config (or a live instance) into a directed graph (an intermediate representation, or IR), then runs checks against it. The graph is a first-class export, so other tools (diagram generators, linters, [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina)) can build on the same representation.
6
+
7
+ machina-inspect is the right entry point for scripts and custom tooling. For the same checks inline in your editor, use [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina), and for test assertions, use [machina-test](https://www.npmjs.com/package/machina-test). Both build on this package.
6
8
 
7
9
  ## Install
8
10
 
@@ -12,12 +14,11 @@ npm install machina-inspect
12
14
  pnpm add machina-inspect
13
15
  ```
14
16
 
15
- `machina` >= 6.0.0 is a peer dependency.
17
+ `machina` >= 7.0.1 is a peer dependency.
16
18
 
17
19
  ## Quick Start
18
20
 
19
21
  ```ts
20
- import { createFsm } from "machina";
21
22
  import { inspect } from "machina-inspect";
22
23
 
23
24
  const config = {
@@ -27,93 +28,117 @@ const config = {
27
28
  green: { timeout: "yellow" },
28
29
  yellow: { timeout: "red" },
29
30
  red: { timeout: "green" },
30
- broken: {}, // unreachable — no transitions lead here
31
+ broken: {}, // unreachable: no transitions lead here
31
32
  },
32
33
  };
33
34
 
34
35
  const findings = inspect(config);
35
- // [{ type: "unreachable-state", states: ["broken"], ... }]
36
+ // [
37
+ // {
38
+ // type: "unreachable-state",
39
+ // states: ["broken"],
40
+ // message: 'State "broken" is unreachable from initialState "green" in FSM "traffic-light".',
41
+ // ...
42
+ // },
43
+ // { type: "missing-handler", states: ["broken"], inputs: ["timeout"], ... },
44
+ // ]
36
45
  ```
37
46
 
38
- Works with live instances too:
47
+ `inspect()` accepts live instances too:
39
48
 
40
49
  ```ts
41
- const fsm = createFsm(config);
42
- const findings = inspect(fsm);
43
- ```
50
+ import { createFsm } from "machina";
44
51
 
45
- ## API
52
+ const fsm = createFsm({
53
+ id: "toggle",
54
+ initialState: "on",
55
+ context: {},
56
+ states: {
57
+ on: { flip: "off" },
58
+ off: { flip: "on" },
59
+ },
60
+ });
46
61
 
47
- ### `inspect(input): Finding[]`
62
+ const findings = inspect(fsm); // []
63
+ ```
48
64
 
49
- Build a state graph and run all checks. Returns an array of findings — empty means no issues.
65
+ ## What It Checks
50
66
 
51
- `input` is either a config object (`{ id, initialState, states }`) or a live `Fsm` / `BehavioralFsm` instance.
67
+ machina-inspect rates each transition edge it discovers as `"definite"` (always taken) or `"possible"` (conditional). The checks below use those ratings, and [Confidence Levels](#confidence-levels) defines them fully.
52
68
 
53
- ### `buildStateGraph(input): StateGraph`
69
+ ### Unreachable States
54
70
 
55
- Build the graph IR without running checks. Use this when you want the graph for other purposes (visualization, custom analysis) and want to avoid building it twice.
71
+ The unreachable check reports each state that the machine cannot enter from `initialState`. Both `"definite"` and `"possible"` edges count, because any path at all makes a state reachable. The search is a breadth-first traversal from `initialState`.
56
72
 
57
- ### `inspectGraph(graph): Finding[]`
73
+ ### `_onEnter` Loops
58
74
 
59
- Run all checks against a pre-built `StateGraph`. Pair with `buildStateGraph()` when you need both the graph and the findings.
75
+ The loop check reports cycles of `_onEnter` transitions in which each edge is `"definite"`, the loops that bounce unconditionally at runtime. A conditional bounce like `if (ctx.error) { return "failed"; }` is an intentional pattern, and the check skips it. Same-state self-loops are also skipped, because the machina runtime ignores same-state transitions. The detection is a depth-first cycle search over the `_onEnter` subgraph.
60
76
 
61
- ```ts
62
- import { buildStateGraph, inspectGraph } from "machina-inspect";
77
+ ### Missing Handlers
63
78
 
64
- const graph = buildStateGraph(config);
65
- // use `graph` for diagram export, etc.
66
- const findings = inspectGraph(graph);
67
- ```
79
+ The missing-handler check collects the input names that appear anywhere in the FSM, then reports the states that leave some of those inputs unhandled. The check is best-effort: only inputs visible as graph edges (string shorthand or statically extractable function returns) enter the set.
68
80
 
69
- ### `analyzeHandler(handler): HandlerTarget[]`
81
+ A state with a `*` catch-all is exempt, since the catch-all handles any input. `_onEnter`, `_onExit`, and `*` are excluded from the input set, and child graphs are checked against their own input sets.
70
82
 
71
- Parse a handler function for transition targets via acorn AST analysis of `handler.toString()`. Returns `{ target, confidence }` pairs.
83
+ ## Findings
72
84
 
73
- ```ts
74
- import { analyzeHandler } from "machina-inspect";
85
+ Findings are a discriminated union. Narrow on `finding.type` to access the type-specific fields.
75
86
 
76
- const targets = analyzeHandler(({ ctx }) => {
77
- if (ctx.ready) {
78
- return "active";
79
- }
80
- });
81
- // [{ target: "active", confidence: "possible" }]
82
- ```
87
+ ```ts
88
+ interface BaseFinding {
89
+ message: string;
90
+ fsmId: string;
91
+ states: string[];
92
+ parentState?: string; // set for child FSM findings
93
+ }
83
94
 
84
- ### `walkHandlerAst(node): HandlerTarget[]`
95
+ interface UnreachableFinding extends BaseFinding {
96
+ type: "unreachable-state";
97
+ }
85
98
 
86
- Walk a pre-parsed ESTree/acorn function AST node to extract transition targets. This is the shared core used by both the runtime path (`analyzeHandler`) and the ESLint plugin. Accepts `AstFunctionNode` — a union of `FunctionDeclaration`, `FunctionExpression`, and `ArrowFunctionExpression`.
99
+ interface OnEnterLoopFinding extends BaseFinding {
100
+ type: "onenter-loop";
101
+ }
87
102
 
88
- ```ts
89
- import { walkHandlerAst } from "machina-inspect";
90
- import type { AstFunctionNode } from "machina-inspect";
103
+ interface MissingHandlerFinding extends BaseFinding {
104
+ type: "missing-handler";
105
+ inputs: string[]; // the input names this state is missing
106
+ }
91
107
 
92
- // Use with a pre-parsed AST node (e.g., from ESLint or acorn)
93
- const targets = walkHandlerAst(functionNode as AstFunctionNode);
108
+ type Finding = UnreachableFinding | OnEnterLoopFinding | MissingHandlerFinding;
94
109
  ```
95
110
 
96
- ## Checks
111
+ ## API
97
112
 
98
- ### Unreachable States
113
+ ### `inspect(input): Finding[]`
99
114
 
100
- BFS from `initialState`. Any state with no inbound path is reported. Both `"definite"` and `"possible"` edges count — if there's any path, the state is reachable.
115
+ Builds a state graph and runs all checks. Returns an array of findings, and an empty array means the config passed.
101
116
 
102
- ### `_onEnter` Loops
117
+ `input` is either a config object (`{ id, initialState, states }`) or a live `Fsm` / `BehavioralFsm` instance.
118
+
119
+ ### `buildStateGraph(input): StateGraph`
120
+
121
+ Builds the graph IR without running the checks. Use it when you want the graph for other purposes (visualization, custom analysis) and want to avoid building it twice.
103
122
 
104
- DFS cycle detection on the subgraph of `_onEnter` transitions. Only reports cycles where **every** edge is `"definite"` (unconditional). Conditional bounces like `if (ctx.error) return "failed"` are intentional patterns, not bugs.
123
+ ### `inspectGraph(graph): Finding[]`
105
124
 
106
- Same-state self-loops are excluded — the machina runtime ignores same-state transitions, so they're no-ops.
125
+ Runs all checks against a pre-built `StateGraph`. Pair it with `buildStateGraph()` when you need both the graph and the findings.
107
126
 
108
- ### Missing Handlers
127
+ ```ts
128
+ import { buildStateGraph, inspectGraph } from "machina-inspect";
129
+
130
+ const graph = buildStateGraph(config);
131
+ // use `graph` for diagram export, etc.
132
+ const findings = inspectGraph(graph);
133
+ ```
109
134
 
110
- Collects the union of all input names across all states in the FSM. States that don't handle inputs present elsewhere are flagged. This is a best-effort check — only inputs visible as graph edges (string shorthand or statically extractable function returns) are included.
135
+ ## For Tool Authors
111
136
 
112
- States with a `*` catch-all handler are excluded (they implicitly handle everything). `_onEnter`, `_onExit`, and `*` are excluded from the input union. Child graphs are checked independently with their own input sets.
137
+ The exports below serve tools built on machina-inspect, like the ESLint plugin and diagram generators.
113
138
 
114
- ## Graph IR
139
+ ### Graph IR
115
140
 
116
- The `StateGraph` is designed to be consumed by downstream tools:
141
+ Downstream tools consume the `StateGraph` directly:
117
142
 
118
143
  ```ts
119
144
  interface StateGraph {
@@ -136,50 +161,45 @@ interface TransitionEdge {
136
161
  }
137
162
  ```
138
163
 
139
- ### Confidence Levels
140
-
141
- - **`"definite"`** — Unconditional transition. String shorthands (`timeout: "yellow"`) and functions with a single top-level return.
142
- - **`"possible"`** — Conditional transition. Returns inside `if`, `switch`, ternary, logical expressions, or `try` blocks. Also applies when a function has multiple return statements.
164
+ The graph builder follows `_child` declarations recursively, and each child graph appears in `children`, keyed by the parent state name. A child graph is analyzed independently, with its own `initialState` and its own reachability.
143
165
 
144
- Function handlers are analyzed via [acorn](https://github.com/acornjs/acorn) (AST parsing of `handler.toString()`). Non-string returns, template literals, and variable returns are ignored — the analysis is best-effort, not exhaustive.
166
+ ### Confidence Levels
145
167
 
146
- ## Child FSMs
168
+ - **`"definite"`**: an unconditional transition. String shorthands (`timeout: "yellow"`) and functions with one top-level return.
169
+ - **`"possible"`**: a conditional transition. Returns inside `if`, `switch`, ternary, logical expressions, or `try` blocks, and any function with multiple return statements.
147
170
 
148
- `_child` declarations are followed recursively. The resulting child graphs appear in `StateGraph.children`, keyed by the parent state name. Each child graph is analyzed independently (its own `initialState`, its own reachability).
171
+ machina-inspect parses `handler.toString()` with [acorn](https://github.com/acornjs/acorn) to find transition targets in function handlers. Non-string returns, template literals, and variable returns are ignored. The analysis is best-effort.
149
172
 
150
- ## Findings
173
+ ### `analyzeHandler(handler): HandlerTarget[]`
151
174
 
152
- Findings are a discriminated union — narrow on `finding.type` to access type-specific fields.
175
+ Parses a handler function for transition targets via acorn AST analysis of `handler.toString()`. Returns `{ target, confidence }` pairs.
153
176
 
154
177
  ```ts
155
- interface BaseFinding {
156
- message: string;
157
- fsmId: string;
158
- states: string[];
159
- parentState?: string; // set for child FSM findings
160
- }
178
+ import { analyzeHandler } from "machina-inspect";
161
179
 
162
- interface UnreachableFinding extends BaseFinding {
163
- type: "unreachable-state";
164
- }
180
+ const targets = analyzeHandler(({ ctx }) => {
181
+ if (ctx.ready) {
182
+ return "active";
183
+ }
184
+ });
185
+ // [{ target: "active", confidence: "possible" }]
186
+ ```
165
187
 
166
- interface OnEnterLoopFinding extends BaseFinding {
167
- type: "onenter-loop";
168
- }
188
+ ### `walkHandlerAst(node): HandlerTarget[]`
169
189
 
170
- interface MissingHandlerFinding extends BaseFinding {
171
- type: "missing-handler";
172
- inputs: string[]; // the input names this state is missing
173
- }
190
+ Walks a pre-parsed ESTree/acorn function AST node to extract transition targets. This is the shared core behind the runtime path (`analyzeHandler`) and the ESLint plugin. It accepts `AstFunctionNode`, a union of `FunctionDeclaration`, `FunctionExpression`, and `ArrowFunctionExpression`.
174
191
 
175
- type Finding = UnreachableFinding | OnEnterLoopFinding | MissingHandlerFinding;
192
+ ```ts
193
+ import { walkHandlerAst } from "machina-inspect";
194
+ import type { AstFunctionNode } from "machina-inspect";
195
+
196
+ // Use with a pre-parsed AST node (e.g., from ESLint or acorn)
197
+ const targets = walkHandlerAst(functionNode as AstFunctionNode);
176
198
  ```
177
199
 
178
- ## See also
200
+ ## See Also
179
201
 
180
- - [machina-test](https://www.npmjs.com/package/machina-test) — Jest/Vitest custom matchers built on machina-inspect's graph analysis
181
- - [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina) — get these checks inline in your editor via ESLint
182
- - [machina-explorer](https://machina-js.org/examples/machina-explorer/) — browser-based paste-and-analyze UI built on machina-inspect
202
+ - [machina-explorer](https://machina-js.org/examples/machina-explorer/), a browser-based paste-and-analyze UI built on machina-inspect
183
203
 
184
204
  ## License
185
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machina-inspect",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Static analysis and structural inspection for machina FSM configs.",
5
5
  "author": "Jim Cowart (http://ifandelse.com)",
6
6
  "license": "MIT",
@@ -44,18 +44,18 @@
44
44
  },
45
45
  "sideEffects": false,
46
46
  "dependencies": {
47
- "acorn": "^8.14.1"
47
+ "acorn": "^8.18.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "machina": ">=6.3.0"
50
+ "machina": ">=7.0.1"
51
51
  },
52
52
  "devDependencies": {
53
- "eslint": "^9.39.4",
54
- "jest": "^30.3.0",
55
- "ts-jest": "^29.4.6",
53
+ "eslint": "^9.39.5",
54
+ "jest": "^30.5.2",
55
+ "ts-jest": "^29.4.14",
56
56
  "tsdown": "^0.20.3",
57
57
  "typescript": "^5.9.3",
58
- "machina": "6.3.0"
58
+ "machina": "8.0.0"
59
59
  },
60
60
  "scripts": {
61
61
  "dev": "tsdown --watch",