@pylonts/dsl 1.1.6 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +4 -0
  2. package/dist/action.d.ts +32 -0
  3. package/dist/action.js +14 -0
  4. package/dist/aggregate.d.ts +38 -0
  5. package/dist/aggregate.js +46 -0
  6. package/dist/business-flow.d.ts +9 -0
  7. package/dist/business-flow.js +72 -0
  8. package/dist/controller.d.ts +17 -9
  9. package/dist/controller.js +8 -2
  10. package/dist/convert.d.ts +28 -10
  11. package/dist/convert.js +16 -5
  12. package/dist/curd.d.ts +7 -10
  13. package/dist/curd.js +3 -1
  14. package/dist/dao.d.ts +81 -53
  15. package/dist/dao.js +291 -12
  16. package/dist/db.d.ts +6 -0
  17. package/dist/db.js +10 -0
  18. package/dist/domain-event.d.ts +48 -0
  19. package/dist/domain-event.js +24 -0
  20. package/dist/dsl.d.ts +17 -2
  21. package/dist/dsl.js +7 -0
  22. package/dist/dto.d.ts +6 -4
  23. package/dist/dto.js +5 -4
  24. package/dist/entity.d.ts +29 -0
  25. package/dist/entity.js +13 -0
  26. package/dist/exception.d.ts +9 -3
  27. package/dist/exception.js +25 -1
  28. package/dist/expr.d.ts +45 -0
  29. package/dist/expr.js +32 -0
  30. package/dist/filter.d.ts +45 -0
  31. package/dist/filter.js +21 -0
  32. package/dist/flow-script.d.ts +108 -0
  33. package/dist/flow-script.js +505 -0
  34. package/dist/flow.d.ts +294 -17
  35. package/dist/flow.js +803 -18
  36. package/dist/index.d.ts +6 -2
  37. package/dist/index.js +6 -2
  38. package/dist/mermaid-driver.js +264 -24
  39. package/dist/mysql-driver.js +3 -0
  40. package/dist/project.d.ts +10 -6
  41. package/dist/project.js +35 -4
  42. package/dist/repository.d.ts +26 -0
  43. package/dist/repository.js +8 -0
  44. package/dist/service.d.ts +14 -2
  45. package/dist/service.js +49 -0
  46. package/dist/third-service.d.ts +5 -0
  47. package/dist/third-service.js +1 -0
  48. package/dist/typebox-driver.js +4 -0
  49. package/dist/utils.d.ts +9 -2
  50. package/dist/utils.js +4 -0
  51. package/docs/aggregate.md +110 -0
  52. package/docs/curd.md +146 -111
  53. package/docs/dao-generation.md +478 -0
  54. package/docs/ddd-principles.md +75 -0
  55. package/docs/domain-event.md +137 -0
  56. package/docs/keyword-matcher.md +182 -0
  57. package/docs/project.md +17 -9
  58. package/docs/token.md +327 -0
  59. package/docs/trans-reentrant.md +85 -0
  60. package/package.json +25 -6
  61. package/src/action.ts +51 -10
  62. package/src/aggregate.ts +104 -0
  63. package/src/business-flow.ts +80 -0
  64. package/src/controller.ts +25 -11
  65. package/src/convert.ts +51 -15
  66. package/src/curd.ts +12 -6
  67. package/src/dao.ts +377 -63
  68. package/src/db.ts +13 -0
  69. package/src/domain-event.ts +74 -0
  70. package/src/dsl.ts +23 -2
  71. package/src/dto.ts +9 -6
  72. package/src/entity.ts +43 -0
  73. package/src/exception.ts +30 -5
  74. package/src/expr.ts +65 -0
  75. package/src/filter.ts +70 -0
  76. package/src/flow-script.ts +696 -0
  77. package/src/flow.ts +1129 -46
  78. package/src/index.ts +6 -2
  79. package/src/mermaid-driver.ts +256 -29
  80. package/src/mysql-driver.ts +3 -0
  81. package/src/project.ts +138 -97
  82. package/src/repository.ts +35 -0
  83. package/src/service.ts +68 -3
  84. package/src/third-service.ts +6 -0
  85. package/src/typebox-driver.ts +4 -0
  86. package/src/utils.ts +13 -2
  87. package/src/endpoint.ts +0 -18
  88. package/src/provider.ts +0 -68
package/dist/flow.js CHANGED
@@ -1,11 +1,189 @@
1
+ import { UnexpectedException } from './exception.js';
2
+ /** Declare a flow's named slots — each bound to the message of a method's
3
+ * args/results (the slot's type). The built-in args slot (the flow input)
4
+ * is added automatically: parameters need no registration. To make its
5
+ * fields accessible (slots.args.amt), declare its input message via the
6
+ * `args` key — that declares the slot's type, not a new slot. */
7
+ export function defineSlots(slots) {
8
+ const wrap = (slot) => {
9
+ const proxy = new Proxy(slot, {
10
+ get(target, prop, receiver) {
11
+ if (typeof prop === 'symbol')
12
+ return Reflect.get(target, prop, receiver);
13
+ // Slot metadata wins over same-named message fields; reflection keys
14
+ // (then/toJSON and Object.prototype members) keep plain object
15
+ // behavior so serialization, promises, and console output never throw.
16
+ if (prop === 'type' ||
17
+ prop === 'description' ||
18
+ prop === 'then' ||
19
+ prop === 'toJSON' ||
20
+ prop in target) {
21
+ return Reflect.get(target, prop, receiver);
22
+ }
23
+ const fields = target.type?.fields;
24
+ if (fields !== undefined && Object.prototype.hasOwnProperty.call(fields, prop)) {
25
+ return { slot: proxy, field: fields[prop] };
26
+ }
27
+ throw new Error(`slot "${target.name}" has no field "${prop}" — its type declares: ${fields ? Object.keys(fields).join(', ') : 'no fields'}`);
28
+ },
29
+ });
30
+ return proxy;
31
+ };
32
+ const registry = Object.create(null);
33
+ for (const key of Object.keys(slots)) {
34
+ if (key === 'args') {
35
+ registry.args = wrap({ name: 'args', type: slots.args, description: 'flow input (flow.args)' });
36
+ warnShadowedFields(registry.args);
37
+ continue;
38
+ }
39
+ registry[key] = wrap({ name: key, type: slots[key] });
40
+ warnShadowedFields(registry[key]);
41
+ }
42
+ if (registry.args === undefined) {
43
+ registry.args = wrap({ name: 'args', description: 'flow input (flow.args)' });
44
+ }
45
+ return registry;
46
+ }
47
+ // A message field named type/name/description is shadowed by slot metadata and
48
+ // unreachable via dot access (slots.args.type reads the message, not the
49
+ // field) — warn instead of failing: the field may never be needed.
50
+ function warnShadowedFields(slot) {
51
+ const fields = slot.type?.fields;
52
+ if (fields === undefined)
53
+ return;
54
+ const shadowed = ['type', 'name', 'description'].filter((k) => k in fields);
55
+ if (shadowed.length > 0) {
56
+ console.warn(`slot "${slot.name}": message fields ${shadowed.join(', ')} are shadowed by slot metadata and unreachable via field access`);
57
+ }
58
+ }
59
+ export function invoke(method, options = {}) {
60
+ return { method, args: options.args, result: options.result };
61
+ }
62
+ export function isCall(m) {
63
+ return 'method' in m;
64
+ }
65
+ /** The underlying method of a (possibly bound) reference. */
66
+ export function methodOf(m) {
67
+ return isCall(m) ? m.method : m;
68
+ }
69
+ function comparison(op, field, value) {
70
+ const ref = field;
71
+ if (typeof ref !== 'object' || ref === null || typeof ref.slot !== 'object' || typeof ref.field !== 'object') {
72
+ throw new Error(`${op}: field must be a slot field access like slots.args.amt`);
73
+ }
74
+ const nullOp = op === 'isNull' || op === 'isNotNull';
75
+ if (nullOp !== (value === undefined)) {
76
+ throw new Error(`${op}: ${nullOp ? 'takes no' : 'requires a'} value`);
77
+ }
78
+ return { kind: 'comparison', op, field: { slot: ref.slot, field: ref.field }, value };
79
+ }
80
+ export function lt(field, value) {
81
+ return comparison('lt', field, value);
82
+ }
83
+ export function le(field, value) {
84
+ return comparison('le', field, value);
85
+ }
86
+ export function gt(field, value) {
87
+ return comparison('gt', field, value);
88
+ }
89
+ export function ge(field, value) {
90
+ return comparison('ge', field, value);
91
+ }
92
+ export function eq(field, value) {
93
+ return comparison('eq', field, value);
94
+ }
95
+ export function ne(field, value) {
96
+ return comparison('ne', field, value);
97
+ }
98
+ export function isNull(field) {
99
+ return comparison('isNull', field);
100
+ }
101
+ export function isNotNull(field) {
102
+ return comparison('isNotNull', field);
103
+ }
104
+ /** An enum value referenced by symbol — the compared-against target of eq/ne
105
+ * (e.g. eq(slots.args.state, enumValue(PayState, 'success'))). */
106
+ export function enumValue(def, symbol) {
107
+ const v = def.values.find((v) => v.symbol === symbol);
108
+ if (v === undefined) {
109
+ throw new Error(`enum ${def.jsName} has no value "${symbol}"`);
110
+ }
111
+ return v;
112
+ }
113
+ /** Decision node: ordered cases (check → target) plus a default branch. */
114
+ export function ifNode(name, options) {
115
+ return {
116
+ type: 'if',
117
+ name,
118
+ description: options.description,
119
+ cases: options.cases,
120
+ else: options.else,
121
+ };
122
+ }
123
+ /** The flow's exception end carrying the given exception name — the implicit
124
+ * target of a guard check throwing it. Undefined when the flow has no such
125
+ * end (guard checks synthesize one at definition time, so this only happens
126
+ * for hand-assembled flows). */
127
+ export function findExceptionEnd(schema, exceptionName) {
128
+ return schema.nodes.find((n) => isEnd(n) && n.type === 'exception' && n.exception?.name === exceptionName);
129
+ }
130
+ /** Where a throw of `exceptionName` from step `n` lands: the target of a typed
131
+ * throws edge (first match), or an untyped exception edge as catch-all.
132
+ * Undefined when no route exists (validation error). Guards route implicitly
133
+ * — see findExceptionEnd.
134
+ * @deprecated no internal use remains; kept for API compatibility. */
135
+ export function resolveThrowTarget(schema, n, exceptionName) {
136
+ for (const e of schema.edges) {
137
+ if (e.start !== n)
138
+ continue;
139
+ if (e.throws && e.throws.name === exceptionName)
140
+ return e.end;
141
+ }
142
+ for (const e of schema.edges) {
143
+ if (e.start === n && e.exception === true && e.throws === undefined)
144
+ return e.end;
145
+ }
146
+ return undefined;
147
+ }
148
+ /** Executor node: an ordered sequence of method invocations (and optionally
149
+ * one event publication). */
1
150
  export function node(name, options = {}) {
2
151
  return {
3
152
  name,
4
153
  flow: options.flow,
5
154
  description: options.description,
6
155
  methods: options.methods,
156
+ publish: options.publish,
157
+ reads: options.reads,
158
+ writes: options.writes,
159
+ };
160
+ }
161
+ /** Guard node: a gate executor — ordered checks, first hit routes out;
162
+ * all misses fall through the guard's normal outgoing edges. */
163
+ export function guard(name, options) {
164
+ return {
165
+ type: 'guard',
166
+ name,
167
+ description: options.description,
168
+ methods: options.methods,
169
+ checks: options.checks,
170
+ };
171
+ }
172
+ /** Try node: a protected region whose body, catch handlers, and optional
173
+ * finally are sub-flows. */
174
+ export function tryNode(name, options) {
175
+ return {
176
+ type: 'try',
177
+ name,
178
+ description: options.description,
179
+ body: options.body,
180
+ catches: options.catches,
181
+ finally: options.finally,
7
182
  };
8
183
  }
184
+ export function defineExceptionEnd(exception, description) {
185
+ return { type: 'exception', name: `throw ${exception.name}`, exception, description };
186
+ }
9
187
  export function edge(start, end, options = {}) {
10
188
  // Auto name for uniformity with SchemaBase; `when` stays the branch marker.
11
189
  return {
@@ -15,45 +193,137 @@ export function edge(start, end, options = {}) {
15
193
  when: options.when,
16
194
  description: options.description,
17
195
  exception: options.exception,
196
+ throws: options.throws,
197
+ check: options.check,
18
198
  };
19
199
  }
20
200
  export function defineFlow(name, schema) {
201
+ const flow = {
202
+ name,
203
+ description: schema.description,
204
+ start: schema.start,
205
+ returnEnd: { type: 'return', name: 'return' },
206
+ nodes: [],
207
+ edges: [],
208
+ args: schema.args,
209
+ results: schema.results,
210
+ slots: schema.slots,
211
+ entrySlots: schema.entrySlots,
212
+ };
213
+ // The input slot's type is the flow's args message: stamp it on first use,
214
+ // and refuse a registry already bound to a different input contract (slot
215
+ // registries belong to one call tree). Same-name messages are the same
216
+ // contract regardless of instance identity.
217
+ if (schema.args !== undefined && schema.slots !== undefined) {
218
+ const t = schema.slots.args.type;
219
+ if (t === undefined) {
220
+ schema.slots.args.type = schema.args;
221
+ warnShadowedFields(schema.slots.args);
222
+ }
223
+ else if (messageName(t) !== schema.args.name) {
224
+ throw new Error(`flow ${name}: the slots args slot carries ${messageName(t)} but the flow declares ${schema.args.name}`);
225
+ }
226
+ }
227
+ const edges = schema.edges(flow);
228
+ flow.edges = edges; // before node collection: guard check targets resolve through it
21
229
  const seen = new Set();
22
230
  const nodes = [];
23
- for (const e of schema.edges) {
24
- for (const n of [e.start, e.end]) {
25
- if (!seen.has(n)) {
26
- seen.add(n);
27
- nodes.push(n);
231
+ const addNode = (n) => {
232
+ if (seen.has(n))
233
+ return;
234
+ seen.add(n);
235
+ nodes.push(n);
236
+ };
237
+ const addGuardTargets = (n) => {
238
+ if (!isGuard(n))
239
+ return;
240
+ for (const c of n.checks) {
241
+ const ex = c.exception;
242
+ if (c.return) {
243
+ addNode(flow.returnEnd);
244
+ }
245
+ else if (ex) {
246
+ // Guard checks exit implicitly: reuse the flow's exception end of the
247
+ // same name (declared by an edge or another guard), else synthesize it.
248
+ let t = nodes.find((x) => isEnd(x) && x.type === 'exception' && x.exception?.name === ex.name);
249
+ if (!t) {
250
+ t = {
251
+ type: 'exception',
252
+ name: `throw ${ex.name}`,
253
+ exception: ex,
254
+ description: 'guard implicit exit',
255
+ };
256
+ }
257
+ addNode(t);
28
258
  }
29
259
  }
260
+ };
261
+ for (const e of edges) {
262
+ addNode(e.start);
263
+ addNode(e.end);
30
264
  }
31
- if (!seen.has(schema.start)) {
32
- seen.add(schema.start);
33
- nodes.push(schema.start);
265
+ addNode(schema.start);
266
+ // Guard targets resolve after every edge endpoint is collected, so an
267
+ // exception end referenced by any edge is reused before synthesis. ifNode
268
+ // targets (cases and else) are collected the same way — an ifNode has no
269
+ // outgoing edges, so its targets enter the flow only here. Targets chain
270
+ // (an ifNode may target another ifNode, or a guard whose implicit exit
271
+ // ends need collecting), so the loop runs over the growing list to a
272
+ // fixpoint instead of a snapshot.
273
+ for (let i = 0; i < nodes.length; i++) {
274
+ const n = nodes[i];
275
+ addGuardTargets(n);
276
+ if (isIfNode(n)) {
277
+ for (const c of n.cases)
278
+ addNode(c.to);
279
+ addNode(n.else);
280
+ }
34
281
  }
35
- const flow = { name, description: schema.description, start: schema.start, nodes, edges: schema.edges };
282
+ flow.nodes = nodes;
36
283
  validate(flow);
37
284
  return flow;
38
285
  }
39
- // Nodes that never appear as an edge start are terminals. Reverse BFS from all
40
- // terminals marks every node that can reach a terminal; unmarked nodes sit on
41
- // a path that never ends (e.g. a cycle without an exit) reject them at
42
- // definition time.
286
+ // Reverse BFS from every FlowEnd marks all nodes that can reach an exit;
287
+ // unmarked nodes sit on a path that never ends (e.g. a cycle without an exit)
288
+ // reject them at definition time. Guard checks count as implicit edges of
289
+ // the guard itself; TryNode internals are validated inside their own flows.
43
290
  function validate(schema) {
44
- const starts = new Set();
45
- for (const e of schema.edges)
46
- starts.add(e.start);
47
291
  const reverse = new Map();
48
292
  for (const n of schema.nodes)
49
293
  reverse.set(n, []);
50
294
  for (const e of schema.edges) {
51
295
  reverse.get(e.end).push(e.start);
52
296
  }
297
+ // guard checks are implicit edges of the guard node itself (return checks
298
+ // to the return end, exception checks to the matching exception end)
299
+ for (const n of schema.nodes) {
300
+ if (!isGuard(n))
301
+ continue;
302
+ for (const c of n.checks) {
303
+ if (c.return) {
304
+ const arr = reverse.get(schema.returnEnd);
305
+ if (arr)
306
+ arr.push(n); // returnEnd is absent when no edge targets it; the guard is then unreachable anyway
307
+ }
308
+ else if (c.exception) {
309
+ const t = findExceptionEnd(schema, c.exception.name);
310
+ if (t)
311
+ reverse.get(t).push(n);
312
+ }
313
+ }
314
+ }
315
+ // ifNode branches are implicit edges of the decision node itself
316
+ for (const n of schema.nodes) {
317
+ if (!isIfNode(n))
318
+ continue;
319
+ for (const c of n.cases)
320
+ reverse.get(c.to).push(n);
321
+ reverse.get(n.else).push(n);
322
+ }
53
323
  const reached = new Set();
54
324
  const queue = [];
55
325
  for (const n of schema.nodes) {
56
- if (!starts.has(n)) {
326
+ if (isEnd(n)) {
57
327
  reached.add(n);
58
328
  queue.push(n);
59
329
  }
@@ -69,7 +339,522 @@ function validate(schema) {
69
339
  }
70
340
  for (const n of schema.nodes) {
71
341
  if (!reached.has(n)) {
72
- throw new Error(`flow ${schema.name}: node "${n.name}" cannot reach a terminal`);
342
+ throw new Error(`flow ${schema.name}: node "${n.name}" cannot reach an end node`);
343
+ }
344
+ }
345
+ // Forward BFS from the start: every node must be reachable, so dead regions
346
+ // cannot silently pollute the escape set (or the catch matching).
347
+ const fromStart = new Set();
348
+ const forward = [schema.start];
349
+ while (forward.length > 0) {
350
+ const cur = forward.shift();
351
+ if (fromStart.has(cur))
352
+ continue;
353
+ fromStart.add(cur);
354
+ for (const e of schema.edges) {
355
+ if (e.start === cur)
356
+ forward.push(e.end);
357
+ }
358
+ if (isGuard(cur)) {
359
+ for (const c of cur.checks) {
360
+ if (c.return) {
361
+ forward.push(schema.returnEnd);
362
+ }
363
+ else if (c.exception) {
364
+ const t = findExceptionEnd(schema, c.exception.name);
365
+ if (t)
366
+ forward.push(t);
367
+ }
368
+ }
369
+ }
370
+ if (isIfNode(cur)) {
371
+ for (const c of cur.cases)
372
+ forward.push(c.to);
373
+ forward.push(cur.else);
374
+ }
375
+ }
376
+ for (const n of schema.nodes) {
377
+ if (!fromStart.has(n)) {
378
+ throw new Error(`flow ${schema.name}: node "${n.name}" is not reachable from the start node`);
73
379
  }
74
380
  }
381
+ validateGuardChecks(schema);
382
+ for (const e of schema.edges) {
383
+ if (e.check !== undefined) {
384
+ validateCondition(`flow ${schema.name}: edge "${e.name}"`, e.check);
385
+ }
386
+ }
387
+ for (const n of schema.nodes) {
388
+ if (isIfNode(n))
389
+ validateIfNode(n, schema);
390
+ }
391
+ validateSlots(schema);
392
+ for (const n of schema.nodes) {
393
+ if (isEnd(n))
394
+ continue;
395
+ validateThrowsCoverage(n, schema);
396
+ if (isTryNode(n))
397
+ validateTryNode(n, schema);
398
+ }
399
+ // Guard check exceptions exit implicitly — a typed throws edge carrying
400
+ // one of the guard's own check exceptions would duplicate the route. Edges
401
+ // carrying other exceptions (or catch-all edges) stay legal: they route
402
+ // throws declared by the guard's methods.
403
+ for (const e of schema.edges) {
404
+ if (!isGuard(e.start))
405
+ continue;
406
+ if (e.throws !== undefined && e.start.checks.some((c) => c.exception?.name === e.throws.name)) {
407
+ throw new Error(`flow ${schema.name}: edge "${e.name}" from guard "${e.start.name}" duplicates the implicit route of check exception ${e.throws.name} — guard checks route implicitly to the matching exception end`);
408
+ }
409
+ }
410
+ // An ifNode owns its exits internally (cases and else) — outgoing edges
411
+ // would be a second way out of the decision.
412
+ for (const e of schema.edges) {
413
+ if (!isIfNode(e.start))
414
+ continue;
415
+ throw new Error(`flow ${schema.name}: edge "${e.name}" from ifNode "${e.start.name}" is redundant — ifNode exits are internal (cases and else)`);
416
+ }
417
+ // Typed throws edges land on an exception end carrying the same exception
418
+ // (escape) — never on a plain step, and no silent renames.
419
+ for (const e of schema.edges) {
420
+ if (e.throws === undefined)
421
+ continue;
422
+ if (!isEnd(e.end) || e.end.type !== 'exception') {
423
+ throw new Error(`flow ${schema.name}: edge "${e.name}" carries throw ${e.throws.name} but its target is not an exception end`);
424
+ }
425
+ if (!e.end.exception) {
426
+ throw new Error(`flow ${schema.name}: edge "${e.name}" targets exception end "${e.end.name}" with no exception type`);
427
+ }
428
+ if (e.end.exception.name !== e.throws.name) {
429
+ throw new Error(`flow ${schema.name}: edge "${e.name}" carries throw ${e.throws.name} but targets the ${e.end.exception.name} exception end`);
430
+ }
431
+ }
432
+ // Untyped catch-all edges also escape — the target must be an exception end.
433
+ for (const e of schema.edges) {
434
+ if (e.exception !== true || e.throws !== undefined)
435
+ continue;
436
+ if (!isEnd(e.end) || e.end.type !== 'exception') {
437
+ throw new Error(`flow ${schema.name}: edge "${e.name}" catch-all exception path must target an exception end`);
438
+ }
439
+ }
440
+ // Plain sub-flows are structure only — exceptions must go through a tryNode.
441
+ for (const n of schema.nodes) {
442
+ if (!isFlowNode(n) || !n.flow)
443
+ continue;
444
+ const ends = exceptionEndNames(n.flow);
445
+ if (ends.size > 0) {
446
+ throw new Error(`flow ${schema.name}: node "${n.name}" sub-flow must not declare exception ends (${[...ends].join(', ')}) — a guard check exception also creates one; use a tryNode for a protected region`);
447
+ }
448
+ }
449
+ }
450
+ // Every guard check must choose exactly one of return / exception; its
451
+ // optional machine-readable condition must be well-formed.
452
+ function validateGuardChecks(schema) {
453
+ for (const n of schema.nodes) {
454
+ if (!isGuard(n))
455
+ continue;
456
+ for (const c of n.checks) {
457
+ const hasReturn = c.return === true;
458
+ const hasException = c.exception !== undefined;
459
+ if (hasReturn === hasException) {
460
+ throw new Error(`flow ${schema.name}: guard "${n.name}" check "${c.when}" must have exactly one of return/exception`);
461
+ }
462
+ if (c.check !== undefined) {
463
+ validateCondition(`flow ${schema.name}: guard "${n.name}" check "${c.when}"`, c.check);
464
+ }
465
+ }
466
+ }
467
+ }
468
+ // A condition must be a utils predicate call (boolean result, no result
469
+ // slot) or a field comparison whose op is known and whose value presence and
470
+ // type match the operator.
471
+ const COMPARE_OPS = ['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'isNull', 'isNotNull'];
472
+ function validateCondition(where, c) {
473
+ if (!isCall(c)) {
474
+ const f = c.field;
475
+ if (typeof f !== 'object' || f === null || typeof f.slot !== 'object' || typeof f.field !== 'object') {
476
+ throw new Error(`${where}: ${c.op} field must be a slot field access like slots.args.amt`);
477
+ }
478
+ if (!COMPARE_OPS.includes(c.op)) {
479
+ throw new Error(`${where}: unknown comparison op ${String(c.op)}`);
480
+ }
481
+ const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
482
+ if (nullOp && c.value !== undefined) {
483
+ throw new Error(`${where}: ${c.op} takes no value`);
484
+ }
485
+ if (!nullOp && c.value === undefined) {
486
+ throw new Error(`${where}: ${c.op} requires a value`);
487
+ }
488
+ const v = c.value;
489
+ if (v !== undefined && typeof v !== 'string' && typeof v !== 'number') {
490
+ const ev = v;
491
+ if (typeof ev !== 'object' || ev === null || ev.value === undefined || ev.symbol === undefined || ev.label === undefined) {
492
+ throw new Error(`${where}: ${c.op} value must be a string, number, or enum value`);
493
+ }
494
+ }
495
+ return;
496
+ }
497
+ const m = c.method;
498
+ if (m.type !== 'utilsMethod') {
499
+ throw new Error(`${where}: check call must be a utils predicate, got ${m.type ?? m.name}`);
500
+ }
501
+ if (c.result !== undefined) {
502
+ throw new Error(`${where}: a predicate call cannot bind a result slot`);
503
+ }
504
+ }
505
+ /** Slots a condition reads: a comparison's field slot or a predicate call's arg slots. */
506
+ function conditionSlots(c) {
507
+ if (!isCall(c))
508
+ return [c.field.slot];
509
+ return c.args ?? [];
510
+ }
511
+ // ifNode rules: at least one case, every condition well-formed, and targets
512
+ // are steps — flow exits (return/throw) belong to guard checks, not to
513
+ // conditional jumps.
514
+ function validateIfNode(n, schema) {
515
+ if (n.cases.length === 0) {
516
+ throw new Error(`flow ${schema.name}: ifNode "${n.name}" must have at least one case`);
517
+ }
518
+ for (const c of n.cases) {
519
+ validateCondition(`flow ${schema.name}: ifNode "${n.name}" case "${c.when}"`, c.check);
520
+ validateIfTarget(schema, n, c.to, `case "${c.when}"`);
521
+ }
522
+ validateIfTarget(schema, n, n.else, 'else');
523
+ }
524
+ function validateIfTarget(schema, n, t, where) {
525
+ if (isEnd(t)) {
526
+ throw new Error(`flow ${schema.name}: ifNode "${n.name}" ${where} must target a step — flow exits (return/throw) belong to guard checks`);
527
+ }
528
+ }
529
+ // Slots: every referenced slot must be declared in this flow's registers
530
+ // (sub-flows analyze their own slots), every declared named slot must be
531
+ // used, and a slot may only be consumed when every path from the start has
532
+ // produced it (must-analysis over the control graph). The built-in args slot
533
+ // is the flow input: it needs no registration and is available on entry.
534
+ // Within one node, calls run in order, so an earlier call's result feeds a
535
+ // later call's args; the node's own writes feed everything after them.
536
+ // Slot/contract type differences convert implicitly and are not validated.
537
+ function validateSlots(schema) {
538
+ const registry = schema.slots;
539
+ const argsSlot = registry?.args;
540
+ const declared = new Set(registry === undefined ? [] : Object.values(registry));
541
+ const used = new Set();
542
+ const produced = new Map();
543
+ for (const n of schema.nodes) {
544
+ const p = new Set();
545
+ if (!isEnd(n) && !isTryNode(n)) {
546
+ if (isIfNode(n)) {
547
+ for (const c of n.cases) {
548
+ for (const t of conditionSlots(c.check))
549
+ used.add(t);
550
+ }
551
+ }
552
+ else {
553
+ for (const ref of n.methods ?? []) {
554
+ if (!isCall(ref))
555
+ continue;
556
+ for (const t of ref.args ?? [])
557
+ used.add(t);
558
+ if (ref.result) {
559
+ used.add(ref.result);
560
+ p.add(ref.result);
561
+ }
562
+ }
563
+ if (isFlowNode(n)) {
564
+ for (const t of n.reads ?? [])
565
+ used.add(t);
566
+ if (n.publish?.payload)
567
+ used.add(n.publish.payload);
568
+ for (const t of n.writes ?? []) {
569
+ used.add(t);
570
+ p.add(t);
571
+ }
572
+ }
573
+ if (isGuard(n)) {
574
+ for (const ch of n.checks) {
575
+ for (const t of ch.reads ?? [])
576
+ used.add(t);
577
+ if (ch.check) {
578
+ for (const t of conditionSlots(ch.check))
579
+ used.add(t);
580
+ }
581
+ }
582
+ }
583
+ }
584
+ }
585
+ produced.set(n, p);
586
+ }
587
+ // a branch edge's condition is decided at its start node
588
+ for (const e of schema.edges) {
589
+ if (!e.check)
590
+ continue;
591
+ for (const t of conditionSlots(e.check))
592
+ used.add(t);
593
+ }
594
+ for (const t of used) {
595
+ if (!declared.has(t)) {
596
+ if (t.name === 'args') {
597
+ throw new Error(`flow ${schema.name}: slot "args" is not declared — declare the flow's slots via defineSlots (its built-in args slot)`);
598
+ }
599
+ throw new Error(`flow ${schema.name}: slot "${t.name}" is not declared in the flow's slots`);
600
+ }
601
+ }
602
+ for (const t of declared) {
603
+ if (t === argsSlot)
604
+ continue; // the input slot needs no use
605
+ if (!used.has(t)) {
606
+ throw new Error(`flow ${schema.name}: slot "${t.name}" is declared but never used`);
607
+ }
608
+ }
609
+ // Fixpoint over the graph: available(n) = intersection over all incoming
610
+ // edges of (available(start) ∪ produced(start)). The input slot is seeded
611
+ // at the start. Sets only grow, so the fixpoint terminates. Incoming edges
612
+ // include the implicit ones (ifNode cases/else), so availability flows
613
+ // through decision nodes the same way reachability does.
614
+ const reverse = new Map();
615
+ for (const n of schema.nodes)
616
+ reverse.set(n, []);
617
+ for (const e of schema.edges)
618
+ reverse.get(e.end).push(e.start);
619
+ for (const n of schema.nodes) {
620
+ if (!isIfNode(n))
621
+ continue;
622
+ for (const c of n.cases)
623
+ reverse.get(c.to).push(n);
624
+ reverse.get(n.else).push(n);
625
+ }
626
+ const avail = new Map();
627
+ for (const n of schema.nodes)
628
+ avail.set(n, new Set());
629
+ const seeded = new Set();
630
+ if (argsSlot)
631
+ seeded.add(argsSlot);
632
+ for (const t of schema.entrySlots ?? []) {
633
+ if (!declared.has(t)) {
634
+ throw new Error(`flow ${schema.name}: entry slot "${t.name}" is not declared in the flow's slots`);
635
+ }
636
+ seeded.add(t);
637
+ }
638
+ // add one by one: this engine's Set.prototype.add takes a single value
639
+ for (const t of seeded)
640
+ avail.get(schema.start).add(t);
641
+ let changed = true;
642
+ while (changed) {
643
+ changed = false;
644
+ for (const n of schema.nodes) {
645
+ if (n === schema.start)
646
+ continue;
647
+ const incoming = reverse.get(n);
648
+ if (incoming.length === 0)
649
+ continue;
650
+ let intersection;
651
+ for (const srcN of incoming) {
652
+ const src = new Set(avail.get(srcN));
653
+ for (const t of produced.get(srcN))
654
+ src.add(t);
655
+ intersection = intersection === undefined ? src : new Set([...intersection].filter((t) => src.has(t)));
656
+ }
657
+ if (intersection === undefined)
658
+ continue;
659
+ for (const t of intersection) {
660
+ if (!avail.get(n).has(t)) {
661
+ avail.get(n).add(t);
662
+ changed = true;
663
+ }
664
+ }
665
+ }
666
+ }
667
+ // Consumption check walks each node's ordered sequence: writes
668
+ // (constructions) first, then calls in order, then branch reads/checks.
669
+ const missing = (n, t) => {
670
+ if (n === schema.start) {
671
+ return new Error(`flow ${schema.name}: start node "${n.name}" cannot consume slot "${t.name}" — nothing else is produced before entry`);
672
+ }
673
+ return new Error(`flow ${schema.name}: node "${n.name}" reads slot "${t.name}" that may not be written on every path`);
674
+ };
675
+ for (const n of schema.nodes) {
676
+ if (isEnd(n) || isTryNode(n))
677
+ continue;
678
+ const inner = new Set(avail.get(n));
679
+ if (isFlowNode(n)) {
680
+ for (const t of n.writes ?? [])
681
+ inner.add(t);
682
+ }
683
+ if (!isIfNode(n)) {
684
+ for (const ref of n.methods ?? []) {
685
+ if (!isCall(ref))
686
+ continue;
687
+ for (const t of ref.args ?? []) {
688
+ if (!inner.has(t))
689
+ throw missing(n, t);
690
+ }
691
+ if (ref.result)
692
+ inner.add(ref.result);
693
+ }
694
+ }
695
+ if (isFlowNode(n)) {
696
+ for (const t of n.reads ?? []) {
697
+ if (!inner.has(t))
698
+ throw missing(n, t);
699
+ }
700
+ if (n.publish?.payload && !inner.has(n.publish.payload))
701
+ throw missing(n, n.publish.payload);
702
+ }
703
+ if (isGuard(n)) {
704
+ for (const ch of n.checks) {
705
+ for (const t of ch.reads ?? []) {
706
+ if (!inner.has(t))
707
+ throw missing(n, t);
708
+ }
709
+ if (ch.check) {
710
+ for (const t of conditionSlots(ch.check)) {
711
+ if (!inner.has(t))
712
+ throw missing(n, t);
713
+ }
714
+ }
715
+ }
716
+ }
717
+ if (isIfNode(n)) {
718
+ for (const c of n.cases) {
719
+ for (const t of conditionSlots(c.check)) {
720
+ if (!inner.has(t))
721
+ throw missing(n, t);
722
+ }
723
+ }
724
+ }
725
+ // branch decisions read their slots at the end of the node
726
+ for (const e of schema.edges) {
727
+ if (e.start !== n || !e.check)
728
+ continue;
729
+ for (const t of conditionSlots(e.check)) {
730
+ if (!inner.has(t))
731
+ throw missing(n, t);
732
+ }
733
+ }
734
+ }
735
+ }
736
+ // Every exception a step declares (methods throws, TryNode rethrows) must be
737
+ // routed: typed throws edges or an untyped exception edge (catch-all). Guard
738
+ // check exceptions route implicitly to the matching exception end and are not
739
+ // covered here. The reverse direction (routed ⊆ declared) is intentionally
740
+ // not validated while steps carry pure descriptors — a MethodSchema has no
741
+ // throws field yet, so typed edges may document throws the descriptor cannot
742
+ // express. Once contract refs replace the descriptors, the reverse check can
743
+ // be enforced.
744
+ function validateThrowsCoverage(n, schema) {
745
+ const declared = new Set();
746
+ if (!isTryNode(n) && !isIfNode(n)) {
747
+ for (const ref of n.methods ?? []) {
748
+ const m = methodOf(ref);
749
+ if ('throws' in m && m.throws) {
750
+ for (const e of m.throws)
751
+ declared.add(e.name);
752
+ }
753
+ }
754
+ }
755
+ if (isTryNode(n)) {
756
+ for (const c of n.catches) {
757
+ for (const name of exceptionEndNames(c.handler))
758
+ declared.add(name);
759
+ }
760
+ }
761
+ if (declared.size === 0)
762
+ return;
763
+ const routed = new Set();
764
+ for (const e of schema.edges) {
765
+ if (e.start !== n)
766
+ continue;
767
+ if (e.throws) {
768
+ routed.add(e.throws.name);
769
+ }
770
+ else if (e.exception === true) {
771
+ return; // untyped exception edge: catch-all
772
+ }
773
+ }
774
+ for (const d of declared) {
775
+ if (!routed.has(d)) {
776
+ throw new Error(`flow ${schema.name}: node "${n.name}" declares throw ${d} but has no typed throws edge for it`);
777
+ }
778
+ }
779
+ }
780
+ // TryNode rules: catches must be unique with UnexpectedException last, every
781
+ // body exception end needs a catch, every catch must match a body exception
782
+ // end (no dead catches), and finally must be pure cleanup (no exception ends).
783
+ // Body end names must be unique so catch matching is unambiguous. Catch
784
+ // matching works on declared end names — reachability of a body end from the
785
+ // body start is not analyzed (a dead region's end would still count).
786
+ function validateTryNode(n, schema) {
787
+ const seen = new Set();
788
+ let catchAll = false;
789
+ for (const c of n.catches) {
790
+ if (seen.has(c.exception.name)) {
791
+ throw new Error(`flow ${schema.name}: tryNode "${n.name}" has duplicate catch ${c.exception.name}`);
792
+ }
793
+ seen.add(c.exception.name);
794
+ if (catchAll) {
795
+ throw new Error(`flow ${schema.name}: tryNode "${n.name}" catch ${c.exception.name} must precede the ${UnexpectedException.name} catch`);
796
+ }
797
+ if (c.exception.name === UnexpectedException.name)
798
+ catchAll = true;
799
+ }
800
+ const bodyEndCounts = new Map();
801
+ for (const node of n.body.nodes) {
802
+ if (isEnd(node) && node.type === 'exception' && node.exception) {
803
+ bodyEndCounts.set(node.exception.name, (bodyEndCounts.get(node.exception.name) ?? 0) + 1);
804
+ }
805
+ }
806
+ for (const [name, count] of bodyEndCounts) {
807
+ if (count > 1) {
808
+ throw new Error(`flow ${schema.name}: tryNode "${n.name}" body has ${count} exception ends named ${name}`);
809
+ }
810
+ }
811
+ const bodyEnds = new Set(bodyEndCounts.keys());
812
+ for (const name of bodyEnds) {
813
+ if (!seen.has(name)) {
814
+ throw new Error(`flow ${schema.name}: tryNode "${n.name}" body exception end ${name} has no catch`);
815
+ }
816
+ }
817
+ for (const c of n.catches) {
818
+ if (!bodyEnds.has(c.exception.name)) {
819
+ throw new Error(`flow ${schema.name}: tryNode "${n.name}" catch ${c.exception.name} matches no body exception end`);
820
+ }
821
+ }
822
+ if (n.finally) {
823
+ const f = exceptionEndNames(n.finally);
824
+ if (f.size > 0) {
825
+ throw new Error(`flow ${schema.name}: tryNode "${n.name}" finally must not declare exception ends: ${[...f].join(', ')}`);
826
+ }
827
+ }
828
+ }
829
+ /** Exception names declared by a flow's exception ends — the flow's escape
830
+ * set (throws that reach the caller unless caught by an outer tryNode). */
831
+ export function exceptionEndNames(flow) {
832
+ const names = new Set();
833
+ for (const n of flow.nodes) {
834
+ if (!isEnd(n) || n.type !== 'exception')
835
+ continue;
836
+ if (!n.exception) {
837
+ throw new Error(`flow ${flow.name}: exception end "${n.name}" has no exception type`);
838
+ }
839
+ names.add(n.exception.name);
840
+ }
841
+ return names;
842
+ }
843
+ export function isEnd(n) {
844
+ return 'type' in n && (n.type === 'return' || n.type === 'exception');
845
+ }
846
+ function messageName(t) {
847
+ return t?.name ?? 'an unnamed message';
848
+ }
849
+ export function isFlowNode(n) {
850
+ return !('type' in n);
851
+ }
852
+ export function isGuard(n) {
853
+ return 'type' in n && n.type === 'guard';
854
+ }
855
+ export function isTryNode(n) {
856
+ return 'type' in n && n.type === 'try';
857
+ }
858
+ export function isIfNode(n) {
859
+ return 'type' in n && n.type === 'if';
75
860
  }