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.
@@ -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: Buffer.from(outcome.stderr).toString("utf8"),
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: outcome.stdout,
206
- stderr: outcome.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: Buffer.from(outcome.stderr).toString("utf8"),
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: outcome.stdout,
320
- stderr: outcome.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
+ }