@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.
- package/dist/index.d.mts +1436 -0
- package/dist/index.d.ts +1436 -0
- package/dist/index.js +4828 -0
- package/dist/index.mjs +4762 -0
- package/package.json +35 -0
- package/package.json.backup +35 -0
- package/src/__tests__/actions.test.ts +187 -0
- package/src/__tests__/blueprint-e2e.test.ts +706 -0
- package/src/__tests__/blueprint-test-runner.test.ts +680 -0
- package/src/__tests__/core-functions.test.ts +78 -0
- package/src/__tests__/dsl-compiler.test.ts +1382 -0
- package/src/__tests__/dsl-grammar.test.ts +1682 -0
- package/src/__tests__/events.test.ts +200 -0
- package/src/__tests__/expression.test.ts +296 -0
- package/src/__tests__/failure-policies.test.ts +110 -0
- package/src/__tests__/frontend-context.test.ts +182 -0
- package/src/__tests__/integration.test.ts +256 -0
- package/src/__tests__/security.test.ts +190 -0
- package/src/__tests__/state-machine.test.ts +450 -0
- package/src/__tests__/testing-engine.test.ts +671 -0
- package/src/actions/dispatcher.ts +80 -0
- package/src/actions/index.ts +7 -0
- package/src/actions/types.ts +25 -0
- package/src/dsl/compiler/component-mapper.ts +289 -0
- package/src/dsl/compiler/field-mapper.ts +187 -0
- package/src/dsl/compiler/index.ts +82 -0
- package/src/dsl/compiler/manifest-compiler.ts +76 -0
- package/src/dsl/compiler/symbol-table.ts +214 -0
- package/src/dsl/compiler/utils.ts +48 -0
- package/src/dsl/compiler/view-compiler.ts +286 -0
- package/src/dsl/compiler/workflow-compiler.ts +600 -0
- package/src/dsl/index.ts +66 -0
- package/src/dsl/ir-migration.ts +221 -0
- package/src/dsl/ir-types.ts +416 -0
- package/src/dsl/lexer.ts +579 -0
- package/src/dsl/parser.ts +115 -0
- package/src/dsl/types.ts +256 -0
- package/src/events/event-bus.ts +68 -0
- package/src/events/index.ts +9 -0
- package/src/events/pattern-matcher.ts +61 -0
- package/src/events/types.ts +27 -0
- package/src/expression/evaluator.ts +676 -0
- package/src/expression/functions.ts +214 -0
- package/src/expression/index.ts +13 -0
- package/src/expression/types.ts +64 -0
- package/src/index.ts +61 -0
- package/src/state-machine/index.ts +16 -0
- package/src/state-machine/interpreter.ts +319 -0
- package/src/state-machine/types.ts +89 -0
- package/src/testing/action-trace.ts +209 -0
- package/src/testing/blueprint-test-runner.ts +214 -0
- package/src/testing/graph-walker.ts +249 -0
- package/src/testing/index.ts +69 -0
- package/src/testing/nrt-comparator.ts +199 -0
- package/src/testing/nrt-types.ts +230 -0
- package/src/testing/test-actions.ts +645 -0
- package/src/testing/test-compiler.ts +278 -0
- package/src/testing/test-runner.ts +444 -0
- package/src/testing/types.ts +231 -0
- package/src/validation/definition-validator.ts +812 -0
- package/src/validation/index.ts +13 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4828 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ActionDispatcher: () => ActionDispatcher,
|
|
24
|
+
CORE_FUNCTIONS: () => CORE_FUNCTIONS,
|
|
25
|
+
CURRENT_IR_VERSION: () => CURRENT_IR_VERSION,
|
|
26
|
+
EventBus: () => EventBus,
|
|
27
|
+
StateMachine: () => StateMachine,
|
|
28
|
+
WEB_FAILURE_POLICIES: () => WEB_FAILURE_POLICIES,
|
|
29
|
+
analyzeDefinition: () => analyzeDefinition,
|
|
30
|
+
buildFunctionMap: () => buildFunctionMap,
|
|
31
|
+
clearExpressionCache: () => clearExpressionCache,
|
|
32
|
+
clearPatternCache: () => clearPatternCache,
|
|
33
|
+
compareNRT: () => compareNRT,
|
|
34
|
+
compile: () => compile,
|
|
35
|
+
compilePattern: () => compilePattern,
|
|
36
|
+
compileTestProgram: () => compileTestProgram,
|
|
37
|
+
compileTestScenario: () => compileTestScenario,
|
|
38
|
+
countByKind: () => countByKind,
|
|
39
|
+
countNodes: () => countNodes,
|
|
40
|
+
createActionRecorder: () => createActionRecorder,
|
|
41
|
+
createApiTestActions: () => createApiTestActions,
|
|
42
|
+
createEmptyNRT: () => createEmptyNRT,
|
|
43
|
+
createEvaluator: () => createEvaluator,
|
|
44
|
+
createInProcessTestActions: () => createInProcessTestActions,
|
|
45
|
+
detectIRVersion: () => detectIRVersion,
|
|
46
|
+
findInteractiveNodes: () => findInteractiveNodes,
|
|
47
|
+
findNode: () => findNode,
|
|
48
|
+
findVisibleNodes: () => findVisibleNodes,
|
|
49
|
+
generateCoverageScenarios: () => generateCoverageScenarios,
|
|
50
|
+
getFinalState: () => getFinalState,
|
|
51
|
+
getTransitionPath: () => getTransitionPath,
|
|
52
|
+
hasTransition: () => hasTransition,
|
|
53
|
+
isViableDefinition: () => isViableDefinition,
|
|
54
|
+
matchTopic: () => matchTopic,
|
|
55
|
+
needsMigration: () => needsMigration,
|
|
56
|
+
normalizeCategory: () => normalizeCategory,
|
|
57
|
+
normalizeDefinition: () => normalizeDefinition,
|
|
58
|
+
runBlueprintScenario: () => runBlueprintScenario,
|
|
59
|
+
runBlueprintTestProgram: () => runBlueprintTestProgram,
|
|
60
|
+
runScenario: () => runScenario,
|
|
61
|
+
runTestProgram: () => runTestProgram,
|
|
62
|
+
validateDefinition: () => validateDefinition
|
|
63
|
+
});
|
|
64
|
+
module.exports = __toCommonJS(index_exports);
|
|
65
|
+
|
|
66
|
+
// src/expression/functions.ts
|
|
67
|
+
var add = {
|
|
68
|
+
name: "add",
|
|
69
|
+
fn: (a, b) => Number(a) + Number(b),
|
|
70
|
+
arity: 2
|
|
71
|
+
};
|
|
72
|
+
var subtract = {
|
|
73
|
+
name: "subtract",
|
|
74
|
+
fn: (a, b) => Number(a) - Number(b),
|
|
75
|
+
arity: 2
|
|
76
|
+
};
|
|
77
|
+
var multiply = {
|
|
78
|
+
name: "multiply",
|
|
79
|
+
fn: (a, b) => Number(a) * Number(b),
|
|
80
|
+
arity: 2
|
|
81
|
+
};
|
|
82
|
+
var divide = {
|
|
83
|
+
name: "divide",
|
|
84
|
+
fn: (a, b) => {
|
|
85
|
+
const d = Number(b);
|
|
86
|
+
return d === 0 ? 0 : Number(a) / d;
|
|
87
|
+
},
|
|
88
|
+
arity: 2
|
|
89
|
+
};
|
|
90
|
+
var abs = {
|
|
91
|
+
name: "abs",
|
|
92
|
+
fn: (a) => Math.abs(Number(a)),
|
|
93
|
+
arity: 1
|
|
94
|
+
};
|
|
95
|
+
var round = {
|
|
96
|
+
name: "round",
|
|
97
|
+
fn: (a, decimals) => {
|
|
98
|
+
const d = decimals != null ? Number(decimals) : 0;
|
|
99
|
+
const factor = Math.pow(10, d);
|
|
100
|
+
return Math.round(Number(a) * factor) / factor;
|
|
101
|
+
},
|
|
102
|
+
arity: -1
|
|
103
|
+
};
|
|
104
|
+
var min = {
|
|
105
|
+
name: "min",
|
|
106
|
+
fn: (...args) => {
|
|
107
|
+
const nums = args.flat().map(Number).filter((n) => !isNaN(n));
|
|
108
|
+
return nums.length === 0 ? 0 : Math.min(...nums);
|
|
109
|
+
},
|
|
110
|
+
arity: -1
|
|
111
|
+
};
|
|
112
|
+
var max = {
|
|
113
|
+
name: "max",
|
|
114
|
+
fn: (...args) => {
|
|
115
|
+
const nums = args.flat().map(Number).filter((n) => !isNaN(n));
|
|
116
|
+
return nums.length === 0 ? 0 : Math.max(...nums);
|
|
117
|
+
},
|
|
118
|
+
arity: -1
|
|
119
|
+
};
|
|
120
|
+
var eq = {
|
|
121
|
+
name: "eq",
|
|
122
|
+
fn: (a, b) => a === b || String(a) === String(b),
|
|
123
|
+
arity: 2
|
|
124
|
+
};
|
|
125
|
+
var neq = {
|
|
126
|
+
name: "neq",
|
|
127
|
+
fn: (a, b) => a !== b && String(a) !== String(b),
|
|
128
|
+
arity: 2
|
|
129
|
+
};
|
|
130
|
+
var gt = {
|
|
131
|
+
name: "gt",
|
|
132
|
+
fn: (a, b) => Number(a) > Number(b),
|
|
133
|
+
arity: 2
|
|
134
|
+
};
|
|
135
|
+
var gte = {
|
|
136
|
+
name: "gte",
|
|
137
|
+
fn: (a, b) => Number(a) >= Number(b),
|
|
138
|
+
arity: 2
|
|
139
|
+
};
|
|
140
|
+
var lt = {
|
|
141
|
+
name: "lt",
|
|
142
|
+
fn: (a, b) => Number(a) < Number(b),
|
|
143
|
+
arity: 2
|
|
144
|
+
};
|
|
145
|
+
var lte = {
|
|
146
|
+
name: "lte",
|
|
147
|
+
fn: (a, b) => Number(a) <= Number(b),
|
|
148
|
+
arity: 2
|
|
149
|
+
};
|
|
150
|
+
var if_fn = {
|
|
151
|
+
name: "if",
|
|
152
|
+
fn: (cond, then, else_) => cond ? then : else_,
|
|
153
|
+
arity: 3
|
|
154
|
+
};
|
|
155
|
+
var and = {
|
|
156
|
+
name: "and",
|
|
157
|
+
fn: (...args) => args.every(Boolean),
|
|
158
|
+
arity: -1
|
|
159
|
+
};
|
|
160
|
+
var or = {
|
|
161
|
+
name: "or",
|
|
162
|
+
fn: (...args) => args.some(Boolean),
|
|
163
|
+
arity: -1
|
|
164
|
+
};
|
|
165
|
+
var not = {
|
|
166
|
+
name: "not",
|
|
167
|
+
fn: (a) => !a,
|
|
168
|
+
arity: 1
|
|
169
|
+
};
|
|
170
|
+
var coalesce = {
|
|
171
|
+
name: "coalesce",
|
|
172
|
+
fn: (...args) => {
|
|
173
|
+
for (const arg of args) {
|
|
174
|
+
if (arg != null) return arg;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
},
|
|
178
|
+
arity: -1
|
|
179
|
+
};
|
|
180
|
+
var concat = {
|
|
181
|
+
name: "concat",
|
|
182
|
+
fn: (...args) => args.map(String).join(""),
|
|
183
|
+
arity: -1
|
|
184
|
+
};
|
|
185
|
+
var upper = {
|
|
186
|
+
name: "upper",
|
|
187
|
+
fn: (s) => String(s ?? "").toUpperCase(),
|
|
188
|
+
arity: 1
|
|
189
|
+
};
|
|
190
|
+
var lower = {
|
|
191
|
+
name: "lower",
|
|
192
|
+
fn: (s) => String(s ?? "").toLowerCase(),
|
|
193
|
+
arity: 1
|
|
194
|
+
};
|
|
195
|
+
var trim = {
|
|
196
|
+
name: "trim",
|
|
197
|
+
fn: (s) => String(s ?? "").trim(),
|
|
198
|
+
arity: 1
|
|
199
|
+
};
|
|
200
|
+
var format = {
|
|
201
|
+
name: "format",
|
|
202
|
+
fn: (template, ...args) => {
|
|
203
|
+
let result = String(template ?? "");
|
|
204
|
+
args.forEach((arg, i) => {
|
|
205
|
+
result = result.replace(`{${i}}`, String(arg ?? ""));
|
|
206
|
+
});
|
|
207
|
+
return result;
|
|
208
|
+
},
|
|
209
|
+
arity: -1
|
|
210
|
+
};
|
|
211
|
+
var length = {
|
|
212
|
+
name: "length",
|
|
213
|
+
fn: (v) => {
|
|
214
|
+
if (Array.isArray(v)) return v.length;
|
|
215
|
+
if (typeof v === "string") return v.length;
|
|
216
|
+
if (v && typeof v === "object") return Object.keys(v).length;
|
|
217
|
+
return 0;
|
|
218
|
+
},
|
|
219
|
+
arity: 1
|
|
220
|
+
};
|
|
221
|
+
var get = {
|
|
222
|
+
name: "get",
|
|
223
|
+
fn: (obj, path) => {
|
|
224
|
+
if (obj == null || typeof path !== "string") return void 0;
|
|
225
|
+
const parts = path.split(".");
|
|
226
|
+
let current = obj;
|
|
227
|
+
for (const part of parts) {
|
|
228
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
229
|
+
current = current[part];
|
|
230
|
+
}
|
|
231
|
+
return current;
|
|
232
|
+
},
|
|
233
|
+
arity: 2
|
|
234
|
+
};
|
|
235
|
+
var includes = {
|
|
236
|
+
name: "includes",
|
|
237
|
+
fn: (collection, value) => {
|
|
238
|
+
if (Array.isArray(collection)) return collection.includes(value);
|
|
239
|
+
if (typeof collection === "string") return collection.includes(String(value));
|
|
240
|
+
return false;
|
|
241
|
+
},
|
|
242
|
+
arity: 2
|
|
243
|
+
};
|
|
244
|
+
var is_defined = {
|
|
245
|
+
name: "is_defined",
|
|
246
|
+
fn: (v) => v !== void 0 && v !== null,
|
|
247
|
+
arity: 1
|
|
248
|
+
};
|
|
249
|
+
var is_empty = {
|
|
250
|
+
name: "is_empty",
|
|
251
|
+
fn: (v) => {
|
|
252
|
+
if (v == null) return true;
|
|
253
|
+
if (typeof v === "string") return v.length === 0;
|
|
254
|
+
if (Array.isArray(v)) return v.length === 0;
|
|
255
|
+
if (typeof v === "object") return Object.keys(v).length === 0;
|
|
256
|
+
return false;
|
|
257
|
+
},
|
|
258
|
+
arity: 1
|
|
259
|
+
};
|
|
260
|
+
var is_null = {
|
|
261
|
+
name: "is_null",
|
|
262
|
+
fn: (v) => v === null || v === void 0,
|
|
263
|
+
arity: 1
|
|
264
|
+
};
|
|
265
|
+
var to_string = {
|
|
266
|
+
name: "to_string",
|
|
267
|
+
fn: (v) => {
|
|
268
|
+
if (v == null) return "";
|
|
269
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
270
|
+
return String(v);
|
|
271
|
+
},
|
|
272
|
+
arity: 1
|
|
273
|
+
};
|
|
274
|
+
var CORE_FUNCTIONS = [
|
|
275
|
+
// Math (8)
|
|
276
|
+
add,
|
|
277
|
+
subtract,
|
|
278
|
+
multiply,
|
|
279
|
+
divide,
|
|
280
|
+
abs,
|
|
281
|
+
round,
|
|
282
|
+
min,
|
|
283
|
+
max,
|
|
284
|
+
// Comparison (6)
|
|
285
|
+
eq,
|
|
286
|
+
neq,
|
|
287
|
+
gt,
|
|
288
|
+
gte,
|
|
289
|
+
lt,
|
|
290
|
+
lte,
|
|
291
|
+
// Logic (5)
|
|
292
|
+
if_fn,
|
|
293
|
+
and,
|
|
294
|
+
or,
|
|
295
|
+
not,
|
|
296
|
+
coalesce,
|
|
297
|
+
// String (6)
|
|
298
|
+
concat,
|
|
299
|
+
upper,
|
|
300
|
+
lower,
|
|
301
|
+
trim,
|
|
302
|
+
format,
|
|
303
|
+
length,
|
|
304
|
+
// Path (3)
|
|
305
|
+
get,
|
|
306
|
+
includes,
|
|
307
|
+
is_defined,
|
|
308
|
+
// Type (3)
|
|
309
|
+
is_empty,
|
|
310
|
+
is_null,
|
|
311
|
+
to_string
|
|
312
|
+
];
|
|
313
|
+
function buildFunctionMap(functions) {
|
|
314
|
+
const map = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const fn of functions) {
|
|
316
|
+
map.set(fn.name, fn.fn);
|
|
317
|
+
}
|
|
318
|
+
return map;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/expression/evaluator.ts
|
|
322
|
+
var MAX_DEPTH = 50;
|
|
323
|
+
var Parser = class {
|
|
324
|
+
pos = 0;
|
|
325
|
+
depth = 0;
|
|
326
|
+
input;
|
|
327
|
+
constructor(input) {
|
|
328
|
+
this.input = input;
|
|
329
|
+
}
|
|
330
|
+
parse() {
|
|
331
|
+
this.skipWhitespace();
|
|
332
|
+
const node = this.parseExpression();
|
|
333
|
+
this.skipWhitespace();
|
|
334
|
+
if (this.pos < this.input.length) {
|
|
335
|
+
throw new Error(`Unexpected character at position ${this.pos}: '${this.input[this.pos]}'`);
|
|
336
|
+
}
|
|
337
|
+
return node;
|
|
338
|
+
}
|
|
339
|
+
guardDepth() {
|
|
340
|
+
if (++this.depth > MAX_DEPTH) {
|
|
341
|
+
throw new Error("Expression too deeply nested");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
parseExpression() {
|
|
345
|
+
this.guardDepth();
|
|
346
|
+
try {
|
|
347
|
+
return this.parseTernary();
|
|
348
|
+
} finally {
|
|
349
|
+
this.depth--;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
parseTernary() {
|
|
353
|
+
let node = this.parseLogicalOr();
|
|
354
|
+
this.skipWhitespace();
|
|
355
|
+
if (this.peek() === "?") {
|
|
356
|
+
this.advance();
|
|
357
|
+
const consequent = this.parseExpression();
|
|
358
|
+
this.skipWhitespace();
|
|
359
|
+
this.expect(":");
|
|
360
|
+
const alternate = this.parseExpression();
|
|
361
|
+
node = { type: "ternary", condition: node, consequent, alternate };
|
|
362
|
+
}
|
|
363
|
+
return node;
|
|
364
|
+
}
|
|
365
|
+
parseLogicalOr() {
|
|
366
|
+
let left = this.parseLogicalAnd();
|
|
367
|
+
this.skipWhitespace();
|
|
368
|
+
while (this.match("||")) {
|
|
369
|
+
const right = this.parseLogicalAnd();
|
|
370
|
+
left = { type: "binary", operator: "||", left, right };
|
|
371
|
+
this.skipWhitespace();
|
|
372
|
+
}
|
|
373
|
+
return left;
|
|
374
|
+
}
|
|
375
|
+
parseLogicalAnd() {
|
|
376
|
+
let left = this.parseEquality();
|
|
377
|
+
this.skipWhitespace();
|
|
378
|
+
while (this.match("&&")) {
|
|
379
|
+
const right = this.parseEquality();
|
|
380
|
+
left = { type: "binary", operator: "&&", left, right };
|
|
381
|
+
this.skipWhitespace();
|
|
382
|
+
}
|
|
383
|
+
return left;
|
|
384
|
+
}
|
|
385
|
+
parseEquality() {
|
|
386
|
+
let left = this.parseComparison();
|
|
387
|
+
this.skipWhitespace();
|
|
388
|
+
while (true) {
|
|
389
|
+
if (this.match("==")) {
|
|
390
|
+
const right = this.parseComparison();
|
|
391
|
+
left = { type: "binary", operator: "==", left, right };
|
|
392
|
+
} else if (this.match("!=")) {
|
|
393
|
+
const right = this.parseComparison();
|
|
394
|
+
left = { type: "binary", operator: "!=", left, right };
|
|
395
|
+
} else {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
this.skipWhitespace();
|
|
399
|
+
}
|
|
400
|
+
return left;
|
|
401
|
+
}
|
|
402
|
+
parseComparison() {
|
|
403
|
+
let left = this.parseUnary();
|
|
404
|
+
this.skipWhitespace();
|
|
405
|
+
while (true) {
|
|
406
|
+
if (this.match(">=")) {
|
|
407
|
+
const right = this.parseUnary();
|
|
408
|
+
left = { type: "binary", operator: ">=", left, right };
|
|
409
|
+
} else if (this.match("<=")) {
|
|
410
|
+
const right = this.parseUnary();
|
|
411
|
+
left = { type: "binary", operator: "<=", left, right };
|
|
412
|
+
} else if (this.peek() === ">" && !this.lookAhead(">=")) {
|
|
413
|
+
this.advance();
|
|
414
|
+
const right = this.parseUnary();
|
|
415
|
+
left = { type: "binary", operator: ">", left, right };
|
|
416
|
+
} else if (this.peek() === "<" && !this.lookAhead("<=")) {
|
|
417
|
+
this.advance();
|
|
418
|
+
const right = this.parseUnary();
|
|
419
|
+
left = { type: "binary", operator: "<", left, right };
|
|
420
|
+
} else {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
this.skipWhitespace();
|
|
424
|
+
}
|
|
425
|
+
return left;
|
|
426
|
+
}
|
|
427
|
+
parseUnary() {
|
|
428
|
+
this.skipWhitespace();
|
|
429
|
+
if (this.peek() === "!") {
|
|
430
|
+
this.advance();
|
|
431
|
+
const operand = this.parseUnary();
|
|
432
|
+
return { type: "unary", operator: "!", operand };
|
|
433
|
+
}
|
|
434
|
+
if (this.peek() === "-") {
|
|
435
|
+
const nextChar = this.input[this.pos + 1];
|
|
436
|
+
if (nextChar !== void 0 && (nextChar >= "0" && nextChar <= "9" || nextChar === ".")) {
|
|
437
|
+
return this.parseCallChain();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return this.parseCallChain();
|
|
441
|
+
}
|
|
442
|
+
parseCallChain() {
|
|
443
|
+
let node = this.parsePrimary();
|
|
444
|
+
while (true) {
|
|
445
|
+
this.skipWhitespace();
|
|
446
|
+
if (this.peek() === "(") {
|
|
447
|
+
this.advance();
|
|
448
|
+
const args = this.parseArgList();
|
|
449
|
+
this.expect(")");
|
|
450
|
+
if (node.type === "identifier") {
|
|
451
|
+
node = { type: "call", name: node.name, args };
|
|
452
|
+
} else if (node.type === "path") {
|
|
453
|
+
const name = node.segments.join(".");
|
|
454
|
+
node = { type: "call", name, args };
|
|
455
|
+
} else if (node.type === "member") {
|
|
456
|
+
node = { type: "method_call", object: node.object, method: node.property, args };
|
|
457
|
+
} else {
|
|
458
|
+
throw new Error("Cannot call non-function");
|
|
459
|
+
}
|
|
460
|
+
} else if (this.peek() === ".") {
|
|
461
|
+
this.advance();
|
|
462
|
+
const prop = this.parseIdentifierName();
|
|
463
|
+
node = { type: "member", object: node, property: prop };
|
|
464
|
+
} else {
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return node;
|
|
469
|
+
}
|
|
470
|
+
parsePrimary() {
|
|
471
|
+
this.skipWhitespace();
|
|
472
|
+
const ch = this.peek();
|
|
473
|
+
if (ch === "(") {
|
|
474
|
+
this.advance();
|
|
475
|
+
const expr = this.parseExpression();
|
|
476
|
+
this.skipWhitespace();
|
|
477
|
+
this.expect(")");
|
|
478
|
+
return expr;
|
|
479
|
+
}
|
|
480
|
+
if (ch === "'" || ch === '"') {
|
|
481
|
+
return this.parseString();
|
|
482
|
+
}
|
|
483
|
+
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
484
|
+
return this.parseNumber();
|
|
485
|
+
}
|
|
486
|
+
if (this.isIdentStart(ch)) {
|
|
487
|
+
return this.parseIdentifierOrPath();
|
|
488
|
+
}
|
|
489
|
+
throw new Error(
|
|
490
|
+
`Unexpected character at position ${this.pos}: '${ch || "EOF"}'`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
parseString() {
|
|
494
|
+
const quote = this.advance();
|
|
495
|
+
let value = "";
|
|
496
|
+
while (this.pos < this.input.length && this.peek() !== quote) {
|
|
497
|
+
if (this.peek() === "\\") {
|
|
498
|
+
this.advance();
|
|
499
|
+
const esc = this.advance();
|
|
500
|
+
if (esc === "n") value += "\n";
|
|
501
|
+
else if (esc === "t") value += " ";
|
|
502
|
+
else if (esc === "r") value += "\r";
|
|
503
|
+
else value += esc;
|
|
504
|
+
} else {
|
|
505
|
+
value += this.advance();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (this.pos >= this.input.length) {
|
|
509
|
+
throw new Error("Unterminated string literal");
|
|
510
|
+
}
|
|
511
|
+
this.advance();
|
|
512
|
+
return { type: "string", value };
|
|
513
|
+
}
|
|
514
|
+
parseNumber() {
|
|
515
|
+
let numStr = "";
|
|
516
|
+
if (this.peek() === "-") {
|
|
517
|
+
numStr += this.advance();
|
|
518
|
+
}
|
|
519
|
+
while (this.pos < this.input.length && (this.input[this.pos] >= "0" && this.input[this.pos] <= "9")) {
|
|
520
|
+
numStr += this.advance();
|
|
521
|
+
}
|
|
522
|
+
if (this.peek() === "." && this.pos + 1 < this.input.length && this.input[this.pos + 1] >= "0" && this.input[this.pos + 1] <= "9") {
|
|
523
|
+
numStr += this.advance();
|
|
524
|
+
while (this.pos < this.input.length && (this.input[this.pos] >= "0" && this.input[this.pos] <= "9")) {
|
|
525
|
+
numStr += this.advance();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return { type: "number", value: Number(numStr) };
|
|
529
|
+
}
|
|
530
|
+
parseIdentifierOrPath() {
|
|
531
|
+
const name = this.parseIdentifierName();
|
|
532
|
+
if (name === "true") return { type: "boolean", value: true };
|
|
533
|
+
if (name === "false") return { type: "boolean", value: false };
|
|
534
|
+
if (name === "null") return { type: "null" };
|
|
535
|
+
if (name === "undefined") return { type: "null" };
|
|
536
|
+
return { type: "identifier", name };
|
|
537
|
+
}
|
|
538
|
+
parseIdentifierName() {
|
|
539
|
+
let name = "";
|
|
540
|
+
if (this.peek() === "$") name += this.advance();
|
|
541
|
+
while (this.pos < this.input.length && this.isIdentPart(this.input[this.pos])) {
|
|
542
|
+
name += this.advance();
|
|
543
|
+
}
|
|
544
|
+
if (!name) {
|
|
545
|
+
throw new Error(`Expected identifier at position ${this.pos}`);
|
|
546
|
+
}
|
|
547
|
+
return name;
|
|
548
|
+
}
|
|
549
|
+
parseArgList() {
|
|
550
|
+
this.skipWhitespace();
|
|
551
|
+
if (this.peek() === ")") return [];
|
|
552
|
+
const args = [];
|
|
553
|
+
args.push(this.parseExpression());
|
|
554
|
+
this.skipWhitespace();
|
|
555
|
+
while (this.peek() === ",") {
|
|
556
|
+
this.advance();
|
|
557
|
+
args.push(this.parseExpression());
|
|
558
|
+
this.skipWhitespace();
|
|
559
|
+
}
|
|
560
|
+
return args;
|
|
561
|
+
}
|
|
562
|
+
// Character utilities
|
|
563
|
+
peek() {
|
|
564
|
+
return this.input[this.pos] ?? "";
|
|
565
|
+
}
|
|
566
|
+
advance() {
|
|
567
|
+
return this.input[this.pos++] ?? "";
|
|
568
|
+
}
|
|
569
|
+
match(str) {
|
|
570
|
+
if (this.input.startsWith(str, this.pos)) {
|
|
571
|
+
this.pos += str.length;
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
lookAhead(str) {
|
|
577
|
+
return this.input.startsWith(str, this.pos);
|
|
578
|
+
}
|
|
579
|
+
expect(ch) {
|
|
580
|
+
this.skipWhitespace();
|
|
581
|
+
if (this.peek() !== ch) {
|
|
582
|
+
throw new Error(`Expected '${ch}' at position ${this.pos}, got '${this.peek() || "EOF"}'`);
|
|
583
|
+
}
|
|
584
|
+
this.advance();
|
|
585
|
+
}
|
|
586
|
+
skipWhitespace() {
|
|
587
|
+
while (this.pos < this.input.length && " \n\r".includes(this.input[this.pos])) {
|
|
588
|
+
this.pos++;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
isIdentStart(ch) {
|
|
592
|
+
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_" || ch === "$";
|
|
593
|
+
}
|
|
594
|
+
isIdentPart(ch) {
|
|
595
|
+
return this.isIdentStart(ch) || ch >= "0" && ch <= "9";
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function evaluateAST(node, context, fnMap) {
|
|
599
|
+
switch (node.type) {
|
|
600
|
+
case "number":
|
|
601
|
+
return node.value;
|
|
602
|
+
case "string":
|
|
603
|
+
return node.value;
|
|
604
|
+
case "boolean":
|
|
605
|
+
return node.value;
|
|
606
|
+
case "null":
|
|
607
|
+
return null;
|
|
608
|
+
case "identifier":
|
|
609
|
+
return resolvePath(node.name, context);
|
|
610
|
+
case "path":
|
|
611
|
+
return resolvePath(node.segments.join("."), context);
|
|
612
|
+
case "member": {
|
|
613
|
+
const obj = evaluateAST(node.object, context, fnMap);
|
|
614
|
+
if (obj == null || typeof obj !== "object") return void 0;
|
|
615
|
+
return obj[node.property];
|
|
616
|
+
}
|
|
617
|
+
case "call": {
|
|
618
|
+
const fn = fnMap.get(node.name);
|
|
619
|
+
if (!fn) return void 0;
|
|
620
|
+
const args = node.args.map((a) => evaluateAST(a, context, fnMap));
|
|
621
|
+
return fn(...args);
|
|
622
|
+
}
|
|
623
|
+
case "method_call": {
|
|
624
|
+
const obj = evaluateAST(node.object, context, fnMap);
|
|
625
|
+
if (obj != null && typeof obj === "object") {
|
|
626
|
+
const method = obj[node.method];
|
|
627
|
+
if (typeof method === "function") {
|
|
628
|
+
const args = node.args.map((a) => evaluateAST(a, context, fnMap));
|
|
629
|
+
return method.apply(obj, args);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
case "unary": {
|
|
635
|
+
const operand = evaluateAST(node.operand, context, fnMap);
|
|
636
|
+
return !operand;
|
|
637
|
+
}
|
|
638
|
+
case "binary": {
|
|
639
|
+
if (node.operator === "&&") {
|
|
640
|
+
const left2 = evaluateAST(node.left, context, fnMap);
|
|
641
|
+
if (!left2) return left2;
|
|
642
|
+
return evaluateAST(node.right, context, fnMap);
|
|
643
|
+
}
|
|
644
|
+
if (node.operator === "||") {
|
|
645
|
+
const left2 = evaluateAST(node.left, context, fnMap);
|
|
646
|
+
if (left2) return left2;
|
|
647
|
+
return evaluateAST(node.right, context, fnMap);
|
|
648
|
+
}
|
|
649
|
+
const left = evaluateAST(node.left, context, fnMap);
|
|
650
|
+
const right = evaluateAST(node.right, context, fnMap);
|
|
651
|
+
switch (node.operator) {
|
|
652
|
+
// eslint-disable-next-line eqeqeq
|
|
653
|
+
case "==":
|
|
654
|
+
return left == right;
|
|
655
|
+
// eslint-disable-next-line eqeqeq
|
|
656
|
+
case "!=":
|
|
657
|
+
return left != right;
|
|
658
|
+
case ">":
|
|
659
|
+
return Number(left) > Number(right);
|
|
660
|
+
case "<":
|
|
661
|
+
return Number(left) < Number(right);
|
|
662
|
+
case ">=":
|
|
663
|
+
return Number(left) >= Number(right);
|
|
664
|
+
case "<=":
|
|
665
|
+
return Number(left) <= Number(right);
|
|
666
|
+
default:
|
|
667
|
+
return void 0;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
case "ternary": {
|
|
671
|
+
const condition = evaluateAST(node.condition, context, fnMap);
|
|
672
|
+
return condition ? evaluateAST(node.consequent, context, fnMap) : evaluateAST(node.alternate, context, fnMap);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
var MAX_CACHE = 500;
|
|
677
|
+
var astCache = /* @__PURE__ */ new Map();
|
|
678
|
+
function evictIfNeeded() {
|
|
679
|
+
if (astCache.size > MAX_CACHE) {
|
|
680
|
+
const keys = Array.from(astCache.keys());
|
|
681
|
+
const evictCount = Math.floor(MAX_CACHE * 0.25);
|
|
682
|
+
for (let i = 0; i < evictCount; i++) {
|
|
683
|
+
astCache.delete(keys[i]);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function parseAndCache(expr) {
|
|
688
|
+
const cached = astCache.get(expr);
|
|
689
|
+
if (cached) return cached;
|
|
690
|
+
const parser = new Parser(expr);
|
|
691
|
+
const ast = parser.parse();
|
|
692
|
+
astCache.set(expr, ast);
|
|
693
|
+
evictIfNeeded();
|
|
694
|
+
return ast;
|
|
695
|
+
}
|
|
696
|
+
var TEMPLATE_RE = /\{\{(.+?)\}\}/g;
|
|
697
|
+
function resolvePath(path, context) {
|
|
698
|
+
const parts = path.split(".");
|
|
699
|
+
let current = context;
|
|
700
|
+
for (const part of parts) {
|
|
701
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
702
|
+
current = current[part];
|
|
703
|
+
}
|
|
704
|
+
return current;
|
|
705
|
+
}
|
|
706
|
+
function evaluateExpression(expr, context, fnMap) {
|
|
707
|
+
const trimmed = expr.trim();
|
|
708
|
+
if (trimmed === "true") return true;
|
|
709
|
+
if (trimmed === "false") return false;
|
|
710
|
+
if (trimmed === "null") return null;
|
|
711
|
+
if (trimmed === "undefined") return void 0;
|
|
712
|
+
const num = Number(trimmed);
|
|
713
|
+
if (!isNaN(num) && trimmed !== "") return num;
|
|
714
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
715
|
+
return trimmed.slice(1, -1);
|
|
716
|
+
}
|
|
717
|
+
if (/^[a-zA-Z_$][\w$.]*$/.test(trimmed)) {
|
|
718
|
+
return resolvePath(trimmed, context);
|
|
719
|
+
}
|
|
720
|
+
const ast = parseAndCache(trimmed);
|
|
721
|
+
return evaluateAST(ast, context, fnMap);
|
|
722
|
+
}
|
|
723
|
+
var WEB_FAILURE_POLICIES = {
|
|
724
|
+
VIEW_BINDING: {
|
|
725
|
+
on_error: "return_fallback",
|
|
726
|
+
fallback_value: "",
|
|
727
|
+
log_level: "warn"
|
|
728
|
+
},
|
|
729
|
+
EVENT_REACTION: {
|
|
730
|
+
on_error: "log_and_skip",
|
|
731
|
+
fallback_value: void 0,
|
|
732
|
+
log_level: "error"
|
|
733
|
+
},
|
|
734
|
+
DURING_ACTION: {
|
|
735
|
+
on_error: "log_and_skip",
|
|
736
|
+
fallback_value: void 0,
|
|
737
|
+
log_level: "error"
|
|
738
|
+
},
|
|
739
|
+
CONDITIONAL_VISIBILITY: {
|
|
740
|
+
on_error: "return_fallback",
|
|
741
|
+
fallback_value: true,
|
|
742
|
+
// Show by default if condition fails
|
|
743
|
+
log_level: "warn"
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function createEvaluator(config) {
|
|
747
|
+
const allFunctions = [...CORE_FUNCTIONS, ...config.functions];
|
|
748
|
+
const fnMap = buildFunctionMap(allFunctions);
|
|
749
|
+
const policy = config.failurePolicy;
|
|
750
|
+
function handleError(expr, error) {
|
|
751
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
752
|
+
if (policy.log_level === "error") {
|
|
753
|
+
console.error(`[player-core] Expression error: "${expr}" \u2014 ${message}`);
|
|
754
|
+
} else if (policy.log_level === "warn") {
|
|
755
|
+
console.warn(`[player-core] Expression error: "${expr}" \u2014 ${message}`);
|
|
756
|
+
}
|
|
757
|
+
switch (policy.on_error) {
|
|
758
|
+
case "throw":
|
|
759
|
+
throw error;
|
|
760
|
+
case "return_fallback":
|
|
761
|
+
return { value: policy.fallback_value, status: "fallback", error: message };
|
|
762
|
+
case "log_and_skip":
|
|
763
|
+
default:
|
|
764
|
+
return { value: policy.fallback_value, status: "error", error: message };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
evaluate(expression, context) {
|
|
769
|
+
try {
|
|
770
|
+
const value = evaluateExpression(expression, context, fnMap);
|
|
771
|
+
return { value, status: "ok" };
|
|
772
|
+
} catch (error) {
|
|
773
|
+
return handleError(expression, error);
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
evaluateTemplate(template, context) {
|
|
777
|
+
try {
|
|
778
|
+
if (!template.includes("{{")) {
|
|
779
|
+
return { value: template, status: "ok" };
|
|
780
|
+
}
|
|
781
|
+
const result = template.replace(TEMPLATE_RE, (_match, expr) => {
|
|
782
|
+
const value = evaluateExpression(expr, context, fnMap);
|
|
783
|
+
return value != null ? String(value) : "";
|
|
784
|
+
});
|
|
785
|
+
return { value: result, status: "ok" };
|
|
786
|
+
} catch (error) {
|
|
787
|
+
return handleError(template, error);
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
validate(expression) {
|
|
791
|
+
const errors = [];
|
|
792
|
+
try {
|
|
793
|
+
parseAndCache(expression);
|
|
794
|
+
} catch (e) {
|
|
795
|
+
errors.push(e instanceof Error ? e.message : String(e));
|
|
796
|
+
}
|
|
797
|
+
return { valid: errors.length === 0, errors };
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function clearExpressionCache() {
|
|
802
|
+
astCache.clear();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/state-machine/interpreter.ts
|
|
806
|
+
var MAX_AUTO_CHAIN = 10;
|
|
807
|
+
var StateMachine = class {
|
|
808
|
+
evaluator;
|
|
809
|
+
actionHandlers;
|
|
810
|
+
listeners = /* @__PURE__ */ new Set();
|
|
811
|
+
instance;
|
|
812
|
+
constructor(definition, initialData = {}, config) {
|
|
813
|
+
this.evaluator = config.evaluator;
|
|
814
|
+
this.actionHandlers = config.actionHandlers ?? /* @__PURE__ */ new Map();
|
|
815
|
+
const startState = definition.states.find((s) => s.type === "START");
|
|
816
|
+
if (!startState) {
|
|
817
|
+
throw new Error(`No START state found in definition ${definition.slug}`);
|
|
818
|
+
}
|
|
819
|
+
this.instance = {
|
|
820
|
+
definition,
|
|
821
|
+
current_state: startState.name,
|
|
822
|
+
state_data: { ...initialData },
|
|
823
|
+
memory: {},
|
|
824
|
+
status: "ACTIVE"
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/** Get the current instance snapshot (immutable copy) */
|
|
828
|
+
getSnapshot() {
|
|
829
|
+
return { ...this.instance, state_data: { ...this.instance.state_data }, memory: { ...this.instance.memory } };
|
|
830
|
+
}
|
|
831
|
+
/** Get current state name */
|
|
832
|
+
get currentState() {
|
|
833
|
+
return this.instance.current_state;
|
|
834
|
+
}
|
|
835
|
+
/** Get current state_data */
|
|
836
|
+
get stateData() {
|
|
837
|
+
return this.instance.state_data;
|
|
838
|
+
}
|
|
839
|
+
/** Get current status */
|
|
840
|
+
get status() {
|
|
841
|
+
return this.instance.status;
|
|
842
|
+
}
|
|
843
|
+
/** Subscribe to state machine events */
|
|
844
|
+
on(listener) {
|
|
845
|
+
this.listeners.add(listener);
|
|
846
|
+
return () => this.listeners.delete(listener);
|
|
847
|
+
}
|
|
848
|
+
/** Register an action handler */
|
|
849
|
+
registerAction(type, handler) {
|
|
850
|
+
this.actionHandlers.set(type, handler);
|
|
851
|
+
}
|
|
852
|
+
/** Execute a named transition */
|
|
853
|
+
async transition(transitionName, data) {
|
|
854
|
+
if (this.instance.status !== "ACTIVE") {
|
|
855
|
+
return {
|
|
856
|
+
success: false,
|
|
857
|
+
from_state: this.instance.current_state,
|
|
858
|
+
to_state: this.instance.current_state,
|
|
859
|
+
actions_executed: [],
|
|
860
|
+
error: `Cannot transition: instance status is ${this.instance.status}`
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const transition = this.instance.definition.transitions.find(
|
|
864
|
+
(t) => t.name === transitionName && t.from.includes(this.instance.current_state)
|
|
865
|
+
);
|
|
866
|
+
if (!transition) {
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
from_state: this.instance.current_state,
|
|
870
|
+
to_state: this.instance.current_state,
|
|
871
|
+
actions_executed: [],
|
|
872
|
+
error: `Transition "${transitionName}" not valid from state "${this.instance.current_state}"`
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (data) {
|
|
876
|
+
this.instance.state_data = { ...this.instance.state_data, ...data };
|
|
877
|
+
}
|
|
878
|
+
if (transition.conditions && transition.conditions.length > 0) {
|
|
879
|
+
const ctx = this.buildContext();
|
|
880
|
+
for (const condition of transition.conditions) {
|
|
881
|
+
const result2 = this.evaluator.evaluate(condition, ctx);
|
|
882
|
+
if (!result2.value) {
|
|
883
|
+
return {
|
|
884
|
+
success: false,
|
|
885
|
+
from_state: this.instance.current_state,
|
|
886
|
+
to_state: this.instance.current_state,
|
|
887
|
+
actions_executed: [],
|
|
888
|
+
error: `Transition condition not met: ${condition}`
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const result = await this.executeTransition(transition);
|
|
894
|
+
if (result.success) {
|
|
895
|
+
await this.drainAutoTransitions();
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
/** Update state_data directly (for on_event set_field actions) */
|
|
900
|
+
setField(field, value) {
|
|
901
|
+
this.instance.state_data = { ...this.instance.state_data, [field]: value };
|
|
902
|
+
}
|
|
903
|
+
/** Update memory */
|
|
904
|
+
setMemory(key, value) {
|
|
905
|
+
this.instance.memory = { ...this.instance.memory, [key]: value };
|
|
906
|
+
}
|
|
907
|
+
/** Get available transitions from the current state */
|
|
908
|
+
getAvailableTransitions() {
|
|
909
|
+
return this.instance.definition.transitions.filter(
|
|
910
|
+
(t) => t.from.includes(this.instance.current_state) && !t.auto
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
/** Get the current state definition */
|
|
914
|
+
getCurrentStateDefinition() {
|
|
915
|
+
return this.instance.definition.states.find((s) => s.name === this.instance.current_state);
|
|
916
|
+
}
|
|
917
|
+
// ===========================================================================
|
|
918
|
+
// Private implementation
|
|
919
|
+
// ===========================================================================
|
|
920
|
+
async executeTransition(transition) {
|
|
921
|
+
const fromState = this.instance.current_state;
|
|
922
|
+
const allActionsExecuted = [];
|
|
923
|
+
const fromStateDef = this.getCurrentStateDefinition();
|
|
924
|
+
if (fromStateDef?.on_exit) {
|
|
925
|
+
await this.executeActions(fromStateDef.on_exit, allActionsExecuted);
|
|
926
|
+
}
|
|
927
|
+
this.emit({
|
|
928
|
+
type: "state_exit",
|
|
929
|
+
instance_id: this.instance.definition.id,
|
|
930
|
+
from_state: fromState
|
|
931
|
+
});
|
|
932
|
+
if (transition.actions) {
|
|
933
|
+
await this.executeActions(transition.actions, allActionsExecuted);
|
|
934
|
+
}
|
|
935
|
+
this.instance.current_state = transition.to;
|
|
936
|
+
const toStateDef = this.instance.definition.states.find((s) => s.name === transition.to);
|
|
937
|
+
if (toStateDef?.type === "END") {
|
|
938
|
+
this.instance.status = "COMPLETED";
|
|
939
|
+
} else if (toStateDef?.type === "CANCELLED") {
|
|
940
|
+
this.instance.status = "CANCELLED";
|
|
941
|
+
}
|
|
942
|
+
this.emit({
|
|
943
|
+
type: "state_enter",
|
|
944
|
+
instance_id: this.instance.definition.id,
|
|
945
|
+
to_state: transition.to
|
|
946
|
+
});
|
|
947
|
+
if (toStateDef?.on_enter) {
|
|
948
|
+
await this.executeActions(toStateDef.on_enter, allActionsExecuted);
|
|
949
|
+
}
|
|
950
|
+
this.emit({
|
|
951
|
+
type: "transition",
|
|
952
|
+
instance_id: this.instance.definition.id,
|
|
953
|
+
from_state: fromState,
|
|
954
|
+
to_state: transition.to
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
success: true,
|
|
958
|
+
from_state: fromState,
|
|
959
|
+
to_state: transition.to,
|
|
960
|
+
actions_executed: allActionsExecuted
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async drainAutoTransitions() {
|
|
964
|
+
for (let depth = 0; depth < MAX_AUTO_CHAIN; depth++) {
|
|
965
|
+
if (this.instance.status !== "ACTIVE") break;
|
|
966
|
+
const autoTransition = this.findMatchingAutoTransition();
|
|
967
|
+
if (!autoTransition) break;
|
|
968
|
+
const result = await this.executeTransition(autoTransition);
|
|
969
|
+
if (!result.success) break;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
findMatchingAutoTransition() {
|
|
973
|
+
const candidates = this.instance.definition.transitions.filter(
|
|
974
|
+
(t) => t.auto && t.from.includes(this.instance.current_state)
|
|
975
|
+
);
|
|
976
|
+
const ctx = this.buildContext();
|
|
977
|
+
for (const candidate of candidates) {
|
|
978
|
+
if (!candidate.conditions || candidate.conditions.length === 0) {
|
|
979
|
+
return candidate;
|
|
980
|
+
}
|
|
981
|
+
const allMet = candidate.conditions.every((condition) => {
|
|
982
|
+
const result = this.evaluator.evaluate(condition, ctx);
|
|
983
|
+
return result.value === true;
|
|
984
|
+
});
|
|
985
|
+
if (allMet) return candidate;
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
async executeActions(actions, collector) {
|
|
990
|
+
const ctx = this.buildContext();
|
|
991
|
+
for (const action of actions) {
|
|
992
|
+
if (action.condition) {
|
|
993
|
+
const condResult = this.evaluator.evaluate(action.condition, ctx);
|
|
994
|
+
if (!condResult.value) continue;
|
|
995
|
+
}
|
|
996
|
+
const handler = this.actionHandlers.get(action.type);
|
|
997
|
+
if (handler) {
|
|
998
|
+
try {
|
|
999
|
+
await handler(action, ctx);
|
|
1000
|
+
collector.push(action);
|
|
1001
|
+
this.emit({
|
|
1002
|
+
type: "action_executed",
|
|
1003
|
+
instance_id: this.instance.definition.id,
|
|
1004
|
+
action
|
|
1005
|
+
});
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
this.emit({
|
|
1008
|
+
type: "error",
|
|
1009
|
+
instance_id: this.instance.definition.id,
|
|
1010
|
+
action,
|
|
1011
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
buildContext() {
|
|
1018
|
+
return {
|
|
1019
|
+
state_data: this.instance.state_data,
|
|
1020
|
+
memory: this.instance.memory,
|
|
1021
|
+
current_state: this.instance.current_state,
|
|
1022
|
+
status: this.instance.status,
|
|
1023
|
+
// Spread state_data for direct field access (e.g., "title" instead of "state_data.title")
|
|
1024
|
+
...this.instance.state_data
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
emit(event) {
|
|
1028
|
+
for (const listener of this.listeners) {
|
|
1029
|
+
try {
|
|
1030
|
+
listener(event);
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// src/events/pattern-matcher.ts
|
|
1038
|
+
var patternCache = /* @__PURE__ */ new Map();
|
|
1039
|
+
var MAX_CACHE2 = 200;
|
|
1040
|
+
function compilePattern(pattern) {
|
|
1041
|
+
const cached = patternCache.get(pattern);
|
|
1042
|
+
if (cached) return cached;
|
|
1043
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<DOUBLESTAR>>").replace(/\*/g, "[^:.]+").replace(/<<DOUBLESTAR>>/g, ".*");
|
|
1044
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
1045
|
+
if (patternCache.size >= MAX_CACHE2) {
|
|
1046
|
+
const firstKey = patternCache.keys().next().value;
|
|
1047
|
+
if (firstKey) patternCache.delete(firstKey);
|
|
1048
|
+
}
|
|
1049
|
+
patternCache.set(pattern, regex);
|
|
1050
|
+
return regex;
|
|
1051
|
+
}
|
|
1052
|
+
function matchTopic(pattern, topic) {
|
|
1053
|
+
return pattern.test(topic);
|
|
1054
|
+
}
|
|
1055
|
+
function clearPatternCache() {
|
|
1056
|
+
patternCache.clear();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/events/event-bus.ts
|
|
1060
|
+
var EventBus = class {
|
|
1061
|
+
subscriptions = [];
|
|
1062
|
+
/**
|
|
1063
|
+
* Subscribe to events matching a glob pattern.
|
|
1064
|
+
* Returns an unsubscribe function.
|
|
1065
|
+
*/
|
|
1066
|
+
subscribe(pattern, handler) {
|
|
1067
|
+
const regex = compilePattern(pattern);
|
|
1068
|
+
const subscription = { pattern, regex, handler };
|
|
1069
|
+
this.subscriptions.push(subscription);
|
|
1070
|
+
return () => {
|
|
1071
|
+
const idx = this.subscriptions.indexOf(subscription);
|
|
1072
|
+
if (idx !== -1) this.subscriptions.splice(idx, 1);
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Publish an event. All matching subscriptions fire (async).
|
|
1077
|
+
* Errors in handlers are caught and logged, never propagated.
|
|
1078
|
+
*/
|
|
1079
|
+
async publish(topic, payload = {}) {
|
|
1080
|
+
const event = { topic, payload };
|
|
1081
|
+
for (const sub of this.subscriptions) {
|
|
1082
|
+
if (matchTopic(sub.regex, topic)) {
|
|
1083
|
+
try {
|
|
1084
|
+
await sub.handler(event);
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
console.warn(
|
|
1087
|
+
`[player-core] Event handler error for pattern "${sub.pattern}" on topic "${topic}":`,
|
|
1088
|
+
error
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Publish synchronously (fire-and-forget).
|
|
1096
|
+
* Useful when you don't need to await handler completion.
|
|
1097
|
+
*/
|
|
1098
|
+
emit(topic, payload = {}) {
|
|
1099
|
+
void this.publish(topic, payload);
|
|
1100
|
+
}
|
|
1101
|
+
/** Get count of active subscriptions */
|
|
1102
|
+
get size() {
|
|
1103
|
+
return this.subscriptions.length;
|
|
1104
|
+
}
|
|
1105
|
+
/** Remove all subscriptions */
|
|
1106
|
+
clear() {
|
|
1107
|
+
this.subscriptions.length = 0;
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
// src/actions/dispatcher.ts
|
|
1112
|
+
var ActionDispatcher = class {
|
|
1113
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1114
|
+
/** Register a handler for an action type */
|
|
1115
|
+
register(type, handler) {
|
|
1116
|
+
this.handlers.set(type, handler);
|
|
1117
|
+
}
|
|
1118
|
+
/** Unregister a handler */
|
|
1119
|
+
unregister(type) {
|
|
1120
|
+
this.handlers.delete(type);
|
|
1121
|
+
}
|
|
1122
|
+
/** Check if a handler is registered for the given type */
|
|
1123
|
+
has(type) {
|
|
1124
|
+
return this.handlers.has(type);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Execute a list of actions sequentially.
|
|
1128
|
+
* Each action's condition is evaluated first (if present).
|
|
1129
|
+
* Missing handlers are skipped with a warning.
|
|
1130
|
+
*/
|
|
1131
|
+
async execute(actions, context, evaluator) {
|
|
1132
|
+
const results = [];
|
|
1133
|
+
for (const action of actions) {
|
|
1134
|
+
if (action.condition && evaluator) {
|
|
1135
|
+
const condResult = evaluator.evaluate(action.condition, context);
|
|
1136
|
+
if (!condResult.value) continue;
|
|
1137
|
+
}
|
|
1138
|
+
const handler = this.handlers.get(action.type);
|
|
1139
|
+
if (!handler) {
|
|
1140
|
+
console.warn(`[player-core] No handler registered for action type "${action.type}"`);
|
|
1141
|
+
results.push({ type: action.type, success: false, error: `No handler for "${action.type}"` });
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
await handler(action.config, context);
|
|
1146
|
+
results.push({ type: action.type, success: true });
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1149
|
+
console.warn(`[player-core] Action "${action.type}" failed: ${message}`);
|
|
1150
|
+
results.push({ type: action.type, success: false, error: message });
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return results;
|
|
1154
|
+
}
|
|
1155
|
+
/** Get count of registered handlers */
|
|
1156
|
+
get size() {
|
|
1157
|
+
return this.handlers.size;
|
|
1158
|
+
}
|
|
1159
|
+
/** Remove all handlers */
|
|
1160
|
+
clear() {
|
|
1161
|
+
this.handlers.clear();
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// src/testing/graph-walker.ts
|
|
1166
|
+
var MAX_PATHS = 1e3;
|
|
1167
|
+
function analyzeDefinition(def) {
|
|
1168
|
+
const states = def.states.map((s) => ({
|
|
1169
|
+
name: s.name,
|
|
1170
|
+
type: s.type,
|
|
1171
|
+
reachable: false,
|
|
1172
|
+
// Will be set during DFS
|
|
1173
|
+
hasOnEvent: Boolean(s.on_event?.length),
|
|
1174
|
+
hasOnEnter: Boolean(s.on_enter?.length),
|
|
1175
|
+
hasOnExit: Boolean(s.on_exit?.length)
|
|
1176
|
+
}));
|
|
1177
|
+
const edges = [];
|
|
1178
|
+
for (const t of def.transitions) {
|
|
1179
|
+
for (const from of t.from) {
|
|
1180
|
+
edges.push({
|
|
1181
|
+
name: t.name,
|
|
1182
|
+
from,
|
|
1183
|
+
to: t.to,
|
|
1184
|
+
auto: Boolean(t.auto),
|
|
1185
|
+
hasConditions: Boolean(t.conditions?.length)
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1190
|
+
for (const s of def.states) {
|
|
1191
|
+
adjacency.set(s.name, []);
|
|
1192
|
+
}
|
|
1193
|
+
for (const e of edges) {
|
|
1194
|
+
adjacency.get(e.from)?.push(e);
|
|
1195
|
+
}
|
|
1196
|
+
const startState = def.states.find((s) => s.type === "START");
|
|
1197
|
+
if (!startState) {
|
|
1198
|
+
return {
|
|
1199
|
+
states,
|
|
1200
|
+
edges,
|
|
1201
|
+
terminalPaths: [],
|
|
1202
|
+
cycles: [],
|
|
1203
|
+
unreachableStates: states.map((s) => s.name),
|
|
1204
|
+
deadEndStates: [],
|
|
1205
|
+
summary: {
|
|
1206
|
+
totalStates: states.length,
|
|
1207
|
+
totalTransitions: edges.length,
|
|
1208
|
+
reachableStates: 0,
|
|
1209
|
+
terminalPaths: 0,
|
|
1210
|
+
cycles: 0,
|
|
1211
|
+
unreachableStates: states.length,
|
|
1212
|
+
deadEndStates: 0
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
const terminalStates = new Set(
|
|
1217
|
+
def.states.filter((s) => s.type === "END" || s.type === "CANCELLED").map((s) => s.name)
|
|
1218
|
+
);
|
|
1219
|
+
const terminalPaths = [];
|
|
1220
|
+
const cycles = [];
|
|
1221
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1222
|
+
function dfs(state, pathStates, pathTransitions, pathSet) {
|
|
1223
|
+
if (terminalPaths.length + cycles.length >= MAX_PATHS) return;
|
|
1224
|
+
const stateNode = states.find((s) => s.name === state);
|
|
1225
|
+
if (stateNode) stateNode.reachable = true;
|
|
1226
|
+
visited.add(state);
|
|
1227
|
+
const isTerminal = terminalStates.has(state);
|
|
1228
|
+
if (isTerminal) {
|
|
1229
|
+
terminalPaths.push({
|
|
1230
|
+
states: [...pathStates],
|
|
1231
|
+
transitions: [...pathTransitions]
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
const outEdges = adjacency.get(state) ?? [];
|
|
1235
|
+
for (const edge of outEdges) {
|
|
1236
|
+
if (terminalPaths.length + cycles.length >= MAX_PATHS) return;
|
|
1237
|
+
if (pathSet.has(edge.to)) {
|
|
1238
|
+
const cycleStart = pathStates.indexOf(edge.to);
|
|
1239
|
+
cycles.push({
|
|
1240
|
+
states: [...pathStates.slice(cycleStart), edge.to],
|
|
1241
|
+
transitions: [...pathTransitions.slice(cycleStart), edge.name]
|
|
1242
|
+
});
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
pathStates.push(edge.to);
|
|
1246
|
+
pathTransitions.push(edge.name);
|
|
1247
|
+
pathSet.add(edge.to);
|
|
1248
|
+
dfs(edge.to, pathStates, pathTransitions, pathSet);
|
|
1249
|
+
pathStates.pop();
|
|
1250
|
+
pathTransitions.pop();
|
|
1251
|
+
pathSet.delete(edge.to);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
dfs(startState.name, [startState.name], [], /* @__PURE__ */ new Set([startState.name]));
|
|
1255
|
+
const unreachableStates = states.filter((s) => !s.reachable).map((s) => s.name);
|
|
1256
|
+
const deadEndStates = states.filter((s) => {
|
|
1257
|
+
if (!s.reachable) return false;
|
|
1258
|
+
if (terminalStates.has(s.name)) return false;
|
|
1259
|
+
const outEdges = adjacency.get(s.name) ?? [];
|
|
1260
|
+
return outEdges.length === 0;
|
|
1261
|
+
}).map((s) => s.name);
|
|
1262
|
+
const uniqueCycles = [];
|
|
1263
|
+
const seenCycleKeys = /* @__PURE__ */ new Set();
|
|
1264
|
+
for (const cycle of cycles) {
|
|
1265
|
+
const key = cycle.states.join("\u2192");
|
|
1266
|
+
if (!seenCycleKeys.has(key)) {
|
|
1267
|
+
seenCycleKeys.add(key);
|
|
1268
|
+
uniqueCycles.push(cycle);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return {
|
|
1272
|
+
states,
|
|
1273
|
+
edges,
|
|
1274
|
+
terminalPaths,
|
|
1275
|
+
cycles: uniqueCycles,
|
|
1276
|
+
unreachableStates,
|
|
1277
|
+
deadEndStates,
|
|
1278
|
+
summary: {
|
|
1279
|
+
totalStates: states.length,
|
|
1280
|
+
totalTransitions: edges.length,
|
|
1281
|
+
reachableStates: states.filter((s) => s.reachable).length,
|
|
1282
|
+
terminalPaths: terminalPaths.length,
|
|
1283
|
+
cycles: uniqueCycles.length,
|
|
1284
|
+
unreachableStates: unreachableStates.length,
|
|
1285
|
+
deadEndStates: deadEndStates.length
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function generateCoverageScenarios(def, analysis) {
|
|
1290
|
+
const resolved = analysis ?? analyzeDefinition(def);
|
|
1291
|
+
const scenarios = [];
|
|
1292
|
+
for (const path of resolved.terminalPaths) {
|
|
1293
|
+
const pathLabel = path.states.join(" \u2192 ");
|
|
1294
|
+
const steps = [];
|
|
1295
|
+
for (let i = 0; i < path.transitions.length; i++) {
|
|
1296
|
+
const transitionName = path.transitions[i];
|
|
1297
|
+
const expectedState = path.states[i + 1];
|
|
1298
|
+
const stateDef = def.states.find((s) => s.name === expectedState);
|
|
1299
|
+
const isTerminal = stateDef?.type === "END" || stateDef?.type === "CANCELLED";
|
|
1300
|
+
const expectedStatus = stateDef?.type === "END" ? "COMPLETED" : stateDef?.type === "CANCELLED" ? "CANCELLED" : "ACTIVE";
|
|
1301
|
+
steps.push({
|
|
1302
|
+
name: `${transitionName} \u2192 ${expectedState}`,
|
|
1303
|
+
action: { type: "transition", name: transitionName },
|
|
1304
|
+
assertions: [
|
|
1305
|
+
{
|
|
1306
|
+
target: "state",
|
|
1307
|
+
operator: "eq",
|
|
1308
|
+
expected: expectedState,
|
|
1309
|
+
label: `State should be "${expectedState}"`
|
|
1310
|
+
},
|
|
1311
|
+
...isTerminal ? [
|
|
1312
|
+
{
|
|
1313
|
+
target: "status",
|
|
1314
|
+
operator: "eq",
|
|
1315
|
+
expected: expectedStatus,
|
|
1316
|
+
label: `Status should be "${expectedStatus}"`
|
|
1317
|
+
}
|
|
1318
|
+
] : []
|
|
1319
|
+
]
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
scenarios.push({
|
|
1323
|
+
name: `Path: ${pathLabel}`,
|
|
1324
|
+
tags: ["auto-generated", "coverage"],
|
|
1325
|
+
steps
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
return scenarios;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/testing/test-runner.ts
|
|
1332
|
+
async function runTestProgram(program, config) {
|
|
1333
|
+
const start = performance.now();
|
|
1334
|
+
const scenarioResults = [];
|
|
1335
|
+
let allPassed = true;
|
|
1336
|
+
for (let i = 0; i < program.scenarios.length; i++) {
|
|
1337
|
+
if (config?.abortSignal?.aborted) {
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
const result = await runScenario(program.scenarios[i], program.definitions, config);
|
|
1341
|
+
scenarioResults.push(result);
|
|
1342
|
+
if (!result.passed) allPassed = false;
|
|
1343
|
+
config?.onScenarioComplete?.(result);
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
passed: allPassed,
|
|
1347
|
+
scenarioResults,
|
|
1348
|
+
durationMs: performance.now() - start
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
async function runScenario(scenario, definitions, config) {
|
|
1352
|
+
const start = performance.now();
|
|
1353
|
+
try {
|
|
1354
|
+
const evaluator = createEvaluator({
|
|
1355
|
+
functions: [],
|
|
1356
|
+
failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION
|
|
1357
|
+
});
|
|
1358
|
+
const eventBus = scenario.connectEventBuses ? new EventBus() : null;
|
|
1359
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1360
|
+
if (scenario.instances?.length) {
|
|
1361
|
+
for (const inst of scenario.instances) {
|
|
1362
|
+
const def = definitions[inst.definitionSlug];
|
|
1363
|
+
if (!def) {
|
|
1364
|
+
return {
|
|
1365
|
+
scenarioName: scenario.name,
|
|
1366
|
+
passed: false,
|
|
1367
|
+
stepResults: [],
|
|
1368
|
+
durationMs: performance.now() - start,
|
|
1369
|
+
error: `Definition not found: "${inst.definitionSlug}"`
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const sm = createStateMachine(def, inst.initialData ?? {}, evaluator, config);
|
|
1373
|
+
instances.set(inst.id, sm);
|
|
1374
|
+
if (eventBus) {
|
|
1375
|
+
wireOnEventSubscriptions(sm, eventBus, evaluator);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
} else {
|
|
1379
|
+
const defKeys = Object.keys(definitions);
|
|
1380
|
+
if (defKeys.length === 0) {
|
|
1381
|
+
return {
|
|
1382
|
+
scenarioName: scenario.name,
|
|
1383
|
+
passed: false,
|
|
1384
|
+
stepResults: [],
|
|
1385
|
+
durationMs: performance.now() - start,
|
|
1386
|
+
error: "No definitions provided"
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
const def = definitions[defKeys[0]];
|
|
1390
|
+
const sm = createStateMachine(def, {}, evaluator, config);
|
|
1391
|
+
instances.set("default", sm);
|
|
1392
|
+
if (eventBus) {
|
|
1393
|
+
wireOnEventSubscriptions(sm, eventBus, evaluator);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const stepResults = [];
|
|
1397
|
+
let allPassed = true;
|
|
1398
|
+
for (let i = 0; i < scenario.steps.length; i++) {
|
|
1399
|
+
if (config?.abortSignal?.aborted) {
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
const step = scenario.steps[i];
|
|
1403
|
+
const stepResult = await executeStep(step, instances, eventBus, evaluator);
|
|
1404
|
+
stepResults.push(stepResult);
|
|
1405
|
+
if (!stepResult.passed) allPassed = false;
|
|
1406
|
+
config?.onStepComplete?.(0, stepResult);
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
scenarioName: scenario.name,
|
|
1410
|
+
passed: allPassed,
|
|
1411
|
+
stepResults,
|
|
1412
|
+
durationMs: performance.now() - start
|
|
1413
|
+
};
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
return {
|
|
1416
|
+
scenarioName: scenario.name,
|
|
1417
|
+
passed: false,
|
|
1418
|
+
stepResults: [],
|
|
1419
|
+
durationMs: performance.now() - start,
|
|
1420
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function createStateMachine(def, initialData, evaluator, config) {
|
|
1425
|
+
const actionHandlers = /* @__PURE__ */ new Map();
|
|
1426
|
+
let smRef = null;
|
|
1427
|
+
actionHandlers.set("set_field", (action) => {
|
|
1428
|
+
if (smRef && typeof action.config.field === "string") {
|
|
1429
|
+
smRef.setField(action.config.field, action.config.value);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
actionHandlers.set("set_memory", (action) => {
|
|
1433
|
+
if (smRef && typeof action.config.key === "string") {
|
|
1434
|
+
smRef.setMemory(action.config.key, action.config.value);
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
if (config?.actionHandlers) {
|
|
1438
|
+
for (const [type, handler] of Object.entries(config.actionHandlers)) {
|
|
1439
|
+
actionHandlers.set(type, (action, ctx) => handler(action.config, ctx));
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const sm = new StateMachine(def, initialData, { evaluator, actionHandlers });
|
|
1443
|
+
smRef = sm;
|
|
1444
|
+
return sm;
|
|
1445
|
+
}
|
|
1446
|
+
function wireOnEventSubscriptions(sm, eventBus, evaluator) {
|
|
1447
|
+
const unsubs = [];
|
|
1448
|
+
function subscribeCurrentState() {
|
|
1449
|
+
for (const unsub of unsubs) unsub();
|
|
1450
|
+
unsubs.length = 0;
|
|
1451
|
+
const stateDef = sm.getCurrentStateDefinition();
|
|
1452
|
+
if (!stateDef?.on_event?.length) return;
|
|
1453
|
+
for (const sub of stateDef.on_event) {
|
|
1454
|
+
const unsub = eventBus.subscribe(sub.match, async (event) => {
|
|
1455
|
+
const ctx = {
|
|
1456
|
+
...sm.stateData,
|
|
1457
|
+
state_data: sm.stateData,
|
|
1458
|
+
event: event.payload,
|
|
1459
|
+
current_state: sm.currentState,
|
|
1460
|
+
status: sm.status
|
|
1461
|
+
};
|
|
1462
|
+
if (sub.conditions?.length) {
|
|
1463
|
+
for (const condition of sub.conditions) {
|
|
1464
|
+
const result = evaluator.evaluate(condition, ctx);
|
|
1465
|
+
if (!result.value) return;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
for (const action of sub.actions) {
|
|
1469
|
+
if (action.type === "set_field" && typeof action.config.field === "string") {
|
|
1470
|
+
sm.setField(action.config.field, action.config.value);
|
|
1471
|
+
} else if (action.type === "set_memory" && typeof action.config.key === "string") {
|
|
1472
|
+
sm.setMemory(action.config.key, action.config.value);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
unsubs.push(unsub);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
subscribeCurrentState();
|
|
1480
|
+
sm.on((event) => {
|
|
1481
|
+
if (event.type === "state_enter") {
|
|
1482
|
+
subscribeCurrentState();
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
async function executeStep(step, instances, eventBus, evaluator) {
|
|
1487
|
+
const stepStart = performance.now();
|
|
1488
|
+
try {
|
|
1489
|
+
const instanceId = step.instanceId ?? "default";
|
|
1490
|
+
const sm = instances.get(instanceId);
|
|
1491
|
+
if (step.action.type !== "assert_only") {
|
|
1492
|
+
if (!sm && step.action.type !== "publish_event") {
|
|
1493
|
+
return {
|
|
1494
|
+
stepName: step.name,
|
|
1495
|
+
passed: false,
|
|
1496
|
+
assertionResults: [],
|
|
1497
|
+
durationMs: performance.now() - stepStart,
|
|
1498
|
+
error: `Instance not found: "${instanceId}"`
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
switch (step.action.type) {
|
|
1502
|
+
case "transition": {
|
|
1503
|
+
const result = await sm.transition(step.action.name, step.action.data);
|
|
1504
|
+
if (!result.success) {
|
|
1505
|
+
return {
|
|
1506
|
+
stepName: step.name,
|
|
1507
|
+
passed: false,
|
|
1508
|
+
assertionResults: [],
|
|
1509
|
+
durationMs: performance.now() - stepStart,
|
|
1510
|
+
error: `Transition failed: ${result.error}`
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
case "set_field": {
|
|
1516
|
+
sm.setField(step.action.field, step.action.value);
|
|
1517
|
+
break;
|
|
1518
|
+
}
|
|
1519
|
+
case "publish_event": {
|
|
1520
|
+
if (eventBus) {
|
|
1521
|
+
await eventBus.publish(step.action.topic, step.action.payload ?? {});
|
|
1522
|
+
}
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const assertionResults = evaluateAssertions(step.assertions, instances, evaluator);
|
|
1528
|
+
const passed = assertionResults.every((r) => r.passed);
|
|
1529
|
+
return {
|
|
1530
|
+
stepName: step.name,
|
|
1531
|
+
passed,
|
|
1532
|
+
assertionResults,
|
|
1533
|
+
durationMs: performance.now() - stepStart
|
|
1534
|
+
};
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
return {
|
|
1537
|
+
stepName: step.name,
|
|
1538
|
+
passed: false,
|
|
1539
|
+
assertionResults: [],
|
|
1540
|
+
durationMs: performance.now() - stepStart,
|
|
1541
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
function evaluateAssertions(assertions, instances, _evaluator) {
|
|
1546
|
+
return assertions.map((assertion) => {
|
|
1547
|
+
try {
|
|
1548
|
+
const instanceId = assertion.instanceId ?? "default";
|
|
1549
|
+
const sm = instances.get(instanceId);
|
|
1550
|
+
if (!sm) {
|
|
1551
|
+
return {
|
|
1552
|
+
passed: false,
|
|
1553
|
+
assertion,
|
|
1554
|
+
actual: void 0,
|
|
1555
|
+
error: `Instance not found: "${instanceId}"`
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
const actual = getAssertionTarget(assertion, sm);
|
|
1559
|
+
const passed = compareValues(actual, assertion.operator, assertion.expected);
|
|
1560
|
+
return { passed, assertion, actual };
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
return {
|
|
1563
|
+
passed: false,
|
|
1564
|
+
assertion,
|
|
1565
|
+
actual: void 0,
|
|
1566
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
function getAssertionTarget(assertion, sm) {
|
|
1572
|
+
switch (assertion.target) {
|
|
1573
|
+
case "state":
|
|
1574
|
+
return sm.currentState;
|
|
1575
|
+
case "status":
|
|
1576
|
+
return sm.status;
|
|
1577
|
+
case "state_data": {
|
|
1578
|
+
if (!assertion.path) return sm.stateData;
|
|
1579
|
+
return getNestedValue(sm.stateData, assertion.path);
|
|
1580
|
+
}
|
|
1581
|
+
case "available_transitions":
|
|
1582
|
+
return sm.getAvailableTransitions().map((t) => t.name);
|
|
1583
|
+
default:
|
|
1584
|
+
throw new Error(`Unknown assertion target: "${assertion.target}"`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function getNestedValue(obj, path) {
|
|
1588
|
+
const parts = path.split(".");
|
|
1589
|
+
let current = obj;
|
|
1590
|
+
for (const part of parts) {
|
|
1591
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
1592
|
+
current = current[part];
|
|
1593
|
+
}
|
|
1594
|
+
return current;
|
|
1595
|
+
}
|
|
1596
|
+
function compareValues(actual, operator, expected) {
|
|
1597
|
+
switch (operator) {
|
|
1598
|
+
case "eq":
|
|
1599
|
+
return deepEqual(actual, expected);
|
|
1600
|
+
case "neq":
|
|
1601
|
+
return !deepEqual(actual, expected);
|
|
1602
|
+
case "gt":
|
|
1603
|
+
return typeof actual === "number" && typeof expected === "number" && actual > expected;
|
|
1604
|
+
case "gte":
|
|
1605
|
+
return typeof actual === "number" && typeof expected === "number" && actual >= expected;
|
|
1606
|
+
case "lt":
|
|
1607
|
+
return typeof actual === "number" && typeof expected === "number" && actual < expected;
|
|
1608
|
+
case "lte":
|
|
1609
|
+
return typeof actual === "number" && typeof expected === "number" && actual <= expected;
|
|
1610
|
+
case "contains": {
|
|
1611
|
+
if (Array.isArray(actual)) return actual.includes(expected);
|
|
1612
|
+
if (typeof actual === "string" && typeof expected === "string") return actual.includes(expected);
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
case "truthy":
|
|
1616
|
+
return Boolean(actual);
|
|
1617
|
+
case "falsy":
|
|
1618
|
+
return !actual;
|
|
1619
|
+
default:
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function deepEqual(a, b) {
|
|
1624
|
+
if (a === b) return true;
|
|
1625
|
+
if (a == null || b == null) return false;
|
|
1626
|
+
if (typeof a !== typeof b) return false;
|
|
1627
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1628
|
+
if (a.length !== b.length) return false;
|
|
1629
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
1632
|
+
const keysA = Object.keys(a);
|
|
1633
|
+
const keysB = Object.keys(b);
|
|
1634
|
+
if (keysA.length !== keysB.length) return false;
|
|
1635
|
+
return keysA.every(
|
|
1636
|
+
(k) => deepEqual(
|
|
1637
|
+
a[k],
|
|
1638
|
+
b[k]
|
|
1639
|
+
)
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/testing/test-compiler.ts
|
|
1646
|
+
function compileTestScenario(scenario, definitions, scenarioIndex = 0) {
|
|
1647
|
+
const states = [];
|
|
1648
|
+
const transitions = [];
|
|
1649
|
+
const stepCount = scenario.steps.length;
|
|
1650
|
+
states.push({
|
|
1651
|
+
name: "__idle",
|
|
1652
|
+
type: "START"
|
|
1653
|
+
});
|
|
1654
|
+
const setupActions = [
|
|
1655
|
+
{
|
|
1656
|
+
type: "__test_init",
|
|
1657
|
+
config: {
|
|
1658
|
+
scenarioName: scenario.name,
|
|
1659
|
+
scenarioIndex,
|
|
1660
|
+
definitions: Object.keys(definitions),
|
|
1661
|
+
connectEventBuses: scenario.connectEventBuses ?? false
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
];
|
|
1665
|
+
if (scenario.instances?.length) {
|
|
1666
|
+
for (const inst of scenario.instances) {
|
|
1667
|
+
setupActions.push({
|
|
1668
|
+
type: "__test_create_instance",
|
|
1669
|
+
config: {
|
|
1670
|
+
testId: inst.id,
|
|
1671
|
+
definitionSlug: inst.definitionSlug,
|
|
1672
|
+
initialData: inst.initialData ?? {}
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
} else {
|
|
1677
|
+
const firstSlug = Object.keys(definitions)[0];
|
|
1678
|
+
if (firstSlug) {
|
|
1679
|
+
setupActions.push({
|
|
1680
|
+
type: "__test_create_instance",
|
|
1681
|
+
config: {
|
|
1682
|
+
testId: "default",
|
|
1683
|
+
definitionSlug: firstSlug,
|
|
1684
|
+
initialData: {}
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
states.push({
|
|
1690
|
+
name: "__setup",
|
|
1691
|
+
type: "REGULAR",
|
|
1692
|
+
on_enter: setupActions
|
|
1693
|
+
});
|
|
1694
|
+
for (let i = 0; i < stepCount; i++) {
|
|
1695
|
+
const step = scenario.steps[i];
|
|
1696
|
+
states.push({
|
|
1697
|
+
name: `__step_${i}`,
|
|
1698
|
+
type: "REGULAR",
|
|
1699
|
+
on_enter: compileStepActions(step, i)
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
states.push({
|
|
1703
|
+
name: "__pass",
|
|
1704
|
+
type: "END",
|
|
1705
|
+
on_enter: [
|
|
1706
|
+
{ type: "__test_report", config: { passed: true } }
|
|
1707
|
+
]
|
|
1708
|
+
});
|
|
1709
|
+
states.push({
|
|
1710
|
+
name: "__fail",
|
|
1711
|
+
type: "CANCELLED",
|
|
1712
|
+
on_enter: [
|
|
1713
|
+
{ type: "__test_report", config: { passed: false } }
|
|
1714
|
+
]
|
|
1715
|
+
});
|
|
1716
|
+
transitions.push({
|
|
1717
|
+
name: "setup",
|
|
1718
|
+
from: ["__idle"],
|
|
1719
|
+
to: "__setup"
|
|
1720
|
+
});
|
|
1721
|
+
transitions.push({
|
|
1722
|
+
name: "next",
|
|
1723
|
+
from: ["__setup"],
|
|
1724
|
+
to: stepCount > 0 ? "__step_0" : "__pass"
|
|
1725
|
+
});
|
|
1726
|
+
for (let i = 0; i < stepCount; i++) {
|
|
1727
|
+
transitions.push({
|
|
1728
|
+
name: "next",
|
|
1729
|
+
from: [`__step_${i}`],
|
|
1730
|
+
to: i < stepCount - 1 ? `__step_${i + 1}` : "__pass"
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
const abortFromStates = ["__setup", ...Array.from({ length: stepCount }, (_, i) => `__step_${i}`)];
|
|
1734
|
+
transitions.push({
|
|
1735
|
+
name: "abort",
|
|
1736
|
+
from: abortFromStates,
|
|
1737
|
+
to: "__fail"
|
|
1738
|
+
});
|
|
1739
|
+
return {
|
|
1740
|
+
id: `__test_scenario_${scenarioIndex}`,
|
|
1741
|
+
slug: `__test_scenario_${scenarioIndex}`,
|
|
1742
|
+
states,
|
|
1743
|
+
transitions
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function compileTestProgram(program) {
|
|
1747
|
+
return program.scenarios.map(
|
|
1748
|
+
(scenario, i) => compileTestScenario(scenario, program.definitions, i)
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
function compileStepActions(step, stepIndex) {
|
|
1752
|
+
const actions = [];
|
|
1753
|
+
const defaultTestId = step.instanceId ?? "default";
|
|
1754
|
+
actions.push({
|
|
1755
|
+
type: "__test_step_begin",
|
|
1756
|
+
config: { stepName: step.name, stepIndex }
|
|
1757
|
+
});
|
|
1758
|
+
switch (step.action.type) {
|
|
1759
|
+
case "transition":
|
|
1760
|
+
actions.push({
|
|
1761
|
+
type: "__test_transition",
|
|
1762
|
+
config: {
|
|
1763
|
+
testId: defaultTestId,
|
|
1764
|
+
transitionName: step.action.name,
|
|
1765
|
+
data: step.action.data
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
break;
|
|
1769
|
+
case "set_field":
|
|
1770
|
+
actions.push({
|
|
1771
|
+
type: "__test_set_field",
|
|
1772
|
+
config: {
|
|
1773
|
+
testId: defaultTestId,
|
|
1774
|
+
field: step.action.field,
|
|
1775
|
+
value: step.action.value
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
break;
|
|
1779
|
+
case "publish_event":
|
|
1780
|
+
actions.push({
|
|
1781
|
+
type: "__test_publish_event",
|
|
1782
|
+
config: {
|
|
1783
|
+
topic: step.action.topic,
|
|
1784
|
+
payload: step.action.payload ?? {}
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
break;
|
|
1788
|
+
case "assert_only":
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
actions.push({
|
|
1792
|
+
type: "__test_settle",
|
|
1793
|
+
config: {}
|
|
1794
|
+
});
|
|
1795
|
+
for (let a = 0; a < step.assertions.length; a++) {
|
|
1796
|
+
const assertion = step.assertions[a];
|
|
1797
|
+
actions.push({
|
|
1798
|
+
type: "__test_assert",
|
|
1799
|
+
config: {
|
|
1800
|
+
testId: assertion.instanceId ?? defaultTestId,
|
|
1801
|
+
target: assertion.target,
|
|
1802
|
+
path: assertion.path,
|
|
1803
|
+
operator: assertion.operator,
|
|
1804
|
+
expected: assertion.expected,
|
|
1805
|
+
label: assertion.label,
|
|
1806
|
+
assertionIndex: a
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
actions.push({
|
|
1811
|
+
type: "__test_step_end",
|
|
1812
|
+
config: { stepIndex }
|
|
1813
|
+
});
|
|
1814
|
+
return actions;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/testing/test-actions.ts
|
|
1818
|
+
function createTestState() {
|
|
1819
|
+
return {
|
|
1820
|
+
stepResults: [],
|
|
1821
|
+
failed: false,
|
|
1822
|
+
currentStep: null,
|
|
1823
|
+
setOrchestratorField: () => {
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
function createInProcessTestActions(definitions) {
|
|
1828
|
+
const testState = createTestState();
|
|
1829
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1830
|
+
const evaluator = createEvaluator({
|
|
1831
|
+
functions: [],
|
|
1832
|
+
failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION
|
|
1833
|
+
});
|
|
1834
|
+
const machines = /* @__PURE__ */ new Map();
|
|
1835
|
+
let eventBus = null;
|
|
1836
|
+
handlers.set("__test_init", (action) => {
|
|
1837
|
+
const config = action.config;
|
|
1838
|
+
if (config.connectEventBuses) {
|
|
1839
|
+
eventBus = new EventBus();
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
handlers.set("__test_create_instance", (action) => {
|
|
1843
|
+
const { testId, definitionSlug, initialData } = action.config;
|
|
1844
|
+
const def = definitions[definitionSlug];
|
|
1845
|
+
if (!def) {
|
|
1846
|
+
throw new Error(`Definition not found: "${definitionSlug}"`);
|
|
1847
|
+
}
|
|
1848
|
+
const smHandlers = createInstanceActionHandlers(def, evaluator);
|
|
1849
|
+
const sm = new StateMachine(def, initialData, { evaluator, actionHandlers: smHandlers });
|
|
1850
|
+
machines.set(testId, sm);
|
|
1851
|
+
if (eventBus) {
|
|
1852
|
+
wireOnEventSubscriptions2(sm, eventBus, evaluator);
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
handlers.set("__test_step_begin", (action) => {
|
|
1856
|
+
const { stepName, stepIndex } = action.config;
|
|
1857
|
+
testState.currentStep = {
|
|
1858
|
+
name: stepName,
|
|
1859
|
+
index: stepIndex,
|
|
1860
|
+
startTime: performance.now(),
|
|
1861
|
+
assertions: []
|
|
1862
|
+
};
|
|
1863
|
+
});
|
|
1864
|
+
handlers.set("__test_step_end", (_action) => {
|
|
1865
|
+
if (!testState.currentStep) return;
|
|
1866
|
+
const step = testState.currentStep;
|
|
1867
|
+
const passed = !step.error && step.assertions.every((a) => a.passed);
|
|
1868
|
+
if (!passed) testState.failed = true;
|
|
1869
|
+
testState.stepResults.push({
|
|
1870
|
+
stepName: step.name,
|
|
1871
|
+
passed,
|
|
1872
|
+
assertionResults: step.assertions,
|
|
1873
|
+
durationMs: performance.now() - step.startTime,
|
|
1874
|
+
error: step.error
|
|
1875
|
+
});
|
|
1876
|
+
testState.setOrchestratorField("__test_step_failed", !passed);
|
|
1877
|
+
testState.currentStep = null;
|
|
1878
|
+
});
|
|
1879
|
+
handlers.set("__test_transition", async (action) => {
|
|
1880
|
+
const { testId, transitionName, data } = action.config;
|
|
1881
|
+
const sm = machines.get(testId);
|
|
1882
|
+
if (!sm) {
|
|
1883
|
+
if (testState.currentStep) {
|
|
1884
|
+
testState.currentStep.error = `Instance not found: "${testId}"`;
|
|
1885
|
+
}
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const result = await sm.transition(transitionName, data);
|
|
1889
|
+
if (!result.success) {
|
|
1890
|
+
if (testState.currentStep) {
|
|
1891
|
+
testState.currentStep.error = `Transition failed: ${result.error}`;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
handlers.set("__test_set_field", (action) => {
|
|
1896
|
+
const { testId, field, value } = action.config;
|
|
1897
|
+
const sm = machines.get(testId);
|
|
1898
|
+
if (!sm) {
|
|
1899
|
+
if (testState.currentStep) {
|
|
1900
|
+
testState.currentStep.error = `Instance not found: "${testId}"`;
|
|
1901
|
+
}
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
sm.setField(field, value);
|
|
1905
|
+
});
|
|
1906
|
+
handlers.set("__test_publish_event", async (action) => {
|
|
1907
|
+
const { topic, payload } = action.config;
|
|
1908
|
+
if (eventBus) {
|
|
1909
|
+
await eventBus.publish(topic, payload);
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
handlers.set("__test_settle", () => {
|
|
1913
|
+
});
|
|
1914
|
+
handlers.set("__test_assert", (action) => {
|
|
1915
|
+
const { testId, target, path, operator, expected, label } = action.config;
|
|
1916
|
+
const sm = machines.get(testId);
|
|
1917
|
+
if (!sm) {
|
|
1918
|
+
testState.currentStep?.assertions.push({
|
|
1919
|
+
passed: false,
|
|
1920
|
+
assertion: { target, path, operator, expected, label },
|
|
1921
|
+
actual: void 0,
|
|
1922
|
+
error: `Instance not found: "${testId}"`
|
|
1923
|
+
});
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const actual = getInProcessTarget(target, path, sm);
|
|
1927
|
+
const passed = compareValues2(actual, operator, expected);
|
|
1928
|
+
testState.currentStep?.assertions.push({
|
|
1929
|
+
passed,
|
|
1930
|
+
assertion: { target, path, operator, expected, label },
|
|
1931
|
+
actual
|
|
1932
|
+
});
|
|
1933
|
+
});
|
|
1934
|
+
handlers.set("__test_report", () => {
|
|
1935
|
+
});
|
|
1936
|
+
handlers.set("set_field", (action) => {
|
|
1937
|
+
testState.setOrchestratorField(
|
|
1938
|
+
action.config.field,
|
|
1939
|
+
action.config.value
|
|
1940
|
+
);
|
|
1941
|
+
});
|
|
1942
|
+
return { handlers, state: testState };
|
|
1943
|
+
}
|
|
1944
|
+
function createApiTestActions(adapter, options) {
|
|
1945
|
+
const testState = createTestState();
|
|
1946
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1947
|
+
const settleDelayMs = options?.settleDelayMs ?? 200;
|
|
1948
|
+
const instanceMap = /* @__PURE__ */ new Map();
|
|
1949
|
+
const createdIds = [];
|
|
1950
|
+
handlers.set("__test_init", () => {
|
|
1951
|
+
});
|
|
1952
|
+
handlers.set("__test_create_instance", async (action) => {
|
|
1953
|
+
const { testId, definitionSlug, initialData } = action.config;
|
|
1954
|
+
const snapshot = await adapter.createInstance({
|
|
1955
|
+
definitionSlug,
|
|
1956
|
+
stateData: Object.keys(initialData).length > 0 ? initialData : void 0
|
|
1957
|
+
});
|
|
1958
|
+
instanceMap.set(testId, snapshot.id);
|
|
1959
|
+
createdIds.push(snapshot.id);
|
|
1960
|
+
await adapter.startInstance(snapshot.id);
|
|
1961
|
+
});
|
|
1962
|
+
handlers.set("__test_step_begin", (action) => {
|
|
1963
|
+
const { stepName, stepIndex } = action.config;
|
|
1964
|
+
testState.currentStep = {
|
|
1965
|
+
name: stepName,
|
|
1966
|
+
index: stepIndex,
|
|
1967
|
+
startTime: performance.now(),
|
|
1968
|
+
assertions: []
|
|
1969
|
+
};
|
|
1970
|
+
});
|
|
1971
|
+
handlers.set("__test_step_end", (_action) => {
|
|
1972
|
+
if (!testState.currentStep) return;
|
|
1973
|
+
const step = testState.currentStep;
|
|
1974
|
+
const passed = !step.error && step.assertions.every((a) => a.passed);
|
|
1975
|
+
if (!passed) testState.failed = true;
|
|
1976
|
+
testState.stepResults.push({
|
|
1977
|
+
stepName: step.name,
|
|
1978
|
+
passed,
|
|
1979
|
+
assertionResults: step.assertions,
|
|
1980
|
+
durationMs: performance.now() - step.startTime,
|
|
1981
|
+
error: step.error
|
|
1982
|
+
});
|
|
1983
|
+
testState.setOrchestratorField("__test_step_failed", !passed);
|
|
1984
|
+
testState.currentStep = null;
|
|
1985
|
+
});
|
|
1986
|
+
handlers.set("__test_transition", async (action) => {
|
|
1987
|
+
const { testId, transitionName, data } = action.config;
|
|
1988
|
+
const serverId = instanceMap.get(testId);
|
|
1989
|
+
if (!serverId) {
|
|
1990
|
+
if (testState.currentStep) {
|
|
1991
|
+
testState.currentStep.error = `Instance not found: "${testId}"`;
|
|
1992
|
+
}
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const result = await adapter.triggerTransition(serverId, transitionName, data);
|
|
1996
|
+
if (!result.success) {
|
|
1997
|
+
if (testState.currentStep) {
|
|
1998
|
+
testState.currentStep.error = `Transition failed: ${result.error ?? "unknown"}`;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
handlers.set("__test_set_field", async (action) => {
|
|
2003
|
+
const { testId, field, value } = action.config;
|
|
2004
|
+
const serverId = instanceMap.get(testId);
|
|
2005
|
+
if (!serverId) {
|
|
2006
|
+
if (testState.currentStep) {
|
|
2007
|
+
testState.currentStep.error = `Instance not found: "${testId}"`;
|
|
2008
|
+
}
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
await adapter.updateStateData(serverId, { [field]: value });
|
|
2012
|
+
});
|
|
2013
|
+
handlers.set("__test_publish_event", async (_action) => {
|
|
2014
|
+
if (testState.currentStep) {
|
|
2015
|
+
testState.currentStep.error = "publish_event not supported in API mode. Events flow via EventRouter when transitions fire.";
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
handlers.set("__test_settle", async () => {
|
|
2019
|
+
if (settleDelayMs > 0) {
|
|
2020
|
+
await new Promise((resolve) => setTimeout(resolve, settleDelayMs));
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
handlers.set("__test_assert", async (action) => {
|
|
2024
|
+
const { testId, target, path, operator, expected, label } = action.config;
|
|
2025
|
+
const serverId = instanceMap.get(testId);
|
|
2026
|
+
if (!serverId) {
|
|
2027
|
+
testState.currentStep?.assertions.push({
|
|
2028
|
+
passed: false,
|
|
2029
|
+
assertion: { target, path, operator, expected, label },
|
|
2030
|
+
actual: void 0,
|
|
2031
|
+
error: `Instance not found: "${testId}"`
|
|
2032
|
+
});
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const snapshot = await adapter.getInstance(serverId);
|
|
2036
|
+
const actual = getApiTarget(target, path, snapshot);
|
|
2037
|
+
const passed = compareValues2(actual, operator, expected);
|
|
2038
|
+
testState.currentStep?.assertions.push({
|
|
2039
|
+
passed,
|
|
2040
|
+
assertion: { target, path, operator, expected, label },
|
|
2041
|
+
actual
|
|
2042
|
+
});
|
|
2043
|
+
});
|
|
2044
|
+
handlers.set("__test_report", () => {
|
|
2045
|
+
});
|
|
2046
|
+
handlers.set("set_field", (action) => {
|
|
2047
|
+
testState.setOrchestratorField(
|
|
2048
|
+
action.config.field,
|
|
2049
|
+
action.config.value
|
|
2050
|
+
);
|
|
2051
|
+
});
|
|
2052
|
+
return {
|
|
2053
|
+
handlers,
|
|
2054
|
+
state: testState,
|
|
2055
|
+
// Expose for cleanup
|
|
2056
|
+
getCreatedInstanceIds: () => createdIds,
|
|
2057
|
+
getInstanceMap: () => instanceMap
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function getInProcessTarget(target, path, sm) {
|
|
2061
|
+
switch (target) {
|
|
2062
|
+
case "state":
|
|
2063
|
+
return sm.currentState;
|
|
2064
|
+
case "status":
|
|
2065
|
+
return sm.status;
|
|
2066
|
+
case "state_data":
|
|
2067
|
+
if (!path) return sm.stateData;
|
|
2068
|
+
return getNestedValue2(sm.stateData, path);
|
|
2069
|
+
case "available_transitions":
|
|
2070
|
+
return sm.getAvailableTransitions().map((t) => t.name);
|
|
2071
|
+
default:
|
|
2072
|
+
throw new Error(`Unknown assertion target: "${target}"`);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function getApiTarget(target, path, snapshot) {
|
|
2076
|
+
switch (target) {
|
|
2077
|
+
case "state":
|
|
2078
|
+
return snapshot.currentState;
|
|
2079
|
+
case "status":
|
|
2080
|
+
return snapshot.status;
|
|
2081
|
+
case "state_data":
|
|
2082
|
+
if (!path) return snapshot.stateData;
|
|
2083
|
+
return getNestedValue2(snapshot.stateData, path);
|
|
2084
|
+
case "available_transitions":
|
|
2085
|
+
return snapshot.availableTransitions;
|
|
2086
|
+
default:
|
|
2087
|
+
throw new Error(`Unknown assertion target: "${target}"`);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function getNestedValue2(obj, path) {
|
|
2091
|
+
const parts = path.split(".");
|
|
2092
|
+
let current = obj;
|
|
2093
|
+
for (const part of parts) {
|
|
2094
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
2095
|
+
current = current[part];
|
|
2096
|
+
}
|
|
2097
|
+
return current;
|
|
2098
|
+
}
|
|
2099
|
+
function compareValues2(actual, operator, expected) {
|
|
2100
|
+
switch (operator) {
|
|
2101
|
+
case "eq":
|
|
2102
|
+
return deepEqual2(actual, expected);
|
|
2103
|
+
case "neq":
|
|
2104
|
+
return !deepEqual2(actual, expected);
|
|
2105
|
+
case "gt":
|
|
2106
|
+
return typeof actual === "number" && typeof expected === "number" && actual > expected;
|
|
2107
|
+
case "gte":
|
|
2108
|
+
return typeof actual === "number" && typeof expected === "number" && actual >= expected;
|
|
2109
|
+
case "lt":
|
|
2110
|
+
return typeof actual === "number" && typeof expected === "number" && actual < expected;
|
|
2111
|
+
case "lte":
|
|
2112
|
+
return typeof actual === "number" && typeof expected === "number" && actual <= expected;
|
|
2113
|
+
case "contains": {
|
|
2114
|
+
if (Array.isArray(actual)) return actual.includes(expected);
|
|
2115
|
+
if (typeof actual === "string" && typeof expected === "string")
|
|
2116
|
+
return actual.includes(expected);
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
case "truthy":
|
|
2120
|
+
return Boolean(actual);
|
|
2121
|
+
case "falsy":
|
|
2122
|
+
return !actual;
|
|
2123
|
+
default:
|
|
2124
|
+
return false;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
function deepEqual2(a, b) {
|
|
2128
|
+
if (a === b) return true;
|
|
2129
|
+
if (a == null || b == null) return false;
|
|
2130
|
+
if (typeof a !== typeof b) return false;
|
|
2131
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
2132
|
+
if (a.length !== b.length) return false;
|
|
2133
|
+
return a.every((v, i) => deepEqual2(v, b[i]));
|
|
2134
|
+
}
|
|
2135
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
2136
|
+
const keysA = Object.keys(a);
|
|
2137
|
+
const keysB = Object.keys(b);
|
|
2138
|
+
if (keysA.length !== keysB.length) return false;
|
|
2139
|
+
return keysA.every(
|
|
2140
|
+
(k) => deepEqual2(
|
|
2141
|
+
a[k],
|
|
2142
|
+
b[k]
|
|
2143
|
+
)
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
2148
|
+
function createInstanceActionHandlers(_def, _evaluator) {
|
|
2149
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
2150
|
+
let smRef = null;
|
|
2151
|
+
handlers.set("set_field", (action) => {
|
|
2152
|
+
if (smRef && typeof action.config.field === "string") {
|
|
2153
|
+
smRef.setField(action.config.field, action.config.value);
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
handlers.set("set_memory", (action) => {
|
|
2157
|
+
if (smRef && typeof action.config.key === "string") {
|
|
2158
|
+
smRef.setMemory(action.config.key, action.config.value);
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
handlers.__bindSm = (sm) => {
|
|
2162
|
+
smRef = sm;
|
|
2163
|
+
};
|
|
2164
|
+
return handlers;
|
|
2165
|
+
}
|
|
2166
|
+
function wireOnEventSubscriptions2(sm, eventBus, evaluator) {
|
|
2167
|
+
const unsubs = [];
|
|
2168
|
+
function subscribeCurrentState() {
|
|
2169
|
+
for (const unsub of unsubs) unsub();
|
|
2170
|
+
unsubs.length = 0;
|
|
2171
|
+
const stateDef = sm.getCurrentStateDefinition();
|
|
2172
|
+
if (!stateDef?.on_event?.length) return;
|
|
2173
|
+
for (const sub of stateDef.on_event) {
|
|
2174
|
+
const unsub = eventBus.subscribe(sub.match, async (event) => {
|
|
2175
|
+
const ctx = {
|
|
2176
|
+
...sm.stateData,
|
|
2177
|
+
state_data: sm.stateData,
|
|
2178
|
+
event: event.payload,
|
|
2179
|
+
current_state: sm.currentState,
|
|
2180
|
+
status: sm.status
|
|
2181
|
+
};
|
|
2182
|
+
if (sub.conditions?.length) {
|
|
2183
|
+
for (const condition of sub.conditions) {
|
|
2184
|
+
const result = evaluator.evaluate(condition, ctx);
|
|
2185
|
+
if (!result.value) return;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
for (const action of sub.actions) {
|
|
2189
|
+
if (action.type === "set_field" && typeof action.config.field === "string") {
|
|
2190
|
+
sm.setField(action.config.field, action.config.value);
|
|
2191
|
+
} else if (action.type === "set_memory" && typeof action.config.key === "string") {
|
|
2192
|
+
sm.setMemory(action.config.key, action.config.value);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
unsubs.push(unsub);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
subscribeCurrentState();
|
|
2200
|
+
sm.on((event) => {
|
|
2201
|
+
if (event.type === "state_enter") {
|
|
2202
|
+
subscribeCurrentState();
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/testing/blueprint-test-runner.ts
|
|
2208
|
+
async function runBlueprintTestProgram(program, config) {
|
|
2209
|
+
const start = performance.now();
|
|
2210
|
+
const scenarioResults = [];
|
|
2211
|
+
let allPassed = true;
|
|
2212
|
+
for (let i = 0; i < program.scenarios.length; i++) {
|
|
2213
|
+
if (config.abortSignal?.aborted) break;
|
|
2214
|
+
const result = await runBlueprintScenario(
|
|
2215
|
+
program.scenarios[i],
|
|
2216
|
+
program.definitions,
|
|
2217
|
+
i,
|
|
2218
|
+
config
|
|
2219
|
+
);
|
|
2220
|
+
scenarioResults.push(result);
|
|
2221
|
+
if (!result.passed) allPassed = false;
|
|
2222
|
+
config.onScenarioComplete?.(result);
|
|
2223
|
+
}
|
|
2224
|
+
return {
|
|
2225
|
+
passed: allPassed,
|
|
2226
|
+
scenarioResults,
|
|
2227
|
+
durationMs: performance.now() - start
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
async function runBlueprintScenario(scenario, definitions, scenarioIndex, config) {
|
|
2231
|
+
const start = performance.now();
|
|
2232
|
+
try {
|
|
2233
|
+
const blueprint = compileTestScenario(scenario, definitions, scenarioIndex);
|
|
2234
|
+
const { handlers, state: testState } = createActionHandlers(definitions, config);
|
|
2235
|
+
const evaluator = createEvaluator({
|
|
2236
|
+
functions: [],
|
|
2237
|
+
failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION
|
|
2238
|
+
});
|
|
2239
|
+
const sm = new StateMachine(blueprint, {}, { evaluator, actionHandlers: handlers });
|
|
2240
|
+
testState.setOrchestratorField = (field, value) => {
|
|
2241
|
+
sm.setField(field, value);
|
|
2242
|
+
};
|
|
2243
|
+
const stepCount = scenario.steps.length;
|
|
2244
|
+
const setupResult = await sm.transition("setup");
|
|
2245
|
+
if (!setupResult.success) {
|
|
2246
|
+
return {
|
|
2247
|
+
scenarioName: scenario.name,
|
|
2248
|
+
passed: false,
|
|
2249
|
+
stepResults: [],
|
|
2250
|
+
durationMs: performance.now() - start,
|
|
2251
|
+
error: `Setup failed: ${setupResult.error}`
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
for (let i = 0; i < stepCount; i++) {
|
|
2255
|
+
if (config.abortSignal?.aborted) break;
|
|
2256
|
+
const result = await sm.transition("next");
|
|
2257
|
+
if (!result.success) {
|
|
2258
|
+
testState.stepResults.push({
|
|
2259
|
+
stepName: scenario.steps[i]?.name ?? `step_${i}`,
|
|
2260
|
+
passed: false,
|
|
2261
|
+
assertionResults: [],
|
|
2262
|
+
durationMs: 0,
|
|
2263
|
+
error: `Blueprint transition failed: ${result.error}`
|
|
2264
|
+
});
|
|
2265
|
+
testState.failed = true;
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
const lastStep = testState.stepResults[testState.stepResults.length - 1];
|
|
2269
|
+
if (lastStep) {
|
|
2270
|
+
config.onStepComplete?.(scenarioIndex, lastStep);
|
|
2271
|
+
}
|
|
2272
|
+
if (sm.stateData.__test_step_failed) {
|
|
2273
|
+
await sm.transition("abort");
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
if (!testState.failed && !config.abortSignal?.aborted) {
|
|
2278
|
+
await sm.transition("next");
|
|
2279
|
+
}
|
|
2280
|
+
const passed = !testState.failed;
|
|
2281
|
+
return {
|
|
2282
|
+
scenarioName: scenario.name,
|
|
2283
|
+
passed,
|
|
2284
|
+
stepResults: testState.stepResults,
|
|
2285
|
+
durationMs: performance.now() - start
|
|
2286
|
+
};
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
return {
|
|
2289
|
+
scenarioName: scenario.name,
|
|
2290
|
+
passed: false,
|
|
2291
|
+
stepResults: [],
|
|
2292
|
+
durationMs: performance.now() - start,
|
|
2293
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function createActionHandlers(definitions, config) {
|
|
2298
|
+
if (config.mode === "api") {
|
|
2299
|
+
return createApiTestActions(config.adapter, {
|
|
2300
|
+
settleDelayMs: config.settleDelayMs
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
return createInProcessTestActions(definitions);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// src/testing/nrt-types.ts
|
|
2307
|
+
function createEmptyNRT() {
|
|
2308
|
+
return {
|
|
2309
|
+
version: 1,
|
|
2310
|
+
root: {
|
|
2311
|
+
id: "root",
|
|
2312
|
+
type: "Root",
|
|
2313
|
+
visible: true,
|
|
2314
|
+
children: []
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
function findNode(nrt, nodeId) {
|
|
2319
|
+
return findNodeRecursive(nrt.root, nodeId);
|
|
2320
|
+
}
|
|
2321
|
+
function findNodeRecursive(node, nodeId) {
|
|
2322
|
+
if (node.id === nodeId) return node;
|
|
2323
|
+
for (const child of node.children) {
|
|
2324
|
+
const found = findNodeRecursive(child, nodeId);
|
|
2325
|
+
if (found) return found;
|
|
2326
|
+
}
|
|
2327
|
+
return void 0;
|
|
2328
|
+
}
|
|
2329
|
+
function findVisibleNodes(nrt) {
|
|
2330
|
+
const result = [];
|
|
2331
|
+
collectVisible(nrt.root, result);
|
|
2332
|
+
return result;
|
|
2333
|
+
}
|
|
2334
|
+
function collectVisible(node, result) {
|
|
2335
|
+
if (node.visible) {
|
|
2336
|
+
result.push(node);
|
|
2337
|
+
for (const child of node.children) {
|
|
2338
|
+
collectVisible(child, result);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function findInteractiveNodes(nrt) {
|
|
2343
|
+
const result = [];
|
|
2344
|
+
collectInteractive(nrt.root, result);
|
|
2345
|
+
return result;
|
|
2346
|
+
}
|
|
2347
|
+
function collectInteractive(node, result) {
|
|
2348
|
+
if (node.events && Object.keys(node.events).length > 0) {
|
|
2349
|
+
result.push(node);
|
|
2350
|
+
}
|
|
2351
|
+
for (const child of node.children) {
|
|
2352
|
+
collectInteractive(child, result);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
function countNodes(nrt) {
|
|
2356
|
+
return countNodesRecursive(nrt.root);
|
|
2357
|
+
}
|
|
2358
|
+
function countNodesRecursive(node) {
|
|
2359
|
+
let count = 1;
|
|
2360
|
+
for (const child of node.children) {
|
|
2361
|
+
count += countNodesRecursive(child);
|
|
2362
|
+
}
|
|
2363
|
+
return count;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// src/testing/nrt-comparator.ts
|
|
2367
|
+
function compareNRT(before, after) {
|
|
2368
|
+
const changes = [];
|
|
2369
|
+
if (before.workflowState !== after.workflowState) {
|
|
2370
|
+
changes.push({
|
|
2371
|
+
type: "modified",
|
|
2372
|
+
path: "workflowState",
|
|
2373
|
+
field: "workflowState",
|
|
2374
|
+
oldValue: before.workflowState,
|
|
2375
|
+
newValue: after.workflowState
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
if (before.nav?.routeState !== after.nav?.routeState) {
|
|
2379
|
+
changes.push({
|
|
2380
|
+
type: "modified",
|
|
2381
|
+
path: "nav.routeState",
|
|
2382
|
+
field: "routeState",
|
|
2383
|
+
oldValue: before.nav?.routeState,
|
|
2384
|
+
newValue: after.nav?.routeState
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
compareNodes(before.root, after.root, "root", changes);
|
|
2388
|
+
return {
|
|
2389
|
+
equal: changes.length === 0,
|
|
2390
|
+
changes
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
function compareNodes(before, after, path, changes) {
|
|
2394
|
+
if (!before && after) {
|
|
2395
|
+
changes.push({
|
|
2396
|
+
type: "added",
|
|
2397
|
+
path,
|
|
2398
|
+
nodeId: after.id,
|
|
2399
|
+
field: "node",
|
|
2400
|
+
newValue: summarizeNode(after)
|
|
2401
|
+
});
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
if (before && !after) {
|
|
2405
|
+
changes.push({
|
|
2406
|
+
type: "removed",
|
|
2407
|
+
path,
|
|
2408
|
+
nodeId: before.id,
|
|
2409
|
+
field: "node",
|
|
2410
|
+
oldValue: summarizeNode(before)
|
|
2411
|
+
});
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (!before || !after) return;
|
|
2415
|
+
if (before.type !== after.type) {
|
|
2416
|
+
changes.push({
|
|
2417
|
+
type: "modified",
|
|
2418
|
+
path,
|
|
2419
|
+
nodeId: after.id,
|
|
2420
|
+
field: "type",
|
|
2421
|
+
oldValue: before.type,
|
|
2422
|
+
newValue: after.type
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
if (before.text !== after.text) {
|
|
2426
|
+
changes.push({
|
|
2427
|
+
type: "modified",
|
|
2428
|
+
path,
|
|
2429
|
+
nodeId: after.id,
|
|
2430
|
+
field: "text",
|
|
2431
|
+
oldValue: before.text,
|
|
2432
|
+
newValue: after.text
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
if (before.visible !== after.visible) {
|
|
2436
|
+
changes.push({
|
|
2437
|
+
type: "modified",
|
|
2438
|
+
path,
|
|
2439
|
+
nodeId: after.id,
|
|
2440
|
+
field: "visible",
|
|
2441
|
+
oldValue: before.visible,
|
|
2442
|
+
newValue: after.visible
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
if (before.enabled !== after.enabled) {
|
|
2446
|
+
changes.push({
|
|
2447
|
+
type: "modified",
|
|
2448
|
+
path,
|
|
2449
|
+
nodeId: after.id,
|
|
2450
|
+
field: "enabled",
|
|
2451
|
+
oldValue: before.enabled,
|
|
2452
|
+
newValue: after.enabled
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
if (JSON.stringify(before.value) !== JSON.stringify(after.value)) {
|
|
2456
|
+
changes.push({
|
|
2457
|
+
type: "modified",
|
|
2458
|
+
path,
|
|
2459
|
+
nodeId: after.id,
|
|
2460
|
+
field: "value",
|
|
2461
|
+
oldValue: before.value,
|
|
2462
|
+
newValue: after.value
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
const beforeEvents = Object.keys(before.events || {});
|
|
2466
|
+
const afterEvents = Object.keys(after.events || {});
|
|
2467
|
+
for (const event of afterEvents) {
|
|
2468
|
+
if (!beforeEvents.includes(event)) {
|
|
2469
|
+
changes.push({
|
|
2470
|
+
type: "added",
|
|
2471
|
+
path: `${path}.events`,
|
|
2472
|
+
nodeId: after.id,
|
|
2473
|
+
field: event,
|
|
2474
|
+
newValue: after.events[event]
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
for (const event of beforeEvents) {
|
|
2479
|
+
if (!afterEvents.includes(event)) {
|
|
2480
|
+
changes.push({
|
|
2481
|
+
type: "removed",
|
|
2482
|
+
path: `${path}.events`,
|
|
2483
|
+
nodeId: before.id,
|
|
2484
|
+
field: event,
|
|
2485
|
+
oldValue: before.events[event]
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
const maxChildren = Math.max(before.children.length, after.children.length);
|
|
2490
|
+
for (let i = 0; i < maxChildren; i++) {
|
|
2491
|
+
compareNodes(
|
|
2492
|
+
before.children[i],
|
|
2493
|
+
after.children[i],
|
|
2494
|
+
`${path}.children[${i}]`,
|
|
2495
|
+
changes
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
function summarizeNode(node) {
|
|
2500
|
+
return {
|
|
2501
|
+
id: node.id,
|
|
2502
|
+
type: node.type,
|
|
2503
|
+
text: node.text,
|
|
2504
|
+
visible: node.visible,
|
|
2505
|
+
childCount: node.children.length
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// src/testing/action-trace.ts
|
|
2510
|
+
function createActionRecorder(workflowSlug, instanceId) {
|
|
2511
|
+
let tickCounter = 0;
|
|
2512
|
+
let ticks = [];
|
|
2513
|
+
let startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2514
|
+
let endedAt;
|
|
2515
|
+
function recordTick(kind, name, extra) {
|
|
2516
|
+
ticks.push({
|
|
2517
|
+
tick: tickCounter++,
|
|
2518
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2519
|
+
kind,
|
|
2520
|
+
name,
|
|
2521
|
+
fromState: extra?.fromState,
|
|
2522
|
+
toState: extra?.toState,
|
|
2523
|
+
durationMs: extra?.durationMs,
|
|
2524
|
+
detail: extra?.detail || {}
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
return {
|
|
2528
|
+
recordTransition(name, fromState, toState, detail) {
|
|
2529
|
+
recordTick("transition", name, { fromState, toState, detail });
|
|
2530
|
+
},
|
|
2531
|
+
recordAction(name, detail) {
|
|
2532
|
+
recordTick("action", name, { detail });
|
|
2533
|
+
},
|
|
2534
|
+
recordTimer(name, detail) {
|
|
2535
|
+
recordTick("timer", name, { detail });
|
|
2536
|
+
},
|
|
2537
|
+
recordQuery(name, detail) {
|
|
2538
|
+
recordTick("query", name, { detail });
|
|
2539
|
+
},
|
|
2540
|
+
recordMutation(name, detail) {
|
|
2541
|
+
recordTick("mutation", name, { detail });
|
|
2542
|
+
},
|
|
2543
|
+
recordEvent(name, detail) {
|
|
2544
|
+
recordTick("event", name, { detail });
|
|
2545
|
+
},
|
|
2546
|
+
recordError(name, detail) {
|
|
2547
|
+
recordTick("error", name, { detail });
|
|
2548
|
+
},
|
|
2549
|
+
getTrace() {
|
|
2550
|
+
return {
|
|
2551
|
+
version: 1,
|
|
2552
|
+
workflowSlug,
|
|
2553
|
+
instanceId,
|
|
2554
|
+
startedAt,
|
|
2555
|
+
endedAt,
|
|
2556
|
+
ticks: [...ticks]
|
|
2557
|
+
};
|
|
2558
|
+
},
|
|
2559
|
+
finalize() {
|
|
2560
|
+
endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2561
|
+
return this.getTrace();
|
|
2562
|
+
},
|
|
2563
|
+
reset() {
|
|
2564
|
+
tickCounter = 0;
|
|
2565
|
+
ticks = [];
|
|
2566
|
+
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2567
|
+
endedAt = void 0;
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
function getTransitionPath(trace) {
|
|
2572
|
+
return trace.ticks.filter((t) => t.kind === "transition").map((t) => `${t.fromState} \u2192 ${t.toState}`);
|
|
2573
|
+
}
|
|
2574
|
+
function getFinalState(trace) {
|
|
2575
|
+
const transitions = trace.ticks.filter((t) => t.kind === "transition");
|
|
2576
|
+
if (transitions.length === 0) return void 0;
|
|
2577
|
+
return transitions[transitions.length - 1].toState;
|
|
2578
|
+
}
|
|
2579
|
+
function countByKind(trace) {
|
|
2580
|
+
const counts = {};
|
|
2581
|
+
for (const tick of trace.ticks) {
|
|
2582
|
+
counts[tick.kind] = (counts[tick.kind] || 0) + 1;
|
|
2583
|
+
}
|
|
2584
|
+
return counts;
|
|
2585
|
+
}
|
|
2586
|
+
function hasTransition(trace, from, to) {
|
|
2587
|
+
return trace.ticks.some(
|
|
2588
|
+
(t) => t.kind === "transition" && t.fromState === from && t.toState === to
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/validation/definition-validator.ts
|
|
2593
|
+
var DEFAULT_BINDING_ROOTS = [
|
|
2594
|
+
"$instance",
|
|
2595
|
+
"$definition",
|
|
2596
|
+
"$instances",
|
|
2597
|
+
"$local",
|
|
2598
|
+
"$entity",
|
|
2599
|
+
"$user",
|
|
2600
|
+
"$fn",
|
|
2601
|
+
"$action",
|
|
2602
|
+
"$item",
|
|
2603
|
+
"$index",
|
|
2604
|
+
"$pagination"
|
|
2605
|
+
];
|
|
2606
|
+
function validateDefinition(def, options = {}) {
|
|
2607
|
+
const issues = [];
|
|
2608
|
+
const knownRoots = options.knownBindingRoots ?? DEFAULT_BINDING_ROOTS;
|
|
2609
|
+
validateStructure(def, issues);
|
|
2610
|
+
validateReferences(def, issues);
|
|
2611
|
+
if (!options.skipSemantic) {
|
|
2612
|
+
validateSemantics(def, issues);
|
|
2613
|
+
}
|
|
2614
|
+
if (!options.skipExperience && def.metadata?.experience) {
|
|
2615
|
+
validateExperienceTree(
|
|
2616
|
+
def.metadata.experience,
|
|
2617
|
+
options.knownComponents,
|
|
2618
|
+
knownRoots,
|
|
2619
|
+
issues
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
2623
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
2624
|
+
return {
|
|
2625
|
+
valid: errorCount === 0,
|
|
2626
|
+
issues,
|
|
2627
|
+
errorCount,
|
|
2628
|
+
warningCount,
|
|
2629
|
+
summary: {
|
|
2630
|
+
stateCount: def.states?.length ?? 0,
|
|
2631
|
+
transitionCount: def.transitions?.length ?? 0,
|
|
2632
|
+
fieldCount: def.fields?.length ?? 0,
|
|
2633
|
+
hasExperience: !!def.metadata?.experience,
|
|
2634
|
+
hasExtensions: !!def.extensions && Object.keys(def.extensions).length > 0
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
function validateStructure(def, issues) {
|
|
2639
|
+
if (!def.slug) {
|
|
2640
|
+
issues.push({
|
|
2641
|
+
code: "MISSING_SLUG",
|
|
2642
|
+
message: "Workflow definition is missing a slug",
|
|
2643
|
+
severity: "error",
|
|
2644
|
+
category: "structural",
|
|
2645
|
+
suggestion: "Add a slug field to the workflow definition"
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
if (!def.name) {
|
|
2649
|
+
issues.push({
|
|
2650
|
+
code: "MISSING_NAME",
|
|
2651
|
+
message: "Workflow definition is missing a name",
|
|
2652
|
+
severity: "warning",
|
|
2653
|
+
category: "structural",
|
|
2654
|
+
suggestion: "Add a name field to the workflow definition"
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
if (!def.version) {
|
|
2658
|
+
issues.push({
|
|
2659
|
+
code: "MISSING_VERSION",
|
|
2660
|
+
message: "Workflow definition is missing a version",
|
|
2661
|
+
severity: "warning",
|
|
2662
|
+
category: "structural",
|
|
2663
|
+
suggestion: 'Add a version field (e.g., "1.0.0")'
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
if (!def.states || def.states.length === 0) {
|
|
2667
|
+
issues.push({
|
|
2668
|
+
code: "NO_STATES",
|
|
2669
|
+
message: "Workflow definition has no states",
|
|
2670
|
+
severity: "error",
|
|
2671
|
+
category: "structural",
|
|
2672
|
+
suggestion: "Add at least one state with type START"
|
|
2673
|
+
});
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
for (let i = 0; i < def.states.length; i++) {
|
|
2677
|
+
validateState(def.states[i], i, issues);
|
|
2678
|
+
}
|
|
2679
|
+
if (def.transitions) {
|
|
2680
|
+
for (let i = 0; i < def.transitions.length; i++) {
|
|
2681
|
+
validateTransitionStructure(def.transitions[i], i, issues);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (def.fields) {
|
|
2685
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
2686
|
+
for (let i = 0; i < def.fields.length; i++) {
|
|
2687
|
+
validateFieldStructure(def.fields[i], i, fieldNames, issues);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function validateState(state, index, issues) {
|
|
2692
|
+
const path = `states[${index}]`;
|
|
2693
|
+
if (!state.name) {
|
|
2694
|
+
issues.push({
|
|
2695
|
+
code: "STATE_MISSING_NAME",
|
|
2696
|
+
message: `State at index ${index} is missing a name`,
|
|
2697
|
+
severity: "error",
|
|
2698
|
+
category: "structural",
|
|
2699
|
+
path
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
const stateType = state.type || state.state_type;
|
|
2703
|
+
if (!stateType) {
|
|
2704
|
+
issues.push({
|
|
2705
|
+
code: "STATE_MISSING_TYPE",
|
|
2706
|
+
message: `State '${state.name || index}' is missing a type`,
|
|
2707
|
+
severity: "error",
|
|
2708
|
+
category: "structural",
|
|
2709
|
+
path,
|
|
2710
|
+
suggestion: "Set type to START, REGULAR, END, or CANCELLED"
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
if (state.on_enter) {
|
|
2714
|
+
for (let j = 0; j < state.on_enter.length; j++) {
|
|
2715
|
+
validateAction(state.on_enter[j], `${path}.on_enter[${j}]`, issues);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (state.on_exit) {
|
|
2719
|
+
for (let j = 0; j < state.on_exit.length; j++) {
|
|
2720
|
+
validateAction(state.on_exit[j], `${path}.on_exit[${j}]`, issues);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
if (state.during) {
|
|
2724
|
+
for (let j = 0; j < state.during.length; j++) {
|
|
2725
|
+
validateDuringAction(state.during[j], `${path}.during[${j}]`, issues);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
function validateAction(action, path, issues) {
|
|
2730
|
+
const actionType = action.type || action.action_type;
|
|
2731
|
+
if (!action.id) {
|
|
2732
|
+
issues.push({
|
|
2733
|
+
code: "ACTION_MISSING_ID",
|
|
2734
|
+
message: `Action at ${path} is missing an id`,
|
|
2735
|
+
severity: "warning",
|
|
2736
|
+
category: "structural",
|
|
2737
|
+
path
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
if (!actionType) {
|
|
2741
|
+
issues.push({
|
|
2742
|
+
code: "ACTION_MISSING_TYPE",
|
|
2743
|
+
message: `Action '${action.id || "unknown"}' at ${path} is missing a type`,
|
|
2744
|
+
severity: "error",
|
|
2745
|
+
category: "structural",
|
|
2746
|
+
path,
|
|
2747
|
+
suggestion: 'Add a type field (e.g., "set_field", "set_memory")'
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function validateDuringAction(during, path, issues) {
|
|
2752
|
+
if (!during.type) {
|
|
2753
|
+
issues.push({
|
|
2754
|
+
code: "DURING_MISSING_TYPE",
|
|
2755
|
+
message: `During action at ${path} is missing a type`,
|
|
2756
|
+
severity: "error",
|
|
2757
|
+
category: "structural",
|
|
2758
|
+
path,
|
|
2759
|
+
suggestion: "Set type to interval, timeout, poll, once, or cron"
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
if (during.type === "interval" && !during.interval_ms) {
|
|
2763
|
+
issues.push({
|
|
2764
|
+
code: "DURING_MISSING_INTERVAL",
|
|
2765
|
+
message: `Interval during action at ${path} is missing interval_ms`,
|
|
2766
|
+
severity: "error",
|
|
2767
|
+
category: "structural",
|
|
2768
|
+
path,
|
|
2769
|
+
suggestion: "Set interval_ms to a positive number (milliseconds)"
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
if (during.type === "cron" && !during.cron) {
|
|
2773
|
+
issues.push({
|
|
2774
|
+
code: "DURING_MISSING_CRON",
|
|
2775
|
+
message: `Cron during action at ${path} is missing cron expression`,
|
|
2776
|
+
severity: "error",
|
|
2777
|
+
category: "structural",
|
|
2778
|
+
path
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
function validateTransitionStructure(transition, index, issues) {
|
|
2783
|
+
const path = `transitions[${index}]`;
|
|
2784
|
+
if (!transition.name) {
|
|
2785
|
+
issues.push({
|
|
2786
|
+
code: "TRANSITION_MISSING_NAME",
|
|
2787
|
+
message: `Transition at index ${index} is missing a name`,
|
|
2788
|
+
severity: "error",
|
|
2789
|
+
category: "structural",
|
|
2790
|
+
path
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
if (!transition.from || transition.from.length === 0) {
|
|
2794
|
+
issues.push({
|
|
2795
|
+
code: "TRANSITION_MISSING_FROM",
|
|
2796
|
+
message: `Transition '${transition.name || index}' has no source states`,
|
|
2797
|
+
severity: "error",
|
|
2798
|
+
category: "structural",
|
|
2799
|
+
path,
|
|
2800
|
+
suggestion: "Add at least one source state in the from array"
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
if (!transition.to) {
|
|
2804
|
+
issues.push({
|
|
2805
|
+
code: "TRANSITION_MISSING_TO",
|
|
2806
|
+
message: `Transition '${transition.name || index}' has no target state`,
|
|
2807
|
+
severity: "error",
|
|
2808
|
+
category: "structural",
|
|
2809
|
+
path,
|
|
2810
|
+
suggestion: "Set the to field to a valid state name"
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
function validateFieldStructure(field, index, seen, issues) {
|
|
2815
|
+
const path = `fields[${index}]`;
|
|
2816
|
+
if (!field.name) {
|
|
2817
|
+
issues.push({
|
|
2818
|
+
code: "FIELD_MISSING_NAME",
|
|
2819
|
+
message: `Field at index ${index} is missing a name`,
|
|
2820
|
+
severity: "error",
|
|
2821
|
+
category: "structural",
|
|
2822
|
+
path
|
|
2823
|
+
});
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
if (seen.has(field.name)) {
|
|
2827
|
+
issues.push({
|
|
2828
|
+
code: "DUPLICATE_FIELD",
|
|
2829
|
+
message: `Duplicate field name '${field.name}'`,
|
|
2830
|
+
severity: "error",
|
|
2831
|
+
category: "structural",
|
|
2832
|
+
path,
|
|
2833
|
+
suggestion: `Rename one of the '${field.name}' fields`
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
seen.add(field.name);
|
|
2837
|
+
const fieldType = field.type || field.field_type;
|
|
2838
|
+
if (!fieldType) {
|
|
2839
|
+
issues.push({
|
|
2840
|
+
code: "FIELD_MISSING_TYPE",
|
|
2841
|
+
message: `Field '${field.name}' is missing a type`,
|
|
2842
|
+
severity: "error",
|
|
2843
|
+
category: "structural",
|
|
2844
|
+
path,
|
|
2845
|
+
suggestion: 'Add a type field (e.g., "text", "number", "boolean")'
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function validateReferences(def, issues) {
|
|
2850
|
+
const stateNames = new Set((def.states || []).map((s) => s.name));
|
|
2851
|
+
const fieldNames = new Set((def.fields || []).map((f) => f.name));
|
|
2852
|
+
if (def.transitions) {
|
|
2853
|
+
for (let i = 0; i < def.transitions.length; i++) {
|
|
2854
|
+
const t = def.transitions[i];
|
|
2855
|
+
const path = `transitions[${i}]`;
|
|
2856
|
+
if (t.from) {
|
|
2857
|
+
for (const fromState of t.from) {
|
|
2858
|
+
if (!stateNames.has(fromState)) {
|
|
2859
|
+
issues.push({
|
|
2860
|
+
code: "TRANSITION_UNKNOWN_FROM",
|
|
2861
|
+
message: `Transition '${t.name}' references unknown source state '${fromState}'`,
|
|
2862
|
+
severity: "error",
|
|
2863
|
+
category: "referential",
|
|
2864
|
+
path,
|
|
2865
|
+
suggestion: `Valid states: ${Array.from(stateNames).join(", ")}`
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (t.to && !stateNames.has(t.to)) {
|
|
2871
|
+
issues.push({
|
|
2872
|
+
code: "TRANSITION_UNKNOWN_TO",
|
|
2873
|
+
message: `Transition '${t.name}' references unknown target state '${t.to}'`,
|
|
2874
|
+
severity: "error",
|
|
2875
|
+
category: "referential",
|
|
2876
|
+
path,
|
|
2877
|
+
suggestion: `Valid states: ${Array.from(stateNames).join(", ")}`
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
if (t.required_fields) {
|
|
2881
|
+
for (const rf of t.required_fields) {
|
|
2882
|
+
if (!fieldNames.has(rf)) {
|
|
2883
|
+
issues.push({
|
|
2884
|
+
code: "TRANSITION_UNKNOWN_FIELD",
|
|
2885
|
+
message: `Transition '${t.name}' requires unknown field '${rf}'`,
|
|
2886
|
+
severity: "warning",
|
|
2887
|
+
category: "referential",
|
|
2888
|
+
path
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
if (def.states) {
|
|
2896
|
+
for (let i = 0; i < def.states.length; i++) {
|
|
2897
|
+
const state = def.states[i];
|
|
2898
|
+
validateActionFieldRefs(state.on_enter, fieldNames, `states[${i}].on_enter`, issues);
|
|
2899
|
+
validateActionFieldRefs(state.on_exit, fieldNames, `states[${i}].on_exit`, issues);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (def.fields) {
|
|
2903
|
+
for (let i = 0; i < def.fields.length; i++) {
|
|
2904
|
+
const field = def.fields[i];
|
|
2905
|
+
const path = `fields[${i}]`;
|
|
2906
|
+
if (field.visible_in_states) {
|
|
2907
|
+
for (const s of field.visible_in_states) {
|
|
2908
|
+
if (!stateNames.has(s)) {
|
|
2909
|
+
issues.push({
|
|
2910
|
+
code: "FIELD_UNKNOWN_STATE",
|
|
2911
|
+
message: `Field '${field.name}' references unknown state '${s}' in visible_in_states`,
|
|
2912
|
+
severity: "warning",
|
|
2913
|
+
category: "referential",
|
|
2914
|
+
path
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (field.editable_in_states) {
|
|
2920
|
+
for (const s of field.editable_in_states) {
|
|
2921
|
+
if (!stateNames.has(s)) {
|
|
2922
|
+
issues.push({
|
|
2923
|
+
code: "FIELD_UNKNOWN_STATE",
|
|
2924
|
+
message: `Field '${field.name}' references unknown state '${s}' in editable_in_states`,
|
|
2925
|
+
severity: "warning",
|
|
2926
|
+
category: "referential",
|
|
2927
|
+
path
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
function validateActionFieldRefs(actions, fieldNames, basePath, issues) {
|
|
2936
|
+
if (!actions) return;
|
|
2937
|
+
for (let j = 0; j < actions.length; j++) {
|
|
2938
|
+
const action = actions[j];
|
|
2939
|
+
const actionType = action.type || action.action_type;
|
|
2940
|
+
if (actionType === "set_field" && action.config?.field) {
|
|
2941
|
+
const fieldName = String(action.config.field);
|
|
2942
|
+
if (!fieldNames.has(fieldName)) {
|
|
2943
|
+
issues.push({
|
|
2944
|
+
code: "ACTION_UNKNOWN_FIELD",
|
|
2945
|
+
message: `Action '${action.id}' references unknown field '${fieldName}'`,
|
|
2946
|
+
severity: "warning",
|
|
2947
|
+
category: "referential",
|
|
2948
|
+
path: `${basePath}[${j}]`,
|
|
2949
|
+
suggestion: `Declare field '${fieldName}' in the fields array`
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
function getStateType(state) {
|
|
2956
|
+
return state.type || state.state_type;
|
|
2957
|
+
}
|
|
2958
|
+
function validateSemantics(def, issues) {
|
|
2959
|
+
if (!def.states || def.states.length === 0) return;
|
|
2960
|
+
const startStates = def.states.filter((s) => getStateType(s) === "START");
|
|
2961
|
+
if (startStates.length === 0) {
|
|
2962
|
+
issues.push({
|
|
2963
|
+
code: "NO_START_STATE",
|
|
2964
|
+
message: "Workflow has no START state",
|
|
2965
|
+
severity: "error",
|
|
2966
|
+
category: "semantic",
|
|
2967
|
+
suggestion: 'Mark one state with type: "START"'
|
|
2968
|
+
});
|
|
2969
|
+
} else if (startStates.length > 1) {
|
|
2970
|
+
issues.push({
|
|
2971
|
+
code: "MULTIPLE_START_STATES",
|
|
2972
|
+
message: `Workflow has ${startStates.length} START states: ${startStates.map((s) => s.name).join(", ")}`,
|
|
2973
|
+
severity: "error",
|
|
2974
|
+
category: "semantic",
|
|
2975
|
+
suggestion: 'Only one state should have type: "START"'
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
const stateNameCounts = /* @__PURE__ */ new Map();
|
|
2979
|
+
for (const state of def.states) {
|
|
2980
|
+
const count = stateNameCounts.get(state.name) || 0;
|
|
2981
|
+
stateNameCounts.set(state.name, count + 1);
|
|
2982
|
+
}
|
|
2983
|
+
for (const [name, count] of stateNameCounts) {
|
|
2984
|
+
if (count > 1) {
|
|
2985
|
+
issues.push({
|
|
2986
|
+
code: "DUPLICATE_STATE",
|
|
2987
|
+
message: `Duplicate state name '${name}' (appears ${count} times)`,
|
|
2988
|
+
severity: "error",
|
|
2989
|
+
category: "semantic"
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
if (def.transitions) {
|
|
2994
|
+
const transitionNames = /* @__PURE__ */ new Map();
|
|
2995
|
+
for (const t of def.transitions) {
|
|
2996
|
+
const count = transitionNames.get(t.name) || 0;
|
|
2997
|
+
transitionNames.set(t.name, count + 1);
|
|
2998
|
+
}
|
|
2999
|
+
for (const [name, count] of transitionNames) {
|
|
3000
|
+
if (count > 1) {
|
|
3001
|
+
issues.push({
|
|
3002
|
+
code: "DUPLICATE_TRANSITION",
|
|
3003
|
+
message: `Duplicate transition name '${name}' (appears ${count} times)`,
|
|
3004
|
+
severity: "warning",
|
|
3005
|
+
category: "semantic",
|
|
3006
|
+
suggestion: "Consider using unique transition names for clarity"
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
if (def.transitions && def.transitions.length > 0) {
|
|
3012
|
+
const reachableStates = /* @__PURE__ */ new Set();
|
|
3013
|
+
for (const state of startStates) {
|
|
3014
|
+
reachableStates.add(state.name);
|
|
3015
|
+
}
|
|
3016
|
+
for (const t of def.transitions) {
|
|
3017
|
+
if (t.to) reachableStates.add(t.to);
|
|
3018
|
+
}
|
|
3019
|
+
for (const state of def.states) {
|
|
3020
|
+
if (!reachableStates.has(state.name)) {
|
|
3021
|
+
issues.push({
|
|
3022
|
+
code: "UNREACHABLE_STATE",
|
|
3023
|
+
message: `State '${state.name}' is unreachable (no transition targets it)`,
|
|
3024
|
+
severity: "warning",
|
|
3025
|
+
category: "semantic",
|
|
3026
|
+
suggestion: `Add a transition with to: "${state.name}" or remove this state`
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
if (def.transitions) {
|
|
3032
|
+
const statesWithOutgoing = /* @__PURE__ */ new Set();
|
|
3033
|
+
for (const t of def.transitions) {
|
|
3034
|
+
if (t.from) {
|
|
3035
|
+
for (const fromState of t.from) {
|
|
3036
|
+
statesWithOutgoing.add(fromState);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
for (const state of def.states) {
|
|
3041
|
+
const sType = getStateType(state);
|
|
3042
|
+
if (sType !== "END" && sType !== "CANCELLED" && !statesWithOutgoing.has(state.name)) {
|
|
3043
|
+
issues.push({
|
|
3044
|
+
code: "DEAD_END_STATE",
|
|
3045
|
+
message: `State '${state.name}' has no outgoing transitions (dead end)`,
|
|
3046
|
+
severity: "info",
|
|
3047
|
+
category: "semantic",
|
|
3048
|
+
suggestion: `Add a transition from "${state.name}" or mark it as an END state`
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
if (def.transitions) {
|
|
3054
|
+
const endStates = new Set(
|
|
3055
|
+
def.states.filter((s) => {
|
|
3056
|
+
const sType = getStateType(s);
|
|
3057
|
+
return sType === "END" || sType === "CANCELLED";
|
|
3058
|
+
}).map((s) => s.name)
|
|
3059
|
+
);
|
|
3060
|
+
for (const t of def.transitions) {
|
|
3061
|
+
if (t.from) {
|
|
3062
|
+
for (const fromState of t.from) {
|
|
3063
|
+
if (endStates.has(fromState)) {
|
|
3064
|
+
issues.push({
|
|
3065
|
+
code: "TRANSITION_FROM_END",
|
|
3066
|
+
message: `Transition '${t.name}' has source state '${fromState}' which is an END/CANCELLED state`,
|
|
3067
|
+
severity: "warning",
|
|
3068
|
+
category: "semantic",
|
|
3069
|
+
suggestion: "Remove transitions from END/CANCELLED states"
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
function validateExperienceTree(node, knownComponents, knownRoots, issues, path = "metadata.experience") {
|
|
3078
|
+
if (node.component && knownComponents) {
|
|
3079
|
+
if (!knownComponents.includes(node.component)) {
|
|
3080
|
+
issues.push({
|
|
3081
|
+
code: "UNKNOWN_COMPONENT",
|
|
3082
|
+
message: `Experience node '${node.id}' references unknown component '${node.component}'`,
|
|
3083
|
+
severity: "warning",
|
|
3084
|
+
category: "experience",
|
|
3085
|
+
path,
|
|
3086
|
+
suggestion: `Registered components: ${knownComponents.slice(0, 10).join(", ")}${knownComponents.length > 10 ? "..." : ""}`
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
if (node.bindings) {
|
|
3091
|
+
for (const [prop, expr] of Object.entries(node.bindings)) {
|
|
3092
|
+
validateBindingExpression(expr, `${path}.bindings.${prop}`, knownRoots, issues);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
if (node.visible_when) {
|
|
3096
|
+
validateBindingExpression(node.visible_when, `${path}.visible_when`, knownRoots, issues);
|
|
3097
|
+
}
|
|
3098
|
+
if (node.children) {
|
|
3099
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
3100
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
3101
|
+
const child = node.children[i];
|
|
3102
|
+
if (child.id && childIds.has(child.id)) {
|
|
3103
|
+
issues.push({
|
|
3104
|
+
code: "DUPLICATE_NODE_ID",
|
|
3105
|
+
message: `Duplicate experience node ID '${child.id}' under '${node.id}'`,
|
|
3106
|
+
severity: "warning",
|
|
3107
|
+
category: "experience",
|
|
3108
|
+
path: `${path}.children[${i}]`
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
if (child.id) childIds.add(child.id);
|
|
3112
|
+
validateExperienceTree(child, knownComponents, knownRoots, issues, `${path}.children[${i}]`);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
function validateBindingExpression(expr, path, knownRoots, issues) {
|
|
3117
|
+
if (!expr) return;
|
|
3118
|
+
const trimmed = expr.trim();
|
|
3119
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'") || /^-?\d/.test(trimmed) || trimmed === "true" || trimmed === "false" || trimmed === "null") {
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (trimmed.startsWith("action:") || trimmed.startsWith("setLocal:")) {
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
if (/^\w+\.fire$/.test(trimmed)) {
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
if (trimmed.startsWith("$")) {
|
|
3129
|
+
const dotIdx = trimmed.indexOf(".");
|
|
3130
|
+
const parenIdx = trimmed.indexOf("(");
|
|
3131
|
+
let root;
|
|
3132
|
+
if (dotIdx > 0 && (parenIdx < 0 || dotIdx < parenIdx)) {
|
|
3133
|
+
root = trimmed.slice(0, dotIdx);
|
|
3134
|
+
} else if (parenIdx > 0) {
|
|
3135
|
+
root = trimmed.slice(0, parenIdx);
|
|
3136
|
+
} else {
|
|
3137
|
+
root = trimmed;
|
|
3138
|
+
}
|
|
3139
|
+
const rootClean = root.replace(/[!=<>&|+\-*/\s].*/g, "").trim();
|
|
3140
|
+
if (rootClean && !knownRoots.includes(rootClean)) {
|
|
3141
|
+
issues.push({
|
|
3142
|
+
code: "UNKNOWN_BINDING_ROOT",
|
|
3143
|
+
message: `Binding expression at ${path} uses unknown root '${rootClean}'`,
|
|
3144
|
+
severity: "warning",
|
|
3145
|
+
category: "experience",
|
|
3146
|
+
path,
|
|
3147
|
+
suggestion: `Known roots: ${knownRoots.join(", ")}`
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
function isViableDefinition(def) {
|
|
3153
|
+
if (!def || typeof def !== "object") return false;
|
|
3154
|
+
const d = def;
|
|
3155
|
+
return typeof d.slug === "string" && Array.isArray(d.states) && d.states.length > 0 && Array.isArray(d.transitions) && Array.isArray(d.fields);
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// src/dsl/lexer.ts
|
|
3159
|
+
var SECTION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
3160
|
+
"things",
|
|
3161
|
+
"paths",
|
|
3162
|
+
"levels",
|
|
3163
|
+
"numbers",
|
|
3164
|
+
"tabs",
|
|
3165
|
+
"controls",
|
|
3166
|
+
"overlay",
|
|
3167
|
+
"actions"
|
|
3168
|
+
]);
|
|
3169
|
+
var QUALIFIER_PATTERNS = [
|
|
3170
|
+
{ pattern: /^(\w[\w\s]*)\s+first$/, kind: "order" },
|
|
3171
|
+
{ pattern: /^searchable\s+by\s+(.+)$/, kind: "searchable" },
|
|
3172
|
+
{ pattern: /^filterable\s+by\s+(.+)$/, kind: "filterable" },
|
|
3173
|
+
{ pattern: /^(\d+)\s+at\s+a\s+time$/, kind: "pagination" }
|
|
3174
|
+
];
|
|
3175
|
+
var TYPE_ADJECTIVES = /* @__PURE__ */ new Set([
|
|
3176
|
+
"required",
|
|
3177
|
+
"unique",
|
|
3178
|
+
"computed",
|
|
3179
|
+
"non-negative",
|
|
3180
|
+
"positive",
|
|
3181
|
+
"negative",
|
|
3182
|
+
"lowercase",
|
|
3183
|
+
"uppercase",
|
|
3184
|
+
"optional",
|
|
3185
|
+
"readonly"
|
|
3186
|
+
]);
|
|
3187
|
+
var FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
3188
|
+
"text",
|
|
3189
|
+
"number",
|
|
3190
|
+
"integer",
|
|
3191
|
+
"time",
|
|
3192
|
+
"rich text",
|
|
3193
|
+
"choice"
|
|
3194
|
+
]);
|
|
3195
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
3196
|
+
"tag",
|
|
3197
|
+
"card",
|
|
3198
|
+
"progress",
|
|
3199
|
+
"meter",
|
|
3200
|
+
"slider",
|
|
3201
|
+
"timeline",
|
|
3202
|
+
"image"
|
|
3203
|
+
]);
|
|
3204
|
+
var PRONOUNS = /* @__PURE__ */ new Set(["its", "my", "the", "this", "these"]);
|
|
3205
|
+
function tokenize(source) {
|
|
3206
|
+
const rawLines = source.split("\n");
|
|
3207
|
+
return rawLines.map((raw, i) => tokenizeLine(raw, i + 1));
|
|
3208
|
+
}
|
|
3209
|
+
function tokenizeLine(raw, lineNumber = 1) {
|
|
3210
|
+
const indent = measureIndent(raw);
|
|
3211
|
+
const trimmed = raw.trim();
|
|
3212
|
+
const data = classifyLine(trimmed);
|
|
3213
|
+
return { indent, lineNumber, raw, data };
|
|
3214
|
+
}
|
|
3215
|
+
function measureIndent(line) {
|
|
3216
|
+
let count = 0;
|
|
3217
|
+
for (const ch of line) {
|
|
3218
|
+
if (ch === " ") count++;
|
|
3219
|
+
else if (ch === " ") count += 2;
|
|
3220
|
+
else break;
|
|
3221
|
+
}
|
|
3222
|
+
return count;
|
|
3223
|
+
}
|
|
3224
|
+
function classifyLine(trimmed) {
|
|
3225
|
+
if (trimmed === "") return { type: "blank" };
|
|
3226
|
+
if (trimmed.startsWith("#")) {
|
|
3227
|
+
return { type: "comment", text: trimmed.slice(1).trim() };
|
|
3228
|
+
}
|
|
3229
|
+
if (trimmed === "pages") return { type: "pages" };
|
|
3230
|
+
if (SECTION_KEYWORDS.has(trimmed)) {
|
|
3231
|
+
return { type: "section", name: trimmed };
|
|
3232
|
+
}
|
|
3233
|
+
const taggedMatch = trimmed.match(/^tagged:\s*(.+)$/);
|
|
3234
|
+
if (taggedMatch) {
|
|
3235
|
+
return {
|
|
3236
|
+
type: "tagged",
|
|
3237
|
+
tags: taggedMatch[1].split(",").map((t) => t.trim())
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
const pathMappingMatch = trimmed.match(
|
|
3241
|
+
/^(\/[\w\/:.-]+)\s*→\s*([\w\s]+?)(?:\s*\((\w+)\))?$/
|
|
3242
|
+
);
|
|
3243
|
+
if (pathMappingMatch) {
|
|
3244
|
+
return {
|
|
3245
|
+
type: "path_mapping",
|
|
3246
|
+
path: pathMappingMatch[1],
|
|
3247
|
+
view: pathMappingMatch[2].trim(),
|
|
3248
|
+
context: pathMappingMatch[3]
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
const levelMatch = trimmed.match(
|
|
3252
|
+
/^(\d+):\s*"([^"]+)",\s*from\s+(\d+)\s+xp$/
|
|
3253
|
+
);
|
|
3254
|
+
if (levelMatch) {
|
|
3255
|
+
return {
|
|
3256
|
+
type: "level_def",
|
|
3257
|
+
level: parseInt(levelMatch[1], 10),
|
|
3258
|
+
title: levelMatch[2],
|
|
3259
|
+
fromXp: parseInt(levelMatch[3], 10)
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
const startsAtMatch = trimmed.match(/^starts\s+at\s+(.+)$/);
|
|
3263
|
+
if (startsAtMatch) {
|
|
3264
|
+
return { type: "starts_at", state: startsAtMatch[1].trim() };
|
|
3265
|
+
}
|
|
3266
|
+
const transitionMatch = trimmed.match(
|
|
3267
|
+
/^can\s+(.+?)\s*→\s*(.+?)(?:,\s*(.+))?$/
|
|
3268
|
+
);
|
|
3269
|
+
if (transitionMatch) {
|
|
3270
|
+
return {
|
|
3271
|
+
type: "transition",
|
|
3272
|
+
verb: transitionMatch[1].trim(),
|
|
3273
|
+
target: transitionMatch[2].trim(),
|
|
3274
|
+
guard: transitionMatch[3]?.trim()
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
if (trimmed.startsWith("when ") || trimmed === "when") {
|
|
3278
|
+
return { type: "when", condition: trimmed.slice(5).trim() };
|
|
3279
|
+
}
|
|
3280
|
+
const setMatch = trimmed.match(/^set\s+(.+?)\s*=\s*(.+)$/);
|
|
3281
|
+
if (setMatch) {
|
|
3282
|
+
return {
|
|
3283
|
+
type: "set_action",
|
|
3284
|
+
field: setMatch[1].trim(),
|
|
3285
|
+
expression: setMatch[2].trim()
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
if (trimmed.startsWith("do ")) {
|
|
3289
|
+
return { type: "do_action", action: trimmed.slice(3).trim() };
|
|
3290
|
+
}
|
|
3291
|
+
const goMatch = trimmed.match(/^go\s+to\s+(.+)$/);
|
|
3292
|
+
if (goMatch) {
|
|
3293
|
+
return { type: "go_action", path: goMatch[1].trim() };
|
|
3294
|
+
}
|
|
3295
|
+
const tellMatch = trimmed.match(/^tell\s+(.+?)\s+"([^"]+)"$/);
|
|
3296
|
+
if (tellMatch) {
|
|
3297
|
+
return {
|
|
3298
|
+
type: "tell_action",
|
|
3299
|
+
target: tellMatch[1].trim(),
|
|
3300
|
+
message: tellMatch[2]
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
const showMatch = trimmed.match(/^show\s+(.+?)(?:\s+(briefly))?$/);
|
|
3304
|
+
if (showMatch) {
|
|
3305
|
+
return {
|
|
3306
|
+
type: "show_action",
|
|
3307
|
+
content: showMatch[1].trim(),
|
|
3308
|
+
modifier: showMatch[2]
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
if (trimmed.startsWith("search ")) {
|
|
3312
|
+
return { type: "search", target: trimmed.slice(7).trim() };
|
|
3313
|
+
}
|
|
3314
|
+
const eachMatch = trimmed.match(
|
|
3315
|
+
/^each\s+(.+?)(?:\s+as\s+([\w\s]+?))?(?:,\s*(big|small))?$/
|
|
3316
|
+
);
|
|
3317
|
+
if (eachMatch) {
|
|
3318
|
+
return {
|
|
3319
|
+
type: "iteration",
|
|
3320
|
+
subject: eachMatch[1].trim(),
|
|
3321
|
+
role: eachMatch[2]?.trim(),
|
|
3322
|
+
emphasis: eachMatch[3]
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
for (const { pattern, kind } of QUALIFIER_PATTERNS) {
|
|
3326
|
+
const qMatch = trimmed.match(pattern);
|
|
3327
|
+
if (qMatch) {
|
|
3328
|
+
return { type: "qualifier", kind, value: qMatch[1] ?? trimmed };
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
const versionedDeclMatch = trimmed.match(
|
|
3332
|
+
/^(?:a\s+)?(.+?)\s+@(\d+\.\d+\.\d+)$/
|
|
3333
|
+
);
|
|
3334
|
+
if (versionedDeclMatch) {
|
|
3335
|
+
const hasPrefix = trimmed.startsWith("a ");
|
|
3336
|
+
const name = versionedDeclMatch[1].replace(/^a\s+/, "").trim();
|
|
3337
|
+
if (hasPrefix) {
|
|
3338
|
+
return { type: "thing_decl", name, version: versionedDeclMatch[2] };
|
|
3339
|
+
}
|
|
3340
|
+
return { type: "space_decl", name, version: versionedDeclMatch[2] };
|
|
3341
|
+
}
|
|
3342
|
+
const fragmentMatch = trimmed.match(/^a\s+(.+):$/);
|
|
3343
|
+
if (fragmentMatch) {
|
|
3344
|
+
return { type: "fragment_def", name: fragmentMatch[1].trim() };
|
|
3345
|
+
}
|
|
3346
|
+
const thingRefMatch = trimmed.match(/^(?:a\s+)?(.+?)\s*\((\w+)\)$/);
|
|
3347
|
+
if (thingRefMatch) {
|
|
3348
|
+
return {
|
|
3349
|
+
type: "thing_ref",
|
|
3350
|
+
name: thingRefMatch[1].trim(),
|
|
3351
|
+
kind: thingRefMatch[2]
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
const stringLitMatch = trimmed.match(
|
|
3355
|
+
/^"([^"]+)"(?:,\s*(big|small))?$/
|
|
3356
|
+
);
|
|
3357
|
+
if (stringLitMatch) {
|
|
3358
|
+
return {
|
|
3359
|
+
type: "string_literal",
|
|
3360
|
+
text: stringLitMatch[1],
|
|
3361
|
+
emphasis: stringLitMatch[2]
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
const navMatch = trimmed.match(/^(.+?)\s*→\s*(.+)$/);
|
|
3365
|
+
if (navMatch) {
|
|
3366
|
+
return {
|
|
3367
|
+
type: "navigation",
|
|
3368
|
+
trigger: navMatch[1].trim(),
|
|
3369
|
+
target: navMatch[2].trim()
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
const dataSourceResult = tryParseDataSource(trimmed);
|
|
3373
|
+
if (dataSourceResult) return dataSourceResult;
|
|
3374
|
+
const contentResult = tryParseContent(trimmed);
|
|
3375
|
+
if (contentResult) return contentResult;
|
|
3376
|
+
const fieldResult = tryParseFieldDef(trimmed);
|
|
3377
|
+
if (fieldResult) return fieldResult;
|
|
3378
|
+
const groupMatch = trimmed.match(/^(.+?)\s+by\s+(.+)$/);
|
|
3379
|
+
if (groupMatch) {
|
|
3380
|
+
return {
|
|
3381
|
+
type: "grouping",
|
|
3382
|
+
collection: groupMatch[1].trim(),
|
|
3383
|
+
key: groupMatch[2].trim()
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
const stateResult = tryParseStateDef(trimmed);
|
|
3387
|
+
if (stateResult) return stateResult;
|
|
3388
|
+
return { type: "unknown", text: trimmed };
|
|
3389
|
+
}
|
|
3390
|
+
function splitOutsideBrackets(str) {
|
|
3391
|
+
const parts = [];
|
|
3392
|
+
let current = "";
|
|
3393
|
+
let depth = 0;
|
|
3394
|
+
for (const ch of str) {
|
|
3395
|
+
if (ch === "[") depth++;
|
|
3396
|
+
else if (ch === "]") depth--;
|
|
3397
|
+
if (ch === "," && depth === 0) {
|
|
3398
|
+
parts.push(current.trim());
|
|
3399
|
+
current = "";
|
|
3400
|
+
} else {
|
|
3401
|
+
current += ch;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
if (current.trim()) parts.push(current.trim());
|
|
3405
|
+
return parts;
|
|
3406
|
+
}
|
|
3407
|
+
function tryParseFieldDef(trimmed) {
|
|
3408
|
+
const asMatch = trimmed.match(/^(.+?)\s+as\s+(.+)$/);
|
|
3409
|
+
if (!asMatch) return null;
|
|
3410
|
+
const name = asMatch[1].trim();
|
|
3411
|
+
const rest = asMatch[2].trim();
|
|
3412
|
+
const firstWord = name.split(/\s+/)[0];
|
|
3413
|
+
if (PRONOUNS.has(firstWord)) return null;
|
|
3414
|
+
if (rest.startsWith('"')) return null;
|
|
3415
|
+
const parts = splitOutsideBrackets(rest);
|
|
3416
|
+
const typeSpec = parts[0];
|
|
3417
|
+
const constraintParts = parts.slice(1);
|
|
3418
|
+
const typeWords = typeSpec.split(/\s+/);
|
|
3419
|
+
const adjectives = [];
|
|
3420
|
+
let baseType = "";
|
|
3421
|
+
const choiceMatch = typeSpec.match(/^(.+?\s+)?choice\s+of\s+\[(.+)\]$/);
|
|
3422
|
+
if (choiceMatch) {
|
|
3423
|
+
const prefix = choiceMatch[1]?.trim() ?? "";
|
|
3424
|
+
if (prefix) {
|
|
3425
|
+
for (const w of prefix.split(/\s+/)) {
|
|
3426
|
+
if (TYPE_ADJECTIVES.has(w)) adjectives.push(w);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
baseType = `choice of [${choiceMatch[2]}]`;
|
|
3430
|
+
} else {
|
|
3431
|
+
let i = 0;
|
|
3432
|
+
while (i < typeWords.length && TYPE_ADJECTIVES.has(typeWords[i])) {
|
|
3433
|
+
adjectives.push(typeWords[i]);
|
|
3434
|
+
i++;
|
|
3435
|
+
}
|
|
3436
|
+
baseType = typeWords.slice(i).join(" ");
|
|
3437
|
+
}
|
|
3438
|
+
if (!baseType) return null;
|
|
3439
|
+
const knownFieldType = FIELD_TYPES.has(baseType) || baseType.startsWith("choice of") || adjectives.length > 0;
|
|
3440
|
+
if (!knownFieldType) return null;
|
|
3441
|
+
if (CONTENT_ROLES.has(baseType) && adjectives.length === 0) return null;
|
|
3442
|
+
const constraints = [];
|
|
3443
|
+
for (const cp of constraintParts) {
|
|
3444
|
+
const constraint = parseConstraint(cp);
|
|
3445
|
+
if (constraint) constraints.push(constraint);
|
|
3446
|
+
}
|
|
3447
|
+
return {
|
|
3448
|
+
type: "field_def",
|
|
3449
|
+
name,
|
|
3450
|
+
adjectives,
|
|
3451
|
+
baseType,
|
|
3452
|
+
constraints
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
function parseConstraint(text) {
|
|
3456
|
+
const trimmed = text.trim();
|
|
3457
|
+
const maxMatch = trimmed.match(/^max\s+(\d+)$/);
|
|
3458
|
+
if (maxMatch) return { kind: "max", value: parseInt(maxMatch[1], 10) };
|
|
3459
|
+
const minMatch = trimmed.match(/^min\s+(\d+)$/);
|
|
3460
|
+
if (minMatch) return { kind: "min", value: parseInt(minMatch[1], 10) };
|
|
3461
|
+
const defaultMatch = trimmed.match(/^default\s+(.+)$/);
|
|
3462
|
+
if (defaultMatch) {
|
|
3463
|
+
const val = defaultMatch[1].trim();
|
|
3464
|
+
const num = Number(val);
|
|
3465
|
+
return { kind: "default", value: isNaN(num) ? val.replace(/^"(.*)"$/, "$1") : num };
|
|
3466
|
+
}
|
|
3467
|
+
const betweenMatch = trimmed.match(/^between\s+(\d+)\s+and\s+(\d+)$/);
|
|
3468
|
+
if (betweenMatch) {
|
|
3469
|
+
return {
|
|
3470
|
+
kind: "between",
|
|
3471
|
+
value: parseInt(betweenMatch[1], 10),
|
|
3472
|
+
value2: parseInt(betweenMatch[2], 10)
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
if (trimmed === "unique") return { kind: "unique", value: true };
|
|
3476
|
+
return null;
|
|
3477
|
+
}
|
|
3478
|
+
function tryParseContent(trimmed) {
|
|
3479
|
+
const pronounMatch = trimmed.match(
|
|
3480
|
+
/^(its|my|the|this)\s+(.+?)(?:,\s*(big|small))?(?:\s+as\s+(.+?))?(?:\s+with\s+"([^"]+)")?$/
|
|
3481
|
+
);
|
|
3482
|
+
if (pronounMatch) {
|
|
3483
|
+
return {
|
|
3484
|
+
type: "content",
|
|
3485
|
+
pronoun: pronounMatch[1],
|
|
3486
|
+
field: pronounMatch[2].trim(),
|
|
3487
|
+
emphasis: pronounMatch[3],
|
|
3488
|
+
role: pronounMatch[4]?.trim(),
|
|
3489
|
+
label: pronounMatch[5]
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
const labelMatch = trimmed.match(
|
|
3493
|
+
/^([\w\s]+?)\s+as\s+"([^"]+)"(?:\s+with\s+"([^"]+)")?$/
|
|
3494
|
+
);
|
|
3495
|
+
if (labelMatch) {
|
|
3496
|
+
return {
|
|
3497
|
+
type: "content",
|
|
3498
|
+
field: labelMatch[1].trim(),
|
|
3499
|
+
label: labelMatch[2]
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
return null;
|
|
3503
|
+
}
|
|
3504
|
+
function tryParseDataSource(trimmed) {
|
|
3505
|
+
const fromMatch = trimmed.match(
|
|
3506
|
+
/^(.+?)\s+from\s+([\w-]+)(?:,\s*(.+?))?(?:\s+for\s+(.+))?$/
|
|
3507
|
+
);
|
|
3508
|
+
if (!fromMatch) return null;
|
|
3509
|
+
const alias = fromMatch[1].trim();
|
|
3510
|
+
const source = fromMatch[2].trim();
|
|
3511
|
+
const qualifierOrLive = fromMatch[3]?.trim();
|
|
3512
|
+
const scope = fromMatch[4]?.trim();
|
|
3513
|
+
return {
|
|
3514
|
+
type: "data_source",
|
|
3515
|
+
alias,
|
|
3516
|
+
source,
|
|
3517
|
+
isLive: qualifierOrLive === "live",
|
|
3518
|
+
qualifier: qualifierOrLive !== "live" ? qualifierOrLive : void 0,
|
|
3519
|
+
scope
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
function tryParseStateDef(trimmed) {
|
|
3523
|
+
const nonStateStarters = [
|
|
3524
|
+
"a ",
|
|
3525
|
+
"an ",
|
|
3526
|
+
"the ",
|
|
3527
|
+
"my ",
|
|
3528
|
+
"its ",
|
|
3529
|
+
"this ",
|
|
3530
|
+
"each ",
|
|
3531
|
+
"when ",
|
|
3532
|
+
"can ",
|
|
3533
|
+
"set ",
|
|
3534
|
+
"do ",
|
|
3535
|
+
"go ",
|
|
3536
|
+
"tell ",
|
|
3537
|
+
"show ",
|
|
3538
|
+
"search ",
|
|
3539
|
+
"from ",
|
|
3540
|
+
"emit ",
|
|
3541
|
+
"tagged",
|
|
3542
|
+
"starts ",
|
|
3543
|
+
"follow ",
|
|
3544
|
+
"facing ",
|
|
3545
|
+
"playing ",
|
|
3546
|
+
"volume ",
|
|
3547
|
+
"playback "
|
|
3548
|
+
];
|
|
3549
|
+
for (const prefix of nonStateStarters) {
|
|
3550
|
+
if (trimmed.startsWith(prefix)) return null;
|
|
3551
|
+
}
|
|
3552
|
+
if (trimmed.includes("\u2192")) return null;
|
|
3553
|
+
if (trimmed.includes(" from ")) return null;
|
|
3554
|
+
if (trimmed.includes(" by ")) return null;
|
|
3555
|
+
if (trimmed.includes(" as ")) return null;
|
|
3556
|
+
const finalMatch = trimmed.match(/^(.+?),\s*final$/);
|
|
3557
|
+
if (finalMatch) {
|
|
3558
|
+
return { type: "state_decl", name: finalMatch[1].trim(), isFinal: true };
|
|
3559
|
+
}
|
|
3560
|
+
if (/^[a-z][\w\s]*$/i.test(trimmed) && !SECTION_KEYWORDS.has(trimmed) && trimmed !== "pages") {
|
|
3561
|
+
return { type: "state_decl", name: trimmed, isFinal: false };
|
|
3562
|
+
}
|
|
3563
|
+
return null;
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// src/dsl/parser.ts
|
|
3567
|
+
function parse(tokens) {
|
|
3568
|
+
const errors = [];
|
|
3569
|
+
const roots = [];
|
|
3570
|
+
const meaningful = tokens.filter(
|
|
3571
|
+
(t) => t.data.type !== "blank" && t.data.type !== "comment"
|
|
3572
|
+
);
|
|
3573
|
+
if (meaningful.length === 0) {
|
|
3574
|
+
return { nodes: [], errors };
|
|
3575
|
+
}
|
|
3576
|
+
const stack = [];
|
|
3577
|
+
for (const token of meaningful) {
|
|
3578
|
+
const node = { token, children: [] };
|
|
3579
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= token.indent) {
|
|
3580
|
+
stack.pop();
|
|
3581
|
+
}
|
|
3582
|
+
if (stack.length === 0) {
|
|
3583
|
+
roots.push(node);
|
|
3584
|
+
} else {
|
|
3585
|
+
stack[stack.length - 1].node.children.push(node);
|
|
3586
|
+
}
|
|
3587
|
+
stack.push({ node, indent: token.indent });
|
|
3588
|
+
}
|
|
3589
|
+
validateStructure2(roots, errors);
|
|
3590
|
+
return { nodes: roots, errors };
|
|
3591
|
+
}
|
|
3592
|
+
function validateStructure2(nodes, errors) {
|
|
3593
|
+
for (const node of nodes) {
|
|
3594
|
+
const { data } = node.token;
|
|
3595
|
+
if (data.type === "transition" && !isInsideState(node, nodes)) {
|
|
3596
|
+
}
|
|
3597
|
+
if (data.type === "set_action" && !hasAncestorType(node, "when", nodes)) {
|
|
3598
|
+
}
|
|
3599
|
+
if (node.children.length > 0) {
|
|
3600
|
+
validateStructure2(node.children, errors);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
function isInsideState(_node, _roots) {
|
|
3605
|
+
return true;
|
|
3606
|
+
}
|
|
3607
|
+
function hasAncestorType(_node, _type, _roots) {
|
|
3608
|
+
return true;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/dsl/compiler/symbol-table.ts
|
|
3612
|
+
var VIEW_CHILD_TYPES = /* @__PURE__ */ new Set([
|
|
3613
|
+
"data_source",
|
|
3614
|
+
"content",
|
|
3615
|
+
"iteration",
|
|
3616
|
+
"section",
|
|
3617
|
+
"string_literal",
|
|
3618
|
+
"search",
|
|
3619
|
+
"grouping",
|
|
3620
|
+
"navigation",
|
|
3621
|
+
"pages",
|
|
3622
|
+
"qualifier"
|
|
3623
|
+
]);
|
|
3624
|
+
function collectSymbols(nodes) {
|
|
3625
|
+
const table = {
|
|
3626
|
+
things: /* @__PURE__ */ new Map(),
|
|
3627
|
+
fragments: /* @__PURE__ */ new Map(),
|
|
3628
|
+
views: /* @__PURE__ */ new Map()
|
|
3629
|
+
};
|
|
3630
|
+
for (const node of nodes) {
|
|
3631
|
+
const { data } = node.token;
|
|
3632
|
+
switch (data.type) {
|
|
3633
|
+
case "space_decl":
|
|
3634
|
+
table.space = collectSpace(node);
|
|
3635
|
+
break;
|
|
3636
|
+
case "thing_decl":
|
|
3637
|
+
table.things.set(data.name, collectThing(node));
|
|
3638
|
+
break;
|
|
3639
|
+
case "fragment_def":
|
|
3640
|
+
table.fragments.set(data.name, { name: data.name, node });
|
|
3641
|
+
break;
|
|
3642
|
+
case "state_decl":
|
|
3643
|
+
if (hasViewChildren(node)) {
|
|
3644
|
+
table.views.set(data.name, { name: data.name, node });
|
|
3645
|
+
}
|
|
3646
|
+
break;
|
|
3647
|
+
default:
|
|
3648
|
+
break;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
return table;
|
|
3652
|
+
}
|
|
3653
|
+
function collectSpace(node) {
|
|
3654
|
+
const data = node.token.data;
|
|
3655
|
+
if (data.type !== "space_decl") throw new Error("Expected space_decl");
|
|
3656
|
+
const space = {
|
|
3657
|
+
name: data.name,
|
|
3658
|
+
version: data.version,
|
|
3659
|
+
node,
|
|
3660
|
+
thingRefs: [],
|
|
3661
|
+
paths: [],
|
|
3662
|
+
tags: []
|
|
3663
|
+
};
|
|
3664
|
+
for (const child of node.children) {
|
|
3665
|
+
const cd = child.token.data;
|
|
3666
|
+
if (cd.type === "tagged") {
|
|
3667
|
+
space.tags.push(...cd.tags);
|
|
3668
|
+
} else if (cd.type === "section" && cd.name === "things") {
|
|
3669
|
+
for (const ref of child.children) {
|
|
3670
|
+
if (ref.token.data.type === "thing_ref") {
|
|
3671
|
+
space.thingRefs.push({
|
|
3672
|
+
name: ref.token.data.name,
|
|
3673
|
+
kind: ref.token.data.kind
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
} else if (cd.type === "section" && cd.name === "paths") {
|
|
3678
|
+
for (const pm of child.children) {
|
|
3679
|
+
if (pm.token.data.type === "path_mapping") {
|
|
3680
|
+
space.paths.push({
|
|
3681
|
+
path: pm.token.data.path,
|
|
3682
|
+
view: pm.token.data.view,
|
|
3683
|
+
context: pm.token.data.context
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return space;
|
|
3690
|
+
}
|
|
3691
|
+
function collectThing(node) {
|
|
3692
|
+
const data = node.token.data;
|
|
3693
|
+
if (data.type !== "thing_decl") throw new Error("Expected thing_decl");
|
|
3694
|
+
const thing = {
|
|
3695
|
+
name: data.name,
|
|
3696
|
+
version: data.version ?? "1.0.0",
|
|
3697
|
+
node,
|
|
3698
|
+
fields: [],
|
|
3699
|
+
states: [],
|
|
3700
|
+
tags: [],
|
|
3701
|
+
levels: []
|
|
3702
|
+
};
|
|
3703
|
+
for (const child of node.children) {
|
|
3704
|
+
const cd = child.token.data;
|
|
3705
|
+
switch (cd.type) {
|
|
3706
|
+
case "field_def":
|
|
3707
|
+
thing.fields.push(child);
|
|
3708
|
+
break;
|
|
3709
|
+
case "state_decl":
|
|
3710
|
+
thing.states.push(child);
|
|
3711
|
+
break;
|
|
3712
|
+
case "starts_at":
|
|
3713
|
+
thing.startsAt = cd.state;
|
|
3714
|
+
break;
|
|
3715
|
+
case "tagged":
|
|
3716
|
+
thing.tags.push(...cd.tags);
|
|
3717
|
+
break;
|
|
3718
|
+
case "section":
|
|
3719
|
+
if (cd.name === "levels") {
|
|
3720
|
+
for (const lvl of child.children) {
|
|
3721
|
+
if (lvl.token.data.type === "level_def") {
|
|
3722
|
+
thing.levels.push(lvl);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
break;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
return thing;
|
|
3730
|
+
}
|
|
3731
|
+
function hasViewChildren(node) {
|
|
3732
|
+
for (const child of node.children) {
|
|
3733
|
+
if (VIEW_CHILD_TYPES.has(child.token.data.type)) {
|
|
3734
|
+
return true;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
return false;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// src/dsl/compiler/utils.ts
|
|
3741
|
+
function slugify(name) {
|
|
3742
|
+
return name.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
|
|
3743
|
+
}
|
|
3744
|
+
function snakeCase(name) {
|
|
3745
|
+
return name.toLowerCase().trim().replace(/[^a-z0-9\s_]/g, "").replace(/\s+/g, "_").replace(/_+/g, "_");
|
|
3746
|
+
}
|
|
3747
|
+
function generateId(...parts) {
|
|
3748
|
+
return parts.map((p) => slugify(p)).join("--");
|
|
3749
|
+
}
|
|
3750
|
+
function generateActionId(context, index) {
|
|
3751
|
+
return `${context}-${index}`;
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// src/dsl/compiler/field-mapper.ts
|
|
3755
|
+
var BASE_TYPE_MAP = {
|
|
3756
|
+
"text": "text",
|
|
3757
|
+
"rich text": "rich_text",
|
|
3758
|
+
"number": "number",
|
|
3759
|
+
"integer": "number",
|
|
3760
|
+
"time": "datetime"
|
|
3761
|
+
};
|
|
3762
|
+
function isTextLikeType(irType) {
|
|
3763
|
+
return irType === "text" || irType === "rich_text";
|
|
3764
|
+
}
|
|
3765
|
+
function mapField(data) {
|
|
3766
|
+
const field = {
|
|
3767
|
+
name: snakeCase(data.name),
|
|
3768
|
+
type: resolveType(data.baseType)
|
|
3769
|
+
};
|
|
3770
|
+
for (const adj of data.adjectives) {
|
|
3771
|
+
applyAdjective(field, adj);
|
|
3772
|
+
}
|
|
3773
|
+
const validation = buildValidation(data, field.type);
|
|
3774
|
+
if (validation) {
|
|
3775
|
+
field.validation = mergeValidation(field.validation, validation);
|
|
3776
|
+
}
|
|
3777
|
+
if (data.baseType === "integer") {
|
|
3778
|
+
if (!field.validation) field.validation = {};
|
|
3779
|
+
if (!field.validation.rules) field.validation.rules = [];
|
|
3780
|
+
field.validation.rules.push({
|
|
3781
|
+
expression: "eq(round($value), $value)",
|
|
3782
|
+
message: "Must be a whole number",
|
|
3783
|
+
severity: "error"
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
if (data.baseType.startsWith("choice of")) {
|
|
3787
|
+
field.type = "select";
|
|
3788
|
+
const optionsMatch = data.baseType.match(/\[(.+)\]/);
|
|
3789
|
+
if (optionsMatch) {
|
|
3790
|
+
if (!field.validation) field.validation = {};
|
|
3791
|
+
field.validation.options = optionsMatch[1].split(",").map((o) => o.trim());
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
const defaultConstraint = data.constraints.find((c) => c.kind === "default");
|
|
3795
|
+
if (defaultConstraint) {
|
|
3796
|
+
field.default_value = defaultConstraint.value;
|
|
3797
|
+
}
|
|
3798
|
+
return field;
|
|
3799
|
+
}
|
|
3800
|
+
function resolveType(baseType) {
|
|
3801
|
+
if (baseType.startsWith("choice of")) return "select";
|
|
3802
|
+
return BASE_TYPE_MAP[baseType] ?? baseType;
|
|
3803
|
+
}
|
|
3804
|
+
function applyAdjective(field, adjective) {
|
|
3805
|
+
switch (adjective) {
|
|
3806
|
+
case "required":
|
|
3807
|
+
field.required = true;
|
|
3808
|
+
break;
|
|
3809
|
+
case "optional":
|
|
3810
|
+
field.required = false;
|
|
3811
|
+
break;
|
|
3812
|
+
case "non-negative":
|
|
3813
|
+
if (!field.validation) field.validation = {};
|
|
3814
|
+
field.validation.min = 0;
|
|
3815
|
+
break;
|
|
3816
|
+
case "positive":
|
|
3817
|
+
if (!field.validation) field.validation = {};
|
|
3818
|
+
field.validation.min = 1;
|
|
3819
|
+
break;
|
|
3820
|
+
case "computed":
|
|
3821
|
+
field.computed = "";
|
|
3822
|
+
break;
|
|
3823
|
+
// lowercase, uppercase, readonly — stored but no direct IR mapping
|
|
3824
|
+
default:
|
|
3825
|
+
break;
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
function buildValidation(data, irType) {
|
|
3829
|
+
if (data.constraints.length === 0) return null;
|
|
3830
|
+
const validation = {};
|
|
3831
|
+
let hasProps = false;
|
|
3832
|
+
for (const c of data.constraints) {
|
|
3833
|
+
switch (c.kind) {
|
|
3834
|
+
case "max":
|
|
3835
|
+
if (isTextLikeType(irType)) {
|
|
3836
|
+
validation.maxLength = c.value;
|
|
3837
|
+
} else {
|
|
3838
|
+
validation.max = c.value;
|
|
3839
|
+
}
|
|
3840
|
+
hasProps = true;
|
|
3841
|
+
break;
|
|
3842
|
+
case "min":
|
|
3843
|
+
if (isTextLikeType(irType)) {
|
|
3844
|
+
validation.minLength = c.value;
|
|
3845
|
+
} else {
|
|
3846
|
+
validation.min = c.value;
|
|
3847
|
+
}
|
|
3848
|
+
hasProps = true;
|
|
3849
|
+
break;
|
|
3850
|
+
case "between":
|
|
3851
|
+
validation.min = c.value;
|
|
3852
|
+
validation.max = c.value2;
|
|
3853
|
+
hasProps = true;
|
|
3854
|
+
break;
|
|
3855
|
+
case "default":
|
|
3856
|
+
break;
|
|
3857
|
+
case "unique":
|
|
3858
|
+
break;
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return hasProps ? validation : null;
|
|
3862
|
+
}
|
|
3863
|
+
function mergeValidation(existing, incoming) {
|
|
3864
|
+
if (!existing) return incoming;
|
|
3865
|
+
return { ...existing, ...incoming };
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// src/dsl/compiler/workflow-compiler.ts
|
|
3869
|
+
function compileWorkflows(symbols) {
|
|
3870
|
+
const workflows = [];
|
|
3871
|
+
const errors = [];
|
|
3872
|
+
for (const [, thing] of symbols.things) {
|
|
3873
|
+
const { workflow, errors: wfErrors } = compileThing(thing);
|
|
3874
|
+
workflows.push(workflow);
|
|
3875
|
+
errors.push(...wfErrors);
|
|
3876
|
+
}
|
|
3877
|
+
return { workflows, errors };
|
|
3878
|
+
}
|
|
3879
|
+
function compileThing(thing) {
|
|
3880
|
+
const errors = [];
|
|
3881
|
+
const slug = slugify(thing.name);
|
|
3882
|
+
const fieldNames = new Set(
|
|
3883
|
+
thing.fields.map((f) => {
|
|
3884
|
+
const fd = f.token.data;
|
|
3885
|
+
return fd.type === "field_def" ? fd.name : "";
|
|
3886
|
+
}).filter(Boolean)
|
|
3887
|
+
);
|
|
3888
|
+
const fields = compileFields(thing.fields);
|
|
3889
|
+
const stateNames = new Set(
|
|
3890
|
+
thing.states.map((s) => {
|
|
3891
|
+
const sd = s.token.data;
|
|
3892
|
+
return sd.type === "state_decl" ? sd.name : "";
|
|
3893
|
+
}).filter(Boolean)
|
|
3894
|
+
);
|
|
3895
|
+
const states = compileStates(thing, fieldNames);
|
|
3896
|
+
const transitions = compileTransitions(thing, fieldNames, stateNames, errors);
|
|
3897
|
+
const roles = collectRoles(thing);
|
|
3898
|
+
if (!thing.startsAt) {
|
|
3899
|
+
errors.push({
|
|
3900
|
+
code: "MISSING_STARTS_AT",
|
|
3901
|
+
message: `Workflow "${thing.name}" has no starts_at declaration`,
|
|
3902
|
+
lineNumber: thing.node.token.lineNumber,
|
|
3903
|
+
severity: "error"
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
const tags = thing.tags.length > 0 ? thing.tags.map((t) => ({ tag_name: t })) : void 0;
|
|
3907
|
+
let metadata;
|
|
3908
|
+
if (thing.levels.length > 0) {
|
|
3909
|
+
metadata = {
|
|
3910
|
+
levels: thing.levels.map((l) => {
|
|
3911
|
+
const ld = l.token.data;
|
|
3912
|
+
if (ld.type === "level_def") {
|
|
3913
|
+
return { level: ld.level, title: ld.title, fromXp: ld.fromXp };
|
|
3914
|
+
}
|
|
3915
|
+
return null;
|
|
3916
|
+
}).filter(Boolean)
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3919
|
+
return {
|
|
3920
|
+
workflow: {
|
|
3921
|
+
slug,
|
|
3922
|
+
name: thing.name,
|
|
3923
|
+
version: thing.version,
|
|
3924
|
+
category: "blueprint",
|
|
3925
|
+
states,
|
|
3926
|
+
transitions,
|
|
3927
|
+
fields,
|
|
3928
|
+
roles,
|
|
3929
|
+
tags,
|
|
3930
|
+
metadata
|
|
3931
|
+
},
|
|
3932
|
+
errors
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
function compileFields(fieldNodes) {
|
|
3936
|
+
return fieldNodes.map((node) => {
|
|
3937
|
+
const data = node.token.data;
|
|
3938
|
+
if (data.type !== "field_def") return null;
|
|
3939
|
+
return mapField(data);
|
|
3940
|
+
}).filter((f) => f !== null);
|
|
3941
|
+
}
|
|
3942
|
+
function compileStates(thing, fieldNames) {
|
|
3943
|
+
return thing.states.map((stateNode) => {
|
|
3944
|
+
const data = stateNode.token.data;
|
|
3945
|
+
if (data.type !== "state_decl") {
|
|
3946
|
+
return null;
|
|
3947
|
+
}
|
|
3948
|
+
const stateName = data.name;
|
|
3949
|
+
const stateType = resolveStateType(stateName, data.isFinal, thing.startsAt);
|
|
3950
|
+
const onEnterActions = [];
|
|
3951
|
+
const onEventSubs = [];
|
|
3952
|
+
for (const child of stateNode.children) {
|
|
3953
|
+
const cd = child.token.data;
|
|
3954
|
+
if (cd.type === "when") {
|
|
3955
|
+
if (cd.condition === "entered") {
|
|
3956
|
+
const actions = compileWhenEnteredActions(child, stateName, fieldNames);
|
|
3957
|
+
onEnterActions.push(...actions);
|
|
3958
|
+
} else if (cd.condition.startsWith("receives ")) {
|
|
3959
|
+
const sub = compileOnEvent(child, stateName, fieldNames);
|
|
3960
|
+
if (sub) onEventSubs.push(sub);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
const state = {
|
|
3965
|
+
name: stateName,
|
|
3966
|
+
type: stateType,
|
|
3967
|
+
on_enter: onEnterActions,
|
|
3968
|
+
during: [],
|
|
3969
|
+
on_exit: []
|
|
3970
|
+
};
|
|
3971
|
+
if (onEventSubs.length > 0) {
|
|
3972
|
+
state.on_event = onEventSubs;
|
|
3973
|
+
}
|
|
3974
|
+
return state;
|
|
3975
|
+
}).filter((s) => s !== null);
|
|
3976
|
+
}
|
|
3977
|
+
function resolveStateType(name, isFinal, startsAt) {
|
|
3978
|
+
if (startsAt && name === startsAt) return "START";
|
|
3979
|
+
if (isFinal && name.toLowerCase().includes("cancel")) return "CANCELLED";
|
|
3980
|
+
if (isFinal) return "END";
|
|
3981
|
+
return "REGULAR";
|
|
3982
|
+
}
|
|
3983
|
+
function compileWhenEnteredActions(whenNode, stateName, fieldNames) {
|
|
3984
|
+
const context = `${slugify(stateName)}-on-enter`;
|
|
3985
|
+
return whenNode.children.map((child, i) => {
|
|
3986
|
+
const cd = child.token.data;
|
|
3987
|
+
if (cd.type === "set_action") {
|
|
3988
|
+
return {
|
|
3989
|
+
id: generateActionId(context, i),
|
|
3990
|
+
type: "set_field",
|
|
3991
|
+
mode: "auto",
|
|
3992
|
+
config: {
|
|
3993
|
+
field: snakeCase(cd.field),
|
|
3994
|
+
expression: transformExpression(cd.expression, fieldNames)
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
if (cd.type === "do_action") {
|
|
3999
|
+
return {
|
|
4000
|
+
id: generateActionId(context, i),
|
|
4001
|
+
type: cd.action,
|
|
4002
|
+
mode: "auto",
|
|
4003
|
+
config: {}
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
return null;
|
|
4007
|
+
}).filter((a) => a !== null);
|
|
4008
|
+
}
|
|
4009
|
+
function compileOnEvent(whenNode, stateName, fieldNames) {
|
|
4010
|
+
const cd = whenNode.token.data;
|
|
4011
|
+
if (cd.type !== "when") return null;
|
|
4012
|
+
const match = cd.condition.match(/^receives\s+"([^"]+)"\s+from\s+(\S+)$/);
|
|
4013
|
+
if (!match) return null;
|
|
4014
|
+
const eventName = match[1];
|
|
4015
|
+
const sourceSlug = slugify(match[2]);
|
|
4016
|
+
const eventSlug = eventName.replace(/\s+/g, ".");
|
|
4017
|
+
const topicPattern = `*:{{ entity_id }}:${sourceSlug}:instance.${eventSlug}`;
|
|
4018
|
+
const actions = whenNode.children.map((child) => {
|
|
4019
|
+
const acd = child.token.data;
|
|
4020
|
+
if (acd.type === "set_action") {
|
|
4021
|
+
return {
|
|
4022
|
+
type: "set_field",
|
|
4023
|
+
field: snakeCase(acd.field),
|
|
4024
|
+
expression: transformExpression(acd.expression, fieldNames)
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
return null;
|
|
4028
|
+
}).filter((a) => a !== null);
|
|
4029
|
+
return {
|
|
4030
|
+
match: topicPattern,
|
|
4031
|
+
description: `On ${eventName} from ${match[2]}`,
|
|
4032
|
+
actions
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
4035
|
+
function compileTransitions(thing, fieldNames, stateNames, errors) {
|
|
4036
|
+
const transitions = [];
|
|
4037
|
+
for (const stateNode of thing.states) {
|
|
4038
|
+
const stateData = stateNode.token.data;
|
|
4039
|
+
if (stateData.type !== "state_decl") continue;
|
|
4040
|
+
const fromState = stateData.name;
|
|
4041
|
+
for (const child of stateNode.children) {
|
|
4042
|
+
const cd = child.token.data;
|
|
4043
|
+
if (cd.type !== "transition") continue;
|
|
4044
|
+
const isAuto = cd.verb.startsWith("auto ");
|
|
4045
|
+
const transitionName = slugify(cd.verb);
|
|
4046
|
+
if (!stateNames.has(cd.target)) {
|
|
4047
|
+
errors.push({
|
|
4048
|
+
code: "UNKNOWN_TARGET_STATE",
|
|
4049
|
+
message: `Transition "${cd.verb}" targets unknown state "${cd.target}"`,
|
|
4050
|
+
lineNumber: child.token.lineNumber,
|
|
4051
|
+
severity: "warning"
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
let roles;
|
|
4055
|
+
if (cd.guard) {
|
|
4056
|
+
roles = parseGuard(cd.guard);
|
|
4057
|
+
}
|
|
4058
|
+
const conditions = compileTransitionConditions(child, fieldNames);
|
|
4059
|
+
const actions = compileTransitionActions(child, transitionName, fieldNames);
|
|
4060
|
+
const transition = {
|
|
4061
|
+
name: transitionName,
|
|
4062
|
+
from: [fromState],
|
|
4063
|
+
to: cd.target,
|
|
4064
|
+
actions
|
|
4065
|
+
};
|
|
4066
|
+
if (isAuto) transition.auto = true;
|
|
4067
|
+
if (roles && roles.length > 0) transition.roles = roles;
|
|
4068
|
+
if (conditions && conditions.length > 0) transition.conditions = conditions;
|
|
4069
|
+
transitions.push(transition);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
return transitions;
|
|
4073
|
+
}
|
|
4074
|
+
function parseGuard(guard) {
|
|
4075
|
+
const cleaned = guard.replace(/\s+only$/, "");
|
|
4076
|
+
return cleaned.split(/\s+and\s+/).map((r) => r.trim());
|
|
4077
|
+
}
|
|
4078
|
+
function compileTransitionConditions(transitionNode, fieldNames) {
|
|
4079
|
+
const conditions = [];
|
|
4080
|
+
for (const child of transitionNode.children) {
|
|
4081
|
+
const cd = child.token.data;
|
|
4082
|
+
if (cd.type === "when") {
|
|
4083
|
+
const parsed = parseConditionExpression(cd.condition, fieldNames);
|
|
4084
|
+
if (parsed) conditions.push(parsed);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
return conditions.length > 0 ? conditions : void 0;
|
|
4088
|
+
}
|
|
4089
|
+
function compileTransitionActions(transitionNode, transitionName, fieldNames) {
|
|
4090
|
+
const actions = [];
|
|
4091
|
+
const context = `${transitionName}-action`;
|
|
4092
|
+
for (const child of transitionNode.children) {
|
|
4093
|
+
const cd = child.token.data;
|
|
4094
|
+
if (cd.type === "set_action") {
|
|
4095
|
+
actions.push({
|
|
4096
|
+
id: generateActionId(context, actions.length),
|
|
4097
|
+
type: "set_field",
|
|
4098
|
+
mode: "auto",
|
|
4099
|
+
config: {
|
|
4100
|
+
field: snakeCase(cd.field),
|
|
4101
|
+
expression: transformExpression(cd.expression, fieldNames)
|
|
4102
|
+
}
|
|
4103
|
+
});
|
|
4104
|
+
} else if (cd.type === "do_action") {
|
|
4105
|
+
actions.push({
|
|
4106
|
+
id: generateActionId(context, actions.length),
|
|
4107
|
+
type: cd.action,
|
|
4108
|
+
mode: "auto",
|
|
4109
|
+
config: {}
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
return actions;
|
|
4114
|
+
}
|
|
4115
|
+
function parseConditionExpression(condition, fieldNames) {
|
|
4116
|
+
if (condition.includes(" and ")) {
|
|
4117
|
+
const parts = condition.split(/\s+and\s+/);
|
|
4118
|
+
const subConditions = parts.map((p) => parseSingleCondition(p.trim(), fieldNames)).filter((c) => c !== null);
|
|
4119
|
+
if (subConditions.length > 1) {
|
|
4120
|
+
return { AND: subConditions };
|
|
4121
|
+
}
|
|
4122
|
+
if (subConditions.length === 1) {
|
|
4123
|
+
return subConditions[0];
|
|
4124
|
+
}
|
|
4125
|
+
return null;
|
|
4126
|
+
}
|
|
4127
|
+
return parseSingleCondition(condition, fieldNames);
|
|
4128
|
+
}
|
|
4129
|
+
function parseSingleCondition(expr, fieldNames) {
|
|
4130
|
+
const opMatch = expr.match(/^(.+?)\s*(>=|<=|>|<|==|!=)\s*(.+)$/);
|
|
4131
|
+
if (!opMatch) return null;
|
|
4132
|
+
const left = opMatch[1].trim();
|
|
4133
|
+
const op = opMatch[2];
|
|
4134
|
+
const right = opMatch[3].trim();
|
|
4135
|
+
const operatorMap = {
|
|
4136
|
+
">=": "gte",
|
|
4137
|
+
"<=": "lte",
|
|
4138
|
+
">": "gt",
|
|
4139
|
+
"<": "lt",
|
|
4140
|
+
"==": "eq",
|
|
4141
|
+
"!=": "ne"
|
|
4142
|
+
};
|
|
4143
|
+
const operator = operatorMap[op];
|
|
4144
|
+
if (!operator) return null;
|
|
4145
|
+
const leftField = resolveFieldRef(left, fieldNames);
|
|
4146
|
+
const rightValue = resolveConditionValue(right, fieldNames);
|
|
4147
|
+
const condition = {
|
|
4148
|
+
field: leftField,
|
|
4149
|
+
operator
|
|
4150
|
+
};
|
|
4151
|
+
if (typeof rightValue === "number") {
|
|
4152
|
+
condition.value = rightValue;
|
|
4153
|
+
} else {
|
|
4154
|
+
condition.expression = rightValue;
|
|
4155
|
+
}
|
|
4156
|
+
return condition;
|
|
4157
|
+
}
|
|
4158
|
+
function resolveFieldRef(name, fieldNames) {
|
|
4159
|
+
if (fieldNames.has(name)) {
|
|
4160
|
+
return `state_data.${snakeCase(name)}`;
|
|
4161
|
+
}
|
|
4162
|
+
for (const fn of fieldNames) {
|
|
4163
|
+
if (fn === name) return `state_data.${snakeCase(fn)}`;
|
|
4164
|
+
}
|
|
4165
|
+
return `state_data.${snakeCase(name)}`;
|
|
4166
|
+
}
|
|
4167
|
+
function resolveConditionValue(value, fieldNames) {
|
|
4168
|
+
const num = Number(value);
|
|
4169
|
+
if (!isNaN(num)) return num;
|
|
4170
|
+
if (fieldNames.has(value)) {
|
|
4171
|
+
return `state_data.${snakeCase(value)}`;
|
|
4172
|
+
}
|
|
4173
|
+
return `state_data.${snakeCase(value)}`;
|
|
4174
|
+
}
|
|
4175
|
+
function transformExpression(expression, fieldNames) {
|
|
4176
|
+
const trimmed = expression.trim();
|
|
4177
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
4178
|
+
return trimmed;
|
|
4179
|
+
}
|
|
4180
|
+
if (trimmed === "now()") return "now()";
|
|
4181
|
+
const addMatch = trimmed.match(/^(.+?)\s*\+\s*(.+)$/);
|
|
4182
|
+
if (addMatch) {
|
|
4183
|
+
const left = resolveExpressionPart(addMatch[1].trim(), fieldNames);
|
|
4184
|
+
const right = resolveExpressionPart(addMatch[2].trim(), fieldNames);
|
|
4185
|
+
return `add(${left}, ${right})`;
|
|
4186
|
+
}
|
|
4187
|
+
const subMatch = trimmed.match(/^(.+?)\s*-\s*(.+)$/);
|
|
4188
|
+
if (subMatch) {
|
|
4189
|
+
const left = resolveExpressionPart(subMatch[1].trim(), fieldNames);
|
|
4190
|
+
const right = resolveExpressionPart(subMatch[2].trim(), fieldNames);
|
|
4191
|
+
return `subtract(${left}, ${right})`;
|
|
4192
|
+
}
|
|
4193
|
+
return resolveExpressionPart(trimmed, fieldNames);
|
|
4194
|
+
}
|
|
4195
|
+
function resolveExpressionPart(part, fieldNames) {
|
|
4196
|
+
const trimmed = part.trim();
|
|
4197
|
+
const num = Number(trimmed);
|
|
4198
|
+
if (!isNaN(num) && trimmed !== "") return String(num);
|
|
4199
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed;
|
|
4200
|
+
const eventMatch = trimmed.match(/^the event's\s+(.+)$/);
|
|
4201
|
+
if (eventMatch) {
|
|
4202
|
+
return `$event.state_data.${snakeCase(eventMatch[1])}`;
|
|
4203
|
+
}
|
|
4204
|
+
if (trimmed === "now()") return "now()";
|
|
4205
|
+
if (fieldNames.has(trimmed)) {
|
|
4206
|
+
return `state_data.${snakeCase(trimmed)}`;
|
|
4207
|
+
}
|
|
4208
|
+
return `state_data.${snakeCase(trimmed)}`;
|
|
4209
|
+
}
|
|
4210
|
+
function collectRoles(thing) {
|
|
4211
|
+
const roleNames = /* @__PURE__ */ new Set();
|
|
4212
|
+
for (const stateNode of thing.states) {
|
|
4213
|
+
for (const child of stateNode.children) {
|
|
4214
|
+
const cd = child.token.data;
|
|
4215
|
+
if (cd.type === "transition" && cd.guard) {
|
|
4216
|
+
const roles = parseGuard(cd.guard);
|
|
4217
|
+
for (const r of roles) roleNames.add(r);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
return Array.from(roleNames).map((name) => ({
|
|
4222
|
+
name,
|
|
4223
|
+
permissions: [`transition:${name}`]
|
|
4224
|
+
}));
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
// src/dsl/compiler/component-mapper.ts
|
|
4228
|
+
var INSTANCE_FIELDS = /* @__PURE__ */ new Set([
|
|
4229
|
+
"id",
|
|
4230
|
+
"current_state",
|
|
4231
|
+
"state",
|
|
4232
|
+
"status",
|
|
4233
|
+
"created_at",
|
|
4234
|
+
"updated_at",
|
|
4235
|
+
"entity_type",
|
|
4236
|
+
"entity_id"
|
|
4237
|
+
]);
|
|
4238
|
+
function mapContent(data, insideEach, parentId, index) {
|
|
4239
|
+
const fieldSnake = snakeCase(data.field);
|
|
4240
|
+
const nodeId = generateId(parentId, data.field, String(index));
|
|
4241
|
+
const prefix = insideEach ? "$item" : "$instance";
|
|
4242
|
+
const isInstanceField = INSTANCE_FIELDS.has(fieldSnake);
|
|
4243
|
+
const bindingPath = isInstanceField ? `${prefix}.${fieldSnake === "state" ? "current_state" : fieldSnake}` : `${prefix}.state_data.${fieldSnake}`;
|
|
4244
|
+
if (data.role) {
|
|
4245
|
+
return mapContentWithRole(data, nodeId, bindingPath);
|
|
4246
|
+
}
|
|
4247
|
+
const node = {
|
|
4248
|
+
id: nodeId,
|
|
4249
|
+
component: "Text",
|
|
4250
|
+
bindings: { value: bindingPath }
|
|
4251
|
+
};
|
|
4252
|
+
if (data.emphasis === "big") {
|
|
4253
|
+
node.config = { variant: "heading" };
|
|
4254
|
+
} else if (data.emphasis === "small") {
|
|
4255
|
+
node.config = { variant: "caption" };
|
|
4256
|
+
}
|
|
4257
|
+
if (data.label) {
|
|
4258
|
+
if (!node.config) node.config = {};
|
|
4259
|
+
node.config.label = data.label;
|
|
4260
|
+
}
|
|
4261
|
+
return node;
|
|
4262
|
+
}
|
|
4263
|
+
function mapContentWithRole(data, nodeId, bindingPath) {
|
|
4264
|
+
switch (data.role) {
|
|
4265
|
+
case "tag":
|
|
4266
|
+
return {
|
|
4267
|
+
id: nodeId,
|
|
4268
|
+
component: "Badge",
|
|
4269
|
+
bindings: { value: bindingPath }
|
|
4270
|
+
};
|
|
4271
|
+
case "progress":
|
|
4272
|
+
case "meter":
|
|
4273
|
+
return {
|
|
4274
|
+
id: nodeId,
|
|
4275
|
+
component: "ProgressTracker",
|
|
4276
|
+
bindings: { value: bindingPath }
|
|
4277
|
+
};
|
|
4278
|
+
case "image":
|
|
4279
|
+
return {
|
|
4280
|
+
id: nodeId,
|
|
4281
|
+
component: "Image",
|
|
4282
|
+
bindings: { src: bindingPath }
|
|
4283
|
+
};
|
|
4284
|
+
case "number": {
|
|
4285
|
+
const node = {
|
|
4286
|
+
id: nodeId,
|
|
4287
|
+
component: "Text",
|
|
4288
|
+
config: { variant: "metric" },
|
|
4289
|
+
bindings: { value: bindingPath }
|
|
4290
|
+
};
|
|
4291
|
+
if (data.label) {
|
|
4292
|
+
node.config.label = data.label;
|
|
4293
|
+
}
|
|
4294
|
+
return node;
|
|
4295
|
+
}
|
|
4296
|
+
default:
|
|
4297
|
+
return {
|
|
4298
|
+
id: nodeId,
|
|
4299
|
+
component: "Text",
|
|
4300
|
+
bindings: { value: bindingPath }
|
|
4301
|
+
};
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
function mapStringLiteral(data, parentId, index) {
|
|
4305
|
+
const nodeId = generateId(parentId, "text", String(index));
|
|
4306
|
+
const node = {
|
|
4307
|
+
id: nodeId,
|
|
4308
|
+
component: "Text",
|
|
4309
|
+
bindings: { value: `"${data.text}"` }
|
|
4310
|
+
};
|
|
4311
|
+
if (data.emphasis === "big") {
|
|
4312
|
+
node.config = { variant: "heading" };
|
|
4313
|
+
} else if (data.emphasis === "small") {
|
|
4314
|
+
node.config = { variant: "caption" };
|
|
4315
|
+
}
|
|
4316
|
+
return node;
|
|
4317
|
+
}
|
|
4318
|
+
function mapIteration(data, children, parentId, index) {
|
|
4319
|
+
const nodeId = generateId(parentId, "each", data.subject);
|
|
4320
|
+
const eachNode = {
|
|
4321
|
+
id: nodeId,
|
|
4322
|
+
component: "Each"
|
|
4323
|
+
};
|
|
4324
|
+
if (data.role === "card") {
|
|
4325
|
+
const cardNode = {
|
|
4326
|
+
id: generateId(nodeId, "card"),
|
|
4327
|
+
component: "Card",
|
|
4328
|
+
children
|
|
4329
|
+
};
|
|
4330
|
+
if (data.emphasis === "small") {
|
|
4331
|
+
cardNode.config = { size: "small" };
|
|
4332
|
+
}
|
|
4333
|
+
if (children.length > 1) {
|
|
4334
|
+
cardNode.layout = "stack";
|
|
4335
|
+
}
|
|
4336
|
+
eachNode.children = [cardNode];
|
|
4337
|
+
} else {
|
|
4338
|
+
eachNode.children = children;
|
|
4339
|
+
}
|
|
4340
|
+
return eachNode;
|
|
4341
|
+
}
|
|
4342
|
+
function mapSection(data, children, parentId) {
|
|
4343
|
+
switch (data.name) {
|
|
4344
|
+
case "numbers":
|
|
4345
|
+
return {
|
|
4346
|
+
id: generateId(parentId, "numbers"),
|
|
4347
|
+
component: "MetricsGrid",
|
|
4348
|
+
children
|
|
4349
|
+
};
|
|
4350
|
+
case "tabs":
|
|
4351
|
+
return {
|
|
4352
|
+
id: generateId(parentId, "tabs"),
|
|
4353
|
+
component: "TabbedLayout",
|
|
4354
|
+
children
|
|
4355
|
+
};
|
|
4356
|
+
case "actions":
|
|
4357
|
+
return {
|
|
4358
|
+
id: generateId(parentId, "actions"),
|
|
4359
|
+
component: "TransitionActions"
|
|
4360
|
+
};
|
|
4361
|
+
case "controls":
|
|
4362
|
+
return {
|
|
4363
|
+
id: generateId(parentId, "controls"),
|
|
4364
|
+
layout: "row",
|
|
4365
|
+
children
|
|
4366
|
+
};
|
|
4367
|
+
default:
|
|
4368
|
+
return {
|
|
4369
|
+
id: generateId(parentId, data.name),
|
|
4370
|
+
children
|
|
4371
|
+
};
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
function mapSearch(data, parentId, index) {
|
|
4375
|
+
return {
|
|
4376
|
+
id: generateId(parentId, "search", String(index)),
|
|
4377
|
+
component: "SearchInput",
|
|
4378
|
+
config: { target: data.target }
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
function mapNavigation(data, insideEach, parentId, index) {
|
|
4382
|
+
const nodeId = generateId(parentId, "nav", String(index));
|
|
4383
|
+
const prefix = insideEach ? "$item" : "$instance";
|
|
4384
|
+
const target = data.target.replace(
|
|
4385
|
+
/\{its\s+id\}/g,
|
|
4386
|
+
`\${${prefix}.id}`
|
|
4387
|
+
);
|
|
4388
|
+
return {
|
|
4389
|
+
id: nodeId,
|
|
4390
|
+
component: "Link",
|
|
4391
|
+
bindings: { onClick: `$action.navigate("${target}")` },
|
|
4392
|
+
config: { trigger: data.trigger }
|
|
4393
|
+
};
|
|
4394
|
+
}
|
|
4395
|
+
function mapPages(parentId, index) {
|
|
4396
|
+
return {
|
|
4397
|
+
id: generateId(parentId, "pages", String(index)),
|
|
4398
|
+
component: "Pagination"
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
// src/dsl/compiler/view-compiler.ts
|
|
4403
|
+
function compileViews(symbols) {
|
|
4404
|
+
const experiences = [];
|
|
4405
|
+
const errors = [];
|
|
4406
|
+
for (const [, fragment] of symbols.fragments) {
|
|
4407
|
+
const exp = compileViewNode(
|
|
4408
|
+
fragment.name,
|
|
4409
|
+
fragment.node,
|
|
4410
|
+
symbols.space
|
|
4411
|
+
);
|
|
4412
|
+
experiences.push(exp);
|
|
4413
|
+
}
|
|
4414
|
+
for (const [, view] of symbols.views) {
|
|
4415
|
+
const exp = compileViewNode(
|
|
4416
|
+
view.name,
|
|
4417
|
+
view.node,
|
|
4418
|
+
symbols.space
|
|
4419
|
+
);
|
|
4420
|
+
experiences.push(exp);
|
|
4421
|
+
}
|
|
4422
|
+
return { experiences, errors };
|
|
4423
|
+
}
|
|
4424
|
+
function compileViewNode(name, node, space) {
|
|
4425
|
+
const viewSlug = slugify(name);
|
|
4426
|
+
const dataSources = [];
|
|
4427
|
+
const workflowSlugs = [];
|
|
4428
|
+
for (const child of node.children) {
|
|
4429
|
+
if (child.token.data.type === "data_source") {
|
|
4430
|
+
const ds = compileDataSource(child);
|
|
4431
|
+
dataSources.push(ds);
|
|
4432
|
+
if (ds.type === "workflow" && ds.slug) {
|
|
4433
|
+
workflowSlugs.push(ds.slug);
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
const children = compileChildren(node.children, viewSlug, false);
|
|
4438
|
+
const rootNode = {
|
|
4439
|
+
id: viewSlug,
|
|
4440
|
+
children: children.length > 0 ? children : void 0,
|
|
4441
|
+
dataSources: dataSources.length > 0 ? dataSources : void 0
|
|
4442
|
+
};
|
|
4443
|
+
if (children.length > 1) {
|
|
4444
|
+
rootNode.layout = "stack";
|
|
4445
|
+
}
|
|
4446
|
+
return {
|
|
4447
|
+
slug: viewSlug,
|
|
4448
|
+
version: space?.version ?? "1.0.0",
|
|
4449
|
+
name,
|
|
4450
|
+
category: "purpose",
|
|
4451
|
+
view_definition: rootNode,
|
|
4452
|
+
workflows: workflowSlugs,
|
|
4453
|
+
children: [],
|
|
4454
|
+
data_bindings: [],
|
|
4455
|
+
is_default: false
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
function compileDataSource(node) {
|
|
4459
|
+
const data = node.token.data;
|
|
4460
|
+
if (data.type !== "data_source") throw new Error("Expected data_source");
|
|
4461
|
+
const query = resolveQueryType(data.alias);
|
|
4462
|
+
const name = stripPronoun(data.alias);
|
|
4463
|
+
const slug = slugify(data.source);
|
|
4464
|
+
const ds = {
|
|
4465
|
+
type: "workflow",
|
|
4466
|
+
name,
|
|
4467
|
+
slug,
|
|
4468
|
+
query
|
|
4469
|
+
};
|
|
4470
|
+
if (data.scope) {
|
|
4471
|
+
ds.parentInstanceId = "{{ parent_instance_id }}";
|
|
4472
|
+
}
|
|
4473
|
+
for (const child of node.children) {
|
|
4474
|
+
const cd = child.token.data;
|
|
4475
|
+
if (cd.type === "qualifier") {
|
|
4476
|
+
applyQualifier(ds, cd);
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
return ds;
|
|
4480
|
+
}
|
|
4481
|
+
function resolveQueryType(alias) {
|
|
4482
|
+
const firstWord = alias.split(/\s+/)[0];
|
|
4483
|
+
if (firstWord === "this") return "latest";
|
|
4484
|
+
return "list";
|
|
4485
|
+
}
|
|
4486
|
+
function stripPronoun(alias) {
|
|
4487
|
+
const pronouns = ["my", "this", "its", "these"];
|
|
4488
|
+
const words = alias.split(/\s+/);
|
|
4489
|
+
if (words.length > 1 && pronouns.includes(words[0])) {
|
|
4490
|
+
return words.slice(1).join(" ");
|
|
4491
|
+
}
|
|
4492
|
+
return alias;
|
|
4493
|
+
}
|
|
4494
|
+
function applyQualifier(ds, qualifier) {
|
|
4495
|
+
switch (qualifier.kind) {
|
|
4496
|
+
case "order":
|
|
4497
|
+
if (qualifier.value === "newest") {
|
|
4498
|
+
ds.sort = "created_at:desc";
|
|
4499
|
+
} else if (qualifier.value === "oldest") {
|
|
4500
|
+
ds.sort = "created_at:asc";
|
|
4501
|
+
} else {
|
|
4502
|
+
ds.sort = `${snakeCase(qualifier.value)}:desc`;
|
|
4503
|
+
}
|
|
4504
|
+
break;
|
|
4505
|
+
case "pagination":
|
|
4506
|
+
ds.paginated = true;
|
|
4507
|
+
ds.pageSize = parseInt(qualifier.value, 10);
|
|
4508
|
+
break;
|
|
4509
|
+
case "searchable":
|
|
4510
|
+
ds.searchFields = qualifier.value.split(/\s+and\s+/).map((f) => snakeCase(f.trim()));
|
|
4511
|
+
break;
|
|
4512
|
+
case "filterable":
|
|
4513
|
+
ds.facets = qualifier.value.split(/\s+and\s+/).map((f) => snakeCase(f.trim()));
|
|
4514
|
+
break;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
function compileChildren(children, parentId, insideEach) {
|
|
4518
|
+
const nodes = [];
|
|
4519
|
+
let childIndex = 0;
|
|
4520
|
+
for (const child of children) {
|
|
4521
|
+
const cd = child.token.data;
|
|
4522
|
+
switch (cd.type) {
|
|
4523
|
+
case "data_source":
|
|
4524
|
+
break;
|
|
4525
|
+
case "qualifier":
|
|
4526
|
+
break;
|
|
4527
|
+
case "content":
|
|
4528
|
+
nodes.push(mapContent(cd, insideEach, parentId, childIndex++));
|
|
4529
|
+
break;
|
|
4530
|
+
case "string_literal": {
|
|
4531
|
+
const strNode = mapStringLiteral(cd, parentId, childIndex++);
|
|
4532
|
+
if (child.children.length > 0) {
|
|
4533
|
+
strNode.children = compileChildren(child.children, strNode.id, insideEach);
|
|
4534
|
+
if (strNode.children.length > 1) {
|
|
4535
|
+
strNode.layout = "stack";
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
nodes.push(strNode);
|
|
4539
|
+
break;
|
|
4540
|
+
}
|
|
4541
|
+
case "iteration": {
|
|
4542
|
+
const iterChildren = compileChildren(child.children, generateId(parentId, "each", cd.subject), true);
|
|
4543
|
+
nodes.push(mapIteration(cd, iterChildren, parentId, childIndex++));
|
|
4544
|
+
break;
|
|
4545
|
+
}
|
|
4546
|
+
case "section": {
|
|
4547
|
+
const sectionChildren = compileChildren(child.children, generateId(parentId, cd.name), insideEach);
|
|
4548
|
+
nodes.push(mapSection(cd, sectionChildren, parentId));
|
|
4549
|
+
break;
|
|
4550
|
+
}
|
|
4551
|
+
case "search":
|
|
4552
|
+
nodes.push(mapSearch(cd, parentId, childIndex++));
|
|
4553
|
+
break;
|
|
4554
|
+
case "navigation":
|
|
4555
|
+
nodes.push(mapNavigation(cd, insideEach, parentId, childIndex++));
|
|
4556
|
+
break;
|
|
4557
|
+
case "pages":
|
|
4558
|
+
nodes.push(mapPages(parentId, childIndex++));
|
|
4559
|
+
break;
|
|
4560
|
+
case "grouping":
|
|
4561
|
+
nodes.push({
|
|
4562
|
+
id: generateId(parentId, "group", cd.collection),
|
|
4563
|
+
config: {
|
|
4564
|
+
groupBy: snakeCase(cd.key),
|
|
4565
|
+
collection: cd.collection
|
|
4566
|
+
},
|
|
4567
|
+
children: compileChildren(child.children, generateId(parentId, "group", cd.collection), insideEach)
|
|
4568
|
+
});
|
|
4569
|
+
break;
|
|
4570
|
+
default:
|
|
4571
|
+
break;
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
return nodes;
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
// src/dsl/compiler/manifest-compiler.ts
|
|
4578
|
+
function compileManifest(symbols) {
|
|
4579
|
+
const errors = [];
|
|
4580
|
+
if (!symbols.space) {
|
|
4581
|
+
return { manifest: void 0, errors };
|
|
4582
|
+
}
|
|
4583
|
+
const space = symbols.space;
|
|
4584
|
+
const workflows = space.thingRefs.map((ref) => ({
|
|
4585
|
+
slug: slugify(ref.name),
|
|
4586
|
+
role: ref.kind ?? "primary"
|
|
4587
|
+
}));
|
|
4588
|
+
const experience_id = slugify(space.name);
|
|
4589
|
+
const routes = space.paths.map((p) => {
|
|
4590
|
+
const route = {
|
|
4591
|
+
path: p.path,
|
|
4592
|
+
node: slugify(p.view)
|
|
4593
|
+
};
|
|
4594
|
+
if (p.context) {
|
|
4595
|
+
route.entityType = p.context;
|
|
4596
|
+
route.entityIdSource = inferEntityIdSource(p.path, p.context);
|
|
4597
|
+
}
|
|
4598
|
+
return route;
|
|
4599
|
+
});
|
|
4600
|
+
return {
|
|
4601
|
+
manifest: {
|
|
4602
|
+
workflows,
|
|
4603
|
+
experience_id,
|
|
4604
|
+
routes: routes.length > 0 ? routes : void 0
|
|
4605
|
+
},
|
|
4606
|
+
errors
|
|
4607
|
+
};
|
|
4608
|
+
}
|
|
4609
|
+
function inferEntityIdSource(path, context) {
|
|
4610
|
+
if (context === "user") return "user";
|
|
4611
|
+
if (path.includes(":id")) return "param";
|
|
4612
|
+
return "user";
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
// src/dsl/compiler/index.ts
|
|
4616
|
+
function compile(source) {
|
|
4617
|
+
const tokens = tokenize(source);
|
|
4618
|
+
const { nodes, errors: parseErrors } = parse(tokens);
|
|
4619
|
+
return compileAST(nodes, parseErrors);
|
|
4620
|
+
}
|
|
4621
|
+
function compileAST(nodes, parseErrors) {
|
|
4622
|
+
const allErrors = [];
|
|
4623
|
+
const allWarnings = [];
|
|
4624
|
+
if (parseErrors) {
|
|
4625
|
+
for (const pe of parseErrors) {
|
|
4626
|
+
allErrors.push({
|
|
4627
|
+
code: "INVALID_EXPRESSION",
|
|
4628
|
+
message: pe.message,
|
|
4629
|
+
lineNumber: pe.lineNumber,
|
|
4630
|
+
severity: "error"
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
const symbols = collectSymbols(nodes);
|
|
4635
|
+
const { workflows, errors: wfErrors } = compileWorkflows(symbols);
|
|
4636
|
+
for (const e of wfErrors) {
|
|
4637
|
+
if (e.severity === "error") allErrors.push(e);
|
|
4638
|
+
else allWarnings.push(e);
|
|
4639
|
+
}
|
|
4640
|
+
const { experiences, errors: viewErrors } = compileViews(symbols);
|
|
4641
|
+
for (const e of viewErrors) {
|
|
4642
|
+
if (e.severity === "error") allErrors.push(e);
|
|
4643
|
+
else allWarnings.push(e);
|
|
4644
|
+
}
|
|
4645
|
+
const { manifest, errors: mfErrors } = compileManifest(symbols);
|
|
4646
|
+
for (const e of mfErrors) {
|
|
4647
|
+
if (e.severity === "error") allErrors.push(e);
|
|
4648
|
+
else allWarnings.push(e);
|
|
4649
|
+
}
|
|
4650
|
+
return {
|
|
4651
|
+
workflows,
|
|
4652
|
+
experiences,
|
|
4653
|
+
manifest,
|
|
4654
|
+
errors: allErrors,
|
|
4655
|
+
warnings: allWarnings
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
// src/dsl/ir-types.ts
|
|
4660
|
+
function normalizeCategory(primary, ...tags) {
|
|
4661
|
+
const uniqueTags = [...new Set(tags.filter((t) => t !== primary))];
|
|
4662
|
+
uniqueTags.sort();
|
|
4663
|
+
return [primary, ...uniqueTags];
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4666
|
+
// src/dsl/ir-migration.ts
|
|
4667
|
+
var CURRENT_IR_VERSION = "1.2";
|
|
4668
|
+
function detectIRVersion(def) {
|
|
4669
|
+
if (typeof def.ir_version === "string") return def.ir_version;
|
|
4670
|
+
if (def.extensions || def.on_event && Array.isArray(def.on_event)) {
|
|
4671
|
+
return "1.2";
|
|
4672
|
+
}
|
|
4673
|
+
const states = def.states;
|
|
4674
|
+
const fields = def.fields;
|
|
4675
|
+
if (states && states.length > 0 && states[0].state_type) return "1.0";
|
|
4676
|
+
if (fields && fields.length > 0 && fields[0].field_type && !fields[0].type) return "1.0";
|
|
4677
|
+
if (states && states.length > 0 && states[0].type) return "1.1";
|
|
4678
|
+
return "1.0";
|
|
4679
|
+
}
|
|
4680
|
+
function migrateV10ToV11(def) {
|
|
4681
|
+
const result = { ...def };
|
|
4682
|
+
if (Array.isArray(result.states)) {
|
|
4683
|
+
result.states = result.states.map((state) => {
|
|
4684
|
+
const normalized = { ...state };
|
|
4685
|
+
if ("state_type" in normalized && !("type" in normalized)) {
|
|
4686
|
+
normalized.type = normalized.state_type;
|
|
4687
|
+
delete normalized.state_type;
|
|
4688
|
+
}
|
|
4689
|
+
if (Array.isArray(normalized.on_enter)) {
|
|
4690
|
+
normalized.on_enter = normalizeActions(normalized.on_enter);
|
|
4691
|
+
}
|
|
4692
|
+
if (Array.isArray(normalized.on_exit)) {
|
|
4693
|
+
normalized.on_exit = normalizeActions(normalized.on_exit);
|
|
4694
|
+
}
|
|
4695
|
+
if (Array.isArray(normalized.during)) {
|
|
4696
|
+
normalized.during = normalized.during.map((d) => {
|
|
4697
|
+
if (Array.isArray(d.actions)) {
|
|
4698
|
+
return { ...d, actions: normalizeActions(d.actions) };
|
|
4699
|
+
}
|
|
4700
|
+
return d;
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
return normalized;
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
if (Array.isArray(result.fields)) {
|
|
4707
|
+
result.fields = result.fields.map((field) => {
|
|
4708
|
+
const normalized = { ...field };
|
|
4709
|
+
if ("field_type" in normalized && !("type" in normalized)) {
|
|
4710
|
+
normalized.type = normalized.field_type;
|
|
4711
|
+
delete normalized.field_type;
|
|
4712
|
+
}
|
|
4713
|
+
return normalized;
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
if (Array.isArray(result.transitions)) {
|
|
4717
|
+
result.transitions = result.transitions.map((t) => {
|
|
4718
|
+
if (Array.isArray(t.actions)) {
|
|
4719
|
+
return { ...t, actions: normalizeActions(t.actions) };
|
|
4720
|
+
}
|
|
4721
|
+
return t;
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
return result;
|
|
4725
|
+
}
|
|
4726
|
+
function migrateV11ToV12(def) {
|
|
4727
|
+
const result = { ...def };
|
|
4728
|
+
if (!result.extensions) {
|
|
4729
|
+
result.extensions = {};
|
|
4730
|
+
}
|
|
4731
|
+
if (Array.isArray(result.states)) {
|
|
4732
|
+
result.states = result.states.map((state) => ({
|
|
4733
|
+
...state,
|
|
4734
|
+
during: state.during ?? [],
|
|
4735
|
+
on_event: state.on_event ?? []
|
|
4736
|
+
}));
|
|
4737
|
+
}
|
|
4738
|
+
return result;
|
|
4739
|
+
}
|
|
4740
|
+
function normalizeActions(actions) {
|
|
4741
|
+
return actions.map((action) => {
|
|
4742
|
+
const normalized = { ...action };
|
|
4743
|
+
if ("action_type" in normalized && !("type" in normalized)) {
|
|
4744
|
+
normalized.type = normalized.action_type;
|
|
4745
|
+
delete normalized.action_type;
|
|
4746
|
+
}
|
|
4747
|
+
return normalized;
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
var MIGRATIONS = [
|
|
4751
|
+
{ from: "1.0", to: "1.1", fn: migrateV10ToV11 },
|
|
4752
|
+
{ from: "1.1", to: "1.2", fn: migrateV11ToV12 }
|
|
4753
|
+
];
|
|
4754
|
+
function normalizeDefinition(def) {
|
|
4755
|
+
let currentVersion = detectIRVersion(def);
|
|
4756
|
+
let current = def;
|
|
4757
|
+
const applied = [];
|
|
4758
|
+
if (currentVersion === CURRENT_IR_VERSION) {
|
|
4759
|
+
return {
|
|
4760
|
+
definition: current,
|
|
4761
|
+
fromVersion: currentVersion,
|
|
4762
|
+
toVersion: CURRENT_IR_VERSION,
|
|
4763
|
+
migrated: false,
|
|
4764
|
+
appliedMigrations: []
|
|
4765
|
+
};
|
|
4766
|
+
}
|
|
4767
|
+
const fromVersion = currentVersion;
|
|
4768
|
+
for (const migration of MIGRATIONS) {
|
|
4769
|
+
if (currentVersion === migration.from) {
|
|
4770
|
+
current = migration.fn(current);
|
|
4771
|
+
currentVersion = migration.to;
|
|
4772
|
+
applied.push(`${migration.from} \u2192 ${migration.to}`);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
return {
|
|
4776
|
+
definition: current,
|
|
4777
|
+
fromVersion,
|
|
4778
|
+
toVersion: currentVersion,
|
|
4779
|
+
migrated: applied.length > 0,
|
|
4780
|
+
appliedMigrations: applied
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
function needsMigration(def) {
|
|
4784
|
+
return detectIRVersion(def) !== CURRENT_IR_VERSION;
|
|
4785
|
+
}
|
|
4786
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4787
|
+
0 && (module.exports = {
|
|
4788
|
+
ActionDispatcher,
|
|
4789
|
+
CORE_FUNCTIONS,
|
|
4790
|
+
CURRENT_IR_VERSION,
|
|
4791
|
+
EventBus,
|
|
4792
|
+
StateMachine,
|
|
4793
|
+
WEB_FAILURE_POLICIES,
|
|
4794
|
+
analyzeDefinition,
|
|
4795
|
+
buildFunctionMap,
|
|
4796
|
+
clearExpressionCache,
|
|
4797
|
+
clearPatternCache,
|
|
4798
|
+
compareNRT,
|
|
4799
|
+
compile,
|
|
4800
|
+
compilePattern,
|
|
4801
|
+
compileTestProgram,
|
|
4802
|
+
compileTestScenario,
|
|
4803
|
+
countByKind,
|
|
4804
|
+
countNodes,
|
|
4805
|
+
createActionRecorder,
|
|
4806
|
+
createApiTestActions,
|
|
4807
|
+
createEmptyNRT,
|
|
4808
|
+
createEvaluator,
|
|
4809
|
+
createInProcessTestActions,
|
|
4810
|
+
detectIRVersion,
|
|
4811
|
+
findInteractiveNodes,
|
|
4812
|
+
findNode,
|
|
4813
|
+
findVisibleNodes,
|
|
4814
|
+
generateCoverageScenarios,
|
|
4815
|
+
getFinalState,
|
|
4816
|
+
getTransitionPath,
|
|
4817
|
+
hasTransition,
|
|
4818
|
+
isViableDefinition,
|
|
4819
|
+
matchTopic,
|
|
4820
|
+
needsMigration,
|
|
4821
|
+
normalizeCategory,
|
|
4822
|
+
normalizeDefinition,
|
|
4823
|
+
runBlueprintScenario,
|
|
4824
|
+
runBlueprintTestProgram,
|
|
4825
|
+
runScenario,
|
|
4826
|
+
runTestProgram,
|
|
4827
|
+
validateDefinition
|
|
4828
|
+
});
|