@reventlessdev/reventless-spec 3.0.0-alpha.133 → 3.0.0-alpha.135
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 +33 -0
- package/package.json +4 -3
- package/run-check-lifecycle.mjs +2 -1
- package/run-prepare-accounts.mjs +3 -0
- package/src/AnsiStyle.res +0 -1
- package/src/LogPrefix.res +4 -13
- package/src/components/AutomationSlice.res +1 -5
- package/src/components/CapabilityManifest.res +6 -3
- package/src/components/CapabilityManifest.res.mjs +13 -2
- package/src/components/DcbDecode.res +6 -2
- package/src/components/DcbScopeInference.res +16 -12
- package/src/components/DcbTag.res +74 -46
- package/src/components/DcbValidation.res +114 -89
- package/src/components/DisplayName.res +4 -2
- package/src/components/ExtensionPoint.res +0 -1
- package/src/components/FieldDefault.res +2 -4
- package/src/components/InboundTranslationSlice.res +0 -2
- package/src/components/OutboundTranslationSlice.res +10 -8
- package/src/components/Owner.res +4 -4
- package/src/components/Plugin.res +10 -4
- package/src/components/ReadModel.res +0 -1
- package/src/components/Reference.res +2 -8
- package/src/components/Sensitive.res +4 -4
- package/src/components/StateAnnotations.res +4 -2
- package/src/components/StateChangeSlice.res +0 -2
- package/src/components/StateViewSlice.res +0 -2
- package/src/components/TaggedUnion.res +1 -2
- package/src/components/Task.res +0 -1
- package/src/components/TraitCertificate.res +2 -2
- package/src/components/TraitManifest.res +1 -1
- package/src/generator/CertifyTrait.res +2 -4
- package/src/generator/Codegen.res +77 -68
- package/src/generator/Discovery.res +29 -12
- package/src/generator/GraftTrait.res +17 -9
- package/src/generator/Pairing.res +39 -13
- package/src/generator/PlatformCodegen.res +14 -11
- package/src/generator/PlatformCodegen.res.mjs +5 -0
- package/src/generator/PlatformManifests.res +1 -3
- package/src/generator/PluginGenerator.res +0 -1
- package/src/generator/TraitManifestCli.res +7 -7
- package/src/lifecycle/CheckLifecycleModel.res +206 -148
- package/src/lifecycle/CheckLifecycleModel.res.mjs +23 -5
- package/src/semantic/Bytes.res +0 -1
- package/src/semantic/CalendarDate.res +2 -2
- package/src/semantic/Capabilities.res +21 -6
- package/src/semantic/Capabilities.res.mjs +18 -12
- package/src/semantic/CapabilityNeed.res +16 -5
- package/src/semantic/CapabilityNeed.res.mjs +9 -4
- package/src/semantic/CaptionedImage.res +0 -1
- package/src/semantic/Color.res +0 -1
- package/src/semantic/Currency.res +189 -170
- package/src/semantic/DateRange.res +3 -4
- package/src/semantic/DateTime.res +5 -5
- package/src/semantic/Duration.res +2 -2
- package/src/semantic/Email.res +0 -1
- package/src/semantic/FileRef.res +2 -2
- package/src/semantic/GeoPoint.res +17 -21
- package/src/semantic/Geocoding.res +8 -9
- package/src/semantic/Geolocation.res +6 -9
- package/src/semantic/IdentityProvider.res +127 -0
- package/src/semantic/IdentityProvider.res.mjs +65 -0
- package/src/semantic/ImageRef.res +2 -2
- package/src/semantic/MemberRef.res +0 -1
- package/src/semantic/Messaging.res +141 -21
- package/src/semantic/Messaging.res.mjs +51 -8
- package/src/semantic/Money.res +23 -20
- package/src/semantic/Offload.res +6 -4
- package/src/semantic/Percent.res +3 -3
- package/src/semantic/Phone.res +0 -1
- package/src/semantic/RowImage.res +4 -3
- package/src/semantic/Secrets.res +76 -0
- package/src/semantic/Secrets.res.mjs +49 -0
- package/src/semantic/Semantic.res +8 -11
- package/src/semantic/StorageRef.res +17 -17
- package/src/semantic/Template.res +5 -7
- package/src/semantic/UploadableFile.res +0 -1
- package/src/semantic/UploadableImage.res +0 -1
- package/src/semantic/Url.res +3 -3
- package/src/types/AccountsManifest.res +281 -0
- package/src/types/AccountsManifest.res.mjs +311 -0
- package/src/types/AdminGroup.res +27 -0
- package/src/types/AdminGroup.res.mjs +9 -0
- package/src/types/Authorization.res +1 -2
- package/src/types/Handler.res +0 -1
- package/src/types/Identity.res +1 -3
- package/src/types/Lifecycle.res +7 -6
- package/src/types/Message.res +6 -4
- package/src/types/OwnerScope.res +28 -3
- package/src/types/OwnerScope.res.mjs +12 -0
- package/src/types/PrepareAccounts.res +119 -0
- package/src/types/PrepareAccounts.res.mjs +148 -0
- package/src/types/Projection.res +9 -2
- package/src/types/StoredEvent.res +2 -4
- package/src/types/Transition.res +8 -4
- package/src/util/Util_Password.res +60 -0
- package/src/util/Util_Password.res.mjs +53 -0
- 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(
|
|
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
|
-
`${
|
|
131
|
-
`${
|
|
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 =
|
|
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 =
|
|
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(
|
|
409
|
-
name
|
|
410
|
-
|
|
411
|
-
|
|
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 =>
|
|
@@ -548,6 +550,18 @@ type derivedCommand = {
|
|
|
548
550
|
accepted and silent. Kept separately because it is what turns a declared
|
|
549
551
|
state the corpus disagrees with into a contradiction rather than a gap. */
|
|
550
552
|
inertStates: array<string>,
|
|
553
|
+
/** The strict subset of [inertStates] where the command was *refused* — an
|
|
554
|
+
error, not an accepted no-op.
|
|
555
|
+
|
|
556
|
+
Separate because the two refute different claims. A named from-set says the
|
|
557
|
+
command **takes effect** from those states, so anything inert there
|
|
558
|
+
contradicts it. `Unrestricted` says only that the command is **never
|
|
559
|
+
refused**, which an accepted `Ok([])` agrees with rather than contradicts —
|
|
560
|
+
returning no events for a command that would change nothing is this
|
|
561
|
+
codebase's idempotency convention, not evidence the state was illegal.
|
|
562
|
+
Reading one field for both questions made every idempotent
|
|
563
|
+
legal-in-every-state command look like a contradiction. */
|
|
564
|
+
refusedStates: array<string>,
|
|
551
565
|
/** Where the edges land. A relation, not a single value: a command observed
|
|
552
566
|
landing in two states is the signal that the published `targetState` cannot
|
|
553
567
|
carry the model, and that is worth seeing before spending the change. */
|
|
@@ -647,9 +661,10 @@ let deriveCommands = (
|
|
|
647
661
|
effective->Array.filterMap(o => o.from == noRow ? None : Some(o.from)),
|
|
648
662
|
),
|
|
649
663
|
inertStates: sortedUnique(
|
|
650
|
-
mine->Array.filterMap(o =>
|
|
651
|
-
|
|
652
|
-
|
|
664
|
+
mine->Array.filterMap(o => o.outcome == Emitted || o.from == noRow ? None : Some(o.from)),
|
|
665
|
+
),
|
|
666
|
+
refusedStates: sortedUnique(
|
|
667
|
+
mine->Array.filterMap(o => o.outcome == Refused && o.from != noRow ? Some(o.from) : None),
|
|
653
668
|
),
|
|
654
669
|
targets: sortedUnique(
|
|
655
670
|
effective->Array.filterMap(o => o.to == o.from || o.to == noRow ? None : Some(o.to)),
|
|
@@ -757,15 +772,24 @@ let compare = (
|
|
|
757
772
|
// effect somewhere agrees with that rather than narrowing it — the corpus
|
|
758
773
|
// covers the states somebody wrote a scenario for, and silence about the
|
|
759
774
|
// rest is not refusal. What DOES refute the claim is a state the command was
|
|
760
|
-
// exercised in and
|
|
775
|
+
// exercised in and **refused**: that is the switch and the behaviour
|
|
761
776
|
// disagreeing about the same row.
|
|
777
|
+
//
|
|
778
|
+
// 🚨 **Refused, not merely inert.** `Unrestricted` claims the command is
|
|
779
|
+
// never refused — it does not claim the command always emits. A state where a
|
|
780
|
+
// scenario shows it accepted and silent AGREES with that: returning `Ok([])`
|
|
781
|
+
// for a command that would change nothing is this codebase's idempotency
|
|
782
|
+
// convention, required because commands arrive at least once. Reading
|
|
783
|
+
// `inertStates` here marked every such command contradicted, which is the
|
|
784
|
+
// opposite of what the corpus showed — a verdict redelivered, or landing
|
|
785
|
+
// after deactivation, is precisely the case the switch declared legal so that
|
|
786
|
+
// the reporting slice would not retry forever.
|
|
762
787
|
| None if cmd.allowedStatesSource == Some("unrestricted") =>
|
|
763
|
-
derived.
|
|
788
|
+
derived.refusedStates->Array.forEach(state =>
|
|
764
789
|
add(
|
|
765
790
|
"contradicted",
|
|
766
791
|
[state],
|
|
767
|
-
`the switch declares it legal in every state, and a scenario from "${state}" ` ++
|
|
768
|
-
`shows it refused or producing nothing`,
|
|
792
|
+
`the switch declares it legal in every state, and a scenario from "${state}" ` ++ `shows it refused`,
|
|
769
793
|
)
|
|
770
794
|
)
|
|
771
795
|
| None =>
|
|
@@ -773,8 +797,9 @@ let compare = (
|
|
|
773
797
|
add(
|
|
774
798
|
"undeclared",
|
|
775
799
|
derived.allowedStates,
|
|
776
|
-
`scenarios show it taking effect from ${derived.allowedStates->Array.join(
|
|
777
|
-
|
|
800
|
+
`scenarios show it taking effect from ${derived.allowedStates->Array.join(
|
|
801
|
+
", ",
|
|
802
|
+
)}, ` ++ `and it declares no edge`,
|
|
778
803
|
)
|
|
779
804
|
}
|
|
780
805
|
| Some(states) =>
|
|
@@ -785,8 +810,7 @@ let compare = (
|
|
|
785
810
|
add(
|
|
786
811
|
"contradicted",
|
|
787
812
|
[state],
|
|
788
|
-
`the switch names "${state}", and a scenario from "${state}" shows it ` ++
|
|
789
|
-
`refused or producing nothing`,
|
|
813
|
+
`the switch names "${state}", and a scenario from "${state}" shows it ` ++ `refused or producing nothing`,
|
|
790
814
|
)
|
|
791
815
|
} else {
|
|
792
816
|
add("unverified", [state], `the switch names "${state}", and no scenario starts there`)
|
|
@@ -806,8 +830,9 @@ let compare = (
|
|
|
806
830
|
add(
|
|
807
831
|
"unverified",
|
|
808
832
|
states,
|
|
809
|
-
`the switch declares ${Array.length(
|
|
810
|
-
|
|
833
|
+
`the switch declares ${Array.length(
|
|
834
|
+
states,
|
|
835
|
+
)->Int.toString} state(s) and ` ++ `no scenario shows the command taking effect anywhere`,
|
|
811
836
|
)
|
|
812
837
|
}
|
|
813
838
|
}
|
|
@@ -830,8 +855,7 @@ let compare = (
|
|
|
830
855
|
add(
|
|
831
856
|
"contradicted",
|
|
832
857
|
[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`,
|
|
858
|
+
`the switch targets "${target}", and a scenario lands in "${state}" — ` ++ `the published targetState carries one state, so this edge cannot be expressed`,
|
|
835
859
|
)
|
|
836
860
|
}
|
|
837
861
|
)
|
|
@@ -933,107 +957,111 @@ let runPlugin = async (
|
|
|
933
957
|
switch await readDeclared(~pluginDir) {
|
|
934
958
|
| Error(msg) => Error(msg)
|
|
935
959
|
| Ok(declared) =>
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
}
|
|
960
|
+
let corpora =
|
|
961
|
+
filesUnder(NodePath.join([pluginDir, "tests"]), ~suffix=".gwt.json")->Array.filterMap(
|
|
962
|
+
readCorpus,
|
|
977
963
|
)
|
|
978
|
-
|
|
979
|
-
|
|
964
|
+
|
|
965
|
+
// Every corpus, not only the ones the walk goes on to use: the kinds that
|
|
966
|
+
// are unreadable in full — extension points, automation and translation
|
|
967
|
+
// slices — are exactly the ones that sit outside the view/writable folders
|
|
968
|
+
// below, and they are the ones a coverage UI would otherwise slander.
|
|
969
|
+
corpora->Array.forEach(c => {
|
|
970
|
+
let unreadable = c.scenarios->Array.filter(s => Array.length(s.whenElements) == 0)
|
|
971
|
+
if Array.length(unreadable) > 0 {
|
|
972
|
+
opaque
|
|
980
973
|
->Array.push({
|
|
981
|
-
severity: "ambiguous",
|
|
982
974
|
plugin,
|
|
983
|
-
component:
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
975
|
+
component: c.component,
|
|
976
|
+
path: c.path,
|
|
977
|
+
scenarios: Array.length(c.scenarios),
|
|
978
|
+
unreadable: Array.length(unreadable),
|
|
987
979
|
})
|
|
988
980
|
->ignore
|
|
989
|
-
|
|
981
|
+
}
|
|
982
|
+
})
|
|
990
983
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
984
|
+
// Views first: a command's history cannot be labelled until the events in
|
|
985
|
+
// it have somewhere to land.
|
|
986
|
+
let mapsByView = Dict.make()
|
|
987
|
+
let ambiguities = []
|
|
988
|
+
corpora->Array.forEach(c =>
|
|
989
|
+
if isViewPath(c.path) {
|
|
990
|
+
switch declared.views->Array.find(v => v.name == c.component) {
|
|
991
|
+
| Some({lifecycleField: Some(field)}) =>
|
|
992
|
+
mapsByView->Dict.set(
|
|
993
|
+
c.component,
|
|
994
|
+
lifecycleMapFor(~scenarios=c.scenarios, ~field, ~ambiguities, ~view=c.component),
|
|
995
|
+
)
|
|
996
|
+
// A view with no lifecycle field labels nothing, and that is ordinary
|
|
997
|
+
// — most views have no lifecycle at all.
|
|
998
|
+
| _ => ()
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
)
|
|
1002
|
+
ambiguities->Array.forEach(((view, message)) =>
|
|
1003
|
+
findings
|
|
1004
|
+
->Array.push({
|
|
1005
|
+
severity: "ambiguous",
|
|
1006
|
+
plugin,
|
|
1007
|
+
component: view,
|
|
1008
|
+
command: "",
|
|
1009
|
+
states: [],
|
|
1010
|
+
message,
|
|
1011
|
+
})
|
|
1012
|
+
->ignore
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
let derived = []
|
|
1016
|
+
corpora->Array.forEach(c =>
|
|
1017
|
+
if isWritablePath(c.path) {
|
|
1018
|
+
switch declared.writables->Array.find(w => w.name == c.component) {
|
|
1019
|
+
| None => ()
|
|
1020
|
+
| Some(writable) =>
|
|
1021
|
+
// The union of the maps of every view this writable feeds. A union
|
|
1022
|
+
// rather than a single view for the reason the platform's own name
|
|
1023
|
+
// check uses one: a slice feeding two views is not claiming which of
|
|
1024
|
+
// them a state belongs to.
|
|
1025
|
+
let map = Dict.make()
|
|
1026
|
+
writable.linkedViews->Array.forEach(view =>
|
|
1027
|
+
switch mapsByView->Dict.get(view) {
|
|
1028
|
+
| Some(m) => m->Dict.forEachWithKey((value, event) => map->Dict.set(event, value))
|
|
1029
|
+
| None => ()
|
|
1030
|
+
}
|
|
1031
|
+
)
|
|
1032
|
+
let labelled = Array.length(map->Dict.keysToArray) > 0
|
|
1033
|
+
let idFieldFor = command =>
|
|
1034
|
+
writable.commands
|
|
1035
|
+
->Array.find(c => c.command == command)
|
|
1036
|
+
->Option.flatMap(c => c.aggregateIdField)
|
|
1037
|
+
let observations = observe(~scenarios=c.scenarios, ~map, ~idFieldFor)
|
|
1038
|
+
let commands = deriveCommands(~component=c.component, ~observations, ~labelled)
|
|
1039
|
+
|
|
1040
|
+
commands->Array.forEach(d => {
|
|
1041
|
+
// Without a lifecycle map every history folds to "no row", so
|
|
1042
|
+
// there is nothing to confirm a claim against and nothing to
|
|
1043
|
+
// contradict it with. Reporting the claim as unverified is the
|
|
1044
|
+
// honest answer; running the comparison would manufacture
|
|
1045
|
+
// contradictions out of a missing map.
|
|
1046
|
+
if labelled {
|
|
1047
|
+
compare(~plugin, ~writable, ~derived=d, ~findings)
|
|
1048
|
+
}
|
|
1049
|
+
derived->Array.push(d)
|
|
1050
|
+
})
|
|
1027
1051
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1052
|
+
let why = labelled
|
|
1053
|
+
? "no scenario exercises the command"
|
|
1054
|
+
: `${writable.linkedViews->Array.join(
|
|
1055
|
+
", ",
|
|
1056
|
+
)} declares no lifecycle field, so its ` ++ `scenarios cannot be labelled`
|
|
1057
|
+
writable.commands->Array.forEach(cmd =>
|
|
1058
|
+
if labelled && commands->Array.some(d => d.command == cmd.command) {
|
|
1059
|
+
()
|
|
1060
|
+
} else {
|
|
1061
|
+
allUnverified(
|
|
1062
|
+
~cmd,
|
|
1063
|
+
~why,
|
|
1064
|
+
~add=(severity, states, message) =>
|
|
1037
1065
|
findings
|
|
1038
1066
|
->Array.push({
|
|
1039
1067
|
severity,
|
|
@@ -1043,14 +1071,14 @@ let runPlugin = async (
|
|
|
1043
1071
|
states,
|
|
1044
1072
|
message: `${plugin}/${writable.name}.${cmd.command}: ${message}`,
|
|
1045
1073
|
})
|
|
1046
|
-
->ignore
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
}
|
|
1074
|
+
->ignore,
|
|
1075
|
+
)
|
|
1076
|
+
}
|
|
1077
|
+
)
|
|
1051
1078
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1079
|
+
}
|
|
1080
|
+
)
|
|
1081
|
+
Ok(derived)
|
|
1054
1082
|
}
|
|
1055
1083
|
|
|
1056
1084
|
// ── The golden ──────────────────────────────────────────────────────────────
|
|
@@ -1160,6 +1188,19 @@ let reportJson = (
|
|
|
1160
1188
|
|
|
1161
1189
|
// ── The value structure assembly reads ──────────────────────────────────────
|
|
1162
1190
|
|
|
1191
|
+
/** The emitted file is committed ReScript, so it faces the tree's format gate
|
|
1192
|
+
like any other source. Laying the text out by hand would put this script and
|
|
1193
|
+
the formatter in a standing argument only one of them can win — so it emits
|
|
1194
|
+
whatever reads clearly and lets the formatter settle the layout. Raising
|
|
1195
|
+
beats falling back: unformatted source would satisfy this check and fail the
|
|
1196
|
+
format gate, which is the stand-off the call exists to end. */
|
|
1197
|
+
let formatted = (source: string): string =>
|
|
1198
|
+
NodeChildProcess.execFileSync(
|
|
1199
|
+
"pnpm",
|
|
1200
|
+
["exec", "rescript", "format", "--stdin", ".res"],
|
|
1201
|
+
{cwd: repoRoot, encoding: "utf8", input: source},
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1163
1204
|
/** The same derivation as a committed ReScript value, so `buildStructure` gets
|
|
1164
1205
|
the model as data. It cannot read the corpus itself: tests are not published
|
|
1165
1206
|
with a plugin package, and metadata that read them would make deleting a test
|
|
@@ -1173,19 +1214,22 @@ let reportJson = (
|
|
|
1173
1214
|
let modelSource = (~plugin: string, ~derived: array<derivedCommand>): string => {
|
|
1174
1215
|
let saysSomething = (d: derivedCommand) =>
|
|
1175
1216
|
d.level != "" || Array.length(d.allowedStates) > 0 || Array.length(d.targets) > 0
|
|
1176
|
-
let quoted = (xs: array<string>) =>
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1217
|
+
let quoted = (xs: array<string>) => "[" ++ xs->Array.map(s => `"${s}"`)->Array.join(", ") ++ "]"
|
|
1218
|
+
let entries =
|
|
1219
|
+
derived
|
|
1220
|
+
->Array.filter(saysSomething)
|
|
1221
|
+
->byComponentThenCommand
|
|
1222
|
+
->Array.map(d => {
|
|
1223
|
+
// Omitted rather than written as an absent value: `level` is an optional
|
|
1224
|
+
// field, and a corpus that could not label this command's histories has
|
|
1225
|
+
// nothing to say about it.
|
|
1226
|
+
let level = switch d.level {
|
|
1227
|
+
| "Collection" | "Instance" => `level: Reventless.Plugin.${d.level}, `
|
|
1228
|
+
| _ => ""
|
|
1229
|
+
}
|
|
1230
|
+
` {component: "${d.component}", command: "${d.command}", ${level}` ++
|
|
1231
|
+
`allowedStates: ${quoted(d.allowedStates)}, targets: ${quoted(d.targets)}},`
|
|
1232
|
+
})
|
|
1189
1233
|
Array.flat([
|
|
1190
1234
|
[
|
|
1191
1235
|
`// AUTO-GENERATED — do not edit. Run \`pnpm run check:lifecycle:update\` to update.`,
|
|
@@ -1200,7 +1244,9 @@ let modelSource = (~plugin: string, ~derived: array<derivedCommand>): string =>
|
|
|
1200
1244
|
],
|
|
1201
1245
|
entries,
|
|
1202
1246
|
["]", ""],
|
|
1203
|
-
])
|
|
1247
|
+
])
|
|
1248
|
+
->Array.join("\n")
|
|
1249
|
+
->formatted
|
|
1204
1250
|
}
|
|
1205
1251
|
|
|
1206
1252
|
let modelPath = (~pluginDir: string) => NodePath.join([pluginDir, "src", "LifecycleModel.res"])
|
|
@@ -1234,8 +1280,9 @@ let main = async () => {
|
|
|
1234
1280
|
// an app with no plugins, and reporting "ok" for it is how that typo survives.
|
|
1235
1281
|
if Array.length(allPluginDirs) == 0 {
|
|
1236
1282
|
Console.error(
|
|
1237
|
-
`no plugins found under ${roots
|
|
1238
|
-
|
|
1283
|
+
`no plugins found under ${roots
|
|
1284
|
+
->Array.map(r => r.dir)
|
|
1285
|
+
->Array.join(", ")} — a plugin is a ` ++ `directory with both src/Plugin.res and tests/`,
|
|
1239
1286
|
)
|
|
1240
1287
|
NodeProcess.exit(1)
|
|
1241
1288
|
}
|
|
@@ -1256,8 +1303,11 @@ let main = async () => {
|
|
|
1256
1303
|
// refusing outright.
|
|
1257
1304
|
if !hasCorpus(~pluginDirs=allPluginDirs) {
|
|
1258
1305
|
Console.error(
|
|
1259
|
-
`no scenario sidecar exists under ${roots
|
|
1260
|
-
|
|
1306
|
+
`no scenario sidecar exists under ${roots
|
|
1307
|
+
->Array.map(r => r.dir)
|
|
1308
|
+
->Array.join(
|
|
1309
|
+
", ",
|
|
1310
|
+
)} after ` ++ `the build. Build that tree with REVENTLESS_EMIT_SIDECAR=1 and pass --reuse-sidecars.`,
|
|
1261
1311
|
)
|
|
1262
1312
|
NodeProcess.exit(1)
|
|
1263
1313
|
}
|
|
@@ -1308,7 +1358,9 @@ let main = async () => {
|
|
|
1308
1358
|
~drifted,
|
|
1309
1359
|
)
|
|
1310
1360
|
Console.log(
|
|
1311
|
-
`ok ${example} — ${Array.length(
|
|
1361
|
+
`ok ${example} — ${Array.length(
|
|
1362
|
+
derived,
|
|
1363
|
+
)->Int.toString} commands derived from scenarios`,
|
|
1312
1364
|
)
|
|
1313
1365
|
}
|
|
1314
1366
|
}
|
|
@@ -1336,8 +1388,9 @@ let main = async () => {
|
|
|
1336
1388
|
|
|
1337
1389
|
if Array.length(drifted) > 0 {
|
|
1338
1390
|
Console.error(
|
|
1339
|
-
`\n${Array.length(
|
|
1340
|
-
|
|
1391
|
+
`\n${Array.length(
|
|
1392
|
+
drifted,
|
|
1393
|
+
)->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
1394
|
)
|
|
1342
1395
|
}
|
|
1343
1396
|
|
|
@@ -1350,4 +1403,9 @@ let main = async () => {
|
|
|
1350
1403
|
}
|
|
1351
1404
|
}
|
|
1352
1405
|
|
|
1353
|
-
|
|
1406
|
+
// 🚨 **No top-level call.** `../../run-check-lifecycle.mjs` invokes [main]; this
|
|
1407
|
+
// module only defines it. While the call was here, importing the module *ran the
|
|
1408
|
+
// whole check* and then called `NodeProcess.exit`, so nothing in this file could
|
|
1409
|
+
// be reached from a test — including [deriveCommands] and [compare], which are
|
|
1410
|
+
// pure and are where every verdict is actually decided. That is the same trap
|
|
1411
|
+
// `ProvisionIdentity` records having shipped a never-matching guard through.
|
|
@@ -469,6 +469,11 @@ function deriveCommands(component, observations, labelled) {
|
|
|
469
469
|
return o.from;
|
|
470
470
|
}
|
|
471
471
|
})),
|
|
472
|
+
refusedStates: sortedUnique(Stdlib_Array.filterMap(mine, o => {
|
|
473
|
+
if (o.outcome === "Refused" && o.from !== noRow) {
|
|
474
|
+
return o.from;
|
|
475
|
+
}
|
|
476
|
+
})),
|
|
472
477
|
targets: sortedUnique(Stdlib_Array.filterMap(effective, o => {
|
|
473
478
|
if (o.to === o.from || o.to === noRow) {
|
|
474
479
|
return;
|
|
@@ -531,7 +536,7 @@ function compare(plugin, writable, derived, findings) {
|
|
|
531
536
|
add("unverified", states, `the switch declares ` + states.length.toString() + ` state(s) and no scenario shows the command taking effect anywhere`);
|
|
532
537
|
}
|
|
533
538
|
} else if (Primitive_object.equal(declared.allowedStatesSource, "unrestricted")) {
|
|
534
|
-
derived.
|
|
539
|
+
derived.refusedStates.forEach(state => add("contradicted", [state], `the switch declares it legal in every state, and a scenario from "` + state + `" shows it refused`));
|
|
535
540
|
} else if (derived.allowedStates.length !== 0) {
|
|
536
541
|
add("undeclared", derived.allowedStates, `scenarios show it taking effect from ` + derived.allowedStates.join(", ") + `, and it declares no edge`);
|
|
537
542
|
}
|
|
@@ -910,6 +915,20 @@ function reportJson(findings, opaque, derived, failures) {
|
|
|
910
915
|
]), undefined, 2) + "\n";
|
|
911
916
|
}
|
|
912
917
|
|
|
918
|
+
function formatted(source) {
|
|
919
|
+
return Nodechild_process.execFileSync("pnpm", [
|
|
920
|
+
"exec",
|
|
921
|
+
"rescript",
|
|
922
|
+
"format",
|
|
923
|
+
"--stdin",
|
|
924
|
+
".res"
|
|
925
|
+
], {
|
|
926
|
+
cwd: repoRoot,
|
|
927
|
+
encoding: "utf8",
|
|
928
|
+
input: source
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
913
932
|
function modelSource(plugin, derived) {
|
|
914
933
|
let saysSomething = d => {
|
|
915
934
|
if (d.level !== "" || d.allowedStates.length !== 0) {
|
|
@@ -932,7 +951,7 @@ function modelSource(plugin, derived) {
|
|
|
932
951
|
}
|
|
933
952
|
return ` {component: "` + d.component + `", command: "` + d.command + `", ` + level + (`allowedStates: ` + quoted(d.allowedStates) + `, targets: ` + quoted(d.targets) + `},`);
|
|
934
953
|
});
|
|
935
|
-
return [
|
|
954
|
+
return formatted([
|
|
936
955
|
[
|
|
937
956
|
`// AUTO-GENERATED — do not edit. Run \`pnpm run check:lifecycle:update\` to update.`,
|
|
938
957
|
`//`,
|
|
@@ -949,7 +968,7 @@ function modelSource(plugin, derived) {
|
|
|
949
968
|
"]",
|
|
950
969
|
""
|
|
951
970
|
]
|
|
952
|
-
].flat().join("\n");
|
|
971
|
+
].flat().join("\n"));
|
|
953
972
|
}
|
|
954
973
|
|
|
955
974
|
function modelPath(pluginDir) {
|
|
@@ -1072,8 +1091,6 @@ async function main() {
|
|
|
1072
1091
|
}
|
|
1073
1092
|
}
|
|
1074
1093
|
|
|
1075
|
-
main();
|
|
1076
|
-
|
|
1077
1094
|
export {
|
|
1078
1095
|
repoRoot,
|
|
1079
1096
|
examplesDir,
|
|
@@ -1126,6 +1143,7 @@ export {
|
|
|
1126
1143
|
goldenJson,
|
|
1127
1144
|
goldenPath,
|
|
1128
1145
|
reportJson,
|
|
1146
|
+
formatted,
|
|
1129
1147
|
modelSource,
|
|
1130
1148
|
modelPath,
|
|
1131
1149
|
writeOrCompare,
|
package/src/semantic/Bytes.res
CHANGED
|
@@ -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> =
|
|
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)
|