space-data-module-sdk 0.8.11 → 0.8.13
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 +85 -1
- package/docs/module-publication-standard.md +7 -3
- package/docs/propagator-abi.md +477 -0
- package/include/orbpro/orbpro_propagator_abi.h +312 -0
- package/package.json +7 -1
- package/schemas/PluginManifest.fbs +46 -1
- package/schemas/orbpro/Propagator.fbs +161 -3
- package/src/browser.js +11 -0
- package/src/bundle/index.js +1 -0
- package/src/bundle/sigdomain.js +22 -0
- package/src/capabilities.js +91 -0
- package/src/compliance/index.js +8 -0
- package/src/compliance/pluginCompliance.js +76 -32
- package/src/flow/flowCompiler.js +231 -3
- package/src/flow/flowRuntimeHost.js +26 -0
- package/src/flow/isomorphicFlowHost.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
- package/src/generated/orbpro/manifest/plugin-family.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
- package/src/generated/orbpro/propagator-abi.js +118 -0
- package/src/generated/orbpro/propagator-abi.ts +199 -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/manifest/normalize.js +113 -4
- package/src/scaffold/copyTemplate.js +71 -0
- package/src/scaffold/index.js +150 -0
- package/src/scaffold/tokens.js +90 -0
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +287 -27
- package/templates/propagator-module/README.md +99 -0
- package/templates/propagator-module/build.js +103 -0
- package/templates/propagator-module/package.json +19 -0
- package/templates/propagator-module/plugin-manifest.json +66 -0
- package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
- package/templates/propagator-module/tests/module.build.test.mjs +103 -0
|
@@ -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({
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# __MODULE_NAME__
|
|
2
|
+
|
|
3
|
+
Scaffolded by `space-data-module init --family propagator --name __MODULE_NAME__`
|
|
4
|
+
from space-data-module-sdk's `templates/propagator-module/` template.
|
|
5
|
+
|
|
6
|
+
This is a **minimal but building** skeleton of an SDN propagator module: every
|
|
7
|
+
ABI obligation is already implemented — exports, wire layout, units, frames,
|
|
8
|
+
identity, threading discipline, error codes, lifetime — and the orbital
|
|
9
|
+
mechanics are a placeholder. It compiles and passes the SDK's own compliance
|
|
10
|
+
checks as-is; it just doesn't propagate anything real yet.
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
- `plugin-manifest.json` — the module manifest. `pluginId` defaults to
|
|
15
|
+
`com.orbpro.<name-with-dots>`; `pluginFamily` is `propagator`; declares one
|
|
16
|
+
invoke method (`ingest_omm`) plus the propagator ABI exports below.
|
|
17
|
+
- `src/__MODULE_NAME_SNAKE__.cpp` — the module source. Search for
|
|
18
|
+
`TODO: your propagation goes here` — there are two spots (element adoption
|
|
19
|
+
in `adopt_omm()`, and the actual propagation in `propagate_entity()`).
|
|
20
|
+
Everything else in the file is ABI plumbing; you should not need to touch
|
|
21
|
+
export names, signatures, error codes, or the state-vector write pattern.
|
|
22
|
+
- `build.js` — compiles through `compileModuleFromSource` (the SDK compiler
|
|
23
|
+
lane). Inlines the generated `orbpro_propagator_abi.h` from your pinned
|
|
24
|
+
`space-data-module-sdk` dependency — never hand-copy that header.
|
|
25
|
+
- `tests/module.build.test.mjs` — manifest shape check (always runs) plus
|
|
26
|
+
compliance/export checks that skip until you've built the module.
|
|
27
|
+
- `package.json` — `"sdn-module"` points at the canonical isomorphic
|
|
28
|
+
artifact; `space-data-module-sdk` is a normal npm dependency.
|
|
29
|
+
|
|
30
|
+
## Naming
|
|
31
|
+
|
|
32
|
+
This module was scaffolded with `--name __MODULE_NAME__`. `space-data-module
|
|
33
|
+
init` substituted four spellings of that name into this tree; if you need to
|
|
34
|
+
introduce your own file or identifier later, reuse the same shapes rather
|
|
35
|
+
than inventing a fifth:
|
|
36
|
+
|
|
37
|
+
| Spelling | This module's value | Used for |
|
|
38
|
+
| --------------------- | -------------------- | ------------------------------------------ |
|
|
39
|
+
| kebab-case | `__MODULE_NAME__` | display text, kebab-case filenames |
|
|
40
|
+
| reverse-DNS plugin id | `__PLUGIN_ID__` | `plugin-manifest.json`'s `pluginId` |
|
|
41
|
+
| snake_case | `__MODULE_NAME_SNAKE__` | C/C++ file and symbol names |
|
|
42
|
+
| camelCase | `__MODULE_NAME_CAMEL__` | a JS-safe identifier (e.g. a bindings key) |
|
|
43
|
+
|
|
44
|
+
## Next steps
|
|
45
|
+
|
|
46
|
+
1. `npm install` (pulls `space-data-module-sdk` and its `spacedatastandards.org`
|
|
47
|
+
dependency).
|
|
48
|
+
2. Fill in the physics: replace the two `TODO: your propagation goes here`
|
|
49
|
+
blocks in `src/__MODULE_NAME_SNAKE__.cpp`. Keep every export, error code,
|
|
50
|
+
and the "zero the struct, set frame explicitly, set VALID last" write
|
|
51
|
+
pattern — those are the ABI contract, not style.
|
|
52
|
+
3. `npm run build` — writes `dist/isomorphic/module.wasm` +
|
|
53
|
+
`dist/plugin-manifest.json`. The build fails loudly if the compiled
|
|
54
|
+
artifact does not pass the SDK's own manifest/artifact validation.
|
|
55
|
+
4. `npm test` — the manifest-shape test always runs; the compliance and
|
|
56
|
+
export-surface tests turn on once step 3 has produced a wasm artifact.
|
|
57
|
+
5. Replace the TODO test at the bottom of `tests/module.build.test.mjs` with
|
|
58
|
+
a real assertion once you have physics to check (ingest a known OMM,
|
|
59
|
+
propagate to a known epoch, compare against an independent reference —
|
|
60
|
+
e.g. another propagator or published ephemeris).
|
|
61
|
+
6. Update `description` in `plugin-manifest.json` and `package.json` — both
|
|
62
|
+
still say "TODO" / a generic scaffold description.
|
|
63
|
+
|
|
64
|
+
## The ABI, in one paragraph
|
|
65
|
+
|
|
66
|
+
A propagator module exports `plugin_init`, `plugin_init_omm`,
|
|
67
|
+
`plugin_ingest_omm_one`, `plugin_propagate`, `plugin_propagate_batch`,
|
|
68
|
+
`plugin_entity_count`, and `plugin_destroy` against the generated
|
|
69
|
+
`OrbProStateVector` / `OrbProOMMRecord` / `OrbProOrbitalElements` structs
|
|
70
|
+
(`orbpro/orbpro_propagator_abi.h` in your pinned `space-data-module-sdk`,
|
|
71
|
+
generated from `schemas/orbpro/Propagator.fbs` — never hand-retype these
|
|
72
|
+
structs, that is the exact drift this generated header exists to end).
|
|
73
|
+
Position/velocity output is always METERS / METERS-PER-SECOND with an
|
|
74
|
+
explicit `reference_frame`; identity is carried by `NORAD_CAT_ID`, never
|
|
75
|
+
derived from array position; every failure returns a named negative code;
|
|
76
|
+
`plugin_destroy` must actually free, not no-op.
|
|
77
|
+
|
|
78
|
+
**Threading.** This module declares `threadModel: "wasi-sequential"` — it
|
|
79
|
+
never spawns a thread of its own, which is the *strong default* for a
|
|
80
|
+
propagator: propagation is embarrassingly parallel across entities but
|
|
81
|
+
sequential within one, and the ABI puts the sharding decision on the HOST
|
|
82
|
+
(e.g. a frame-worker pool), not the module. `build.js` passes
|
|
83
|
+
`threadModel: manifest.threadModel` to the compiler EXPLICITLY — do not
|
|
84
|
+
remove that. `resolveThreadModel` reads the compile option, not
|
|
85
|
+
`manifest.threadModel`, and otherwise infers the model from
|
|
86
|
+
`runtimeTargets`, where `"wasmedge"` infers the OTHER model
|
|
87
|
+
(`emscripten-pthreads`, which in this SDK means the clang
|
|
88
|
+
`wasm32-wasip1-threads` / wasi-threads contract — never `emcc -pthread`,
|
|
89
|
+
which cannot thread under WasmEdge at all). Passing `threadModel` in
|
|
90
|
+
`build.js` sidesteps that inference and is what keeps this manifest's
|
|
91
|
+
declared model and the compiled artifact in agreement — `build.js` also
|
|
92
|
+
asserts they agree after compiling. See `docs/propagator-abi.md`
|
|
93
|
+
"Threading" if you ever need the other model.
|
|
94
|
+
|
|
95
|
+
The one SDN invoke method, `ingest_omm`, is separate from the propagator ABI
|
|
96
|
+
exports above: it is how a flow graph feeds this module SDS `$OMM` records
|
|
97
|
+
over the generic invoke surface, while `plugin_propagate` /
|
|
98
|
+
`plugin_propagate_batch` are called directly by a host that has already
|
|
99
|
+
linked this module as a propagator.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build __MODULE_NAME__ through the SDK compiler lane.
|
|
3
|
+
*
|
|
4
|
+
* `compileModuleFromSource` takes ONE translation unit, so the generated
|
|
5
|
+
* OrbPro propagator ABI header is INLINED into the source before compiling.
|
|
6
|
+
* That inlining is mechanical and one-directional: the header is read from
|
|
7
|
+
* the pinned `space-data-module-sdk` package, never copied into this repo.
|
|
8
|
+
* If the SDK's ABI changes, this build picks it up on the next `npm run
|
|
9
|
+
* build` — do not hand-vendor a copy of the header beside this file.
|
|
10
|
+
*
|
|
11
|
+
* Thread model: the manifest declares `threadModel: "wasi-sequential"` (see
|
|
12
|
+
* plugin-manifest.json's `sequentialJustification`) — this module never
|
|
13
|
+
* spawns a thread, which is the strong default for a propagator (sharding a
|
|
14
|
+
* batch belongs to the HOST, not the module; see docs/propagator-abi.md
|
|
15
|
+
* "Threading"). Both `wasi-sequential` and the threaded `emscripten-pthreads`
|
|
16
|
+
* model compile through the SAME clang `wasm32-wasip1-threads` toolchain —
|
|
17
|
+
* never `emcc -pthread`, which is browser-only and cannot thread under
|
|
18
|
+
* WasmEdge — they differ only in which link-time contract the SDK's
|
|
19
|
+
* post-link artifact guard then validates against the emitted wasm.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from "node:fs/promises";
|
|
23
|
+
import { createRequire } from "node:module";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
import { compileModuleFromSource } from "space-data-module-sdk/compiler";
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const packageRoot = fileURLToPath(new URL(".", import.meta.url));
|
|
31
|
+
const manifestPath = path.join(packageRoot, "plugin-manifest.json");
|
|
32
|
+
const sourcePath = path.join(packageRoot, "src", "__MODULE_NAME_SNAKE__.cpp");
|
|
33
|
+
const distRoot = path.join(packageRoot, "dist");
|
|
34
|
+
const outputPath = path.join(distRoot, "isomorphic", "module.wasm");
|
|
35
|
+
|
|
36
|
+
const standardsRoot = path.dirname(
|
|
37
|
+
require.resolve("spacedatastandards.org/package.json"),
|
|
38
|
+
);
|
|
39
|
+
process.env.SPACE_DATA_STANDARDS_ROOT ??= `${standardsRoot}${path.sep}`;
|
|
40
|
+
|
|
41
|
+
/** The ONE source of the ABI, resolved from the pinned SDK package. */
|
|
42
|
+
const abiHeaderPath = require.resolve(
|
|
43
|
+
"space-data-module-sdk/include/orbpro/orbpro_propagator_abi.h",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
47
|
+
const abiHeader = await fs.readFile(abiHeaderPath, "utf8");
|
|
48
|
+
const rawSource = await fs.readFile(sourcePath, "utf8");
|
|
49
|
+
|
|
50
|
+
const INCLUDE_LINE = '#include "orbpro/orbpro_propagator_abi.h"';
|
|
51
|
+
if (!rawSource.includes(INCLUDE_LINE)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${path.relative(packageRoot, sourcePath)} no longer includes the generated ABI header. ` +
|
|
54
|
+
`A propagator module must build against the ONE generated ABI, not a local copy.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sourceCode = rawSource.replace(
|
|
59
|
+
INCLUDE_LINE,
|
|
60
|
+
[
|
|
61
|
+
`// --- BEGIN INLINED ${path.basename(abiHeaderPath)} (from ${manifest.pluginId}'s pinned SDK) ---`,
|
|
62
|
+
abiHeader,
|
|
63
|
+
`// --- END INLINED ${path.basename(abiHeaderPath)} ---`,
|
|
64
|
+
].join("\n"),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await fs.rm(distRoot, { recursive: true, force: true });
|
|
68
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
69
|
+
|
|
70
|
+
const compilation = await compileModuleFromSource({
|
|
71
|
+
manifest,
|
|
72
|
+
sourceCode,
|
|
73
|
+
language: "c++",
|
|
74
|
+
outputPath,
|
|
75
|
+
// PASSED EXPLICITLY ON PURPOSE. `resolveThreadModel` reads the compile
|
|
76
|
+
// OPTION, not `manifest.threadModel`, and otherwise infers the model from
|
|
77
|
+
// `runtimeTargets` — where "wasmedge" infers pthreads. A manifest that
|
|
78
|
+
// declares `wasi-sequential` and does not pass it here would be silently
|
|
79
|
+
// compiled under the OTHER model and then rejected by the post-link
|
|
80
|
+
// artifact guard for not spawning a thread it never claimed to spawn.
|
|
81
|
+
// Filed as `sdk-manifest-threadmodel-silently-ignored`; keep this explicit
|
|
82
|
+
// until that lands.
|
|
83
|
+
threadModel: manifest.threadModel,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (compilation.threadModel !== manifest.threadModel) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`threadModel drift: the manifest declares ${manifest.threadModel} but the ` +
|
|
89
|
+
`compiler resolved ${compilation.threadModel}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await fs.copyFile(manifestPath, path.join(distRoot, "plugin-manifest.json"));
|
|
94
|
+
|
|
95
|
+
if (!compilation.report?.ok) {
|
|
96
|
+
const issues = JSON.stringify(compilation.report?.issues ?? [], null, 2);
|
|
97
|
+
throw new Error(`Compiled __MODULE_NAME__ artifact failed SDK validation:\n${issues}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(
|
|
101
|
+
`Built ${path.relative(packageRoot, outputPath)} ` +
|
|
102
|
+
`(${compilation.compiler}, threadModel=${compilation.threadModel})`,
|
|
103
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "space-data-network-module-propagator-__MODULE_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__MODULE_NAME__ — an SDN propagator module scaffolded from the space-data-module-sdk propagator-module template.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sdn-module": "./dist/isomorphic/module.wasm",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./plugin-manifest.json": "./plugin-manifest.json",
|
|
9
|
+
"./dist/*": "./dist/*"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "node build.js",
|
|
13
|
+
"test": "node --test tests/*.test.mjs"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"space-data-module-sdk": "^0.8.12"
|
|
17
|
+
},
|
|
18
|
+
"private": true
|
|
19
|
+
}
|