@reventlessdev/reventless-spec 3.0.0-alpha.132 → 3.0.0-alpha.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +2 -2
  3. package/src/AnsiStyle.res +0 -1
  4. package/src/LogPrefix.res +4 -13
  5. package/src/components/AutomationSlice.res +1 -5
  6. package/src/components/CapabilityManifest.res +2 -3
  7. package/src/components/DcbDecode.res +6 -2
  8. package/src/components/DcbScopeInference.res +16 -12
  9. package/src/components/DcbTag.res +74 -46
  10. package/src/components/DcbValidation.res +114 -89
  11. package/src/components/DisplayName.res +4 -2
  12. package/src/components/ExtensionPoint.res +0 -1
  13. package/src/components/FieldDefault.res +52 -0
  14. package/src/components/FieldDefault.res.mjs +50 -0
  15. package/src/components/InboundTranslationSlice.res +0 -2
  16. package/src/components/OutboundTranslationSlice.res +10 -8
  17. package/src/components/Owner.res +4 -4
  18. package/src/components/Plugin.res +10 -4
  19. package/src/components/ReadModel.res +0 -1
  20. package/src/components/Reference.res +2 -8
  21. package/src/components/Sensitive.res +4 -4
  22. package/src/components/StateAnnotations.res +4 -2
  23. package/src/components/StateChangeSlice.res +0 -2
  24. package/src/components/StateViewSlice.res +0 -2
  25. package/src/components/TaggedUnion.res +1 -2
  26. package/src/components/Task.res +0 -1
  27. package/src/components/TraitCertificate.res +2 -2
  28. package/src/components/TraitManifest.res +1 -1
  29. package/src/generator/CertifyTrait.res +2 -4
  30. package/src/generator/Codegen.res +77 -68
  31. package/src/generator/Discovery.res +29 -12
  32. package/src/generator/GraftTrait.res +17 -9
  33. package/src/generator/Pairing.res +39 -13
  34. package/src/generator/PlatformCodegen.res +13 -11
  35. package/src/generator/PlatformManifests.res +1 -3
  36. package/src/generator/PluginGenerator.res +0 -1
  37. package/src/generator/TraitManifestCli.res +7 -7
  38. package/src/lifecycle/CheckLifecycleModel.res +173 -145
  39. package/src/lifecycle/CheckLifecycleModel.res.mjs +17 -2
  40. package/src/semantic/Bytes.res +0 -1
  41. package/src/semantic/CalendarDate.res +2 -2
  42. package/src/semantic/Capabilities.res +4 -6
  43. package/src/semantic/Capabilities.res.mjs +9 -11
  44. package/src/semantic/CapabilityNeed.res +4 -5
  45. package/src/semantic/CaptionedImage.res +0 -1
  46. package/src/semantic/Color.res +0 -1
  47. package/src/semantic/Currency.res +189 -170
  48. package/src/semantic/DateRange.res +3 -4
  49. package/src/semantic/DateTime.res +5 -5
  50. package/src/semantic/Duration.res +2 -2
  51. package/src/semantic/Email.res +0 -1
  52. package/src/semantic/FileRef.res +2 -2
  53. package/src/semantic/GeoPoint.res +17 -21
  54. package/src/semantic/Geocoding.res +8 -9
  55. package/src/semantic/Geolocation.res +6 -9
  56. package/src/semantic/ImageRef.res +2 -2
  57. package/src/semantic/MemberRef.res +0 -1
  58. package/src/semantic/Messaging.res +141 -21
  59. package/src/semantic/Messaging.res.mjs +51 -8
  60. package/src/semantic/Money.res +23 -20
  61. package/src/semantic/Offload.res +6 -4
  62. package/src/semantic/Percent.res +3 -3
  63. package/src/semantic/Phone.res +0 -1
  64. package/src/semantic/RowImage.res +4 -3
  65. package/src/semantic/Semantic.res +8 -11
  66. package/src/semantic/StorageRef.res +17 -17
  67. package/src/semantic/Template.res +5 -7
  68. package/src/semantic/UploadableFile.res +0 -1
  69. package/src/semantic/UploadableImage.res +0 -1
  70. package/src/semantic/Url.res +3 -3
  71. package/src/types/Authorization.res +1 -2
  72. package/src/types/Handler.res +0 -1
  73. package/src/types/Identity.res +1 -3
  74. package/src/types/Lifecycle.res +7 -6
  75. package/src/types/Message.res +6 -4
  76. package/src/types/OwnerScope.res +1 -3
  77. package/src/types/Projection.res +9 -2
  78. package/src/types/StoredEvent.res +2 -4
  79. package/src/types/Transition.res +8 -4
  80. package/src/util/Util_Sury.res +1 -4
@@ -48,7 +48,9 @@ let moduleBase = (traitPackage: string) =>
48
48
 
49
49
  let resolveFrom = (specifier: string) =>
50
50
  try Some(
51
- NodeModule.createRequire(NodeProcess.cwd() ++ "/index.js")->NodeModule.requireResolve(specifier),
51
+ NodeModule.createRequire(NodeProcess.cwd() ++ "/index.js")->NodeModule.requireResolve(
52
+ specifier,
53
+ ),
52
54
  ) catch {
53
55
  | _ => None
54
56
  }
@@ -80,8 +82,7 @@ let main = async () => {
80
82
  let packageJson = switch resolveFrom(`${traitPackage}/package.json`) {
81
83
  | None =>
82
84
  fail(
83
- `${traitPackage} is not installed here. A manifest is derived from the trait, so ` ++
84
- `the trait has to be resolvable.`,
85
+ `${traitPackage} is not installed here. A manifest is derived from the trait, so ` ++ `the trait has to be resolvable.`,
85
86
  )
86
87
  JSON.Encode.null
87
88
  | Some(path) => NodeFs.readFileSync(path)->JSON.parseOrThrow
@@ -95,8 +96,7 @@ let main = async () => {
95
96
  | None =>
96
97
  fail(
97
98
  `${traitPackage} exports no ${base} module, so its capability needs cannot be read.\n` ++
98
- ` A trait states them as a value — an empty array if it brokers nothing — because ` ++
99
- `an unstated need fails silently at run time.`,
99
+ ` A trait states them as a value — an empty array if it brokers nothing — because ` ++ `an unstated need fails silently at run time.`,
100
100
  )
101
101
  %raw(`undefined`)
102
102
  | Some(path) => await dynImport(NodeUrl.pathToFileURL(path)["href"])
@@ -127,8 +127,8 @@ let main = async () => {
127
127
  NodeFs.writeFileSync(out, manifest->TraitManifest.render)
128
128
  Console.log(
129
129
  `trait-manifest: ${manifest.trait}@${manifest.version} — ` ++
130
- `${(manifest.capabilities->Array.length)->Int.toString} capabilit(ies), ` ++
131
- `${(manifest.config->Array.length)->Int.toString} config field(s)`,
130
+ `${manifest.capabilities->Array.length->Int.toString} capabilit(ies), ` ++
131
+ `${manifest.config->Array.length->Int.toString} config field(s)`,
132
132
  )
133
133
  Console.log(`Wrote: ${out}`)
134
134
  }
@@ -60,7 +60,6 @@ check-lifecycle --root . --reuse-sidecars --json
60
60
  check-lifecycle --root . --update # write this app's models and golden
61
61
  ```
62
62
  */
63
-
64
63
  // The model this script writes is folded into `pluginStructure`, so by the time
65
64
  // it runs again the "declared" side it reads back would be its own last answer —
66
65
  // every edge confirmed, and a real disagreement between an annotation and the
@@ -103,7 +102,8 @@ type appRoot = {label: string, dir: string}
103
102
  corpus is on disk; a second pass over a warm tree buys nothing and costs the
104
103
  multi-root build chain's habit of cleaning artifacts outside the root it is
105
104
  building, which lands intermittently on a stale `.cmi`. */
106
- let reuseSidecars = NodeProcess.argv->Array.includes("--reuse-sidecars")
105
+ let reuseSidecars =
106
+ NodeProcess.argv->Array.includes("--reuse-sidecars")
107
107
 
108
108
  /** Report the run as one JSON document on stdout instead of the grouped prose.
109
109
 
@@ -117,7 +117,8 @@ let reuseSidecars = NodeProcess.argv->Array.includes("--reuse-sidecars")
117
117
  repository says it said — nor fail because the two have drifted, which is a
118
118
  fact about the repository rather than about the corpus it was asked to
119
119
  report. A contradiction still exits non-zero, so this composes with a gate. */
120
- let json = NodeProcess.argv->Array.includes("--json")
120
+ let json =
121
+ NodeProcess.argv->Array.includes("--json")
121
122
 
122
123
  /** The label a state carries when no row exists yet. Not a lifecycle case — no
123
124
  enum declares it — so it is spelled in a way no constructor can name, and a
@@ -295,8 +296,7 @@ let checkSidecars = (~pluginDirs: array<string>): result<unit, string> =>
295
296
  hasCorpus(~pluginDirs)
296
297
  ? Ok()
297
298
  : Error(
298
- "--reuse-sidecars was passed, but no scenario sidecar exists. Build with " ++
299
- "REVENTLESS_EMIT_SIDECAR=1 first, or drop the flag.",
299
+ "--reuse-sidecars was passed, but no scenario sidecar exists. Build with " ++ "REVENTLESS_EMIT_SIDECAR=1 first, or drop the flag.",
300
300
  )
301
301
 
302
302
  /** Sidecars are gitignored build artifacts, so the harvest produces its own
@@ -405,11 +405,13 @@ let declaredOf = (structure: JSON.t): option<declared> =>
405
405
  getObjs(s, key)->Array.filterMap(w =>
406
406
  w
407
407
  ->getStr("name")
408
- ->Option.map(name => {
409
- name,
410
- linkedViews: getStrs(w, "linkedViews"),
411
- commands: getObjs(w, "commands")->Array.filterMap(declaredCommandOf),
412
- })
408
+ ->Option.map(
409
+ name => {
410
+ name,
411
+ linkedViews: getStrs(w, "linkedViews"),
412
+ commands: getObjs(w, "commands")->Array.filterMap(declaredCommandOf),
413
+ },
414
+ )
413
415
  )
414
416
  let viewsFrom = key =>
415
417
  getObjs(s, key)->Array.filterMap(v =>
@@ -647,9 +649,7 @@ let deriveCommands = (
647
649
  effective->Array.filterMap(o => o.from == noRow ? None : Some(o.from)),
648
650
  ),
649
651
  inertStates: sortedUnique(
650
- mine->Array.filterMap(o =>
651
- o.outcome == Emitted || o.from == noRow ? None : Some(o.from)
652
- ),
652
+ mine->Array.filterMap(o => o.outcome == Emitted || o.from == noRow ? None : Some(o.from)),
653
653
  ),
654
654
  targets: sortedUnique(
655
655
  effective->Array.filterMap(o => o.to == o.from || o.to == noRow ? None : Some(o.to)),
@@ -764,8 +764,7 @@ let compare = (
764
764
  add(
765
765
  "contradicted",
766
766
  [state],
767
- `the switch declares it legal in every state, and a scenario from "${state}" ` ++
768
- `shows it refused or producing nothing`,
767
+ `the switch declares it legal in every state, and a scenario from "${state}" ` ++ `shows it refused or producing nothing`,
769
768
  )
770
769
  )
771
770
  | None =>
@@ -773,8 +772,9 @@ let compare = (
773
772
  add(
774
773
  "undeclared",
775
774
  derived.allowedStates,
776
- `scenarios show it taking effect from ${derived.allowedStates->Array.join(", ")}, ` ++
777
- `and it declares no edge`,
775
+ `scenarios show it taking effect from ${derived.allowedStates->Array.join(
776
+ ", ",
777
+ )}, ` ++ `and it declares no edge`,
778
778
  )
779
779
  }
780
780
  | Some(states) =>
@@ -785,8 +785,7 @@ let compare = (
785
785
  add(
786
786
  "contradicted",
787
787
  [state],
788
- `the switch names "${state}", and a scenario from "${state}" shows it ` ++
789
- `refused or producing nothing`,
788
+ `the switch names "${state}", and a scenario from "${state}" shows it ` ++ `refused or producing nothing`,
790
789
  )
791
790
  } else {
792
791
  add("unverified", [state], `the switch names "${state}", and no scenario starts there`)
@@ -806,8 +805,9 @@ let compare = (
806
805
  add(
807
806
  "unverified",
808
807
  states,
809
- `the switch declares ${Array.length(states)->Int.toString} state(s) and ` ++
810
- `no scenario shows the command taking effect anywhere`,
808
+ `the switch declares ${Array.length(
809
+ states,
810
+ )->Int.toString} state(s) and ` ++ `no scenario shows the command taking effect anywhere`,
811
811
  )
812
812
  }
813
813
  }
@@ -830,8 +830,7 @@ let compare = (
830
830
  add(
831
831
  "contradicted",
832
832
  [target, state],
833
- `the switch targets "${target}", and a scenario lands in "${state}" — ` ++
834
- `the published targetState carries one state, so this edge cannot be expressed`,
833
+ `the switch targets "${target}", and a scenario lands in "${state}" — ` ++ `the published targetState carries one state, so this edge cannot be expressed`,
835
834
  )
836
835
  }
837
836
  )
@@ -933,107 +932,111 @@ let runPlugin = async (
933
932
  switch await readDeclared(~pluginDir) {
934
933
  | Error(msg) => Error(msg)
935
934
  | Ok(declared) =>
936
- let corpora =
937
- filesUnder(NodePath.join([pluginDir, "tests"]), ~suffix=".gwt.json")->Array.filterMap(
938
- readCorpus,
939
- )
940
-
941
- // Every corpus, not only the ones the walk goes on to use: the kinds that
942
- // are unreadable in full — extension points, automation and translation
943
- // slices — are exactly the ones that sit outside the view/writable folders
944
- // below, and they are the ones a coverage UI would otherwise slander.
945
- corpora->Array.forEach(c => {
946
- let unreadable = c.scenarios->Array.filter(s => Array.length(s.whenElements) == 0)
947
- if Array.length(unreadable) > 0 {
948
- opaque
949
- ->Array.push({
950
- plugin,
951
- component: c.component,
952
- path: c.path,
953
- scenarios: Array.length(c.scenarios),
954
- unreadable: Array.length(unreadable),
955
- })
956
- ->ignore
957
- }
958
- })
959
-
960
- // Views first: a command's history cannot be labelled until the events in
961
- // it have somewhere to land.
962
- let mapsByView = Dict.make()
963
- let ambiguities = []
964
- corpora->Array.forEach(c =>
965
- if isViewPath(c.path) {
966
- switch declared.views->Array.find(v => v.name == c.component) {
967
- | Some({lifecycleField: Some(field)}) =>
968
- mapsByView->Dict.set(
969
- c.component,
970
- lifecycleMapFor(~scenarios=c.scenarios, ~field, ~ambiguities, ~view=c.component),
971
- )
972
- // A view with no lifecycle field labels nothing, and that is ordinary
973
- // — most views have no lifecycle at all.
974
- | _ => ()
975
- }
976
- }
935
+ let corpora =
936
+ filesUnder(NodePath.join([pluginDir, "tests"]), ~suffix=".gwt.json")->Array.filterMap(
937
+ readCorpus,
977
938
  )
978
- ambiguities->Array.forEach(((view, message)) =>
979
- findings
939
+
940
+ // Every corpus, not only the ones the walk goes on to use: the kinds that
941
+ // are unreadable in full — extension points, automation and translation
942
+ // slices — are exactly the ones that sit outside the view/writable folders
943
+ // below, and they are the ones a coverage UI would otherwise slander.
944
+ corpora->Array.forEach(c => {
945
+ let unreadable = c.scenarios->Array.filter(s => Array.length(s.whenElements) == 0)
946
+ if Array.length(unreadable) > 0 {
947
+ opaque
980
948
  ->Array.push({
981
- severity: "ambiguous",
982
949
  plugin,
983
- component: view,
984
- command: "",
985
- states: [],
986
- message,
950
+ component: c.component,
951
+ path: c.path,
952
+ scenarios: Array.length(c.scenarios),
953
+ unreadable: Array.length(unreadable),
987
954
  })
988
955
  ->ignore
989
- )
956
+ }
957
+ })
990
958
 
991
- let derived = []
992
- corpora->Array.forEach(c =>
993
- if isWritablePath(c.path) {
994
- switch declared.writables->Array.find(w => w.name == c.component) {
995
- | None => ()
996
- | Some(writable) =>
997
- // The union of the maps of every view this writable feeds. A union
998
- // rather than a single view for the reason the platform's own name
999
- // check uses one: a slice feeding two views is not claiming which of
1000
- // them a state belongs to.
1001
- let map = Dict.make()
1002
- writable.linkedViews->Array.forEach(view =>
1003
- switch mapsByView->Dict.get(view) {
1004
- | Some(m) => m->Dict.forEachWithKey((value, event) => map->Dict.set(event, value))
1005
- | None => ()
1006
- }
1007
- )
1008
- let labelled = Array.length(map->Dict.keysToArray) > 0
1009
- let idFieldFor = command =>
1010
- writable.commands
1011
- ->Array.find(c => c.command == command)
1012
- ->Option.flatMap(c => c.aggregateIdField)
1013
- let observations = observe(~scenarios=c.scenarios, ~map, ~idFieldFor)
1014
- let commands = deriveCommands(~component=c.component, ~observations, ~labelled)
1015
-
1016
- commands->Array.forEach(d => {
1017
- // Without a lifecycle map every history folds to "no row", so
1018
- // there is nothing to confirm a claim against and nothing to
1019
- // contradict it with. Reporting the claim as unverified is the
1020
- // honest answer; running the comparison would manufacture
1021
- // contradictions out of a missing map.
1022
- if labelled {
1023
- compare(~plugin, ~writable, ~derived=d, ~findings)
1024
- }
1025
- derived->Array.push(d)
1026
- })
959
+ // Views first: a command's history cannot be labelled until the events in
960
+ // it have somewhere to land.
961
+ let mapsByView = Dict.make()
962
+ let ambiguities = []
963
+ corpora->Array.forEach(c =>
964
+ if isViewPath(c.path) {
965
+ switch declared.views->Array.find(v => v.name == c.component) {
966
+ | Some({lifecycleField: Some(field)}) =>
967
+ mapsByView->Dict.set(
968
+ c.component,
969
+ lifecycleMapFor(~scenarios=c.scenarios, ~field, ~ambiguities, ~view=c.component),
970
+ )
971
+ // A view with no lifecycle field labels nothing, and that is ordinary
972
+ // — most views have no lifecycle at all.
973
+ | _ => ()
974
+ }
975
+ }
976
+ )
977
+ ambiguities->Array.forEach(((view, message)) =>
978
+ findings
979
+ ->Array.push({
980
+ severity: "ambiguous",
981
+ plugin,
982
+ component: view,
983
+ command: "",
984
+ states: [],
985
+ message,
986
+ })
987
+ ->ignore
988
+ )
989
+
990
+ let derived = []
991
+ corpora->Array.forEach(c =>
992
+ if isWritablePath(c.path) {
993
+ switch declared.writables->Array.find(w => w.name == c.component) {
994
+ | None => ()
995
+ | Some(writable) =>
996
+ // The union of the maps of every view this writable feeds. A union
997
+ // rather than a single view for the reason the platform's own name
998
+ // check uses one: a slice feeding two views is not claiming which of
999
+ // them a state belongs to.
1000
+ let map = Dict.make()
1001
+ writable.linkedViews->Array.forEach(view =>
1002
+ switch mapsByView->Dict.get(view) {
1003
+ | Some(m) => m->Dict.forEachWithKey((value, event) => map->Dict.set(event, value))
1004
+ | None => ()
1005
+ }
1006
+ )
1007
+ let labelled = Array.length(map->Dict.keysToArray) > 0
1008
+ let idFieldFor = command =>
1009
+ writable.commands
1010
+ ->Array.find(c => c.command == command)
1011
+ ->Option.flatMap(c => c.aggregateIdField)
1012
+ let observations = observe(~scenarios=c.scenarios, ~map, ~idFieldFor)
1013
+ let commands = deriveCommands(~component=c.component, ~observations, ~labelled)
1014
+
1015
+ commands->Array.forEach(d => {
1016
+ // Without a lifecycle map every history folds to "no row", so
1017
+ // there is nothing to confirm a claim against and nothing to
1018
+ // contradict it with. Reporting the claim as unverified is the
1019
+ // honest answer; running the comparison would manufacture
1020
+ // contradictions out of a missing map.
1021
+ if labelled {
1022
+ compare(~plugin, ~writable, ~derived=d, ~findings)
1023
+ }
1024
+ derived->Array.push(d)
1025
+ })
1027
1026
 
1028
- let why = labelled
1029
- ? "no scenario exercises the command"
1030
- : `${writable.linkedViews->Array.join(", ")} declares no lifecycle field, so its ` ++
1031
- `scenarios cannot be labelled`
1032
- writable.commands->Array.forEach(cmd =>
1033
- if labelled && commands->Array.some(d => d.command == cmd.command) {
1034
- ()
1035
- } else {
1036
- allUnverified(~cmd, ~why, ~add=(severity, states, message) =>
1027
+ let why = labelled
1028
+ ? "no scenario exercises the command"
1029
+ : `${writable.linkedViews->Array.join(
1030
+ ", ",
1031
+ )} declares no lifecycle field, so its ` ++ `scenarios cannot be labelled`
1032
+ writable.commands->Array.forEach(cmd =>
1033
+ if labelled && commands->Array.some(d => d.command == cmd.command) {
1034
+ ()
1035
+ } else {
1036
+ allUnverified(
1037
+ ~cmd,
1038
+ ~why,
1039
+ ~add=(severity, states, message) =>
1037
1040
  findings
1038
1041
  ->Array.push({
1039
1042
  severity,
@@ -1043,14 +1046,14 @@ let runPlugin = async (
1043
1046
  states,
1044
1047
  message: `${plugin}/${writable.name}.${cmd.command}: ${message}`,
1045
1048
  })
1046
- ->ignore
1047
- )
1048
- }
1049
- )
1050
- }
1049
+ ->ignore,
1050
+ )
1051
+ }
1052
+ )
1051
1053
  }
1052
- )
1053
- Ok(derived)
1054
+ }
1055
+ )
1056
+ Ok(derived)
1054
1057
  }
1055
1058
 
1056
1059
  // ── The golden ──────────────────────────────────────────────────────────────
@@ -1160,6 +1163,19 @@ let reportJson = (
1160
1163
 
1161
1164
  // ── The value structure assembly reads ──────────────────────────────────────
1162
1165
 
1166
+ /** The emitted file is committed ReScript, so it faces the tree's format gate
1167
+ like any other source. Laying the text out by hand would put this script and
1168
+ the formatter in a standing argument only one of them can win — so it emits
1169
+ whatever reads clearly and lets the formatter settle the layout. Raising
1170
+ beats falling back: unformatted source would satisfy this check and fail the
1171
+ format gate, which is the stand-off the call exists to end. */
1172
+ let formatted = (source: string): string =>
1173
+ NodeChildProcess.execFileSync(
1174
+ "pnpm",
1175
+ ["exec", "rescript", "format", "--stdin", ".res"],
1176
+ {cwd: repoRoot, encoding: "utf8", input: source},
1177
+ )
1178
+
1163
1179
  /** The same derivation as a committed ReScript value, so `buildStructure` gets
1164
1180
  the model as data. It cannot read the corpus itself: tests are not published
1165
1181
  with a plugin package, and metadata that read them would make deleting a test
@@ -1173,19 +1189,22 @@ let reportJson = (
1173
1189
  let modelSource = (~plugin: string, ~derived: array<derivedCommand>): string => {
1174
1190
  let saysSomething = (d: derivedCommand) =>
1175
1191
  d.level != "" || Array.length(d.allowedStates) > 0 || Array.length(d.targets) > 0
1176
- let quoted = (xs: array<string>) =>
1177
- "[" ++ xs->Array.map(s => `"${s}"`)->Array.join(", ") ++ "]"
1178
- let entries = derived->Array.filter(saysSomething)->byComponentThenCommand->Array.map(d => {
1179
- // Omitted rather than written as an absent value: `level` is an optional
1180
- // field, and a corpus that could not label this command's histories has
1181
- // nothing to say about it.
1182
- let level = switch d.level {
1183
- | "Collection" | "Instance" => `level: Reventless.Plugin.${d.level}, `
1184
- | _ => ""
1185
- }
1186
- ` {component: "${d.component}", command: "${d.command}", ${level}` ++
1187
- `allowedStates: ${quoted(d.allowedStates)}, targets: ${quoted(d.targets)}},`
1188
- })
1192
+ let quoted = (xs: array<string>) => "[" ++ xs->Array.map(s => `"${s}"`)->Array.join(", ") ++ "]"
1193
+ let entries =
1194
+ derived
1195
+ ->Array.filter(saysSomething)
1196
+ ->byComponentThenCommand
1197
+ ->Array.map(d => {
1198
+ // Omitted rather than written as an absent value: `level` is an optional
1199
+ // field, and a corpus that could not label this command's histories has
1200
+ // nothing to say about it.
1201
+ let level = switch d.level {
1202
+ | "Collection" | "Instance" => `level: Reventless.Plugin.${d.level}, `
1203
+ | _ => ""
1204
+ }
1205
+ ` {component: "${d.component}", command: "${d.command}", ${level}` ++
1206
+ `allowedStates: ${quoted(d.allowedStates)}, targets: ${quoted(d.targets)}},`
1207
+ })
1189
1208
  Array.flat([
1190
1209
  [
1191
1210
  `// AUTO-GENERATED — do not edit. Run \`pnpm run check:lifecycle:update\` to update.`,
@@ -1200,7 +1219,9 @@ let modelSource = (~plugin: string, ~derived: array<derivedCommand>): string =>
1200
1219
  ],
1201
1220
  entries,
1202
1221
  ["]", ""],
1203
- ])->Array.join("\n")
1222
+ ])
1223
+ ->Array.join("\n")
1224
+ ->formatted
1204
1225
  }
1205
1226
 
1206
1227
  let modelPath = (~pluginDir: string) => NodePath.join([pluginDir, "src", "LifecycleModel.res"])
@@ -1234,8 +1255,9 @@ let main = async () => {
1234
1255
  // an app with no plugins, and reporting "ok" for it is how that typo survives.
1235
1256
  if Array.length(allPluginDirs) == 0 {
1236
1257
  Console.error(
1237
- `no plugins found under ${roots->Array.map(r => r.dir)->Array.join(", ")} — a plugin is a ` ++
1238
- `directory with both src/Plugin.res and tests/`,
1258
+ `no plugins found under ${roots
1259
+ ->Array.map(r => r.dir)
1260
+ ->Array.join(", ")} — a plugin is a ` ++ `directory with both src/Plugin.res and tests/`,
1239
1261
  )
1240
1262
  NodeProcess.exit(1)
1241
1263
  }
@@ -1256,8 +1278,11 @@ let main = async () => {
1256
1278
  // refusing outright.
1257
1279
  if !hasCorpus(~pluginDirs=allPluginDirs) {
1258
1280
  Console.error(
1259
- `no scenario sidecar exists under ${roots->Array.map(r => r.dir)->Array.join(", ")} after ` ++
1260
- `the build. Build that tree with REVENTLESS_EMIT_SIDECAR=1 and pass --reuse-sidecars.`,
1281
+ `no scenario sidecar exists under ${roots
1282
+ ->Array.map(r => r.dir)
1283
+ ->Array.join(
1284
+ ", ",
1285
+ )} after ` ++ `the build. Build that tree with REVENTLESS_EMIT_SIDECAR=1 and pass --reuse-sidecars.`,
1261
1286
  )
1262
1287
  NodeProcess.exit(1)
1263
1288
  }
@@ -1308,7 +1333,9 @@ let main = async () => {
1308
1333
  ~drifted,
1309
1334
  )
1310
1335
  Console.log(
1311
- `ok ${example} — ${Array.length(derived)->Int.toString} commands derived from scenarios`,
1336
+ `ok ${example} — ${Array.length(
1337
+ derived,
1338
+ )->Int.toString} commands derived from scenarios`,
1312
1339
  )
1313
1340
  }
1314
1341
  }
@@ -1336,8 +1363,9 @@ let main = async () => {
1336
1363
 
1337
1364
  if Array.length(drifted) > 0 {
1338
1365
  Console.error(
1339
- `\n${Array.length(drifted)->Int.toString} lifecycle artifact(s) changed. If the change is ` ++
1340
- `intended, re-run with --update and commit them alongside the change that moved them.`,
1366
+ `\n${Array.length(
1367
+ drifted,
1368
+ )->Int.toString} lifecycle artifact(s) changed. If the change is ` ++ `intended, re-run with --update and commit them alongside the change that moved them.`,
1341
1369
  )
1342
1370
  }
1343
1371
 
@@ -910,6 +910,20 @@ function reportJson(findings, opaque, derived, failures) {
910
910
  ]), undefined, 2) + "\n";
911
911
  }
912
912
 
913
+ function formatted(source) {
914
+ return Nodechild_process.execFileSync("pnpm", [
915
+ "exec",
916
+ "rescript",
917
+ "format",
918
+ "--stdin",
919
+ ".res"
920
+ ], {
921
+ cwd: repoRoot,
922
+ encoding: "utf8",
923
+ input: source
924
+ });
925
+ }
926
+
913
927
  function modelSource(plugin, derived) {
914
928
  let saysSomething = d => {
915
929
  if (d.level !== "" || d.allowedStates.length !== 0) {
@@ -932,7 +946,7 @@ function modelSource(plugin, derived) {
932
946
  }
933
947
  return ` {component: "` + d.component + `", command: "` + d.command + `", ` + level + (`allowedStates: ` + quoted(d.allowedStates) + `, targets: ` + quoted(d.targets) + `},`);
934
948
  });
935
- return [
949
+ return formatted([
936
950
  [
937
951
  `// AUTO-GENERATED — do not edit. Run \`pnpm run check:lifecycle:update\` to update.`,
938
952
  `//`,
@@ -949,7 +963,7 @@ function modelSource(plugin, derived) {
949
963
  "]",
950
964
  ""
951
965
  ]
952
- ].flat().join("\n");
966
+ ].flat().join("\n"));
953
967
  }
954
968
 
955
969
  function modelPath(pluginDir) {
@@ -1126,6 +1140,7 @@ export {
1126
1140
  goldenJson,
1127
1141
  goldenPath,
1128
1142
  reportJson,
1143
+ formatted,
1129
1144
  modelSource,
1130
1145
  modelPath,
1131
1146
  writeOrCompare,
@@ -13,7 +13,6 @@ a JSON number either way.
13
13
  }
14
14
  ```
15
15
  */
16
-
17
16
  /** The count's representation. Transparent `float`; see the note above on why
18
17
  it is not `int`. */
19
18
  type t = float
@@ -31,7 +31,6 @@ type state = {
31
31
  }
32
32
  ```
33
33
  */
34
-
35
34
  /** The day's representation. Transparent `string`, like the rest of the branded
36
35
  scalars: the marker refines an existing field rather than replacing it. */
37
36
  type t = string
@@ -52,7 +51,8 @@ let fromString = (raw: string): result<t, string> =>
52
51
 
53
52
  /** The sury schema for a calendar-date field. Write the field's type as
54
53
  `Reventless.CalendarDate.t` and sury-ppx resolves it. */
55
- let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.date, ~check=fromString)
54
+ let schema: S.t<t> =
55
+ S.string->Semantic.refined(~id=Semantic.Id.date, ~check=fromString)
56
56
 
57
57
  /** Whether a field schema carries the calendar-date marker. */
58
58
  let isCalendarDate = (fieldSchema: S.t<unknown>) => fieldSchema->Semantic.has(~id=Semantic.Id.date)
@@ -50,10 +50,8 @@ let none: t = {
50
50
  // `UnsupportedChannel` because a caller that got this far is looking at a
51
51
  // deployment gap, not at a fact about the recipient, and abandoning the message
52
52
  // would record the second.
53
- messaging: {
54
- channels: [],
55
- send: async (~recipient as _, ~message as _) => Error(
56
- Unavailable("no messaging provider is configured for this platform"),
57
- ),
58
- },
53
+ messaging: Messaging.makeProvider(~emailAndSms=[], ~pushServices=[], ~send=async (
54
+ ~recipient as _,
55
+ ~message as _,
56
+ ) => Error(Unavailable("no messaging provider is configured for this platform"))),
59
57
  }
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Messaging$Reventless from "./Messaging.res.mjs";
3
4
 
4
5
  async function none_geocode(param) {
5
6
  return {
@@ -11,16 +12,13 @@ async function none_geocode(param) {
11
12
  };
12
13
  }
13
14
 
14
- let none_messaging = {
15
- channels: [],
16
- send: async (param, param$1) => ({
17
- TAG: "Error",
18
- _0: {
19
- TAG: "Unavailable",
20
- _0: "no messaging provider is configured for this platform"
21
- }
22
- })
23
- };
15
+ let none_messaging = Messaging$Reventless.makeProvider([], [], async (param, param$1) => ({
16
+ TAG: "Error",
17
+ _0: {
18
+ TAG: "Unavailable",
19
+ _0: "no messaging provider is configured for this platform"
20
+ }
21
+ }));
24
22
 
25
23
  let none = {
26
24
  geocode: none_geocode,
@@ -30,4 +28,4 @@ let none = {
30
28
  export {
31
29
  none,
32
30
  }
33
- /* No side effect */
31
+ /* none Not a pure module */
@@ -18,12 +18,12 @@ rather than a string a README repeats.
18
18
  */
19
19
  type t =
20
20
  | Geocoding
21
- | /** Sending a message to a person, reached through `Capabilities.messaging`.
21
+ /** Sending a message to a person, reached through `Capabilities.messaging`.
22
22
  One need however many channels the platform provisions — which channels
23
23
  those are is a runtime answer the provider publishes, not a second
24
24
  declaration, because a plugin that named `Sms` would fail a deploy it
25
25
  could have run on email. */
26
- Messaging
26
+ | Messaging
27
27
 
28
28
  /** The spelling carried in `pluginStructure` and in `capabilities.json`.
29
29
  Persisted structures hold strings, not enum members, so a plugin built
@@ -62,7 +62,7 @@ unprovisioned capability it never named degrades exactly as it always has, which
62
62
  is the modelled outcome for a deployment that simply does not have one.
63
63
  */
64
64
  let unmet = (~declared: array<(string, string)>, ~provisioned: array<t>): array<unmet> =>
65
- declared->Array.filterMap((((capability, component))) =>
65
+ declared->Array.filterMap(((capability, component)) =>
66
66
  switch fromString(capability) {
67
67
  | Some(need) if !(provisioned->Array.includes(need)) => Some({need, component})
68
68
  | _ => None
@@ -77,5 +77,4 @@ let unmetMessage = (unmet: array<unmet>): string =>
77
77
  ->Array.map(u => `${toString(u.need)} (${u.component})`)
78
78
  ->Array.join(", ")}.\n` ++
79
79
  ` Provision them in the platform stack and redeploy the platform first.\n` ++
80
- ` Without it every call answers Unavailable, the slice exhausts its retries, and a ` ++
81
- `permanent verdict is recorded against data that is fine — no error anywhere.`
80
+ ` Without it every call answers Unavailable, the slice exhausts its retries, and a ` ++ `permanent verdict is recorded against data that is fine — no error anywhere.`