@reventlessdev/reventless-spec 3.0.0-alpha.130 → 3.0.0-alpha.131

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.
@@ -0,0 +1,1107 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
5
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
6
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
7
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
8
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
9
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
11
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
12
+ import * as Nodechild_process from "node:child_process";
13
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
14
+ import * as LoadPluginStructureMjs from "./loadPluginStructure.mjs";
15
+
16
+ process.env["REVENTLESS_DECLARED_TRANSITIONS_ONLY"] = "1";
17
+
18
+ let repoRoot = process.cwd();
19
+
20
+ let examplesDir = Nodepath.join(repoRoot, "examples");
21
+
22
+ let update = process.argv.includes("--update");
23
+
24
+ function flagValues(flag) {
25
+ let argv = process.argv;
26
+ let out = [];
27
+ for (let i = 0, i_finish = argv.length; i < i_finish; ++i) {
28
+ if (Primitive_object.equal(argv[i], flag)) {
29
+ let value = argv[i + 1 | 0];
30
+ if (value !== undefined && !value.startsWith("--")) {
31
+ out.push(value);
32
+ }
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ let reuseSidecars = process.argv.includes("--reuse-sidecars");
39
+
40
+ let json = process.argv.includes("--json");
41
+
42
+ let noRow = "(none)";
43
+
44
+ function asObj(j) {
45
+ return Stdlib_JSON.Decode.object(j);
46
+ }
47
+
48
+ function getStr(d, k) {
49
+ return Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
50
+ }
51
+
52
+ function getArr(d, k) {
53
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.array), []);
54
+ }
55
+
56
+ function getObjs(d, k) {
57
+ return Stdlib_Array.filterMap(getArr(d, k), asObj);
58
+ }
59
+
60
+ function getStrs(d, k) {
61
+ return Stdlib_Array.filterMap(getArr(d, k), Stdlib_JSON.Decode.string);
62
+ }
63
+
64
+ function getStrsOpt(d, k) {
65
+ let v = d[k];
66
+ if (v !== undefined) {
67
+ return Stdlib_Option.map(Stdlib_JSON.Decode.array(v), a => Stdlib_Array.filterMap(a, Stdlib_JSON.Decode.string));
68
+ }
69
+ }
70
+
71
+ function last(xs) {
72
+ return xs[xs.length - 1 | 0];
73
+ }
74
+
75
+ function lastSegment(name) {
76
+ let parts = name.split(".");
77
+ return Stdlib_Option.getOr(last(parts), name);
78
+ }
79
+
80
+ function sortedUnique(xs) {
81
+ let out = [];
82
+ xs.forEach(x => {
83
+ if (!out.includes(x)) {
84
+ out.push(x);
85
+ return;
86
+ }
87
+ });
88
+ return out.toSorted(Primitive_string.compare);
89
+ }
90
+
91
+ function valuesOf(step) {
92
+ return Stdlib_Array.filterMap(getArr(step, "values"), v => {
93
+ let match = Stdlib_JSON.Decode.array(v);
94
+ if (match === undefined) {
95
+ return;
96
+ }
97
+ if (match.length !== 2) {
98
+ return;
99
+ }
100
+ let name = match[0];
101
+ let value = match[1];
102
+ return Stdlib_Option.map(Stdlib_JSON.Decode.string(name), n => [
103
+ n,
104
+ value
105
+ ]);
106
+ });
107
+ }
108
+
109
+ function elementsOf(steps) {
110
+ return Stdlib_Array.filterMap(steps, s => Stdlib_Option.map(getStr(s, "element"), name => ({
111
+ name: lastSegment(name),
112
+ values: valuesOf(s)
113
+ })));
114
+ }
115
+
116
+ function kindOf(steps) {
117
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(steps[0], s => getStr(s, "kind")), "");
118
+ }
119
+
120
+ function scenarioOf(j) {
121
+ return Stdlib_Option.map(Stdlib_JSON.Decode.object(j), d => {
122
+ let given = getObjs(d, "given");
123
+ let when_ = getObjs(d, "when");
124
+ let then_ = getObjs(d, "then");
125
+ return {
126
+ title: Stdlib_Option.getOr(getStr(d, "title"), ""),
127
+ given: elementsOf(given),
128
+ whenKind: kindOf(when_),
129
+ whenElements: elementsOf(when_),
130
+ thenKind: kindOf(then_),
131
+ thenElements: elementsOf(then_),
132
+ thenValues: Stdlib_Option.getOr(Stdlib_Option.map(then_[0], valuesOf), [])
133
+ };
134
+ });
135
+ }
136
+
137
+ function readCorpus(path) {
138
+ let d;
139
+ try {
140
+ d = Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(path, "utf8")));
141
+ } catch (exn) {
142
+ return;
143
+ }
144
+ if (d === undefined) {
145
+ return;
146
+ }
147
+ let stem = Stdlib_Option.getOr(getStr(d, "stem"), "");
148
+ return {
149
+ component: stem.replace("_GWT", ""),
150
+ path: path,
151
+ scenarios: Stdlib_Array.filterMap(getArr(d, "scenarios"), scenarioOf)
152
+ };
153
+ }
154
+
155
+ function filesUnder(dir, suffix) {
156
+ let entries;
157
+ try {
158
+ entries = Nodefs.readdirSync(dir, {
159
+ withFileTypes: true
160
+ });
161
+ } catch (exn) {
162
+ return [];
163
+ }
164
+ return entries.flatMap(entry => {
165
+ let name = entry.name;
166
+ let full = Nodepath.join(dir, name);
167
+ if (entry.isDirectory()) {
168
+ if (name === "node_modules" || name === "lib" || name.startsWith(".")) {
169
+ return [];
170
+ } else {
171
+ return filesUnder(full, suffix);
172
+ }
173
+ } else if (name.endsWith(suffix)) {
174
+ return [full];
175
+ } else {
176
+ return [];
177
+ }
178
+ });
179
+ }
180
+
181
+ function gwtSources(pluginDirs) {
182
+ return pluginDirs.flatMap(dir => filesUnder(Nodepath.join(dir, "tests"), "_GWT.res"));
183
+ }
184
+
185
+ function sidecarOf(gwt) {
186
+ return gwt.replace("_GWT.res", "_GWT.gwt.json");
187
+ }
188
+
189
+ function hasCorpus(pluginDirs) {
190
+ return gwtSources(pluginDirs).some(f => Nodefs.existsSync(f.replace("_GWT.res", "_GWT.gwt.json")));
191
+ }
192
+
193
+ function checkSidecars(pluginDirs) {
194
+ if (hasCorpus(pluginDirs)) {
195
+ return {
196
+ TAG: "Ok",
197
+ _0: undefined
198
+ };
199
+ } else {
200
+ return {
201
+ TAG: "Error",
202
+ _0: "--reuse-sidecars was passed, but no scenario sidecar exists. Build with REVENTLESS_EMIT_SIDECAR=1 first, or drop the flag."
203
+ };
204
+ }
205
+ }
206
+
207
+ function emitSidecars(pluginDirs) {
208
+ let now = Date.now() / 1000.0;
209
+ gwtSources(pluginDirs).forEach(f => {
210
+ Nodefs.utimesSync(f, now, now);
211
+ });
212
+ let env = Object.fromEntries(Object.entries(process.env).concat([[
213
+ "REVENTLESS_EMIT_SIDECAR",
214
+ "1"
215
+ ]]));
216
+ try {
217
+ Nodechild_process.execFileSync("pnpm", [
218
+ "run",
219
+ "build"
220
+ ], {
221
+ cwd: repoRoot,
222
+ encoding: "utf8",
223
+ env: env,
224
+ maxBuffer: 268435456
225
+ });
226
+ return {
227
+ TAG: "Ok",
228
+ _0: undefined
229
+ };
230
+ } catch (raw_exn) {
231
+ let exn = Primitive_exceptions.internalToException(raw_exn);
232
+ return {
233
+ TAG: "Error",
234
+ _0: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "the build that emits the scenario sidecars failed")
235
+ };
236
+ }
237
+ }
238
+
239
+ function levelOf(d) {
240
+ let match = d["level"];
241
+ if (match === undefined) {
242
+ return "";
243
+ }
244
+ if (match === null || Array.isArray(match)) {
245
+ return "";
246
+ }
247
+ switch (typeof match) {
248
+ case "string" :
249
+ return match;
250
+ case "object" :
251
+ return Stdlib_Option.getOr(getStr(match, "TAG"), "");
252
+ default:
253
+ return "";
254
+ }
255
+ }
256
+
257
+ function declaredCommandOf(d) {
258
+ return Stdlib_Option.map(getStr(d, "name"), command => ({
259
+ command: command,
260
+ level: levelOf(d),
261
+ aggregateIdField: getStr(d, "aggregateIdField"),
262
+ allowedStates: getStrsOpt(d, "allowedStates"),
263
+ targetState: getStr(d, "targetState"),
264
+ allowedStatesSource: getStr(d, "allowedStatesSource")
265
+ }));
266
+ }
267
+
268
+ function declaredOf(structure) {
269
+ return Stdlib_Option.map(Stdlib_JSON.Decode.object(structure), s => {
270
+ let writablesFrom = key => Stdlib_Array.filterMap(getObjs(s, key), w => Stdlib_Option.map(getStr(w, "name"), name => ({
271
+ name: name,
272
+ linkedViews: getStrs(w, "linkedViews"),
273
+ commands: Stdlib_Array.filterMap(getObjs(w, "commands"), declaredCommandOf)
274
+ })));
275
+ let viewsFrom = key => Stdlib_Array.filterMap(getObjs(s, key), v => Stdlib_Option.map(getStr(v, "name"), name => ({
276
+ name: name,
277
+ lifecycleField: getStr(v, "lifecycleField")
278
+ })));
279
+ return {
280
+ writables: writablesFrom("aggregates").concat(writablesFrom("stateChangeSlices")),
281
+ views: viewsFrom("readModels").concat(viewsFrom("stateViewSlices"))
282
+ };
283
+ });
284
+ }
285
+
286
+ function loadPluginStructure(prim) {
287
+ return LoadPluginStructureMjs.loadPluginStructure(prim);
288
+ }
289
+
290
+ async function readDeclared(pluginDir) {
291
+ let raw = await LoadPluginStructureMjs.loadPluginStructure(pluginDir);
292
+ let d = Stdlib_JSON.Decode.object(raw);
293
+ if (d === undefined) {
294
+ return {
295
+ TAG: "Error",
296
+ _0: "the structure loader returned something unreadable"
297
+ };
298
+ }
299
+ let match = d["ok"];
300
+ if (match === true) {
301
+ let declared = Stdlib_Option.flatMap(d["structure"], declaredOf);
302
+ if (declared !== undefined) {
303
+ return {
304
+ TAG: "Ok",
305
+ _0: declared
306
+ };
307
+ } else {
308
+ return {
309
+ TAG: "Error",
310
+ _0: "the plugin structure could not be read"
311
+ };
312
+ }
313
+ }
314
+ return {
315
+ TAG: "Error",
316
+ _0: Stdlib_Option.getOr(getStr(d, "error"), "the plugin structure could not be loaded")
317
+ };
318
+ }
319
+
320
+ function lifecycleValue(values, field) {
321
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(values.find(param => param[0] === field), param => Stdlib_JSON.Decode.object(param[1])), v => {
322
+ let match = getStr(v, "kind");
323
+ if (match === undefined) {
324
+ return;
325
+ }
326
+ switch (match) {
327
+ case "enum" :
328
+ case "string" :
329
+ return Stdlib_Option.map(getStr(v, "value"), lastSegment);
330
+ default:
331
+ return;
332
+ }
333
+ });
334
+ }
335
+
336
+ function lifecycleMapFor(scenarios, field, ambiguities, view) {
337
+ let map = {};
338
+ let fold = events => Stdlib_Array.reduce(events, noRow, (current, event) => {
339
+ let value = map[event.name];
340
+ if (value !== undefined) {
341
+ return value;
342
+ } else {
343
+ return current;
344
+ }
345
+ });
346
+ let changed = true;
347
+ let rounds = 0;
348
+ while (changed && rounds < 8) {
349
+ changed = false;
350
+ rounds = rounds + 1 | 0;
351
+ let before = Object.keys(map).length;
352
+ scenarios.forEach(s => {
353
+ if (s.whenKind !== "event" || s.thenKind !== "state") {
354
+ return;
355
+ }
356
+ let match = last(s.whenElements);
357
+ let match$1 = lifecycleValue(s.thenValues, field);
358
+ if (match !== undefined && match$1 !== undefined && (s.given.length === 0 || fold(s.given) !== match$1)) {
359
+ let event = match.name;
360
+ let title = s.title;
361
+ let existing = map[event];
362
+ if (existing !== undefined) {
363
+ if (existing !== match$1) {
364
+ ambiguities.push([
365
+ view,
366
+ view + `: ` + event + ` is projected as both "` + existing + `" and "` + match$1 + `" ` + (`(seen in "` + title + `") — the harvest keeps "` + existing + `"`)
367
+ ]);
368
+ return;
369
+ } else {
370
+ return;
371
+ }
372
+ } else {
373
+ map[event] = match$1;
374
+ return;
375
+ }
376
+ }
377
+ });
378
+ if (Object.keys(map).length !== before) {
379
+ changed = true;
380
+ }
381
+ };
382
+ return map;
383
+ }
384
+
385
+ function outcomeOf(s) {
386
+ let match = s.thenKind;
387
+ switch (match) {
388
+ case "error" :
389
+ return "Refused";
390
+ case "event" :
391
+ return "Emitted";
392
+ default:
393
+ return "NoChange";
394
+ }
395
+ }
396
+
397
+ function valueOf(values, field) {
398
+ return Stdlib_Option.map(values.find(param => param[0] === field), param => param[1]);
399
+ }
400
+
401
+ function sameRow(event, idField, idValue) {
402
+ if (idField === undefined) {
403
+ return true;
404
+ }
405
+ if (idValue === undefined) {
406
+ return true;
407
+ }
408
+ let actual = valueOf(event.values, idField);
409
+ if (actual !== undefined) {
410
+ return Primitive_object.equal(actual, idValue);
411
+ } else {
412
+ return true;
413
+ }
414
+ }
415
+
416
+ function observe(scenarios, map, idFieldFor) {
417
+ let fold = events => Stdlib_Array.reduce(events, noRow, (current, event) => {
418
+ let value = map[event.name];
419
+ if (value !== undefined) {
420
+ return value;
421
+ } else {
422
+ return current;
423
+ }
424
+ });
425
+ return Stdlib_Array.filterMap(scenarios, s => {
426
+ let command = s.whenElements[0];
427
+ if (command === undefined) {
428
+ return;
429
+ }
430
+ if (s.whenKind !== "command") {
431
+ return;
432
+ }
433
+ let idField = idFieldFor(command.name);
434
+ let idValue = Stdlib_Option.flatMap(idField, field => valueOf(command.values, field));
435
+ let history = s.given.filter(e => sameRow(e, idField, idValue));
436
+ let from = fold(history);
437
+ let outcome = outcomeOf(s);
438
+ return {
439
+ title: s.title,
440
+ command: command.name,
441
+ from: from,
442
+ outcome: outcome,
443
+ to: outcome === "Emitted" ? fold(history.concat(s.thenElements)) : from
444
+ };
445
+ });
446
+ }
447
+
448
+ function deriveCommands(component, observations, labelled) {
449
+ let names = sortedUnique(observations.map(o => o.command));
450
+ return names.map(command => {
451
+ let mine = observations.filter(o => o.command === command);
452
+ let effective = mine.filter(o => o.outcome === "Emitted");
453
+ let match = effective.length;
454
+ return {
455
+ component: component,
456
+ command: command,
457
+ allowedStates: sortedUnique(Stdlib_Array.filterMap(effective, o => {
458
+ if (o.from === noRow) {
459
+ return;
460
+ } else {
461
+ return o.from;
462
+ }
463
+ })),
464
+ inertStates: sortedUnique(Stdlib_Array.filterMap(mine, o => {
465
+ if (o.outcome === "Emitted" || o.from === noRow) {
466
+ return;
467
+ } else {
468
+ return o.from;
469
+ }
470
+ })),
471
+ targets: sortedUnique(Stdlib_Array.filterMap(effective, o => {
472
+ if (o.to === o.from || o.to === noRow) {
473
+ return;
474
+ } else {
475
+ return o.to;
476
+ }
477
+ })),
478
+ level: labelled && match !== 0 ? (
479
+ effective.every(o => o.from === noRow) ? "Collection" : "Instance"
480
+ ) : "",
481
+ scenarios: mine.length
482
+ };
483
+ });
484
+ }
485
+
486
+ function allUnverified(cmd, add, why) {
487
+ let states = cmd.allowedStates;
488
+ if (states !== undefined && states.length !== 0) {
489
+ add("unverified", states, `the switch names ` + states.join(", ") + `, and ` + why);
490
+ }
491
+ let target = cmd.targetState;
492
+ if (target !== undefined) {
493
+ return add("unverified", [target], `the switch targets "` + target + `", and ` + why);
494
+ }
495
+ }
496
+
497
+ function compare(plugin, writable, derived, findings) {
498
+ let where = plugin + `/` + writable.name + `.` + derived.command;
499
+ let add = (severity, states, message) => {
500
+ findings.push({
501
+ severity: severity,
502
+ plugin: plugin,
503
+ component: writable.name,
504
+ command: derived.command,
505
+ states: states,
506
+ message: where + `: ` + message
507
+ });
508
+ };
509
+ let declared = writable.commands.find(c => c.command === derived.command);
510
+ if (declared === undefined) {
511
+ return;
512
+ }
513
+ let states = declared.allowedStates;
514
+ if (states !== undefined) {
515
+ states.forEach(state => {
516
+ if (derived.allowedStates.includes(state)) {
517
+ return;
518
+ } else if (derived.inertStates.includes(state)) {
519
+ return add("contradicted", [state], `the switch names "` + state + `", and a scenario from "` + state + `" shows it refused or producing nothing`);
520
+ } else {
521
+ return add("unverified", [state], `the switch names "` + state + `", and no scenario starts there`);
522
+ }
523
+ });
524
+ derived.allowedStates.forEach(state => {
525
+ if (!states.includes(state)) {
526
+ return add("contradicted", [state], `a scenario shows it taking effect from "` + state + `", which its declared ` + (`from-set (` + states.join(", ") + `) excludes`));
527
+ }
528
+ });
529
+ if (states.length !== 0 && derived.allowedStates.length === 0) {
530
+ add("unverified", states, `the switch declares ` + states.length.toString() + ` state(s) and no scenario shows the command taking effect anywhere`);
531
+ }
532
+ } else if (Primitive_object.equal(declared.allowedStatesSource, "unrestricted")) {
533
+ derived.inertStates.forEach(state => add("contradicted", [state], `the switch declares it legal in every state, and a scenario from "` + state + `" shows it refused or producing nothing`));
534
+ } else if (derived.allowedStates.length !== 0) {
535
+ add("undeclared", derived.allowedStates, `scenarios show it taking effect from ` + derived.allowedStates.join(", ") + `, and it declares no edge`);
536
+ }
537
+ let match = declared.targetState;
538
+ let match$1 = derived.targets;
539
+ if (match !== undefined) {
540
+ if (match$1.length !== 0) {
541
+ if (!match$1.includes(match)) {
542
+ add("contradicted", [match], `the switch targets "` + match + `", and scenarios land in ` + match$1.join(", "));
543
+ }
544
+ match$1.forEach(state => {
545
+ if (state !== match) {
546
+ return add("contradicted", [
547
+ match,
548
+ state
549
+ ], `the switch targets "` + match + `", and a scenario lands in "` + state + `" — the published targetState carries one state, so this edge cannot be expressed`);
550
+ }
551
+ });
552
+ } else {
553
+ add("unverified", [match], `the switch targets "` + match + `", and no scenario shows an edge`);
554
+ }
555
+ }
556
+ if (derived.level !== "" && declared.level !== "" && derived.level !== declared.level) {
557
+ return add("level", [], `scenarios make it ` + derived.level + `-level; the published metadata says ` + declared.level);
558
+ }
559
+ }
560
+
561
+ function pluginDirsIn(exampleDir) {
562
+ let entries;
563
+ try {
564
+ entries = Nodefs.readdirSync(exampleDir, {
565
+ withFileTypes: true
566
+ });
567
+ } catch (exn) {
568
+ return [];
569
+ }
570
+ return entries.filter(e => e.isDirectory()).map(e => Nodepath.join(exampleDir, e.name)).filter(dir => {
571
+ if (Nodefs.existsSync(Nodepath.join(dir, "src", "Plugin.res"))) {
572
+ return Nodefs.existsSync(Nodepath.join(dir, "tests"));
573
+ } else {
574
+ return false;
575
+ }
576
+ });
577
+ }
578
+
579
+ if (process.argv.includes("--root") && flagValues("--root").length === 0) {
580
+ console.error("--root needs a directory after it");
581
+ process.exit(1);
582
+ }
583
+
584
+ let given = flagValues("--root");
585
+
586
+ let roots;
587
+
588
+ if (given.length !== 0) {
589
+ roots = given.map(given => {
590
+ let dir = Nodepath.resolve(given);
591
+ return {
592
+ label: Nodepath.basename(dir),
593
+ dir: dir
594
+ };
595
+ });
596
+ } else {
597
+ let exit = 0;
598
+ let entries;
599
+ try {
600
+ entries = Nodefs.readdirSync(examplesDir, {
601
+ withFileTypes: true
602
+ });
603
+ exit = 1;
604
+ } catch (exn) {
605
+ roots = [];
606
+ }
607
+ if (exit === 1) {
608
+ roots = entries.filter(e => e.isDirectory()).map(e => e.name).toSorted(Primitive_string.compare).map(name => ({
609
+ label: name,
610
+ dir: Nodepath.join(examplesDir, name)
611
+ }));
612
+ }
613
+ }
614
+
615
+ function isViewPath(path) {
616
+ return [
617
+ "/ReadModel/",
618
+ "/ReadModelStream/",
619
+ "/StateViewSlice/",
620
+ "/StateViewSliceStream/"
621
+ ].some(seg => path.includes(seg));
622
+ }
623
+
624
+ function isWritablePath(path) {
625
+ return [
626
+ "/Aggregate/",
627
+ "/StateChangeSlice/"
628
+ ].some(seg => path.includes(seg));
629
+ }
630
+
631
+ async function runPlugin(plugin, pluginDir, findings, opaque) {
632
+ let msg = await readDeclared(pluginDir);
633
+ if (msg.TAG !== "Ok") {
634
+ return {
635
+ TAG: "Error",
636
+ _0: msg._0
637
+ };
638
+ }
639
+ let declared = msg._0;
640
+ let corpora = Stdlib_Array.filterMap(filesUnder(Nodepath.join(pluginDir, "tests"), ".gwt.json"), readCorpus);
641
+ corpora.forEach(c => {
642
+ let unreadable = c.scenarios.filter(s => s.whenElements.length === 0);
643
+ if (unreadable.length !== 0) {
644
+ opaque.push({
645
+ plugin: plugin,
646
+ component: c.component,
647
+ path: c.path,
648
+ scenarios: c.scenarios.length,
649
+ unreadable: unreadable.length
650
+ });
651
+ return;
652
+ }
653
+ });
654
+ let mapsByView = {};
655
+ let ambiguities = [];
656
+ corpora.forEach(c => {
657
+ if (!isViewPath(c.path)) {
658
+ return;
659
+ }
660
+ let match = declared.views.find(v => v.name === c.component);
661
+ if (match === undefined) {
662
+ return;
663
+ }
664
+ let field = match.lifecycleField;
665
+ if (field !== undefined) {
666
+ mapsByView[c.component] = lifecycleMapFor(c.scenarios, field, ambiguities, c.component);
667
+ return;
668
+ }
669
+ });
670
+ ambiguities.forEach(param => {
671
+ findings.push({
672
+ severity: "ambiguous",
673
+ plugin: plugin,
674
+ component: param[0],
675
+ command: "",
676
+ states: [],
677
+ message: param[1]
678
+ });
679
+ });
680
+ let derived = [];
681
+ corpora.forEach(c => {
682
+ if (!isWritablePath(c.path)) {
683
+ return;
684
+ }
685
+ let writable = declared.writables.find(w => w.name === c.component);
686
+ if (writable === undefined) {
687
+ return;
688
+ }
689
+ let map = {};
690
+ writable.linkedViews.forEach(view => {
691
+ let m = mapsByView[view];
692
+ if (m !== undefined) {
693
+ return Stdlib_Dict.forEachWithKey(m, (value, event) => {
694
+ map[event] = value;
695
+ });
696
+ }
697
+ });
698
+ let labelled = Object.keys(map).length !== 0;
699
+ let idFieldFor = command => Stdlib_Option.flatMap(writable.commands.find(c => c.command === command), c => c.aggregateIdField);
700
+ let observations = observe(c.scenarios, map, idFieldFor);
701
+ let commands = deriveCommands(c.component, observations, labelled);
702
+ commands.forEach(d => {
703
+ if (labelled) {
704
+ compare(plugin, writable, d, findings);
705
+ }
706
+ derived.push(d);
707
+ });
708
+ let why = labelled ? "no scenario exercises the command" : writable.linkedViews.join(", ") + ` declares no lifecycle field, so its scenarios cannot be labelled`;
709
+ writable.commands.forEach(cmd => {
710
+ if (labelled && commands.some(d => d.command === cmd.command)) {
711
+ return;
712
+ } else {
713
+ return allUnverified(cmd, (severity, states, message) => {
714
+ findings.push({
715
+ severity: severity,
716
+ plugin: plugin,
717
+ component: writable.name,
718
+ command: cmd.command,
719
+ states: states,
720
+ message: plugin + `/` + writable.name + `.` + cmd.command + `: ` + message
721
+ });
722
+ }, why);
723
+ }
724
+ });
725
+ });
726
+ return {
727
+ TAG: "Ok",
728
+ _0: derived
729
+ };
730
+ }
731
+
732
+ function byComponentThenCommand(derived) {
733
+ return derived.toSorted((a, b) => {
734
+ let c = Primitive_string.compare(a.component, b.component);
735
+ if (c !== 0) {
736
+ return c;
737
+ } else {
738
+ return Primitive_string.compare(a.command, b.command);
739
+ }
740
+ });
741
+ }
742
+
743
+ function goldenJson(derived) {
744
+ let entries = byComponentThenCommand(derived).map(d => Object.fromEntries([
745
+ [
746
+ "component",
747
+ d.component
748
+ ],
749
+ [
750
+ "command",
751
+ d.command
752
+ ],
753
+ [
754
+ "level",
755
+ d.level
756
+ ],
757
+ [
758
+ "allowedStates",
759
+ d.allowedStates.map(prim => prim)
760
+ ],
761
+ [
762
+ "targets",
763
+ d.targets.map(prim => prim)
764
+ ],
765
+ [
766
+ "scenarios",
767
+ d.scenarios
768
+ ]
769
+ ]));
770
+ return JSON.stringify(entries, undefined, 2) + "\n";
771
+ }
772
+
773
+ function goldenPath(root) {
774
+ return Nodepath.join(root.dir, "schema", "lifecycle-model.json");
775
+ }
776
+
777
+ function reportJson(findings, opaque, derived, failures) {
778
+ let findingJson = f => Object.fromEntries([
779
+ [
780
+ "verdict",
781
+ f.severity
782
+ ],
783
+ [
784
+ "plugin",
785
+ f.plugin
786
+ ],
787
+ [
788
+ "component",
789
+ f.component
790
+ ],
791
+ [
792
+ "command",
793
+ f.command
794
+ ],
795
+ [
796
+ "states",
797
+ f.states.map(prim => prim)
798
+ ],
799
+ [
800
+ "message",
801
+ f.message
802
+ ]
803
+ ]);
804
+ let commandJson = param => {
805
+ let d = param[1];
806
+ return Object.fromEntries([
807
+ [
808
+ "plugin",
809
+ param[0]
810
+ ],
811
+ [
812
+ "component",
813
+ d.component
814
+ ],
815
+ [
816
+ "command",
817
+ d.command
818
+ ],
819
+ [
820
+ "level",
821
+ d.level
822
+ ],
823
+ [
824
+ "allowedStates",
825
+ d.allowedStates.map(prim => prim)
826
+ ],
827
+ [
828
+ "inertStates",
829
+ d.inertStates.map(prim => prim)
830
+ ],
831
+ [
832
+ "targets",
833
+ d.targets.map(prim => prim)
834
+ ],
835
+ [
836
+ "scenarios",
837
+ d.scenarios
838
+ ]
839
+ ]);
840
+ };
841
+ let opaqueJson = o => Object.fromEntries([
842
+ [
843
+ "plugin",
844
+ o.plugin
845
+ ],
846
+ [
847
+ "component",
848
+ o.component
849
+ ],
850
+ [
851
+ "path",
852
+ o.path
853
+ ],
854
+ [
855
+ "scenarios",
856
+ o.scenarios
857
+ ],
858
+ [
859
+ "unreadable",
860
+ o.unreadable
861
+ ]
862
+ ]);
863
+ return JSON.stringify(Object.fromEntries([
864
+ [
865
+ "version",
866
+ 1
867
+ ],
868
+ [
869
+ "findings",
870
+ findings.map(findingJson)
871
+ ],
872
+ [
873
+ "commands",
874
+ derived.map(commandJson)
875
+ ],
876
+ [
877
+ "opaque",
878
+ opaque.map(opaqueJson)
879
+ ],
880
+ [
881
+ "unreadable",
882
+ failures.map(prim => prim)
883
+ ]
884
+ ]), undefined, 2) + "\n";
885
+ }
886
+
887
+ function modelSource(plugin, derived) {
888
+ let saysSomething = d => {
889
+ if (d.level !== "" || d.allowedStates.length !== 0) {
890
+ return true;
891
+ } else {
892
+ return d.targets.length !== 0;
893
+ }
894
+ };
895
+ let quoted = xs => "[" + xs.map(s => `"` + s + `"`).join(", ") + "]";
896
+ let entries = byComponentThenCommand(derived.filter(saysSomething)).map(d => {
897
+ let match = d.level;
898
+ let level;
899
+ switch (match) {
900
+ case "Collection" :
901
+ case "Instance" :
902
+ level = `level: Reventless.Plugin.` + d.level + `, `;
903
+ break;
904
+ default:
905
+ level = "";
906
+ }
907
+ return ` {component: "` + d.component + `", command: "` + d.command + `", ` + level + (`allowedStates: ` + quoted(d.allowedStates) + `, targets: ` + quoted(d.targets) + `},`);
908
+ });
909
+ return [
910
+ [
911
+ `// AUTO-GENERATED — do not edit. Run \`pnpm run check:lifecycle:update\` to update.`,
912
+ `//`,
913
+ `// What ` + plugin + `'s own given/when/then scenarios say about each command: the`,
914
+ `// states one shows it taking effect from, the states those land in, and whether`,
915
+ `// it brings a row into existence. \`Plugin_Structure\` prefers this to the`,
916
+ `// \`@transition\` annotation where it says anything, and falls back to the`,
917
+ `// annotation where it is silent.`,
918
+ ``,
919
+ `let model: array<Reventless.Plugin.derivedEdge> = [`
920
+ ],
921
+ entries,
922
+ [
923
+ "]",
924
+ ""
925
+ ]
926
+ ].flat().join("\n");
927
+ }
928
+
929
+ function modelPath(pluginDir) {
930
+ return Nodepath.join(pluginDir, "src", "LifecycleModel.res");
931
+ }
932
+
933
+ function writeOrCompare(path, actual, label, drifted) {
934
+ let existed = Nodefs.existsSync(path);
935
+ if (update || !existed) {
936
+ Nodefs.writeFileSync(path, actual, "utf8");
937
+ console.log((
938
+ existed ? "updated" : "wrote"
939
+ ) + ` ` + label);
940
+ return;
941
+ } else if (Nodefs.readFileSync(path, "utf8") !== actual) {
942
+ drifted.push(label);
943
+ console.error(`\ndrift in ` + label);
944
+ return;
945
+ } else {
946
+ return;
947
+ }
948
+ }
949
+
950
+ async function main() {
951
+ let findings = [];
952
+ let opaque = [];
953
+ let failures = [];
954
+ let drifted = [];
955
+ let allDerived = [];
956
+ let allPluginDirs = roots.flatMap(root => pluginDirsIn(root.dir));
957
+ if (allPluginDirs.length === 0) {
958
+ console.error(`no plugins found under ` + roots.map(r => r.dir).join(", ") + ` — a plugin is a directory with both src/Plugin.res and tests/`);
959
+ process.exit(1);
960
+ }
961
+ let msg = reuseSidecars ? checkSidecars(allPluginDirs) : emitSidecars(allPluginDirs);
962
+ if (msg.TAG !== "Ok") {
963
+ console.error(msg._0);
964
+ process.exit(1);
965
+ }
966
+ if (!hasCorpus(allPluginDirs)) {
967
+ console.error(`no scenario sidecar exists under ` + roots.map(r => r.dir).join(", ") + ` after the build. Build that tree with REVENTLESS_EMIT_SIDECAR=1 and pass --reuse-sidecars.`);
968
+ process.exit(1);
969
+ }
970
+ for (let i = 0, i_finish = roots.length; i < i_finish; ++i) {
971
+ let root = roots[i];
972
+ if (root !== undefined) {
973
+ let example = root.label;
974
+ let exampleDir = root.dir;
975
+ let derived = [];
976
+ let dirs = pluginDirsIn(exampleDir);
977
+ for (let j = 0, j_finish = dirs.length; j < j_finish; ++j) {
978
+ let pluginDir = dirs[j];
979
+ if (pluginDir !== undefined) {
980
+ let plugin = Nodepath.basename(pluginDir);
981
+ let qualified = example + `/` + plugin;
982
+ let commands = await runPlugin(qualified, pluginDir, findings, opaque);
983
+ if (commands.TAG === "Ok") {
984
+ let commands$1 = commands._0;
985
+ commands$1.forEach(c => {
986
+ derived.push(c);
987
+ allDerived.push([
988
+ qualified,
989
+ c
990
+ ]);
991
+ });
992
+ if (!json) {
993
+ writeOrCompare(modelPath(pluginDir), modelSource(plugin, commands$1), example + `/` + plugin + `/src/LifecycleModel.res`, drifted);
994
+ }
995
+ } else {
996
+ failures.push(example + `/` + plugin + `: ` + commands._0);
997
+ }
998
+ }
999
+ }
1000
+ if (dirs.length !== 0 && !json) {
1001
+ let dir = Nodepath.join(exampleDir, "schema");
1002
+ if (!Nodefs.existsSync(dir)) {
1003
+ Nodefs.mkdirSync(dir, {
1004
+ recursive: true
1005
+ });
1006
+ }
1007
+ writeOrCompare(goldenPath(root), goldenJson(derived), example + `/schema/lifecycle-model.json`, drifted);
1008
+ console.log(`ok ` + example + ` — ` + derived.length.toString() + ` commands derived from scenarios`);
1009
+ }
1010
+ }
1011
+ }
1012
+ let of_ = severity => findings.filter(f => f.severity === severity);
1013
+ let contradicted = of_("contradicted");
1014
+ if (json) {
1015
+ console.log(reportJson(findings, opaque, allDerived, failures));
1016
+ } else {
1017
+ [
1018
+ "contradicted",
1019
+ "unverified",
1020
+ "undeclared",
1021
+ "level",
1022
+ "ambiguous"
1023
+ ].forEach(severity => {
1024
+ let group = of_(severity);
1025
+ if (group.length !== 0) {
1026
+ console.log(`\n` + severity + ` (` + group.length.toString() + `)`);
1027
+ group.forEach(f => {
1028
+ console.log(` ` + f.message);
1029
+ });
1030
+ return;
1031
+ }
1032
+ });
1033
+ }
1034
+ if (failures.length !== 0 && !json) {
1035
+ console.error(`\ncould not read:`);
1036
+ failures.forEach(f => {
1037
+ console.error(` ` + f);
1038
+ });
1039
+ }
1040
+ if (drifted.length !== 0) {
1041
+ console.error(`\n` + drifted.length.toString() + ` lifecycle artifact(s) changed. If the change is intended, re-run with --update and commit them alongside the change that moved them.`);
1042
+ }
1043
+ if (contradicted.length !== 0 || drifted.length !== 0 || failures.length !== 0) {
1044
+ process.exit(1);
1045
+ return;
1046
+ }
1047
+ }
1048
+
1049
+ main();
1050
+
1051
+ export {
1052
+ repoRoot,
1053
+ examplesDir,
1054
+ update,
1055
+ flagValues,
1056
+ reuseSidecars,
1057
+ json,
1058
+ noRow,
1059
+ asObj,
1060
+ getStr,
1061
+ getArr,
1062
+ getObjs,
1063
+ getStrs,
1064
+ getStrsOpt,
1065
+ last,
1066
+ lastSegment,
1067
+ sortedUnique,
1068
+ valuesOf,
1069
+ elementsOf,
1070
+ kindOf,
1071
+ scenarioOf,
1072
+ readCorpus,
1073
+ filesUnder,
1074
+ gwtSources,
1075
+ sidecarOf,
1076
+ hasCorpus,
1077
+ checkSidecars,
1078
+ emitSidecars,
1079
+ levelOf,
1080
+ declaredCommandOf,
1081
+ declaredOf,
1082
+ loadPluginStructure,
1083
+ readDeclared,
1084
+ lifecycleValue,
1085
+ lifecycleMapFor,
1086
+ outcomeOf,
1087
+ valueOf,
1088
+ sameRow,
1089
+ observe,
1090
+ deriveCommands,
1091
+ allUnverified,
1092
+ compare,
1093
+ pluginDirsIn,
1094
+ roots,
1095
+ isViewPath,
1096
+ isWritablePath,
1097
+ runPlugin,
1098
+ byComponentThenCommand,
1099
+ goldenJson,
1100
+ goldenPath,
1101
+ reportJson,
1102
+ modelSource,
1103
+ modelPath,
1104
+ writeOrCompare,
1105
+ main,
1106
+ }
1107
+ /* Not a pure module */