@doeixd/machine 0.0.4 → 0.0.5

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/src/extract.ts CHANGED
@@ -10,12 +10,49 @@
10
10
  *
11
11
  * @usage
12
12
  * 1. Ensure you have `ts-node` and `ts-morph` installed: `npm install -D ts-node ts-morph`
13
- * 2. Configure the settings in the `generateChart` function below.
14
- * 3. Run the script from your project root: `npx ts-node ./scripts/extract.ts > chart.json`
13
+ * 2. Create a configuration object or use .statechart.config.ts
14
+ * 3. Run the script from your project root: `npx ts-node ./scripts/extract-statechart.ts`
15
15
  */
16
16
 
17
- import { Project, ts, Type, Symbol as TSSymbol, Node } from 'ts-morph';
18
- import { META_KEY } from './primitives';
17
+ import { Project, Type, Node } from 'ts-morph';
18
+
19
+ // =============================================================================
20
+ // SECTION: CONFIGURATION TYPES
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Configuration for a single machine to extract
25
+ */
26
+ export interface MachineConfig {
27
+ /** Path to the source file containing the machine */
28
+ input: string;
29
+ /** Array of class names that represent states */
30
+ classes: string[];
31
+ /** Output file path (optional, defaults to stdout) */
32
+ output?: string;
33
+ /** Top-level ID for the statechart */
34
+ id: string;
35
+ /** Name of the class that represents the initial state */
36
+ initialState: string;
37
+ /** Optional description of the machine */
38
+ description?: string;
39
+ }
40
+
41
+ /**
42
+ * Global extraction configuration
43
+ */
44
+ export interface ExtractionConfig {
45
+ /** Array of machines to extract */
46
+ machines: MachineConfig[];
47
+ /** Validate output against XState JSON schema (optional) */
48
+ validate?: boolean;
49
+ /** Output format (json, mermaid, or both) */
50
+ format?: 'json' | 'mermaid' | 'both';
51
+ /** Watch mode - auto-regenerate on file changes */
52
+ watch?: boolean;
53
+ /** Verbose logging */
54
+ verbose?: boolean;
55
+ }
19
56
 
20
57
  // =============================================================================
21
58
  // SECTION: CORE ANALYSIS LOGIC
@@ -27,9 +64,10 @@ import { META_KEY } from './primitives';
27
64
  * types into their string names.
28
65
  *
29
66
  * @param type - The `ts-morph` Type object to serialize.
67
+ * @param verbose - Enable debug logging
30
68
  * @returns A JSON-compatible value (string, number, object, array).
31
69
  */
32
- function typeToJson(type: Type): any {
70
+ function typeToJson(type: Type, verbose = false): any {
33
71
  // --- Terminal Types ---
34
72
  const symbol = type.getSymbol();
35
73
  if (symbol && symbol.getDeclarations().some(Node.isClassDeclaration)) {
@@ -38,42 +76,273 @@ function typeToJson(type: Type): any {
38
76
  if (type.isStringLiteral()) return type.getLiteralValue();
39
77
  if (type.isNumberLiteral()) return type.getLiteralValue();
40
78
  if (type.isBooleanLiteral()) return type.getLiteralValue();
79
+ if (type.isString()) return 'string';
80
+ if (type.isNumber()) return 'number';
81
+ if (type.isBoolean()) return 'boolean';
41
82
 
42
83
  // --- Recursive Types ---
43
84
  if (type.isArray()) {
44
85
  const elementType = type.getArrayElementTypeOrThrow();
45
- return [typeToJson(elementType)]; // Note: Assumes homogenous array for simplicity
86
+ return [typeToJson(elementType, verbose)];
46
87
  }
47
- if (type.isObject()) {
88
+
89
+ // --- Object Types ---
90
+ if (type.isObject() || type.isIntersection()) {
48
91
  const obj: { [key: string]: any } = {};
49
- for (const prop of type.getProperties()) {
92
+ const properties = type.getProperties();
93
+
94
+ // Filter out symbol properties and internal properties
95
+ for (const prop of properties) {
96
+ const propName = prop.getName();
97
+
98
+ // Skip symbol properties (those starting with "__@")
99
+ if (propName.startsWith('__@')) continue;
100
+
50
101
  const declaration = prop.getValueDeclaration();
51
102
  if (!declaration) continue;
52
- obj[prop.getName()] = typeToJson(declaration.getType());
103
+
104
+ try {
105
+ obj[propName] = typeToJson(declaration.getType(), verbose);
106
+ } catch (e) {
107
+ if (verbose) console.error(` Warning: Failed to serialize property ${propName}:`, e);
108
+ obj[propName] = 'unknown';
109
+ }
53
110
  }
54
- return obj;
111
+
112
+ // If we got an empty object, return null (no metadata)
113
+ return Object.keys(obj).length > 0 ? obj : null;
114
+ }
115
+
116
+ if (verbose) {
117
+ console.error(` Unhandled type: ${type.getText()}`);
55
118
  }
56
119
 
57
120
  return 'unknown'; // Fallback for unhandled types
58
121
  }
59
122
 
123
+ // =============================================================================
124
+ // SECTION: AST-BASED METADATA EXTRACTION
125
+ // =============================================================================
126
+
127
+ /**
128
+ * Resolves a class name from an AST node (handles identifiers and typeof expressions)
129
+ */
130
+ function resolveClassName(node: Node): string {
131
+ // Handle: LoggingInMachine
132
+ if (Node.isIdentifier(node)) {
133
+ return node.getText();
134
+ }
135
+
136
+ // Handle: typeof LoggingInMachine
137
+ if (Node.isTypeOfExpression(node)) {
138
+ return node.getExpression().getText();
139
+ }
140
+
141
+ return 'unknown';
142
+ }
143
+
144
+ /**
145
+ * Parses an object literal expression into a plain JavaScript object
146
+ */
147
+ function parseObjectLiteral(obj: Node): any {
148
+ if (!Node.isObjectLiteralExpression(obj)) {
149
+ return {};
150
+ }
151
+
152
+ const result: any = {};
153
+
154
+ for (const prop of obj.getProperties()) {
155
+ if (Node.isPropertyAssignment(prop)) {
156
+ const name = prop.getName();
157
+ const init = prop.getInitializer();
158
+
159
+ if (init) {
160
+ if (Node.isStringLiteral(init)) {
161
+ result[name] = init.getLiteralValue();
162
+ } else if (Node.isNumericLiteral(init)) {
163
+ result[name] = init.getLiteralValue();
164
+ } else if (init.getText() === 'true' || init.getText() === 'false') {
165
+ result[name] = init.getText() === 'true';
166
+ } else if (Node.isIdentifier(init)) {
167
+ result[name] = init.getText();
168
+ } else if (Node.isObjectLiteralExpression(init)) {
169
+ result[name] = parseObjectLiteral(init);
170
+ } else if (Node.isArrayLiteralExpression(init)) {
171
+ result[name] = init.getElements().map(el => {
172
+ if (Node.isObjectLiteralExpression(el)) {
173
+ return parseObjectLiteral(el);
174
+ }
175
+ return el.getText();
176
+ });
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * Parses an invoke service configuration, resolving class names for onDone/onError
187
+ */
188
+ function parseInvokeService(obj: Node): any {
189
+ if (!Node.isObjectLiteralExpression(obj)) {
190
+ return {};
191
+ }
192
+
193
+ const service: any = {};
194
+
195
+ for (const prop of obj.getProperties()) {
196
+ if (Node.isPropertyAssignment(prop)) {
197
+ const name = prop.getName();
198
+ const init = prop.getInitializer();
199
+
200
+ if (!init) continue;
201
+
202
+ if (name === 'onDone' || name === 'onError') {
203
+ // Resolve class names for state targets
204
+ service[name] = resolveClassName(init);
205
+ } else if (Node.isStringLiteral(init)) {
206
+ service[name] = init.getLiteralValue();
207
+ } else if (Node.isIdentifier(init)) {
208
+ service[name] = init.getText();
209
+ }
210
+ }
211
+ }
212
+
213
+ return service;
214
+ }
215
+
60
216
  /**
61
- * Given a type, this function looks for our special `META_KEY` brand and,
62
- * if found, extracts and serializes the metadata type into a plain object.
217
+ * Recursively extracts metadata from a call expression chain
218
+ * Handles nested DSL primitive calls like: describe(text, guarded(guard, transitionTo(...)))
219
+ */
220
+ function extractFromCallExpression(call: Node, verbose = false): any | null {
221
+ if (!Node.isCallExpression(call)) {
222
+ return null;
223
+ }
224
+
225
+ const expression = call.getExpression();
226
+ const fnName = Node.isIdentifier(expression) ? expression.getText() : null;
227
+
228
+ if (!fnName) {
229
+ return null;
230
+ }
231
+
232
+ const metadata: any = {};
233
+ const args = call.getArguments();
234
+
235
+ switch (fnName) {
236
+ case 'transitionTo':
237
+ // Args: (target, implementation)
238
+ if (args[0]) {
239
+ metadata.target = resolveClassName(args[0]);
240
+ }
241
+ // The second argument might be another call expression, but we don't recurse there
242
+ // because transitionTo is the innermost wrapper
243
+ break;
244
+
245
+ case 'describe':
246
+ // Args: (description, transition)
247
+ if (args[0] && Node.isStringLiteral(args[0])) {
248
+ metadata.description = args[0].getLiteralValue();
249
+ }
250
+ // Recurse into wrapped transition
251
+ if (args[1] && Node.isCallExpression(args[1])) {
252
+ const nested = extractFromCallExpression(args[1], verbose);
253
+ if (nested) {
254
+ Object.assign(metadata, nested);
255
+ }
256
+ }
257
+ break;
258
+
259
+ case 'guarded':
260
+ // Args: (guard, transition)
261
+ if (args[0]) {
262
+ const guard = parseObjectLiteral(args[0]);
263
+ if (Object.keys(guard).length > 0) {
264
+ metadata.guards = [guard];
265
+ }
266
+ }
267
+ // Recurse into wrapped transition
268
+ if (args[1] && Node.isCallExpression(args[1])) {
269
+ const nested = extractFromCallExpression(args[1], verbose);
270
+ if (nested) {
271
+ Object.assign(metadata, nested);
272
+ }
273
+ }
274
+ break;
275
+
276
+ case 'invoke':
277
+ // Args: (service, implementation)
278
+ if (args[0]) {
279
+ const service = parseInvokeService(args[0]);
280
+ if (Object.keys(service).length > 0) {
281
+ metadata.invoke = service;
282
+ }
283
+ }
284
+ break;
285
+
286
+ case 'action':
287
+ // Args: (action, transition)
288
+ if (args[0]) {
289
+ const actionMeta = parseObjectLiteral(args[0]);
290
+ if (Object.keys(actionMeta).length > 0) {
291
+ metadata.actions = [actionMeta];
292
+ }
293
+ }
294
+ // Recurse into wrapped transition
295
+ if (args[1] && Node.isCallExpression(args[1])) {
296
+ const nested = extractFromCallExpression(args[1], verbose);
297
+ if (nested) {
298
+ Object.assign(metadata, nested);
299
+ }
300
+ }
301
+ break;
302
+
303
+ default:
304
+ // Not a DSL primitive we recognize
305
+ return null;
306
+ }
307
+
308
+ return Object.keys(metadata).length > 0 ? metadata : null;
309
+ }
310
+
311
+ /**
312
+ * Extracts metadata by parsing the AST of DSL primitive calls.
313
+ * This is the new approach that solves the generic type parameter resolution problem.
63
314
  *
64
- * @param type - The type of a class member (e.g., a transition method).
315
+ * @param member - The class member (property declaration) to analyze
316
+ * @param verbose - Enable debug logging
65
317
  * @returns The extracted metadata object, or `null` if no metadata is found.
66
318
  */
67
- function extractMetaFromType(type: Type): any | null {
68
- // The META_KEY is escaped because it's a unique symbol, not a plain string property.
69
- const escapedKey = String(ts.escapeLeadingUnderscores(META_KEY.description!));
70
- const metaProperty = type.getProperty(escapedKey);
71
- if (!metaProperty) return null;
319
+ function extractMetaFromMember(member: Node, verbose = false): any | null {
320
+ // Only process property declarations (methods with initializers)
321
+ if (!Node.isPropertyDeclaration(member)) {
322
+ if (verbose) console.error(` ⚠️ Not a property declaration`);
323
+ return null;
324
+ }
325
+
326
+ const initializer = member.getInitializer();
327
+ if (!initializer) {
328
+ if (verbose) console.error(` ⚠️ No initializer`);
329
+ return null;
330
+ }
331
+
332
+ // Check if it's a call expression (DSL primitive call)
333
+ if (!Node.isCallExpression(initializer)) {
334
+ if (verbose) console.error(` ⚠️ Initializer is not a call expression`);
335
+ return null;
336
+ }
337
+
338
+ // Extract metadata by parsing the call chain
339
+ const metadata = extractFromCallExpression(initializer, verbose);
72
340
 
73
- const declaration = metaProperty.getValueDeclaration();
74
- if (!declaration) return null;
341
+ if (metadata && verbose) {
342
+ console.error(` ✅ Extracted metadata:`, JSON.stringify(metadata, null, 2));
343
+ }
75
344
 
76
- return typeToJson(declaration.getType());
345
+ return metadata;
77
346
  }
78
347
 
79
348
  /**
@@ -81,22 +350,41 @@ function extractMetaFromType(type: Type): any | null {
81
350
  * building a state node definition for the final statechart.
82
351
  *
83
352
  * @param classSymbol - The `ts-morph` Symbol for the class to analyze.
353
+ * @param verbose - Enable verbose logging
84
354
  * @returns A state node object (e.g., `{ on: {...}, invoke: [...] }`).
85
355
  */
86
- function analyzeStateNode(classSymbol: TSSymbol): object {
356
+ function analyzeStateNode(classSymbol: any, verbose = false): object {
87
357
  const chartNode: any = { on: {} };
88
358
  const classDeclaration = classSymbol.getDeclarations()[0];
89
359
  if (!classDeclaration || !Node.isClassDeclaration(classDeclaration)) {
360
+ if (verbose) {
361
+ console.error(`⚠️ Warning: Could not get class declaration for ${classSymbol.getName()}`);
362
+ }
90
363
  return chartNode;
91
364
  }
92
365
 
366
+ const className = classSymbol.getName();
367
+ if (verbose) {
368
+ console.error(` Analyzing state: ${className}`);
369
+ }
370
+
93
371
  for (const member of classDeclaration.getInstanceMembers()) {
94
- const meta = extractMetaFromType(member.getType());
372
+ const memberName = member.getName();
373
+ if (verbose) {
374
+ console.error(` Checking member: ${memberName}`);
375
+ }
376
+
377
+ // NEW: Use AST-based extraction instead of type-based
378
+ const meta = extractMetaFromMember(member, verbose);
95
379
  if (!meta) continue;
96
380
 
381
+ if (verbose) {
382
+ console.error(` Found transition: ${memberName}`);
383
+ }
384
+
97
385
  // Separate `invoke` metadata from standard `on` transitions, as it's a
98
386
  // special property of a state node in XState/Stately syntax.
99
- const { invoke, ...onEntry } = meta;
387
+ const { invoke, actions, guards, ...onEntry } = meta;
100
388
 
101
389
  if (invoke) {
102
390
  if (!chartNode.invoke) chartNode.invoke = [];
@@ -106,15 +394,40 @@ function analyzeStateNode(classSymbol: TSSymbol): object {
106
394
  onError: { target: invoke.onError },
107
395
  description: invoke.description,
108
396
  });
397
+ if (verbose) {
398
+ console.error(` → Invoke: ${invoke.src}`);
399
+ }
109
400
  }
110
401
 
111
402
  // If there's a target, it's a standard event transition.
112
403
  if (onEntry.target) {
113
- if (onEntry.guards) {
114
- // Stately/XState syntax for guards is the `cond` property.
115
- onEntry.cond = onEntry.guards.map((g: any) => g.name).join(' && ');
404
+ const transition: any = { target: onEntry.target };
405
+
406
+ // Add description if present
407
+ if (onEntry.description) {
408
+ transition.description = onEntry.description;
409
+ }
410
+
411
+ // Add guards as 'cond' property
412
+ if (guards) {
413
+ transition.cond = guards.map((g: any) => g.name).join(' && ');
414
+ if (verbose) {
415
+ console.error(` → Guard: ${transition.cond}`);
416
+ }
417
+ }
418
+
419
+ // Add actions array
420
+ if (actions && actions.length > 0) {
421
+ transition.actions = actions.map((a: any) => a.name);
422
+ if (verbose) {
423
+ console.error(` → Actions: ${transition.actions.join(', ')}`);
424
+ }
425
+ }
426
+
427
+ chartNode.on[memberName] = transition;
428
+ if (verbose) {
429
+ console.error(` → Target: ${onEntry.target}`);
116
430
  }
117
- chartNode.on[member.getName()] = onEntry;
118
431
  }
119
432
  }
120
433
 
@@ -126,62 +439,134 @@ function analyzeStateNode(classSymbol: TSSymbol): object {
126
439
  // =============================================================================
127
440
 
128
441
  /**
129
- * The main analysis function.
130
- * Configures the project, specifies which files and classes to analyze,
131
- * and orchestrates the generation of the final JSON chart to standard output.
442
+ * Extracts a single machine configuration to a statechart
443
+ *
444
+ * @param config - Machine configuration
445
+ * @param project - ts-morph Project instance
446
+ * @param verbose - Enable verbose logging
447
+ * @returns The generated statechart object
132
448
  */
133
- export function generateChart() {
134
- // --- 🎨 CONFIGURATION 🎨 ---
135
- // Adjust these settings to match your project structure.
136
-
137
- /** The relative path to the file containing your machine class definitions. */
138
- const sourceFilePath = "src/authMachine.ts";
139
-
140
- /** An array of the string names of all classes that represent a state. */
141
- const classesToAnalyze = [
142
- "LoggedOutMachine",
143
- "LoggedInMachine",
144
- ];
145
-
146
- /** The top-level ID for your statechart. */
147
- const chartId = "auth";
148
-
149
- /** The string name of the class that represents the initial state. */
150
- const initialState = "LoggedOutMachine";
151
-
152
- // --- End Configuration ---
153
-
154
- console.error("🔍 Analyzing state machine from:", sourceFilePath);
155
-
156
- const project = new Project();
157
- project.addSourceFilesAtPaths("src/**/*.ts");
449
+ export function extractMachine(
450
+ config: MachineConfig,
451
+ project: Project,
452
+ verbose = false
453
+ ): any {
454
+ if (verbose) {
455
+ console.error(`\n🔍 Analyzing machine: ${config.id}`);
456
+ console.error(` Source: ${config.input}`);
457
+ }
158
458
 
159
- const sourceFile = project.getSourceFile(sourceFilePath);
459
+ const sourceFile = project.getSourceFile(config.input);
160
460
  if (!sourceFile) {
161
- console.error(`❌ Error: Source file not found at '${sourceFilePath}'.`);
162
- process.exit(1);
461
+ throw new Error(`Source file not found: ${config.input}`);
163
462
  }
164
463
 
165
464
  const fullChart: any = {
166
- id: chartId,
167
- initial: initialState,
465
+ id: config.id,
466
+ initial: config.initialState,
168
467
  states: {},
169
468
  };
170
469
 
171
- for (const className of classesToAnalyze) {
470
+ if (config.description) {
471
+ fullChart.description = config.description;
472
+ }
473
+
474
+ for (const className of config.classes) {
172
475
  const classDeclaration = sourceFile.getClass(className);
173
476
  if (!classDeclaration) {
174
- console.warn(`⚠️ Warning: Class '${className}' not found in '${sourceFilePath}'. Skipping.`);
477
+ console.warn(`⚠️ Warning: Class '${className}' not found in '${config.input}'. Skipping.`);
175
478
  continue;
176
479
  }
177
480
  const classSymbol = classDeclaration.getSymbolOrThrow();
178
- const stateNode = analyzeStateNode(classSymbol);
481
+ const stateNode = analyzeStateNode(classSymbol, verbose);
179
482
  fullChart.states[className] = stateNode;
180
483
  }
181
484
 
182
- console.error("✅ Analysis complete. Generating JSON chart...");
183
- // Print the final JSON to stdout so it can be piped to a file.
184
- console.log(JSON.stringify(fullChart, null, 2));
485
+ if (verbose) {
486
+ console.error(` Extracted ${config.classes.length} states`);
487
+ }
488
+
489
+ return fullChart;
490
+ }
491
+
492
+ /**
493
+ * Extracts multiple machines based on configuration
494
+ *
495
+ * @param config - Full extraction configuration
496
+ * @returns Array of generated statecharts
497
+ */
498
+ export function extractMachines(config: ExtractionConfig): any[] {
499
+ const verbose = config.verbose ?? false;
500
+
501
+ if (verbose) {
502
+ console.error(`\n📊 Starting statechart extraction`);
503
+ console.error(` Machines to extract: ${config.machines.length}`);
504
+ }
505
+
506
+ const project = new Project();
507
+ project.addSourceFilesAtPaths("src/**/*.ts");
508
+ project.addSourceFilesAtPaths("examples/**/*.ts");
509
+
510
+ const results: any[] = [];
511
+
512
+ for (const machineConfig of config.machines) {
513
+ try {
514
+ const chart = extractMachine(machineConfig, project, verbose);
515
+ results.push(chart);
516
+ } catch (error) {
517
+ console.error(`❌ Error extracting machine '${machineConfig.id}':`, error);
518
+ if (!verbose) {
519
+ console.error(` Run with --verbose for more details`);
520
+ }
521
+ }
522
+ }
523
+
524
+ if (verbose) {
525
+ console.error(`\n✅ Extraction complete: ${results.length}/${config.machines.length} machines extracted`);
526
+ }
527
+
528
+ return results;
529
+ }
530
+
531
+ /**
532
+ * Legacy function for backwards compatibility
533
+ * Extracts a single hardcoded machine configuration
534
+ * @deprecated Use extractMachine or extractMachines instead
535
+ */
536
+ export function generateChart() {
537
+ // --- 🎨 CONFIGURATION 🎨 ---
538
+ // Adjust these settings to match your project structure.
539
+
540
+ const config: MachineConfig = {
541
+ input: "examples/authMachine.ts",
542
+ classes: [
543
+ "LoggedOutMachine",
544
+ "LoggingInMachine",
545
+ "LoggedInMachine",
546
+ "SessionExpiredMachine",
547
+ "ErrorMachine"
548
+ ],
549
+ id: "auth",
550
+ initialState: "LoggedOutMachine",
551
+ description: "Authentication state machine"
552
+ };
553
+
554
+ // --- End Configuration ---
555
+
556
+ console.error("🔍 Using legacy generateChart function");
557
+ console.error("⚠️ Consider using extractMachines() with a config file instead\n");
558
+
559
+ const project = new Project();
560
+ project.addSourceFilesAtPaths("src/**/*.ts");
561
+ project.addSourceFilesAtPaths("examples/**/*.ts");
562
+
563
+ try {
564
+ const chart = extractMachine(config, project, true);
565
+ console.log(JSON.stringify(chart, null, 2));
566
+ } catch (error) {
567
+ console.error(`❌ Error:`, error);
568
+ process.exit(1);
569
+ }
185
570
  }
186
571
 
187
572
  // This allows the script to be executed directly from the command line.
package/src/index.ts CHANGED
@@ -526,3 +526,52 @@ export {
526
526
  runAsync,
527
527
  stepAsync
528
528
  } from './generators';
529
+
530
+ // =============================================================================
531
+ // SECTION: TYPE-LEVEL METADATA PRIMITIVES
532
+ // =============================================================================
533
+
534
+ export {
535
+ transitionTo,
536
+ describe,
537
+ guarded,
538
+ invoke,
539
+ action,
540
+ metadata,
541
+ META_KEY,
542
+ type TransitionMeta,
543
+ type GuardMeta,
544
+ type InvokeMeta,
545
+ type ActionMeta,
546
+ type ClassConstructor,
547
+ type WithMeta
548
+ } from './primitives';
549
+
550
+ // =============================================================================
551
+ // SECTION: STATECHART EXTRACTION
552
+ // =============================================================================
553
+
554
+ export {
555
+ extractMachine,
556
+ extractMachines,
557
+ generateChart,
558
+ type MachineConfig,
559
+ type ExtractionConfig
560
+ } from './extract';
561
+
562
+ // =============================================================================
563
+ // SECTION: RUNTIME EXTRACTION
564
+ // =============================================================================
565
+
566
+ export {
567
+ extractFunctionMetadata,
568
+ extractStateNode,
569
+ generateStatechart,
570
+ extractFromInstance
571
+ } from './runtime-extract';
572
+
573
+ // Export runtime metadata symbol and type (for advanced use)
574
+ export { RUNTIME_META, type RuntimeTransitionMeta } from './primitives';
575
+
576
+
577
+ export * from './multi'