space-data-module-sdk 0.8.10 → 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/standards/catalogCore.js +28 -1
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +351 -38
|
@@ -58,9 +58,12 @@ 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,
|
|
66
|
+
readWasmExportNames,
|
|
64
67
|
resolveHostSurface,
|
|
65
68
|
} from "./hostContract.js";
|
|
66
69
|
import { normalizeWasmEdgeOutcome } from "./wasmedgeOutput.js";
|
|
@@ -109,8 +112,56 @@ export const ContractVerdict = Object.freeze({
|
|
|
109
112
|
ShimGap: "shim-gap",
|
|
110
113
|
/** The lane could not run at all. */
|
|
111
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",
|
|
125
|
+
});
|
|
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",
|
|
112
132
|
});
|
|
113
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
|
+
|
|
114
165
|
// --- Manifest ------------------------------------------------------------------
|
|
115
166
|
|
|
116
167
|
/**
|
|
@@ -180,6 +231,16 @@ export async function loadGateManifest(manifestPath = DEFAULT_GATE_MANIFEST) {
|
|
|
180
231
|
? path.resolve(path.dirname(resolved), String(spec.fixture))
|
|
181
232
|
: null,
|
|
182
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
|
+
),
|
|
183
244
|
note: spec.note ? String(spec.note) : null,
|
|
184
245
|
negativeControl: spec.negativeControl === true,
|
|
185
246
|
}),
|
|
@@ -204,6 +265,11 @@ export function makeExternalArtifact(spec) {
|
|
|
204
265
|
profile: String(spec.profile ?? "library"),
|
|
205
266
|
fixture: spec.fixture ? path.resolve(String(spec.fixture)) : null,
|
|
206
267
|
required: spec.required !== false,
|
|
268
|
+
expectedLanes: Object.freeze(
|
|
269
|
+
Array.isArray(spec.expectedLanes)
|
|
270
|
+
? spec.expectedLanes.map((lane) => String(lane))
|
|
271
|
+
: [],
|
|
272
|
+
),
|
|
207
273
|
note: spec.note ? String(spec.note) : "injected by caller (external artifact)",
|
|
208
274
|
negativeControl: spec.negativeControl === true,
|
|
209
275
|
});
|
|
@@ -250,6 +316,21 @@ export function classifyWasmEdgeProbe({
|
|
|
250
316
|
if (/(loading failed|validation failed|magic header|malformed|invalid section)/i.test(text)) {
|
|
251
317
|
return { outcome: "compile-error", missingImport: null, detail: head() };
|
|
252
318
|
}
|
|
319
|
+
// The CLI rejected the INVOCATION, before loading anything: reactor mode
|
|
320
|
+
// needs an entry name. This says nothing about the artifact, so it must not
|
|
321
|
+
// be reported against the artifact — stageArtifact() now names `_initialize`
|
|
322
|
+
// for reactor artifacts, and this branch exists so a regression there is
|
|
323
|
+
// legible instead of masquerading as a cross-runtime divergence.
|
|
324
|
+
if (/function name is required when reactor mode is enabled/i.test(text)) {
|
|
325
|
+
return {
|
|
326
|
+
outcome: "probe-failure",
|
|
327
|
+
missingImport: null,
|
|
328
|
+
detail:
|
|
329
|
+
"WasmEdge refused the invocation: reactor mode requires an entry " +
|
|
330
|
+
"function name. This is a PROBE defect, not an artifact defect — the " +
|
|
331
|
+
"lane must pass the artifact's reactor entry (see resolveReactorEntry).",
|
|
332
|
+
};
|
|
333
|
+
}
|
|
253
334
|
// Linked, then the CLI could not find a start entry: a reactor artifact.
|
|
254
335
|
// Only reachable after successful instantiation.
|
|
255
336
|
if (/(wasm function not found|function not found|_start|_initialize)/i.test(text)) {
|
|
@@ -359,15 +440,11 @@ async function probeWithNativeWasmEdge(context, staged) {
|
|
|
359
440
|
context.pin,
|
|
360
441
|
`native binary ${detected.binary}`,
|
|
361
442
|
);
|
|
362
|
-
const outcome = await spawnCapture(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
env: { PATH: process.env.PATH ?? "" },
|
|
368
|
-
timeoutMs: context.timeoutMs,
|
|
369
|
-
},
|
|
370
|
-
);
|
|
443
|
+
const outcome = await spawnCapture(detected.binary, wasmEdgeProbeArgs(staged), {
|
|
444
|
+
cwd: staged.dir,
|
|
445
|
+
env: { PATH: process.env.PATH ?? "" },
|
|
446
|
+
timeoutMs: context.timeoutMs,
|
|
447
|
+
});
|
|
371
448
|
const normalized = normalizeWasmEdgeOutcome(outcome);
|
|
372
449
|
return classifyWasmEdgeProbe({
|
|
373
450
|
code: outcome.code,
|
|
@@ -391,7 +468,7 @@ async function probeWithDockerWasmEdge(context, staged) {
|
|
|
391
468
|
"/parity",
|
|
392
469
|
];
|
|
393
470
|
if (context.dockerPlatform) args.push("--platform", String(context.dockerPlatform));
|
|
394
|
-
args.push(context.pin.dockerImage,
|
|
471
|
+
args.push(context.pin.dockerImage, ...wasmEdgeProbeArgs(staged));
|
|
395
472
|
const outcome = await spawnCapture(context.dockerBinary ?? "docker", args, {
|
|
396
473
|
cwd: staged.dir,
|
|
397
474
|
env: process.env,
|
|
@@ -678,7 +755,49 @@ export function deriveContractVerdict({
|
|
|
678
755
|
* under the bare WasmEdge CLI is NOT a divergence, and the report says so in
|
|
679
756
|
* those words rather than pretending the lanes matched.
|
|
680
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
|
+
|
|
681
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
|
+
}
|
|
682
801
|
const equivalence = (verdict) =>
|
|
683
802
|
verdict === ContractVerdict.RunnerCannotSupplyCapability
|
|
684
803
|
? ContractVerdict.Satisfied
|
|
@@ -688,11 +807,52 @@ export function lanesAgree(a, b) {
|
|
|
688
807
|
|
|
689
808
|
// --- Orchestrator ---------------------------------------------------------------
|
|
690
809
|
|
|
810
|
+
/**
|
|
811
|
+
* A REACTOR artifact has no `_start`; its initialisation entry is `_initialize`
|
|
812
|
+
* (clang `-mexec-model=reactor`). The WasmEdge CLI refuses to run one without
|
|
813
|
+
* being told which function to call — "A function name is required when reactor
|
|
814
|
+
* mode is enabled." on stderr, exit 1, and NO runtime diagnostic — which the
|
|
815
|
+
* probe classifier could only honestly report as `probe-failure`. The effect
|
|
816
|
+
* was that a CORRECTLY built library module (the shape the module contract
|
|
817
|
+
* mandates for the RF family) was reported as a P1 cross-runtime divergence
|
|
818
|
+
* while the browser lane passed: the gate failed the artifact for the gate's
|
|
819
|
+
* own inability to invoke it.
|
|
820
|
+
*
|
|
821
|
+
* Naming the entry is also STRICTLY STRONGER evidence than the old bare
|
|
822
|
+
* invocation: a clean exit 0 means the runtime linked the imports, instantiated
|
|
823
|
+
* the module, and RAN its initialiser — observed, not inferred from an error
|
|
824
|
+
* string.
|
|
825
|
+
*/
|
|
826
|
+
export function resolveReactorEntry(loadableBytes) {
|
|
827
|
+
let exportNames;
|
|
828
|
+
try {
|
|
829
|
+
exportNames = readWasmExportNames(loadableBytes);
|
|
830
|
+
} catch {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
if (exportNames.includes("_start")) return null;
|
|
834
|
+
return exportNames.includes("_initialize") ? "_initialize" : null;
|
|
835
|
+
}
|
|
836
|
+
|
|
691
837
|
async function stageArtifact(artifact) {
|
|
692
838
|
const dir = await mkdtemp(path.join(os.tmpdir(), `sdm-gate-${artifact.id}-`));
|
|
693
839
|
const basename = "artifact.wasm";
|
|
694
840
|
await writeFile(path.join(dir, basename), artifact.loadableBytes);
|
|
695
|
-
return {
|
|
841
|
+
return {
|
|
842
|
+
dir,
|
|
843
|
+
basename,
|
|
844
|
+
reactorEntry: resolveReactorEntry(artifact.loadableBytes),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The invocation tail shared by both WasmEdge lanes, so the native and Docker
|
|
850
|
+
* lanes can never drift into probing the same artifact two different ways.
|
|
851
|
+
*/
|
|
852
|
+
export function wasmEdgeProbeArgs(staged) {
|
|
853
|
+
return staged.reactorEntry
|
|
854
|
+
? ["--enable-threads", "--reactor", staged.basename, staged.reactorEntry]
|
|
855
|
+
: ["--enable-threads", staged.basename];
|
|
696
856
|
}
|
|
697
857
|
|
|
698
858
|
/**
|
|
@@ -733,6 +893,10 @@ export async function runParityGate(options = {}) {
|
|
|
733
893
|
// --- resolve + load artifacts
|
|
734
894
|
const artifacts = [];
|
|
735
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 ?? {};
|
|
736
900
|
for (const spec of declared) {
|
|
737
901
|
try {
|
|
738
902
|
const rawBytes = new Uint8Array(await readFile(spec.artifactPath));
|
|
@@ -744,6 +908,11 @@ export async function runParityGate(options = {}) {
|
|
|
744
908
|
artifactSha256: sha256Hex(rawBytes),
|
|
745
909
|
moduleSha256: sha256Hex(loadableBytes),
|
|
746
910
|
structural: classifyArtifactImports(loadableBytes, spec.surface),
|
|
911
|
+
declaredRuntimeTargets: declaredRuntimeTargets(loadableBytes),
|
|
912
|
+
declaredCapabilities: declaredCapabilities(loadableBytes),
|
|
913
|
+
...(expectedLanesById[spec.id]
|
|
914
|
+
? { expectedLanes: expectedLanesById[spec.id] }
|
|
915
|
+
: {}),
|
|
747
916
|
});
|
|
748
917
|
} catch (error) {
|
|
749
918
|
if (spec.required) {
|
|
@@ -824,10 +993,25 @@ export async function runParityGate(options = {}) {
|
|
|
824
993
|
// --- Tier A: real instantiation probes
|
|
825
994
|
const laneProbes = new Map(); // lane -> Map(artifactId -> probe)
|
|
826
995
|
|
|
827
|
-
|
|
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) {
|
|
828
1012
|
const probes = new Map();
|
|
829
1013
|
try {
|
|
830
|
-
const results = await runBrowserProbeLane(context,
|
|
1014
|
+
const results = await runBrowserProbeLane(context, browserArtifacts);
|
|
831
1015
|
for (const result of results) {
|
|
832
1016
|
probes.set(result.id, {
|
|
833
1017
|
outcome: result.outcome,
|
|
@@ -837,7 +1021,9 @@ export async function runParityGate(options = {}) {
|
|
|
837
1021
|
crossOriginIsolated: result.crossOriginIsolated === true,
|
|
838
1022
|
});
|
|
839
1023
|
}
|
|
840
|
-
const missing =
|
|
1024
|
+
const missing = browserArtifacts.filter(
|
|
1025
|
+
(artifact) => !probes.has(artifact.id),
|
|
1026
|
+
);
|
|
841
1027
|
for (const artifact of missing) {
|
|
842
1028
|
probes.set(artifact.id, {
|
|
843
1029
|
outcome: "lane-unavailable",
|
|
@@ -846,7 +1032,7 @@ export async function runParityGate(options = {}) {
|
|
|
846
1032
|
});
|
|
847
1033
|
}
|
|
848
1034
|
} catch (error) {
|
|
849
|
-
for (const artifact of
|
|
1035
|
+
for (const artifact of browserArtifacts) {
|
|
850
1036
|
probes.set(artifact.id, {
|
|
851
1037
|
outcome: "lane-unavailable",
|
|
852
1038
|
detail: error?.message ?? String(error),
|
|
@@ -864,7 +1050,7 @@ export async function runParityGate(options = {}) {
|
|
|
864
1050
|
|
|
865
1051
|
for (const lane of wasmedgeLanes) {
|
|
866
1052
|
const probes = new Map();
|
|
867
|
-
for (const artifact of
|
|
1053
|
+
for (const artifact of inScopeArtifacts(lane)) {
|
|
868
1054
|
const staged = await stageArtifact(artifact);
|
|
869
1055
|
try {
|
|
870
1056
|
const probe =
|
|
@@ -889,26 +1075,105 @@ export async function runParityGate(options = {}) {
|
|
|
889
1075
|
const artifactReports = [];
|
|
890
1076
|
for (const artifact of artifacts) {
|
|
891
1077
|
const lanes = [];
|
|
892
|
-
|
|
893
|
-
|
|
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) ?? {
|
|
894
1083
|
outcome: "lane-unavailable",
|
|
895
1084
|
detail: "no probe recorded",
|
|
896
1085
|
missingImport: null,
|
|
897
1086
|
};
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
+
};
|
|
903
1106
|
lanes.push({
|
|
904
1107
|
lane,
|
|
905
|
-
evidence:
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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,
|
|
909
1114
|
contractVerdict: derived.verdict,
|
|
910
1115
|
reason: derived.reason,
|
|
911
|
-
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",
|
|
912
1177
|
});
|
|
913
1178
|
}
|
|
914
1179
|
|
|
@@ -937,14 +1202,8 @@ export async function runParityGate(options = {}) {
|
|
|
937
1202
|
}
|
|
938
1203
|
}
|
|
939
1204
|
|
|
940
|
-
for (
|
|
941
|
-
|
|
942
|
-
failures.push({
|
|
943
|
-
artifact: artifact.id,
|
|
944
|
-
kind: "lane-divergence",
|
|
945
|
-
message: `P1 cross-runtime divergence: ${lanes[0].lane}=${lanes[0].contractVerdict} vs ${lanes[index].lane}=${lanes[index].contractVerdict} (${lanes[index].reason ?? "-"})`,
|
|
946
|
-
});
|
|
947
|
-
}
|
|
1205
|
+
for (const message of laneDivergences(comparedLanes)) {
|
|
1206
|
+
failures.push({ artifact: artifact.id, kind: "lane-divergence", message });
|
|
948
1207
|
}
|
|
949
1208
|
|
|
950
1209
|
artifactReports.push({
|
|
@@ -955,6 +1214,7 @@ export async function runParityGate(options = {}) {
|
|
|
955
1214
|
artifactSha256: artifact.artifactSha256,
|
|
956
1215
|
moduleSha256: artifact.moduleSha256,
|
|
957
1216
|
byteLength: artifact.loadableBytes.length,
|
|
1217
|
+
declaredRuntimeTargets: artifact.declaredRuntimeTargets,
|
|
958
1218
|
structural: {
|
|
959
1219
|
verdict: artifact.structural.verdict,
|
|
960
1220
|
summary: describeClassification(artifact.structural),
|
|
@@ -972,7 +1232,50 @@ export async function runParityGate(options = {}) {
|
|
|
972
1232
|
const behavioral = [];
|
|
973
1233
|
for (const artifact of artifacts) {
|
|
974
1234
|
if (!artifact.fixture) continue;
|
|
975
|
-
|
|
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
|
+
}
|
|
976
1279
|
try {
|
|
977
1280
|
const report = await runParityHarness({
|
|
978
1281
|
wasmPath: artifact.artifactPath,
|
|
@@ -984,7 +1287,17 @@ export async function runParityGate(options = {}) {
|
|
|
984
1287
|
timeoutMs: context.timeoutMs,
|
|
985
1288
|
log,
|
|
986
1289
|
});
|
|
987
|
-
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
|
+
});
|
|
988
1301
|
if (!report.ok) {
|
|
989
1302
|
for (const failure of report.failures) {
|
|
990
1303
|
failures.push({
|