@reventlessdev/reventless-spec 3.0.0-alpha.142 → 3.0.0-alpha.144
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/CHANGELOG.md +21 -0
- package/package.json +2 -2
- package/run-certify-trait.mjs +2 -1
- package/run-generator.mjs +2 -1
- package/run-graft-trait.mjs +2 -1
- package/run-platform-generator.mjs +2 -1
- package/run-trait-manifest.mjs +2 -1
- package/src/CliArgs.res +209 -0
- package/src/CliArgs.res.mjs +325 -0
- package/src/generator/CertifyTrait.res +96 -93
- package/src/generator/CertifyTrait.res.mjs +92 -75
- package/src/generator/GraftTrait.res +147 -148
- package/src/generator/GraftTrait.res.mjs +131 -128
- package/src/generator/PlatformGenerator.res +50 -31
- package/src/generator/PlatformGenerator.res.mjs +107 -76
- package/src/generator/PluginGenerator.res +29 -29
- package/src/generator/PluginGenerator.res.mjs +87 -55
- package/src/generator/TraitManifestCli.res +69 -68
- package/src/generator/TraitManifestCli.res.mjs +67 -51
- package/src/lifecycle/CheckLifecycleModel.res +203 -189
- package/src/lifecycle/CheckLifecycleModel.res.mjs +146 -141
- package/src/types/PrepareAccounts.res +38 -57
- package/src/types/PrepareAccounts.res.mjs +28 -82
|
@@ -71,25 +71,6 @@ NodeProcess.env->Dict.set("REVENTLESS_DECLARED_TRANSITIONS_ONLY", "1")
|
|
|
71
71
|
|
|
72
72
|
let repoRoot = NodeProcess.cwd()
|
|
73
73
|
let examplesDir = NodePath.join([repoRoot, "examples"])
|
|
74
|
-
let update = NodeProcess.argv->Array.includes("--update")
|
|
75
|
-
|
|
76
|
-
/** Every `<flag> <value>` pair on the command line, so a flag can be repeated.
|
|
77
|
-
|
|
78
|
-
A value that looks like another flag is not consumed, so `--root --json`
|
|
79
|
-
reports no roots rather than silently checking a directory named `--json`. */
|
|
80
|
-
let flagValues = (flag: string): array<string> => {
|
|
81
|
-
let argv = NodeProcess.argv
|
|
82
|
-
let out = []
|
|
83
|
-
for i in 0 to Array.length(argv) - 1 {
|
|
84
|
-
if argv->Array.get(i) == Some(flag) {
|
|
85
|
-
switch argv->Array.get(i + 1) {
|
|
86
|
-
| Some(value) if !(value->String.startsWith("--")) => out->Array.push(value)->ignore
|
|
87
|
-
| _ => ()
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
out
|
|
92
|
-
}
|
|
93
74
|
|
|
94
75
|
/** An app whose plugins are checked together, and the name it is reported under.
|
|
95
76
|
|
|
@@ -97,28 +78,51 @@ let flagValues = (flag: string): array<string> => {
|
|
|
97
78
|
other plugins it shares an event log with. */
|
|
98
79
|
type appRoot = {label: string, dir: string}
|
|
99
80
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
81
|
+
/** The command line.
|
|
82
|
+
|
|
83
|
+
`reuseSidecars` reads the sidecars a prior build already wrote instead of
|
|
84
|
+
driving one. CI's build step sets `REVENTLESS_EMIT_SIDECAR=1`, so by the time
|
|
85
|
+
this runs the corpus is on disk; a second pass over a warm tree buys nothing
|
|
86
|
+
and costs the multi-root build chain's habit of cleaning artifacts outside the
|
|
87
|
+
root it is building, which lands intermittently on a stale `.cmi`.
|
|
88
|
+
|
|
89
|
+
`json` reports the run as one JSON document on stdout instead of the grouped
|
|
90
|
+
prose, for a consumer that has to place a finding somewhere — an editor
|
|
91
|
+
putting a squiggle on the arm that made the claim — rather than read it.
|
|
92
|
+
Artifacts are neither written nor compared under it: a reader asking what the
|
|
93
|
+
corpus says now must not, as a side effect, rewrite what the repository says
|
|
94
|
+
it said — nor fail because the two have drifted. A contradiction still exits
|
|
95
|
+
non-zero, so this composes with a gate. */
|
|
96
|
+
type args = {
|
|
97
|
+
update: bool,
|
|
98
|
+
roots: array<string>,
|
|
99
|
+
reuseSidecars: bool,
|
|
100
|
+
json: bool,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let usage = `Usage: check-lifecycle [--root <dir>]... [--update] [--reuse-sidecars] [--json]
|
|
104
|
+
|
|
105
|
+
--root <dir> an app whose plugins are checked together; repeatable.
|
|
106
|
+
Without one, every directory under ./examples
|
|
107
|
+
--update rewrite the goldens and the models instead of comparing
|
|
108
|
+
--reuse-sidecars read the scenario sidecars a prior build wrote instead of
|
|
109
|
+
building with REVENTLESS_EMIT_SIDECAR=1
|
|
110
|
+
--json report as one JSON document on stdout; writes and
|
|
111
|
+
compares nothing
|
|
112
|
+
|
|
113
|
+
Contradictions, drift and unreadable plugins exit 1.`
|
|
114
|
+
|
|
115
|
+
let parseArgs = (argv: array<string>): result<args, string> =>
|
|
116
|
+
CliArgs.parse(~bools=["update", "reuse-sidecars", "json"], ~lists=["root"], argv)
|
|
117
|
+
->Result.flatMap(CliArgs.noPositionals)
|
|
118
|
+
->Result.map(a => {
|
|
119
|
+
update: a->CliArgs.bool("update"),
|
|
120
|
+
roots: a->CliArgs.strings("root"),
|
|
121
|
+
reuseSidecars: a->CliArgs.bool("reuse-sidecars"),
|
|
122
|
+
json: a->CliArgs.bool("json"),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
let cli: CliArgs.cli<args> = {bin: "check-lifecycle", usage, parse: parseArgs}
|
|
122
126
|
|
|
123
127
|
/** The label a state carries when no row exists yet. Not a lifecycle case — no
|
|
124
128
|
enum declares it — so it is spelled in a way no constructor can name, and a
|
|
@@ -978,32 +982,24 @@ let pluginDirsIn = (exampleDir: string): array<string> =>
|
|
|
978
982
|
get. A `--root` is resolved against the working directory so a relative one
|
|
979
983
|
means what the person who typed it meant, and labelled by its basename so the
|
|
980
984
|
prose reads the same either way. */
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
| []
|
|
992
|
-
|
|
993
|
-
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
->Array.map(name => {label: name, dir: NodePath.join([examplesDir, name])})
|
|
999
|
-
| exception _ => []
|
|
985
|
+
let rootsOf = (given: array<string>): array<appRoot> =>
|
|
986
|
+
switch given {
|
|
987
|
+
| [] =>
|
|
988
|
+
switch NodeFs.readdirSync(examplesDir, {withFileTypes: true}) {
|
|
989
|
+
| entries =>
|
|
990
|
+
entries
|
|
991
|
+
->Array.filter(e => e->NodeFs.isDirectory)
|
|
992
|
+
->Array.map(e => e->NodeFs.direntName)
|
|
993
|
+
->Array.toSorted(String.compare)
|
|
994
|
+
->Array.map(name => {label: name, dir: NodePath.join([examplesDir, name])})
|
|
995
|
+
| exception _ => []
|
|
996
|
+
}
|
|
997
|
+
| given =>
|
|
998
|
+
given->Array.map(given => {
|
|
999
|
+
let dir = NodePath.resolve([given])
|
|
1000
|
+
{label: NodePath.basename(dir), dir}
|
|
1001
|
+
})
|
|
1000
1002
|
}
|
|
1001
|
-
| given =>
|
|
1002
|
-
given->Array.map(given => {
|
|
1003
|
-
let dir = NodePath.resolve([given])
|
|
1004
|
-
{label: NodePath.basename(dir), dir}
|
|
1005
|
-
})
|
|
1006
|
-
}
|
|
1007
1003
|
|
|
1008
1004
|
/** The kind a sidecar's source folder names, read through `ComponentKind` — the
|
|
1009
1005
|
one vocabulary the plugin generator and the PPX already classify a folder by,
|
|
@@ -1363,7 +1359,13 @@ let modelPath = (~pluginDir: string) => NodePath.join([pluginDir, "src", "Lifecy
|
|
|
1363
1359
|
/** Rewrite under `--update`, and when nothing is there yet so a plugin harvested
|
|
1364
1360
|
for the first time is not a failure. Otherwise compare, and record the drift:
|
|
1365
1361
|
a derivation that moved belongs in the diff of the change that moved it. */
|
|
1366
|
-
let writeOrCompare = (
|
|
1362
|
+
let writeOrCompare = (
|
|
1363
|
+
~update: bool,
|
|
1364
|
+
~path: string,
|
|
1365
|
+
~actual: string,
|
|
1366
|
+
~label: string,
|
|
1367
|
+
~drifted: array<string>,
|
|
1368
|
+
) => {
|
|
1367
1369
|
let existed = path->NodeFs.existsSync
|
|
1368
1370
|
if update || !existed {
|
|
1369
1371
|
NodeFs.writeFileSync(path, actual)
|
|
@@ -1376,142 +1378,154 @@ let writeOrCompare = (~path: string, ~actual: string, ~label: string, ~drifted:
|
|
|
1376
1378
|
|
|
1377
1379
|
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
1378
1380
|
|
|
1379
|
-
let main =
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1381
|
+
let main = () =>
|
|
1382
|
+
CliArgs.run(cli, async ({update, roots, reuseSidecars, json}) => {
|
|
1383
|
+
let roots = rootsOf(roots)
|
|
1384
|
+
let findings = []
|
|
1385
|
+
let opaque = []
|
|
1386
|
+
let outcomes = []
|
|
1387
|
+
let failures = []
|
|
1388
|
+
let drifted = []
|
|
1389
|
+
let allDerived = []
|
|
1390
|
+
|
|
1391
|
+
let allPluginDirs = roots->Array.flatMap(root => pluginDirsIn(root.dir))
|
|
1392
|
+
|
|
1393
|
+
// A run that found nothing to check is a mistyped `--root` far more often than
|
|
1394
|
+
// an app with no plugins, and reporting "ok" for it is how that typo survives.
|
|
1395
|
+
if Array.length(allPluginDirs) == 0 {
|
|
1396
|
+
Console.error(
|
|
1397
|
+
`no plugins found under ${roots
|
|
1398
|
+
->Array.map(r => r.dir)
|
|
1399
|
+
->Array.join(
|
|
1400
|
+
", ",
|
|
1401
|
+
)} — a plugin is a ` ++ `directory with both src/Plugin.res and tests/`,
|
|
1402
|
+
)
|
|
1403
|
+
NodeProcess.exit(1)
|
|
1404
|
+
}
|
|
1399
1405
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1406
|
+
switch reuseSidecars
|
|
1407
|
+
? checkSidecars(~pluginDirs=allPluginDirs)
|
|
1408
|
+
: emitSidecars(~pluginDirs=allPluginDirs) {
|
|
1409
|
+
| Error(msg) =>
|
|
1410
|
+
Console.error(msg)
|
|
1411
|
+
NodeProcess.exit(1)
|
|
1412
|
+
| Ok() => ()
|
|
1413
|
+
}
|
|
1408
1414
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1415
|
+
// The build above is the working directory's, so a `--root` elsewhere can come
|
|
1416
|
+
// back successful having emitted nothing for the tree actually being checked.
|
|
1417
|
+
// An empty corpus reads as every edge unverified — a warning — so without this
|
|
1418
|
+
// the run would pass having checked nothing, which is the one outcome worth
|
|
1419
|
+
// refusing outright.
|
|
1420
|
+
if !hasCorpus(~pluginDirs=allPluginDirs) {
|
|
1421
|
+
Console.error(
|
|
1422
|
+
`no scenario sidecar exists under ${roots
|
|
1423
|
+
->Array.map(r => r.dir)
|
|
1424
|
+
->Array.join(
|
|
1425
|
+
", ",
|
|
1426
|
+
)} after ` ++ `the build. Build that tree with REVENTLESS_EMIT_SIDECAR=1 and pass --reuse-sidecars.`,
|
|
1427
|
+
)
|
|
1428
|
+
NodeProcess.exit(1)
|
|
1429
|
+
}
|
|
1424
1430
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1431
|
+
for i in 0 to Array.length(roots) - 1 {
|
|
1432
|
+
switch roots->Array.get(i) {
|
|
1433
|
+
| None => ()
|
|
1434
|
+
| Some(root) =>
|
|
1435
|
+
let example = root.label
|
|
1436
|
+
let exampleDir = root.dir
|
|
1437
|
+
let derived = []
|
|
1438
|
+
let dirs = pluginDirsIn(exampleDir)
|
|
1439
|
+
|
|
1440
|
+
for j in 0 to Array.length(dirs) - 1 {
|
|
1441
|
+
switch dirs->Array.get(j) {
|
|
1442
|
+
| None => ()
|
|
1443
|
+
| Some(pluginDir) =>
|
|
1444
|
+
let plugin = NodePath.basename(pluginDir)
|
|
1445
|
+
let qualified = `${example}/${plugin}`
|
|
1446
|
+
switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque, ~outcomes) {
|
|
1447
|
+
| Ok(commands) =>
|
|
1448
|
+
commands->Array.forEach(c => {
|
|
1449
|
+
derived->Array.push(c)
|
|
1450
|
+
allDerived->Array.push((qualified, c))
|
|
1451
|
+
})
|
|
1452
|
+
if !json {
|
|
1453
|
+
writeOrCompare(
|
|
1454
|
+
~update,
|
|
1455
|
+
~path=modelPath(~pluginDir),
|
|
1456
|
+
~actual=modelSource(~plugin, ~derived=commands),
|
|
1457
|
+
~label=`${example}/${plugin}/src/LifecycleModel.res`,
|
|
1458
|
+
~drifted,
|
|
1459
|
+
)
|
|
1460
|
+
}
|
|
1461
|
+
| Error(msg) => failures->Array.push(`${example}/${plugin}: ${msg}`)->ignore
|
|
1453
1462
|
}
|
|
1454
|
-
| Error(msg) => failures->Array.push(`${example}/${plugin}: ${msg}`)->ignore
|
|
1455
1463
|
}
|
|
1456
1464
|
}
|
|
1457
|
-
}
|
|
1458
1465
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1466
|
+
if Array.length(dirs) > 0 && !json {
|
|
1467
|
+
let dir = NodePath.join([exampleDir, "schema"])
|
|
1468
|
+
if !(dir->NodeFs.existsSync) {
|
|
1469
|
+
NodeFs.mkdirSync(dir, {recursive: true})
|
|
1470
|
+
}
|
|
1471
|
+
writeOrCompare(
|
|
1472
|
+
~update,
|
|
1473
|
+
~path=goldenPath(~root),
|
|
1474
|
+
~actual=goldenJson(derived),
|
|
1475
|
+
~label=`${example}/schema/lifecycle-model.json`,
|
|
1476
|
+
~drifted,
|
|
1477
|
+
)
|
|
1478
|
+
Console.log(
|
|
1479
|
+
`ok ${example} — ${Array.length(
|
|
1480
|
+
derived,
|
|
1481
|
+
)->Int.toString} commands derived from scenarios`,
|
|
1482
|
+
)
|
|
1463
1483
|
}
|
|
1464
|
-
writeOrCompare(
|
|
1465
|
-
~path=goldenPath(~root),
|
|
1466
|
-
~actual=goldenJson(derived),
|
|
1467
|
-
~label=`${example}/schema/lifecycle-model.json`,
|
|
1468
|
-
~drifted,
|
|
1469
|
-
)
|
|
1470
|
-
Console.log(
|
|
1471
|
-
`ok ${example} — ${Array.length(
|
|
1472
|
-
derived,
|
|
1473
|
-
)->Int.toString} commands derived from scenarios`,
|
|
1474
|
-
)
|
|
1475
1484
|
}
|
|
1476
1485
|
}
|
|
1477
|
-
}
|
|
1478
1486
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
if json {
|
|
1483
|
-
Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~outcomes, ~failures))
|
|
1484
|
-
} else {
|
|
1485
|
-
["contradicted", "unverified", "undeclared", "level", "ambiguous"]->Array.forEach(severity => {
|
|
1486
|
-
let group = of_(severity)
|
|
1487
|
-
if Array.length(group) > 0 {
|
|
1488
|
-
Console.log(`\n${severity} (${Array.length(group)->Int.toString})`)
|
|
1489
|
-
group->Array.forEach(f => Console.log(` ${f.message}`))
|
|
1490
|
-
}
|
|
1491
|
-
})
|
|
1492
|
-
}
|
|
1487
|
+
let of_ = severity => findings->Array.filter(f => f.severity == severity)
|
|
1488
|
+
let contradicted = of_("contradicted")
|
|
1493
1489
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1490
|
+
if json {
|
|
1491
|
+
Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~outcomes, ~failures))
|
|
1492
|
+
} else {
|
|
1493
|
+
[
|
|
1494
|
+
"contradicted",
|
|
1495
|
+
"unverified",
|
|
1496
|
+
"undeclared",
|
|
1497
|
+
"level",
|
|
1498
|
+
"ambiguous",
|
|
1499
|
+
]->Array.forEach(severity => {
|
|
1500
|
+
let group = of_(severity)
|
|
1501
|
+
if Array.length(group) > 0 {
|
|
1502
|
+
Console.log(`\n${severity} (${Array.length(group)->Int.toString})`)
|
|
1503
|
+
group->Array.forEach(f => Console.log(` ${f.message}`))
|
|
1504
|
+
}
|
|
1505
|
+
})
|
|
1506
|
+
}
|
|
1498
1507
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
)->Int.toString} lifecycle artifact(s) changed. If the change is ` ++ `intended, re-run with --update and commit them alongside the change that moved them.`,
|
|
1504
|
-
)
|
|
1505
|
-
}
|
|
1508
|
+
if Array.length(failures) > 0 && !json {
|
|
1509
|
+
Console.error(`\ncould not read:`)
|
|
1510
|
+
failures->Array.forEach(f => Console.error(` ${f}`))
|
|
1511
|
+
}
|
|
1506
1512
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1513
|
+
if Array.length(drifted) > 0 {
|
|
1514
|
+
Console.error(
|
|
1515
|
+
`\n${Array.length(
|
|
1516
|
+
drifted,
|
|
1517
|
+
)->Int.toString} lifecycle artifact(s) changed. If the change is ` ++ `intended, re-run with --update and commit them alongside the change that moved them.`,
|
|
1518
|
+
)
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Warnings do not fail the build: an unverified edge is a corpus that has not
|
|
1522
|
+
// caught up, which is a thing to work on rather than a thing to stop for. A
|
|
1523
|
+
// contradiction is a disagreement between two statements about the same
|
|
1524
|
+
// command, and one of them is wrong.
|
|
1525
|
+
if Array.length(contradicted) > 0 || Array.length(drifted) > 0 || Array.length(failures) > 0 {
|
|
1526
|
+
NodeProcess.exit(1)
|
|
1527
|
+
}
|
|
1528
|
+
})
|
|
1515
1529
|
|
|
1516
1530
|
// 🚨 **No top-level call.** `../../run-check-lifecycle.mjs` invokes [main]; this
|
|
1517
1531
|
// module only defines it. While the call was here, importing the module *ran the
|