@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,89 @@
1
+ /**
2
+ * State Machine Types — browser-side workflow interpretation.
3
+ *
4
+ * Mirrors the backend PureWorkflowDefinition/Instance structure
5
+ * but stripped to only what the browser engine needs.
6
+ */
7
+
8
+ import type { ExpressionContext } from '../expression/types';
9
+
10
+ /** State type in the workflow */
11
+ export type StateType = 'START' | 'REGULAR' | 'END' | 'CANCELLED';
12
+
13
+ /** Action that runs during on_event handling */
14
+ export interface PlayerAction {
15
+ type: string;
16
+ config: Record<string, unknown>;
17
+ condition?: string;
18
+ }
19
+
20
+ /** On-event subscription for browser-side processing */
21
+ export interface PlayerOnEventSubscription {
22
+ match: string; // Topic pattern with wildcards
23
+ conditions?: string[];
24
+ actions: PlayerAction[];
25
+ }
26
+
27
+ /** State definition for the browser engine */
28
+ export interface PlayerStateDefinition {
29
+ name: string;
30
+ type: StateType;
31
+ on_enter?: PlayerAction[];
32
+ on_exit?: PlayerAction[];
33
+ on_event?: PlayerOnEventSubscription[];
34
+ }
35
+
36
+ /** Transition definition */
37
+ export interface PlayerTransitionDefinition {
38
+ name: string;
39
+ from: string[];
40
+ to: string;
41
+ conditions?: string[];
42
+ auto?: boolean;
43
+ actions?: PlayerAction[];
44
+ }
45
+
46
+ /** Workflow definition for the browser engine */
47
+ export interface PlayerWorkflowDefinition {
48
+ id: string;
49
+ slug: string;
50
+ states: PlayerStateDefinition[];
51
+ transitions: PlayerTransitionDefinition[];
52
+ }
53
+
54
+ /** Runtime instance state */
55
+ export interface PlayerInstance {
56
+ definition: PlayerWorkflowDefinition;
57
+ current_state: string;
58
+ state_data: Record<string, unknown>;
59
+ memory: Record<string, unknown>;
60
+ status: 'ACTIVE' | 'COMPLETED' | 'CANCELLED';
61
+ }
62
+
63
+ /** Transition result from the state machine */
64
+ export interface TransitionResult {
65
+ success: boolean;
66
+ from_state: string;
67
+ to_state: string;
68
+ actions_executed: PlayerAction[];
69
+ error?: string;
70
+ }
71
+
72
+ /** State machine event listener */
73
+ export type StateMachineListener = (event: StateMachineEvent) => void;
74
+
75
+ /** Events emitted by the state machine */
76
+ export interface StateMachineEvent {
77
+ type: 'transition' | 'state_enter' | 'state_exit' | 'action_executed' | 'error';
78
+ instance_id: string;
79
+ from_state?: string;
80
+ to_state?: string;
81
+ action?: PlayerAction;
82
+ error?: string;
83
+ }
84
+
85
+ /** Action handler registered with the state machine */
86
+ export type ActionHandler = (
87
+ action: PlayerAction,
88
+ context: ExpressionContext,
89
+ ) => void | Promise<void>;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Action Trace — records all workflow actions during execution.
3
+ *
4
+ * Captures a chronological trace of every transition, action, timer,
5
+ * and query that occurs during a workflow session. This enables:
6
+ *
7
+ * 1. Replay debugging (step through recorded actions)
8
+ * 2. Test assertions on action sequences
9
+ * 3. Performance profiling (timing of each tick)
10
+ * 4. Compliance auditing (complete action log)
11
+ *
12
+ * Usage:
13
+ * const recorder = createActionRecorder();
14
+ * // ... hook into workflow runtime ...
15
+ * const trace = recorder.getTrace();
16
+ * expect(trace.ticks).toHaveLength(3);
17
+ * expect(trace.ticks[0].kind).toBe('transition');
18
+ */
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ /**
25
+ * A complete action trace for a workflow session.
26
+ */
27
+ export interface ActionTrace {
28
+ /** Trace format version. */
29
+ version: 1;
30
+ /** Workflow definition slug. */
31
+ workflowSlug: string;
32
+ /** Instance ID (if tracking a specific instance). */
33
+ instanceId?: string;
34
+ /** When the trace started. */
35
+ startedAt: string;
36
+ /** When the trace ended (if finalized). */
37
+ endedAt?: string;
38
+ /** Ordered list of action ticks. */
39
+ ticks: ActionTick[];
40
+ }
41
+
42
+ /**
43
+ * A single action tick in the trace.
44
+ */
45
+ export interface ActionTick {
46
+ /** Monotonically increasing tick number. */
47
+ tick: number;
48
+ /** ISO timestamp of the tick. */
49
+ timestamp: string;
50
+ /** Kind of action. */
51
+ kind: 'transition' | 'action' | 'timer' | 'query' | 'mutation' | 'event' | 'error';
52
+ /** Name/identifier of the action. */
53
+ name: string;
54
+ /** Duration in milliseconds (if measurable). */
55
+ durationMs?: number;
56
+ /** Workflow state before this tick. */
57
+ fromState?: string;
58
+ /** Workflow state after this tick. */
59
+ toState?: string;
60
+ /** Detailed information about the tick. */
61
+ detail: Record<string, unknown>;
62
+ }
63
+
64
+ // =============================================================================
65
+ // Recorder
66
+ // =============================================================================
67
+
68
+ /**
69
+ * Action recorder — accumulates ticks into a trace.
70
+ */
71
+ export interface ActionRecorder {
72
+ /** Record a transition. */
73
+ recordTransition: (name: string, fromState: string, toState: string, detail?: Record<string, unknown>) => void;
74
+ /** Record an action execution. */
75
+ recordAction: (name: string, detail?: Record<string, unknown>) => void;
76
+ /** Record a timer event. */
77
+ recordTimer: (name: string, detail?: Record<string, unknown>) => void;
78
+ /** Record a data query. */
79
+ recordQuery: (name: string, detail?: Record<string, unknown>) => void;
80
+ /** Record a mutation. */
81
+ recordMutation: (name: string, detail?: Record<string, unknown>) => void;
82
+ /** Record an event. */
83
+ recordEvent: (name: string, detail?: Record<string, unknown>) => void;
84
+ /** Record an error. */
85
+ recordError: (name: string, detail?: Record<string, unknown>) => void;
86
+ /** Get the current trace. */
87
+ getTrace: () => ActionTrace;
88
+ /** Finalize the trace (sets endedAt). */
89
+ finalize: () => ActionTrace;
90
+ /** Reset the recorder. */
91
+ reset: () => void;
92
+ }
93
+
94
+ /**
95
+ * Creates a new action recorder.
96
+ */
97
+ export function createActionRecorder(
98
+ workflowSlug: string,
99
+ instanceId?: string
100
+ ): ActionRecorder {
101
+ let tickCounter = 0;
102
+ let ticks: ActionTick[] = [];
103
+ let startedAt = new Date().toISOString();
104
+ let endedAt: string | undefined;
105
+
106
+ function recordTick(kind: ActionTick['kind'], name: string, extra?: {
107
+ fromState?: string;
108
+ toState?: string;
109
+ durationMs?: number;
110
+ detail?: Record<string, unknown>;
111
+ }): void {
112
+ ticks.push({
113
+ tick: tickCounter++,
114
+ timestamp: new Date().toISOString(),
115
+ kind,
116
+ name,
117
+ fromState: extra?.fromState,
118
+ toState: extra?.toState,
119
+ durationMs: extra?.durationMs,
120
+ detail: extra?.detail || {},
121
+ });
122
+ }
123
+
124
+ return {
125
+ recordTransition(name, fromState, toState, detail) {
126
+ recordTick('transition', name, { fromState, toState, detail });
127
+ },
128
+ recordAction(name, detail) {
129
+ recordTick('action', name, { detail });
130
+ },
131
+ recordTimer(name, detail) {
132
+ recordTick('timer', name, { detail });
133
+ },
134
+ recordQuery(name, detail) {
135
+ recordTick('query', name, { detail });
136
+ },
137
+ recordMutation(name, detail) {
138
+ recordTick('mutation', name, { detail });
139
+ },
140
+ recordEvent(name, detail) {
141
+ recordTick('event', name, { detail });
142
+ },
143
+ recordError(name, detail) {
144
+ recordTick('error', name, { detail });
145
+ },
146
+ getTrace() {
147
+ return {
148
+ version: 1,
149
+ workflowSlug,
150
+ instanceId,
151
+ startedAt,
152
+ endedAt,
153
+ ticks: [...ticks],
154
+ };
155
+ },
156
+ finalize() {
157
+ endedAt = new Date().toISOString();
158
+ return this.getTrace();
159
+ },
160
+ reset() {
161
+ tickCounter = 0;
162
+ ticks = [];
163
+ startedAt = new Date().toISOString();
164
+ endedAt = undefined;
165
+ },
166
+ };
167
+ }
168
+
169
+ // =============================================================================
170
+ // Analysis utilities
171
+ // =============================================================================
172
+
173
+ /**
174
+ * Filters a trace to only transition ticks.
175
+ */
176
+ export function getTransitionPath(trace: ActionTrace): string[] {
177
+ return trace.ticks
178
+ .filter((t) => t.kind === 'transition')
179
+ .map((t) => `${t.fromState} → ${t.toState}`);
180
+ }
181
+
182
+ /**
183
+ * Gets the final state from a trace.
184
+ */
185
+ export function getFinalState(trace: ActionTrace): string | undefined {
186
+ const transitions = trace.ticks.filter((t) => t.kind === 'transition');
187
+ if (transitions.length === 0) return undefined;
188
+ return transitions[transitions.length - 1].toState;
189
+ }
190
+
191
+ /**
192
+ * Counts ticks by kind.
193
+ */
194
+ export function countByKind(trace: ActionTrace): Record<string, number> {
195
+ const counts: Record<string, number> = {};
196
+ for (const tick of trace.ticks) {
197
+ counts[tick.kind] = (counts[tick.kind] || 0) + 1;
198
+ }
199
+ return counts;
200
+ }
201
+
202
+ /**
203
+ * Checks if a trace contains a specific transition.
204
+ */
205
+ export function hasTransition(trace: ActionTrace, from: string, to: string): boolean {
206
+ return trace.ticks.some(
207
+ (t) => t.kind === 'transition' && t.fromState === from && t.toState === to
208
+ );
209
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Blueprint Test Runner — executes test programs AS Blueprints on a Player.
3
+ *
4
+ * The test orchestrator IS a Player. This runner:
5
+ * 1. Compiles TestProgram → PlayerWorkflowDefinition (Blueprint)
6
+ * 2. Creates a StateMachine (Player) with the compiled Blueprint
7
+ * 3. Registers test action handlers (in-process or API)
8
+ * 4. Drives the StateMachine through transitions: next, next, next...
9
+ * 5. Collects results from the action handler state
10
+ *
11
+ * The compiled Blueprint can also be extracted, stored, and executed
12
+ * independently on any Player (browser, iOS, server).
13
+ *
14
+ * Exports:
15
+ * - runBlueprintTestProgram(): Full program execution
16
+ * - runBlueprintScenario(): Single scenario execution
17
+ * - Supports both in-process and API modes via action handler factory
18
+ */
19
+
20
+ import { StateMachine } from '../state-machine/interpreter';
21
+ import { createEvaluator, WEB_FAILURE_POLICIES } from '../expression';
22
+ import { compileTestScenario } from './test-compiler';
23
+ import { createInProcessTestActions, createApiTestActions } from './test-actions';
24
+ import type { TestActionState } from './test-actions';
25
+ import type { PlayerWorkflowDefinition, ActionHandler } from '../state-machine/types';
26
+ import type {
27
+ TestProgram,
28
+ TestScenario,
29
+ TestProgramResult,
30
+ ScenarioResult,
31
+ StepResult,
32
+ ApiTestAdapter,
33
+ } from './types';
34
+
35
+ // =============================================================================
36
+ // Configuration
37
+ // =============================================================================
38
+
39
+ interface BlueprintTestConfig {
40
+ onStepComplete?: (scenarioIdx: number, step: StepResult) => void;
41
+ onScenarioComplete?: (result: ScenarioResult) => void;
42
+ abortSignal?: AbortSignal;
43
+ }
44
+
45
+ interface InProcessConfig extends BlueprintTestConfig {
46
+ mode: 'in-process';
47
+ }
48
+
49
+ interface ApiConfig extends BlueprintTestConfig {
50
+ mode: 'api';
51
+ adapter: ApiTestAdapter;
52
+ settleDelayMs?: number;
53
+ cleanupInstances?: boolean;
54
+ }
55
+
56
+ export type BlueprintRunnerConfig = InProcessConfig | ApiConfig;
57
+
58
+ // =============================================================================
59
+ // Public API
60
+ // =============================================================================
61
+
62
+ /**
63
+ * Execute a complete test program using the Blueprint runner.
64
+ *
65
+ * The test program is compiled to Blueprints and executed on a Player
66
+ * (StateMachine) with the appropriate action handlers for the mode.
67
+ */
68
+ export async function runBlueprintTestProgram(
69
+ program: TestProgram,
70
+ config: BlueprintRunnerConfig,
71
+ ): Promise<TestProgramResult> {
72
+ const start = performance.now();
73
+ const scenarioResults: ScenarioResult[] = [];
74
+ let allPassed = true;
75
+
76
+ for (let i = 0; i < program.scenarios.length; i++) {
77
+ if (config.abortSignal?.aborted) break;
78
+
79
+ const result = await runBlueprintScenario(
80
+ program.scenarios[i],
81
+ program.definitions,
82
+ i,
83
+ config,
84
+ );
85
+ scenarioResults.push(result);
86
+ if (!result.passed) allPassed = false;
87
+ config.onScenarioComplete?.(result);
88
+ }
89
+
90
+ return {
91
+ passed: allPassed,
92
+ scenarioResults,
93
+ durationMs: performance.now() - start,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Execute a single scenario as a Blueprint on a Player.
99
+ */
100
+ export async function runBlueprintScenario(
101
+ scenario: TestScenario,
102
+ definitions: Record<string, PlayerWorkflowDefinition>,
103
+ scenarioIndex: number,
104
+ config: BlueprintRunnerConfig,
105
+ ): Promise<ScenarioResult> {
106
+ const start = performance.now();
107
+
108
+ try {
109
+ // 1. Compile the scenario to a Blueprint
110
+ const blueprint = compileTestScenario(scenario, definitions, scenarioIndex);
111
+
112
+ // 2. Create action handlers based on execution mode
113
+ const { handlers, state: testState } = createActionHandlers(definitions, config);
114
+
115
+ // 3. Create the Player (StateMachine) with the compiled Blueprint
116
+ const evaluator = createEvaluator({
117
+ functions: [],
118
+ failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION,
119
+ });
120
+
121
+ const sm = new StateMachine(blueprint, {}, { evaluator, actionHandlers: handlers });
122
+
123
+ // Bind the orchestrator reference so handlers can set fields
124
+ testState.setOrchestratorField = (field: string, value: unknown) => {
125
+ sm.setField(field, value);
126
+ };
127
+
128
+ // 4. Drive the Player through the test steps
129
+ // SM starts in __idle. Transition 'setup' → __setup (creates instances).
130
+ // Then 'next' for each step.
131
+ const stepCount = scenario.steps.length;
132
+
133
+ // Setup: __idle → __setup (fires on_enter which creates test instances)
134
+ const setupResult = await sm.transition('setup');
135
+ if (!setupResult.success) {
136
+ return {
137
+ scenarioName: scenario.name,
138
+ passed: false,
139
+ stepResults: [],
140
+ durationMs: performance.now() - start,
141
+ error: `Setup failed: ${setupResult.error}`,
142
+ };
143
+ }
144
+
145
+ for (let i = 0; i < stepCount; i++) {
146
+ if (config.abortSignal?.aborted) break;
147
+
148
+ const result = await sm.transition('next');
149
+ if (!result.success) {
150
+ // Transition itself failed — record as step error
151
+ testState.stepResults.push({
152
+ stepName: scenario.steps[i]?.name ?? `step_${i}`,
153
+ passed: false,
154
+ assertionResults: [],
155
+ durationMs: 0,
156
+ error: `Blueprint transition failed: ${result.error}`,
157
+ });
158
+ testState.failed = true;
159
+ break;
160
+ }
161
+
162
+ // Report step completion
163
+ const lastStep = testState.stepResults[testState.stepResults.length - 1];
164
+ if (lastStep) {
165
+ config.onStepComplete?.(scenarioIndex, lastStep);
166
+ }
167
+
168
+ // Check if step failed
169
+ if (sm.stateData.__test_step_failed) {
170
+ await sm.transition('abort');
171
+ break;
172
+ }
173
+ }
174
+
175
+ // 5. Advance to terminal state if all steps passed
176
+ if (!testState.failed && !config.abortSignal?.aborted) {
177
+ await sm.transition('next'); // → __pass
178
+ }
179
+
180
+ const passed = !testState.failed;
181
+
182
+ return {
183
+ scenarioName: scenario.name,
184
+ passed,
185
+ stepResults: testState.stepResults,
186
+ durationMs: performance.now() - start,
187
+ };
188
+ } catch (error) {
189
+ return {
190
+ scenarioName: scenario.name,
191
+ passed: false,
192
+ stepResults: [],
193
+ durationMs: performance.now() - start,
194
+ error: error instanceof Error ? error.message : String(error),
195
+ };
196
+ }
197
+ }
198
+
199
+ // =============================================================================
200
+ // Internals
201
+ // =============================================================================
202
+
203
+ function createActionHandlers(
204
+ definitions: Record<string, PlayerWorkflowDefinition>,
205
+ config: BlueprintRunnerConfig,
206
+ ): { handlers: Map<string, ActionHandler>; state: TestActionState } {
207
+ if (config.mode === 'api') {
208
+ return createApiTestActions(config.adapter, {
209
+ settleDelayMs: config.settleDelayMs,
210
+ });
211
+ }
212
+
213
+ return createInProcessTestActions(definitions);
214
+ }