@mmapp/player-core 0.1.0-alpha.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 (63) hide show
  1. package/dist/index.d.mts +1436 -0
  2. package/dist/index.d.ts +1436 -0
  3. package/dist/index.js +4828 -0
  4. package/dist/index.mjs +4762 -0
  5. package/package.json +35 -0
  6. package/package.json.backup +35 -0
  7. package/src/__tests__/actions.test.ts +187 -0
  8. package/src/__tests__/blueprint-e2e.test.ts +706 -0
  9. package/src/__tests__/blueprint-test-runner.test.ts +680 -0
  10. package/src/__tests__/core-functions.test.ts +78 -0
  11. package/src/__tests__/dsl-compiler.test.ts +1382 -0
  12. package/src/__tests__/dsl-grammar.test.ts +1682 -0
  13. package/src/__tests__/events.test.ts +200 -0
  14. package/src/__tests__/expression.test.ts +296 -0
  15. package/src/__tests__/failure-policies.test.ts +110 -0
  16. package/src/__tests__/frontend-context.test.ts +182 -0
  17. package/src/__tests__/integration.test.ts +256 -0
  18. package/src/__tests__/security.test.ts +190 -0
  19. package/src/__tests__/state-machine.test.ts +450 -0
  20. package/src/__tests__/testing-engine.test.ts +671 -0
  21. package/src/actions/dispatcher.ts +80 -0
  22. package/src/actions/index.ts +7 -0
  23. package/src/actions/types.ts +25 -0
  24. package/src/dsl/compiler/component-mapper.ts +289 -0
  25. package/src/dsl/compiler/field-mapper.ts +187 -0
  26. package/src/dsl/compiler/index.ts +82 -0
  27. package/src/dsl/compiler/manifest-compiler.ts +76 -0
  28. package/src/dsl/compiler/symbol-table.ts +214 -0
  29. package/src/dsl/compiler/utils.ts +48 -0
  30. package/src/dsl/compiler/view-compiler.ts +286 -0
  31. package/src/dsl/compiler/workflow-compiler.ts +600 -0
  32. package/src/dsl/index.ts +66 -0
  33. package/src/dsl/ir-migration.ts +221 -0
  34. package/src/dsl/ir-types.ts +416 -0
  35. package/src/dsl/lexer.ts +579 -0
  36. package/src/dsl/parser.ts +115 -0
  37. package/src/dsl/types.ts +256 -0
  38. package/src/events/event-bus.ts +68 -0
  39. package/src/events/index.ts +9 -0
  40. package/src/events/pattern-matcher.ts +61 -0
  41. package/src/events/types.ts +27 -0
  42. package/src/expression/evaluator.ts +676 -0
  43. package/src/expression/functions.ts +214 -0
  44. package/src/expression/index.ts +13 -0
  45. package/src/expression/types.ts +64 -0
  46. package/src/index.ts +61 -0
  47. package/src/state-machine/index.ts +16 -0
  48. package/src/state-machine/interpreter.ts +319 -0
  49. package/src/state-machine/types.ts +89 -0
  50. package/src/testing/action-trace.ts +209 -0
  51. package/src/testing/blueprint-test-runner.ts +214 -0
  52. package/src/testing/graph-walker.ts +249 -0
  53. package/src/testing/index.ts +69 -0
  54. package/src/testing/nrt-comparator.ts +199 -0
  55. package/src/testing/nrt-types.ts +230 -0
  56. package/src/testing/test-actions.ts +645 -0
  57. package/src/testing/test-compiler.ts +278 -0
  58. package/src/testing/test-runner.ts +444 -0
  59. package/src/testing/types.ts +231 -0
  60. package/src/validation/definition-validator.ts +812 -0
  61. package/src/validation/index.ts +13 -0
  62. package/tsconfig.json +26 -0
  63. package/vitest.config.ts +8 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Core Expression Functions — 31 functions across 6 categories.
3
+ *
4
+ * These are the standard functions available in every player context.
5
+ * Platform-specific functions ($domain, $fe_ref, etc.) are registered
6
+ * separately by the host environment.
7
+ */
8
+
9
+ import type { ExpressionFunction } from './types';
10
+
11
+ // =============================================================================
12
+ // Math (8 functions)
13
+ // =============================================================================
14
+
15
+ const add: ExpressionFunction = {
16
+ name: 'add', fn: (a: unknown, b: unknown) => Number(a) + Number(b), arity: 2,
17
+ };
18
+ const subtract: ExpressionFunction = {
19
+ name: 'subtract', fn: (a: unknown, b: unknown) => Number(a) - Number(b), arity: 2,
20
+ };
21
+ const multiply: ExpressionFunction = {
22
+ name: 'multiply', fn: (a: unknown, b: unknown) => Number(a) * Number(b), arity: 2,
23
+ };
24
+ const divide: ExpressionFunction = {
25
+ name: 'divide', fn: (a: unknown, b: unknown) => {
26
+ const d = Number(b);
27
+ return d === 0 ? 0 : Number(a) / d;
28
+ }, arity: 2,
29
+ };
30
+ const abs: ExpressionFunction = {
31
+ name: 'abs', fn: (a: unknown) => Math.abs(Number(a)), arity: 1,
32
+ };
33
+ const round: ExpressionFunction = {
34
+ name: 'round', fn: (a: unknown, decimals?: unknown) => {
35
+ const d = decimals != null ? Number(decimals) : 0;
36
+ const factor = Math.pow(10, d);
37
+ return Math.round(Number(a) * factor) / factor;
38
+ }, arity: -1,
39
+ };
40
+ const min: ExpressionFunction = {
41
+ name: 'min', fn: (...args: unknown[]) => {
42
+ const nums = args.flat().map(Number).filter(n => !isNaN(n));
43
+ return nums.length === 0 ? 0 : Math.min(...nums);
44
+ }, arity: -1,
45
+ };
46
+ const max: ExpressionFunction = {
47
+ name: 'max', fn: (...args: unknown[]) => {
48
+ const nums = args.flat().map(Number).filter(n => !isNaN(n));
49
+ return nums.length === 0 ? 0 : Math.max(...nums);
50
+ }, arity: -1,
51
+ };
52
+
53
+ // =============================================================================
54
+ // Comparison (6 functions)
55
+ // =============================================================================
56
+
57
+ const eq: ExpressionFunction = {
58
+ name: 'eq', fn: (a: unknown, b: unknown) => a === b || String(a) === String(b), arity: 2,
59
+ };
60
+ const neq: ExpressionFunction = {
61
+ name: 'neq', fn: (a: unknown, b: unknown) => a !== b && String(a) !== String(b), arity: 2,
62
+ };
63
+ const gt: ExpressionFunction = {
64
+ name: 'gt', fn: (a: unknown, b: unknown) => Number(a) > Number(b), arity: 2,
65
+ };
66
+ const gte: ExpressionFunction = {
67
+ name: 'gte', fn: (a: unknown, b: unknown) => Number(a) >= Number(b), arity: 2,
68
+ };
69
+ const lt: ExpressionFunction = {
70
+ name: 'lt', fn: (a: unknown, b: unknown) => Number(a) < Number(b), arity: 2,
71
+ };
72
+ const lte: ExpressionFunction = {
73
+ name: 'lte', fn: (a: unknown, b: unknown) => Number(a) <= Number(b), arity: 2,
74
+ };
75
+
76
+ // =============================================================================
77
+ // Logic (5 functions)
78
+ // =============================================================================
79
+
80
+ const if_fn: ExpressionFunction = {
81
+ name: 'if', fn: (cond: unknown, then: unknown, else_: unknown) => cond ? then : else_, arity: 3,
82
+ };
83
+ const and: ExpressionFunction = {
84
+ name: 'and', fn: (...args: unknown[]) => args.every(Boolean), arity: -1,
85
+ };
86
+ const or: ExpressionFunction = {
87
+ name: 'or', fn: (...args: unknown[]) => args.some(Boolean), arity: -1,
88
+ };
89
+ const not: ExpressionFunction = {
90
+ name: 'not', fn: (a: unknown) => !a, arity: 1,
91
+ };
92
+ const coalesce: ExpressionFunction = {
93
+ name: 'coalesce', fn: (...args: unknown[]) => {
94
+ for (const arg of args) {
95
+ if (arg != null) return arg;
96
+ }
97
+ return null;
98
+ }, arity: -1,
99
+ };
100
+
101
+ // =============================================================================
102
+ // String (6 functions)
103
+ // =============================================================================
104
+
105
+ const concat: ExpressionFunction = {
106
+ name: 'concat', fn: (...args: unknown[]) => args.map(String).join(''), arity: -1,
107
+ };
108
+ const upper: ExpressionFunction = {
109
+ name: 'upper', fn: (s: unknown) => String(s ?? '').toUpperCase(), arity: 1,
110
+ };
111
+ const lower: ExpressionFunction = {
112
+ name: 'lower', fn: (s: unknown) => String(s ?? '').toLowerCase(), arity: 1,
113
+ };
114
+ const trim: ExpressionFunction = {
115
+ name: 'trim', fn: (s: unknown) => String(s ?? '').trim(), arity: 1,
116
+ };
117
+ const format: ExpressionFunction = {
118
+ name: 'format', fn: (template: unknown, ...args: unknown[]) => {
119
+ let result = String(template ?? '');
120
+ args.forEach((arg, i) => {
121
+ result = result.replace(`{${i}}`, String(arg ?? ''));
122
+ });
123
+ return result;
124
+ }, arity: -1,
125
+ };
126
+ const length: ExpressionFunction = {
127
+ name: 'length', fn: (v: unknown) => {
128
+ if (Array.isArray(v)) return v.length;
129
+ if (typeof v === 'string') return v.length;
130
+ if (v && typeof v === 'object') return Object.keys(v).length;
131
+ return 0;
132
+ }, arity: 1,
133
+ };
134
+
135
+ // =============================================================================
136
+ // Path (3 functions)
137
+ // =============================================================================
138
+
139
+ const get: ExpressionFunction = {
140
+ name: 'get', fn: (obj: unknown, path: unknown) => {
141
+ if (obj == null || typeof path !== 'string') return undefined;
142
+ const parts = path.split('.');
143
+ let current: unknown = obj;
144
+ for (const part of parts) {
145
+ if (current == null || typeof current !== 'object') return undefined;
146
+ current = (current as Record<string, unknown>)[part];
147
+ }
148
+ return current;
149
+ }, arity: 2,
150
+ };
151
+ const includes: ExpressionFunction = {
152
+ name: 'includes', fn: (collection: unknown, value: unknown) => {
153
+ if (Array.isArray(collection)) return collection.includes(value);
154
+ if (typeof collection === 'string') return collection.includes(String(value));
155
+ return false;
156
+ }, arity: 2,
157
+ };
158
+ const is_defined: ExpressionFunction = {
159
+ name: 'is_defined', fn: (v: unknown) => v !== undefined && v !== null, arity: 1,
160
+ };
161
+
162
+ // =============================================================================
163
+ // Type (3 functions)
164
+ // =============================================================================
165
+
166
+ const is_empty: ExpressionFunction = {
167
+ name: 'is_empty', fn: (v: unknown) => {
168
+ if (v == null) return true;
169
+ if (typeof v === 'string') return v.length === 0;
170
+ if (Array.isArray(v)) return v.length === 0;
171
+ if (typeof v === 'object') return Object.keys(v).length === 0;
172
+ return false;
173
+ }, arity: 1,
174
+ };
175
+ const is_null: ExpressionFunction = {
176
+ name: 'is_null', fn: (v: unknown) => v === null || v === undefined, arity: 1,
177
+ };
178
+ const to_string: ExpressionFunction = {
179
+ name: 'to_string', fn: (v: unknown) => {
180
+ if (v == null) return '';
181
+ if (typeof v === 'object') return JSON.stringify(v);
182
+ return String(v);
183
+ }, arity: 1,
184
+ };
185
+
186
+ // =============================================================================
187
+ // Export all 31 core functions
188
+ // =============================================================================
189
+
190
+ export const CORE_FUNCTIONS: ExpressionFunction[] = [
191
+ // Math (8)
192
+ add, subtract, multiply, divide, abs, round, min, max,
193
+ // Comparison (6)
194
+ eq, neq, gt, gte, lt, lte,
195
+ // Logic (5)
196
+ if_fn, and, or, not, coalesce,
197
+ // String (6)
198
+ concat, upper, lower, trim, format, length,
199
+ // Path (3)
200
+ get, includes, is_defined,
201
+ // Type (3)
202
+ is_empty, is_null, to_string,
203
+ ];
204
+
205
+ /** Lookup map for O(1) function resolution */
206
+ export function buildFunctionMap(
207
+ functions: ExpressionFunction[],
208
+ ): Map<string, ExpressionFunction['fn']> {
209
+ const map = new Map<string, ExpressionFunction['fn']>();
210
+ for (const fn of functions) {
211
+ map.set(fn.name, fn.fn);
212
+ }
213
+ return map;
214
+ }
@@ -0,0 +1,13 @@
1
+ export type {
2
+ ExpressionResult,
3
+ ExpressionDiagnostics,
4
+ FailurePolicy,
5
+ ExpressionContext,
6
+ ExpressionFunction,
7
+ EvaluatorConfig,
8
+ Evaluator,
9
+ FailurePolicyContext,
10
+ } from './types';
11
+
12
+ export { CORE_FUNCTIONS, buildFunctionMap } from './functions';
13
+ export { createEvaluator, WEB_FAILURE_POLICIES, clearExpressionCache } from './evaluator';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Expression Engine Types
3
+ *
4
+ * Defines the contract for expression evaluation across all player contexts.
5
+ * See player-specification.md §2.3 for full specification.
6
+ */
7
+
8
+ /** Result of evaluating an expression */
9
+ export interface ExpressionResult<T = unknown> {
10
+ value: T;
11
+ status: 'ok' | 'error' | 'fallback';
12
+ error?: string;
13
+ diagnostics?: ExpressionDiagnostics;
14
+ }
15
+
16
+ /** Diagnostic information for debugging expressions */
17
+ export interface ExpressionDiagnostics {
18
+ expression: string;
19
+ duration_ms: number;
20
+ resolved_paths: string[];
21
+ unresolved_paths: string[];
22
+ }
23
+
24
+ /** Failure policy determines behavior when an expression fails */
25
+ export interface FailurePolicy {
26
+ on_error: 'return_fallback' | 'throw' | 'log_and_skip';
27
+ fallback_value: unknown;
28
+ log_level: 'silent' | 'warn' | 'error';
29
+ }
30
+
31
+ /** Expression context for evaluation */
32
+ export interface ExpressionContext {
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ /** A registered function available in expressions */
37
+ export interface ExpressionFunction {
38
+ name: string;
39
+ fn: (...args: unknown[]) => unknown;
40
+ arity?: number; // -1 means variadic
41
+ description?: string;
42
+ }
43
+
44
+ /** Configuration for createEvaluator factory */
45
+ export interface EvaluatorConfig {
46
+ functions: ExpressionFunction[];
47
+ failurePolicy: FailurePolicy;
48
+ maxDepth?: number;
49
+ maxOperations?: number;
50
+ }
51
+
52
+ /** The evaluator interface returned by createEvaluator */
53
+ export interface Evaluator {
54
+ evaluate<T = unknown>(expression: string, context: ExpressionContext): ExpressionResult<T>;
55
+ evaluateTemplate(template: string, context: ExpressionContext): ExpressionResult<string>;
56
+ validate(expression: string): { valid: boolean; errors: string[] };
57
+ }
58
+
59
+ /** Pre-defined failure policy contexts for the Web Player */
60
+ export type FailurePolicyContext =
61
+ | 'VIEW_BINDING'
62
+ | 'EVENT_REACTION'
63
+ | 'DURING_ACTION'
64
+ | 'CONDITIONAL_VISIBILITY';
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @mindmatrix/player-core — Framework-agnostic browser-side workflow engine.
3
+ *
4
+ * Modules:
5
+ * - expression: Expression evaluation with 31 core functions
6
+ * - state-machine: Workflow state machine interpreter
7
+ * - events: Pub/sub event bus with glob pattern matching
8
+ * - actions: Action dispatcher with handler registry
9
+ * - dsl: DSL compiler pipeline
10
+ */
11
+
12
+ export * from './expression';
13
+ export * from './state-machine';
14
+ export * from './events';
15
+ export * from './actions';
16
+ export * from './testing';
17
+ export * from './validation';
18
+
19
+ export { compile } from './dsl/compiler';
20
+ export type {
21
+ CompilationResult,
22
+ IRWorkflowDefinition,
23
+ CompilerError,
24
+ CompilerErrorCode,
25
+ IRStateDefinition,
26
+ IRTransitionDefinition,
27
+ IRFieldDefinition,
28
+ IRFieldValidation,
29
+ IRActionDefinition,
30
+ IRDuringAction,
31
+ IROnEventSubscription,
32
+ IROnEventAction,
33
+ IRConditionDefinition,
34
+ IRRoleDefinition,
35
+ IRExperienceNode,
36
+ IRExperienceDefinition,
37
+ IRBlueprintManifest,
38
+ IRDataSource,
39
+ IRWorkflowDataSource,
40
+ IRApiDataSource,
41
+ IRRefDataSource,
42
+ IRStaticDataSource,
43
+ IRGrammarIsland,
44
+ IRWorkflowFieldType,
45
+ IRStateType,
46
+ IRActionMode,
47
+ IRStateHome,
48
+ RuntimeProfile,
49
+ PureFormWorkflow,
50
+ CompiledOutput,
51
+ } from './dsl/ir-types';
52
+ export { normalizeCategory } from './dsl/ir-types';
53
+
54
+ // IR Migration
55
+ export {
56
+ normalizeDefinition,
57
+ detectIRVersion,
58
+ needsMigration,
59
+ CURRENT_IR_VERSION,
60
+ } from './dsl/ir-migration';
61
+ export type { MigrationResult } from './dsl/ir-migration';
@@ -0,0 +1,16 @@
1
+ export type {
2
+ StateType,
3
+ PlayerAction,
4
+ PlayerOnEventSubscription,
5
+ PlayerStateDefinition,
6
+ PlayerTransitionDefinition,
7
+ PlayerWorkflowDefinition,
8
+ PlayerInstance,
9
+ TransitionResult,
10
+ StateMachineListener,
11
+ StateMachineEvent,
12
+ ActionHandler,
13
+ } from './types';
14
+
15
+ export { StateMachine } from './interpreter';
16
+ export type { StateMachineConfig } from './interpreter';
@@ -0,0 +1,319 @@
1
+ /**
2
+ * State Machine Interpreter — browser-side workflow engine.
3
+ *
4
+ * Interprets PlayerWorkflowDefinition instances with:
5
+ * - State transitions (manual + auto)
6
+ * - on_enter / on_exit action execution
7
+ * - Condition evaluation via expression engine
8
+ * - Listener-based event emission
9
+ * - Auto-transition drain loop with depth limit
10
+ */
11
+
12
+ import type { Evaluator, ExpressionContext } from '../expression/types';
13
+ import type {
14
+ PlayerWorkflowDefinition,
15
+ PlayerInstance,
16
+ PlayerStateDefinition,
17
+ PlayerTransitionDefinition,
18
+ TransitionResult,
19
+ StateMachineListener,
20
+ StateMachineEvent,
21
+ ActionHandler,
22
+ PlayerAction,
23
+ } from './types';
24
+
25
+ const MAX_AUTO_CHAIN = 10;
26
+
27
+ export interface StateMachineConfig {
28
+ evaluator: Evaluator;
29
+ actionHandlers?: Map<string, ActionHandler>;
30
+ }
31
+
32
+ export class StateMachine {
33
+ private readonly evaluator: Evaluator;
34
+ private readonly actionHandlers: Map<string, ActionHandler>;
35
+ private readonly listeners: Set<StateMachineListener> = new Set();
36
+ private instance: PlayerInstance;
37
+
38
+ constructor(
39
+ definition: PlayerWorkflowDefinition,
40
+ initialData: Record<string, unknown> = {},
41
+ config: StateMachineConfig,
42
+ ) {
43
+ this.evaluator = config.evaluator;
44
+ this.actionHandlers = config.actionHandlers ?? new Map();
45
+
46
+ const startState = definition.states.find(s => s.type === 'START');
47
+ if (!startState) {
48
+ throw new Error(`No START state found in definition ${definition.slug}`);
49
+ }
50
+
51
+ this.instance = {
52
+ definition,
53
+ current_state: startState.name,
54
+ state_data: { ...initialData },
55
+ memory: {},
56
+ status: 'ACTIVE',
57
+ };
58
+ }
59
+
60
+ /** Get the current instance snapshot (immutable copy) */
61
+ getSnapshot(): Readonly<PlayerInstance> {
62
+ return { ...this.instance, state_data: { ...this.instance.state_data }, memory: { ...this.instance.memory } };
63
+ }
64
+
65
+ /** Get current state name */
66
+ get currentState(): string {
67
+ return this.instance.current_state;
68
+ }
69
+
70
+ /** Get current state_data */
71
+ get stateData(): Record<string, unknown> {
72
+ return this.instance.state_data;
73
+ }
74
+
75
+ /** Get current status */
76
+ get status(): string {
77
+ return this.instance.status;
78
+ }
79
+
80
+ /** Subscribe to state machine events */
81
+ on(listener: StateMachineListener): () => void {
82
+ this.listeners.add(listener);
83
+ return () => this.listeners.delete(listener);
84
+ }
85
+
86
+ /** Register an action handler */
87
+ registerAction(type: string, handler: ActionHandler): void {
88
+ this.actionHandlers.set(type, handler);
89
+ }
90
+
91
+ /** Execute a named transition */
92
+ async transition(transitionName: string, data?: Record<string, unknown>): Promise<TransitionResult> {
93
+ if (this.instance.status !== 'ACTIVE') {
94
+ return {
95
+ success: false,
96
+ from_state: this.instance.current_state,
97
+ to_state: this.instance.current_state,
98
+ actions_executed: [],
99
+ error: `Cannot transition: instance status is ${this.instance.status}`,
100
+ };
101
+ }
102
+
103
+ const transition = this.instance.definition.transitions.find(
104
+ t => t.name === transitionName && t.from.includes(this.instance.current_state),
105
+ );
106
+
107
+ if (!transition) {
108
+ return {
109
+ success: false,
110
+ from_state: this.instance.current_state,
111
+ to_state: this.instance.current_state,
112
+ actions_executed: [],
113
+ error: `Transition "${transitionName}" not valid from state "${this.instance.current_state}"`,
114
+ };
115
+ }
116
+
117
+ // Merge transition data
118
+ if (data) {
119
+ this.instance.state_data = { ...this.instance.state_data, ...data };
120
+ }
121
+
122
+ // Check conditions
123
+ if (transition.conditions && transition.conditions.length > 0) {
124
+ const ctx = this.buildContext();
125
+ for (const condition of transition.conditions) {
126
+ const result = this.evaluator.evaluate<boolean>(condition, ctx);
127
+ if (!result.value) {
128
+ return {
129
+ success: false,
130
+ from_state: this.instance.current_state,
131
+ to_state: this.instance.current_state,
132
+ actions_executed: [],
133
+ error: `Transition condition not met: ${condition}`,
134
+ };
135
+ }
136
+ }
137
+ }
138
+
139
+ const result = await this.executeTransition(transition);
140
+
141
+ // Drain auto-transitions
142
+ if (result.success) {
143
+ await this.drainAutoTransitions();
144
+ }
145
+
146
+ return result;
147
+ }
148
+
149
+ /** Update state_data directly (for on_event set_field actions) */
150
+ setField(field: string, value: unknown): void {
151
+ this.instance.state_data = { ...this.instance.state_data, [field]: value };
152
+ }
153
+
154
+ /** Update memory */
155
+ setMemory(key: string, value: unknown): void {
156
+ this.instance.memory = { ...this.instance.memory, [key]: value };
157
+ }
158
+
159
+ /** Get available transitions from the current state */
160
+ getAvailableTransitions(): PlayerTransitionDefinition[] {
161
+ return this.instance.definition.transitions.filter(
162
+ t => t.from.includes(this.instance.current_state) && !t.auto,
163
+ );
164
+ }
165
+
166
+ /** Get the current state definition */
167
+ getCurrentStateDefinition(): PlayerStateDefinition | undefined {
168
+ return this.instance.definition.states.find(s => s.name === this.instance.current_state);
169
+ }
170
+
171
+ // ===========================================================================
172
+ // Private implementation
173
+ // ===========================================================================
174
+
175
+ private async executeTransition(transition: PlayerTransitionDefinition): Promise<TransitionResult> {
176
+ const fromState = this.instance.current_state;
177
+ const allActionsExecuted: PlayerAction[] = [];
178
+
179
+ // Phase 1: on_exit actions
180
+ const fromStateDef = this.getCurrentStateDefinition();
181
+ if (fromStateDef?.on_exit) {
182
+ await this.executeActions(fromStateDef.on_exit, allActionsExecuted);
183
+ }
184
+
185
+ this.emit({
186
+ type: 'state_exit',
187
+ instance_id: this.instance.definition.id,
188
+ from_state: fromState,
189
+ });
190
+
191
+ // Phase 2: transition actions
192
+ if (transition.actions) {
193
+ await this.executeActions(transition.actions, allActionsExecuted);
194
+ }
195
+
196
+ // Phase 3: update state
197
+ this.instance.current_state = transition.to;
198
+
199
+ // Phase 4: check terminal state
200
+ const toStateDef = this.instance.definition.states.find(s => s.name === transition.to);
201
+ if (toStateDef?.type === 'END') {
202
+ this.instance.status = 'COMPLETED';
203
+ } else if (toStateDef?.type === 'CANCELLED') {
204
+ this.instance.status = 'CANCELLED';
205
+ }
206
+
207
+ this.emit({
208
+ type: 'state_enter',
209
+ instance_id: this.instance.definition.id,
210
+ to_state: transition.to,
211
+ });
212
+
213
+ // Phase 5: on_enter actions
214
+ if (toStateDef?.on_enter) {
215
+ await this.executeActions(toStateDef.on_enter, allActionsExecuted);
216
+ }
217
+
218
+ this.emit({
219
+ type: 'transition',
220
+ instance_id: this.instance.definition.id,
221
+ from_state: fromState,
222
+ to_state: transition.to,
223
+ });
224
+
225
+ return {
226
+ success: true,
227
+ from_state: fromState,
228
+ to_state: transition.to,
229
+ actions_executed: allActionsExecuted,
230
+ };
231
+ }
232
+
233
+ private async drainAutoTransitions(): Promise<void> {
234
+ for (let depth = 0; depth < MAX_AUTO_CHAIN; depth++) {
235
+ if (this.instance.status !== 'ACTIVE') break;
236
+
237
+ const autoTransition = this.findMatchingAutoTransition();
238
+ if (!autoTransition) break;
239
+
240
+ const result = await this.executeTransition(autoTransition);
241
+ if (!result.success) break;
242
+ }
243
+ }
244
+
245
+ private findMatchingAutoTransition(): PlayerTransitionDefinition | null {
246
+ const candidates = this.instance.definition.transitions.filter(
247
+ t => t.auto && t.from.includes(this.instance.current_state),
248
+ );
249
+
250
+ const ctx = this.buildContext();
251
+ for (const candidate of candidates) {
252
+ if (!candidate.conditions || candidate.conditions.length === 0) {
253
+ return candidate;
254
+ }
255
+
256
+ const allMet = candidate.conditions.every(condition => {
257
+ const result = this.evaluator.evaluate<boolean>(condition, ctx);
258
+ return result.value === true;
259
+ });
260
+
261
+ if (allMet) return candidate;
262
+ }
263
+
264
+ return null;
265
+ }
266
+
267
+ private async executeActions(actions: PlayerAction[], collector: PlayerAction[]): Promise<void> {
268
+ const ctx = this.buildContext();
269
+
270
+ for (const action of actions) {
271
+ // Check per-action condition
272
+ if (action.condition) {
273
+ const condResult = this.evaluator.evaluate<boolean>(action.condition, ctx);
274
+ if (!condResult.value) continue;
275
+ }
276
+
277
+ const handler = this.actionHandlers.get(action.type);
278
+ if (handler) {
279
+ try {
280
+ await handler(action, ctx);
281
+ collector.push(action);
282
+ this.emit({
283
+ type: 'action_executed',
284
+ instance_id: this.instance.definition.id,
285
+ action,
286
+ });
287
+ } catch (error) {
288
+ this.emit({
289
+ type: 'error',
290
+ instance_id: this.instance.definition.id,
291
+ action,
292
+ error: error instanceof Error ? error.message : String(error),
293
+ });
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ private buildContext(): ExpressionContext {
300
+ return {
301
+ state_data: this.instance.state_data,
302
+ memory: this.instance.memory,
303
+ current_state: this.instance.current_state,
304
+ status: this.instance.status,
305
+ // Spread state_data for direct field access (e.g., "title" instead of "state_data.title")
306
+ ...this.instance.state_data,
307
+ };
308
+ }
309
+
310
+ private emit(event: StateMachineEvent): void {
311
+ for (const listener of this.listeners) {
312
+ try {
313
+ listener(event);
314
+ } catch {
315
+ // Listener errors don't break the state machine
316
+ }
317
+ }
318
+ }
319
+ }