@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,221 @@
1
+ /**
2
+ * IR Migration — forward-compatibility layer for workflow definitions.
3
+ *
4
+ * Handles differences between IR versions so that older compiled definitions
5
+ * continue to work with newer runtimes. Each migration transforms from
6
+ * one version to the next; they chain automatically.
7
+ *
8
+ * Version history:
9
+ * - v1.0: Original format (states use state_type, actions use action_type)
10
+ * - v1.1: Normalized format (states use type, actions use type, fields use type)
11
+ * - v1.2: Added extensions, during actions, on_event at workflow level
12
+ *
13
+ * The current version is always the latest. Older definitions are migrated
14
+ * transparently by normalizeDefinition().
15
+ */
16
+
17
+ import type { IRWorkflowDefinition } from './ir-types';
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ export const CURRENT_IR_VERSION = '1.2';
24
+
25
+ export interface MigrationResult {
26
+ /** The migrated definition. */
27
+ definition: IRWorkflowDefinition;
28
+ /** Original IR version detected. */
29
+ fromVersion: string;
30
+ /** Target IR version after migration. */
31
+ toVersion: string;
32
+ /** Whether any migration was applied. */
33
+ migrated: boolean;
34
+ /** Migrations applied (in order). */
35
+ appliedMigrations: string[];
36
+ }
37
+
38
+ // =============================================================================
39
+ // Version Detection
40
+ // =============================================================================
41
+
42
+ /**
43
+ * Detect the IR version of a definition.
44
+ * Uses heuristics based on field naming conventions.
45
+ */
46
+ export function detectIRVersion(def: Record<string, unknown>): string {
47
+ // Check explicit ir_version field
48
+ if (typeof def.ir_version === 'string') return def.ir_version;
49
+
50
+ // v1.2: has extensions or workflow-level on_event
51
+ if (def.extensions || (def.on_event && Array.isArray(def.on_event))) {
52
+ return '1.2';
53
+ }
54
+
55
+ // v1.0 detection: uses state_type/action_type/field_type naming
56
+ const states = def.states as Array<Record<string, unknown>> | undefined;
57
+ const fields = def.fields as Array<Record<string, unknown>> | undefined;
58
+
59
+ if (states && states.length > 0 && states[0].state_type) return '1.0';
60
+ if (fields && fields.length > 0 && fields[0].field_type && !fields[0].type) return '1.0';
61
+
62
+ // v1.1: uses normalized type naming
63
+ if (states && states.length > 0 && states[0].type) return '1.1';
64
+
65
+ return '1.0'; // Default to oldest version
66
+ }
67
+
68
+ // =============================================================================
69
+ // Migrations
70
+ // =============================================================================
71
+
72
+ type MigrationFn = (def: Record<string, unknown>) => Record<string, unknown>;
73
+
74
+ /**
75
+ * v1.0 → v1.1: Normalize field names (state_type → type, etc.)
76
+ */
77
+ function migrateV10ToV11(def: Record<string, unknown>): Record<string, unknown> {
78
+ const result = { ...def };
79
+
80
+ // Normalize states: state_type → type
81
+ if (Array.isArray(result.states)) {
82
+ result.states = (result.states as Array<Record<string, unknown>>).map(state => {
83
+ const normalized: Record<string, unknown> = { ...state };
84
+ if ('state_type' in normalized && !('type' in normalized)) {
85
+ normalized.type = normalized.state_type;
86
+ delete normalized.state_type;
87
+ }
88
+
89
+ // Normalize actions within states
90
+ if (Array.isArray(normalized.on_enter)) {
91
+ normalized.on_enter = normalizeActions(normalized.on_enter as Array<Record<string, unknown>>);
92
+ }
93
+ if (Array.isArray(normalized.on_exit)) {
94
+ normalized.on_exit = normalizeActions(normalized.on_exit as Array<Record<string, unknown>>);
95
+ }
96
+ if (Array.isArray(normalized.during)) {
97
+ normalized.during = (normalized.during as Array<Record<string, unknown>>).map(d => {
98
+ if (Array.isArray(d.actions)) {
99
+ return { ...d, actions: normalizeActions(d.actions as Array<Record<string, unknown>>) };
100
+ }
101
+ return d;
102
+ });
103
+ }
104
+
105
+ return normalized;
106
+ });
107
+ }
108
+
109
+ // Normalize fields: field_type → type
110
+ if (Array.isArray(result.fields)) {
111
+ result.fields = (result.fields as Array<Record<string, unknown>>).map(field => {
112
+ const normalized: Record<string, unknown> = { ...field };
113
+ if ('field_type' in normalized && !('type' in normalized)) {
114
+ normalized.type = normalized.field_type;
115
+ delete normalized.field_type;
116
+ }
117
+ return normalized;
118
+ });
119
+ }
120
+
121
+ // Normalize transition actions
122
+ if (Array.isArray(result.transitions)) {
123
+ result.transitions = (result.transitions as Array<Record<string, unknown>>).map(t => {
124
+ if (Array.isArray(t.actions)) {
125
+ return { ...t, actions: normalizeActions(t.actions as Array<Record<string, unknown>>) };
126
+ }
127
+ return t;
128
+ });
129
+ }
130
+
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * v1.1 → v1.2: Add extensions, during, on_event defaults
136
+ */
137
+ function migrateV11ToV12(def: Record<string, unknown>): Record<string, unknown> {
138
+ const result = { ...def };
139
+
140
+ // Ensure extensions field exists
141
+ if (!result.extensions) {
142
+ result.extensions = {};
143
+ }
144
+
145
+ // Ensure states have during and on_event arrays
146
+ if (Array.isArray(result.states)) {
147
+ result.states = (result.states as Array<Record<string, unknown>>).map(state => ({
148
+ ...state,
149
+ during: state.during ?? [],
150
+ on_event: state.on_event ?? [],
151
+ }));
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ function normalizeActions(actions: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
158
+ return actions.map(action => {
159
+ const normalized: Record<string, unknown> = { ...action };
160
+ if ('action_type' in normalized && !('type' in normalized)) {
161
+ normalized.type = normalized.action_type;
162
+ delete normalized.action_type;
163
+ }
164
+ return normalized;
165
+ });
166
+ }
167
+
168
+ // =============================================================================
169
+ // Migration Chain
170
+ // =============================================================================
171
+
172
+ const MIGRATIONS: Array<{ from: string; to: string; fn: MigrationFn }> = [
173
+ { from: '1.0', to: '1.1', fn: migrateV10ToV11 },
174
+ { from: '1.1', to: '1.2', fn: migrateV11ToV12 },
175
+ ];
176
+
177
+ /**
178
+ * Normalize a definition to the current IR version.
179
+ * Applies all necessary migrations in sequence.
180
+ */
181
+ export function normalizeDefinition(def: Record<string, unknown>): MigrationResult {
182
+ let currentVersion = detectIRVersion(def);
183
+ let current = def;
184
+ const applied: string[] = [];
185
+
186
+ if (currentVersion === CURRENT_IR_VERSION) {
187
+ return {
188
+ definition: current as unknown as IRWorkflowDefinition,
189
+ fromVersion: currentVersion,
190
+ toVersion: CURRENT_IR_VERSION,
191
+ migrated: false,
192
+ appliedMigrations: [],
193
+ };
194
+ }
195
+
196
+ const fromVersion = currentVersion;
197
+
198
+ // Apply migrations in order
199
+ for (const migration of MIGRATIONS) {
200
+ if (currentVersion === migration.from) {
201
+ current = migration.fn(current);
202
+ currentVersion = migration.to;
203
+ applied.push(`${migration.from} → ${migration.to}`);
204
+ }
205
+ }
206
+
207
+ return {
208
+ definition: current as unknown as IRWorkflowDefinition,
209
+ fromVersion,
210
+ toVersion: currentVersion,
211
+ migrated: applied.length > 0,
212
+ appliedMigrations: applied,
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Check if a definition needs migration.
218
+ */
219
+ export function needsMigration(def: Record<string, unknown>): boolean {
220
+ return detectIRVersion(def) !== CURRENT_IR_VERSION;
221
+ }
@@ -0,0 +1,416 @@
1
+ /**
2
+ * IR Types — the complete Intermediate Representation contract.
3
+ *
4
+ * These types mirror the MM-IR Reference (0-MM-IR-REFERENCE.md).
5
+ * Standalone — no imports from backend/frontend packages.
6
+ * The compiler transforms AST nodes into these shapes.
7
+ */
8
+
9
+ // =============================================================================
10
+ // Enums & Primitives
11
+ // =============================================================================
12
+
13
+ export type IRStateType = 'START' | 'REGULAR' | 'END' | 'CANCELLED';
14
+ export type IRActionMode = 'auto' | 'manual';
15
+
16
+ /**
17
+ * Field type is an open string slug referencing an Element definition.
18
+ *
19
+ * Canonical seed types: text, rich_text, number, boolean, date, datetime,
20
+ * select, multi_select, url, email, phone, currency, percentage, rating,
21
+ * duration, color, file, image, link_to_one, link_to_many, formula, rollup,
22
+ * count, lookup, created_at, updated_at, created_by, auto_number, barcode.
23
+ *
24
+ * User-defined types use slugs like 'my-org/custom-address'.
25
+ * The 14 original types remain valid — this is backward compatible.
26
+ */
27
+ export type IRWorkflowFieldType = string;
28
+
29
+ export type RuntimeProfile = 'backend' | 'collaborative' | 'p2p' | 'local' | 'ephemeral' | 'edge';
30
+
31
+ export interface IRStateHome {
32
+ scope: 'ephemeral' | 'route' | 'instance' | 'global';
33
+ persistence: 'none' | 'local' | 'durable';
34
+ sync: 'none' | 'tenant' | 'p2p';
35
+ }
36
+
37
+ // =============================================================================
38
+ // Action Definitions
39
+ // =============================================================================
40
+
41
+ export interface IRActionDefinition {
42
+ id: string;
43
+ type: string;
44
+ mode: IRActionMode;
45
+ config: Record<string, unknown>;
46
+ condition?: string;
47
+ }
48
+
49
+ // =============================================================================
50
+ // During Actions (scheduled/recurring while in state)
51
+ // =============================================================================
52
+
53
+ export interface IRDuringAction {
54
+ id: string;
55
+ type: 'interval' | 'timeout' | 'poll' | 'once' | 'cron';
56
+ interval_ms?: number;
57
+ cron?: string;
58
+ delay_ms?: number;
59
+ actions: IRActionDefinition[];
60
+ condition?: string;
61
+ }
62
+
63
+ // =============================================================================
64
+ // On-Event Subscriptions (cross-instance)
65
+ // =============================================================================
66
+
67
+ export interface IROnEventAction {
68
+ type: 'set_field' | 'set_memory' | 'log_event' | 'create_instance';
69
+ field?: string;
70
+ expression?: string;
71
+ key?: string;
72
+ message?: string;
73
+ config?: Record<string, unknown>;
74
+ conditions?: string[];
75
+ }
76
+
77
+ export interface IROnEventSubscription {
78
+ match: string;
79
+ description?: string;
80
+ conditions?: string[];
81
+ actions: IROnEventAction[];
82
+ }
83
+
84
+ // =============================================================================
85
+ // State Definition
86
+ // =============================================================================
87
+
88
+ export interface IRStateDefinition {
89
+ name: string;
90
+ description?: string;
91
+ type: IRStateType;
92
+ on_enter: IRActionDefinition[];
93
+ during: IRDuringAction[];
94
+ on_exit: IRActionDefinition[];
95
+ on_event?: IROnEventSubscription[];
96
+ }
97
+
98
+ // =============================================================================
99
+ // Condition Definition
100
+ // =============================================================================
101
+
102
+ export interface IRConditionDefinition {
103
+ type?: string;
104
+ field?: string;
105
+ operator?: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'contains' | 'is_set' | 'is_empty';
106
+ value?: unknown;
107
+ expression?: string;
108
+ OR?: IRConditionDefinition[];
109
+ AND?: IRConditionDefinition[];
110
+ }
111
+
112
+ // =============================================================================
113
+ // Transition Definition
114
+ // =============================================================================
115
+
116
+ export interface IRTransitionDefinition {
117
+ name: string;
118
+ description?: string;
119
+ from: string[];
120
+ to: string;
121
+ conditions?: IRConditionDefinition[];
122
+ actions: IRActionDefinition[];
123
+ roles?: string[];
124
+ auto?: boolean;
125
+ required_fields?: string[];
126
+ }
127
+
128
+ // =============================================================================
129
+ // Field Definition
130
+ // =============================================================================
131
+
132
+ export interface IRFieldValidation {
133
+ min?: number;
134
+ max?: number;
135
+ minLength?: number;
136
+ maxLength?: number;
137
+ options?: string[];
138
+ rules?: Array<{
139
+ expression: string;
140
+ message: string;
141
+ severity: 'error' | 'warning';
142
+ }>;
143
+ }
144
+
145
+ export interface IRFieldDefinition {
146
+ name: string;
147
+ type: IRWorkflowFieldType;
148
+ /** Version pin: { type, typeVersion } is canonical storage/API form */
149
+ typeVersion?: string;
150
+ /** Structural inheritance: storage + widget + primitive intrinsics from base type */
151
+ baseType?: string;
152
+ label?: string;
153
+ required?: boolean;
154
+ default_value?: unknown;
155
+ validation?: IRFieldValidation;
156
+ computed?: string;
157
+ computed_deps?: string[];
158
+ // Field-level ACL (matching backend WorkflowFieldDefinition)
159
+ visible_in_states?: string[];
160
+ editable_in_states?: string[];
161
+ visible_to_roles?: string[];
162
+ editable_by_roles?: string[];
163
+ visible_when?: string;
164
+ editable_when?: string;
165
+ // State home (scope/persistence/sync)
166
+ state_home?: IRStateHome;
167
+ }
168
+
169
+ // =============================================================================
170
+ // Role Definition
171
+ // =============================================================================
172
+
173
+ export interface IRRoleDefinition {
174
+ name: string;
175
+ description?: string;
176
+ permissions: string[];
177
+ }
178
+
179
+ // =============================================================================
180
+ // Workflow Definition
181
+ // =============================================================================
182
+
183
+ export interface IRWorkflowDefinition {
184
+ slug: string;
185
+ name: string;
186
+ version: string;
187
+ description?: string;
188
+ category: string | string[];
189
+ states: IRStateDefinition[];
190
+ transitions: IRTransitionDefinition[];
191
+ fields: IRFieldDefinition[];
192
+ roles: IRRoleDefinition[];
193
+ tags?: Array<{ tag_name: string }>;
194
+ metadata?: Record<string, unknown>;
195
+ on_event?: IROnEventSubscription[];
196
+ extensions?: Record<string, IRGrammarIsland[]>;
197
+ }
198
+
199
+ // =============================================================================
200
+ // Grammar Islands
201
+ // =============================================================================
202
+
203
+ export interface IRGrammarIsland {
204
+ slug: string;
205
+ contextTag: string;
206
+ rawSource: string;
207
+ parsed?: unknown;
208
+ }
209
+
210
+ // =============================================================================
211
+ // Experience Node
212
+ // =============================================================================
213
+
214
+ export interface IRExperienceNode {
215
+ id: string;
216
+ /** User-assigned display name for authoring UIs (e.g., "Hero Section", "Login Form") */
217
+ displayName?: string;
218
+ /** Reference to a registered experience definition (prefab instantiation) */
219
+ experienceId?: string;
220
+ component?: string;
221
+ /** Slot placeholder — rendered by collecting contributions from sub-workflows */
222
+ slot?: string;
223
+ children?: IRExperienceNode[];
224
+ dataSources?: IRDataSource[];
225
+ dataScope?: string;
226
+ layout?: string;
227
+ /** CSS class on the wrapper div */
228
+ className?: string;
229
+ /** Style IR — structured cross-platform styling (opaque to player-core) */
230
+ style?: Record<string, unknown>;
231
+ config?: Record<string, unknown>;
232
+ bindings?: Record<string, string>;
233
+ /** Overrides for a referenced experience's defaults */
234
+ overrides?: Record<string, unknown>;
235
+ visible_when?: string;
236
+ }
237
+
238
+ // =============================================================================
239
+ // Data Sources (discriminated union)
240
+ // =============================================================================
241
+
242
+ export interface IRWorkflowDataSource {
243
+ type: 'workflow';
244
+ name: string;
245
+ slug?: string;
246
+ query: 'latest' | 'list' | 'count';
247
+ /** Fetch a specific instance by ID. Supports template interpolation: "{{entity_id}}" */
248
+ instanceId?: string;
249
+ /** Entity scope for the query (null = no filtering, omitted = inherited) */
250
+ entity?: { type: string; id: string } | null;
251
+ paginated?: boolean;
252
+ pageSize?: number;
253
+ /** Static filters always applied (e.g., { current_state: 'todo' }) */
254
+ filter?: Record<string, string>;
255
+ /** Dynamic filters with bind expressions */
256
+ filters?: Record<string, string | { bind: string }>;
257
+ sort?: string;
258
+ search?: string;
259
+ searchFields?: string[];
260
+ facets?: string[];
261
+ /** Range filters: { field: { min?, max? } } */
262
+ range?: Record<string, { min?: unknown; max?: unknown }>;
263
+ /** Aggregate functions: "field:fn,field:fn" */
264
+ aggregate?: string;
265
+ groupBy?: string;
266
+ parentInstanceId?: string;
267
+ autoStart?: boolean;
268
+ initialData?: Record<string, unknown>;
269
+ includeDefinition?: boolean;
270
+ }
271
+
272
+ export interface IRApiDataSource {
273
+ type: 'api';
274
+ name: string;
275
+ endpoint: string;
276
+ method?: 'GET' | 'POST';
277
+ /** Map API response to InstanceData shape */
278
+ mapping?: {
279
+ id?: string;
280
+ state?: string;
281
+ data?: string;
282
+ items?: string;
283
+ total?: string;
284
+ };
285
+ staleTime?: number;
286
+ }
287
+
288
+ export interface IRRefDataSource {
289
+ type: 'ref';
290
+ name: string;
291
+ expression: string;
292
+ }
293
+
294
+ export interface IRStaticDataSource {
295
+ type: 'static';
296
+ name: string;
297
+ data: Record<string, unknown> | Record<string, unknown>[];
298
+ }
299
+
300
+ export type IRDataSource =
301
+ | IRWorkflowDataSource
302
+ | IRApiDataSource
303
+ | IRRefDataSource
304
+ | IRStaticDataSource;
305
+
306
+ // =============================================================================
307
+ // Experience Definition
308
+ // =============================================================================
309
+
310
+ export interface IRExperienceDefinition {
311
+ slug: string;
312
+ version: string;
313
+ name: string;
314
+ description?: string;
315
+ category: string | string[];
316
+ view_definition: IRExperienceNode;
317
+ workflows: string[];
318
+ children: string[];
319
+ data_bindings: unknown[];
320
+ is_default: boolean;
321
+ }
322
+
323
+ // =============================================================================
324
+ // Blueprint Manifest
325
+ // =============================================================================
326
+
327
+ export interface IRBlueprintManifest {
328
+ workflows: Array<{
329
+ slug: string;
330
+ role: 'primary' | 'child' | 'derived' | 'utility';
331
+ }>;
332
+ experience_id: string;
333
+ routes?: Array<{
334
+ path: string;
335
+ node: string;
336
+ entityType?: string;
337
+ entityIdSource?: 'user' | 'param';
338
+ }>;
339
+ }
340
+
341
+ // =============================================================================
342
+ // Canonical Tree (Pure Form per 041 protocol)
343
+ // =============================================================================
344
+
345
+ /**
346
+ * Canonical workflow tree per spec 041 §1.1.
347
+ * This is the universal type: { slug, category, parts, metadata }.
348
+ *
349
+ * Category invariants (INV-1):
350
+ * category.length >= 1
351
+ * category[0] = primary (dispatch target, must be IDENT)
352
+ * category[1..] = tags (sorted bytewise UTF-8, unique, primary not in tags)
353
+ */
354
+ export interface PureFormWorkflow {
355
+ slug: string;
356
+ category: string[];
357
+ parts?: PureFormWorkflow[];
358
+ metadata?: Record<string, unknown>;
359
+ }
360
+
361
+ /**
362
+ * Compiled output: canonical tree (truth) + lowered IR (cache).
363
+ * Per spec 048 §1.2: canonical tree IS the storage format,
364
+ * IR is a derivable runtime cache.
365
+ */
366
+ export interface CompiledOutput {
367
+ canonical: PureFormWorkflow;
368
+ ir: IRWorkflowDefinition;
369
+ }
370
+
371
+ /**
372
+ * Normalizes a category array to satisfy INV-1:
373
+ * - category[0] = primary (unchanged)
374
+ * - category[1..] = sorted bytewise UTF-8, unique, primary excluded
375
+ */
376
+ export function normalizeCategory(primary: string, ...tags: string[]): string[] {
377
+ const uniqueTags = [...new Set(tags.filter(t => t !== primary))];
378
+ uniqueTags.sort();
379
+ return [primary, ...uniqueTags];
380
+ }
381
+
382
+ // =============================================================================
383
+ // Compilation Output
384
+ // =============================================================================
385
+
386
+ export type CompilerErrorCode =
387
+ | 'MISSING_STARTS_AT'
388
+ | 'UNKNOWN_TARGET_STATE'
389
+ | 'DUPLICATE_FIELD'
390
+ | 'DUPLICATE_STATE'
391
+ | 'UNKNOWN_FIELD_TYPE'
392
+ | 'INVALID_EXPRESSION'
393
+ | 'UNKNOWN_FRAGMENT'
394
+ | 'EMPTY_WORKFLOW'
395
+ | 'STRICT_USE_EFFECT'
396
+ | 'STRICT_USE_REF'
397
+ | 'STRICT_USE_MEMO'
398
+ | 'STRICT_USE_CALLBACK'
399
+ | 'STRICT_USE_LAYOUT_EFFECT'
400
+ | 'STRICT_FORBIDDEN_IMPORT'
401
+ | 'INFER_RAW_JSX';
402
+
403
+ export interface CompilerError {
404
+ code: CompilerErrorCode;
405
+ message: string;
406
+ lineNumber?: number;
407
+ severity: 'error' | 'warning';
408
+ }
409
+
410
+ export interface CompilationResult {
411
+ workflows: IRWorkflowDefinition[];
412
+ experiences: IRExperienceDefinition[];
413
+ manifest?: IRBlueprintManifest;
414
+ errors: CompilerError[];
415
+ warnings: CompilerError[];
416
+ }