machina-inspect 1.0.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/index.mjs ADDED
@@ -0,0 +1,556 @@
1
+ import { MACHINA_TYPE } from "machina";
2
+ import * as acorn from "acorn";
3
+
4
+ //#region src/handler-analyzer.ts
5
+ const CONDITIONAL_ANCESTORS = new Set([
6
+ "IfStatement",
7
+ "SwitchStatement",
8
+ "SwitchCase",
9
+ "ConditionalExpression",
10
+ "LogicalExpression",
11
+ "TryStatement"
12
+ ]);
13
+ const PARSE_OPTS = {
14
+ ecmaVersion: 2022,
15
+ sourceType: "script"
16
+ };
17
+ /**
18
+ * Try to parse a function's toString() output using several strategies.
19
+ *
20
+ * Function.prototype.toString() produces different formats:
21
+ * - Named function declaration: `function name() { ... }` → parses as-is
22
+ * - Anonymous function expression: `function () { ... }` → needs parens wrapper
23
+ * - Arrow function: `() => { ... }` → parses as-is
24
+ * - Method shorthand: `name() { ... }` → needs object literal wrapper
25
+ *
26
+ * Returns null if none of the strategies succeed (unusual syntax, etc.).
27
+ */
28
+ function tryParse(source) {
29
+ try {
30
+ return acorn.parse(source, PARSE_OPTS);
31
+ } catch {}
32
+ try {
33
+ return acorn.parse(`(${source})`, PARSE_OPTS);
34
+ } catch {}
35
+ try {
36
+ return acorn.parse(`({${source}})`, PARSE_OPTS);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * Find the first ArrowFunctionExpression node in a parsed AST.
43
+ * Returns undefined if no arrow function exists at the top level.
44
+ */
45
+ function findArrowFunction(node) {
46
+ if (node.type === "ArrowFunctionExpression") return node;
47
+ if (node.type === "Program" || node.type === "ExpressionStatement") {
48
+ const children = getChildren(node);
49
+ for (const child of children) {
50
+ const found = findArrowFunction(child);
51
+ if (found) return found;
52
+ }
53
+ }
54
+ }
55
+ /**
56
+ * Handle concise arrow functions (`() => "state"` with no block body).
57
+ *
58
+ * Acorn represents these with `expression: true` on the ArrowFunctionExpression
59
+ * and the body as a Literal directly — there is no ReturnStatement, so the
60
+ * general walkForReturns() misses them entirely.
61
+ *
62
+ * Returns:
63
+ * - HandlerTarget[] with one definite entry if the body is a string Literal
64
+ * - [] if the body is a non-string expression (no false edges produced)
65
+ * - undefined if the AST doesn't match the concise arrow pattern at all
66
+ * (caller should fall through to the general walker)
67
+ */
68
+ function checkConciseArrow(node) {
69
+ if (node.type !== "ArrowFunctionExpression" || !node.expression) return;
70
+ const body = node.body;
71
+ if (body && body.type === "Literal" && typeof body.value === "string") return [{
72
+ target: body.value,
73
+ confidence: "definite"
74
+ }];
75
+ return [];
76
+ }
77
+ /**
78
+ * Handle concise arrow functions found inside a parsed Program/ExpressionStatement.
79
+ * Used by analyzeHandler after tryParse() wraps the source in a Program node.
80
+ */
81
+ function checkConciseArrowInAst(ast) {
82
+ const arrowNode = findArrowFunction(ast);
83
+ if (!arrowNode || !arrowNode.expression) return;
84
+ const body = arrowNode.body;
85
+ if (body && body.type === "Literal" && typeof body.value === "string") return [{
86
+ target: body.value,
87
+ confidence: "definite"
88
+ }];
89
+ return [];
90
+ }
91
+ /**
92
+ * Walk a pre-parsed handler function AST node and extract transition targets.
93
+ *
94
+ * Accepts a FunctionDeclaration, FunctionExpression, or ArrowFunctionExpression
95
+ * node (as produced by acorn or the ESLint rule engine). Returns an array of
96
+ * { target, confidence } pairs. Empty array means no statically-determinable
97
+ * string return targets were found.
98
+ *
99
+ * This is the shared core used by both:
100
+ * - analyzeHandler() — the runtime path (parses fn.toString() first)
101
+ * - ESLint rules — the AST path (ESLint already has the parsed node)
102
+ */
103
+ function walkHandlerAst(node) {
104
+ const conciseResult = checkConciseArrow(node);
105
+ if (conciseResult !== void 0) return conciseResult;
106
+ const returns = [];
107
+ walkForReturns(node, [], returns);
108
+ if (returns.length === 0) return [];
109
+ const allNonConditional = returns.every((r) => !r.isConditional);
110
+ const isSoleReturn = returns.length === 1;
111
+ return returns.map(({ target, isConditional }) => ({
112
+ target,
113
+ confidence: allNonConditional && isSoleReturn && !isConditional ? "definite" : "possible"
114
+ }));
115
+ }
116
+ /**
117
+ * Analyze a handler function and extract transition targets.
118
+ *
119
+ * Returns an array of { target, confidence } pairs. Empty array means
120
+ * no statically-determinable string return targets were found.
121
+ *
122
+ * Safe to call on any function — parse errors return empty (fail silently).
123
+ */
124
+ function analyzeHandler(fn) {
125
+ const source = fn.toString();
126
+ if (source.includes("[native code]")) return [];
127
+ const ast = tryParse(source);
128
+ if (!ast) return [];
129
+ const conciseResult = checkConciseArrowInAst(ast);
130
+ if (conciseResult !== void 0) return conciseResult;
131
+ const fnNode = findTopLevelFunctionNode(ast);
132
+ if (fnNode) return walkHandlerAst(fnNode);
133
+ const returns = [];
134
+ walkForReturns(ast, [], returns);
135
+ if (returns.length === 0) return [];
136
+ const allNonConditional = returns.every((r) => !r.isConditional);
137
+ const isSoleReturn = returns.length === 1;
138
+ return returns.map(({ target, isConditional }) => ({
139
+ target,
140
+ confidence: allNonConditional && isSoleReturn && !isConditional ? "definite" : "possible"
141
+ }));
142
+ }
143
+ /**
144
+ * Find the top-level function node in a parsed Program AST.
145
+ * Returns the first FunctionDeclaration, FunctionExpression, or
146
+ * ArrowFunctionExpression at the top level.
147
+ */
148
+ function findTopLevelFunctionNode(ast) {
149
+ const children = getChildren(ast);
150
+ for (const child of children) {
151
+ if (child.type === "FunctionDeclaration" || child.type === "FunctionExpression" || child.type === "ArrowFunctionExpression") return child;
152
+ if (child.type === "ExpressionStatement") {
153
+ const grandChildren = getChildren(child);
154
+ for (const gc of grandChildren) if (gc.type === "FunctionDeclaration" || gc.type === "FunctionExpression" || gc.type === "ArrowFunctionExpression") return gc;
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Collect string literals from a conditional expression tree.
160
+ * Used when a ReturnStatement's argument is a ternary or logical expression.
161
+ * All findings are marked as "possible" since they're conditional.
162
+ */
163
+ function collectStringLiteralsFromExpression(node, results) {
164
+ if (!node || typeof node !== "object") return;
165
+ if (node.type === "Literal" && typeof node.value === "string") {
166
+ results.push({
167
+ target: node.value,
168
+ isConditional: true
169
+ });
170
+ return;
171
+ }
172
+ if (node.type === "ConditionalExpression") {
173
+ collectStringLiteralsFromExpression(node.consequent, results);
174
+ collectStringLiteralsFromExpression(node.alternate, results);
175
+ } else if (node.type === "LogicalExpression") {
176
+ collectStringLiteralsFromExpression(node.left, results);
177
+ collectStringLiteralsFromExpression(node.right, results);
178
+ }
179
+ }
180
+ /**
181
+ * Recursively walk the AST, collecting ReturnStatement nodes that
182
+ * return string literals. Tracks whether any CONDITIONAL_ANCESTOR
183
+ * node exists in the ancestor chain.
184
+ *
185
+ * We skip nested function bodies (arrow functions, regular functions,
186
+ * class methods) — their returns belong to a different function scope.
187
+ */
188
+ function walkForReturns(node, ancestors, results) {
189
+ if (!node || typeof node !== "object") return;
190
+ if (node.type === "ReturnStatement") {
191
+ const arg = node.argument;
192
+ if (!arg) return;
193
+ if (arg.type === "Literal" && typeof arg.value === "string") {
194
+ const isConditional = ancestors.some((a) => CONDITIONAL_ANCESTORS.has(a));
195
+ results.push({
196
+ target: arg.value,
197
+ isConditional
198
+ });
199
+ } else collectStringLiteralsFromExpression(arg, results);
200
+ return;
201
+ }
202
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
203
+ if (ancestors.some((a) => a === "FunctionDeclaration" || a === "FunctionExpression" || a === "ArrowFunctionExpression")) return;
204
+ }
205
+ const nextAncestors = [...ancestors, node.type];
206
+ const children = getChildren(node);
207
+ for (const child of children) if (child && typeof child === "object") walkForReturns(child, nextAncestors, results);
208
+ }
209
+ /**
210
+ * Extract the child nodes we want to recurse into from an AST node.
211
+ * Returns a flat list of child nodes (some may be arrays in the AST).
212
+ */
213
+ function getChildren(node) {
214
+ const children = [];
215
+ const pushIfNode = (val) => {
216
+ if (val && typeof val === "object" && "type" in val) children.push(val);
217
+ };
218
+ const pushArray = (arr) => {
219
+ if (Array.isArray(arr)) for (const item of arr) pushIfNode(item);
220
+ };
221
+ switch (node.type) {
222
+ case "Program":
223
+ pushArray(node.body);
224
+ break;
225
+ case "ExpressionStatement":
226
+ pushIfNode(node.expression);
227
+ break;
228
+ case "FunctionDeclaration":
229
+ case "FunctionExpression":
230
+ case "ArrowFunctionExpression":
231
+ if (node.body) pushIfNode(node.body);
232
+ break;
233
+ case "BlockStatement":
234
+ pushArray(node.body);
235
+ break;
236
+ case "ReturnStatement":
237
+ pushIfNode(node.argument);
238
+ break;
239
+ case "IfStatement":
240
+ pushIfNode(node.consequent);
241
+ pushIfNode(node.alternate);
242
+ break;
243
+ case "SwitchStatement":
244
+ pushArray(node.cases);
245
+ break;
246
+ case "SwitchCase":
247
+ pushArray(node.consequent);
248
+ break;
249
+ case "TryStatement":
250
+ pushIfNode(node.block);
251
+ pushIfNode(node.handler);
252
+ pushIfNode(node.finalizer);
253
+ break;
254
+ case "CatchClause":
255
+ pushIfNode(node.body);
256
+ break;
257
+ case "ConditionalExpression":
258
+ pushIfNode(node.consequent);
259
+ pushIfNode(node.alternate);
260
+ break;
261
+ case "LogicalExpression":
262
+ pushIfNode(node.left);
263
+ pushIfNode(node.right);
264
+ break;
265
+ case "VariableDeclaration":
266
+ pushArray(node.declarations);
267
+ break;
268
+ case "VariableDeclarator":
269
+ pushIfNode(node.init);
270
+ break;
271
+ case "ForStatement":
272
+ pushIfNode(node.body);
273
+ break;
274
+ case "WhileStatement":
275
+ case "DoWhileStatement":
276
+ pushIfNode(node.body);
277
+ break;
278
+ default: for (const key of Object.keys(node)) {
279
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "parent" || key === "range") continue;
280
+ const val = node[key];
281
+ if (Array.isArray(val)) pushArray(val);
282
+ else pushIfNode(val);
283
+ }
284
+ }
285
+ return children;
286
+ }
287
+
288
+ //#endregion
289
+ //#region src/graph-builder.ts
290
+ const SPECIAL_KEYS = new Set([
291
+ "_onEnter",
292
+ "_onExit",
293
+ "_child"
294
+ ]);
295
+ /**
296
+ * Normalize the input to a consistent internal shape.
297
+ * Config objects are passed through; live instances have their
298
+ * public fields extracted.
299
+ */
300
+ function normalizeInput(input) {
301
+ if (MACHINA_TYPE in input) {
302
+ const instance = input;
303
+ return {
304
+ id: instance.id,
305
+ initialState: instance.initialState,
306
+ states: instance.states
307
+ };
308
+ }
309
+ return input;
310
+ }
311
+ /**
312
+ * Extract transition edges from a single handler value.
313
+ * String shorthands produce one definite edge. Function handlers
314
+ * are analyzed by the acorn-based handler-analyzer.
315
+ */
316
+ function extractEdges(fromState, inputName, handlerValue) {
317
+ if (typeof handlerValue === "string") return [{
318
+ inputName,
319
+ from: fromState,
320
+ to: handlerValue,
321
+ confidence: "definite"
322
+ }];
323
+ if (typeof handlerValue === "function") return analyzeHandler(handlerValue).map(({ target, confidence }) => ({
324
+ inputName,
325
+ from: fromState,
326
+ to: target,
327
+ confidence
328
+ }));
329
+ return [];
330
+ }
331
+ /**
332
+ * Detect whether a _child value is a ChildLink wrapper (post-construction)
333
+ * vs a raw FSM instance (pre-construction config path).
334
+ *
335
+ * ChildLink objects have a `canHandle` method and an `instance` field.
336
+ * Raw FSM instances have MACHINA_TYPE stamped on them.
337
+ */
338
+ function isChildLink(value) {
339
+ return typeof value === "object" && value !== null && "canHandle" in value && "instance" in value;
340
+ }
341
+ /**
342
+ * Detect a plain config object that looks like an FSM config (has `initialState`
343
+ * and `states`). This covers eval'd user input in the explorer, where _child is
344
+ * a bare object literal rather than a createFsm() instance.
345
+ */
346
+ function isConfigShaped(value) {
347
+ return typeof value === "object" && value !== null && "initialState" in value && "states" in value;
348
+ }
349
+ /**
350
+ * Build a StateGraph from an FSM config or live instance.
351
+ * Child FSMs declared via _child are recursively built into child graphs.
352
+ *
353
+ * @param input - A config object or a live Fsm/BehavioralFsm instance
354
+ */
355
+ function buildStateGraph(input) {
356
+ const fsm = normalizeInput(input);
357
+ const nodes = {};
358
+ const children = {};
359
+ for (const stateName of Object.keys(fsm.states)) {
360
+ const stateObj = fsm.states[stateName];
361
+ const edges = [];
362
+ for (const key of Object.keys(stateObj)) {
363
+ const value = stateObj[key];
364
+ if (key === "_child") {
365
+ if (isChildLink(value)) children[stateName] = buildStateGraph(value.instance);
366
+ else if (typeof value === "object" && value !== null && MACHINA_TYPE in value) children[stateName] = buildStateGraph(value);
367
+ else if (isConfigShaped(value)) children[stateName] = buildStateGraph(value);
368
+ continue;
369
+ }
370
+ if (SPECIAL_KEYS.has(key)) {
371
+ if (key === "_onEnter" && typeof value === "function") {
372
+ const targets = analyzeHandler(value);
373
+ for (const { target, confidence } of targets) edges.push({
374
+ inputName: "_onEnter",
375
+ from: stateName,
376
+ to: target,
377
+ confidence
378
+ });
379
+ }
380
+ continue;
381
+ }
382
+ for (const edge of extractEdges(stateName, key, value)) edges.push(edge);
383
+ }
384
+ nodes[stateName] = {
385
+ name: stateName,
386
+ edges
387
+ };
388
+ }
389
+ return {
390
+ fsmId: fsm.id,
391
+ initialState: fsm.initialState,
392
+ nodes,
393
+ children
394
+ };
395
+ }
396
+
397
+ //#endregion
398
+ //#region src/checks/unreachable.ts
399
+ /**
400
+ * Check for unreachable states in the graph.
401
+ * BFS from initialState — any unvisited node is a finding.
402
+ * Recurses into child graphs independently.
403
+ */
404
+ function checkUnreachable(graph, parentState) {
405
+ const findings = [];
406
+ findings.push(...findUnreachableInGraph(graph, parentState));
407
+ for (const [childParentState, childGraph] of Object.entries(graph.children)) findings.push(...checkUnreachable(childGraph, childParentState));
408
+ return findings;
409
+ }
410
+ function findUnreachableInGraph(graph, parentState) {
411
+ const { fsmId, initialState, nodes } = graph;
412
+ const visited = /* @__PURE__ */ new Set();
413
+ const queue = [];
414
+ if (initialState in nodes) {
415
+ queue.push(initialState);
416
+ visited.add(initialState);
417
+ }
418
+ while (queue.length > 0) {
419
+ const node = nodes[queue.shift()];
420
+ if (!node) continue;
421
+ for (const edge of node.edges) if (!visited.has(edge.to) && edge.to in nodes) {
422
+ visited.add(edge.to);
423
+ queue.push(edge.to);
424
+ }
425
+ }
426
+ const findings = [];
427
+ for (const stateName of Object.keys(nodes)) if (!visited.has(stateName)) findings.push({
428
+ type: "unreachable-state",
429
+ message: `State "${stateName}" is unreachable from initialState "${initialState}" in FSM "${fsmId}".`,
430
+ fsmId,
431
+ states: [stateName],
432
+ parentState
433
+ });
434
+ return findings;
435
+ }
436
+
437
+ //#endregion
438
+ //#region src/checks/onenter-loop.ts
439
+ /**
440
+ * Check for _onEnter transition loops in the graph.
441
+ * Only reports cycles where every edge is "definite".
442
+ * Recurses into child graphs.
443
+ */
444
+ function checkOnEnterLoops(graph, parentState) {
445
+ const findings = [];
446
+ findings.push(...findOnEnterLoopsInGraph(graph, parentState));
447
+ for (const [childParentState, childGraph] of Object.entries(graph.children)) findings.push(...checkOnEnterLoops(childGraph, childParentState));
448
+ return findings;
449
+ }
450
+ function findOnEnterLoopsInGraph(graph, parentState) {
451
+ const { fsmId, nodes } = graph;
452
+ const onEnterEdges = {};
453
+ for (const [stateName, node] of Object.entries(nodes)) {
454
+ const definiteOnEnterTargets = node.edges.filter((e) => e.inputName === "_onEnter" && e.confidence === "definite" && e.to !== stateName).map((e) => e.to);
455
+ if (definiteOnEnterTargets.length > 0) onEnterEdges[stateName] = definiteOnEnterTargets;
456
+ }
457
+ const WHITE = 0, GRAY = 1, BLACK = 2;
458
+ const color = {};
459
+ for (const stateName of Object.keys(nodes)) color[stateName] = WHITE;
460
+ const findings = [];
461
+ const reportedCycles = /* @__PURE__ */ new Set();
462
+ const dfs = (node, stack) => {
463
+ color[node] = GRAY;
464
+ stack.push(node);
465
+ for (const neighbor of onEnterEdges[node] ?? []) if (color[neighbor] === GRAY) {
466
+ const cycleStart = stack.indexOf(neighbor);
467
+ const cycle = stack.slice(cycleStart);
468
+ const cycleKey = [...cycle].sort().join(",");
469
+ if (!reportedCycles.has(cycleKey)) {
470
+ reportedCycles.add(cycleKey);
471
+ findings.push({
472
+ type: "onenter-loop",
473
+ message: `Unconditional _onEnter loop detected in FSM "${fsmId}": ` + cycle.join(" → ") + ` → ${neighbor}`,
474
+ fsmId,
475
+ states: cycle,
476
+ parentState
477
+ });
478
+ }
479
+ } else if (color[neighbor] === WHITE) dfs(neighbor, stack);
480
+ stack.pop();
481
+ color[node] = BLACK;
482
+ };
483
+ for (const stateName of Object.keys(nodes)) if (color[stateName] === WHITE) dfs(stateName, []);
484
+ return findings;
485
+ }
486
+
487
+ //#endregion
488
+ //#region src/checks/missing-handler.ts
489
+ const EXCLUDED_INPUTS = new Set([
490
+ "_onEnter",
491
+ "_onExit",
492
+ "*"
493
+ ]);
494
+ /**
495
+ * Check for states with missing handlers relative to the FSM's full input set.
496
+ * States with a `*` catch-all edge are skipped entirely.
497
+ * Recurses into child graphs independently.
498
+ */
499
+ function checkMissingHandlers(graph, parentState) {
500
+ const findings = [];
501
+ findings.push(...findMissingHandlersInGraph(graph, parentState));
502
+ for (const [childParentState, childGraph] of Object.entries(graph.children)) findings.push(...checkMissingHandlers(childGraph, childParentState));
503
+ return findings;
504
+ }
505
+ function findMissingHandlersInGraph(graph, parentState) {
506
+ const { fsmId, nodes } = graph;
507
+ const allInputs = /* @__PURE__ */ new Set();
508
+ for (const node of Object.values(nodes)) for (const edge of node.edges) if (!EXCLUDED_INPUTS.has(edge.inputName)) allInputs.add(edge.inputName);
509
+ if (allInputs.size === 0) return [];
510
+ const findings = [];
511
+ for (const [stateName, node] of Object.entries(nodes)) {
512
+ if (node.edges.some((e) => e.inputName === "*")) continue;
513
+ const stateInputs = /* @__PURE__ */ new Set();
514
+ for (const edge of node.edges) if (!EXCLUDED_INPUTS.has(edge.inputName)) stateInputs.add(edge.inputName);
515
+ const missingInputs = [...allInputs].filter((input) => !stateInputs.has(input));
516
+ if (missingInputs.length > 0) findings.push({
517
+ type: "missing-handler",
518
+ message: `State "${stateName}" in FSM "${fsmId}" is missing handlers for: ` + missingInputs.map((i) => `"${i}"`).join(", ") + ".",
519
+ fsmId,
520
+ states: [stateName],
521
+ inputs: missingInputs,
522
+ parentState
523
+ });
524
+ }
525
+ return findings;
526
+ }
527
+
528
+ //#endregion
529
+ //#region src/inspect.ts
530
+ /**
531
+ * Build a state graph from the input and run all structural checks.
532
+ * Returns an array of findings — empty means no issues detected.
533
+ *
534
+ * @param input - A config object or a live Fsm/BehavioralFsm instance
535
+ */
536
+ function inspect(input) {
537
+ return inspectGraph(buildStateGraph(input));
538
+ }
539
+ /**
540
+ * Run all structural checks against a pre-built state graph.
541
+ * Useful when you've already called buildStateGraph() for another purpose
542
+ * (e.g., diagram export) and want to avoid building it twice.
543
+ *
544
+ * @param graph - A StateGraph produced by buildStateGraph()
545
+ */
546
+ function inspectGraph(graph) {
547
+ return [
548
+ ...checkUnreachable(graph),
549
+ ...checkOnEnterLoops(graph),
550
+ ...checkMissingHandlers(graph)
551
+ ];
552
+ }
553
+
554
+ //#endregion
555
+ export { analyzeHandler, buildStateGraph, inspect, inspectGraph, walkHandlerAst };
556
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/handler-analyzer.ts","../src/graph-builder.ts","../src/checks/unreachable.ts","../src/checks/onenter-loop.ts","../src/checks/missing-handler.ts","../src/inspect.ts"],"sourcesContent":["// =============================================================================\n// handler-analyzer.ts — Acorn-based static analysis of function handlers\n//\n// Parses handler.toString() with acorn, walks the AST for ReturnStatement\n// nodes that return string literals, and determines confidence based on\n// whether the return is nested in a conditional or not.\n//\n// This is best-effort: we catch explicit `return \"stateName\"` patterns.\n// Dynamic returns (variables, template literals, computed expressions) are\n// silently skipped — we produce no false edges, just miss some.\n// =============================================================================\n\nimport * as acorn from \"acorn\";\n\nexport interface HandlerTarget {\n target: string;\n confidence: \"definite\" | \"possible\";\n}\n\n// acorn nodes have a `type` string field plus arbitrary children.\n// We use a loose type to keep the AST walking code simple.\n// Declared early so AstFunctionNode can reference it.\ntype AcornNode = {\n type: string;\n body?: AcornNode | AcornNode[];\n argument?: AcornNode | null;\n value?: unknown;\n consequent?: AcornNode | AcornNode[];\n alternate?: AcornNode | null;\n cases?: AcornNode[];\n block?: AcornNode;\n handler?: AcornNode | null;\n finalizer?: AcornNode | null;\n left?: AcornNode;\n right?: AcornNode;\n expression?: AcornNode | boolean;\n declarations?: AcornNode[];\n init?: AcornNode | null;\n test?: AcornNode | null;\n update?: AcornNode | null;\n params?: AcornNode[];\n [key: string]: unknown;\n};\n\n/**\n * A union of ESTree node types that represent a callable handler function.\n * Structurally compatible with both acorn nodes and ESLint's @types/estree,\n * so walkHandlerAst can accept either without type juggling at the call site.\n *\n * The index signature `[key: string]: unknown` keeps this loose enough that\n * acorn's extra fields (start, end, loc) don't cause structural mismatches.\n */\nexport type AstFunctionNode = {\n type: \"FunctionDeclaration\" | \"FunctionExpression\" | \"ArrowFunctionExpression\";\n body: AcornNode;\n expression?: boolean; // true for concise arrow functions (no block body)\n params: AcornNode[];\n [key: string]: unknown;\n};\n\n// Node types that make a return \"conditional\" — any string-returning\n// ReturnStatement nested inside one of these becomes \"possible\".\nconst CONDITIONAL_ANCESTORS = new Set([\n \"IfStatement\",\n \"SwitchStatement\",\n \"SwitchCase\",\n \"ConditionalExpression\",\n \"LogicalExpression\",\n \"TryStatement\",\n]);\n\ninterface ReturnInfo {\n target: string;\n isConditional: boolean;\n}\n\nconst PARSE_OPTS: acorn.Options = { ecmaVersion: 2022, sourceType: \"script\" };\n\n/**\n * Try to parse a function's toString() output using several strategies.\n *\n * Function.prototype.toString() produces different formats:\n * - Named function declaration: `function name() { ... }` → parses as-is\n * - Anonymous function expression: `function () { ... }` → needs parens wrapper\n * - Arrow function: `() => { ... }` → parses as-is\n * - Method shorthand: `name() { ... }` → needs object literal wrapper\n *\n * Returns null if none of the strategies succeed (unusual syntax, etc.).\n */\nfunction tryParse(source: string): acorn.Node | null {\n // Strategy 1: parse as-is (named function declarations, arrow functions)\n try {\n return acorn.parse(source, PARSE_OPTS);\n } catch {\n // continue to next strategy\n }\n\n // Strategy 2: wrap in parens (anonymous function expressions)\n try {\n return acorn.parse(`(${source})`, PARSE_OPTS);\n } catch {\n // continue to next strategy\n }\n\n // Strategy 3: wrap as object method (method shorthand `name() { ... }`)\n try {\n return acorn.parse(`({${source}})`, PARSE_OPTS);\n } catch {\n // All strategies exhausted — fail silently\n return null;\n }\n}\n\n/**\n * Find the first ArrowFunctionExpression node in a parsed AST.\n * Returns undefined if no arrow function exists at the top level.\n */\nfunction findArrowFunction(node: AcornNode): AcornNode | undefined {\n if (node.type === \"ArrowFunctionExpression\") {\n return node;\n }\n if (node.type === \"Program\" || node.type === \"ExpressionStatement\") {\n const children = getChildren(node);\n for (const child of children) {\n const found = findArrowFunction(child);\n if (found) {\n return found;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Handle concise arrow functions (`() => \"state\"` with no block body).\n *\n * Acorn represents these with `expression: true` on the ArrowFunctionExpression\n * and the body as a Literal directly — there is no ReturnStatement, so the\n * general walkForReturns() misses them entirely.\n *\n * Returns:\n * - HandlerTarget[] with one definite entry if the body is a string Literal\n * - [] if the body is a non-string expression (no false edges produced)\n * - undefined if the AST doesn't match the concise arrow pattern at all\n * (caller should fall through to the general walker)\n */\nfunction checkConciseArrow(node: AstFunctionNode): HandlerTarget[] | undefined {\n // A concise arrow has `expression: true` and the body is not a BlockStatement.\n if (node.type !== \"ArrowFunctionExpression\" || !node.expression) {\n // Not a concise arrow — let checkConciseArrow's caller also check\n // the parsed Program wrapper for arrow functions embedded in Program nodes.\n return undefined;\n }\n const body = node.body as AcornNode;\n if (body && body.type === \"Literal\" && typeof body.value === \"string\") {\n return [{ target: body.value, confidence: \"definite\" }];\n }\n // Non-string concise arrow (e.g., `() => someVar`) — no targets extractable\n return [];\n}\n\n/**\n * Handle concise arrow functions found inside a parsed Program/ExpressionStatement.\n * Used by analyzeHandler after tryParse() wraps the source in a Program node.\n */\nfunction checkConciseArrowInAst(ast: AcornNode): HandlerTarget[] | undefined {\n const arrowNode = findArrowFunction(ast);\n if (!arrowNode || !arrowNode.expression) {\n return undefined;\n }\n const body = arrowNode.body as AcornNode;\n if (body && body.type === \"Literal\" && typeof body.value === \"string\") {\n return [{ target: body.value, confidence: \"definite\" }];\n }\n return [];\n}\n\n/**\n * Walk a pre-parsed handler function AST node and extract transition targets.\n *\n * Accepts a FunctionDeclaration, FunctionExpression, or ArrowFunctionExpression\n * node (as produced by acorn or the ESLint rule engine). Returns an array of\n * { target, confidence } pairs. Empty array means no statically-determinable\n * string return targets were found.\n *\n * This is the shared core used by both:\n * - analyzeHandler() — the runtime path (parses fn.toString() first)\n * - ESLint rules — the AST path (ESLint already has the parsed node)\n */\nexport function walkHandlerAst(node: AstFunctionNode): HandlerTarget[] {\n // Concise arrow functions (`() => \"state\"`) have no ReturnStatement.\n // The body is the expression directly — handle before the general walker.\n const conciseResult = checkConciseArrow(node);\n if (conciseResult !== undefined) {\n return conciseResult;\n }\n\n const returns: ReturnInfo[] = [];\n walkForReturns(node as unknown as AcornNode, [], returns);\n\n if (returns.length === 0) {\n return [];\n }\n\n // Determine confidence:\n // - A single non-conditional return that's the ONLY string return → \"definite\"\n // - Multiple returns or conditional returns → all \"possible\"\n const allNonConditional = returns.every(r => !r.isConditional);\n const isSoleReturn = returns.length === 1;\n\n return returns.map(({ target, isConditional }) => ({\n target,\n confidence: allNonConditional && isSoleReturn && !isConditional ? \"definite\" : \"possible\",\n }));\n}\n\n/**\n * Analyze a handler function and extract transition targets.\n *\n * Returns an array of { target, confidence } pairs. Empty array means\n * no statically-determinable string return targets were found.\n *\n * Safe to call on any function — parse errors return empty (fail silently).\n */\nexport function analyzeHandler(fn: (...args: unknown[]) => unknown): HandlerTarget[] {\n const source = fn.toString();\n\n // Native/bound functions have no analyzable body — skip them.\n if (source.includes(\"[native code]\")) {\n return [];\n }\n\n const ast = tryParse(source);\n if (!ast) {\n return [];\n }\n\n // Concise arrow functions (`() => \"state\"`) have no ReturnStatement in their\n // AST. Instead, the ArrowFunctionExpression node has `expression: true` and\n // the body is a Literal directly. Handle this before the general walker.\n const conciseResult = checkConciseArrowInAst(ast as unknown as AcornNode);\n if (conciseResult !== undefined) {\n return conciseResult;\n }\n\n // For block-body functions, find the top-level function node and delegate\n // to the shared AST walker.\n const fnNode = findTopLevelFunctionNode(ast as unknown as AcornNode);\n if (fnNode) {\n return walkHandlerAst(fnNode as unknown as AstFunctionNode);\n }\n\n // Fallback: walk the raw AST directly (method shorthand wrapper case)\n const returns: ReturnInfo[] = [];\n walkForReturns(ast as unknown as AcornNode, [], returns);\n\n if (returns.length === 0) {\n return [];\n }\n\n const allNonConditional = returns.every(r => !r.isConditional);\n const isSoleReturn = returns.length === 1;\n\n return returns.map(({ target, isConditional }) => ({\n target,\n confidence: allNonConditional && isSoleReturn && !isConditional ? \"definite\" : \"possible\",\n }));\n}\n\n/**\n * Find the top-level function node in a parsed Program AST.\n * Returns the first FunctionDeclaration, FunctionExpression, or\n * ArrowFunctionExpression at the top level.\n */\nfunction findTopLevelFunctionNode(ast: AcornNode): AcornNode | undefined {\n const children = getChildren(ast);\n for (const child of children) {\n if (\n child.type === \"FunctionDeclaration\" ||\n child.type === \"FunctionExpression\" ||\n child.type === \"ArrowFunctionExpression\"\n ) {\n return child;\n }\n // ExpressionStatement wrapper (anonymous function expressions wrapped in parens)\n if (child.type === \"ExpressionStatement\") {\n const grandChildren = getChildren(child);\n for (const gc of grandChildren) {\n if (\n gc.type === \"FunctionDeclaration\" ||\n gc.type === \"FunctionExpression\" ||\n gc.type === \"ArrowFunctionExpression\"\n ) {\n return gc;\n }\n }\n }\n }\n return undefined;\n}\n\n/**\n * Collect string literals from a conditional expression tree.\n * Used when a ReturnStatement's argument is a ternary or logical expression.\n * All findings are marked as \"possible\" since they're conditional.\n */\nfunction collectStringLiteralsFromExpression(node: AcornNode, results: ReturnInfo[]): void {\n if (!node || typeof node !== \"object\") {\n return;\n }\n if (node.type === \"Literal\" && typeof node.value === \"string\") {\n results.push({ target: node.value as string, isConditional: true });\n return;\n }\n // Recurse into conditional/logical branches\n if (node.type === \"ConditionalExpression\") {\n collectStringLiteralsFromExpression(node.consequent as AcornNode, results);\n collectStringLiteralsFromExpression(node.alternate as AcornNode, results);\n } else if (node.type === \"LogicalExpression\") {\n collectStringLiteralsFromExpression(node.left as AcornNode, results);\n collectStringLiteralsFromExpression(node.right as AcornNode, results);\n }\n // For any other expression type, don't recurse further — we can't reliably\n // determine whether the result will be a state name.\n}\n\n/**\n * Recursively walk the AST, collecting ReturnStatement nodes that\n * return string literals. Tracks whether any CONDITIONAL_ANCESTOR\n * node exists in the ancestor chain.\n *\n * We skip nested function bodies (arrow functions, regular functions,\n * class methods) — their returns belong to a different function scope.\n */\nfunction walkForReturns(node: AcornNode, ancestors: string[], results: ReturnInfo[]): void {\n if (!node || typeof node !== \"object\") {\n return;\n }\n\n if (node.type === \"ReturnStatement\") {\n const arg = node.argument;\n if (!arg) {\n return;\n }\n if (arg.type === \"Literal\" && typeof arg.value === \"string\") {\n const isConditional = ancestors.some(a => CONDITIONAL_ANCESTORS.has(a));\n results.push({ target: arg.value, isConditional });\n } else {\n // Non-literal return value (ternary, logical, expression, etc.)\n // Walk into the argument to find any string literals in conditional branches.\n // All targets found this way are \"possible\" since they're behind some expression.\n collectStringLiteralsFromExpression(arg, results);\n }\n return;\n }\n\n // Skip nested function scopes — their returns are not ours\n if (\n node.type === \"FunctionDeclaration\" ||\n node.type === \"FunctionExpression\" ||\n node.type === \"ArrowFunctionExpression\"\n ) {\n // The outermost function (index 0 in a Program body) IS our function,\n // so we need to visit it. We detect \"outermost\" by checking if ancestors\n // has any function-type ancestor already.\n const hasFunctionAncestor = ancestors.some(\n a =>\n a === \"FunctionDeclaration\" ||\n a === \"FunctionExpression\" ||\n a === \"ArrowFunctionExpression\"\n );\n if (hasFunctionAncestor) {\n // Nested function — skip its body entirely\n return;\n }\n // Fall through to recurse into the outermost function body\n }\n\n const nextAncestors = [...ancestors, node.type];\n const children = getChildren(node);\n for (const child of children) {\n if (child && typeof child === \"object\") {\n walkForReturns(child as AcornNode, nextAncestors, results);\n }\n }\n}\n\n/**\n * Extract the child nodes we want to recurse into from an AST node.\n * Returns a flat list of child nodes (some may be arrays in the AST).\n */\nfunction getChildren(node: AcornNode): AcornNode[] {\n const children: AcornNode[] = [];\n\n const pushIfNode = (val: unknown) => {\n if (val && typeof val === \"object\" && \"type\" in (val as object)) {\n children.push(val as AcornNode);\n }\n };\n\n const pushArray = (arr: unknown) => {\n if (Array.isArray(arr)) {\n for (const item of arr) {\n pushIfNode(item);\n }\n }\n };\n\n switch (node.type) {\n case \"Program\":\n pushArray(node.body);\n break;\n case \"ExpressionStatement\":\n pushIfNode(node.expression);\n break;\n case \"FunctionDeclaration\":\n case \"FunctionExpression\":\n case \"ArrowFunctionExpression\":\n if (node.body) {\n pushIfNode(node.body);\n }\n break;\n case \"BlockStatement\":\n pushArray(node.body);\n break;\n case \"ReturnStatement\":\n pushIfNode(node.argument);\n break;\n case \"IfStatement\":\n pushIfNode(node.consequent);\n pushIfNode(node.alternate);\n break;\n case \"SwitchStatement\":\n pushArray(node.cases);\n break;\n case \"SwitchCase\":\n pushArray(node.consequent);\n break;\n case \"TryStatement\":\n pushIfNode(node.block);\n pushIfNode(node.handler);\n pushIfNode(node.finalizer);\n break;\n case \"CatchClause\":\n pushIfNode(node.body as AcornNode | undefined);\n break;\n case \"ConditionalExpression\":\n pushIfNode(node.consequent as AcornNode | undefined);\n pushIfNode(node.alternate as AcornNode | undefined);\n break;\n case \"LogicalExpression\":\n pushIfNode(node.left);\n pushIfNode(node.right);\n break;\n case \"VariableDeclaration\":\n pushArray(node.declarations);\n break;\n case \"VariableDeclarator\":\n pushIfNode(node.init);\n break;\n case \"ForStatement\":\n pushIfNode(node.body as AcornNode | undefined);\n break;\n case \"WhileStatement\":\n case \"DoWhileStatement\":\n pushIfNode(node.body as AcornNode | undefined);\n break;\n default:\n // For any node type we don't explicitly handle, try common child fields.\n // This prevents us from silently missing returns in unusual constructs.\n for (const key of Object.keys(node)) {\n // ESLint AST nodes have circular `parent` pointers that Acorn nodes lack.\n // Traversing into `parent` causes infinite recursion.\n if (\n key === \"type\" ||\n key === \"start\" ||\n key === \"end\" ||\n key === \"loc\" ||\n key === \"parent\" ||\n key === \"range\"\n ) {\n continue;\n }\n const val = node[key];\n if (Array.isArray(val)) {\n pushArray(val);\n } else {\n pushIfNode(val);\n }\n }\n }\n\n return children;\n}\n","// =============================================================================\n// graph-builder.ts — Build a StateGraph IR from an FSM config or live instance\n//\n// Two input shapes exist:\n// 1. Config object (pre-instantiation) — _child values are raw FSM instances\n// 2. Live instance (post-construction) — _child values are ChildLink wrappers\n// (BehavioralFsm mutates config.states in-place during wrapChildLinks())\n//\n// The MACHINA_TYPE symbol discriminates between config and live instance inputs,\n// and between raw FSM instances and ChildLink wrappers on _child fields.\n// =============================================================================\n\nimport { MACHINA_TYPE, type ChildLink } from \"machina\";\nimport { analyzeHandler } from \"./handler-analyzer\";\nimport type { StateGraph, StateNode, TransitionEdge, InspectInput } from \"./types\";\n\n// Keys that are lifecycle/structural — not input handlers\nconst SPECIAL_KEYS = new Set([\"_onEnter\", \"_onExit\", \"_child\"]);\n\n// Shape of what we need from a live FSM instance\ninterface FsmLike {\n id: string;\n initialState: string;\n states: Record<string, Record<string, unknown>>;\n}\n\n/**\n * Normalize the input to a consistent internal shape.\n * Config objects are passed through; live instances have their\n * public fields extracted.\n */\nfunction normalizeInput(input: InspectInput): FsmLike {\n // Live instance: has MACHINA_TYPE stamped on it\n if (MACHINA_TYPE in (input as object)) {\n const instance = input as unknown as FsmLike & { [key: symbol]: string };\n return {\n id: instance.id,\n initialState: instance.initialState,\n states: instance.states,\n };\n }\n // Config object: read directly\n return input as FsmLike;\n}\n\n/**\n * Extract transition edges from a single handler value.\n * String shorthands produce one definite edge. Function handlers\n * are analyzed by the acorn-based handler-analyzer.\n */\nfunction extractEdges(\n fromState: string,\n inputName: string,\n handlerValue: unknown\n): TransitionEdge[] {\n if (typeof handlerValue === \"string\") {\n return [{ inputName, from: fromState, to: handlerValue, confidence: \"definite\" }];\n }\n\n if (typeof handlerValue === \"function\") {\n const targets = analyzeHandler(handlerValue as (...args: unknown[]) => unknown);\n return targets.map(({ target, confidence }) => ({\n inputName,\n from: fromState,\n to: target,\n confidence,\n }));\n }\n\n return [];\n}\n\n/**\n * Detect whether a _child value is a ChildLink wrapper (post-construction)\n * vs a raw FSM instance (pre-construction config path).\n *\n * ChildLink objects have a `canHandle` method and an `instance` field.\n * Raw FSM instances have MACHINA_TYPE stamped on them.\n */\nfunction isChildLink(value: unknown): value is ChildLink {\n return (\n typeof value === \"object\" && value !== null && \"canHandle\" in value && \"instance\" in value\n );\n}\n\n/**\n * Detect a plain config object that looks like an FSM config (has `initialState`\n * and `states`). This covers eval'd user input in the explorer, where _child is\n * a bare object literal rather than a createFsm() instance.\n */\nfunction isConfigShaped(value: unknown): value is FsmLike {\n return (\n typeof value === \"object\" && value !== null && \"initialState\" in value && \"states\" in value\n );\n}\n\n/**\n * Build a StateGraph from an FSM config or live instance.\n * Child FSMs declared via _child are recursively built into child graphs.\n *\n * @param input - A config object or a live Fsm/BehavioralFsm instance\n */\nexport function buildStateGraph(input: InspectInput): StateGraph {\n const fsm = normalizeInput(input);\n const nodes: Record<string, StateNode> = {};\n const children: Record<string, StateGraph> = {};\n\n for (const stateName of Object.keys(fsm.states)) {\n const stateObj = fsm.states[stateName];\n const edges: TransitionEdge[] = [];\n\n for (const key of Object.keys(stateObj)) {\n const value = stateObj[key];\n\n if (key === \"_child\") {\n // _child values come in three shapes:\n // 1. ChildLink wrapper (live instance post-construction)\n // 2. Raw FSM instance with MACHINA_TYPE (config passed a createFsm() result)\n // 3. Plain config object with initialState + states (e.g. from eval'd user input)\n if (isChildLink(value)) {\n children[stateName] = buildStateGraph(\n value.instance as unknown as InspectInput\n );\n } else if (typeof value === \"object\" && value !== null && MACHINA_TYPE in value) {\n children[stateName] = buildStateGraph(value as unknown as InspectInput);\n } else if (isConfigShaped(value)) {\n children[stateName] = buildStateGraph(value as unknown as InspectInput);\n }\n continue;\n }\n\n if (SPECIAL_KEYS.has(key)) {\n // _onEnter / _onExit: analyze function handlers for bounce edges,\n // but label them with source \"_onEnter\" or \"_onExit\" so the\n // loop detector can find them.\n if (key === \"_onEnter\" && typeof value === \"function\") {\n const targets = analyzeHandler(value as (...args: unknown[]) => unknown);\n for (const { target, confidence } of targets) {\n edges.push({\n inputName: \"_onEnter\",\n from: stateName,\n to: target,\n confidence,\n });\n }\n }\n continue;\n }\n\n // Regular input handlers (including catch-all \"*\")\n for (const edge of extractEdges(stateName, key, value)) {\n edges.push(edge);\n }\n }\n\n nodes[stateName] = { name: stateName, edges };\n }\n\n const graph: StateGraph = {\n fsmId: fsm.id,\n initialState: fsm.initialState,\n nodes,\n children,\n };\n\n return graph;\n}\n","// =============================================================================\n// unreachable.ts — Detect states with no inbound path from initialState\n//\n// BFS from initialState, following ALL edges (definite and possible).\n// Any node not reached is unreachable. Child graphs are analyzed independently\n// from their own initialState.\n// =============================================================================\n\nimport type { StateGraph, Finding } from \"../types\";\n\n/**\n * Check for unreachable states in the graph.\n * BFS from initialState — any unvisited node is a finding.\n * Recurses into child graphs independently.\n */\nexport function checkUnreachable(graph: StateGraph, parentState?: string): Finding[] {\n const findings: Finding[] = [];\n\n findings.push(...findUnreachableInGraph(graph, parentState));\n\n for (const [childParentState, childGraph] of Object.entries(graph.children)) {\n findings.push(...checkUnreachable(childGraph, childParentState));\n }\n\n return findings;\n}\n\nfunction findUnreachableInGraph(graph: StateGraph, parentState?: string): Finding[] {\n const { fsmId, initialState, nodes } = graph;\n const visited = new Set<string>();\n const queue: string[] = [];\n\n if (initialState in nodes) {\n queue.push(initialState);\n visited.add(initialState);\n }\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const node = nodes[current];\n if (!node) {\n continue;\n }\n for (const edge of node.edges) {\n if (!visited.has(edge.to) && edge.to in nodes) {\n visited.add(edge.to);\n queue.push(edge.to);\n }\n }\n }\n\n const findings: Finding[] = [];\n for (const stateName of Object.keys(nodes)) {\n if (!visited.has(stateName)) {\n findings.push({\n type: \"unreachable-state\",\n message: `State \"${stateName}\" is unreachable from initialState \"${initialState}\" in FSM \"${fsmId}\".`,\n fsmId,\n states: [stateName],\n parentState,\n });\n }\n }\n\n return findings;\n}\n","// =============================================================================\n// onenter-loop.ts — Detect unconditional _onEnter transition cycles\n//\n// Only reports cycles where ALL edges are \"definite\" (unconditional).\n// Conditional _onEnter bounces are an intentional pattern — not bugs.\n// Self-loops are excluded: the runtime silently ignores same-state transitions.\n// =============================================================================\n\nimport type { StateGraph, Finding } from \"../types\";\n\n/**\n * Check for _onEnter transition loops in the graph.\n * Only reports cycles where every edge is \"definite\".\n * Recurses into child graphs.\n */\nexport function checkOnEnterLoops(graph: StateGraph, parentState?: string): Finding[] {\n const findings: Finding[] = [];\n\n findings.push(...findOnEnterLoopsInGraph(graph, parentState));\n\n for (const [childParentState, childGraph] of Object.entries(graph.children)) {\n findings.push(...checkOnEnterLoops(childGraph, childParentState));\n }\n\n return findings;\n}\n\nfunction findOnEnterLoopsInGraph(graph: StateGraph, parentState?: string): Finding[] {\n const { fsmId, nodes } = graph;\n\n // Build the _onEnter-only subgraph: edges where inputName === \"_onEnter\"\n // and confidence === \"definite\", excluding self-loops (same-state transitions\n // are no-ops in the runtime — they hit the same-state guard and return early).\n const onEnterEdges: Record<string, string[]> = {};\n\n for (const [stateName, node] of Object.entries(nodes)) {\n const definiteOnEnterTargets = node.edges\n .filter(\n e => e.inputName === \"_onEnter\" && e.confidence === \"definite\" && e.to !== stateName // exclude self-loops\n )\n .map(e => e.to);\n\n if (definiteOnEnterTargets.length > 0) {\n onEnterEdges[stateName] = definiteOnEnterTargets;\n }\n }\n\n // DFS cycle detection on the _onEnter subgraph.\n // We use the \"white-gray-black\" algorithm: unvisited, in-stack, done.\n const WHITE = 0,\n GRAY = 1,\n BLACK = 2;\n const color: Record<string, number> = {};\n\n for (const stateName of Object.keys(nodes)) {\n color[stateName] = WHITE;\n }\n\n const findings: Finding[] = [];\n const reportedCycles = new Set<string>();\n\n const dfs = (node: string, stack: string[]): void => {\n color[node] = GRAY;\n stack.push(node);\n\n for (const neighbor of onEnterEdges[node] ?? []) {\n if (color[neighbor] === GRAY) {\n // Found a back edge — extract the cycle from the stack\n const cycleStart = stack.indexOf(neighbor);\n const cycle = stack.slice(cycleStart);\n\n // Deduplicate: sort cycle members to produce a canonical key\n const cycleKey = [...cycle].sort().join(\",\");\n if (!reportedCycles.has(cycleKey)) {\n reportedCycles.add(cycleKey);\n findings.push({\n type: \"onenter-loop\",\n message:\n `Unconditional _onEnter loop detected in FSM \"${fsmId}\": ` +\n cycle.join(\" → \") +\n ` → ${neighbor}`,\n fsmId,\n states: cycle,\n parentState,\n });\n }\n } else if (color[neighbor] === WHITE) {\n dfs(neighbor, stack);\n }\n }\n\n stack.pop();\n color[node] = BLACK;\n };\n\n for (const stateName of Object.keys(nodes)) {\n if (color[stateName] === WHITE) {\n dfs(stateName, []);\n }\n }\n\n return findings;\n}\n","// =============================================================================\n// missing-handler.ts — Detect states missing handlers for known inputs\n//\n// Collects the union of all input names across all edges in a graph\n// (excluding _onEnter, _onExit, and *), then flags any state that handles\n// fewer inputs than the global union — unless that state has a * catch-all,\n// which implicitly handles every input.\n//\n// Limitation: Only inputs visible as graph edges are included. Function\n// handlers that produce no statically-extractable return target are invisible\n// to the graph, so the input union may be incomplete. This check is\n// best-effort and operates on what the graph knows.\n//\n// Does NOT recurse into child graphs automatically — child graphs have their\n// own independent input sets. The top-level checkMissingHandlers recurses\n// into children the same way all other checks do.\n// =============================================================================\n\nimport type { StateGraph, Finding } from \"../types\";\n\n// These are structural keys in the state object, not real inputs.\n// Including them in the \"all inputs\" union would cause false positives\n// on every FSM.\nconst EXCLUDED_INPUTS = new Set([\"_onEnter\", \"_onExit\", \"*\"]);\n\n/**\n * Check for states with missing handlers relative to the FSM's full input set.\n * States with a `*` catch-all edge are skipped entirely.\n * Recurses into child graphs independently.\n */\nexport function checkMissingHandlers(graph: StateGraph, parentState?: string): Finding[] {\n const findings: Finding[] = [];\n\n findings.push(...findMissingHandlersInGraph(graph, parentState));\n\n for (const [childParentState, childGraph] of Object.entries(graph.children)) {\n findings.push(...checkMissingHandlers(childGraph, childParentState));\n }\n\n return findings;\n}\n\nfunction findMissingHandlersInGraph(graph: StateGraph, parentState?: string): Finding[] {\n const { fsmId, nodes } = graph;\n\n // Collect the global input union: every inputName across all edges in\n // all states, minus the structural keys we never want to check.\n const allInputs = new Set<string>();\n for (const node of Object.values(nodes)) {\n for (const edge of node.edges) {\n if (!EXCLUDED_INPUTS.has(edge.inputName)) {\n allInputs.add(edge.inputName);\n }\n }\n }\n\n // Nothing to compare if the FSM has no named inputs at all (e.g., single\n // state with only _onEnter, or an empty FSM).\n if (allInputs.size === 0) {\n return [];\n }\n\n const findings: Finding[] = [];\n\n for (const [stateName, node] of Object.entries(nodes)) {\n // States with a * catch-all handle every input by definition.\n // Reporting missing handlers here would always be a false positive.\n const hasCatchAll = node.edges.some(e => e.inputName === \"*\");\n if (hasCatchAll) {\n continue;\n }\n\n // Collect the inputs this state explicitly handles (via edges).\n const stateInputs = new Set<string>();\n for (const edge of node.edges) {\n if (!EXCLUDED_INPUTS.has(edge.inputName)) {\n stateInputs.add(edge.inputName);\n }\n }\n\n // Diff: inputs the FSM knows about that this state doesn't handle.\n const missingInputs = [...allInputs].filter(input => !stateInputs.has(input));\n\n if (missingInputs.length > 0) {\n findings.push({\n type: \"missing-handler\",\n message:\n `State \"${stateName}\" in FSM \"${fsmId}\" is missing handlers for: ` +\n missingInputs.map(i => `\"${i}\"`).join(\", \") +\n \".\",\n fsmId,\n states: [stateName],\n inputs: missingInputs,\n parentState,\n });\n }\n }\n\n return findings;\n}\n","// =============================================================================\n// inspect.ts — Public API entry points\n//\n// inspect() is the convenience function: build graph + run all checks.\n// inspectGraph() is for callers that already have a graph (e.g., tools\n// that build the graph once and run multiple check passes).\n// =============================================================================\n\nimport { buildStateGraph } from \"./graph-builder\";\nimport { checkUnreachable } from \"./checks/unreachable\";\nimport { checkOnEnterLoops } from \"./checks/onenter-loop\";\nimport { checkMissingHandlers } from \"./checks/missing-handler\";\nimport type { StateGraph, Finding, InspectInput } from \"./types\";\n\n/**\n * Build a state graph from the input and run all structural checks.\n * Returns an array of findings — empty means no issues detected.\n *\n * @param input - A config object or a live Fsm/BehavioralFsm instance\n */\nexport function inspect(input: InspectInput): Finding[] {\n const graph = buildStateGraph(input);\n return inspectGraph(graph);\n}\n\n/**\n * Run all structural checks against a pre-built state graph.\n * Useful when you've already called buildStateGraph() for another purpose\n * (e.g., diagram export) and want to avoid building it twice.\n *\n * @param graph - A StateGraph produced by buildStateGraph()\n */\nexport function inspectGraph(graph: StateGraph): Finding[] {\n return [\n ...checkUnreachable(graph),\n ...checkOnEnterLoops(graph),\n ...checkMissingHandlers(graph),\n ];\n}\n"],"mappings":"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"}