@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,76 @@
1
+ /**
2
+ * Manifest Compiler — Pass 4 of compilation.
3
+ *
4
+ * Transforms space declaration → IRBlueprintManifest.
5
+ */
6
+
7
+ import type { IRBlueprintManifest, CompilerError } from '../ir-types';
8
+ import type { SymbolTable } from './symbol-table';
9
+ import { slugify } from './utils';
10
+
11
+ // =============================================================================
12
+ // Public API
13
+ // =============================================================================
14
+
15
+ export function compileManifest(
16
+ symbols: SymbolTable,
17
+ ): { manifest?: IRBlueprintManifest; errors: CompilerError[] } {
18
+ const errors: CompilerError[] = [];
19
+
20
+ if (!symbols.space) {
21
+ return { manifest: undefined, errors };
22
+ }
23
+
24
+ const space = symbols.space;
25
+
26
+ // Workflows from thing refs
27
+ const workflows = space.thingRefs.map(ref => ({
28
+ slug: slugify(ref.name),
29
+ role: (ref.kind ?? 'primary') as 'primary' | 'child' | 'derived' | 'utility',
30
+ }));
31
+
32
+ // Experience ID from space name
33
+ const experience_id = slugify(space.name);
34
+
35
+ // Routes from paths
36
+ const routes = space.paths.map(p => {
37
+ const route: {
38
+ path: string;
39
+ node: string;
40
+ entityType?: string;
41
+ entityIdSource?: 'user' | 'param';
42
+ } = {
43
+ path: p.path,
44
+ node: slugify(p.view),
45
+ };
46
+
47
+ if (p.context) {
48
+ route.entityType = p.context;
49
+ route.entityIdSource = inferEntityIdSource(p.path, p.context);
50
+ }
51
+
52
+ return route;
53
+ });
54
+
55
+ return {
56
+ manifest: {
57
+ workflows,
58
+ experience_id,
59
+ routes: routes.length > 0 ? routes : undefined,
60
+ },
61
+ errors,
62
+ };
63
+ }
64
+
65
+ // =============================================================================
66
+ // Entity ID Source Inference
67
+ // =============================================================================
68
+
69
+ function inferEntityIdSource(
70
+ path: string,
71
+ context: string,
72
+ ): 'user' | 'param' {
73
+ if (context === 'user') return 'user';
74
+ if (path.includes(':id')) return 'param';
75
+ return 'user';
76
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Symbol Table — Pass 1 of compilation.
3
+ *
4
+ * Walks all root-level AST nodes and collects declared names:
5
+ * space, things (workflows), fragments, and views.
6
+ */
7
+
8
+ import type { ASTNode } from '../types';
9
+
10
+ // =============================================================================
11
+ // Symbol Types
12
+ // =============================================================================
13
+
14
+ export interface SpaceSymbol {
15
+ name: string;
16
+ version: string;
17
+ node: ASTNode;
18
+ thingRefs: Array<{ name: string; kind?: string }>;
19
+ paths: Array<{ path: string; view: string; context?: string }>;
20
+ tags: string[];
21
+ }
22
+
23
+ export interface ThingSymbol {
24
+ name: string;
25
+ version: string;
26
+ node: ASTNode;
27
+ fields: ASTNode[];
28
+ states: ASTNode[];
29
+ startsAt?: string;
30
+ tags: string[];
31
+ levels: ASTNode[];
32
+ }
33
+
34
+ export interface FragmentSymbol {
35
+ name: string;
36
+ node: ASTNode;
37
+ }
38
+
39
+ export interface ViewSymbol {
40
+ name: string;
41
+ node: ASTNode;
42
+ }
43
+
44
+ export interface SymbolTable {
45
+ space?: SpaceSymbol;
46
+ things: Map<string, ThingSymbol>;
47
+ fragments: Map<string, FragmentSymbol>;
48
+ views: Map<string, ViewSymbol>;
49
+ }
50
+
51
+ // =============================================================================
52
+ // View-like child types (used to distinguish root views from states)
53
+ // =============================================================================
54
+
55
+ const VIEW_CHILD_TYPES = new Set([
56
+ 'data_source', 'content', 'iteration', 'section',
57
+ 'string_literal', 'search', 'grouping', 'navigation', 'pages',
58
+ 'qualifier',
59
+ ]);
60
+
61
+ // =============================================================================
62
+ // Public API
63
+ // =============================================================================
64
+
65
+ export function collectSymbols(nodes: ASTNode[]): SymbolTable {
66
+ const table: SymbolTable = {
67
+ things: new Map(),
68
+ fragments: new Map(),
69
+ views: new Map(),
70
+ };
71
+
72
+ for (const node of nodes) {
73
+ const { data } = node.token;
74
+
75
+ switch (data.type) {
76
+ case 'space_decl':
77
+ table.space = collectSpace(node);
78
+ break;
79
+
80
+ case 'thing_decl':
81
+ table.things.set(data.name, collectThing(node));
82
+ break;
83
+
84
+ case 'fragment_def':
85
+ table.fragments.set(data.name, { name: data.name, node });
86
+ break;
87
+
88
+ case 'state_decl':
89
+ // Root-level state_decl with view-like children → it's actually a view
90
+ if (hasViewChildren(node)) {
91
+ table.views.set(data.name, { name: data.name, node });
92
+ }
93
+ break;
94
+
95
+ default:
96
+ break;
97
+ }
98
+ }
99
+
100
+ return table;
101
+ }
102
+
103
+ // =============================================================================
104
+ // Space Collection
105
+ // =============================================================================
106
+
107
+ function collectSpace(node: ASTNode): SpaceSymbol {
108
+ const data = node.token.data;
109
+ if (data.type !== 'space_decl') throw new Error('Expected space_decl');
110
+
111
+ const space: SpaceSymbol = {
112
+ name: data.name,
113
+ version: data.version,
114
+ node,
115
+ thingRefs: [],
116
+ paths: [],
117
+ tags: [],
118
+ };
119
+
120
+ for (const child of node.children) {
121
+ const cd = child.token.data;
122
+
123
+ if (cd.type === 'tagged') {
124
+ space.tags.push(...cd.tags);
125
+ } else if (cd.type === 'section' && cd.name === 'things') {
126
+ for (const ref of child.children) {
127
+ if (ref.token.data.type === 'thing_ref') {
128
+ space.thingRefs.push({
129
+ name: ref.token.data.name,
130
+ kind: ref.token.data.kind,
131
+ });
132
+ }
133
+ }
134
+ } else if (cd.type === 'section' && cd.name === 'paths') {
135
+ for (const pm of child.children) {
136
+ if (pm.token.data.type === 'path_mapping') {
137
+ space.paths.push({
138
+ path: pm.token.data.path,
139
+ view: pm.token.data.view,
140
+ context: pm.token.data.context,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ return space;
148
+ }
149
+
150
+ // =============================================================================
151
+ // Thing Collection
152
+ // =============================================================================
153
+
154
+ function collectThing(node: ASTNode): ThingSymbol {
155
+ const data = node.token.data;
156
+ if (data.type !== 'thing_decl') throw new Error('Expected thing_decl');
157
+
158
+ const thing: ThingSymbol = {
159
+ name: data.name,
160
+ version: data.version ?? '1.0.0',
161
+ node,
162
+ fields: [],
163
+ states: [],
164
+ tags: [],
165
+ levels: [],
166
+ };
167
+
168
+ for (const child of node.children) {
169
+ const cd = child.token.data;
170
+
171
+ switch (cd.type) {
172
+ case 'field_def':
173
+ thing.fields.push(child);
174
+ break;
175
+
176
+ case 'state_decl':
177
+ thing.states.push(child);
178
+ break;
179
+
180
+ case 'starts_at':
181
+ thing.startsAt = cd.state;
182
+ break;
183
+
184
+ case 'tagged':
185
+ thing.tags.push(...cd.tags);
186
+ break;
187
+
188
+ case 'section':
189
+ if (cd.name === 'levels') {
190
+ for (const lvl of child.children) {
191
+ if (lvl.token.data.type === 'level_def') {
192
+ thing.levels.push(lvl);
193
+ }
194
+ }
195
+ }
196
+ break;
197
+ }
198
+ }
199
+
200
+ return thing;
201
+ }
202
+
203
+ // =============================================================================
204
+ // Heuristic: Does a root node have view-like children?
205
+ // =============================================================================
206
+
207
+ function hasViewChildren(node: ASTNode): boolean {
208
+ for (const child of node.children) {
209
+ if (VIEW_CHILD_TYPES.has(child.token.data.type)) {
210
+ return true;
211
+ }
212
+ }
213
+ return false;
214
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Compiler Utilities — pure string transformation helpers.
3
+ */
4
+
5
+ /**
6
+ * Convert a name to kebab-case slug.
7
+ * "project management" → "project-management"
8
+ * "user stats" → "user-stats"
9
+ */
10
+ export function slugify(name: string): string {
11
+ return name
12
+ .toLowerCase()
13
+ .trim()
14
+ .replace(/[^a-z0-9\s-]/g, '')
15
+ .replace(/\s+/g, '-')
16
+ .replace(/-+/g, '-');
17
+ }
18
+
19
+ /**
20
+ * Convert a name to snake_case.
21
+ * "total tasks" → "total_tasks"
22
+ * "started at" → "started_at"
23
+ */
24
+ export function snakeCase(name: string): string {
25
+ return name
26
+ .toLowerCase()
27
+ .trim()
28
+ .replace(/[^a-z0-9\s_]/g, '')
29
+ .replace(/\s+/g, '_')
30
+ .replace(/_+/g, '_');
31
+ }
32
+
33
+ /**
34
+ * Generate a deterministic ID from parts.
35
+ * generateId("project", "draft") → "project--draft"
36
+ * generateId("task", "in progress") → "task--in-progress"
37
+ */
38
+ export function generateId(...parts: string[]): string {
39
+ return parts.map(p => slugify(p)).join('--');
40
+ }
41
+
42
+ /**
43
+ * Generate a deterministic action ID.
44
+ * generateActionId("active-on-enter", 0) → "active-on-enter-0"
45
+ */
46
+ export function generateActionId(context: string, index: number): string {
47
+ return `${context}-${index}`;
48
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * View Compiler — Pass 3 of compilation.
3
+ *
4
+ * Transforms fragments and views → IRExperienceDefinition with ExperienceNode trees.
5
+ * Handles data sources, content, iteration, layout, and bindings.
6
+ */
7
+
8
+ import type { ASTNode } from '../types';
9
+ import type {
10
+ IRExperienceDefinition,
11
+ IRExperienceNode,
12
+ IRWorkflowDataSource,
13
+ IRDataSource,
14
+ CompilerError,
15
+ } from '../ir-types';
16
+ import type { SymbolTable, SpaceSymbol } from './symbol-table';
17
+ import { slugify, snakeCase, generateId } from './utils';
18
+ import {
19
+ mapContent,
20
+ mapStringLiteral,
21
+ mapIteration,
22
+ mapSection,
23
+ mapSearch,
24
+ mapNavigation,
25
+ mapPages,
26
+ } from './component-mapper';
27
+
28
+ // =============================================================================
29
+ // Public API
30
+ // =============================================================================
31
+
32
+ export function compileViews(
33
+ symbols: SymbolTable,
34
+ ): { experiences: IRExperienceDefinition[]; errors: CompilerError[] } {
35
+ const experiences: IRExperienceDefinition[] = [];
36
+ const errors: CompilerError[] = [];
37
+
38
+ // Compile fragments
39
+ for (const [, fragment] of symbols.fragments) {
40
+ const exp = compileViewNode(
41
+ fragment.name,
42
+ fragment.node,
43
+ symbols.space,
44
+ );
45
+ experiences.push(exp);
46
+ }
47
+
48
+ // Compile views
49
+ for (const [, view] of symbols.views) {
50
+ const exp = compileViewNode(
51
+ view.name,
52
+ view.node,
53
+ symbols.space,
54
+ );
55
+ experiences.push(exp);
56
+ }
57
+
58
+ return { experiences, errors };
59
+ }
60
+
61
+ // =============================================================================
62
+ // View Compilation
63
+ // =============================================================================
64
+
65
+ function compileViewNode(
66
+ name: string,
67
+ node: ASTNode,
68
+ space?: SpaceSymbol,
69
+ ): IRExperienceDefinition {
70
+ const viewSlug = slugify(name);
71
+ const dataSources: IRDataSource[] = [];
72
+ const workflowSlugs: string[] = [];
73
+
74
+ // First pass: collect data sources
75
+ for (const child of node.children) {
76
+ if (child.token.data.type === 'data_source') {
77
+ const ds = compileDataSource(child);
78
+ dataSources.push(ds);
79
+ if (ds.type === 'workflow' && ds.slug) {
80
+ workflowSlugs.push(ds.slug);
81
+ }
82
+ }
83
+ }
84
+
85
+ // Second pass: build ExperienceNode tree from remaining children
86
+ const children = compileChildren(node.children, viewSlug, false);
87
+
88
+ // Build the root node
89
+ const rootNode: IRExperienceNode = {
90
+ id: viewSlug,
91
+ children: children.length > 0 ? children : undefined,
92
+ dataSources: dataSources.length > 0 ? dataSources : undefined,
93
+ };
94
+
95
+ if (children.length > 1) {
96
+ rootNode.layout = 'stack';
97
+ }
98
+
99
+ return {
100
+ slug: viewSlug,
101
+ version: space?.version ?? '1.0.0',
102
+ name,
103
+ category: 'purpose',
104
+ view_definition: rootNode,
105
+ workflows: workflowSlugs,
106
+ children: [],
107
+ data_bindings: [],
108
+ is_default: false,
109
+ };
110
+ }
111
+
112
+ // =============================================================================
113
+ // Data Source Compilation
114
+ // =============================================================================
115
+
116
+ function compileDataSource(node: ASTNode): IRWorkflowDataSource {
117
+ const data = node.token.data;
118
+ if (data.type !== 'data_source') throw new Error('Expected data_source');
119
+
120
+ // Determine query type from alias pronoun
121
+ const query = resolveQueryType(data.alias);
122
+
123
+ // Strip pronoun from alias to get the name
124
+ const name = stripPronoun(data.alias);
125
+
126
+ // Slug from source
127
+ const slug = slugify(data.source);
128
+
129
+ const ds: IRWorkflowDataSource = {
130
+ type: 'workflow',
131
+ name,
132
+ slug,
133
+ query,
134
+ };
135
+
136
+ // Scope
137
+ if (data.scope) {
138
+ ds.parentInstanceId = '{{ parent_instance_id }}';
139
+ }
140
+
141
+ // Process qualifier children
142
+ for (const child of node.children) {
143
+ const cd = child.token.data;
144
+ if (cd.type === 'qualifier') {
145
+ applyQualifier(ds, cd);
146
+ }
147
+ }
148
+
149
+ return ds;
150
+ }
151
+
152
+ function resolveQueryType(alias: string): 'latest' | 'list' | 'count' {
153
+ const firstWord = alias.split(/\s+/)[0];
154
+ if (firstWord === 'this') return 'latest';
155
+ // my, its, these → list
156
+ return 'list';
157
+ }
158
+
159
+ function stripPronoun(alias: string): string {
160
+ const pronouns = ['my', 'this', 'its', 'these'];
161
+ const words = alias.split(/\s+/);
162
+ if (words.length > 1 && pronouns.includes(words[0])) {
163
+ return words.slice(1).join(' ');
164
+ }
165
+ return alias;
166
+ }
167
+
168
+ function applyQualifier(
169
+ ds: IRWorkflowDataSource,
170
+ qualifier: { kind: string; value: string },
171
+ ): void {
172
+ switch (qualifier.kind) {
173
+ case 'order':
174
+ if (qualifier.value === 'newest') {
175
+ ds.sort = 'created_at:desc';
176
+ } else if (qualifier.value === 'oldest') {
177
+ ds.sort = 'created_at:asc';
178
+ } else {
179
+ ds.sort = `${snakeCase(qualifier.value)}:desc`;
180
+ }
181
+ break;
182
+
183
+ case 'pagination':
184
+ ds.paginated = true;
185
+ ds.pageSize = parseInt(qualifier.value, 10);
186
+ break;
187
+
188
+ case 'searchable':
189
+ ds.searchFields = qualifier.value
190
+ .split(/\s+and\s+/)
191
+ .map(f => snakeCase(f.trim()));
192
+ break;
193
+
194
+ case 'filterable':
195
+ ds.facets = qualifier.value
196
+ .split(/\s+and\s+/)
197
+ .map(f => snakeCase(f.trim()));
198
+ break;
199
+ }
200
+ }
201
+
202
+ // =============================================================================
203
+ // Children Compilation
204
+ // =============================================================================
205
+
206
+ function compileChildren(
207
+ children: ASTNode[],
208
+ parentId: string,
209
+ insideEach: boolean,
210
+ ): IRExperienceNode[] {
211
+ const nodes: IRExperienceNode[] = [];
212
+ let childIndex = 0;
213
+
214
+ for (const child of children) {
215
+ const cd = child.token.data;
216
+
217
+ switch (cd.type) {
218
+ case 'data_source':
219
+ // Data sources are handled at the root level, skip here
220
+ break;
221
+
222
+ case 'qualifier':
223
+ // Qualifiers attach to their parent data source, skip here
224
+ break;
225
+
226
+ case 'content':
227
+ nodes.push(mapContent(cd, insideEach, parentId, childIndex++));
228
+ break;
229
+
230
+ case 'string_literal': {
231
+ const strNode = mapStringLiteral(cd, parentId, childIndex++);
232
+ // If string literal has children, they're nested under it
233
+ if (child.children.length > 0) {
234
+ strNode.children = compileChildren(child.children, strNode.id, insideEach);
235
+ if (strNode.children.length > 1) {
236
+ strNode.layout = 'stack';
237
+ }
238
+ }
239
+ nodes.push(strNode);
240
+ break;
241
+ }
242
+
243
+ case 'iteration': {
244
+ const iterChildren = compileChildren(child.children, generateId(parentId, 'each', cd.subject), true);
245
+ nodes.push(mapIteration(cd, iterChildren, parentId, childIndex++));
246
+ break;
247
+ }
248
+
249
+ case 'section': {
250
+ const sectionChildren = compileChildren(child.children, generateId(parentId, cd.name), insideEach);
251
+ nodes.push(mapSection(cd, sectionChildren, parentId));
252
+ break;
253
+ }
254
+
255
+ case 'search':
256
+ nodes.push(mapSearch(cd, parentId, childIndex++));
257
+ break;
258
+
259
+ case 'navigation':
260
+ nodes.push(mapNavigation(cd, insideEach, parentId, childIndex++));
261
+ break;
262
+
263
+ case 'pages':
264
+ nodes.push(mapPages(parentId, childIndex++));
265
+ break;
266
+
267
+ case 'grouping':
268
+ // grouping affects data source rather than creating a node
269
+ // but we record it as a node for structural completeness
270
+ nodes.push({
271
+ id: generateId(parentId, 'group', cd.collection),
272
+ config: {
273
+ groupBy: snakeCase(cd.key),
274
+ collection: cd.collection,
275
+ },
276
+ children: compileChildren(child.children, generateId(parentId, 'group', cd.collection), insideEach),
277
+ });
278
+ break;
279
+
280
+ default:
281
+ break;
282
+ }
283
+ }
284
+
285
+ return nodes;
286
+ }