space-data-module-sdk 0.8.11 → 0.8.12
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/README.md +92 -0
- package/bin/space-data-module.js +33 -1
- package/package.json +2 -1
- package/src/browser.js +11 -0
- package/src/capabilities.js +39 -0
- package/src/compliance/index.js +2 -0
- package/src/compliance/pluginCompliance.js +2 -18
- package/src/flow/flowCompiler.js +215 -3
- package/src/flow/flowRuntimeHost.js +26 -0
- package/src/flow/isomorphicFlowHost.js +8 -0
- package/src/host/browserModuleHarness.js +26 -0
- package/src/host/isomorphicLoader.js +57 -11
- package/src/host/runtimeTargetGate.js +256 -0
- package/src/host/workerModuleHarness.js +7 -0
- package/src/index.d.ts +47 -0
- package/src/index.js +11 -0
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +287 -27
|
@@ -58,6 +58,8 @@ import { fileURLToPath } from "node:url";
|
|
|
58
58
|
import { promisify } from "node:util";
|
|
59
59
|
|
|
60
60
|
import { toLoadableWasmBytes } from "../bundle/artifactBytes.js";
|
|
61
|
+
import { locateEmbeddedPlgManifest } from "../compliance/index.js";
|
|
62
|
+
import { runtimeTargetSatisfies } from "../host/runtimeTargetGate.js";
|
|
61
63
|
import {
|
|
62
64
|
classifyArtifactImports,
|
|
63
65
|
describeClassification,
|
|
@@ -110,8 +112,56 @@ export const ContractVerdict = Object.freeze({
|
|
|
110
112
|
ShimGap: "shim-gap",
|
|
111
113
|
/** The lane could not run at all. */
|
|
112
114
|
Unavailable: "lane-unavailable",
|
|
115
|
+
/**
|
|
116
|
+
* The artifact DECLARES that it does not run on this lane's runtime target.
|
|
117
|
+
* Not a pass, not a divergence, not a trap: a scope statement the artifact
|
|
118
|
+
* itself makes, recorded as evidence. Composed flows derive their
|
|
119
|
+
* runtimeTargets from their parts, so a WasmEdge-only flow is legitimate —
|
|
120
|
+
* and without this class its (correct, loud) refusal by the browser harness
|
|
121
|
+
* scored as a P1 cross-runtime divergence, which would have made the gate
|
|
122
|
+
* fail exactly the artifacts this SDK just taught the compiler to emit.
|
|
123
|
+
*/
|
|
124
|
+
OutOfDeclaredScope: "out-of-declared-scope",
|
|
113
125
|
});
|
|
114
126
|
|
|
127
|
+
/** Which runtime target each parity lane stands for. */
|
|
128
|
+
export const LANE_RUNTIME_TARGET = Object.freeze({
|
|
129
|
+
browser: "browser",
|
|
130
|
+
"wasmedge-native": "wasmedge",
|
|
131
|
+
"wasmedge-docker": "wasmedge",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The artifact's own declared runtimeTargets, read from its embedded `$PLG`.
|
|
136
|
+
* An artifact that declares none is unconstrained — every lane is in scope,
|
|
137
|
+
* which is the historical behaviour and stays the default.
|
|
138
|
+
*/
|
|
139
|
+
export function declaredRuntimeTargets(loadableBytes) {
|
|
140
|
+
try {
|
|
141
|
+
const located = locateEmbeddedPlgManifest(loadableBytes);
|
|
142
|
+
const declared = located?.decoded?.runtimeTargets;
|
|
143
|
+
return Array.isArray(declared)
|
|
144
|
+
? declared.map((t) => String(t ?? "").trim().toLowerCase()).filter(Boolean)
|
|
145
|
+
: [];
|
|
146
|
+
} catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function declaredCapabilities(loadableBytes) {
|
|
152
|
+
try {
|
|
153
|
+
const located = locateEmbeddedPlgManifest(loadableBytes);
|
|
154
|
+
return located?.decoded?.capabilities ?? [];
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function laneIsInDeclaredScope(laneName, targets, capabilities) {
|
|
161
|
+
const leg = LANE_RUNTIME_TARGET[laneName];
|
|
162
|
+
return leg ? runtimeTargetSatisfies(targets, leg, capabilities) : true;
|
|
163
|
+
}
|
|
164
|
+
|
|
115
165
|
// --- Manifest ------------------------------------------------------------------
|
|
116
166
|
|
|
117
167
|
/**
|
|
@@ -181,6 +231,16 @@ export async function loadGateManifest(manifestPath = DEFAULT_GATE_MANIFEST) {
|
|
|
181
231
|
? path.resolve(path.dirname(resolved), String(spec.fixture))
|
|
182
232
|
: null,
|
|
183
233
|
required: spec.required !== false,
|
|
234
|
+
// The GATE'S claim about which lanes this artifact owes evidence on.
|
|
235
|
+
// Authority for that belongs to the manifest, not to the artifact's
|
|
236
|
+
// own `$PLG`: an artifact must not be able to reduce what it is
|
|
237
|
+
// examined on by editing its own declaration. Omitted means "every
|
|
238
|
+
// active lane its declaration admits".
|
|
239
|
+
expectedLanes: Object.freeze(
|
|
240
|
+
Array.isArray(spec.expectedLanes)
|
|
241
|
+
? spec.expectedLanes.map((lane) => String(lane))
|
|
242
|
+
: [],
|
|
243
|
+
),
|
|
184
244
|
note: spec.note ? String(spec.note) : null,
|
|
185
245
|
negativeControl: spec.negativeControl === true,
|
|
186
246
|
}),
|
|
@@ -205,6 +265,11 @@ export function makeExternalArtifact(spec) {
|
|
|
205
265
|
profile: String(spec.profile ?? "library"),
|
|
206
266
|
fixture: spec.fixture ? path.resolve(String(spec.fixture)) : null,
|
|
207
267
|
required: spec.required !== false,
|
|
268
|
+
expectedLanes: Object.freeze(
|
|
269
|
+
Array.isArray(spec.expectedLanes)
|
|
270
|
+
? spec.expectedLanes.map((lane) => String(lane))
|
|
271
|
+
: [],
|
|
272
|
+
),
|
|
208
273
|
note: spec.note ? String(spec.note) : "injected by caller (external artifact)",
|
|
209
274
|
negativeControl: spec.negativeControl === true,
|
|
210
275
|
});
|
|
@@ -690,7 +755,49 @@ export function deriveContractVerdict({
|
|
|
690
755
|
* under the bare WasmEdge CLI is NOT a divergence, and the report says so in
|
|
691
756
|
* those words rather than pretending the lanes matched.
|
|
692
757
|
*/
|
|
758
|
+
/**
|
|
759
|
+
* The P1 cross-runtime divergences among the lanes that ACTUALLY RAN.
|
|
760
|
+
*
|
|
761
|
+
* Extracted and exported so a test can drive the real comparison instead of
|
|
762
|
+
* re-implementing it: a test that hand-rolls this loop passes whether or not
|
|
763
|
+
* the shipped one is correct, which is precisely how the defect below survived
|
|
764
|
+
* its own regression test.
|
|
765
|
+
*
|
|
766
|
+
* Pass the COMPARED lanes — the ones not `out-of-declared-scope`. This used to
|
|
767
|
+
* pivot on `lanes[0]`, which is the BROWSER lane, and for a WasmEdge-only
|
|
768
|
+
* artifact that entry is `out-of-declared-scope`, which agrees with everything.
|
|
769
|
+
* Pivoting on it silently stopped comparing wasmedge-native against
|
|
770
|
+
* wasmedge-docker: the gate reported such an artifact adequately gated on two
|
|
771
|
+
* lanes while comparing nothing at all, defeating the very rule
|
|
772
|
+
* `certified-on-a-single-lane` states. Scoping a lane out must remove that LANE
|
|
773
|
+
* from the comparison, never the comparison itself.
|
|
774
|
+
*
|
|
775
|
+
* @param {Array<{lane: string, contractVerdict: string, reason?: string|null}>} comparedLanes
|
|
776
|
+
* @returns {string[]} one message per divergence
|
|
777
|
+
*/
|
|
778
|
+
export function laneDivergences(comparedLanes) {
|
|
779
|
+
const messages = [];
|
|
780
|
+
for (let index = 1; index < (comparedLanes?.length ?? 0); index += 1) {
|
|
781
|
+
const pivot = comparedLanes[0];
|
|
782
|
+
const other = comparedLanes[index];
|
|
783
|
+
if (lanesAgree(pivot.contractVerdict, other.contractVerdict)) continue;
|
|
784
|
+
messages.push(
|
|
785
|
+
`P1 cross-runtime divergence: ${pivot.lane}=${pivot.contractVerdict} vs ` +
|
|
786
|
+
`${other.lane}=${other.contractVerdict} (${other.reason ?? "-"})`,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return messages;
|
|
790
|
+
}
|
|
791
|
+
|
|
693
792
|
export function lanesAgree(a, b) {
|
|
793
|
+
// A lane the artifact declared itself out of cannot disagree with anything:
|
|
794
|
+
// there is no behaviour to compare, only a scope statement.
|
|
795
|
+
if (
|
|
796
|
+
a === ContractVerdict.OutOfDeclaredScope ||
|
|
797
|
+
b === ContractVerdict.OutOfDeclaredScope
|
|
798
|
+
) {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
694
801
|
const equivalence = (verdict) =>
|
|
695
802
|
verdict === ContractVerdict.RunnerCannotSupplyCapability
|
|
696
803
|
? ContractVerdict.Satisfied
|
|
@@ -786,6 +893,10 @@ export async function runParityGate(options = {}) {
|
|
|
786
893
|
// --- resolve + load artifacts
|
|
787
894
|
const artifacts = [];
|
|
788
895
|
const failures = [];
|
|
896
|
+
// A caller may state the lanes an artifact owes from OUTSIDE the manifest
|
|
897
|
+
// file (`--artifact-lanes`), which is the only way to bind a cross-repo
|
|
898
|
+
// injected artifact whose declaration this repo does not own.
|
|
899
|
+
const expectedLanesById = options.expectedLanesById ?? {};
|
|
789
900
|
for (const spec of declared) {
|
|
790
901
|
try {
|
|
791
902
|
const rawBytes = new Uint8Array(await readFile(spec.artifactPath));
|
|
@@ -797,6 +908,11 @@ export async function runParityGate(options = {}) {
|
|
|
797
908
|
artifactSha256: sha256Hex(rawBytes),
|
|
798
909
|
moduleSha256: sha256Hex(loadableBytes),
|
|
799
910
|
structural: classifyArtifactImports(loadableBytes, spec.surface),
|
|
911
|
+
declaredRuntimeTargets: declaredRuntimeTargets(loadableBytes),
|
|
912
|
+
declaredCapabilities: declaredCapabilities(loadableBytes),
|
|
913
|
+
...(expectedLanesById[spec.id]
|
|
914
|
+
? { expectedLanes: expectedLanesById[spec.id] }
|
|
915
|
+
: {}),
|
|
800
916
|
});
|
|
801
917
|
} catch (error) {
|
|
802
918
|
if (spec.required) {
|
|
@@ -877,10 +993,25 @@ export async function runParityGate(options = {}) {
|
|
|
877
993
|
// --- Tier A: real instantiation probes
|
|
878
994
|
const laneProbes = new Map(); // lane -> Map(artifactId -> probe)
|
|
879
995
|
|
|
880
|
-
|
|
996
|
+
// SCOPE BEFORE PROBING. An artifact that declares it does not run on a lane
|
|
997
|
+
// has nothing to learn from being launched there; probing anyway costs a
|
|
998
|
+
// Chrome start or a WasmEdge spawn per artifact for evidence the verdict
|
|
999
|
+
// then discards, and leaves the (expected) trap in the log as noise the
|
|
1000
|
+
// report says was out of scope.
|
|
1001
|
+
const inScopeArtifacts = (lane) =>
|
|
1002
|
+
artifacts.filter((artifact) =>
|
|
1003
|
+
laneIsInDeclaredScope(
|
|
1004
|
+
lane,
|
|
1005
|
+
artifact.declaredRuntimeTargets,
|
|
1006
|
+
artifact.declaredCapabilities,
|
|
1007
|
+
),
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
const browserArtifacts = inScopeArtifacts("browser");
|
|
1011
|
+
if (activeLanes.includes("browser") && browserArtifacts.length > 0) {
|
|
881
1012
|
const probes = new Map();
|
|
882
1013
|
try {
|
|
883
|
-
const results = await runBrowserProbeLane(context,
|
|
1014
|
+
const results = await runBrowserProbeLane(context, browserArtifacts);
|
|
884
1015
|
for (const result of results) {
|
|
885
1016
|
probes.set(result.id, {
|
|
886
1017
|
outcome: result.outcome,
|
|
@@ -890,7 +1021,9 @@ export async function runParityGate(options = {}) {
|
|
|
890
1021
|
crossOriginIsolated: result.crossOriginIsolated === true,
|
|
891
1022
|
});
|
|
892
1023
|
}
|
|
893
|
-
const missing =
|
|
1024
|
+
const missing = browserArtifacts.filter(
|
|
1025
|
+
(artifact) => !probes.has(artifact.id),
|
|
1026
|
+
);
|
|
894
1027
|
for (const artifact of missing) {
|
|
895
1028
|
probes.set(artifact.id, {
|
|
896
1029
|
outcome: "lane-unavailable",
|
|
@@ -899,7 +1032,7 @@ export async function runParityGate(options = {}) {
|
|
|
899
1032
|
});
|
|
900
1033
|
}
|
|
901
1034
|
} catch (error) {
|
|
902
|
-
for (const artifact of
|
|
1035
|
+
for (const artifact of browserArtifacts) {
|
|
903
1036
|
probes.set(artifact.id, {
|
|
904
1037
|
outcome: "lane-unavailable",
|
|
905
1038
|
detail: error?.message ?? String(error),
|
|
@@ -917,7 +1050,7 @@ export async function runParityGate(options = {}) {
|
|
|
917
1050
|
|
|
918
1051
|
for (const lane of wasmedgeLanes) {
|
|
919
1052
|
const probes = new Map();
|
|
920
|
-
for (const artifact of
|
|
1053
|
+
for (const artifact of inScopeArtifacts(lane)) {
|
|
921
1054
|
const staged = await stageArtifact(artifact);
|
|
922
1055
|
try {
|
|
923
1056
|
const probe =
|
|
@@ -942,26 +1075,105 @@ export async function runParityGate(options = {}) {
|
|
|
942
1075
|
const artifactReports = [];
|
|
943
1076
|
for (const artifact of artifacts) {
|
|
944
1077
|
const lanes = [];
|
|
945
|
-
|
|
946
|
-
|
|
1078
|
+
// Iterate the ACTIVE lanes, not the probed ones: a lane with no in-scope
|
|
1079
|
+
// artifact is never launched, and its scoping still has to appear in the
|
|
1080
|
+
// report. A skipped lane is evidence; a missing row is silence.
|
|
1081
|
+
for (const lane of activeLanes) {
|
|
1082
|
+
const probe = laneProbes.get(lane)?.get(artifact.id) ?? {
|
|
947
1083
|
outcome: "lane-unavailable",
|
|
948
1084
|
detail: "no probe recorded",
|
|
949
1085
|
missingImport: null,
|
|
950
1086
|
};
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1087
|
+
const inScope = laneIsInDeclaredScope(
|
|
1088
|
+
lane,
|
|
1089
|
+
artifact.declaredRuntimeTargets,
|
|
1090
|
+
artifact.declaredCapabilities,
|
|
1091
|
+
);
|
|
1092
|
+
const derived = inScope
|
|
1093
|
+
? deriveContractVerdict({
|
|
1094
|
+
laneSuppliesCapabilities: lane === "browser",
|
|
1095
|
+
probe,
|
|
1096
|
+
structural: artifact.structural,
|
|
1097
|
+
})
|
|
1098
|
+
: {
|
|
1099
|
+
verdict: ContractVerdict.OutOfDeclaredScope,
|
|
1100
|
+
reason:
|
|
1101
|
+
`the artifact declares runtimeTargets ` +
|
|
1102
|
+
`[${artifact.declaredRuntimeTargets.join(", ")}], which does not include ` +
|
|
1103
|
+
`"${LANE_RUNTIME_TARGET[lane]}" — this lane is out of its declared scope, ` +
|
|
1104
|
+
"so its refusal here is the contract working, not a divergence",
|
|
1105
|
+
};
|
|
956
1106
|
lanes.push({
|
|
957
1107
|
lane,
|
|
958
|
-
evidence:
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1108
|
+
evidence: inScope
|
|
1109
|
+
? (LANE_EVIDENCE[lane] ?? "unlabelled lane")
|
|
1110
|
+
: "artifact's own embedded runtimeTargets declaration",
|
|
1111
|
+
outcome: inScope ? probe.outcome : "out-of-declared-scope",
|
|
1112
|
+
missingImport: inScope ? (probe.missingImport ?? null) : null,
|
|
1113
|
+
detail: inScope ? (probe.detail ?? null) : derived.reason,
|
|
962
1114
|
contractVerdict: derived.verdict,
|
|
963
1115
|
reason: derived.reason,
|
|
964
|
-
crossOriginIsolated: probe.crossOriginIsolated,
|
|
1116
|
+
crossOriginIsolated: inScope ? (probe.crossOriginIsolated ?? null) : null,
|
|
1117
|
+
declaredRuntimeTargets: artifact.declaredRuntimeTargets,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// THE ARTIFACT DOES NOT PICK ITS EXAMINERS.
|
|
1122
|
+
//
|
|
1123
|
+
// Scoping a lane out is a legitimate statement about where an artifact
|
|
1124
|
+
// runs; it is not a way to be certified without being run. An artifact in
|
|
1125
|
+
// the gate's own certified set that scopes ITSELF out of every lane — or
|
|
1126
|
+
// out of all but one, leaving nothing to compare — has not been gated at
|
|
1127
|
+
// all, and `lanesAgree` returning true pairwise must never stand in for
|
|
1128
|
+
// "two lanes were compared". One manifest string would otherwise disarm
|
|
1129
|
+
// the negative control.
|
|
1130
|
+
const comparedLanes = lanes.filter(
|
|
1131
|
+
(entry) => entry.contractVerdict !== ContractVerdict.OutOfDeclaredScope,
|
|
1132
|
+
);
|
|
1133
|
+
const comparedLaneNames = comparedLanes.map((entry) => entry.lane);
|
|
1134
|
+
if (comparedLanes.length === 0) {
|
|
1135
|
+
failures.push({
|
|
1136
|
+
artifact: artifact.id,
|
|
1137
|
+
kind: "artifact-out-of-every-lane",
|
|
1138
|
+
message:
|
|
1139
|
+
`declares runtimeTargets [${artifact.declaredRuntimeTargets.join(", ")}], ` +
|
|
1140
|
+
`which admits none of the active lanes [${activeLanes.join(", ")}] — ` +
|
|
1141
|
+
"it cannot be certified by a gate that never ran it",
|
|
1142
|
+
});
|
|
1143
|
+
} else if (activeLanes.length > 1 && comparedLanes.length < 2) {
|
|
1144
|
+
// NO ARTIFACT IS CERTIFIED ON THE EVIDENCE OF A SINGLE LANE. The rule is
|
|
1145
|
+
// about evidence, not about the artifact's shape: one lane is one
|
|
1146
|
+
// execution, and one execution is not parity. A WasmEdge-only artifact
|
|
1147
|
+
// still clears this — native host vs pinned container at one pin is a
|
|
1148
|
+
// real comparison, and those pins bump together by law. A browser-only
|
|
1149
|
+
// artifact does not, until the second browser lane (sequential vs
|
|
1150
|
+
// worker-pool, owed by browser-worker-topology) exists to compare it to.
|
|
1151
|
+
failures.push({
|
|
1152
|
+
artifact: artifact.id,
|
|
1153
|
+
kind: "certified-on-a-single-lane",
|
|
1154
|
+
message:
|
|
1155
|
+
`only lane "${comparedLanes[0].lane}" of [${activeLanes.join(", ")}] compared it ` +
|
|
1156
|
+
`(it declares runtimeTargets [${artifact.declaredRuntimeTargets.join(", ")}]) — ` +
|
|
1157
|
+
"one execution is not parity, so no artifact is certified on the evidence of a " +
|
|
1158
|
+
"single lane",
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
// The manifest's claim outranks the artifact's declaration: an artifact
|
|
1162
|
+
// may not shrink the set of lanes it is examined on by editing its own
|
|
1163
|
+
// `$PLG`.
|
|
1164
|
+
const owed = (artifact.expectedLanes ?? []).filter((lane) =>
|
|
1165
|
+
activeLanes.includes(lane),
|
|
1166
|
+
);
|
|
1167
|
+
const unmet = owed.filter((lane) => !comparedLaneNames.includes(lane));
|
|
1168
|
+
if (unmet.length > 0) {
|
|
1169
|
+
failures.push({
|
|
1170
|
+
artifact: artifact.id,
|
|
1171
|
+
kind: "expected-lane-not-compared",
|
|
1172
|
+
message:
|
|
1173
|
+
`the gate manifest requires lanes [${owed.join(", ")}] for this artifact, but ` +
|
|
1174
|
+
`[${unmet.join(", ")}] produced no comparison — its own declaration ` +
|
|
1175
|
+
`[${artifact.declaredRuntimeTargets.join(", ")}] cannot narrow what the gate ` +
|
|
1176
|
+
"claims to certify",
|
|
965
1177
|
});
|
|
966
1178
|
}
|
|
967
1179
|
|
|
@@ -990,14 +1202,8 @@ export async function runParityGate(options = {}) {
|
|
|
990
1202
|
}
|
|
991
1203
|
}
|
|
992
1204
|
|
|
993
|
-
for (
|
|
994
|
-
|
|
995
|
-
failures.push({
|
|
996
|
-
artifact: artifact.id,
|
|
997
|
-
kind: "lane-divergence",
|
|
998
|
-
message: `P1 cross-runtime divergence: ${lanes[0].lane}=${lanes[0].contractVerdict} vs ${lanes[index].lane}=${lanes[index].contractVerdict} (${lanes[index].reason ?? "-"})`,
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1205
|
+
for (const message of laneDivergences(comparedLanes)) {
|
|
1206
|
+
failures.push({ artifact: artifact.id, kind: "lane-divergence", message });
|
|
1001
1207
|
}
|
|
1002
1208
|
|
|
1003
1209
|
artifactReports.push({
|
|
@@ -1008,6 +1214,7 @@ export async function runParityGate(options = {}) {
|
|
|
1008
1214
|
artifactSha256: artifact.artifactSha256,
|
|
1009
1215
|
moduleSha256: artifact.moduleSha256,
|
|
1010
1216
|
byteLength: artifact.loadableBytes.length,
|
|
1217
|
+
declaredRuntimeTargets: artifact.declaredRuntimeTargets,
|
|
1011
1218
|
structural: {
|
|
1012
1219
|
verdict: artifact.structural.verdict,
|
|
1013
1220
|
summary: describeClassification(artifact.structural),
|
|
@@ -1025,7 +1232,50 @@ export async function runParityGate(options = {}) {
|
|
|
1025
1232
|
const behavioral = [];
|
|
1026
1233
|
for (const artifact of artifacts) {
|
|
1027
1234
|
if (!artifact.fixture) continue;
|
|
1028
|
-
|
|
1235
|
+
// Behavioral lanes are scoped by the artifact's OWN declaration too: a
|
|
1236
|
+
// WasmEdge-only artifact has no browser behaviour to compare, and running
|
|
1237
|
+
// it there would score the harness's (correct) refusal as a divergence.
|
|
1238
|
+
// The scoping is RECORDED — a skipped lane is evidence, never silence.
|
|
1239
|
+
const harnessLanes = [
|
|
1240
|
+
"browser",
|
|
1241
|
+
...(wasmedgeLanes.includes("wasmedge-native") ? ["wasmedge"] : []),
|
|
1242
|
+
"docker-wasmedge",
|
|
1243
|
+
].filter((laneName) =>
|
|
1244
|
+
laneIsInDeclaredScope(
|
|
1245
|
+
laneName === "wasmedge"
|
|
1246
|
+
? "wasmedge-native"
|
|
1247
|
+
: laneName === "docker-wasmedge"
|
|
1248
|
+
? "wasmedge-docker"
|
|
1249
|
+
: laneName,
|
|
1250
|
+
artifact.declaredRuntimeTargets,
|
|
1251
|
+
artifact.declaredCapabilities,
|
|
1252
|
+
),
|
|
1253
|
+
);
|
|
1254
|
+
const outOfScopeLanes = ["browser", "wasmedge", "docker-wasmedge"].filter(
|
|
1255
|
+
(laneName) => !harnessLanes.includes(laneName),
|
|
1256
|
+
);
|
|
1257
|
+
if (harnessLanes.length < 2) {
|
|
1258
|
+
// An artifact that declared a FIXTURE asked to be compared. If its own
|
|
1259
|
+
// declaration leaves nothing to compare it against, that is a defect in
|
|
1260
|
+
// the certified set, not a free pass.
|
|
1261
|
+
const message =
|
|
1262
|
+
`behavioral parity needs two comparable lanes; this artifact declares ` +
|
|
1263
|
+
`runtimeTargets [${artifact.declaredRuntimeTargets.join(", ")}] and only ` +
|
|
1264
|
+
`${harnessLanes.length} lane(s) remain in scope`;
|
|
1265
|
+
behavioral.push({
|
|
1266
|
+
artifact: artifact.id,
|
|
1267
|
+
ok: false,
|
|
1268
|
+
outOfDeclaredScope: outOfScopeLanes,
|
|
1269
|
+
declaredRuntimeTargets: artifact.declaredRuntimeTargets,
|
|
1270
|
+
note: message,
|
|
1271
|
+
});
|
|
1272
|
+
failures.push({
|
|
1273
|
+
artifact: artifact.id,
|
|
1274
|
+
kind: "behavioral-not-comparable",
|
|
1275
|
+
message,
|
|
1276
|
+
});
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1029
1279
|
try {
|
|
1030
1280
|
const report = await runParityHarness({
|
|
1031
1281
|
wasmPath: artifact.artifactPath,
|
|
@@ -1037,7 +1287,17 @@ export async function runParityGate(options = {}) {
|
|
|
1037
1287
|
timeoutMs: context.timeoutMs,
|
|
1038
1288
|
log,
|
|
1039
1289
|
});
|
|
1040
|
-
behavioral.push({
|
|
1290
|
+
behavioral.push({
|
|
1291
|
+
artifact: artifact.id,
|
|
1292
|
+
ok: report.ok,
|
|
1293
|
+
report,
|
|
1294
|
+
...(outOfScopeLanes.length > 0
|
|
1295
|
+
? {
|
|
1296
|
+
outOfDeclaredScope: outOfScopeLanes,
|
|
1297
|
+
declaredRuntimeTargets: artifact.declaredRuntimeTargets,
|
|
1298
|
+
}
|
|
1299
|
+
: {}),
|
|
1300
|
+
});
|
|
1041
1301
|
if (!report.ok) {
|
|
1042
1302
|
for (const failure of report.failures) {
|
|
1043
1303
|
failures.push({
|