space-data-module-sdk 0.8.8 → 0.8.10
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/bin/space-data-module.js +92 -0
- package/docs/flatsql-host-contract.md +95 -0
- package/docs/tri-runtime-parity-gate.md +146 -0
- package/package.json +8 -2
- package/parity/gate.json +28 -0
- package/parity/negative-control.json +14 -0
- package/parity/sdk-command.json +22 -0
- package/src/testing/hostContract.js +467 -0
- package/src/testing/index.js +32 -0
- package/src/testing/parityGate.js +1094 -0
- package/src/testing/parityGateBrowserProbe.js +171 -0
- package/src/testing/parityLanes.js +11 -6
- package/src/testing/wasmedgeOutput.js +77 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side host-contract instantiation probe.
|
|
3
|
+
*
|
|
4
|
+
* Bundled by parityGate.js and served to a REAL headless Chrome behind
|
|
5
|
+
* COOP/COEP (cross-origin isolated, SAB-capable). Never jsdom — jsdom masks
|
|
6
|
+
* Illegal-invocation and threading realities, and a probe that lies about the
|
|
7
|
+
* browser is worse than no probe.
|
|
8
|
+
*
|
|
9
|
+
* For each artifact the probe synthesizes EXACTLY the declared host surface —
|
|
10
|
+
* WASI preview1 stubs plus the declared capability imports, with the real
|
|
11
|
+
* signatures read from the binary — and instantiates. Anything the artifact
|
|
12
|
+
* demands beyond that surface produces a LinkError naming the offender, which
|
|
13
|
+
* is the answer we want.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
classifyArtifactImports,
|
|
18
|
+
readWasmImportDescriptors,
|
|
19
|
+
resolveHostSurface,
|
|
20
|
+
WASI_PREVIEW1_MODULE,
|
|
21
|
+
} from "./hostContract.js";
|
|
22
|
+
|
|
23
|
+
const ZERO_BY_RESULT = {
|
|
24
|
+
i32: 0,
|
|
25
|
+
i64: 0n,
|
|
26
|
+
f32: 0,
|
|
27
|
+
f64: 0,
|
|
28
|
+
v128: 0,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function stubReturn(results) {
|
|
32
|
+
if (!results || results.length === 0) return undefined;
|
|
33
|
+
return ZERO_BY_RESULT[results[0]] ?? 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build an import object containing ONLY surface-legal entries. Out-of-surface
|
|
38
|
+
* imports are deliberately NOT supplied: their absence is the signal.
|
|
39
|
+
*/
|
|
40
|
+
export function buildSurfaceImports(bytes, surfaceId) {
|
|
41
|
+
const surface = resolveHostSurface(surfaceId);
|
|
42
|
+
const allowedExtra = new Set(surface.extra);
|
|
43
|
+
const descriptors = readWasmImportDescriptors(bytes);
|
|
44
|
+
const imports = Object.create(null);
|
|
45
|
+
const supplied = [];
|
|
46
|
+
|
|
47
|
+
for (const entry of descriptors) {
|
|
48
|
+
const key = `${entry.module}.${entry.name}`;
|
|
49
|
+
const inSurface =
|
|
50
|
+
(surface.wasiAny && entry.module === WASI_PREVIEW1_MODULE) ||
|
|
51
|
+
allowedExtra.has(key);
|
|
52
|
+
if (!inSurface) continue;
|
|
53
|
+
|
|
54
|
+
imports[entry.module] ??= Object.create(null);
|
|
55
|
+
if (entry.kind === "function") {
|
|
56
|
+
const results = entry.results;
|
|
57
|
+
imports[entry.module][entry.name] = () => stubReturn(results);
|
|
58
|
+
} else if (entry.kind === "memory") {
|
|
59
|
+
imports[entry.module][entry.name] = new WebAssembly.Memory({
|
|
60
|
+
initial: entry.initial,
|
|
61
|
+
maximum: entry.maximum ?? entry.initial,
|
|
62
|
+
shared: entry.shared,
|
|
63
|
+
});
|
|
64
|
+
} else if (entry.kind === "table") {
|
|
65
|
+
imports[entry.module][entry.name] = new WebAssembly.Table({
|
|
66
|
+
element: entry.element === "anyfunc" ? "anyfunc" : "externref",
|
|
67
|
+
initial: entry.initial,
|
|
68
|
+
maximum: entry.maximum,
|
|
69
|
+
});
|
|
70
|
+
} else if (entry.kind === "global") {
|
|
71
|
+
imports[entry.module][entry.name] = new WebAssembly.Global(
|
|
72
|
+
{ value: entry.valtype, mutable: entry.mutable },
|
|
73
|
+
entry.valtype === "i64" ? 0n : 0,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
supplied.push(key);
|
|
77
|
+
}
|
|
78
|
+
return { imports, supplied };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function linkErrorTarget(message) {
|
|
82
|
+
// Chrome: 'WebAssembly.instantiate(): Import #3 "env" "flatsql_io_open":
|
|
83
|
+
// function import requires a callable'
|
|
84
|
+
const named = /Import\s+#\d+\s+"([^"]*)"\s+"([^"]*)"/.exec(String(message));
|
|
85
|
+
if (named) return `${named[1]}.${named[2]}`;
|
|
86
|
+
const moduleOnly = /module="([^"]*)"\s*function="([^"]*)"/.exec(String(message));
|
|
87
|
+
if (moduleOnly) return `${moduleOnly[1]}.${moduleOnly[2]}`;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function probeArtifactInBrowser(bytes, surfaceId) {
|
|
92
|
+
const structural = classifyArtifactImports(bytes, surfaceId);
|
|
93
|
+
let compiled;
|
|
94
|
+
try {
|
|
95
|
+
compiled = await WebAssembly.compile(bytes);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
outcome: "compile-error",
|
|
99
|
+
detail: String(error?.message ?? error),
|
|
100
|
+
missingImport: null,
|
|
101
|
+
structural,
|
|
102
|
+
exportCount: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const { imports, supplied } = buildSurfaceImports(bytes, surfaceId);
|
|
106
|
+
try {
|
|
107
|
+
const instance = await WebAssembly.instantiate(compiled, imports);
|
|
108
|
+
return {
|
|
109
|
+
outcome: "instantiated",
|
|
110
|
+
detail: null,
|
|
111
|
+
missingImport: null,
|
|
112
|
+
structural,
|
|
113
|
+
suppliedImports: supplied,
|
|
114
|
+
exportCount: Object.keys(instance.exports ?? {}).length,
|
|
115
|
+
exportNames: Object.keys(instance.exports ?? {}).sort(),
|
|
116
|
+
};
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = String(error?.message ?? error);
|
|
119
|
+
const isLink = error instanceof WebAssembly.LinkError || /Import #/.test(message);
|
|
120
|
+
return {
|
|
121
|
+
outcome: isLink ? "link-error" : "instantiate-error",
|
|
122
|
+
detail: message,
|
|
123
|
+
missingImport: linkErrorTarget(message),
|
|
124
|
+
structural,
|
|
125
|
+
suppliedImports: supplied,
|
|
126
|
+
exportCount: 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Entry point executed inside the page. */
|
|
132
|
+
export async function runBrowserGateProbe() {
|
|
133
|
+
const status = document.getElementById("status");
|
|
134
|
+
const report = (text) => {
|
|
135
|
+
if (status) status.textContent = text;
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
const plan = await (await fetch("/gate-plan")).json();
|
|
139
|
+
report(`probing ${plan.artifacts.length} artifact(s)…`);
|
|
140
|
+
const results = [];
|
|
141
|
+
for (const artifact of plan.artifacts) {
|
|
142
|
+
const bytes = new Uint8Array(
|
|
143
|
+
await (await fetch(artifact.url)).arrayBuffer(),
|
|
144
|
+
);
|
|
145
|
+
const probe = await probeArtifactInBrowser(bytes, artifact.surface);
|
|
146
|
+
results.push({
|
|
147
|
+
id: artifact.id,
|
|
148
|
+
surface: artifact.surface,
|
|
149
|
+
byteLength: bytes.length,
|
|
150
|
+
crossOriginIsolated: Boolean(globalThis.crossOriginIsolated),
|
|
151
|
+
sharedArrayBufferAvailable: typeof SharedArrayBuffer === "function",
|
|
152
|
+
...probe,
|
|
153
|
+
});
|
|
154
|
+
report(`probed ${results.length}/${plan.artifacts.length}`);
|
|
155
|
+
}
|
|
156
|
+
await fetch("/done", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: JSON.stringify({ results }),
|
|
159
|
+
});
|
|
160
|
+
report("done");
|
|
161
|
+
} catch (error) {
|
|
162
|
+
await fetch("/done", {
|
|
163
|
+
method: "POST",
|
|
164
|
+
body: JSON.stringify({ fatal: String(error?.stack ?? error) }),
|
|
165
|
+
}).catch(() => {});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof document !== "undefined") {
|
|
170
|
+
runBrowserGateProbe();
|
|
171
|
+
}
|
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from "node:url";
|
|
|
29
29
|
import { promisify } from "node:util";
|
|
30
30
|
|
|
31
31
|
import { ExitClass, assertWasmEdgeVersionMatchesPin } from "./parityHarness.js";
|
|
32
|
+
import { normalizeWasmEdgeOutcome } from "./wasmedgeOutput.js";
|
|
32
33
|
|
|
33
34
|
const execFile = promisify(execFileCallback);
|
|
34
35
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -192,18 +193,21 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
192
193
|
timeoutMs: context.timeoutMs,
|
|
193
194
|
},
|
|
194
195
|
);
|
|
196
|
+
// WasmEdge logs its own diagnostics to STDOUT; pull them out before
|
|
197
|
+
// anything compares guest bytes or classifies an exit.
|
|
198
|
+
const normalized = normalizeWasmEdgeOutcome(outcome);
|
|
195
199
|
const { exitClass, exitDetail } = classifyProcessOutcome({
|
|
196
200
|
code: outcome.code,
|
|
197
201
|
signal: outcome.signal,
|
|
198
|
-
stderrText:
|
|
202
|
+
stderrText: normalized.diagnosticText,
|
|
199
203
|
});
|
|
200
204
|
runs.push({
|
|
201
205
|
caseId: planCase.id,
|
|
202
206
|
threadCount,
|
|
203
207
|
exitClass,
|
|
204
208
|
exitDetail,
|
|
205
|
-
stdout:
|
|
206
|
-
stderr:
|
|
209
|
+
stdout: normalized.stdout,
|
|
210
|
+
stderr: normalized.stderr,
|
|
207
211
|
stateFiles: null,
|
|
208
212
|
});
|
|
209
213
|
}
|
|
@@ -306,18 +310,19 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
306
310
|
stdinBytes: planCase.stdinBytes,
|
|
307
311
|
timeoutMs: context.timeoutMs,
|
|
308
312
|
});
|
|
313
|
+
const normalized = normalizeWasmEdgeOutcome(outcome);
|
|
309
314
|
const { exitClass, exitDetail } = classifyProcessOutcome({
|
|
310
315
|
code: outcome.code,
|
|
311
316
|
signal: outcome.signal,
|
|
312
|
-
stderrText:
|
|
317
|
+
stderrText: normalized.diagnosticText,
|
|
313
318
|
});
|
|
314
319
|
runs.push({
|
|
315
320
|
caseId: planCase.id,
|
|
316
321
|
threadCount,
|
|
317
322
|
exitClass,
|
|
318
323
|
exitDetail,
|
|
319
|
-
stdout:
|
|
320
|
-
stderr:
|
|
324
|
+
stdout: normalized.stdout,
|
|
325
|
+
stderr: normalized.stderr,
|
|
321
326
|
stateFiles: null,
|
|
322
327
|
});
|
|
323
328
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WasmEdge CLI output normalization — an SDK HOST SHIM, which is the only
|
|
3
|
+
* place a runtime difference is ever allowed to be absorbed.
|
|
4
|
+
*
|
|
5
|
+
* MEASURED (WasmEdge 0.16.4, native and in the pinned container): the CLI
|
|
6
|
+
* writes its own diagnostics — `[2026-08-08 00:00:00.000] [error] …` — to
|
|
7
|
+
* **stdout**, not stderr. Two consequences, both of which produced wrong
|
|
8
|
+
* answers before this shim existed:
|
|
9
|
+
*
|
|
10
|
+
* 1. A classifier that reads only stderr sees NOTHING when instantiation
|
|
11
|
+
* fails, so a module that cannot link reads as a clean run. That is a
|
|
12
|
+
* FALSE PASS in the isomorphism gate — the worst possible defect in an
|
|
13
|
+
* acceptance instrument, and exactly the failure class this stack keeps
|
|
14
|
+
* hitting: a measurement that reads as evidence.
|
|
15
|
+
* 2. Those diagnostic lines land inside the bytes the parity harness
|
|
16
|
+
* byte-compares. A browser lane that reports the same failure through a
|
|
17
|
+
* different channel would then look like an OUTPUT divergence, sending a
|
|
18
|
+
* reader hunting for a nonexistent computational difference.
|
|
19
|
+
*
|
|
20
|
+
* So: strip the runtime's own log lines out of the guest's stdout, hand them
|
|
21
|
+
* back as diagnostics, and compare the guest bytes. The guest's own output is
|
|
22
|
+
* untouched — nothing here inspects or rewrites a single byte the module
|
|
23
|
+
* wrote.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const DIAGNOSTIC_LINE =
|
|
27
|
+
/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\] \[(error|warning|warn|info|debug|critical)\]/;
|
|
28
|
+
|
|
29
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
30
|
+
const encoder = new TextEncoder();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Split WasmEdge's own diagnostic lines out of a captured stdout buffer.
|
|
34
|
+
*
|
|
35
|
+
* @returns {{stdout: Uint8Array, diagnostics: string}}
|
|
36
|
+
*/
|
|
37
|
+
export function splitWasmEdgeDiagnostics(stdoutBytes) {
|
|
38
|
+
const bytes = stdoutBytes ?? new Uint8Array(0);
|
|
39
|
+
const text = decoder.decode(bytes);
|
|
40
|
+
if (!DIAGNOSTIC_LINE.test(text) && !text.includes("] [error]")) {
|
|
41
|
+
return { stdout: bytes, diagnostics: "" };
|
|
42
|
+
}
|
|
43
|
+
const guestLines = [];
|
|
44
|
+
const diagnosticLines = [];
|
|
45
|
+
// Preserve a trailing newline distinction: split on "\n" and rejoin, so a
|
|
46
|
+
// guest payload without a final newline stays without one.
|
|
47
|
+
const lines = text.split("\n");
|
|
48
|
+
const lastIndex = lines.length - 1;
|
|
49
|
+
for (let index = 0; index <= lastIndex; index += 1) {
|
|
50
|
+
const line = lines[index];
|
|
51
|
+
if (DIAGNOSTIC_LINE.test(line)) diagnosticLines.push(line);
|
|
52
|
+
else guestLines.push(line);
|
|
53
|
+
}
|
|
54
|
+
// If every non-diagnostic line is empty the guest produced nothing.
|
|
55
|
+
const rejoined = guestLines.join("\n");
|
|
56
|
+
const guestText = guestLines.every((line) => line.length === 0) ? "" : rejoined;
|
|
57
|
+
return {
|
|
58
|
+
stdout: encoder.encode(guestText),
|
|
59
|
+
diagnostics: diagnosticLines.join("\n"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalize one WasmEdge process outcome: guest stdout with the runtime's
|
|
65
|
+
* diagnostics removed, and a combined diagnostic text (stderr + whatever the
|
|
66
|
+
* runtime logged to stdout) for classification.
|
|
67
|
+
*/
|
|
68
|
+
export function normalizeWasmEdgeOutcome({ stdout, stderr }) {
|
|
69
|
+
const { stdout: guestStdout, diagnostics } = splitWasmEdgeDiagnostics(stdout);
|
|
70
|
+
const stderrText = decoder.decode(stderr ?? new Uint8Array(0));
|
|
71
|
+
const combined = [stderrText, diagnostics].filter(Boolean).join("\n");
|
|
72
|
+
return {
|
|
73
|
+
stdout: guestStdout,
|
|
74
|
+
stderr: encoder.encode(combined),
|
|
75
|
+
diagnosticText: combined,
|
|
76
|
+
};
|
|
77
|
+
}
|