machina-inspect 4.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.
- package/README.md +103 -83
- 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.
|
|
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
|
-
|
|
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` >=
|
|
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
|
|
31
|
+
broken: {}, // unreachable: no transitions lead here
|
|
31
32
|
},
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const findings = inspect(config);
|
|
35
|
-
// [
|
|
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
|
-
|
|
47
|
+
`inspect()` accepts live instances too:
|
|
39
48
|
|
|
40
49
|
```ts
|
|
41
|
-
|
|
42
|
-
const findings = inspect(fsm);
|
|
43
|
-
```
|
|
50
|
+
import { createFsm } from "machina";
|
|
44
51
|
|
|
45
|
-
|
|
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
|
-
|
|
62
|
+
const findings = inspect(fsm); // []
|
|
63
|
+
```
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
## What It Checks
|
|
50
66
|
|
|
51
|
-
|
|
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
|
-
###
|
|
69
|
+
### Unreachable States
|
|
54
70
|
|
|
55
|
-
|
|
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
|
-
### `
|
|
73
|
+
### `_onEnter` Loops
|
|
58
74
|
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
import { buildStateGraph, inspectGraph } from "machina-inspect";
|
|
77
|
+
### Missing Handlers
|
|
63
78
|
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
## Findings
|
|
72
84
|
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
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
|
-
|
|
95
|
+
interface UnreachableFinding extends BaseFinding {
|
|
96
|
+
type: "unreachable-state";
|
|
97
|
+
}
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
interface OnEnterLoopFinding extends BaseFinding {
|
|
100
|
+
type: "onenter-loop";
|
|
101
|
+
}
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
interface MissingHandlerFinding extends BaseFinding {
|
|
104
|
+
type: "missing-handler";
|
|
105
|
+
inputs: string[]; // the input names this state is missing
|
|
106
|
+
}
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
const targets = walkHandlerAst(functionNode as AstFunctionNode);
|
|
108
|
+
type Finding = UnreachableFinding | OnEnterLoopFinding | MissingHandlerFinding;
|
|
94
109
|
```
|
|
95
110
|
|
|
96
|
-
##
|
|
111
|
+
## API
|
|
97
112
|
|
|
98
|
-
###
|
|
113
|
+
### `inspect(input): Finding[]`
|
|
99
114
|
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
### `inspectGraph(graph): Finding[]`
|
|
105
124
|
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
+
## For Tool Authors
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
The exports below serve tools built on machina-inspect, like the ESLint plugin and diagram generators.
|
|
113
138
|
|
|
114
|
-
|
|
139
|
+
### Graph IR
|
|
115
140
|
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
+
### Confidence Levels
|
|
145
167
|
|
|
146
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
173
|
+
### `analyzeHandler(handler): HandlerTarget[]`
|
|
151
174
|
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
const targets = analyzeHandler(({ ctx }) => {
|
|
181
|
+
if (ctx.ready) {
|
|
182
|
+
return "active";
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// [{ target: "active", confidence: "possible" }]
|
|
186
|
+
```
|
|
165
187
|
|
|
166
|
-
|
|
167
|
-
type: "onenter-loop";
|
|
168
|
-
}
|
|
188
|
+
### `walkHandlerAst(node): HandlerTarget[]`
|
|
169
189
|
|
|
170
|
-
|
|
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
|
-
|
|
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
|
|
200
|
+
## See Also
|
|
179
201
|
|
|
180
|
-
- [machina-
|
|
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": "4.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.
|
|
47
|
+
"acorn": "^8.18.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"machina": ">=7.0.
|
|
50
|
+
"machina": ">=7.0.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"eslint": "^9.39.
|
|
54
|
-
"jest": "^30.
|
|
55
|
-
"ts-jest": "^29.4.
|
|
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": "
|
|
58
|
+
"machina": "8.0.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"dev": "tsdown --watch",
|