@reventlessdev/reventless-spec 3.0.0-alpha.124 → 3.0.0-alpha.126
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 +5 -2
- package/run-certify-trait.mjs +2 -0
- package/run-graft-trait.mjs +2 -0
- package/run-trait-manifest.mjs +2 -0
- package/schema/platform-api.graphql +17 -3
- package/src/components/Aggregate.res +23 -0
- package/src/components/AutomationSlice.res +29 -3
- package/src/components/CapabilityManifest.res +52 -24
- package/src/components/CapabilityManifest.res.mjs +35 -11
- package/src/components/InboundTranslationSlice.res +14 -0
- package/src/components/OutboundTranslationSlice.res +24 -0
- package/src/components/Plugin.res +55 -2
- package/src/components/Plugin.res.mjs +23 -1
- package/src/components/StateAnnotations.res +2 -2
- package/src/components/StateChangeSlice.res +22 -0
- package/src/components/TraitCertificate.res +105 -0
- package/src/components/TraitCertificate.res.mjs +65 -0
- package/src/components/TraitManifest.res +90 -0
- package/src/components/TraitManifest.res.mjs +48 -0
- package/src/generator/CertifyTrait.res +190 -0
- package/src/generator/CertifyTrait.res.mjs +154 -0
- package/src/generator/GraftTrait.res +230 -0
- package/src/generator/GraftTrait.res.mjs +193 -0
- package/src/generator/PlatformCodegen.res +44 -30
- package/src/generator/PlatformCodegen.res.mjs +36 -17
- package/src/generator/TraitManifestCli.res +138 -0
- package/src/generator/TraitManifestCli.res.mjs +105 -0
- package/src/semantic/Capabilities.res +19 -3
- package/src/semantic/Capabilities.res.mjs +18 -2
- package/src/semantic/CapabilityNeed.res +81 -0
- package/src/semantic/CapabilityNeed.res.mjs +46 -0
- package/src/semantic/Messaging.res +127 -0
- package/src/semantic/Messaging.res.mjs +57 -0
- package/src/types/Trait.res +62 -0
- package/src/types/Trait.res.mjs +18 -0
- package/src/types/Transition.res +70 -0
- package/src/types/Transition.res.mjs +36 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
What a trait's conformance suite proved, against one host, as data.
|
|
3
|
+
|
|
4
|
+
The suite has run inside a consumer's build since the emitter shipped. What was
|
|
5
|
+
missing is any way for something downstream to *read* the result: it was console
|
|
6
|
+
output, so a listing could carry a claim nobody could check and a CI gate had
|
|
7
|
+
nothing to gate on. This is that result, typed and versioned like every other
|
|
8
|
+
persisted shape here.
|
|
9
|
+
|
|
10
|
+
## What it is evidence of, and what it is not
|
|
11
|
+
|
|
12
|
+
It says: this trait, at this version, asserted these rules through this host, and
|
|
13
|
+
they held. It does **not** say the host is correct — the suite deliberately covers
|
|
14
|
+
only what the trait has an opinion about, and the host's own lifecycle refusals,
|
|
15
|
+
authorization and projections are asserted by the host's own tests and are none of
|
|
16
|
+
the trait's business.
|
|
17
|
+
|
|
18
|
+
So a listing that carries this can say "verified against its declared hosts" and
|
|
19
|
+
cannot say "this application works". The distinction is the whole reason the
|
|
20
|
+
assertion names travel with the counts: a reader can see *what* was proved rather
|
|
21
|
+
than trusting a number.
|
|
22
|
+
|
|
23
|
+
## Why the results come from a test report
|
|
24
|
+
|
|
25
|
+
The framework does not own the runner. A consumer runs their own suite, their own
|
|
26
|
+
way, and hands the report here — so this module is a pure transformation with no
|
|
27
|
+
opinion about Jest, CI, or where files live. `fromReport` is the whole of it.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** One assertion, and whether it held. Named, not numbered: a count that changed
|
|
31
|
+
tells a reader nothing, and a name that disappeared tells them everything. */
|
|
32
|
+
@schema
|
|
33
|
+
type assertion = {name: string, passed: bool}
|
|
34
|
+
|
|
35
|
+
@schema
|
|
36
|
+
type t = {
|
|
37
|
+
/** The trait's package name — its identity everywhere else too. */
|
|
38
|
+
trait: string,
|
|
39
|
+
traitVersion: string,
|
|
40
|
+
/** The framework the suite ran against. A trait is certified against a
|
|
41
|
+
framework version, not in the abstract: `marketplace`'s Verified tier is
|
|
42
|
+
"passes its suite against the latest framework", and without this the claim
|
|
43
|
+
cannot be aged out. */
|
|
44
|
+
framework: string,
|
|
45
|
+
/** The bound host's component name — `Spec.name` from the binding. */
|
|
46
|
+
host: string,
|
|
47
|
+
/** The suite's own title, as the trait composes it. Carried so a reader can
|
|
48
|
+
find the run this came from without reconstructing the string. */
|
|
49
|
+
suite: string,
|
|
50
|
+
assertions: array<assertion>,
|
|
51
|
+
passed: int,
|
|
52
|
+
failed: int,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
The badge rule, stated once.
|
|
57
|
+
|
|
58
|
+
Every assertion held, and there was at least one. The second half matters more
|
|
59
|
+
than it looks: a binding that registers nothing produces an empty, all-passing
|
|
60
|
+
certificate, and "zero assertions, zero failures" is exactly the shape a broken
|
|
61
|
+
graft takes. A certificate that called that verified would certify silence.
|
|
62
|
+
*/
|
|
63
|
+
let verified = (certificate: t) =>
|
|
64
|
+
certificate.failed == 0 && certificate.assertions->Array.length > 0
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
Build a certificate from a suite's results.
|
|
68
|
+
|
|
69
|
+
`results` is `(assertion name, passed)` in the order the suite registered them —
|
|
70
|
+
whatever produced them. Counts are derived rather than passed in, so a caller
|
|
71
|
+
cannot hand over a total that disagrees with the list it accompanies.
|
|
72
|
+
*/
|
|
73
|
+
let fromReport = (
|
|
74
|
+
~trait: string,
|
|
75
|
+
~traitVersion: string,
|
|
76
|
+
~framework: string,
|
|
77
|
+
~host: string,
|
|
78
|
+
~suite: string,
|
|
79
|
+
~results: array<(string, bool)>,
|
|
80
|
+
): t => {
|
|
81
|
+
let assertions = results->Array.map(((name, passed)) => {name, passed})
|
|
82
|
+
{
|
|
83
|
+
trait,
|
|
84
|
+
traitVersion,
|
|
85
|
+
framework,
|
|
86
|
+
host,
|
|
87
|
+
suite,
|
|
88
|
+
assertions,
|
|
89
|
+
passed: assertions->Array.filter(a => a.passed)->Array.length,
|
|
90
|
+
failed: assertions->Array.filter(a => !a.passed)->Array.length,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Deterministic rendering: 2-space indent, trailing newline. Rebuilding from an
|
|
95
|
+
unchanged run must produce a byte-identical file, so a committed certificate
|
|
96
|
+
does not churn and a diff means something moved. */
|
|
97
|
+
let render = (certificate: t): string =>
|
|
98
|
+
JSON.stringify(certificate->Util_Sury.toJson(schema), ~space=2) ++ "\n"
|
|
99
|
+
|
|
100
|
+
/** A one-line summary for a build log — the counts plus the verdict, so a
|
|
101
|
+
console reader and a machine reader agree without either restating the rule. */
|
|
102
|
+
let summarize = (certificate: t): string =>
|
|
103
|
+
`${certificate.trait}@${certificate.traitVersion} → ${certificate.host}: ` ++
|
|
104
|
+
`${certificate.passed->Int.toString}/${(certificate.assertions->Array.length)
|
|
105
|
+
->Int.toString} assertions, ` ++ (certificate->verified ? "verified" : "NOT verified")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
5
|
+
|
|
6
|
+
let assertionSchema = Sury.$schema(s => ({
|
|
7
|
+
name: s.m(Sury.string),
|
|
8
|
+
passed: s.m(Sury.bool)
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
let schema = Sury.$schema(s => ({
|
|
12
|
+
trait: s.m(Sury.string),
|
|
13
|
+
traitVersion: s.m(Sury.string),
|
|
14
|
+
framework: s.m(Sury.string),
|
|
15
|
+
host: s.m(Sury.string),
|
|
16
|
+
suite: s.m(Sury.string),
|
|
17
|
+
assertions: s.m(Sury.array(assertionSchema)),
|
|
18
|
+
passed: s.m(Sury.int),
|
|
19
|
+
failed: s.m(Sury.int)
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
function verified(certificate) {
|
|
23
|
+
if (certificate.failed === 0) {
|
|
24
|
+
return certificate.assertions.length !== 0;
|
|
25
|
+
} else {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fromReport(trait, traitVersion, framework, host, suite, results) {
|
|
31
|
+
let assertions = results.map(param => ({
|
|
32
|
+
name: param[0],
|
|
33
|
+
passed: param[1]
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
trait: trait,
|
|
37
|
+
traitVersion: traitVersion,
|
|
38
|
+
framework: framework,
|
|
39
|
+
host: host,
|
|
40
|
+
suite: suite,
|
|
41
|
+
assertions: assertions,
|
|
42
|
+
passed: assertions.filter(a => a.passed).length,
|
|
43
|
+
failed: assertions.filter(a => !a.passed).length
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function render(certificate) {
|
|
48
|
+
return JSON.stringify(Util_Sury$Reventless.toJson(certificate, schema), undefined, 2) + "\n";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function summarize(certificate) {
|
|
52
|
+
return certificate.trait + `@` + certificate.traitVersion + ` → ` + certificate.host + `: ` + (certificate.passed.toString() + `/` + certificate.assertions.length.toString() + ` assertions, `) + (
|
|
53
|
+
verified(certificate) ? "verified" : "NOT verified"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
assertionSchema,
|
|
59
|
+
schema,
|
|
60
|
+
verified,
|
|
61
|
+
fromReport,
|
|
62
|
+
render,
|
|
63
|
+
summarize,
|
|
64
|
+
}
|
|
65
|
+
/* assertionSchema Not a pure module */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A trait's listing metadata, derived rather than written.
|
|
3
|
+
|
|
4
|
+
The old design had a hand-written `trait.yaml`. Everything in it is already a
|
|
5
|
+
fact somewhere typed — the package declares the identity, the trait exports what
|
|
6
|
+
it needs, and the emitter's config schema declares what a graft must be told — so
|
|
7
|
+
a second copy could only be a place for those to disagree.
|
|
8
|
+
|
|
9
|
+
## What is here, and what a run has to supply
|
|
10
|
+
|
|
11
|
+
This is the **static** half: identity, needs, and the config surface. All of it is
|
|
12
|
+
readable without running anything, which is what a listing wants before anybody
|
|
13
|
+
installs.
|
|
14
|
+
|
|
15
|
+
The **dynamic** half is `TraitCertificate` — what the suite proved, against a
|
|
16
|
+
host. The two are deliberately separate artifacts: a listing carries the manifest
|
|
17
|
+
always, and a certificate only once somebody has run the suite, so folding them
|
|
18
|
+
together would force a listing to claim a proof it does not have.
|
|
19
|
+
|
|
20
|
+
## What is deliberately absent
|
|
21
|
+
|
|
22
|
+
The assertion list. What a trait certifies cannot be enumerated without running
|
|
23
|
+
the suite — the assertions are registered by a functor, not declared as data —
|
|
24
|
+
so a manifest that listed them would be restating the certificate from memory.
|
|
25
|
+
A reader who wants to know what was proved reads a certificate.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** One field the emitter's config declares. `required` is read off the schema,
|
|
29
|
+
so an optional field cannot be listed as mandatory by a stale hand. */
|
|
30
|
+
@schema
|
|
31
|
+
type configField = {name: string, required: bool}
|
|
32
|
+
|
|
33
|
+
@schema
|
|
34
|
+
type t = {
|
|
35
|
+
/** The package name — this trait's identity everywhere else too. */
|
|
36
|
+
trait: string,
|
|
37
|
+
version: string,
|
|
38
|
+
description: string,
|
|
39
|
+
license: string,
|
|
40
|
+
/** Platform capabilities a host of this trait must have provisioned, as
|
|
41
|
+
`CapabilityNeed.toString` spells them. **Empty is a statement**, not a
|
|
42
|
+
silence: the attachments trait needs an object *store*, which is declared
|
|
43
|
+
by the field carrying it rather than as a capability, so its empty list is
|
|
44
|
+
the true answer. */
|
|
45
|
+
capabilities: array<string>,
|
|
46
|
+
/** What a graft must be told, from the emitter's own sury-validated config.
|
|
47
|
+
Sorted by name so the file does not churn on a field reordering. */
|
|
48
|
+
config: array<configField>,
|
|
49
|
+
/** Whether the trait ships an emitter at all. A trait whose graft is all
|
|
50
|
+
patches has nothing to write, and a consumer should know that before
|
|
51
|
+
reaching for `graft-trait` and being told. */
|
|
52
|
+
scaffolded: bool,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Deterministic rendering: 2-space indent, trailing newline. */
|
|
56
|
+
let render = (manifest: t): string =>
|
|
57
|
+
JSON.stringify(manifest->Util_Sury.toJson(schema), ~space=2) ++ "\n"
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
The emitter's config surface, read off its schema.
|
|
61
|
+
|
|
62
|
+
Introspecting the schema rather than being handed a list is the whole point: a
|
|
63
|
+
field added to the config appears here without anyone remembering to say so, and
|
|
64
|
+
one renamed cannot linger under its old name.
|
|
65
|
+
*/
|
|
66
|
+
let configFieldsOf = (schema: S.t<unknown>): array<configField> =>
|
|
67
|
+
switch schema {
|
|
68
|
+
| Object({properties}) =>
|
|
69
|
+
properties
|
|
70
|
+
->Dict.toArray
|
|
71
|
+
->Array.map(((name, field)) => {
|
|
72
|
+
name,
|
|
73
|
+
// sury models an optional field as a union with `undefined`, which is how
|
|
74
|
+
// `?` reaches this side. Anything else is required.
|
|
75
|
+
required: switch field {
|
|
76
|
+
| AnyOf({anyOf}) =>
|
|
77
|
+
!(
|
|
78
|
+
anyOf->Array.some(m =>
|
|
79
|
+
switch m {
|
|
80
|
+
| Undefined(_) => true
|
|
81
|
+
| _ => false
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
| _ => true
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
->Array.toSorted((a, b) => String.compare(a.name, b.name))
|
|
89
|
+
| _ => []
|
|
90
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
5
|
+
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
6
|
+
|
|
7
|
+
let configFieldSchema = Sury.$schema(s => ({
|
|
8
|
+
name: s.m(Sury.string),
|
|
9
|
+
required: s.m(Sury.bool)
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
let schema = Sury.$schema(s => ({
|
|
13
|
+
trait: s.m(Sury.string),
|
|
14
|
+
version: s.m(Sury.string),
|
|
15
|
+
description: s.m(Sury.string),
|
|
16
|
+
license: s.m(Sury.string),
|
|
17
|
+
capabilities: s.m(Sury.array(Sury.string)),
|
|
18
|
+
config: s.m(Sury.array(configFieldSchema)),
|
|
19
|
+
scaffolded: s.m(Sury.bool)
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
function render(manifest) {
|
|
23
|
+
return JSON.stringify(Util_Sury$Reventless.toJson(manifest, schema), undefined, 2) + "\n";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function configFieldsOf(schema) {
|
|
27
|
+
if (schema.type === "object") {
|
|
28
|
+
return Object.entries(schema.properties).map(param => {
|
|
29
|
+
let field = param[1];
|
|
30
|
+
let tmp;
|
|
31
|
+
tmp = field.type === "anyOf" ? !field.anyOf.some(m => m.type === "undefined") : true;
|
|
32
|
+
return {
|
|
33
|
+
name: param[0],
|
|
34
|
+
required: tmp
|
|
35
|
+
};
|
|
36
|
+
}).toSorted((a, b) => Primitive_string.compare(a.name, b.name));
|
|
37
|
+
} else {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
configFieldSchema,
|
|
44
|
+
schema,
|
|
45
|
+
render,
|
|
46
|
+
configFieldsOf,
|
|
47
|
+
}
|
|
48
|
+
/* configFieldSchema Not a pure module */
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Turn a test report into a trait conformance certificate.
|
|
2
|
+
//
|
|
3
|
+
// Usage: certify-trait <trait-package> --host <componentName> --report <jest.json> --out <file>
|
|
4
|
+
//
|
|
5
|
+
// The trait's suite has run inside a consumer's build since the emitter shipped.
|
|
6
|
+
// What was missing is a result something downstream can read: it was console
|
|
7
|
+
// output, so a listing could carry a claim nobody could check.
|
|
8
|
+
//
|
|
9
|
+
// **The framework does not own the runner.** A consumer runs their suite their
|
|
10
|
+
// own way and hands the report here — this reads it, selects the assertions
|
|
11
|
+
// belonging to the trait's suite, and writes `Reventless.TraitCertificate`. The
|
|
12
|
+
// decision of what "verified" means lives in that module, once, so a registry and
|
|
13
|
+
// a build gate cannot disagree about it.
|
|
14
|
+
//
|
|
15
|
+
// The trait is resolved **by the name it is given** and its conformance module is
|
|
16
|
+
// dynamically imported, exactly as `graft-trait` imports the scaffold — so
|
|
17
|
+
// `reventless-spec` still depends on no trait.
|
|
18
|
+
|
|
19
|
+
@val external dynImport: string => promise<'a> = "import"
|
|
20
|
+
|
|
21
|
+
// ── The dynamic-import boundary ──────────────────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// A trait's conformance module exports its suite title as a function of the host
|
|
24
|
+
// name. Read rather than reconstructed: the suite registers that exact string,
|
|
25
|
+
// and a CLI that re-derived it from prose would be guessing at the trait's own
|
|
26
|
+
// wording and would break the day it was reworded.
|
|
27
|
+
type conformanceExports = {suiteName: string => string}
|
|
28
|
+
|
|
29
|
+
let fail = (message: string) => {
|
|
30
|
+
Console.error("certify-trait: " ++ message)
|
|
31
|
+
NodeProcess.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let usage = `Usage: certify-trait <trait-package> --host <componentName> --report <jest.json> --out <file>
|
|
35
|
+
|
|
36
|
+
<trait-package> the installed trait, e.g. @reventlessdev/trait-attachments
|
|
37
|
+
--host the bound component's name, as its Spec declares it
|
|
38
|
+
--report a Jest JSON report (jest --json --outputFile=…)
|
|
39
|
+
--out where to write the certificate
|
|
40
|
+
|
|
41
|
+
Exits non-zero if the suite is absent from the report, or if it did not pass —
|
|
42
|
+
an absent suite is the failure worth catching, since a build that never ran the
|
|
43
|
+
conformance suite is indistinguishable from one that ran it green.`
|
|
44
|
+
|
|
45
|
+
// ── The report, at the boundary ──────────────────────────────────────────────
|
|
46
|
+
//
|
|
47
|
+
// Only the three fields this needs, decoded leniently: a report carries a great
|
|
48
|
+
// deal more, and a strict shape here would break on a runner version that added
|
|
49
|
+
// a field.
|
|
50
|
+
@schema
|
|
51
|
+
type reportAssertion = {
|
|
52
|
+
ancestorTitles: array<string>,
|
|
53
|
+
title: string,
|
|
54
|
+
status: string,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@schema
|
|
58
|
+
type reportSuite = {assertionResults: array<reportAssertion>}
|
|
59
|
+
|
|
60
|
+
@schema
|
|
61
|
+
type report = {testResults: array<reportSuite>}
|
|
62
|
+
|
|
63
|
+
/** Every assertion registered under this suite title, in report order.
|
|
64
|
+
|
|
65
|
+
Matched on the *full* ancestor title rather than a prefix or a substring: two
|
|
66
|
+
hosts of one trait produce two suites whose titles differ only by the host's
|
|
67
|
+
name, and a prefix match would fold one into the other and certify a host
|
|
68
|
+
against another host's run. */
|
|
69
|
+
let assertionsFor = (report: report, ~suite: string): array<(string, bool)> =>
|
|
70
|
+
report.testResults->Array.flatMap(s =>
|
|
71
|
+
s.assertionResults->Array.filterMap(a =>
|
|
72
|
+
a.ancestorTitles->Array.includes(suite) ? Some((a.title, a.status == "passed")) : None
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
let readJson = (path: string) =>
|
|
77
|
+
switch NodeFs.readFileSync(path)->JSON.parseOrThrow {
|
|
78
|
+
| json => json
|
|
79
|
+
| exception _ =>
|
|
80
|
+
fail(`could not read ${path} as JSON.`)
|
|
81
|
+
JSON.Encode.null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
let main = async () => {
|
|
87
|
+
let argv = NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)
|
|
88
|
+
let flag = key =>
|
|
89
|
+
switch argv->Array.indexOf("--" ++ key) {
|
|
90
|
+
| -1 => None
|
|
91
|
+
| i => argv->Array.get(i + 1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
switch argv->Array.get(0) {
|
|
95
|
+
| None | Some("") | Some("--help") | Some("-h") => {
|
|
96
|
+
Console.log(usage)
|
|
97
|
+
NodeProcess.exit(argv->Array.length == 0 ? 1 : 0)
|
|
98
|
+
}
|
|
99
|
+
| Some(traitPackage) =>
|
|
100
|
+
switch (flag("host"), flag("report"), flag("out")) {
|
|
101
|
+
| (None, _, _) | (_, None, _) | (_, _, None) =>
|
|
102
|
+
fail("--host, --report and --out are all required.\n\n" ++ usage)
|
|
103
|
+
| (Some(host), Some(reportPath), Some(out)) => {
|
|
104
|
+
// `@scope/trait-attachments` → `Attachments_Conformance`, the same
|
|
105
|
+
// derivation `graft-trait` does for `_Scaffold`.
|
|
106
|
+
let conformanceModule =
|
|
107
|
+
traitPackage
|
|
108
|
+
->String.split("/")
|
|
109
|
+
->Array.last
|
|
110
|
+
->Option.getOr("")
|
|
111
|
+
->String.replace("trait-", "")
|
|
112
|
+
->String.split("-")
|
|
113
|
+
->Array.map(part =>
|
|
114
|
+
part->String.charAt(0)->String.toUpperCase ++
|
|
115
|
+
part->String.slice(~start=1, ~end=part->String.length)
|
|
116
|
+
)
|
|
117
|
+
->Array.join("") ++ "_Conformance"
|
|
118
|
+
let specifier = `${traitPackage}/src/${conformanceModule}.res.mjs`
|
|
119
|
+
// Resolved from the **caller's** directory: the trait is a dependency of
|
|
120
|
+
// the plugin being certified and deliberately not one of this package.
|
|
121
|
+
let modulePath = try NodeModule.createRequire(
|
|
122
|
+
NodeProcess.cwd() ++ "/index.js",
|
|
123
|
+
)->NodeModule.requireResolve(specifier) catch {
|
|
124
|
+
| _ => specifier
|
|
125
|
+
}
|
|
126
|
+
let conformance: conformanceExports = try await dynImport(
|
|
127
|
+
NodeUrl.pathToFileURL(modulePath)["href"],
|
|
128
|
+
) catch {
|
|
129
|
+
| _ =>
|
|
130
|
+
fail(
|
|
131
|
+
`${traitPackage} ships no conformance suite (looked for ${specifier}).\n` ++
|
|
132
|
+
` A trait without one cannot be certified — there is nothing to prove.`,
|
|
133
|
+
)
|
|
134
|
+
%raw(`undefined`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let suite = conformance.suiteName(host)
|
|
138
|
+
let parsed = switch readJson(reportPath)->Util_Sury.fromJson(reportSchema) {
|
|
139
|
+
| value => value
|
|
140
|
+
| exception _ =>
|
|
141
|
+
fail(`${reportPath} is not a Jest JSON report (expected \`testResults\`).`)
|
|
142
|
+
%raw(`undefined`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
switch parsed->assertionsFor(~suite) {
|
|
146
|
+
// The failure worth catching. A green build that never ran the suite
|
|
147
|
+
// looks exactly like one that ran it and passed, so silence is refused
|
|
148
|
+
// rather than certified as zero-of-zero.
|
|
149
|
+
| [] =>
|
|
150
|
+
fail(
|
|
151
|
+
`the report contains no suite titled "${suite}".\n` ++
|
|
152
|
+
` Either the conformance binding was never registered, or ${host} is not the ` ++
|
|
153
|
+
`name its Spec declares.`,
|
|
154
|
+
)
|
|
155
|
+
| results => {
|
|
156
|
+
let resolveVersion = specifier =>
|
|
157
|
+
try {
|
|
158
|
+
let entry =
|
|
159
|
+
NodeModule.createRequire(
|
|
160
|
+
NodeProcess.cwd() ++ "/index.js",
|
|
161
|
+
)->NodeModule.requireResolve(specifier)
|
|
162
|
+
PackageVersion.fromModuleUrl(NodeUrl.pathToFileURL(entry)["href"])
|
|
163
|
+
} catch {
|
|
164
|
+
| _ => "0.0.0"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let certificate = TraitCertificate.fromReport(
|
|
168
|
+
~trait=traitPackage,
|
|
169
|
+
~traitVersion=resolveVersion(specifier),
|
|
170
|
+
// The framework the suite ran against, read the same way — a trait
|
|
171
|
+
// is certified against a version, never in the abstract.
|
|
172
|
+
~framework=resolveVersion("@reventlessdev/reventless-spec/package.json"),
|
|
173
|
+
~host,
|
|
174
|
+
~suite,
|
|
175
|
+
~results,
|
|
176
|
+
)
|
|
177
|
+
NodeFs.writeFileSync(out, certificate->TraitCertificate.render)
|
|
178
|
+
Console.log(`certify-trait: ${certificate->TraitCertificate.summarize}`)
|
|
179
|
+
Console.log(`Wrote: ${out}`)
|
|
180
|
+
if !(certificate->TraitCertificate.verified) {
|
|
181
|
+
NodeProcess.exit(1)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main()->Promise.ignore
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Nodefs from "node:fs";
|
|
5
|
+
import * as Nodeurl from "node:url";
|
|
6
|
+
import * as Nodemodule from "node:module";
|
|
7
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
8
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
9
|
+
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
10
|
+
import * as PackageVersion$Reventless from "../PackageVersion.res.mjs";
|
|
11
|
+
import * as TraitCertificate$Reventless from "../components/TraitCertificate.res.mjs";
|
|
12
|
+
|
|
13
|
+
function fail(message) {
|
|
14
|
+
console.error("certify-trait: " + message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let usage = `Usage: certify-trait <trait-package> --host <componentName> --report <jest.json> --out <file>
|
|
19
|
+
|
|
20
|
+
<trait-package> the installed trait, e.g. @reventlessdev/trait-attachments
|
|
21
|
+
--host the bound component's name, as its Spec declares it
|
|
22
|
+
--report a Jest JSON report (jest --json --outputFile=…)
|
|
23
|
+
--out where to write the certificate
|
|
24
|
+
|
|
25
|
+
Exits non-zero if the suite is absent from the report, or if it did not pass —
|
|
26
|
+
an absent suite is the failure worth catching, since a build that never ran the
|
|
27
|
+
conformance suite is indistinguishable from one that ran it green.`;
|
|
28
|
+
|
|
29
|
+
let reportAssertionSchema = Sury.$schema(s => ({
|
|
30
|
+
ancestorTitles: s.m(Sury.array(Sury.string)),
|
|
31
|
+
title: s.m(Sury.string),
|
|
32
|
+
status: s.m(Sury.string)
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
let reportSuiteSchema = Sury.$schema(s => ({
|
|
36
|
+
assertionResults: s.m(Sury.array(reportAssertionSchema))
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
let reportSchema = Sury.$schema(s => ({
|
|
40
|
+
testResults: s.m(Sury.array(reportSuiteSchema))
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
function assertionsFor(report, suite) {
|
|
44
|
+
return report.testResults.flatMap(s => Stdlib_Array.filterMap(s.assertionResults, a => {
|
|
45
|
+
if (a.ancestorTitles.includes(suite)) {
|
|
46
|
+
return [
|
|
47
|
+
a.title,
|
|
48
|
+
a.status === "passed"
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readJson(path) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(Nodefs.readFileSync(path, "utf8"));
|
|
57
|
+
} catch (exn) {
|
|
58
|
+
fail(`could not read ` + path + ` as JSON.`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
let argv = process.argv.slice(2, process.argv.length);
|
|
65
|
+
let flag = key => {
|
|
66
|
+
let i = argv.indexOf("--" + key);
|
|
67
|
+
if (i !== -1) {
|
|
68
|
+
return argv[i + 1 | 0];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
let traitPackage = argv[0];
|
|
72
|
+
if (traitPackage !== undefined) {
|
|
73
|
+
switch (traitPackage) {
|
|
74
|
+
case "" :
|
|
75
|
+
case "--help" :
|
|
76
|
+
case "-h" :
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
let match = flag("host");
|
|
80
|
+
let match$1 = flag("report");
|
|
81
|
+
let match$2 = flag("out");
|
|
82
|
+
if (match === undefined) {
|
|
83
|
+
return fail("--host, --report and --out are all required.\n\n" + usage);
|
|
84
|
+
}
|
|
85
|
+
if (match$1 === undefined) {
|
|
86
|
+
return fail("--host, --report and --out are all required.\n\n" + usage);
|
|
87
|
+
}
|
|
88
|
+
if (match$2 === undefined) {
|
|
89
|
+
return fail("--host, --report and --out are all required.\n\n" + usage);
|
|
90
|
+
}
|
|
91
|
+
let conformanceModule = Stdlib_Option.getOr(Stdlib_Array.last(traitPackage.split("/")), "").replace("trait-", "").split("-").map(part => part.charAt(0).toUpperCase() + part.slice(1, part.length)).join("") + "_Conformance";
|
|
92
|
+
let specifier = traitPackage + `/src/` + conformanceModule + `.res.mjs`;
|
|
93
|
+
let modulePath;
|
|
94
|
+
try {
|
|
95
|
+
modulePath = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
|
|
96
|
+
} catch (exn) {
|
|
97
|
+
modulePath = specifier;
|
|
98
|
+
}
|
|
99
|
+
let conformance;
|
|
100
|
+
try {
|
|
101
|
+
conformance = await import(Nodeurl.pathToFileURL(modulePath).href);
|
|
102
|
+
} catch (exn$1) {
|
|
103
|
+
fail(traitPackage + ` ships no conformance suite (looked for ` + specifier + `).\n A trait without one cannot be certified — there is nothing to prove.`);
|
|
104
|
+
conformance = undefined;
|
|
105
|
+
}
|
|
106
|
+
let suite = conformance.suiteName(match);
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = Util_Sury$Reventless.fromJson(readJson(match$1), reportSchema);
|
|
110
|
+
} catch (exn$2) {
|
|
111
|
+
fail(match$1 + ` is not a Jest JSON report (expected \`testResults\`).`);
|
|
112
|
+
parsed = undefined;
|
|
113
|
+
}
|
|
114
|
+
let results = assertionsFor(parsed, suite);
|
|
115
|
+
if (results.length === 0) {
|
|
116
|
+
return fail(`the report contains no suite titled "` + suite + `".\n` + (` Either the conformance binding was never registered, or ` + match + ` is not the `) + `name its Spec declares.`);
|
|
117
|
+
}
|
|
118
|
+
let resolveVersion = specifier => {
|
|
119
|
+
try {
|
|
120
|
+
let entry = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
|
|
121
|
+
return PackageVersion$Reventless.fromModuleUrl(Nodeurl.pathToFileURL(entry).href);
|
|
122
|
+
} catch (exn) {
|
|
123
|
+
return "0.0.0";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
let certificate = TraitCertificate$Reventless.fromReport(traitPackage, resolveVersion(specifier), resolveVersion("@reventlessdev/reventless-spec/package.json"), match, suite, results);
|
|
127
|
+
Nodefs.writeFileSync(match$2, TraitCertificate$Reventless.render(certificate), "utf8");
|
|
128
|
+
console.log(`certify-trait: ` + TraitCertificate$Reventless.summarize(certificate));
|
|
129
|
+
console.log(`Wrote: ` + match$2);
|
|
130
|
+
if (!TraitCertificate$Reventless.verified(certificate)) {
|
|
131
|
+
process.exit(1);
|
|
132
|
+
return;
|
|
133
|
+
} else {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
console.log(usage);
|
|
139
|
+
process.exit(argv.length === 0 ? 1 : 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main();
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
fail,
|
|
146
|
+
usage,
|
|
147
|
+
reportAssertionSchema,
|
|
148
|
+
reportSuiteSchema,
|
|
149
|
+
reportSchema,
|
|
150
|
+
assertionsFor,
|
|
151
|
+
readJson,
|
|
152
|
+
main,
|
|
153
|
+
}
|
|
154
|
+
/* reportAssertionSchema Not a pure module */
|