@reventlessdev/reventless-spec 3.0.0-alpha.134 → 3.0.0-alpha.136
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 +44 -0
- package/package.json +4 -3
- package/run-check-lifecycle.mjs +2 -1
- package/run-prepare-accounts.mjs +3 -0
- package/src/components/CapabilityManifest.res +4 -0
- package/src/components/CapabilityManifest.res.mjs +13 -2
- package/src/components/DcbScopeInference.res +224 -73
- package/src/components/DcbScopeInference.res.mjs +187 -49
- package/src/components/DcbTag.res +142 -186
- package/src/components/DcbTag.res.mjs +137 -128
- package/src/components/DcbValidation.res +50 -0
- package/src/components/DcbValidation.res.mjs +51 -0
- package/src/generator/Codegen.res +2 -1
- package/src/generator/Codegen.res.mjs +1 -1
- package/src/generator/PlatformCodegen.res +1 -0
- package/src/generator/PlatformCodegen.res.mjs +5 -0
- package/src/lifecycle/CheckLifecycleModel.res +148 -8
- package/src/lifecycle/CheckLifecycleModel.res.mjs +154 -7
- package/src/semantic/Capabilities.res +17 -0
- package/src/semantic/Capabilities.res.mjs +9 -1
- package/src/semantic/CapabilityNeed.res +12 -0
- package/src/semantic/CapabilityNeed.res.mjs +9 -4
- package/src/semantic/IdentityProvider.res +127 -0
- package/src/semantic/IdentityProvider.res.mjs +65 -0
- package/src/semantic/Secrets.res +76 -0
- package/src/semantic/Secrets.res.mjs +49 -0
- package/src/types/AccountsManifest.res +287 -0
- package/src/types/AccountsManifest.res.mjs +312 -0
- package/src/types/AdminGroup.res +27 -0
- package/src/types/AdminGroup.res.mjs +9 -0
- package/src/types/OwnerScope.res +27 -0
- 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/util/Util_Password.res +60 -0
- package/src/util/Util_Password.res.mjs +53 -0
|
@@ -550,6 +550,18 @@ type derivedCommand = {
|
|
|
550
550
|
accepted and silent. Kept separately because it is what turns a declared
|
|
551
551
|
state the corpus disagrees with into a contradiction rather than a gap. */
|
|
552
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>,
|
|
553
565
|
/** Where the edges land. A relation, not a single value: a command observed
|
|
554
566
|
landing in two states is the signal that the published `targetState` cannot
|
|
555
567
|
carry the model, and that is worth seeing before spending the change. */
|
|
@@ -651,6 +663,9 @@ let deriveCommands = (
|
|
|
651
663
|
inertStates: sortedUnique(
|
|
652
664
|
mine->Array.filterMap(o => o.outcome == Emitted || o.from == noRow ? None : Some(o.from)),
|
|
653
665
|
),
|
|
666
|
+
refusedStates: sortedUnique(
|
|
667
|
+
mine->Array.filterMap(o => o.outcome == Refused && o.from != noRow ? Some(o.from) : None),
|
|
668
|
+
),
|
|
654
669
|
targets: sortedUnique(
|
|
655
670
|
effective->Array.filterMap(o => o.to == o.from || o.to == noRow ? None : Some(o.to)),
|
|
656
671
|
),
|
|
@@ -666,6 +681,90 @@ let deriveCommands = (
|
|
|
666
681
|
})
|
|
667
682
|
}
|
|
668
683
|
|
|
684
|
+
// ── What each command yields ────────────────────────────────────────────────
|
|
685
|
+
|
|
686
|
+
/** Which command yields which event or error, as its scenarios show it — what a
|
|
687
|
+
consumer draws a command's outcome arrows from, read off the corpus rather
|
|
688
|
+
than guessed from names. It needs no lifecycle labelling, so every corpus a
|
|
689
|
+
plugin has is read, though only its command scenarios count.
|
|
690
|
+
|
|
691
|
+
An empty `then` is skipped rather than read as "accepted, nothing happened".
|
|
692
|
+
The PPX currently writes `then: []` for `thenNoEvent`, so an empty `then` is
|
|
693
|
+
indistinguishable from one the sidecar could not read, and an outcome shown
|
|
694
|
+
here has to be one a scenario actually states. */
|
|
695
|
+
type shownOutcome = {
|
|
696
|
+
kind: string, // "event" | "error" | "noEvent"
|
|
697
|
+
/** The whole `then`, in order: `thenEvents([A, B])` is one outcome of two
|
|
698
|
+
events — both, not either. Empty for `noEvent`. */
|
|
699
|
+
names: array<string>,
|
|
700
|
+
/** The title of every scenario showing this outcome, in corpus order. */
|
|
701
|
+
scenarios: array<string>,
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
type commandOutcomes = {
|
|
705
|
+
plugin: string,
|
|
706
|
+
component: string,
|
|
707
|
+
command: string,
|
|
708
|
+
/** One per distinct outcome, in order of first appearance. */
|
|
709
|
+
outcomes: array<shownOutcome>,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let shownOutcomeOf = (s: scenario): option<(string, array<string>)> =>
|
|
713
|
+
switch s.thenKind {
|
|
714
|
+
| "event" | "error" => Some((s.thenKind, s.thenElements->Array.map(e => e.name)))
|
|
715
|
+
| "noEvent" => Some(("noEvent", []))
|
|
716
|
+
// The empty `then` above, and a `then` that is not about this command's
|
|
717
|
+
// result — a side effect, a published command.
|
|
718
|
+
| _ => None
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let byPluginComponentCommand = (xs: array<commandOutcomes>): array<commandOutcomes> =>
|
|
722
|
+
xs->Array.toSorted((a, b) =>
|
|
723
|
+
switch String.compare(a.plugin, b.plugin) {
|
|
724
|
+
| 0. =>
|
|
725
|
+
switch String.compare(a.component, b.component) {
|
|
726
|
+
| 0. => String.compare(a.command, b.command)
|
|
727
|
+
| c => c
|
|
728
|
+
}
|
|
729
|
+
| c => c
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
/** A command with no readable outcome is left out: an entry must say something. */
|
|
734
|
+
let commandOutcomes = (~plugin: string, ~corpora: array<corpus>): array<commandOutcomes> => {
|
|
735
|
+
let entries: array<commandOutcomes> = []
|
|
736
|
+
corpora->Array.forEach(c =>
|
|
737
|
+
c.scenarios->Array.forEach(s =>
|
|
738
|
+
switch (s.whenKind, s.whenElements->Array.get(0), shownOutcomeOf(s)) {
|
|
739
|
+
| ("command", Some(command), Some((kind, names))) =>
|
|
740
|
+
let entry = switch entries->Array.find(
|
|
741
|
+
e => e.component == c.component && e.command == command.name,
|
|
742
|
+
) {
|
|
743
|
+
| Some(e) => e
|
|
744
|
+
| None =>
|
|
745
|
+
let e: commandOutcomes = {
|
|
746
|
+
plugin,
|
|
747
|
+
component: c.component,
|
|
748
|
+
command: command.name,
|
|
749
|
+
outcomes: [],
|
|
750
|
+
}
|
|
751
|
+
entries->Array.push(e)
|
|
752
|
+
e
|
|
753
|
+
}
|
|
754
|
+
switch entry.outcomes->Array.find(o => o.kind == kind && o.names == names) {
|
|
755
|
+
| Some(o) =>
|
|
756
|
+
if !(o.scenarios->Array.includes(s.title)) {
|
|
757
|
+
o.scenarios->Array.push(s.title)
|
|
758
|
+
}
|
|
759
|
+
| None => entry.outcomes->Array.push({kind, names, scenarios: [s.title]})
|
|
760
|
+
}
|
|
761
|
+
| _ => ()
|
|
762
|
+
}
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
entries->byPluginComponentCommand
|
|
766
|
+
}
|
|
767
|
+
|
|
669
768
|
// ── The three verdicts ──────────────────────────────────────────────────────
|
|
670
769
|
|
|
671
770
|
type finding = {
|
|
@@ -757,14 +856,24 @@ let compare = (
|
|
|
757
856
|
// effect somewhere agrees with that rather than narrowing it — the corpus
|
|
758
857
|
// covers the states somebody wrote a scenario for, and silence about the
|
|
759
858
|
// rest is not refusal. What DOES refute the claim is a state the command was
|
|
760
|
-
// exercised in and
|
|
859
|
+
// exercised in and **refused**: that is the switch and the behaviour
|
|
761
860
|
// disagreeing about the same row.
|
|
861
|
+
//
|
|
862
|
+
// 🚨 **Refused, not merely inert.** `Unrestricted` claims the command is
|
|
863
|
+
// never refused — it does not claim the command always emits. A state where a
|
|
864
|
+
// scenario shows it accepted and silent AGREES with that: returning `Ok([])`
|
|
865
|
+
// for a command that would change nothing is this codebase's idempotency
|
|
866
|
+
// convention, required because commands arrive at least once. Reading
|
|
867
|
+
// `inertStates` here marked every such command contradicted, which is the
|
|
868
|
+
// opposite of what the corpus showed — a verdict redelivered, or landing
|
|
869
|
+
// after deactivation, is precisely the case the switch declared legal so that
|
|
870
|
+
// the reporting slice would not retry forever.
|
|
762
871
|
| None if cmd.allowedStatesSource == Some("unrestricted") =>
|
|
763
|
-
derived.
|
|
872
|
+
derived.refusedStates->Array.forEach(state =>
|
|
764
873
|
add(
|
|
765
874
|
"contradicted",
|
|
766
875
|
[state],
|
|
767
|
-
`the switch declares it legal in every state, and a scenario from "${state}" ` ++ `shows it refused
|
|
876
|
+
`the switch declares it legal in every state, and a scenario from "${state}" ` ++ `shows it refused`,
|
|
768
877
|
)
|
|
769
878
|
)
|
|
770
879
|
| None =>
|
|
@@ -928,6 +1037,7 @@ let runPlugin = async (
|
|
|
928
1037
|
~pluginDir: string,
|
|
929
1038
|
~findings: array<finding>,
|
|
930
1039
|
~opaque: array<opaque>,
|
|
1040
|
+
~outcomes: array<commandOutcomes>,
|
|
931
1041
|
): result<array<derivedCommand>, string> =>
|
|
932
1042
|
switch await readDeclared(~pluginDir) {
|
|
933
1043
|
| Error(msg) => Error(msg)
|
|
@@ -937,6 +1047,8 @@ let runPlugin = async (
|
|
|
937
1047
|
readCorpus,
|
|
938
1048
|
)
|
|
939
1049
|
|
|
1050
|
+
outcomes->Array.pushMany(commandOutcomes(~plugin, ~corpora))
|
|
1051
|
+
|
|
940
1052
|
// Every corpus, not only the ones the walk goes on to use: the kinds that
|
|
941
1053
|
// are unreadable in full — extension points, automation and translation
|
|
942
1054
|
// slices — are exactly the ones that sit outside the view/writable folders
|
|
@@ -1106,13 +1218,16 @@ let goldenPath = (~root: appRoot) => NodePath.join([root.dir, "schema", "lifecyc
|
|
|
1106
1218
|
ever exercised, which is a sharper "missing scenario" than a verdict.
|
|
1107
1219
|
- `opaque` — the corpora the walk cannot read, so a consumer can say "not
|
|
1108
1220
|
analysed" where it would otherwise say "not covered".
|
|
1221
|
+
- `outcomes` — which command yields which event or error, and the scenarios
|
|
1222
|
+
that show it, sorted by plugin, component and command.
|
|
1109
1223
|
|
|
1110
|
-
`
|
|
1111
|
-
change. */
|
|
1224
|
+
`version` is the schema's own version, bumped when a consumer would have to
|
|
1225
|
+
change. An added section is not such a change. */
|
|
1112
1226
|
let reportJson = (
|
|
1113
1227
|
~findings: array<finding>,
|
|
1114
1228
|
~opaque: array<opaque>,
|
|
1115
1229
|
~derived: array<(string, derivedCommand)>,
|
|
1230
|
+
~outcomes: array<commandOutcomes>,
|
|
1116
1231
|
~failures: array<string>,
|
|
1117
1232
|
): string => {
|
|
1118
1233
|
let strs = xs => JSON.Encode.array(xs->Array.map(JSON.Encode.string))
|
|
@@ -1149,11 +1264,30 @@ let reportJson = (
|
|
|
1149
1264
|
("unreadable", JSON.Encode.int(o.unreadable)),
|
|
1150
1265
|
])
|
|
1151
1266
|
|
|
1267
|
+
let shownOutcomeJson = (o: shownOutcome) =>
|
|
1268
|
+
obj([
|
|
1269
|
+
("kind", JSON.Encode.string(o.kind)),
|
|
1270
|
+
("names", strs(o.names)),
|
|
1271
|
+
("scenarios", strs(o.scenarios)),
|
|
1272
|
+
])
|
|
1273
|
+
|
|
1274
|
+
let commandOutcomesJson = (e: commandOutcomes) =>
|
|
1275
|
+
obj([
|
|
1276
|
+
("plugin", JSON.Encode.string(e.plugin)),
|
|
1277
|
+
("component", JSON.Encode.string(e.component)),
|
|
1278
|
+
("command", JSON.Encode.string(e.command)),
|
|
1279
|
+
("outcomes", JSON.Encode.array(e.outcomes->Array.map(shownOutcomeJson))),
|
|
1280
|
+
])
|
|
1281
|
+
|
|
1152
1282
|
JSON.stringify(
|
|
1153
1283
|
obj([
|
|
1154
1284
|
("version", JSON.Encode.int(1)),
|
|
1155
1285
|
("findings", JSON.Encode.array(findings->Array.map(findingJson))),
|
|
1156
1286
|
("commands", JSON.Encode.array(derived->Array.map(commandJson))),
|
|
1287
|
+
(
|
|
1288
|
+
"outcomes",
|
|
1289
|
+
JSON.Encode.array(outcomes->byPluginComponentCommand->Array.map(commandOutcomesJson)),
|
|
1290
|
+
),
|
|
1157
1291
|
("opaque", JSON.Encode.array(opaque->Array.map(opaqueJson))),
|
|
1158
1292
|
("unreadable", strs(failures)),
|
|
1159
1293
|
]),
|
|
@@ -1245,6 +1379,7 @@ let writeOrCompare = (~path: string, ~actual: string, ~label: string, ~drifted:
|
|
|
1245
1379
|
let main = async () => {
|
|
1246
1380
|
let findings = []
|
|
1247
1381
|
let opaque = []
|
|
1382
|
+
let outcomes = []
|
|
1248
1383
|
let failures = []
|
|
1249
1384
|
let drifted = []
|
|
1250
1385
|
let allDerived = []
|
|
@@ -1302,7 +1437,7 @@ let main = async () => {
|
|
|
1302
1437
|
| Some(pluginDir) =>
|
|
1303
1438
|
let plugin = NodePath.basename(pluginDir)
|
|
1304
1439
|
let qualified = `${example}/${plugin}`
|
|
1305
|
-
switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque) {
|
|
1440
|
+
switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque, ~outcomes) {
|
|
1306
1441
|
| Ok(commands) =>
|
|
1307
1442
|
commands->Array.forEach(c => {
|
|
1308
1443
|
derived->Array.push(c)
|
|
@@ -1345,7 +1480,7 @@ let main = async () => {
|
|
|
1345
1480
|
let contradicted = of_("contradicted")
|
|
1346
1481
|
|
|
1347
1482
|
if json {
|
|
1348
|
-
Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~failures))
|
|
1483
|
+
Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~outcomes, ~failures))
|
|
1349
1484
|
} else {
|
|
1350
1485
|
["contradicted", "unverified", "undeclared", "level", "ambiguous"]->Array.forEach(severity => {
|
|
1351
1486
|
let group = of_(severity)
|
|
@@ -1378,4 +1513,9 @@ let main = async () => {
|
|
|
1378
1513
|
}
|
|
1379
1514
|
}
|
|
1380
1515
|
|
|
1381
|
-
|
|
1516
|
+
// 🚨 **No top-level call.** `../../run-check-lifecycle.mjs` invokes [main]; this
|
|
1517
|
+
// module only defines it. While the call was here, importing the module *ran the
|
|
1518
|
+
// whole check* and then called `NodeProcess.exit`, so nothing in this file could
|
|
1519
|
+
// be reached from a test — including [deriveCommands] and [compare], which are
|
|
1520
|
+
// pure and are where every verdict is actually decided. That is the same trap
|
|
1521
|
+
// `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;
|
|
@@ -484,6 +489,109 @@ function deriveCommands(component, observations, labelled) {
|
|
|
484
489
|
});
|
|
485
490
|
}
|
|
486
491
|
|
|
492
|
+
function shownOutcomeOf(s) {
|
|
493
|
+
let match = s.thenKind;
|
|
494
|
+
switch (match) {
|
|
495
|
+
case "error" :
|
|
496
|
+
case "event" :
|
|
497
|
+
break;
|
|
498
|
+
case "noEvent" :
|
|
499
|
+
return [
|
|
500
|
+
"noEvent",
|
|
501
|
+
[]
|
|
502
|
+
];
|
|
503
|
+
default:
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
return [
|
|
507
|
+
s.thenKind,
|
|
508
|
+
s.thenElements.map(e => e.name)
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function byPluginComponentCommand(xs) {
|
|
513
|
+
return xs.toSorted((a, b) => {
|
|
514
|
+
let c = Primitive_string.compare(a.plugin, b.plugin);
|
|
515
|
+
if (c !== 0) {
|
|
516
|
+
return c;
|
|
517
|
+
}
|
|
518
|
+
let c$1 = Primitive_string.compare(a.component, b.component);
|
|
519
|
+
if (c$1 !== 0) {
|
|
520
|
+
return c$1;
|
|
521
|
+
} else {
|
|
522
|
+
return Primitive_string.compare(a.command, b.command);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function commandOutcomes(plugin, corpora) {
|
|
528
|
+
let entries = [];
|
|
529
|
+
corpora.forEach(c => {
|
|
530
|
+
c.scenarios.forEach(s => {
|
|
531
|
+
let match = s.whenKind;
|
|
532
|
+
let match$1 = s.whenElements[0];
|
|
533
|
+
let match$2 = shownOutcomeOf(s);
|
|
534
|
+
if (match !== "command") {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (match$1 === undefined) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (match$2 === undefined) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
let names = match$2[1];
|
|
544
|
+
let kind = match$2[0];
|
|
545
|
+
let e = entries.find(e => {
|
|
546
|
+
if (e.component === c.component) {
|
|
547
|
+
return e.command === match$1.name;
|
|
548
|
+
} else {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
let entry;
|
|
553
|
+
if (e !== undefined) {
|
|
554
|
+
entry = e;
|
|
555
|
+
} else {
|
|
556
|
+
let e_component = c.component;
|
|
557
|
+
let e_command = match$1.name;
|
|
558
|
+
let e_outcomes = [];
|
|
559
|
+
let e$1 = {
|
|
560
|
+
plugin: plugin,
|
|
561
|
+
component: e_component,
|
|
562
|
+
command: e_command,
|
|
563
|
+
outcomes: e_outcomes
|
|
564
|
+
};
|
|
565
|
+
entries.push(e$1);
|
|
566
|
+
entry = e$1;
|
|
567
|
+
}
|
|
568
|
+
let o = entry.outcomes.find(o => {
|
|
569
|
+
if (o.kind === kind) {
|
|
570
|
+
return Primitive_object.equal(o.names, names);
|
|
571
|
+
} else {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
if (o !== undefined) {
|
|
576
|
+
if (!o.scenarios.includes(s.title)) {
|
|
577
|
+
o.scenarios.push(s.title);
|
|
578
|
+
return;
|
|
579
|
+
} else {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
entry.outcomes.push({
|
|
584
|
+
kind: kind,
|
|
585
|
+
names: names,
|
|
586
|
+
scenarios: [s.title]
|
|
587
|
+
});
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
return byPluginComponentCommand(entries);
|
|
593
|
+
}
|
|
594
|
+
|
|
487
595
|
function allUnverified(cmd, add, why) {
|
|
488
596
|
let states = cmd.allowedStates;
|
|
489
597
|
if (states !== undefined && states.length !== 0) {
|
|
@@ -531,7 +639,7 @@ function compare(plugin, writable, derived, findings) {
|
|
|
531
639
|
add("unverified", states, `the switch declares ` + states.length.toString() + ` state(s) and no scenario shows the command taking effect anywhere`);
|
|
532
640
|
}
|
|
533
641
|
} else if (Primitive_object.equal(declared.allowedStatesSource, "unrestricted")) {
|
|
534
|
-
derived.
|
|
642
|
+
derived.refusedStates.forEach(state => add("contradicted", [state], `the switch declares it legal in every state, and a scenario from "` + state + `" shows it refused`));
|
|
535
643
|
} else if (derived.allowedStates.length !== 0) {
|
|
536
644
|
add("undeclared", derived.allowedStates, `scenarios show it taking effect from ` + derived.allowedStates.join(", ") + `, and it declares no edge`);
|
|
537
645
|
}
|
|
@@ -654,7 +762,7 @@ function isWritablePath(path) {
|
|
|
654
762
|
}
|
|
655
763
|
}
|
|
656
764
|
|
|
657
|
-
async function runPlugin(plugin, pluginDir, findings, opaque) {
|
|
765
|
+
async function runPlugin(plugin, pluginDir, findings, opaque, outcomes) {
|
|
658
766
|
let msg = await readDeclared(pluginDir);
|
|
659
767
|
if (msg.TAG !== "Ok") {
|
|
660
768
|
return {
|
|
@@ -664,6 +772,7 @@ async function runPlugin(plugin, pluginDir, findings, opaque) {
|
|
|
664
772
|
}
|
|
665
773
|
let declared = msg._0;
|
|
666
774
|
let corpora = Stdlib_Array.filterMap(filesUnder(Nodepath.join(pluginDir, "tests"), ".gwt.json"), readCorpus);
|
|
775
|
+
outcomes.push(...commandOutcomes(plugin, corpora));
|
|
667
776
|
corpora.forEach(c => {
|
|
668
777
|
let unreadable = c.scenarios.filter(s => s.whenElements.length === 0);
|
|
669
778
|
if (unreadable.length !== 0) {
|
|
@@ -800,7 +909,7 @@ function goldenPath(root) {
|
|
|
800
909
|
return Nodepath.join(root.dir, "schema", "lifecycle-model.json");
|
|
801
910
|
}
|
|
802
911
|
|
|
803
|
-
function reportJson(findings, opaque, derived, failures) {
|
|
912
|
+
function reportJson(findings, opaque, derived, outcomes, failures) {
|
|
804
913
|
let findingJson = f => Object.fromEntries([
|
|
805
914
|
[
|
|
806
915
|
"verdict",
|
|
@@ -886,6 +995,38 @@ function reportJson(findings, opaque, derived, failures) {
|
|
|
886
995
|
o.unreadable
|
|
887
996
|
]
|
|
888
997
|
]);
|
|
998
|
+
let shownOutcomeJson = o => Object.fromEntries([
|
|
999
|
+
[
|
|
1000
|
+
"kind",
|
|
1001
|
+
o.kind
|
|
1002
|
+
],
|
|
1003
|
+
[
|
|
1004
|
+
"names",
|
|
1005
|
+
o.names.map(prim => prim)
|
|
1006
|
+
],
|
|
1007
|
+
[
|
|
1008
|
+
"scenarios",
|
|
1009
|
+
o.scenarios.map(prim => prim)
|
|
1010
|
+
]
|
|
1011
|
+
]);
|
|
1012
|
+
let commandOutcomesJson = e => Object.fromEntries([
|
|
1013
|
+
[
|
|
1014
|
+
"plugin",
|
|
1015
|
+
e.plugin
|
|
1016
|
+
],
|
|
1017
|
+
[
|
|
1018
|
+
"component",
|
|
1019
|
+
e.component
|
|
1020
|
+
],
|
|
1021
|
+
[
|
|
1022
|
+
"command",
|
|
1023
|
+
e.command
|
|
1024
|
+
],
|
|
1025
|
+
[
|
|
1026
|
+
"outcomes",
|
|
1027
|
+
e.outcomes.map(shownOutcomeJson)
|
|
1028
|
+
]
|
|
1029
|
+
]);
|
|
889
1030
|
return JSON.stringify(Object.fromEntries([
|
|
890
1031
|
[
|
|
891
1032
|
"version",
|
|
@@ -899,6 +1040,10 @@ function reportJson(findings, opaque, derived, failures) {
|
|
|
899
1040
|
"commands",
|
|
900
1041
|
derived.map(commandJson)
|
|
901
1042
|
],
|
|
1043
|
+
[
|
|
1044
|
+
"outcomes",
|
|
1045
|
+
byPluginComponentCommand(outcomes).map(commandOutcomesJson)
|
|
1046
|
+
],
|
|
902
1047
|
[
|
|
903
1048
|
"opaque",
|
|
904
1049
|
opaque.map(opaqueJson)
|
|
@@ -990,6 +1135,7 @@ function writeOrCompare(path, actual, label, drifted) {
|
|
|
990
1135
|
async function main() {
|
|
991
1136
|
let findings = [];
|
|
992
1137
|
let opaque = [];
|
|
1138
|
+
let outcomes = [];
|
|
993
1139
|
let failures = [];
|
|
994
1140
|
let drifted = [];
|
|
995
1141
|
let allDerived = [];
|
|
@@ -1019,7 +1165,7 @@ async function main() {
|
|
|
1019
1165
|
if (pluginDir !== undefined) {
|
|
1020
1166
|
let plugin = Nodepath.basename(pluginDir);
|
|
1021
1167
|
let qualified = example + `/` + plugin;
|
|
1022
|
-
let commands = await runPlugin(qualified, pluginDir, findings, opaque);
|
|
1168
|
+
let commands = await runPlugin(qualified, pluginDir, findings, opaque, outcomes);
|
|
1023
1169
|
if (commands.TAG === "Ok") {
|
|
1024
1170
|
let commands$1 = commands._0;
|
|
1025
1171
|
commands$1.forEach(c => {
|
|
@@ -1052,7 +1198,7 @@ async function main() {
|
|
|
1052
1198
|
let of_ = severity => findings.filter(f => f.severity === severity);
|
|
1053
1199
|
let contradicted = of_("contradicted");
|
|
1054
1200
|
if (json) {
|
|
1055
|
-
console.log(reportJson(findings, opaque, allDerived, failures));
|
|
1201
|
+
console.log(reportJson(findings, opaque, allDerived, outcomes, failures));
|
|
1056
1202
|
} else {
|
|
1057
1203
|
[
|
|
1058
1204
|
"contradicted",
|
|
@@ -1086,8 +1232,6 @@ async function main() {
|
|
|
1086
1232
|
}
|
|
1087
1233
|
}
|
|
1088
1234
|
|
|
1089
|
-
main();
|
|
1090
|
-
|
|
1091
1235
|
export {
|
|
1092
1236
|
repoRoot,
|
|
1093
1237
|
examplesDir,
|
|
@@ -1128,6 +1272,9 @@ export {
|
|
|
1128
1272
|
sameRow,
|
|
1129
1273
|
observe,
|
|
1130
1274
|
deriveCommands,
|
|
1275
|
+
shownOutcomeOf,
|
|
1276
|
+
byPluginComponentCommand,
|
|
1277
|
+
commandOutcomes,
|
|
1131
1278
|
allUnverified,
|
|
1132
1279
|
compare,
|
|
1133
1280
|
pluginDirsIn,
|
|
@@ -27,6 +27,15 @@ type t = {
|
|
|
27
27
|
/** Send a message to a person, and say which channels this deployment can
|
|
28
28
|
attempt at all. See `Messaging.provider`. */
|
|
29
29
|
messaging: Messaging.provider,
|
|
30
|
+
/** Make, group and unmake principals, and say which of those this
|
|
31
|
+
deployment's provider can do at all. Administrative only — authenticating
|
|
32
|
+
one is `Auth_Adapter.Provider`'s. See `IdentityProvider.t`. */
|
|
33
|
+
identityProvider: IdentityProvider.t,
|
|
34
|
+
/** Unguessable material and a one-way function over it. Injected rather than
|
|
35
|
+
imported so a test can pin the secret a flow is about to use — a suite
|
|
36
|
+
cannot assert what a flow does with a value it cannot predict. See
|
|
37
|
+
`Secrets.t`. */
|
|
38
|
+
secrets: Secrets.t,
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
/**
|
|
@@ -54,4 +63,12 @@ let none: t = {
|
|
|
54
63
|
~recipient as _,
|
|
55
64
|
~message as _,
|
|
56
65
|
) => Error(Unavailable("no messaging provider is configured for this platform"))),
|
|
66
|
+
identityProvider: IdentityProvider.unavailable(
|
|
67
|
+
~reason="no identity provider is configured for this platform",
|
|
68
|
+
),
|
|
69
|
+
// Refusing rather than falling back to a weak source. Every runtime has a
|
|
70
|
+
// cryptographic one, so a platform reaching this arm has skipped wiring rather
|
|
71
|
+
// than declined to provision — and a guessable token would be accepted
|
|
72
|
+
// everywhere while looking exactly like a real one.
|
|
73
|
+
secrets: Secrets.unavailable(~reason="no secret source is configured for this platform"),
|
|
57
74
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Secrets$Reventless from "./Secrets.res.mjs";
|
|
3
4
|
import * as Messaging$Reventless from "./Messaging.res.mjs";
|
|
5
|
+
import * as IdentityProvider$Reventless from "./IdentityProvider.res.mjs";
|
|
4
6
|
|
|
5
7
|
async function none_geocode(param) {
|
|
6
8
|
return {
|
|
@@ -20,9 +22,15 @@ let none_messaging = Messaging$Reventless.makeProvider([], [], async (param, par
|
|
|
20
22
|
}
|
|
21
23
|
}));
|
|
22
24
|
|
|
25
|
+
let none_identityProvider = IdentityProvider$Reventless.unavailable("no identity provider is configured for this platform");
|
|
26
|
+
|
|
27
|
+
let none_secrets = Secrets$Reventless.unavailable("no secret source is configured for this platform");
|
|
28
|
+
|
|
23
29
|
let none = {
|
|
24
30
|
geocode: none_geocode,
|
|
25
|
-
messaging: none_messaging
|
|
31
|
+
messaging: none_messaging,
|
|
32
|
+
identityProvider: none_identityProvider,
|
|
33
|
+
secrets: none_secrets
|
|
26
34
|
};
|
|
27
35
|
|
|
28
36
|
export {
|
|
@@ -24,6 +24,16 @@ type t =
|
|
|
24
24
|
declaration, because a plugin that named `Sms` would fail a deploy it
|
|
25
25
|
could have run on email. */
|
|
26
26
|
| Messaging
|
|
27
|
+
/** Making, grouping and unmaking principals, reached through
|
|
28
|
+
`Capabilities.identityProvider`. No payload, for the reason `Geocoding`
|
|
29
|
+
has none: one identity provider per deployment is a real answer, and a
|
|
30
|
+
deployment wanting a second pool is the `{alternatives, min}` question
|
|
31
|
+
`Messaging` already answers by publishing what it provisioned.
|
|
32
|
+
|
|
33
|
+
Not authentication. Token issuance and session handling are
|
|
34
|
+
`Auth_Adapter.Provider`'s, read at a different time — two seams over one
|
|
35
|
+
provider, the way this type and `Capabilities.t` are already two. */
|
|
36
|
+
| IdentityProvider
|
|
27
37
|
|
|
28
38
|
/** The spelling carried in `pluginStructure` and in `capabilities.json`.
|
|
29
39
|
Persisted structures hold strings, not enum members, so a plugin built
|
|
@@ -32,6 +42,7 @@ let toString = (need: t): string =>
|
|
|
32
42
|
switch need {
|
|
33
43
|
| Geocoding => "Geocoding"
|
|
34
44
|
| Messaging => "Messaging"
|
|
45
|
+
| IdentityProvider => "IdentityProvider"
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
/** The inverse, for readers of a persisted structure or a committed manifest.
|
|
@@ -41,6 +52,7 @@ let fromString = (name: string): option<t> =>
|
|
|
41
52
|
switch name {
|
|
42
53
|
| "Geocoding" => Some(Geocoding)
|
|
43
54
|
| "Messaging" => Some(Messaging)
|
|
55
|
+
| "IdentityProvider" => Some(IdentityProvider)
|
|
44
56
|
| _ => None
|
|
45
57
|
}
|
|
46
58
|
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
4
|
|
|
5
5
|
function toString(need) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
switch (need) {
|
|
7
|
+
case "Geocoding" :
|
|
8
|
+
return "Geocoding";
|
|
9
|
+
case "Messaging" :
|
|
10
|
+
return "Messaging";
|
|
11
|
+
case "IdentityProvider" :
|
|
12
|
+
return "IdentityProvider";
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -14,6 +17,8 @@ function fromString(name) {
|
|
|
14
17
|
switch (name) {
|
|
15
18
|
case "Geocoding" :
|
|
16
19
|
return "Geocoding";
|
|
20
|
+
case "IdentityProvider" :
|
|
21
|
+
return "IdentityProvider";
|
|
17
22
|
case "Messaging" :
|
|
18
23
|
return "Messaging";
|
|
19
24
|
default:
|