@pylonts/dsl 1.1.12 → 1.1.13
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/convert.d.ts +6 -8
- package/dist/curd.js +1 -1
- package/dist/dao.d.ts +10 -7
- package/dist/dao.js +20 -7
- package/dist/dsl.d.ts +18 -1
- package/dist/dsl.js +40 -0
- package/dist/dto.d.ts +13 -8
- package/dist/dto.js +68 -13
- package/dist/entity.d.ts +4 -3
- package/dist/entity.js +1 -1
- package/dist/filter.d.ts +6 -4
- package/dist/filter.js +1 -1
- package/dist/flow-script.js +8 -2
- package/dist/flow.d.ts +10 -2
- package/dist/flow.js +44 -4
- package/dist/mermaid-driver.js +2 -2
- package/dist/service.d.ts +13 -8
- package/dist/service.js +1 -1
- package/dist/third-service.d.ts +10 -53
- package/dist/third-service.js +3 -78
- package/dist/typebox-driver.d.ts +0 -6
- package/dist/typebox-driver.js +8 -36
- package/dist/utils.d.ts +2 -2
- package/docs/curd.md +146 -146
- package/docs/dao-generation.md +477 -477
- package/docs/project.md +31 -31
- package/docs/token.md +326 -326
- package/package.json +1 -1
- package/src/action.ts +51 -51
- package/src/controller.ts +53 -53
- package/src/convert.ts +76 -78
- package/src/curd.ts +104 -104
- package/src/dao.ts +504 -485
- package/src/dsl.ts +296 -257
- package/src/dto.ts +323 -266
- package/src/entity.ts +43 -42
- package/src/expr.ts +64 -64
- package/src/filter.ts +71 -69
- package/src/flow-script.ts +702 -695
- package/src/flow.ts +1272 -1226
- package/src/index.ts +46 -46
- package/src/mermaid-driver.ts +339 -339
- package/src/mysql-driver.ts +108 -108
- package/src/project.ts +138 -138
- package/src/service.ts +112 -107
- package/src/third-service.ts +68 -191
- package/src/typebox-driver.ts +234 -268
- package/src/utils.ts +74 -74
package/src/flow.ts
CHANGED
|
@@ -1,1227 +1,1273 @@
|
|
|
1
|
-
import type { EnumDef, EnumValue, SchemaBase } from './dsl.js';
|
|
2
|
-
import type { DtoMessage } from './dto.js';
|
|
3
|
-
import type { DomainEventSchema } from './domain-event.js';
|
|
4
|
-
import { UnexpectedException } from './exception.js';
|
|
5
|
-
import type { ExceptionSchema } from './exception.js';
|
|
6
|
-
import type { MethodSchema } from './method.js';
|
|
7
|
-
import type { ConvertMethodSchema } from './convert.js';
|
|
8
|
-
import type { DaoMethodSchema } from './dao.js';
|
|
9
|
-
import type { ServiceMethodSchema } from './service.js';
|
|
10
|
-
import type { ThirdServiceMethodSchema } from './third-service.js';
|
|
11
|
-
import type { UtilsMethodSchema } from './utils.js';
|
|
12
|
-
|
|
13
|
-
// Flow model: nodes are executors, control flow lives on edges. A FlowNode is
|
|
14
|
-
// an ordered sequence of method invocations (a basic block); when a method in
|
|
15
|
-
// the sequence throws, the rest does not run. A GuardNode is a gate executor:
|
|
16
|
-
// its checks are ordered decisions, the first check that holds routes out
|
|
17
|
-
// (return or throw). A TryNode is a protected region: its body, every catch
|
|
18
|
-
// handler, and the optional finally are all sub-flows — the body's exception
|
|
19
|
-
// ends are matched against the catches by name, a handler's exception ends
|
|
20
|
-
// rethrow out of the region, and all fall-through paths continue via the
|
|
21
|
-
// TryNode's outgoing edges.
|
|
22
|
-
//
|
|
23
|
-
// Every flow has a built-in return exit (flow.returnEnd) plus any number of
|
|
24
|
-
// exception exits: explicit defineExceptionEnd nodes (targets of typed throws
|
|
25
|
-
// edges) or ends synthesized from guard checks. A guard exits implicitly — a
|
|
26
|
-
// check returning routes to the return end, a check throwing routes to the
|
|
27
|
-
// flow's exception end carrying the same name; no edges needed. A step's
|
|
28
|
-
// declared exceptions (methods' contracts and TryNode rethrows) must be
|
|
29
|
-
// routed by a typed throws edge or an untyped exception edge (catch-all).
|
|
30
|
-
//
|
|
31
|
-
// Data flows through slots (a compiler-like register file): the built-in args
|
|
32
|
-
// slot carries the flow input and needs no registration, named slots carry
|
|
33
|
-
// derived objects and are declared via defineSlots, and calls bind slots to
|
|
34
|
-
// method args/results (converting implicitly when contracts differ).
|
|
35
|
-
|
|
36
|
-
/** A method a flow step can invoke: contract references, or a pure descriptor
|
|
37
|
-
* (MethodSchema) for the period before the contract file exists.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* args
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
slot
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
prop === '
|
|
97
|
-
prop === '
|
|
98
|
-
prop
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
throw new Error(
|
|
241
|
-
}
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
*
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
exception
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
export
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
export
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
exception?:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
edges
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
description
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const n
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
function
|
|
877
|
-
if (!isCall(c))
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
)
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
//
|
|
1001
|
-
for (const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
//
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (!
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
for (const t of
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1
|
+
import type { EnumDef, EnumValue, SchemaBase } from './dsl.js';
|
|
2
|
+
import type { DtoMessage } from './dto.js';
|
|
3
|
+
import type { DomainEventSchema } from './domain-event.js';
|
|
4
|
+
import { UnexpectedException } from './exception.js';
|
|
5
|
+
import type { ExceptionSchema } from './exception.js';
|
|
6
|
+
import type { MethodSchema } from './method.js';
|
|
7
|
+
import type { ConvertMethodSchema } from './convert.js';
|
|
8
|
+
import type { DaoMethodSchema } from './dao.js';
|
|
9
|
+
import type { ServiceMethodSchema } from './service.js';
|
|
10
|
+
import type { ThirdServiceMethodSchema } from './third-service.js';
|
|
11
|
+
import type { UtilsMethodSchema } from './utils.js';
|
|
12
|
+
|
|
13
|
+
// Flow model: nodes are executors, control flow lives on edges. A FlowNode is
|
|
14
|
+
// an ordered sequence of method invocations (a basic block); when a method in
|
|
15
|
+
// the sequence throws, the rest does not run. A GuardNode is a gate executor:
|
|
16
|
+
// its checks are ordered decisions, the first check that holds routes out
|
|
17
|
+
// (return or throw). A TryNode is a protected region: its body, every catch
|
|
18
|
+
// handler, and the optional finally are all sub-flows — the body's exception
|
|
19
|
+
// ends are matched against the catches by name, a handler's exception ends
|
|
20
|
+
// rethrow out of the region, and all fall-through paths continue via the
|
|
21
|
+
// TryNode's outgoing edges.
|
|
22
|
+
//
|
|
23
|
+
// Every flow has a built-in return exit (flow.returnEnd) plus any number of
|
|
24
|
+
// exception exits: explicit defineExceptionEnd nodes (targets of typed throws
|
|
25
|
+
// edges) or ends synthesized from guard checks. A guard exits implicitly — a
|
|
26
|
+
// check returning routes to the return end, a check throwing routes to the
|
|
27
|
+
// flow's exception end carrying the same name; no edges needed. A step's
|
|
28
|
+
// declared exceptions (methods' contracts and TryNode rethrows) must be
|
|
29
|
+
// routed by a typed throws edge or an untyped exception edge (catch-all).
|
|
30
|
+
//
|
|
31
|
+
// Data flows through slots (a compiler-like register file): the built-in args
|
|
32
|
+
// slot carries the flow input and needs no registration, named slots carry
|
|
33
|
+
// derived objects and are declared via defineSlots, and calls bind slots to
|
|
34
|
+
// method args/results (converting implicitly when contracts differ).
|
|
35
|
+
|
|
36
|
+
/** A method a flow step can invoke: contract references, or a pure descriptor
|
|
37
|
+
* (MethodSchema) for the period before the contract file exists.
|
|
38
|
+
* Third-party methods are their own contract (ThirdServiceMethodSchema) —
|
|
39
|
+
* they can never bind a flow. */
|
|
40
|
+
export type FlowMethodRef =
|
|
41
|
+
| MethodSchema
|
|
42
|
+
| ConvertMethodSchema
|
|
43
|
+
| DaoMethodSchema
|
|
44
|
+
| ServiceMethodSchema
|
|
45
|
+
| ThirdServiceMethodSchema
|
|
46
|
+
| UtilsMethodSchema;
|
|
47
|
+
|
|
48
|
+
/** One named data slot of a flow — a register holding a whole object (like a
|
|
49
|
+
* compiler's register file). The built-in args slot carries the flow input
|
|
50
|
+
* (flow.args) and needs no registration: parameters exist in every flow.
|
|
51
|
+
* Named slots carry derived objects and are declared by binding a method's
|
|
52
|
+
* args/results message. The declared type is documentation — passing a slot
|
|
53
|
+
* to a method with a different contract converts implicitly. Slots are scoped
|
|
54
|
+
* to their flow: they cross into a sub-flow only through the shared input
|
|
55
|
+
* (flow.args) and out of it only through the wrapping node's reads/writes
|
|
56
|
+
* declarations. */
|
|
57
|
+
export interface FlowSlot {
|
|
58
|
+
name: string;
|
|
59
|
+
/** The contract message this slot was declared with (documentation only —
|
|
60
|
+
* call sites convert implicitly). */
|
|
61
|
+
type?: unknown;
|
|
62
|
+
description?: string;
|
|
63
|
+
/** Field access (e.g. slots.args.amt): resolved at access time against the
|
|
64
|
+
* slot's declared message fields, returning a SlotFieldRef for the
|
|
65
|
+
* comparison builders. Unknown fields throw with the declared field list. */
|
|
66
|
+
[field: string]: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A field access on a slot (slots.args.amt): the slot plus the resolved
|
|
70
|
+
* field object from the slot's declared message. */
|
|
71
|
+
export interface SlotFieldRef {
|
|
72
|
+
slot: FlowSlot;
|
|
73
|
+
/** The resolved field (DtoField | Field) — carries its schema for codegen. */
|
|
74
|
+
field: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A flow's slot registers: the built-in args slot plus named slots. */
|
|
78
|
+
export type FlowSlots = { args: FlowSlot } & Record<string, FlowSlot>;
|
|
79
|
+
|
|
80
|
+
/** Declare a flow's named slots — each bound to the message of a method's
|
|
81
|
+
* args/results (the slot's type). The built-in args slot (the flow input)
|
|
82
|
+
* is added automatically: parameters need no registration. To make its
|
|
83
|
+
* fields accessible (slots.args.amt), declare its input message via the
|
|
84
|
+
* `args` key — that declares the slot's type, not a new slot. */
|
|
85
|
+
export function defineSlots<T extends Record<string, unknown>>(
|
|
86
|
+
slots: T,
|
|
87
|
+
): { [K in keyof T]: FlowSlot & { name: K } } & { args: FlowSlot } {
|
|
88
|
+
const wrap = (slot: FlowSlot): FlowSlot => {
|
|
89
|
+
const proxy = new Proxy(slot, {
|
|
90
|
+
get(target, prop, receiver) {
|
|
91
|
+
if (typeof prop === 'symbol') return Reflect.get(target, prop, receiver);
|
|
92
|
+
// Slot metadata wins over same-named message fields; reflection keys
|
|
93
|
+
// (then/toJSON and Object.prototype members) keep plain object
|
|
94
|
+
// behavior so serialization, promises, and console output never throw.
|
|
95
|
+
if (
|
|
96
|
+
prop === 'type' ||
|
|
97
|
+
prop === 'description' ||
|
|
98
|
+
prop === 'then' ||
|
|
99
|
+
prop === 'toJSON' ||
|
|
100
|
+
prop in target
|
|
101
|
+
) {
|
|
102
|
+
return Reflect.get(target, prop, receiver);
|
|
103
|
+
}
|
|
104
|
+
const typeObj = target.type as
|
|
105
|
+
| { fields?: Record<string, unknown>; columns?: Array<{ name: string }> }
|
|
106
|
+
| undefined;
|
|
107
|
+
const fields = slotFields(typeObj);
|
|
108
|
+
if (fields !== undefined && Object.prototype.hasOwnProperty.call(fields, prop)) {
|
|
109
|
+
return { slot: proxy, field: fields[prop] };
|
|
110
|
+
}
|
|
111
|
+
throw new Error(
|
|
112
|
+
`slot "${target.name}" has no field "${prop}" — its type declares: ${
|
|
113
|
+
fields ? Object.keys(fields).join(', ') : 'no fields'
|
|
114
|
+
}`,
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return proxy;
|
|
119
|
+
};
|
|
120
|
+
const registry: Record<string, FlowSlot> = Object.create(null);
|
|
121
|
+
for (const key of Object.keys(slots)) {
|
|
122
|
+
if (key === 'args') {
|
|
123
|
+
registry.args = wrap({ name: 'args', type: slots.args, description: 'flow input (flow.args)' });
|
|
124
|
+
warnShadowedFields(registry.args);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
registry[key] = wrap({ name: key, type: slots[key] });
|
|
128
|
+
warnShadowedFields(registry[key]);
|
|
129
|
+
}
|
|
130
|
+
if (registry.args === undefined) {
|
|
131
|
+
registry.args = wrap({ name: 'args', description: 'flow input (flow.args)' });
|
|
132
|
+
}
|
|
133
|
+
return registry as { [K in keyof T]: FlowSlot & { name: K } } & { args: FlowSlot };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A message field named type/name/description is shadowed by slot metadata and
|
|
137
|
+
// unreachable via dot access (slots.args.type reads the message, not the
|
|
138
|
+
// field) — warn instead of failing: the field may never be needed.
|
|
139
|
+
function warnShadowedFields(slot: FlowSlot): void {
|
|
140
|
+
const fields = slotFields(slot.type);
|
|
141
|
+
if (fields === undefined) return;
|
|
142
|
+
const shadowed = ['type', 'name', 'description'].filter((k) => k in fields);
|
|
143
|
+
if (shadowed.length > 0) {
|
|
144
|
+
console.warn(
|
|
145
|
+
`slot "${slot.name}": message fields ${shadowed.join(', ')} are shadowed by slot metadata and unreachable via field access`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Field map of a slot's declared type: dto/third messages carry `fields`,
|
|
151
|
+
* entity rows carry `columns` (named Fields). */
|
|
152
|
+
function slotFields(type: unknown): Record<string, unknown> | undefined {
|
|
153
|
+
if (typeof type !== 'object' || type === null) return undefined;
|
|
154
|
+
const t = type as { fields?: Record<string, unknown>; columns?: Array<{ name: string }> };
|
|
155
|
+
if (t.fields !== undefined) return t.fields;
|
|
156
|
+
if (t.columns !== undefined) {
|
|
157
|
+
return Object.fromEntries(t.columns.map((c) => [c.name, c]));
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A method call with slot bindings: the arg slots are passed to the method
|
|
163
|
+
* (converted implicitly when the contract differs), the result is assigned
|
|
164
|
+
* to the result slot. A plain method ref (or an invoke with no bindings) is
|
|
165
|
+
* a call with args = [slots.args] and no result — the common case. */
|
|
166
|
+
export interface FlowCall {
|
|
167
|
+
method: FlowMethodRef;
|
|
168
|
+
/** Slots passed as the method's args — the input slot or derived slots.
|
|
169
|
+
* A single-message contract takes the one slot; multiple slots are for
|
|
170
|
+
* multi-param signatures (not modeled yet). */
|
|
171
|
+
args?: FlowSlot[];
|
|
172
|
+
/** Slot assigned from the method's results. */
|
|
173
|
+
result?: FlowSlot;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function invoke(
|
|
177
|
+
method: FlowMethodRef,
|
|
178
|
+
options: { args?: FlowSlot[]; result?: FlowSlot } = {},
|
|
179
|
+
): FlowCall {
|
|
180
|
+
return { method, args: options.args, result: options.result };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A node method: a plain contract reference or a call with slot bindings. */
|
|
184
|
+
export type FlowNodeMethodRef = FlowMethodRef | FlowCall;
|
|
185
|
+
|
|
186
|
+
export function isCall(m: FlowNodeMethodRef | GuardCondition): m is FlowCall {
|
|
187
|
+
return 'method' in m;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The underlying method of a (possibly bound) reference. */
|
|
191
|
+
export function methodOf(m: FlowNodeMethodRef): FlowMethodRef {
|
|
192
|
+
return isCall(m) ? m.method : m;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Comparison operators of a structured condition: ordering, (in)equality
|
|
196
|
+
* against a literal or enum value, and null checks. */
|
|
197
|
+
export type CompareOp = 'lt' | 'le' | 'gt' | 'ge' | 'eq' | 'ne' | 'isNull' | 'isNotNull';
|
|
198
|
+
|
|
199
|
+
/** A field comparison: a slot field (slots.args.amt) against a literal or an
|
|
200
|
+
* enum value — the machine-readable form of a when text. isNull/isNotNull
|
|
201
|
+
* take no value; the others require one. */
|
|
202
|
+
export interface Comparison {
|
|
203
|
+
kind: 'comparison';
|
|
204
|
+
op: CompareOp;
|
|
205
|
+
/** Field-level comparisons bind a slot field; slot-level null checks
|
|
206
|
+
* (isNull/isNotNull on the slot itself) bind the slot directly. */
|
|
207
|
+
field: SlotFieldRef | FlowSlot;
|
|
208
|
+
/** Compared-against value; null checks take none. */
|
|
209
|
+
value?: string | number | EnumValue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The machine-readable condition behind a guard check or a branch edge
|
|
213
|
+
* (optional): a utils predicate call (its boolean result decides) or a field
|
|
214
|
+
* comparison. */
|
|
215
|
+
export type GuardCondition = FlowCall | Comparison;
|
|
216
|
+
|
|
217
|
+
/** True when the condition operand is the slot itself (slot-level null check),
|
|
218
|
+
* not a field access on it. Slot metadata (name) lives on the proxy target and
|
|
219
|
+
* reads without field interception; a field access resolves to { slot, field }. */
|
|
220
|
+
export function isFlowSlot(v: unknown): v is FlowSlot {
|
|
221
|
+
return typeof v === 'object' && v !== null && typeof (v as FlowSlot).name === 'string';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function comparison(op: CompareOp, field: unknown, value?: string | number | EnumValue): Comparison {
|
|
225
|
+
if (isFlowSlot(field)) {
|
|
226
|
+
if (op !== 'isNull' && op !== 'isNotNull') {
|
|
227
|
+
throw new Error(`${op}: a slot-level check only supports isNull/isNotNull — field comparisons need slots.args.amt`);
|
|
228
|
+
}
|
|
229
|
+
if (value !== undefined) {
|
|
230
|
+
throw new Error(`${op}: takes no value`);
|
|
231
|
+
}
|
|
232
|
+
return { kind: 'comparison', op, field, value };
|
|
233
|
+
}
|
|
234
|
+
const ref = field as Partial<SlotFieldRef> | null;
|
|
235
|
+
if (typeof ref !== 'object' || ref === null || typeof ref.slot !== 'object' || typeof ref.field !== 'object') {
|
|
236
|
+
throw new Error(`${op}: field must be a slot field access like slots.args.amt`);
|
|
237
|
+
}
|
|
238
|
+
const nullOp = op === 'isNull' || op === 'isNotNull';
|
|
239
|
+
if (nullOp !== (value === undefined)) {
|
|
240
|
+
throw new Error(`${op}: ${nullOp ? 'takes no' : 'requires a'} value`);
|
|
241
|
+
}
|
|
242
|
+
return { kind: 'comparison', op, field: { slot: ref.slot, field: ref.field }, value };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function lt(field: unknown, value: string | number): Comparison {
|
|
246
|
+
return comparison('lt', field, value);
|
|
247
|
+
}
|
|
248
|
+
export function le(field: unknown, value: string | number): Comparison {
|
|
249
|
+
return comparison('le', field, value);
|
|
250
|
+
}
|
|
251
|
+
export function gt(field: unknown, value: string | number): Comparison {
|
|
252
|
+
return comparison('gt', field, value);
|
|
253
|
+
}
|
|
254
|
+
export function ge(field: unknown, value: string | number): Comparison {
|
|
255
|
+
return comparison('ge', field, value);
|
|
256
|
+
}
|
|
257
|
+
export function eq(field: unknown, value: string | number | EnumValue): Comparison {
|
|
258
|
+
return comparison('eq', field, value);
|
|
259
|
+
}
|
|
260
|
+
export function ne(field: unknown, value: string | number | EnumValue): Comparison {
|
|
261
|
+
return comparison('ne', field, value);
|
|
262
|
+
}
|
|
263
|
+
export function isNull(field: unknown): Comparison {
|
|
264
|
+
return comparison('isNull', field);
|
|
265
|
+
}
|
|
266
|
+
export function isNotNull(field: unknown): Comparison {
|
|
267
|
+
return comparison('isNotNull', field);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** An enum value referenced by symbol — the compared-against target of eq/ne
|
|
271
|
+
* (e.g. eq(slots.args.state, enumValue(PayState, 'success'))). */
|
|
272
|
+
export function enumValue(def: EnumDef, symbol: string): EnumValue {
|
|
273
|
+
const v = def.values.find((v) => v.symbol === symbol);
|
|
274
|
+
if (v === undefined) {
|
|
275
|
+
throw new Error(`enum ${def.jsName} has no value "${symbol}"`);
|
|
276
|
+
}
|
|
277
|
+
return v;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Event publication: a flow node may publish one domain event. The outbox
|
|
281
|
+
* write joins the node's surrounding transaction — the event is durable with
|
|
282
|
+
* the node's other side effects. The payload slot defaults to the flow input
|
|
283
|
+
* (slots.args) when omitted. */
|
|
284
|
+
export interface FlowPublish {
|
|
285
|
+
event: DomainEventSchema;
|
|
286
|
+
/** The slot carrying the event payload. */
|
|
287
|
+
payload?: FlowSlot;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface FlowNode extends SchemaBase {
|
|
291
|
+
/** Optional sub-flow. When present, entering this node runs the sub-flow;
|
|
292
|
+
* after the sub-flow reaches any terminal node, the outer flow continues
|
|
293
|
+
* via this node's outgoing edges. Sub-flows nest recursively. A plain
|
|
294
|
+
* sub-flow may not declare exception ends — it is structure only; use a
|
|
295
|
+
* tryNode for a protected region. */
|
|
296
|
+
flow?: FlowSchema;
|
|
297
|
+
/** Ordered execution sequence (basic block): methods run in order; if one
|
|
298
|
+
* throws, the remaining methods are skipped. A plain reference is a call
|
|
299
|
+
* receiving the input slot (slots.args) — the common case. */
|
|
300
|
+
methods?: FlowNodeMethodRef[];
|
|
301
|
+
/** A domain event published by this node (same transaction as its methods). */
|
|
302
|
+
publish?: FlowPublish;
|
|
303
|
+
/** Slots this node reads to decide its outgoing branches — a semantic
|
|
304
|
+
* declaration, not enforced per branch. */
|
|
305
|
+
reads?: FlowSlot[];
|
|
306
|
+
/** Slots this node assigns without a method call (constructions). */
|
|
307
|
+
writes?: FlowSlot[];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Plain FlowNodes carry no `type` field — the discriminant against
|
|
311
|
+
// GuardNode/TryNode/FlowEnd is its absence (see the is* predicates).
|
|
312
|
+
|
|
313
|
+
/** One gate check: when the condition holds, either return (early return to
|
|
314
|
+
* the flow's returnEnd) or throw the given exception (routed implicitly to
|
|
315
|
+
* the flow's exception end carrying the same name). Exactly one of
|
|
316
|
+
* return/exception must be set. */
|
|
317
|
+
export interface GuardCheck {
|
|
318
|
+
/** Condition description shown on the branch edge. */
|
|
319
|
+
when: string;
|
|
320
|
+
/** The check returns early (to the flow's returnEnd) — no edge needed. */
|
|
321
|
+
return?: boolean;
|
|
322
|
+
/** The check throws this exception — routed implicitly to the flow's
|
|
323
|
+
* exception end carrying the same name (no edge needed). */
|
|
324
|
+
exception?: ExceptionSchema;
|
|
325
|
+
/** Slots the check reads to decide. */
|
|
326
|
+
reads?: FlowSlot[];
|
|
327
|
+
/** Machine-readable condition behind `when` (optional): a utils predicate
|
|
328
|
+
* call or a field comparison. Omitted when the condition stays descriptive. */
|
|
329
|
+
check?: GuardCondition;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Gate executor: an ordered set of checks. The first check whose condition
|
|
333
|
+
* holds routes out implicitly (return to the flow's return end, throw to the
|
|
334
|
+
* matching exception end); when none holds, the guard falls through its
|
|
335
|
+
* normal outgoing edges (linked with edge like any node). A guard is the
|
|
336
|
+
* graph form of a run of `if (bad) throw / return` guards. */
|
|
337
|
+
export interface GuardNode extends SchemaBase {
|
|
338
|
+
type: 'guard';
|
|
339
|
+
/** Ordered decision sequence: methods run in order; if one throws, the
|
|
340
|
+
* remaining methods are skipped. */
|
|
341
|
+
methods?: FlowNodeMethodRef[];
|
|
342
|
+
/** Ordered checks — first hit wins. */
|
|
343
|
+
checks: GuardCheck[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** One catch route of a TryNode: body exception type → handler sub-flow.
|
|
347
|
+
* First match wins, UnexpectedException is the catch-all and must come
|
|
348
|
+
* last. The handler's exception ends rethrow out of the TryNode. */
|
|
349
|
+
export interface TryCatch {
|
|
350
|
+
/** Exception this catch handles — must match a body exception end. */
|
|
351
|
+
exception: ExceptionSchema;
|
|
352
|
+
/** Handler sub-flow: its return end falls through, its exception ends
|
|
353
|
+
* rethrow (declared as the TryNode's own throws). */
|
|
354
|
+
handler: FlowSchema;
|
|
355
|
+
description?: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Protected region: body, catch handlers, and optional finally are all
|
|
359
|
+
* sub-flows. Entering the TryNode runs the body; the body's exception ends
|
|
360
|
+
* are matched against the catches, every catch handler and the body's normal
|
|
361
|
+
* completion pass through finally (when present), then continue via the
|
|
362
|
+
* TryNode's outgoing edges. Handler exception ends rethrow out of the region. */
|
|
363
|
+
export interface TryNode extends SchemaBase {
|
|
364
|
+
type: 'try';
|
|
365
|
+
/** The protected region. Its exception ends are the region's throw sites. */
|
|
366
|
+
body: FlowSchema;
|
|
367
|
+
/** Ordered catch routes; UnexpectedException (catch-all) must be last. */
|
|
368
|
+
catches: TryCatch[];
|
|
369
|
+
/** Cleanup sub-flow: may not declare exception ends (Java finally rules). */
|
|
370
|
+
finally?: FlowSchema;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** One branch of an ifNode: when the check holds, control moves to `to`. */
|
|
374
|
+
export interface IfCase {
|
|
375
|
+
/** Branch label (rendered on the branch line). */
|
|
376
|
+
when: string;
|
|
377
|
+
/** Machine-readable condition — required (the ifNode's whole purpose). */
|
|
378
|
+
check: GuardCondition;
|
|
379
|
+
/** Jump target: a step (node/guard/tryNode/ifNode). Flow exits — return
|
|
380
|
+
* and throw — belong to guard checks, so ends are rejected. */
|
|
381
|
+
to: FlowNodeOrEnd;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Decision node — the jump twin of a guard: ordered cases evaluated
|
|
385
|
+
* first-hit, the first check that holds routes to its target; when none
|
|
386
|
+
* holds, control goes to `else`. All exits are internal (cases and else),
|
|
387
|
+
* so the node has no outgoing edges. A guard decides exits (return/throw),
|
|
388
|
+
* an ifNode decides where control goes next (steps only). */
|
|
389
|
+
export interface IfNode extends SchemaBase {
|
|
390
|
+
type: 'if';
|
|
391
|
+
/** Ordered branches — first hit wins. */
|
|
392
|
+
cases: IfCase[];
|
|
393
|
+
/** Default branch: a step (never a flow exit). */
|
|
394
|
+
else: FlowNodeOrEnd;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Decision node: ordered cases (check → target) plus a default branch. */
|
|
398
|
+
export function ifNode(
|
|
399
|
+
name: string,
|
|
400
|
+
options: {
|
|
401
|
+
cases: IfCase[];
|
|
402
|
+
else: FlowNodeOrEnd;
|
|
403
|
+
description?: string;
|
|
404
|
+
},
|
|
405
|
+
): IfNode {
|
|
406
|
+
return {
|
|
407
|
+
type: 'if',
|
|
408
|
+
name,
|
|
409
|
+
description: options.description,
|
|
410
|
+
cases: options.cases,
|
|
411
|
+
else: options.else,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Any node an edge can start from. */
|
|
416
|
+
export type FlowStep = FlowNode | GuardNode | TryNode | IfNode;
|
|
417
|
+
|
|
418
|
+
/** Any node an edge can point at: a step or a flow exit. */
|
|
419
|
+
export type FlowNodeOrEnd = FlowStep | FlowEnd;
|
|
420
|
+
|
|
421
|
+
/** A flow exit: normal return or uncaught exception. Never an edge start. */
|
|
422
|
+
export interface FlowEnd extends SchemaBase {
|
|
423
|
+
type: 'return' | 'exception';
|
|
424
|
+
/** Uncaught exception this exit throws — set when type='exception'. */
|
|
425
|
+
exception?: ExceptionSchema;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The flow's exception end carrying the given exception name — the implicit
|
|
429
|
+
* target of a guard check throwing it. Undefined when the flow has no such
|
|
430
|
+
* end (guard checks synthesize one at definition time, so this only happens
|
|
431
|
+
* for hand-assembled flows). */
|
|
432
|
+
export function findExceptionEnd(schema: FlowSchema, exceptionName: string): FlowEnd | undefined {
|
|
433
|
+
return schema.nodes.find(
|
|
434
|
+
(n): n is FlowEnd => isEnd(n) && n.type === 'exception' && n.exception?.name === exceptionName,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Where a throw of `exceptionName` from step `n` lands: the target of a typed
|
|
439
|
+
* throws edge (first match), or an untyped exception edge as catch-all.
|
|
440
|
+
* Undefined when no route exists (validation error). Guards route implicitly
|
|
441
|
+
* — see findExceptionEnd.
|
|
442
|
+
* @deprecated no internal use remains; kept for API compatibility. */
|
|
443
|
+
export function resolveThrowTarget(schema: FlowSchema, n: FlowStep, exceptionName: string): FlowNodeOrEnd | undefined {
|
|
444
|
+
for (const e of schema.edges) {
|
|
445
|
+
if (e.start !== n) continue;
|
|
446
|
+
if (e.throws && e.throws.name === exceptionName) return e.end;
|
|
447
|
+
}
|
|
448
|
+
for (const e of schema.edges) {
|
|
449
|
+
if (e.start === n && e.exception === true && e.throws === undefined) return e.end;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export interface FlowEdge extends SchemaBase {
|
|
455
|
+
/** Trigger condition; undefined = default path (success/normal). */
|
|
456
|
+
when?: string;
|
|
457
|
+
start: FlowStep;
|
|
458
|
+
end: FlowNodeOrEnd;
|
|
459
|
+
/** Exception path (rendered dashed); without `throws` it is a catch-all. */
|
|
460
|
+
exception?: boolean;
|
|
461
|
+
/** Typed exception propagation: this edge carries the named exception
|
|
462
|
+
* (rendered dashed with a `throw X` label). Must target an exception end.
|
|
463
|
+
* Carrying one of a guard's own check exceptions is rejected — guards
|
|
464
|
+
* route those implicitly. */
|
|
465
|
+
throws?: ExceptionSchema;
|
|
466
|
+
/** Machine-readable condition behind `when` (optional): a utils predicate
|
|
467
|
+
* call or a field comparison. */
|
|
468
|
+
check?: GuardCondition;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export interface FlowSchema extends SchemaBase {
|
|
472
|
+
/** Entry node. */
|
|
473
|
+
start: FlowStep;
|
|
474
|
+
/** Default exit — every flow has one; edge(node, flow.returnEnd) is a return. */
|
|
475
|
+
returnEnd: FlowEnd;
|
|
476
|
+
/** All nodes, collected from edges (deduplicated by object identity). */
|
|
477
|
+
nodes: FlowNodeOrEnd[];
|
|
478
|
+
/** Independent edges; a node may be start of many edges, so cycles are expressible. */
|
|
479
|
+
edges: FlowEdge[];
|
|
480
|
+
/** Input contract — mirrors a service method's args. */
|
|
481
|
+
args?: DtoMessage;
|
|
482
|
+
/** Output contract — mirrors a service method's results. */
|
|
483
|
+
results?: DtoMessage;
|
|
484
|
+
/** Declared slot registers of this flow (named slots + the built-in args
|
|
485
|
+
* slot). Sub-flows analyze their own slots; sharing happens through the
|
|
486
|
+
* same input object (args) or the wrapping node's reads/writes. */
|
|
487
|
+
slots?: FlowSlots;
|
|
488
|
+
/** Slots the enclosing flow has already produced before this sub-flow's
|
|
489
|
+
* entry (input inheritance). Only meaningful for sub-flows (try bodies,
|
|
490
|
+
* catch handlers, named sub-flows) — a top-level flow has none. */
|
|
491
|
+
entrySlots?: FlowSlot[];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Executor node: an ordered sequence of method invocations (and optionally
|
|
495
|
+
* one event publication). */
|
|
496
|
+
export function node(
|
|
497
|
+
name: string,
|
|
498
|
+
options: {
|
|
499
|
+
flow?: FlowSchema;
|
|
500
|
+
description?: string;
|
|
501
|
+
methods?: FlowNodeMethodRef[];
|
|
502
|
+
publish?: FlowPublish;
|
|
503
|
+
reads?: FlowSlot[];
|
|
504
|
+
writes?: FlowSlot[];
|
|
505
|
+
} = {},
|
|
506
|
+
): FlowNode {
|
|
507
|
+
return {
|
|
508
|
+
name,
|
|
509
|
+
flow: options.flow,
|
|
510
|
+
description: options.description,
|
|
511
|
+
methods: options.methods,
|
|
512
|
+
publish: options.publish,
|
|
513
|
+
reads: options.reads,
|
|
514
|
+
writes: options.writes,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Guard node: a gate executor — ordered checks, first hit routes out;
|
|
519
|
+
* all misses fall through the guard's normal outgoing edges. */
|
|
520
|
+
export function guard(
|
|
521
|
+
name: string,
|
|
522
|
+
options: {
|
|
523
|
+
checks: GuardCheck[];
|
|
524
|
+
methods?: FlowNodeMethodRef[];
|
|
525
|
+
description?: string;
|
|
526
|
+
},
|
|
527
|
+
): GuardNode {
|
|
528
|
+
return {
|
|
529
|
+
type: 'guard',
|
|
530
|
+
name,
|
|
531
|
+
description: options.description,
|
|
532
|
+
methods: options.methods,
|
|
533
|
+
checks: options.checks,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Try node: a protected region whose body, catch handlers, and optional
|
|
538
|
+
* finally are sub-flows. */
|
|
539
|
+
export function tryNode(
|
|
540
|
+
name: string,
|
|
541
|
+
options: {
|
|
542
|
+
body: FlowSchema;
|
|
543
|
+
catches: TryCatch[];
|
|
544
|
+
finally?: FlowSchema;
|
|
545
|
+
description?: string;
|
|
546
|
+
},
|
|
547
|
+
): TryNode {
|
|
548
|
+
return {
|
|
549
|
+
type: 'try',
|
|
550
|
+
name,
|
|
551
|
+
description: options.description,
|
|
552
|
+
body: options.body,
|
|
553
|
+
catches: options.catches,
|
|
554
|
+
finally: options.finally,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function defineExceptionEnd(exception: ExceptionSchema, description?: string): FlowEnd {
|
|
559
|
+
return { type: 'exception', name: `throw ${exception.name}`, exception, description };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function edge(
|
|
563
|
+
start: FlowStep,
|
|
564
|
+
end: FlowNodeOrEnd,
|
|
565
|
+
options: {
|
|
566
|
+
when?: string;
|
|
567
|
+
description?: string;
|
|
568
|
+
exception?: boolean;
|
|
569
|
+
throws?: ExceptionSchema;
|
|
570
|
+
check?: GuardCondition;
|
|
571
|
+
} = {},
|
|
572
|
+
): FlowEdge {
|
|
573
|
+
// Auto name for uniformity with SchemaBase; `when` stays the branch marker.
|
|
574
|
+
return {
|
|
575
|
+
name: `${start.name}->${end.name}`,
|
|
576
|
+
start,
|
|
577
|
+
end,
|
|
578
|
+
when: options.when,
|
|
579
|
+
description: options.description,
|
|
580
|
+
exception: options.exception,
|
|
581
|
+
throws: options.throws,
|
|
582
|
+
check: options.check,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function defineFlow(
|
|
587
|
+
name: string,
|
|
588
|
+
schema: {
|
|
589
|
+
start: FlowStep;
|
|
590
|
+
edges: (flow: FlowSchema) => FlowEdge[];
|
|
591
|
+
args?: DtoMessage;
|
|
592
|
+
results?: DtoMessage;
|
|
593
|
+
slots?: FlowSlots;
|
|
594
|
+
entrySlots?: FlowSlot[];
|
|
595
|
+
description?: string;
|
|
596
|
+
},
|
|
597
|
+
): FlowSchema {
|
|
598
|
+
const flow: FlowSchema = {
|
|
599
|
+
name,
|
|
600
|
+
description: schema.description,
|
|
601
|
+
start: schema.start,
|
|
602
|
+
returnEnd: { type: 'return', name: 'return' },
|
|
603
|
+
nodes: [],
|
|
604
|
+
edges: [],
|
|
605
|
+
args: schema.args,
|
|
606
|
+
results: schema.results,
|
|
607
|
+
slots: schema.slots,
|
|
608
|
+
entrySlots: schema.entrySlots,
|
|
609
|
+
};
|
|
610
|
+
// The input slot's type is the flow's args message: stamp it on first use,
|
|
611
|
+
// and refuse a registry already bound to a different input contract (slot
|
|
612
|
+
// registries belong to one call tree). Same-name messages are the same
|
|
613
|
+
// contract regardless of instance identity.
|
|
614
|
+
if (schema.args !== undefined && schema.slots !== undefined) {
|
|
615
|
+
const t = schema.slots.args.type;
|
|
616
|
+
if (t === undefined) {
|
|
617
|
+
schema.slots.args.type = schema.args;
|
|
618
|
+
warnShadowedFields(schema.slots.args);
|
|
619
|
+
} else if (messageName(t) !== schema.args.name) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`flow ${name}: the slots args slot carries ${messageName(t)} but the flow declares ${schema.args.name}`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const edges = schema.edges(flow);
|
|
626
|
+
flow.edges = edges; // before node collection: guard check targets resolve through it
|
|
627
|
+
const seen = new Set<FlowNodeOrEnd>();
|
|
628
|
+
const nodes: FlowNodeOrEnd[] = [];
|
|
629
|
+
const addNode = (n: FlowNodeOrEnd): void => {
|
|
630
|
+
if (seen.has(n)) return;
|
|
631
|
+
seen.add(n);
|
|
632
|
+
nodes.push(n);
|
|
633
|
+
};
|
|
634
|
+
const addGuardTargets = (n: FlowStep): void => {
|
|
635
|
+
if (!isGuard(n)) return;
|
|
636
|
+
for (const c of n.checks) {
|
|
637
|
+
const ex = c.exception;
|
|
638
|
+
if (c.return) {
|
|
639
|
+
addNode(flow.returnEnd);
|
|
640
|
+
} else if (ex) {
|
|
641
|
+
// Guard checks exit implicitly: reuse the flow's exception end of the
|
|
642
|
+
// same name (declared by an edge or another guard), else synthesize it.
|
|
643
|
+
let t = nodes.find(
|
|
644
|
+
(x): x is FlowEnd => isEnd(x) && x.type === 'exception' && x.exception?.name === ex.name,
|
|
645
|
+
);
|
|
646
|
+
if (!t) {
|
|
647
|
+
t = {
|
|
648
|
+
type: 'exception',
|
|
649
|
+
name: `throw ${ex.name}`,
|
|
650
|
+
exception: ex,
|
|
651
|
+
description: 'guard implicit exit',
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
addNode(t);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
for (const e of edges) {
|
|
659
|
+
addNode(e.start);
|
|
660
|
+
addNode(e.end);
|
|
661
|
+
}
|
|
662
|
+
addNode(schema.start);
|
|
663
|
+
// Guard targets resolve after every edge endpoint is collected, so an
|
|
664
|
+
// exception end referenced by any edge is reused before synthesis. ifNode
|
|
665
|
+
// targets (cases and else) are collected the same way — an ifNode has no
|
|
666
|
+
// outgoing edges, so its targets enter the flow only here. Targets chain
|
|
667
|
+
// (an ifNode may target another ifNode, or a guard whose implicit exit
|
|
668
|
+
// ends need collecting), so the loop runs over the growing list to a
|
|
669
|
+
// fixpoint instead of a snapshot.
|
|
670
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
671
|
+
const n = nodes[i];
|
|
672
|
+
addGuardTargets(n);
|
|
673
|
+
if (isIfNode(n)) {
|
|
674
|
+
for (const c of n.cases) addNode(c.to);
|
|
675
|
+
addNode(n.else);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
flow.nodes = nodes;
|
|
679
|
+
validate(flow);
|
|
680
|
+
return flow;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Reverse BFS from every FlowEnd marks all nodes that can reach an exit;
|
|
684
|
+
// unmarked nodes sit on a path that never ends (e.g. a cycle without an exit)
|
|
685
|
+
// — reject them at definition time. Guard checks count as implicit edges of
|
|
686
|
+
// the guard itself; TryNode internals are validated inside their own flows.
|
|
687
|
+
function validate(schema: FlowSchema): void {
|
|
688
|
+
const reverse = new Map<FlowNodeOrEnd, FlowNodeOrEnd[]>();
|
|
689
|
+
for (const n of schema.nodes) reverse.set(n, []);
|
|
690
|
+
for (const e of schema.edges) {
|
|
691
|
+
reverse.get(e.end)!.push(e.start);
|
|
692
|
+
}
|
|
693
|
+
// guard checks are implicit edges of the guard node itself (return checks
|
|
694
|
+
// to the return end, exception checks to the matching exception end)
|
|
695
|
+
for (const n of schema.nodes) {
|
|
696
|
+
if (!isGuard(n)) continue;
|
|
697
|
+
for (const c of n.checks) {
|
|
698
|
+
if (c.return) {
|
|
699
|
+
const arr = reverse.get(schema.returnEnd);
|
|
700
|
+
if (arr) arr.push(n); // returnEnd is absent when no edge targets it; the guard is then unreachable anyway
|
|
701
|
+
} else if (c.exception) {
|
|
702
|
+
const t = findExceptionEnd(schema, c.exception.name);
|
|
703
|
+
if (t) reverse.get(t)!.push(n);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// ifNode branches are implicit edges of the decision node itself
|
|
708
|
+
for (const n of schema.nodes) {
|
|
709
|
+
if (!isIfNode(n)) continue;
|
|
710
|
+
for (const c of n.cases) reverse.get(c.to)!.push(n);
|
|
711
|
+
reverse.get(n.else)!.push(n);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const reached = new Set<FlowNodeOrEnd>();
|
|
715
|
+
const queue: FlowNodeOrEnd[] = [];
|
|
716
|
+
for (const n of schema.nodes) {
|
|
717
|
+
if (isEnd(n)) {
|
|
718
|
+
reached.add(n);
|
|
719
|
+
queue.push(n);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
while (queue.length > 0) {
|
|
723
|
+
const cur = queue.shift()!;
|
|
724
|
+
for (const prev of reverse.get(cur)!) {
|
|
725
|
+
if (!reached.has(prev)) {
|
|
726
|
+
reached.add(prev);
|
|
727
|
+
queue.push(prev);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const n of schema.nodes) {
|
|
733
|
+
if (!reached.has(n)) {
|
|
734
|
+
throw new Error(`flow ${schema.name}: node "${n.name}" cannot reach an end node`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Forward BFS from the start: every node must be reachable, so dead regions
|
|
739
|
+
// cannot silently pollute the escape set (or the catch matching).
|
|
740
|
+
const fromStart = new Set<FlowNodeOrEnd>();
|
|
741
|
+
const forward: FlowNodeOrEnd[] = [schema.start];
|
|
742
|
+
while (forward.length > 0) {
|
|
743
|
+
const cur = forward.shift()!;
|
|
744
|
+
if (fromStart.has(cur)) continue;
|
|
745
|
+
fromStart.add(cur);
|
|
746
|
+
for (const e of schema.edges) {
|
|
747
|
+
if (e.start === cur) forward.push(e.end);
|
|
748
|
+
}
|
|
749
|
+
if (isGuard(cur)) {
|
|
750
|
+
for (const c of cur.checks) {
|
|
751
|
+
if (c.return) {
|
|
752
|
+
forward.push(schema.returnEnd);
|
|
753
|
+
} else if (c.exception) {
|
|
754
|
+
const t = findExceptionEnd(schema, c.exception.name);
|
|
755
|
+
if (t) forward.push(t);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (isIfNode(cur)) {
|
|
760
|
+
for (const c of cur.cases) forward.push(c.to);
|
|
761
|
+
forward.push(cur.else);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
for (const n of schema.nodes) {
|
|
765
|
+
if (!fromStart.has(n)) {
|
|
766
|
+
throw new Error(`flow ${schema.name}: node "${n.name}" is not reachable from the start node`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
validateGuardChecks(schema);
|
|
771
|
+
for (const e of schema.edges) {
|
|
772
|
+
if (e.check !== undefined) {
|
|
773
|
+
validateCondition(`flow ${schema.name}: edge "${e.name}"`, e.check);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
for (const n of schema.nodes) {
|
|
777
|
+
if (isIfNode(n)) validateIfNode(n, schema);
|
|
778
|
+
}
|
|
779
|
+
validateSlots(schema);
|
|
780
|
+
for (const n of schema.nodes) {
|
|
781
|
+
if (isEnd(n)) continue;
|
|
782
|
+
validateThrowsCoverage(n, schema);
|
|
783
|
+
if (isTryNode(n)) validateTryNode(n, schema);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Guard check exceptions exit implicitly — a typed throws edge carrying
|
|
787
|
+
// one of the guard's own check exceptions would duplicate the route. Edges
|
|
788
|
+
// carrying other exceptions (or catch-all edges) stay legal: they route
|
|
789
|
+
// throws declared by the guard's methods.
|
|
790
|
+
for (const e of schema.edges) {
|
|
791
|
+
if (!isGuard(e.start)) continue;
|
|
792
|
+
if (e.throws !== undefined && e.start.checks.some((c) => c.exception?.name === e.throws!.name)) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`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`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// An ifNode owns its exits internally (cases and else) — outgoing edges
|
|
800
|
+
// would be a second way out of the decision.
|
|
801
|
+
for (const e of schema.edges) {
|
|
802
|
+
if (!isIfNode(e.start)) continue;
|
|
803
|
+
throw new Error(
|
|
804
|
+
`flow ${schema.name}: edge "${e.name}" from ifNode "${e.start.name}" is redundant — ifNode exits are internal (cases and else)`,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Typed throws edges land on an exception end carrying the same exception
|
|
809
|
+
// (escape) — never on a plain step, and no silent renames.
|
|
810
|
+
for (const e of schema.edges) {
|
|
811
|
+
if (e.throws === undefined) continue;
|
|
812
|
+
if (!isEnd(e.end) || e.end.type !== 'exception') {
|
|
813
|
+
throw new Error(
|
|
814
|
+
`flow ${schema.name}: edge "${e.name}" carries throw ${e.throws.name} but its target is not an exception end`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (!e.end.exception) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`flow ${schema.name}: edge "${e.name}" targets exception end "${e.end.name}" with no exception type`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (e.end.exception.name !== e.throws.name) {
|
|
823
|
+
throw new Error(
|
|
824
|
+
`flow ${schema.name}: edge "${e.name}" carries throw ${e.throws.name} but targets the ${e.end.exception.name} exception end`,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Untyped catch-all edges also escape — the target must be an exception end.
|
|
830
|
+
for (const e of schema.edges) {
|
|
831
|
+
if (e.exception !== true || e.throws !== undefined) continue;
|
|
832
|
+
if (!isEnd(e.end) || e.end.type !== 'exception') {
|
|
833
|
+
throw new Error(
|
|
834
|
+
`flow ${schema.name}: edge "${e.name}" catch-all exception path must target an exception end`,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Plain sub-flows are structure only — exceptions must go through a tryNode.
|
|
840
|
+
for (const n of schema.nodes) {
|
|
841
|
+
if (!isFlowNode(n) || !n.flow) continue;
|
|
842
|
+
const ends = exceptionEndNames(n.flow);
|
|
843
|
+
if (ends.size > 0) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`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`,
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Every guard check must choose exactly one of return / exception; its
|
|
852
|
+
// optional machine-readable condition must be well-formed.
|
|
853
|
+
function validateGuardChecks(schema: FlowSchema): void {
|
|
854
|
+
for (const n of schema.nodes) {
|
|
855
|
+
if (!isGuard(n)) continue;
|
|
856
|
+
for (const c of n.checks) {
|
|
857
|
+
const hasReturn = c.return === true;
|
|
858
|
+
const hasException = c.exception !== undefined;
|
|
859
|
+
if (hasReturn === hasException) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`flow ${schema.name}: guard "${n.name}" check "${c.when}" must have exactly one of return/exception`,
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
if (c.check !== undefined) {
|
|
865
|
+
validateCondition(`flow ${schema.name}: guard "${n.name}" check "${c.when}"`, c.check);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// A condition must be a utils predicate call (boolean result, no result
|
|
872
|
+
// slot) or a field comparison whose op is known and whose value presence and
|
|
873
|
+
// type match the operator.
|
|
874
|
+
const COMPARE_OPS: readonly CompareOp[] = ['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'isNull', 'isNotNull'];
|
|
875
|
+
|
|
876
|
+
function validateCondition(where: string, c: GuardCondition): void {
|
|
877
|
+
if (!isCall(c)) {
|
|
878
|
+
if (isFlowSlot(c.field)) {
|
|
879
|
+
const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
|
|
880
|
+
if (!nullOp) {
|
|
881
|
+
throw new Error(`${where}: a slot-level check only supports isNull/isNotNull`);
|
|
882
|
+
}
|
|
883
|
+
if (c.value !== undefined) {
|
|
884
|
+
throw new Error(`${where}: ${c.op} takes no value`);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const f = c.field as Partial<SlotFieldRef> | null;
|
|
889
|
+
if (typeof f !== 'object' || f === null || typeof f.slot !== 'object' || typeof f.field !== 'object') {
|
|
890
|
+
throw new Error(`${where}: ${c.op} field must be a slot field access like slots.args.amt`);
|
|
891
|
+
}
|
|
892
|
+
if (!(COMPARE_OPS as readonly unknown[]).includes(c.op)) {
|
|
893
|
+
throw new Error(`${where}: unknown comparison op ${String(c.op)}`);
|
|
894
|
+
}
|
|
895
|
+
const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
|
|
896
|
+
if (nullOp && c.value !== undefined) {
|
|
897
|
+
throw new Error(`${where}: ${c.op} takes no value`);
|
|
898
|
+
}
|
|
899
|
+
if (!nullOp && c.value === undefined) {
|
|
900
|
+
throw new Error(`${where}: ${c.op} requires a value`);
|
|
901
|
+
}
|
|
902
|
+
const v = c.value;
|
|
903
|
+
if (v !== undefined && typeof v !== 'string' && typeof v !== 'number') {
|
|
904
|
+
const ev = v as Partial<EnumValue> | null;
|
|
905
|
+
if (typeof ev !== 'object' || ev === null || ev.value === undefined || ev.symbol === undefined || ev.label === undefined) {
|
|
906
|
+
throw new Error(`${where}: ${c.op} value must be a string, number, or enum value`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const m = c.method as { type?: string; name: string };
|
|
912
|
+
if (m.type !== 'utilsMethod') {
|
|
913
|
+
throw new Error(`${where}: check call must be a utils predicate, got ${m.type ?? m.name}`);
|
|
914
|
+
}
|
|
915
|
+
if (c.result !== undefined) {
|
|
916
|
+
throw new Error(`${where}: a predicate call cannot bind a result slot`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Slots a condition reads: a comparison's field slot (or the slot itself for
|
|
921
|
+
* slot-level checks) or a predicate call's arg slots. */
|
|
922
|
+
function conditionSlots(c: GuardCondition): FlowSlot[] {
|
|
923
|
+
if (!isCall(c)) return [isFlowSlot(c.field) ? c.field : c.field.slot];
|
|
924
|
+
return c.args ?? [];
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// ifNode rules: at least one case, every condition well-formed, and targets
|
|
928
|
+
// are steps — flow exits (return/throw) belong to guard checks, not to
|
|
929
|
+
// conditional jumps.
|
|
930
|
+
function validateIfNode(n: IfNode, schema: FlowSchema): void {
|
|
931
|
+
if (n.cases.length === 0) {
|
|
932
|
+
throw new Error(`flow ${schema.name}: ifNode "${n.name}" must have at least one case`);
|
|
933
|
+
}
|
|
934
|
+
for (const c of n.cases) {
|
|
935
|
+
validateCondition(`flow ${schema.name}: ifNode "${n.name}" case "${c.when}"`, c.check);
|
|
936
|
+
validateIfTarget(schema, n, c.to, `case "${c.when}"`);
|
|
937
|
+
}
|
|
938
|
+
validateIfTarget(schema, n, n.else, 'else');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function validateIfTarget(schema: FlowSchema, n: IfNode, t: FlowNodeOrEnd, where: string): void {
|
|
942
|
+
if (isEnd(t)) {
|
|
943
|
+
throw new Error(
|
|
944
|
+
`flow ${schema.name}: ifNode "${n.name}" ${where} must target a step — flow exits (return/throw) belong to guard checks`,
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Slots: every referenced slot must be declared in this flow's registers
|
|
950
|
+
// (sub-flows analyze their own slots), every declared named slot must be
|
|
951
|
+
// used, and a slot may only be consumed when every path from the start has
|
|
952
|
+
// produced it (must-analysis over the control graph). The built-in args slot
|
|
953
|
+
// is the flow input: it needs no registration and is available on entry.
|
|
954
|
+
// Within one node, calls run in order, so an earlier call's result feeds a
|
|
955
|
+
// later call's args; the node's own writes feed everything after them.
|
|
956
|
+
// Slot/contract type differences convert implicitly and are not validated.
|
|
957
|
+
function validateSlots(schema: FlowSchema): void {
|
|
958
|
+
const registry = schema.slots;
|
|
959
|
+
const argsSlot = registry?.args;
|
|
960
|
+
const declared = new Set<FlowSlot>(registry === undefined ? [] : Object.values(registry));
|
|
961
|
+
const used = new Set<FlowSlot>();
|
|
962
|
+
|
|
963
|
+
const produced = new Map<FlowNodeOrEnd, Set<FlowSlot>>();
|
|
964
|
+
for (const n of schema.nodes) {
|
|
965
|
+
const p = new Set<FlowSlot>();
|
|
966
|
+
if (!isEnd(n) && !isTryNode(n)) {
|
|
967
|
+
if (isIfNode(n)) {
|
|
968
|
+
for (const c of n.cases) {
|
|
969
|
+
for (const t of conditionSlots(c.check)) used.add(t);
|
|
970
|
+
}
|
|
971
|
+
} else {
|
|
972
|
+
for (const ref of n.methods ?? []) {
|
|
973
|
+
if (!isCall(ref)) continue;
|
|
974
|
+
for (const t of ref.args ?? []) used.add(t);
|
|
975
|
+
if (ref.result) {
|
|
976
|
+
used.add(ref.result);
|
|
977
|
+
p.add(ref.result);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (isFlowNode(n)) {
|
|
981
|
+
for (const t of n.reads ?? []) used.add(t);
|
|
982
|
+
if (n.publish?.payload) used.add(n.publish.payload);
|
|
983
|
+
for (const t of n.writes ?? []) {
|
|
984
|
+
used.add(t);
|
|
985
|
+
p.add(t);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (isGuard(n)) {
|
|
989
|
+
for (const ch of n.checks) {
|
|
990
|
+
for (const t of ch.reads ?? []) used.add(t);
|
|
991
|
+
if (ch.check) {
|
|
992
|
+
for (const t of conditionSlots(ch.check)) used.add(t);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
produced.set(n, p);
|
|
999
|
+
}
|
|
1000
|
+
// a branch edge's condition is decided at its start node
|
|
1001
|
+
for (const e of schema.edges) {
|
|
1002
|
+
if (!e.check) continue;
|
|
1003
|
+
for (const t of conditionSlots(e.check)) used.add(t);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
for (const t of used) {
|
|
1007
|
+
if (!declared.has(t)) {
|
|
1008
|
+
if (t.name === 'args') {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`flow ${schema.name}: slot "args" is not declared — declare the flow's slots via defineSlots (its built-in args slot)`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
throw new Error(`flow ${schema.name}: slot "${t.name}" is not declared in the flow's slots`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
for (const t of declared) {
|
|
1017
|
+
if (t === argsSlot) continue; // the input slot needs no use
|
|
1018
|
+
if (!used.has(t)) {
|
|
1019
|
+
throw new Error(`flow ${schema.name}: slot "${t.name}" is declared but never used`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Fixpoint over the graph: available(n) = intersection over all incoming
|
|
1024
|
+
// edges of (available(start) ∪ produced(start)). The input slot is seeded
|
|
1025
|
+
// at the start. Sets only grow, so the fixpoint terminates. Incoming edges
|
|
1026
|
+
// include the implicit ones (ifNode cases/else), so availability flows
|
|
1027
|
+
// through decision nodes the same way reachability does.
|
|
1028
|
+
const reverse = new Map<FlowNodeOrEnd, FlowNodeOrEnd[]>();
|
|
1029
|
+
for (const n of schema.nodes) reverse.set(n, []);
|
|
1030
|
+
for (const e of schema.edges) reverse.get(e.end)!.push(e.start);
|
|
1031
|
+
for (const n of schema.nodes) {
|
|
1032
|
+
if (!isIfNode(n)) continue;
|
|
1033
|
+
for (const c of n.cases) reverse.get(c.to)!.push(n);
|
|
1034
|
+
reverse.get(n.else)!.push(n);
|
|
1035
|
+
}
|
|
1036
|
+
const avail = new Map<FlowNodeOrEnd, Set<FlowSlot>>();
|
|
1037
|
+
for (const n of schema.nodes) avail.set(n, new Set<FlowSlot>());
|
|
1038
|
+
const seeded = new Set<FlowSlot>();
|
|
1039
|
+
if (argsSlot) seeded.add(argsSlot);
|
|
1040
|
+
for (const t of schema.entrySlots ?? []) {
|
|
1041
|
+
if (!declared.has(t)) {
|
|
1042
|
+
throw new Error(`flow ${schema.name}: entry slot "${t.name}" is not declared in the flow's slots`);
|
|
1043
|
+
}
|
|
1044
|
+
seeded.add(t);
|
|
1045
|
+
}
|
|
1046
|
+
// add one by one: this engine's Set.prototype.add takes a single value
|
|
1047
|
+
for (const t of seeded) avail.get(schema.start)!.add(t);
|
|
1048
|
+
let changed = true;
|
|
1049
|
+
while (changed) {
|
|
1050
|
+
changed = false;
|
|
1051
|
+
for (const n of schema.nodes) {
|
|
1052
|
+
if (n === schema.start) continue;
|
|
1053
|
+
const incoming = reverse.get(n)!;
|
|
1054
|
+
if (incoming.length === 0) continue;
|
|
1055
|
+
let intersection: Set<FlowSlot> | undefined;
|
|
1056
|
+
for (const srcN of incoming) {
|
|
1057
|
+
const src = new Set(avail.get(srcN)!);
|
|
1058
|
+
for (const t of produced.get(srcN)!) src.add(t);
|
|
1059
|
+
intersection = intersection === undefined ? src : new Set([...intersection].filter((t) => src.has(t)));
|
|
1060
|
+
}
|
|
1061
|
+
if (intersection === undefined) continue;
|
|
1062
|
+
for (const t of intersection) {
|
|
1063
|
+
if (!avail.get(n)!.has(t)) {
|
|
1064
|
+
avail.get(n)!.add(t);
|
|
1065
|
+
changed = true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Consumption check walks each node's ordered sequence: writes
|
|
1072
|
+
// (constructions) first, then calls in order, then branch reads/checks.
|
|
1073
|
+
const missing = (n: FlowStep, t: FlowSlot): Error => {
|
|
1074
|
+
if (n === schema.start) {
|
|
1075
|
+
return new Error(
|
|
1076
|
+
`flow ${schema.name}: start node "${n.name}" cannot consume slot "${t.name}" — nothing else is produced before entry`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
return new Error(`flow ${schema.name}: node "${n.name}" reads slot "${t.name}" that may not be written on every path`);
|
|
1080
|
+
};
|
|
1081
|
+
for (const n of schema.nodes) {
|
|
1082
|
+
if (isEnd(n) || isTryNode(n)) continue;
|
|
1083
|
+
const inner = new Set(avail.get(n)!);
|
|
1084
|
+
if (isFlowNode(n)) {
|
|
1085
|
+
for (const t of n.writes ?? []) inner.add(t);
|
|
1086
|
+
}
|
|
1087
|
+
if (!isIfNode(n)) {
|
|
1088
|
+
for (const ref of n.methods ?? []) {
|
|
1089
|
+
if (!isCall(ref)) continue;
|
|
1090
|
+
for (const t of ref.args ?? []) {
|
|
1091
|
+
if (!inner.has(t)) throw missing(n, t);
|
|
1092
|
+
}
|
|
1093
|
+
if (ref.result) inner.add(ref.result);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (isFlowNode(n)) {
|
|
1097
|
+
for (const t of n.reads ?? []) {
|
|
1098
|
+
if (!inner.has(t)) throw missing(n, t);
|
|
1099
|
+
}
|
|
1100
|
+
if (n.publish?.payload && !inner.has(n.publish.payload)) throw missing(n, n.publish.payload);
|
|
1101
|
+
}
|
|
1102
|
+
if (isGuard(n)) {
|
|
1103
|
+
for (const ch of n.checks) {
|
|
1104
|
+
for (const t of ch.reads ?? []) {
|
|
1105
|
+
if (!inner.has(t)) throw missing(n, t);
|
|
1106
|
+
}
|
|
1107
|
+
if (ch.check) {
|
|
1108
|
+
for (const t of conditionSlots(ch.check)) {
|
|
1109
|
+
if (!inner.has(t)) throw missing(n, t);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (isIfNode(n)) {
|
|
1115
|
+
for (const c of n.cases) {
|
|
1116
|
+
for (const t of conditionSlots(c.check)) {
|
|
1117
|
+
if (!inner.has(t)) throw missing(n, t);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// branch decisions read their slots at the end of the node
|
|
1122
|
+
for (const e of schema.edges) {
|
|
1123
|
+
if (e.start !== n || !e.check) continue;
|
|
1124
|
+
for (const t of conditionSlots(e.check)) {
|
|
1125
|
+
if (!inner.has(t)) throw missing(n, t);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Every exception a step declares (methods throws, TryNode rethrows) must be
|
|
1132
|
+
// routed: typed throws edges or an untyped exception edge (catch-all). Guard
|
|
1133
|
+
// check exceptions route implicitly to the matching exception end and are not
|
|
1134
|
+
// covered here. The reverse direction (routed ⊆ declared) is intentionally
|
|
1135
|
+
// not validated while steps carry pure descriptors — a MethodSchema has no
|
|
1136
|
+
// throws field yet, so typed edges may document throws the descriptor cannot
|
|
1137
|
+
// express. Once contract refs replace the descriptors, the reverse check can
|
|
1138
|
+
// be enforced.
|
|
1139
|
+
function validateThrowsCoverage(n: FlowStep, schema: FlowSchema): void {
|
|
1140
|
+
const declared = new Set<string>();
|
|
1141
|
+
if (!isTryNode(n) && !isIfNode(n)) {
|
|
1142
|
+
for (const ref of n.methods ?? []) {
|
|
1143
|
+
const m = methodOf(ref);
|
|
1144
|
+
if ('throws' in m && m.throws) {
|
|
1145
|
+
for (const e of m.throws) declared.add(e.name);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (isTryNode(n)) {
|
|
1150
|
+
for (const c of n.catches) {
|
|
1151
|
+
for (const name of exceptionEndNames(c.handler)) declared.add(name);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (declared.size === 0) return;
|
|
1155
|
+
|
|
1156
|
+
const routed = new Set<string>();
|
|
1157
|
+
for (const e of schema.edges) {
|
|
1158
|
+
if (e.start !== n) continue;
|
|
1159
|
+
if (e.throws) {
|
|
1160
|
+
routed.add(e.throws.name);
|
|
1161
|
+
} else if (e.exception === true) {
|
|
1162
|
+
return; // untyped exception edge: catch-all
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
for (const d of declared) {
|
|
1166
|
+
if (!routed.has(d)) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
`flow ${schema.name}: node "${n.name}" declares throw ${d} but has no typed throws edge for it`,
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// TryNode rules: catches must be unique with UnexpectedException last, every
|
|
1175
|
+
// body exception end needs a catch, every catch must match a body exception
|
|
1176
|
+
// end (no dead catches), and finally must be pure cleanup (no exception ends).
|
|
1177
|
+
// Body end names must be unique so catch matching is unambiguous. Catch
|
|
1178
|
+
// matching works on declared end names — reachability of a body end from the
|
|
1179
|
+
// body start is not analyzed (a dead region's end would still count).
|
|
1180
|
+
function validateTryNode(n: TryNode, schema: FlowSchema): void {
|
|
1181
|
+
const seen = new Set<string>();
|
|
1182
|
+
let catchAll = false;
|
|
1183
|
+
for (const c of n.catches) {
|
|
1184
|
+
if (seen.has(c.exception.name)) {
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
`flow ${schema.name}: tryNode "${n.name}" has duplicate catch ${c.exception.name}`,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
seen.add(c.exception.name);
|
|
1190
|
+
if (catchAll) {
|
|
1191
|
+
throw new Error(
|
|
1192
|
+
`flow ${schema.name}: tryNode "${n.name}" catch ${c.exception.name} must precede the ${UnexpectedException.name} catch`,
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
if (c.exception.name === UnexpectedException.name) catchAll = true;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const bodyEndCounts = new Map<string, number>();
|
|
1199
|
+
for (const node of n.body.nodes) {
|
|
1200
|
+
if (isEnd(node) && node.type === 'exception' && node.exception) {
|
|
1201
|
+
bodyEndCounts.set(node.exception.name, (bodyEndCounts.get(node.exception.name) ?? 0) + 1);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
for (const [name, count] of bodyEndCounts) {
|
|
1205
|
+
if (count > 1) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`flow ${schema.name}: tryNode "${n.name}" body has ${count} exception ends named ${name}`,
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const bodyEnds = new Set(bodyEndCounts.keys());
|
|
1212
|
+
for (const name of bodyEnds) {
|
|
1213
|
+
if (!seen.has(name)) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`flow ${schema.name}: tryNode "${n.name}" body exception end ${name} has no catch`,
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (const c of n.catches) {
|
|
1220
|
+
if (!bodyEnds.has(c.exception.name)) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`flow ${schema.name}: tryNode "${n.name}" catch ${c.exception.name} matches no body exception end`,
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (n.finally) {
|
|
1228
|
+
const f = exceptionEndNames(n.finally);
|
|
1229
|
+
if (f.size > 0) {
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
`flow ${schema.name}: tryNode "${n.name}" finally must not declare exception ends: ${[...f].join(', ')}`,
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/** Exception names declared by a flow's exception ends — the flow's escape
|
|
1238
|
+
* set (throws that reach the caller unless caught by an outer tryNode). */
|
|
1239
|
+
export function exceptionEndNames(flow: FlowSchema): Set<string> {
|
|
1240
|
+
const names = new Set<string>();
|
|
1241
|
+
for (const n of flow.nodes) {
|
|
1242
|
+
if (!isEnd(n) || n.type !== 'exception') continue;
|
|
1243
|
+
if (!n.exception) {
|
|
1244
|
+
throw new Error(`flow ${flow.name}: exception end "${n.name}" has no exception type`);
|
|
1245
|
+
}
|
|
1246
|
+
names.add(n.exception.name);
|
|
1247
|
+
}
|
|
1248
|
+
return names;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
export function isEnd(n: FlowNodeOrEnd): n is FlowEnd {
|
|
1252
|
+
return 'type' in n && (n.type === 'return' || n.type === 'exception');
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function messageName(t: unknown): string {
|
|
1256
|
+
return (t as { name?: string } | null | undefined)?.name ?? 'an unnamed message';
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
export function isFlowNode(n: FlowNodeOrEnd): n is FlowNode {
|
|
1260
|
+
return !('type' in n);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
export function isGuard(n: FlowNodeOrEnd): n is GuardNode {
|
|
1264
|
+
return 'type' in n && n.type === 'guard';
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
export function isTryNode(n: FlowNodeOrEnd): n is TryNode {
|
|
1268
|
+
return 'type' in n && n.type === 'try';
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
export function isIfNode(n: FlowNodeOrEnd): n is IfNode {
|
|
1272
|
+
return 'type' in n && n.type === 'if';
|
|
1227
1273
|
}
|