@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,80 @@
1
+ /**
2
+ * Action Dispatcher — executes action definitions against registered handlers.
3
+ *
4
+ * The dispatcher:
5
+ * - Maintains a registry of handler functions by action type
6
+ * - Evaluates per-action conditions before execution
7
+ * - Collects results (success/failure) for each action
8
+ * - Never throws — errors are captured in ActionResult
9
+ */
10
+
11
+ import type { Evaluator, ExpressionContext } from '../expression/types';
12
+ import type { ActionDefinition, ActionHandlerFn, ActionResult } from './types';
13
+
14
+ export class ActionDispatcher {
15
+ private readonly handlers = new Map<string, ActionHandlerFn>();
16
+
17
+ /** Register a handler for an action type */
18
+ register(type: string, handler: ActionHandlerFn): void {
19
+ this.handlers.set(type, handler);
20
+ }
21
+
22
+ /** Unregister a handler */
23
+ unregister(type: string): void {
24
+ this.handlers.delete(type);
25
+ }
26
+
27
+ /** Check if a handler is registered for the given type */
28
+ has(type: string): boolean {
29
+ return this.handlers.has(type);
30
+ }
31
+
32
+ /**
33
+ * Execute a list of actions sequentially.
34
+ * Each action's condition is evaluated first (if present).
35
+ * Missing handlers are skipped with a warning.
36
+ */
37
+ async execute(
38
+ actions: ActionDefinition[],
39
+ context: ExpressionContext,
40
+ evaluator?: Evaluator,
41
+ ): Promise<ActionResult[]> {
42
+ const results: ActionResult[] = [];
43
+
44
+ for (const action of actions) {
45
+ // Check per-action condition
46
+ if (action.condition && evaluator) {
47
+ const condResult = evaluator.evaluate<boolean>(action.condition, context);
48
+ if (!condResult.value) continue; // Skip this action
49
+ }
50
+
51
+ const handler = this.handlers.get(action.type);
52
+ if (!handler) {
53
+ console.warn(`[player-core] No handler registered for action type "${action.type}"`);
54
+ results.push({ type: action.type, success: false, error: `No handler for "${action.type}"` });
55
+ continue;
56
+ }
57
+
58
+ try {
59
+ await handler(action.config, context);
60
+ results.push({ type: action.type, success: true });
61
+ } catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ console.warn(`[player-core] Action "${action.type}" failed: ${message}`);
64
+ results.push({ type: action.type, success: false, error: message });
65
+ }
66
+ }
67
+
68
+ return results;
69
+ }
70
+
71
+ /** Get count of registered handlers */
72
+ get size(): number {
73
+ return this.handlers.size;
74
+ }
75
+
76
+ /** Remove all handlers */
77
+ clear(): void {
78
+ this.handlers.clear();
79
+ }
80
+ }
@@ -0,0 +1,7 @@
1
+ export type {
2
+ ActionDefinition,
3
+ ActionHandlerFn,
4
+ ActionResult,
5
+ } from './types';
6
+
7
+ export { ActionDispatcher } from './dispatcher';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Action System Types — handler registry and dispatch.
3
+ */
4
+
5
+ import type { ExpressionContext } from '../expression/types';
6
+
7
+ /** Action definition from workflow state/transition */
8
+ export interface ActionDefinition {
9
+ type: string;
10
+ config: Record<string, unknown>;
11
+ condition?: string;
12
+ }
13
+
14
+ /** Registered action handler */
15
+ export type ActionHandlerFn = (
16
+ config: Record<string, unknown>,
17
+ context: ExpressionContext,
18
+ ) => void | Promise<void>;
19
+
20
+ /** Result of executing an action */
21
+ export interface ActionResult {
22
+ type: string;
23
+ success: boolean;
24
+ error?: string;
25
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Component Mapper — maps DSL content/iteration patterns to ExperienceNode IR.
3
+ *
4
+ * Maps to the 34 frozen component IDs from the component registry (§9).
5
+ */
6
+
7
+ import type {
8
+ ContentData,
9
+ StringLiteralData,
10
+ IterationData,
11
+ SectionData,
12
+ SearchData,
13
+ NavigationData,
14
+ } from '../types';
15
+ import type { IRExperienceNode } from '../ir-types';
16
+ import { snakeCase, slugify, generateId } from './utils';
17
+
18
+ // =============================================================================
19
+ // Instance-level fields (NOT in state_data)
20
+ // =============================================================================
21
+
22
+ const INSTANCE_FIELDS = new Set([
23
+ 'id', 'current_state', 'state', 'status',
24
+ 'created_at', 'updated_at', 'entity_type', 'entity_id',
25
+ ]);
26
+
27
+ // =============================================================================
28
+ // Content → ExperienceNode
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Map a content node to an ExperienceNode.
33
+ * @param data - parsed content data
34
+ * @param insideEach - whether this content is inside an iteration (changes binding scope)
35
+ * @param parentId - parent ID for generating unique child IDs
36
+ * @param index - child index for ID uniqueness
37
+ */
38
+ export function mapContent(
39
+ data: ContentData,
40
+ insideEach: boolean,
41
+ parentId: string,
42
+ index: number,
43
+ ): IRExperienceNode {
44
+ const fieldSnake = snakeCase(data.field);
45
+ const nodeId = generateId(parentId, data.field, String(index));
46
+ const prefix = insideEach ? '$item' : '$instance';
47
+
48
+ // Determine binding path
49
+ const isInstanceField = INSTANCE_FIELDS.has(fieldSnake);
50
+ const bindingPath = isInstanceField
51
+ ? `${prefix}.${fieldSnake === 'state' ? 'current_state' : fieldSnake}`
52
+ : `${prefix}.state_data.${fieldSnake}`;
53
+
54
+ // Special component based on role
55
+ if (data.role) {
56
+ return mapContentWithRole(data, nodeId, bindingPath);
57
+ }
58
+
59
+ // Default: Text component
60
+ const node: IRExperienceNode = {
61
+ id: nodeId,
62
+ component: 'Text',
63
+ bindings: { value: bindingPath },
64
+ };
65
+
66
+ if (data.emphasis === 'big') {
67
+ node.config = { variant: 'heading' };
68
+ } else if (data.emphasis === 'small') {
69
+ node.config = { variant: 'caption' };
70
+ }
71
+
72
+ if (data.label) {
73
+ if (!node.config) node.config = {};
74
+ node.config.label = data.label;
75
+ }
76
+
77
+ return node;
78
+ }
79
+
80
+ function mapContentWithRole(
81
+ data: ContentData,
82
+ nodeId: string,
83
+ bindingPath: string,
84
+ ): IRExperienceNode {
85
+ switch (data.role) {
86
+ case 'tag':
87
+ return {
88
+ id: nodeId,
89
+ component: 'Badge',
90
+ bindings: { value: bindingPath },
91
+ };
92
+
93
+ case 'progress':
94
+ case 'meter':
95
+ return {
96
+ id: nodeId,
97
+ component: 'ProgressTracker',
98
+ bindings: { value: bindingPath },
99
+ };
100
+
101
+ case 'image':
102
+ return {
103
+ id: nodeId,
104
+ component: 'Image',
105
+ bindings: { src: bindingPath },
106
+ };
107
+
108
+ case 'number': {
109
+ const node: IRExperienceNode = {
110
+ id: nodeId,
111
+ component: 'Text',
112
+ config: { variant: 'metric' },
113
+ bindings: { value: bindingPath },
114
+ };
115
+ if (data.label) {
116
+ node.config!.label = data.label;
117
+ }
118
+ return node;
119
+ }
120
+
121
+ default:
122
+ return {
123
+ id: nodeId,
124
+ component: 'Text',
125
+ bindings: { value: bindingPath },
126
+ };
127
+ }
128
+ }
129
+
130
+ // =============================================================================
131
+ // String Literal → ExperienceNode
132
+ // =============================================================================
133
+
134
+ export function mapStringLiteral(
135
+ data: StringLiteralData,
136
+ parentId: string,
137
+ index: number,
138
+ ): IRExperienceNode {
139
+ const nodeId = generateId(parentId, 'text', String(index));
140
+ const node: IRExperienceNode = {
141
+ id: nodeId,
142
+ component: 'Text',
143
+ bindings: { value: `"${data.text}"` },
144
+ };
145
+
146
+ if (data.emphasis === 'big') {
147
+ node.config = { variant: 'heading' };
148
+ } else if (data.emphasis === 'small') {
149
+ node.config = { variant: 'caption' };
150
+ }
151
+
152
+ return node;
153
+ }
154
+
155
+ // =============================================================================
156
+ // Iteration → ExperienceNode
157
+ // =============================================================================
158
+
159
+ export function mapIteration(
160
+ data: IterationData,
161
+ children: IRExperienceNode[],
162
+ parentId: string,
163
+ index: number,
164
+ ): IRExperienceNode {
165
+ const nodeId = generateId(parentId, 'each', data.subject);
166
+ const eachNode: IRExperienceNode = {
167
+ id: nodeId,
168
+ component: 'Each',
169
+ };
170
+
171
+ if (data.role === 'card') {
172
+ // Wrap children in a Card
173
+ const cardNode: IRExperienceNode = {
174
+ id: generateId(nodeId, 'card'),
175
+ component: 'Card',
176
+ children,
177
+ };
178
+ if (data.emphasis === 'small') {
179
+ cardNode.config = { size: 'small' };
180
+ }
181
+ if (children.length > 1) {
182
+ cardNode.layout = 'stack';
183
+ }
184
+ eachNode.children = [cardNode];
185
+ } else {
186
+ eachNode.children = children;
187
+ }
188
+
189
+ return eachNode;
190
+ }
191
+
192
+ // =============================================================================
193
+ // Section → ExperienceNode
194
+ // =============================================================================
195
+
196
+ export function mapSection(
197
+ data: SectionData,
198
+ children: IRExperienceNode[],
199
+ parentId: string,
200
+ ): IRExperienceNode {
201
+ switch (data.name) {
202
+ case 'numbers':
203
+ return {
204
+ id: generateId(parentId, 'numbers'),
205
+ component: 'MetricsGrid',
206
+ children,
207
+ };
208
+
209
+ case 'tabs':
210
+ return {
211
+ id: generateId(parentId, 'tabs'),
212
+ component: 'TabbedLayout',
213
+ children,
214
+ };
215
+
216
+ case 'actions':
217
+ return {
218
+ id: generateId(parentId, 'actions'),
219
+ component: 'TransitionActions',
220
+ };
221
+
222
+ case 'controls':
223
+ return {
224
+ id: generateId(parentId, 'controls'),
225
+ layout: 'row',
226
+ children,
227
+ };
228
+
229
+ default:
230
+ return {
231
+ id: generateId(parentId, data.name),
232
+ children,
233
+ };
234
+ }
235
+ }
236
+
237
+ // =============================================================================
238
+ // Search → ExperienceNode
239
+ // =============================================================================
240
+
241
+ export function mapSearch(
242
+ data: SearchData,
243
+ parentId: string,
244
+ index: number,
245
+ ): IRExperienceNode {
246
+ return {
247
+ id: generateId(parentId, 'search', String(index)),
248
+ component: 'SearchInput',
249
+ config: { target: data.target },
250
+ };
251
+ }
252
+
253
+ // =============================================================================
254
+ // Navigation → ExperienceNode binding
255
+ // =============================================================================
256
+
257
+ export function mapNavigation(
258
+ data: NavigationData,
259
+ insideEach: boolean,
260
+ parentId: string,
261
+ index: number,
262
+ ): IRExperienceNode {
263
+ const nodeId = generateId(parentId, 'nav', String(index));
264
+ const prefix = insideEach ? '$item' : '$instance';
265
+
266
+ // Replace {its id} with binding template
267
+ const target = data.target.replace(
268
+ /\{its\s+id\}/g,
269
+ `\${${prefix}.id}`,
270
+ );
271
+
272
+ return {
273
+ id: nodeId,
274
+ component: 'Link',
275
+ bindings: { onClick: `$action.navigate("${target}")` },
276
+ config: { trigger: data.trigger },
277
+ };
278
+ }
279
+
280
+ // =============================================================================
281
+ // Pages → ExperienceNode
282
+ // =============================================================================
283
+
284
+ export function mapPages(parentId: string, index: number): IRExperienceNode {
285
+ return {
286
+ id: generateId(parentId, 'pages', String(index)),
287
+ component: 'Pagination',
288
+ };
289
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Field Mapper — converts DSL FieldDefData to IR field definitions.
3
+ *
4
+ * Handles base type mapping, adjective→property mapping,
5
+ * and constraint→validation mapping.
6
+ */
7
+
8
+ import type { FieldDefData } from '../types';
9
+ import type { IRFieldDefinition, IRFieldValidation, IRWorkflowFieldType } from '../ir-types';
10
+ import { snakeCase } from './utils';
11
+
12
+ // =============================================================================
13
+ // Base Type Mapping
14
+ // =============================================================================
15
+
16
+ const BASE_TYPE_MAP: Record<string, IRWorkflowFieldType> = {
17
+ 'text': 'text',
18
+ 'rich text': 'rich_text',
19
+ 'number': 'number',
20
+ 'integer': 'number',
21
+ 'time': 'datetime',
22
+ };
23
+
24
+ function isTextLikeType(irType: IRWorkflowFieldType): boolean {
25
+ return irType === 'text' || irType === 'rich_text';
26
+ }
27
+
28
+ // =============================================================================
29
+ // Public API
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Map a DSL field definition to an IR field definition.
34
+ */
35
+ export function mapField(data: FieldDefData): IRFieldDefinition {
36
+ const field: IRFieldDefinition = {
37
+ name: snakeCase(data.name),
38
+ type: resolveType(data.baseType),
39
+ };
40
+
41
+ // Adjective mapping
42
+ for (const adj of data.adjectives) {
43
+ applyAdjective(field, adj);
44
+ }
45
+
46
+ // Constraint mapping
47
+ const validation = buildValidation(data, field.type);
48
+ if (validation) {
49
+ field.validation = mergeValidation(field.validation, validation);
50
+ }
51
+
52
+ // Integer → add validation rule for whole numbers
53
+ if (data.baseType === 'integer') {
54
+ if (!field.validation) field.validation = {};
55
+ if (!field.validation.rules) field.validation.rules = [];
56
+ field.validation.rules.push({
57
+ expression: 'eq(round($value), $value)',
58
+ message: 'Must be a whole number',
59
+ severity: 'error',
60
+ });
61
+ }
62
+
63
+ // Choice of [...] → extract options
64
+ if (data.baseType.startsWith('choice of')) {
65
+ field.type = 'select';
66
+ const optionsMatch = data.baseType.match(/\[(.+)\]/);
67
+ if (optionsMatch) {
68
+ if (!field.validation) field.validation = {};
69
+ field.validation.options = optionsMatch[1]
70
+ .split(',')
71
+ .map(o => o.trim());
72
+ }
73
+ }
74
+
75
+ // Default value from constraints
76
+ const defaultConstraint = data.constraints.find(c => c.kind === 'default');
77
+ if (defaultConstraint) {
78
+ field.default_value = defaultConstraint.value;
79
+ }
80
+
81
+ return field;
82
+ }
83
+
84
+ // =============================================================================
85
+ // Type Resolution
86
+ // =============================================================================
87
+
88
+ function resolveType(baseType: string): IRWorkflowFieldType {
89
+ if (baseType.startsWith('choice of')) return 'select';
90
+ // Known types map to canonical slugs; unknown types pass through as-is
91
+ // (custom Element-based field types use slugs like 'email', 'currency', 'my-org/address')
92
+ return BASE_TYPE_MAP[baseType] ?? baseType;
93
+ }
94
+
95
+ // =============================================================================
96
+ // Adjective Application
97
+ // =============================================================================
98
+
99
+ function applyAdjective(field: IRFieldDefinition, adjective: string): void {
100
+ switch (adjective) {
101
+ case 'required':
102
+ field.required = true;
103
+ break;
104
+ case 'optional':
105
+ field.required = false;
106
+ break;
107
+ case 'non-negative':
108
+ if (!field.validation) field.validation = {};
109
+ field.validation.min = 0;
110
+ break;
111
+ case 'positive':
112
+ if (!field.validation) field.validation = {};
113
+ field.validation.min = 1;
114
+ break;
115
+ case 'computed':
116
+ field.computed = '';
117
+ break;
118
+ // lowercase, uppercase, readonly — stored but no direct IR mapping
119
+ default:
120
+ break;
121
+ }
122
+ }
123
+
124
+ // =============================================================================
125
+ // Constraint → Validation
126
+ // =============================================================================
127
+
128
+ function buildValidation(
129
+ data: FieldDefData,
130
+ irType: IRWorkflowFieldType,
131
+ ): IRFieldValidation | null {
132
+ if (data.constraints.length === 0) return null;
133
+
134
+ const validation: IRFieldValidation = {};
135
+ let hasProps = false;
136
+
137
+ for (const c of data.constraints) {
138
+ switch (c.kind) {
139
+ case 'max':
140
+ if (isTextLikeType(irType)) {
141
+ validation.maxLength = c.value as number;
142
+ } else {
143
+ validation.max = c.value as number;
144
+ }
145
+ hasProps = true;
146
+ break;
147
+
148
+ case 'min':
149
+ if (isTextLikeType(irType)) {
150
+ validation.minLength = c.value as number;
151
+ } else {
152
+ validation.min = c.value as number;
153
+ }
154
+ hasProps = true;
155
+ break;
156
+
157
+ case 'between':
158
+ validation.min = c.value as number;
159
+ validation.max = c.value2 as number;
160
+ hasProps = true;
161
+ break;
162
+
163
+ case 'default':
164
+ // default_value is set on the field itself, not validation
165
+ break;
166
+
167
+ case 'unique':
168
+ // metadata flag — not a validation rule
169
+ break;
170
+ }
171
+ }
172
+
173
+ // Handle default_value separately (on the field, not in validation)
174
+ return hasProps ? validation : null;
175
+ }
176
+
177
+ /**
178
+ * Extract default_value from constraints.
179
+ * Called internally by mapField — the default_value is set on the field.
180
+ */
181
+ function mergeValidation(
182
+ existing: IRFieldValidation | undefined,
183
+ incoming: IRFieldValidation,
184
+ ): IRFieldValidation {
185
+ if (!existing) return incoming;
186
+ return { ...existing, ...incoming };
187
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * DSL Compiler — entry point.
3
+ *
4
+ * Pipeline: tokenize → parse → collectSymbols → compileWorkflows → compileViews → compileManifest
5
+ */
6
+
7
+ import { tokenize } from '../lexer';
8
+ import { parse } from '../parser';
9
+ import type { ASTNode, ParseError } from '../types';
10
+ import type { CompilationResult, CompilerError } from '../ir-types';
11
+ import { collectSymbols } from './symbol-table';
12
+ import { compileWorkflows } from './workflow-compiler';
13
+ import { compileViews } from './view-compiler';
14
+ import { compileManifest } from './manifest-compiler';
15
+
16
+ // =============================================================================
17
+ // Public API
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Compile DSL source text into the full IR.
22
+ */
23
+ export function compile(source: string): CompilationResult {
24
+ const tokens = tokenize(source);
25
+ const { nodes, errors: parseErrors } = parse(tokens);
26
+ return compileAST(nodes, parseErrors);
27
+ }
28
+
29
+ /**
30
+ * Compile pre-parsed AST nodes into the full IR.
31
+ */
32
+ export function compileAST(
33
+ nodes: ASTNode[],
34
+ parseErrors?: ParseError[],
35
+ ): CompilationResult {
36
+ const allErrors: CompilerError[] = [];
37
+ const allWarnings: CompilerError[] = [];
38
+
39
+ // Convert parse errors
40
+ if (parseErrors) {
41
+ for (const pe of parseErrors) {
42
+ allErrors.push({
43
+ code: 'INVALID_EXPRESSION',
44
+ message: pe.message,
45
+ lineNumber: pe.lineNumber,
46
+ severity: 'error',
47
+ });
48
+ }
49
+ }
50
+
51
+ // Pass 1: Symbol collection
52
+ const symbols = collectSymbols(nodes);
53
+
54
+ // Pass 2: Workflow compilation
55
+ const { workflows, errors: wfErrors } = compileWorkflows(symbols);
56
+ for (const e of wfErrors) {
57
+ if (e.severity === 'error') allErrors.push(e);
58
+ else allWarnings.push(e);
59
+ }
60
+
61
+ // Pass 3: View compilation
62
+ const { experiences, errors: viewErrors } = compileViews(symbols);
63
+ for (const e of viewErrors) {
64
+ if (e.severity === 'error') allErrors.push(e);
65
+ else allWarnings.push(e);
66
+ }
67
+
68
+ // Pass 4: Manifest compilation
69
+ const { manifest, errors: mfErrors } = compileManifest(symbols);
70
+ for (const e of mfErrors) {
71
+ if (e.severity === 'error') allErrors.push(e);
72
+ else allWarnings.push(e);
73
+ }
74
+
75
+ return {
76
+ workflows,
77
+ experiences,
78
+ manifest,
79
+ errors: allErrors,
80
+ warnings: allWarnings,
81
+ };
82
+ }