@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,249 @@
1
+ /**
2
+ * Graph Walker — static analysis of workflow definitions.
3
+ *
4
+ * Treats a PlayerWorkflowDefinition as a directed graph and provides:
5
+ * - analyzeDefinition(): Full graph analysis (states, edges, paths, cycles)
6
+ * - generateCoverageScenarios(): Auto-generate test scenarios from paths
7
+ *
8
+ * Pure functions, zero dependencies beyond player-core types.
9
+ */
10
+
11
+ import type { PlayerWorkflowDefinition } from '../state-machine/types';
12
+ import type {
13
+ AnalysisResult,
14
+ StateNode,
15
+ TransitionEdge,
16
+ GraphPath,
17
+ TestScenario,
18
+ TestStep,
19
+ } from './types';
20
+
21
+ const MAX_PATHS = 1000;
22
+
23
+ /**
24
+ * Analyze a workflow definition as a directed graph.
25
+ * Returns all states, edges, terminal paths, cycles, unreachable/dead-end states.
26
+ */
27
+ export function analyzeDefinition(def: PlayerWorkflowDefinition): AnalysisResult {
28
+ // Build state nodes
29
+ const states: StateNode[] = def.states.map(s => ({
30
+ name: s.name,
31
+ type: s.type,
32
+ reachable: false, // Will be set during DFS
33
+ hasOnEvent: Boolean(s.on_event?.length),
34
+ hasOnEnter: Boolean(s.on_enter?.length),
35
+ hasOnExit: Boolean(s.on_exit?.length),
36
+ }));
37
+
38
+ // Build edges (expand multi-from transitions)
39
+ const edges: TransitionEdge[] = [];
40
+ for (const t of def.transitions) {
41
+ for (const from of t.from) {
42
+ edges.push({
43
+ name: t.name,
44
+ from,
45
+ to: t.to,
46
+ auto: Boolean(t.auto),
47
+ hasConditions: Boolean(t.conditions?.length),
48
+ });
49
+ }
50
+ }
51
+
52
+ // Build adjacency list
53
+ const adjacency = new Map<string, TransitionEdge[]>();
54
+ for (const s of def.states) {
55
+ adjacency.set(s.name, []);
56
+ }
57
+ for (const e of edges) {
58
+ adjacency.get(e.from)?.push(e);
59
+ }
60
+
61
+ // Find start state
62
+ const startState = def.states.find(s => s.type === 'START');
63
+ if (!startState) {
64
+ return {
65
+ states,
66
+ edges,
67
+ terminalPaths: [],
68
+ cycles: [],
69
+ unreachableStates: states.map(s => s.name),
70
+ deadEndStates: [],
71
+ summary: {
72
+ totalStates: states.length,
73
+ totalTransitions: edges.length,
74
+ reachableStates: 0,
75
+ terminalPaths: 0,
76
+ cycles: 0,
77
+ unreachableStates: states.length,
78
+ deadEndStates: 0,
79
+ },
80
+ };
81
+ }
82
+
83
+ // Terminal state names
84
+ const terminalStates = new Set(
85
+ def.states.filter(s => s.type === 'END' || s.type === 'CANCELLED').map(s => s.name),
86
+ );
87
+
88
+ // DFS to find all paths and cycles
89
+ const terminalPaths: GraphPath[] = [];
90
+ const cycles: GraphPath[] = [];
91
+ const visited = new Set<string>();
92
+
93
+ function dfs(
94
+ state: string,
95
+ pathStates: string[],
96
+ pathTransitions: string[],
97
+ pathSet: Set<string>,
98
+ ): void {
99
+ if (terminalPaths.length + cycles.length >= MAX_PATHS) return;
100
+
101
+ // Mark as reachable
102
+ const stateNode = states.find(s => s.name === state);
103
+ if (stateNode) stateNode.reachable = true;
104
+ visited.add(state);
105
+
106
+ // Terminal state → record path (but still check outgoing edges for cycles)
107
+ const isTerminal = terminalStates.has(state);
108
+ if (isTerminal) {
109
+ terminalPaths.push({
110
+ states: [...pathStates],
111
+ transitions: [...pathTransitions],
112
+ });
113
+ }
114
+
115
+ const outEdges = adjacency.get(state) ?? [];
116
+
117
+ for (const edge of outEdges) {
118
+ if (terminalPaths.length + cycles.length >= MAX_PATHS) return;
119
+
120
+ if (pathSet.has(edge.to)) {
121
+ // Cycle detected
122
+ const cycleStart = pathStates.indexOf(edge.to);
123
+ cycles.push({
124
+ states: [...pathStates.slice(cycleStart), edge.to],
125
+ transitions: [...pathTransitions.slice(cycleStart), edge.name],
126
+ });
127
+ continue;
128
+ }
129
+
130
+ pathStates.push(edge.to);
131
+ pathTransitions.push(edge.name);
132
+ pathSet.add(edge.to);
133
+
134
+ dfs(edge.to, pathStates, pathTransitions, pathSet);
135
+
136
+ pathStates.pop();
137
+ pathTransitions.pop();
138
+ pathSet.delete(edge.to);
139
+ }
140
+ }
141
+
142
+ dfs(startState.name, [startState.name], [], new Set([startState.name]));
143
+
144
+ // Identify unreachable states
145
+ const unreachableStates = states
146
+ .filter(s => !s.reachable)
147
+ .map(s => s.name);
148
+
149
+ // Identify dead-end states: non-terminal, reachable, with no outgoing transitions
150
+ const deadEndStates = states
151
+ .filter(s => {
152
+ if (!s.reachable) return false;
153
+ if (terminalStates.has(s.name)) return false;
154
+ const outEdges = adjacency.get(s.name) ?? [];
155
+ return outEdges.length === 0;
156
+ })
157
+ .map(s => s.name);
158
+
159
+ // Deduplicate cycles by string key
160
+ const uniqueCycles: GraphPath[] = [];
161
+ const seenCycleKeys = new Set<string>();
162
+ for (const cycle of cycles) {
163
+ const key = cycle.states.join('→');
164
+ if (!seenCycleKeys.has(key)) {
165
+ seenCycleKeys.add(key);
166
+ uniqueCycles.push(cycle);
167
+ }
168
+ }
169
+
170
+ return {
171
+ states,
172
+ edges,
173
+ terminalPaths,
174
+ cycles: uniqueCycles,
175
+ unreachableStates,
176
+ deadEndStates,
177
+ summary: {
178
+ totalStates: states.length,
179
+ totalTransitions: edges.length,
180
+ reachableStates: states.filter(s => s.reachable).length,
181
+ terminalPaths: terminalPaths.length,
182
+ cycles: uniqueCycles.length,
183
+ unreachableStates: unreachableStates.length,
184
+ deadEndStates: deadEndStates.length,
185
+ },
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Generate one test scenario per terminal path in the definition.
191
+ * Each step = one transition, asserting the landing state.
192
+ */
193
+ export function generateCoverageScenarios(
194
+ def: PlayerWorkflowDefinition,
195
+ analysis?: AnalysisResult,
196
+ ): TestScenario[] {
197
+ const resolved = analysis ?? analyzeDefinition(def);
198
+ const scenarios: TestScenario[] = [];
199
+
200
+ for (const path of resolved.terminalPaths) {
201
+ const pathLabel = path.states.join(' → ');
202
+ const steps: TestStep[] = [];
203
+
204
+ for (let i = 0; i < path.transitions.length; i++) {
205
+ const transitionName = path.transitions[i];
206
+ const expectedState = path.states[i + 1];
207
+
208
+ // Check if this is a terminal state
209
+ const stateDef = def.states.find(s => s.name === expectedState);
210
+ const isTerminal = stateDef?.type === 'END' || stateDef?.type === 'CANCELLED';
211
+ const expectedStatus = stateDef?.type === 'END'
212
+ ? 'COMPLETED'
213
+ : stateDef?.type === 'CANCELLED'
214
+ ? 'CANCELLED'
215
+ : 'ACTIVE';
216
+
217
+ steps.push({
218
+ name: `${transitionName} → ${expectedState}`,
219
+ action: { type: 'transition', name: transitionName },
220
+ assertions: [
221
+ {
222
+ target: 'state',
223
+ operator: 'eq',
224
+ expected: expectedState,
225
+ label: `State should be "${expectedState}"`,
226
+ },
227
+ ...(isTerminal
228
+ ? [
229
+ {
230
+ target: 'status' as const,
231
+ operator: 'eq' as const,
232
+ expected: expectedStatus,
233
+ label: `Status should be "${expectedStatus}"`,
234
+ },
235
+ ]
236
+ : []),
237
+ ],
238
+ });
239
+ }
240
+
241
+ scenarios.push({
242
+ name: `Path: ${pathLabel}`,
243
+ tags: ['auto-generated', 'coverage'],
244
+ steps,
245
+ });
246
+ }
247
+
248
+ return scenarios;
249
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Testing OS — test declarations and execution for player-core.
3
+ *
4
+ * Three layers:
5
+ * 1. Graph walker: static analysis + coverage generation
6
+ * 2. In-process runner: direct StateMachine execution (legacy, still works)
7
+ * 3. Blueprint runner: test IS a Blueprint, runs on any Player
8
+ * - Compiler: TestProgram → PlayerWorkflowDefinition
9
+ * - Action handlers: pluggable (in-process, API, cross-platform)
10
+ * - Runner: thin executor that drives the Player
11
+ */
12
+
13
+ // Types
14
+ export type {
15
+ // Static analysis
16
+ StateNode,
17
+ TransitionEdge,
18
+ GraphPath,
19
+ AnalysisSummary,
20
+ AnalysisResult,
21
+ // Test program
22
+ TestProgram,
23
+ TestInstance,
24
+ TestScenario,
25
+ TestStep,
26
+ TestAssertion,
27
+ // Results
28
+ AssertionResult,
29
+ StepResult,
30
+ ScenarioResult,
31
+ TestProgramResult,
32
+ // In-process config
33
+ TestRunnerConfig,
34
+ // API adapter + config
35
+ ApiTestAdapter,
36
+ ApiTestRunnerConfig,
37
+ ApiInstanceSnapshot,
38
+ ApiTransitionResult,
39
+ } from './types';
40
+
41
+ // Graph walker
42
+ export { analyzeDefinition, generateCoverageScenarios } from './graph-walker';
43
+
44
+ // In-process runner (direct StateMachine, no Blueprint compilation)
45
+ export { runTestProgram, runScenario } from './test-runner';
46
+
47
+ // Blueprint runner (test IS a Blueprint, runs on any Player)
48
+ export { compileTestScenario, compileTestProgram } from './test-compiler';
49
+ export { createInProcessTestActions, createApiTestActions } from './test-actions';
50
+ export type { TestActionState } from './test-actions';
51
+ export { runBlueprintTestProgram, runBlueprintScenario } from './blueprint-test-runner';
52
+ export type { BlueprintRunnerConfig } from './blueprint-test-runner';
53
+
54
+ // NRT (Normalized Render Trace)
55
+ export type {
56
+ NRT,
57
+ NRTNode,
58
+ NRTEvent,
59
+ NRTEventTarget,
60
+ NRTNavState,
61
+ NRTDiff,
62
+ NRTChange,
63
+ } from './nrt-types';
64
+ export { createEmptyNRT, findNode, findVisibleNodes, findInteractiveNodes, countNodes } from './nrt-types';
65
+ export { compareNRT } from './nrt-comparator';
66
+
67
+ // Action Trace
68
+ export type { ActionTrace, ActionTick, ActionRecorder } from './action-trace';
69
+ export { createActionRecorder, getTransitionPath, getFinalState, countByKind, hasTransition } from './action-trace';
@@ -0,0 +1,199 @@
1
+ /**
2
+ * NRT Comparator — semantic diff between two NRT snapshots.
3
+ *
4
+ * Compares structure, text, visibility, and events while ignoring
5
+ * platform-specific details like classNames, styles, and layout metrics.
6
+ *
7
+ * Usage:
8
+ * const diff = compareNRT(before, after);
9
+ * if (!diff.equal) {
10
+ * console.log('Changes:', diff.changes);
11
+ * }
12
+ */
13
+
14
+ import type { NRT, NRTNode, NRTDiff, NRTChange } from './nrt-types';
15
+
16
+ // =============================================================================
17
+ // Comparison
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Compares two NRT snapshots and returns a semantic diff.
22
+ */
23
+ export function compareNRT(before: NRT, after: NRT): NRTDiff {
24
+ const changes: NRTChange[] = [];
25
+
26
+ // Compare workflow state
27
+ if (before.workflowState !== after.workflowState) {
28
+ changes.push({
29
+ type: 'modified',
30
+ path: 'workflowState',
31
+ field: 'workflowState',
32
+ oldValue: before.workflowState,
33
+ newValue: after.workflowState,
34
+ });
35
+ }
36
+
37
+ // Compare navigation state
38
+ if (before.nav?.routeState !== after.nav?.routeState) {
39
+ changes.push({
40
+ type: 'modified',
41
+ path: 'nav.routeState',
42
+ field: 'routeState',
43
+ oldValue: before.nav?.routeState,
44
+ newValue: after.nav?.routeState,
45
+ });
46
+ }
47
+
48
+ // Compare node trees
49
+ compareNodes(before.root, after.root, 'root', changes);
50
+
51
+ return {
52
+ equal: changes.length === 0,
53
+ changes,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Recursively compares two NRT nodes.
59
+ */
60
+ function compareNodes(
61
+ before: NRTNode | undefined,
62
+ after: NRTNode | undefined,
63
+ path: string,
64
+ changes: NRTChange[]
65
+ ): void {
66
+ // Node added
67
+ if (!before && after) {
68
+ changes.push({
69
+ type: 'added',
70
+ path,
71
+ nodeId: after.id,
72
+ field: 'node',
73
+ newValue: summarizeNode(after),
74
+ });
75
+ return;
76
+ }
77
+
78
+ // Node removed
79
+ if (before && !after) {
80
+ changes.push({
81
+ type: 'removed',
82
+ path,
83
+ nodeId: before.id,
84
+ field: 'node',
85
+ oldValue: summarizeNode(before),
86
+ });
87
+ return;
88
+ }
89
+
90
+ if (!before || !after) return;
91
+
92
+ // Compare semantic fields (ignore platform-specific details)
93
+ if (before.type !== after.type) {
94
+ changes.push({
95
+ type: 'modified',
96
+ path,
97
+ nodeId: after.id,
98
+ field: 'type',
99
+ oldValue: before.type,
100
+ newValue: after.type,
101
+ });
102
+ }
103
+
104
+ if (before.text !== after.text) {
105
+ changes.push({
106
+ type: 'modified',
107
+ path,
108
+ nodeId: after.id,
109
+ field: 'text',
110
+ oldValue: before.text,
111
+ newValue: after.text,
112
+ });
113
+ }
114
+
115
+ if (before.visible !== after.visible) {
116
+ changes.push({
117
+ type: 'modified',
118
+ path,
119
+ nodeId: after.id,
120
+ field: 'visible',
121
+ oldValue: before.visible,
122
+ newValue: after.visible,
123
+ });
124
+ }
125
+
126
+ if (before.enabled !== after.enabled) {
127
+ changes.push({
128
+ type: 'modified',
129
+ path,
130
+ nodeId: after.id,
131
+ field: 'enabled',
132
+ oldValue: before.enabled,
133
+ newValue: after.enabled,
134
+ });
135
+ }
136
+
137
+ if (JSON.stringify(before.value) !== JSON.stringify(after.value)) {
138
+ changes.push({
139
+ type: 'modified',
140
+ path,
141
+ nodeId: after.id,
142
+ field: 'value',
143
+ oldValue: before.value,
144
+ newValue: after.value,
145
+ });
146
+ }
147
+
148
+ // Compare events
149
+ const beforeEvents = Object.keys(before.events || {});
150
+ const afterEvents = Object.keys(after.events || {});
151
+
152
+ for (const event of afterEvents) {
153
+ if (!beforeEvents.includes(event)) {
154
+ changes.push({
155
+ type: 'added',
156
+ path: `${path}.events`,
157
+ nodeId: after.id,
158
+ field: event,
159
+ newValue: after.events![event],
160
+ });
161
+ }
162
+ }
163
+
164
+ for (const event of beforeEvents) {
165
+ if (!afterEvents.includes(event)) {
166
+ changes.push({
167
+ type: 'removed',
168
+ path: `${path}.events`,
169
+ nodeId: before.id,
170
+ field: event,
171
+ oldValue: before.events![event],
172
+ });
173
+ }
174
+ }
175
+
176
+ // Compare children
177
+ const maxChildren = Math.max(before.children.length, after.children.length);
178
+ for (let i = 0; i < maxChildren; i++) {
179
+ compareNodes(
180
+ before.children[i],
181
+ after.children[i],
182
+ `${path}.children[${i}]`,
183
+ changes
184
+ );
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Creates a brief summary of a node for diff output.
190
+ */
191
+ function summarizeNode(node: NRTNode): Record<string, unknown> {
192
+ return {
193
+ id: node.id,
194
+ type: node.type,
195
+ text: node.text,
196
+ visible: node.visible,
197
+ childCount: node.children.length,
198
+ };
199
+ }