@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.
@@ -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
- /** Read the sidecars a prior build already wrote instead of driving one. CI's
101
- build step sets `REVENTLESS_EMIT_SIDECAR=1`, so by the time this runs the
102
- corpus is on disk; a second pass over a warm tree buys nothing and costs the
103
- multi-root build chain's habit of cleaning artifacts outside the root it is
104
- building, which lands intermittently on a stale `.cmi`. */
105
- let reuseSidecars =
106
- NodeProcess.argv->Array.includes("--reuse-sidecars")
107
-
108
- /** Report the run as one JSON document on stdout instead of the grouped prose.
109
-
110
- For a consumer that has to place a finding somewhere — an editor putting a
111
- squiggle on the arm that made the claim — rather than read it. The prose
112
- bakes component, command and state into a sentence; this keeps them as
113
- fields, so the consumer does not parse English back into a range.
114
-
115
- Artifacts are neither written nor compared under `--json`: a reader asking
116
- what the corpus says now must not, as a side effect, rewrite what the
117
- repository says it said — nor fail because the two have drifted, which is a
118
- fact about the repository rather than about the corpus it was asked to
119
- report. A contradiction still exits non-zero, so this composes with a gate. */
120
- let json =
121
- NodeProcess.argv->Array.includes("--json")
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
- // `--root` typed with nothing usable after it — `--root --json`, or a trailing
982
- // `--root`. Falling back to the default scan below would check the whole examples
983
- // tree while the person believed they had narrowed it to one app, so refuse here
984
- // rather than answer a question nobody asked.
985
- if NodeProcess.argv->Array.includes("--root") && Array.length(flagValues("--root")) == 0 {
986
- Console.error("--root needs a directory after it")
987
- NodeProcess.exit(1)
988
- }
989
-
990
- let roots: array<appRoot> = switch flagValues("--root") {
991
- | [] =>
992
- switch NodeFs.readdirSync(examplesDir, {withFileTypes: true}) {
993
- | entries =>
994
- entries
995
- ->Array.filter(e => e->NodeFs.isDirectory)
996
- ->Array.map(e => e->NodeFs.direntName)
997
- ->Array.toSorted(String.compare)
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 = (~path: string, ~actual: string, ~label: string, ~drifted: array<string>) => {
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 = async () => {
1380
- let findings = []
1381
- let opaque = []
1382
- let outcomes = []
1383
- let failures = []
1384
- let drifted = []
1385
- let allDerived = []
1386
-
1387
- let allPluginDirs = roots->Array.flatMap(root => pluginDirsIn(root.dir))
1388
-
1389
- // A run that found nothing to check is a mistyped `--root` far more often than
1390
- // an app with no plugins, and reporting "ok" for it is how that typo survives.
1391
- if Array.length(allPluginDirs) == 0 {
1392
- Console.error(
1393
- `no plugins found under ${roots
1394
- ->Array.map(r => r.dir)
1395
- ->Array.join(", ")} — a plugin is a ` ++ `directory with both src/Plugin.res and tests/`,
1396
- )
1397
- NodeProcess.exit(1)
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
- switch reuseSidecars
1401
- ? checkSidecars(~pluginDirs=allPluginDirs)
1402
- : emitSidecars(~pluginDirs=allPluginDirs) {
1403
- | Error(msg) =>
1404
- Console.error(msg)
1405
- NodeProcess.exit(1)
1406
- | Ok() => ()
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
- // The build above is the working directory's, so a `--root` elsewhere can come
1410
- // back successful having emitted nothing for the tree actually being checked.
1411
- // An empty corpus reads as every edge unverified — a warning — so without this
1412
- // the run would pass having checked nothing, which is the one outcome worth
1413
- // refusing outright.
1414
- if !hasCorpus(~pluginDirs=allPluginDirs) {
1415
- Console.error(
1416
- `no scenario sidecar exists under ${roots
1417
- ->Array.map(r => r.dir)
1418
- ->Array.join(
1419
- ", ",
1420
- )} after ` ++ `the build. Build that tree with REVENTLESS_EMIT_SIDECAR=1 and pass --reuse-sidecars.`,
1421
- )
1422
- NodeProcess.exit(1)
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
- for i in 0 to Array.length(roots) - 1 {
1426
- switch roots->Array.get(i) {
1427
- | None => ()
1428
- | Some(root) =>
1429
- let example = root.label
1430
- let exampleDir = root.dir
1431
- let derived = []
1432
- let dirs = pluginDirsIn(exampleDir)
1433
-
1434
- for j in 0 to Array.length(dirs) - 1 {
1435
- switch dirs->Array.get(j) {
1436
- | None => ()
1437
- | Some(pluginDir) =>
1438
- let plugin = NodePath.basename(pluginDir)
1439
- let qualified = `${example}/${plugin}`
1440
- switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque, ~outcomes) {
1441
- | Ok(commands) =>
1442
- commands->Array.forEach(c => {
1443
- derived->Array.push(c)
1444
- allDerived->Array.push((qualified, c))
1445
- })
1446
- if !json {
1447
- writeOrCompare(
1448
- ~path=modelPath(~pluginDir),
1449
- ~actual=modelSource(~plugin, ~derived=commands),
1450
- ~label=`${example}/${plugin}/src/LifecycleModel.res`,
1451
- ~drifted,
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
- if Array.length(dirs) > 0 && !json {
1460
- let dir = NodePath.join([exampleDir, "schema"])
1461
- if !(dir->NodeFs.existsSync) {
1462
- NodeFs.mkdirSync(dir, {recursive: true})
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
- let of_ = severity => findings->Array.filter(f => f.severity == severity)
1480
- let contradicted = of_("contradicted")
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
- if Array.length(failures) > 0 && !json {
1495
- Console.error(`\ncould not read:`)
1496
- failures->Array.forEach(f => Console.error(` ${f}`))
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
- if Array.length(drifted) > 0 {
1500
- Console.error(
1501
- `\n${Array.length(
1502
- drifted,
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
- // Warnings do not fail the build: an unverified edge is a corpus that has not
1508
- // caught up, which is a thing to work on rather than a thing to stop for. A
1509
- // contradiction is a disagreement between two statements about the same
1510
- // command, and one of them is wrong.
1511
- if Array.length(contradicted) > 0 || Array.length(drifted) > 0 || Array.length(failures) > 0 {
1512
- NodeProcess.exit(1)
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