space-data-module-sdk 0.8.14 → 0.8.15

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.
@@ -42,6 +42,8 @@ async function main(argv) {
42
42
  return runParity(rest);
43
43
  case "parity-gate":
44
44
  return runParityGateCommand(rest);
45
+ case "conformance":
46
+ return runConformanceCommand(rest);
45
47
  case "protect":
46
48
  return runProtect(rest);
47
49
  case "sign":
@@ -241,6 +243,9 @@ function printUsage() {
241
243
  space-data-module parity-gate (THE isomorphism acceptance gate: certified artifact set x real lanes)
242
244
  space-data-module parity-gate --artifact rf-fspl=./dist/isomorphic/module.wasm:module --json
243
245
  space-data-module parity-gate --gate-manifest ./parity/negative-control.json --expect-fail (prove the gate can fail)
246
+ space-data-module conformance propagator --artifact ./dist/isomorphic/module.wasm (official-harness conformance: WASM artifacts only)
247
+ space-data-module conformance propagator --artifact ./dist/isomorphic/module.wasm --vectors ./vectors/vectors.json --json
248
+ space-data-module conformance propagator --self-test (must exit 0 BY failing: planted defects all caught)
244
249
  space-data-module flow check ./flows/my.flow.json --deps ./modules-root
245
250
  space-data-module flow compile ./flows/my.flow.json --deps ./modules-root [--out ./flows/my/dist]
246
251
  space-data-module protect --manifest ./manifest.json --wasm ./dist/module.wasm --json
@@ -401,6 +406,92 @@ async function runParity(argv) {
401
406
  return report.ok ? 0 : 2;
402
407
  }
403
408
 
409
+ // space-data-module conformance <family> --artifact ./dist/isomorphic/module.wasm
410
+ // [--vectors ./vectors/vectors.json] [--json]
411
+ // [--leak-warmup N] [--leak-cycles N] [--leak-entities N]
412
+ // space-data-module conformance <family> --self-test (must exit 0 BY failing)
413
+ //
414
+ // The official-harness conformance runner (finding
415
+ // graph/findings/official-harness-shapes.md §5): WASM artifacts ONLY, family-
416
+ // dispatched, shipped with its own negative control. PASS / PASS-WITH-GAPS
417
+ // exit 0 (gaps are named); FAIL exits 1. The self-test runs the suite against
418
+ // mock propagators with planted defects — one per real defect class the
419
+ // finding documented — and exits 0 only when every defect is CAUGHT.
420
+ async function runConformanceCommand(argv) {
421
+ const [family, ...rest] = argv;
422
+ if (!family || family.startsWith("--")) {
423
+ throw new Error(
424
+ "conformance requires a family argument, e.g. `space-data-module conformance propagator ...`",
425
+ );
426
+ }
427
+ const options = { json: false, selfTest: false, leak: {} };
428
+ for (let index = 0; index < rest.length; index += 1) {
429
+ const value = rest[index];
430
+ switch (value) {
431
+ case "--artifact":
432
+ case "--wasm":
433
+ options.artifactPath = path.resolve(requireValue(rest, ++index, value));
434
+ break;
435
+ case "--vectors":
436
+ options.vectorsPath = path.resolve(requireValue(rest, ++index, value));
437
+ break;
438
+ case "--json":
439
+ options.json = true;
440
+ break;
441
+ case "--self-test":
442
+ options.selfTest = true;
443
+ break;
444
+ case "--leak-warmup":
445
+ options.leak.warmupCycles = Number(requireValue(rest, ++index, value));
446
+ break;
447
+ case "--leak-cycles":
448
+ options.leak.measureCycles = Number(requireValue(rest, ++index, value));
449
+ break;
450
+ case "--leak-entities":
451
+ options.leak.entities = Number(requireValue(rest, ++index, value));
452
+ break;
453
+ default:
454
+ throw new Error(`conformance: unknown flag ${value}`);
455
+ }
456
+ }
457
+
458
+ const conformance = await import("../src/conformance/index.js");
459
+
460
+ if (options.selfTest) {
461
+ if (family !== "propagator") {
462
+ throw new conformance.UnknownConformanceFamilyError(family);
463
+ }
464
+ const outcome = await conformance.runPropagatorSelfTest();
465
+ if (options.json) {
466
+ console.log(JSON.stringify(outcome, null, 2));
467
+ } else {
468
+ console.log(conformance.formatSelfTestReport(outcome));
469
+ }
470
+ return outcome.ok ? 0 : 1;
471
+ }
472
+
473
+ if (!options.artifactPath) {
474
+ throw new Error("conformance requires --artifact <module.wasm> (or --self-test).");
475
+ }
476
+ const report = await conformance.runConformance({
477
+ family,
478
+ artifactPath: options.artifactPath,
479
+ vectorsPath: options.vectorsPath,
480
+ leak: options.leak,
481
+ });
482
+ if (options.json) {
483
+ console.log(JSON.stringify(report, null, 2));
484
+ } else {
485
+ const text = conformance.formatConformanceReport(report);
486
+ if (report.verdict === "FAIL") {
487
+ console.error(text);
488
+ } else {
489
+ console.log(text);
490
+ }
491
+ }
492
+ return report.verdict === "FAIL" ? 1 : 0;
493
+ }
494
+
404
495
  // space-data-module parity-gate [--gate-manifest ./parity/gate.json]
405
496
  // [--artifact <id>=<path>[:<surface>]] [--lanes ...] [--json]
406
497
  // [--require-native-wasmedge] [--expect-fail] [--timeout-sec N]
@@ -0,0 +1,92 @@
1
+ # Conformance runner
2
+
3
+ **Status:** v1 — W1.4 of `graph/tasks/official-harness-shapes-program.md`,
4
+ implementing `graph/findings/official-harness-shapes.md` §5. Family kits:
5
+ `propagator` (SHIP 1). Maneuver is Wave 2 (EXPERIMENTAL, fix-then-freeze); OD
6
+ is deferred by ruling. **A family with no kit can never be `CORE`.**
7
+
8
+ WASM artifacts ONLY (owner ruling 2026-08-10: "No JS propagator!!!! WASM
9
+ ONLY"). The runner instantiates a compiled `dist/isomorphic/module.wasm` and
10
+ drives the family's ABI directly; there is no path that certifies a JavaScript
11
+ object, because JS registries are internal engine plumbing, never a public
12
+ contract.
13
+
14
+ ## Commands
15
+
16
+ ```
17
+ space-data-module conformance propagator --artifact ./dist/isomorphic/module.wasm
18
+ space-data-module conformance propagator --artifact ./dist/isomorphic/module.wasm \
19
+ --vectors ./vectors/vectors.json --json
20
+ space-data-module conformance propagator --self-test # must exit 0 BY failing
21
+ ```
22
+
23
+ - `--artifact` — the compiled module. Its sha256 goes in the report; a
24
+ conformance claim binds to CONTENT, not to a name.
25
+ - `--vectors` — the module's corpus (`vectors.json` + `PROVENANCE.md`, the
26
+ format proven by the reference module's `vectors/` suite). Default: the
27
+ runner walks up from the artifact to the package root and takes
28
+ `vectors/vectors.json`. **The corpus is the module's own**: Tier B anchors
29
+ are model-specific, so a two-body corpus is never forced onto an SGP4
30
+ module, and a missing corpus is a NAMED GAP, never a silent pass.
31
+ - `--leak-warmup / --leak-cycles / --leak-entities` — lifecycle-leak window
32
+ overrides (defaults 20 / 200 / 256, the reference module's proven numbers).
33
+ - Exit codes: `PASS` and `PASS-WITH-GAPS` exit 0 (gaps are listed in the
34
+ report); `FAIL` exits 1 with the offending check and case named.
35
+
36
+ Library surface: `space-data-module-sdk/conformance` exports
37
+ `runConformance`, `runPropagatorSuite`, `runPropagatorSelfTest`,
38
+ `computeVerdict`, the ABI driver and the error-code table.
39
+
40
+ ## What is checked
41
+
42
+ | Tier | Check | Source of authority |
43
+ |---|---|---|
44
+ | 0 | real instantiation + required export set | [propagator-abi.md](propagator-abi.md) §The export set |
45
+ | 0 | cross-runtime byte-identity | **GAP here by design** — the parity gate (`space-data-module parity-gate`) is the Tier 0 authority; this runner never re-certifies half of it |
46
+ | B | corpus anchors reproduced within band | the module's `vectors.json` (tolerance policy `abs + rel * |expected|`); NaN is its own failure class |
47
+ | C | vis-viva closure, period closure | corpus-declared invariants (`conformance.mu` from the corpus; ECEF un-rotation when the module declares frame 3) — run only where the corpus declares them applicable to the model |
48
+ | C | determinism as BYTES, surviving destroy/re-ingest | ABI §Parity envelope |
49
+ | C | frame/flags/reserved declared, corpus-consistent | ABI §Frames — a frame declaration that contradicts the module's own corpus is the silently-wrong-numbers defect |
50
+ | C | batch and single agree exactly | ABI §Threading — the batch path is the same physics |
51
+ | C | typed refusals (NOT_INITIALIZED / BAD_ENTITY_INDEX / unphysical ingest) | ABI §Error codes — the degradation ladder needs distinguishable codes |
52
+ | C | create RETURNS its handle | ABI §Identity — "the entity I just created is count−1" is the race the harness exists to kill (finding §4.4) |
53
+ | 4 | lifecycle leak: zero page growth after warm-up | ABI §Lifetime |
54
+ | 4 | destroy idempotent, refuses typed, comes back cleanly | ABI §Lifetime |
55
+ | 4 | leak-metric negative control | a gate never observed to fail is indistinguishable from one that cannot fail |
56
+
57
+ ## The self-test
58
+
59
+ `--self-test` runs the SAME suite against mock propagators, each carrying ONE
60
+ planted defect drawn from a real defect class the finding documented live:
61
+
62
+ | Planted defect | Real-world citation | Must be caught by |
63
+ |---|---|---|
64
+ | `units-km` (1000× error) | the `orbpro_propagator.h` km/meters contradiction (finding §4.1) | `tierB/anchors` |
65
+ | `leaky-destroy` | `destroySource(){}` in both shipped propagators (§4.5) | `tier4/lifecycle-leak` |
66
+ | `confident-nonsense` (accepts e ≥ 1) | the underground phasing orbit (§5) | `tierC/typed-refusals` |
67
+ | `count-fallback` (returns success, not the handle) | three families deriving count−1 (§4.4) | `tierC/create-returns-handle` |
68
+ | `frame-lies` (declares TEME, writes ECEF) | the Δv frame never pinned (§4.3) | `tierC/frame-flags-reserved-declared` |
69
+ | `batch-divergence` | batch path not the same physics | `tierC/batch-single-agreement` |
70
+ | `nondeterministic` | byte-determinism is the parity envelope's floor | `tierC/determinism-byte-identity` |
71
+ | `missing-exports` | destroy was optional once; it is not now | `tier0/instantiation-and-exports` |
72
+
73
+ The self-test exits 0 only when the conformant baseline mock is clean AND
74
+ every planted defect is caught by the check that owns it. It needs no
75
+ toolchain, no artifact and no network — `npm run conformance:self-test`.
76
+
77
+ ## Receipt & trust (Wave 4, not yet wired)
78
+
79
+ The conformance receipt travels as a bundle `ATTESTATION` entry
80
+ (publisher-signed under bundle scope); the graduated listing requirement
81
+ (receipt REQUIRED for `CORE`+`ANONYMOUS`, badge for `RECOMMENDED`) and the
82
+ `SDN-CONFORMANCE-RECEIPT-V1` third-party attestor domain are W4.1/W4.2 of the
83
+ program — see the finding §5 "Receipt & trust".
84
+
85
+ ## Reference implementation
86
+
87
+ `space-data-network-modules propagator/keplerian-reference` is the exemplar
88
+ the kit was generalized from: its `tests/` are the original expression of
89
+ these checks, its `vectors/` suite is the corpus format, and
90
+ `tests/sdk-conformance-runner.test.mjs` proves the runner reaches the same
91
+ verdict on the same artifact — including the corrupted-corpus negative
92
+ control.
@@ -28,6 +28,7 @@ test fixture.
28
28
  - [Lifetime](#lifetime)
29
29
  - [Versioning](#versioning)
30
30
  - [Parity envelope](#parity-envelope)
31
+ - [Conformance](#conformance)
31
32
  - [Consumer seam](#consumer-seam)
32
33
  - [Guest usage](#guest-usage)
33
34
 
@@ -339,15 +340,16 @@ and must leave the module usable — a destroyed module refuses to propagate
339
340
  (`NOT_INITIALIZED`) rather than reading freed state, and comes back cleanly on
340
341
  the next ingest.
341
342
 
342
- > **Known defect, stated so it is not mistaken for the standard.**
343
- > `destroySource()` is literally `{}` in BOTH shipped first-party propagators,
344
- > and both therefore FAIL this leak test today. sgp4's `createSourceFromState`
345
- > additionally re-ingests the whole catalogue and tears down the 120 fps worker
346
- > pool for a single burn, polluting the identity table with a synthetic NORAD.
347
- > Fixing them is **W1.5** in `graph/tasks/official-harness-shapes-program.md`;
348
- > the finding's analysis is §4.5. The reference module passes the leak test
349
- > today, deliberately it sets the bar W1.5 brings the first-party
350
- > propagators up to.
343
+ > **Historical defect, retired 2026-08-12.** `destroySource()` was literally
344
+ > `{}` in BOTH shipped first-party propagators when this ABI was written
345
+ > (finding §4.5), and sgp4's `createSourceFromState` re-ingested the whole
346
+ > catalogue for a single burn. The maneuver program landed the fix on OrbPro
347
+ > `main` (`2db279766c` and `97cc38127e`): both propagators now recycle
348
+ > post-burn source SLOTS the entity stays resident so no index above it
349
+ > moves, destroy marks the slot free, and creating a source re-points the
350
+ > recycled entity in place. The reference module remains the leak-test bar,
351
+ > now enforced mechanically by
352
+ > `space-data-module conformance propagator` (tier4/lifecycle-leak).
351
353
 
352
354
  ## Versioning
353
355
 
@@ -399,6 +401,23 @@ Run it:
399
401
  space-data-module parity-gate --artifact <id>=./dist/isomorphic/module.wasm:module
400
402
  ```
401
403
 
404
+ ## Conformance
405
+
406
+ What "official" buys a third party: one command, family-dispatched, shipped
407
+ with its own negative control ([docs/conformance.md](conformance.md)):
408
+
409
+ ```
410
+ space-data-module conformance propagator --artifact ./dist/isomorphic/module.wasm
411
+ space-data-module conformance propagator --self-test # must exit 0 BY failing
412
+ ```
413
+
414
+ The runner adjudicates everything this document specifies that a single lane
415
+ can observe — the export set, the corpus anchors, the invariants with no
416
+ stored expectation, the error-code table, and the leak test — and reports the
417
+ cross-runtime lane honestly as a gap that only the parity gate above closes.
418
+ `PASS` / `PASS-WITH-GAPS` exit 0; `FAIL` exits 1 with the offending check
419
+ named.
420
+
402
421
  ## Consumer seam
403
422
 
404
423
  Per the pluggable-propagation law (owner, 2026-07-29), **every surface that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.8.14",
3
+ "version": "0.8.15",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
6
6
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "./include/*": "./include/*",
34
34
  "./generated/propagator-abi": "./src/generated/orbpro/propagator-abi.js",
35
+ "./conformance": "./src/conformance/index.js",
35
36
  "./compiler": "./src/compiler/index.js",
36
37
  "./compiler/emception": "./src/compiler/emception.js",
37
38
  "./bundle": "./src/bundle/index.js",
@@ -67,6 +68,7 @@
67
68
  "default": "./src/host/moduleFlatbufferStreamPump.js"
68
69
  },
69
70
  "./testing": "./src/testing/index.js",
71
+ "./testing/isomorphic": "./src/testing/isomorphicHarness.js",
70
72
  "./standards": {
71
73
  "browser": "./src/standards/browser.js",
72
74
  "default": "./src/standards/index.js"
@@ -94,6 +96,7 @@
94
96
  "test:parity-gate-lanes": "SPACE_DATA_MODULE_SDK_ENABLE_PARITY_GATE=1 node --test test/parity-gate-lanes.test.js",
95
97
  "gate:parity": "node ./bin/space-data-module.js parity-gate",
96
98
  "gate:parity-negative-control": "node ./bin/space-data-module.js parity-gate --gate-manifest ./parity/negative-control.json --expect-fail",
99
+ "conformance:self-test": "node ./bin/space-data-module.js conformance propagator --self-test",
97
100
  "test:stream-ingest": "node --test test/runtime-host-stream-ingest.test.js",
98
101
  "test:module-stream": "node --test test/module-flatbuffer-stream-pump.test.js",
99
102
  "benchmark:stream-1gib": "SPACE_DATA_MODULE_SDK_ENABLE_1GB_STREAM_TEST=1 node --test test/runtime-host-stream-ingest.test.js",
@@ -122,4 +125,4 @@
122
125
  "engines": {
123
126
  "node": ">=20.0.0"
124
127
  }
125
- }
128
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The propagator-family conformance driver: load a WASM artifact and drive the
3
+ * official OrbPro propagator ABI (docs/propagator-abi.md) from JavaScript.
4
+ *
5
+ * Note what this file does NOT contain: a single hard-coded byte offset. Every
6
+ * read goes through ORBPRO_STATE_VECTOR / ORBPRO_OMM_RECORD, the bindings
7
+ * GENERATED from the same IDL the C header comes from
8
+ * (schemas/orbpro/Propagator.fbs -> scripts/generate-propagator-abi.mjs).
9
+ * The JS reader and the C writer cannot disagree, because neither of them
10
+ * wrote the layout down.
11
+ *
12
+ * This is the generalization of the reference module's test harness
13
+ * (space-data-network-modules propagator/keplerian-reference/tests/harness.mjs,
14
+ * the W1.2 exemplar) into the SDK, so `space-data-module conformance` can
15
+ * target ANY artifact claiming the propagator family — W1.4 of
16
+ * graph/tasks/official-harness-shapes-program.md.
17
+ */
18
+
19
+ import fs from "node:fs/promises";
20
+
21
+ import {
22
+ ORBPRO_OMM_RECORD,
23
+ ORBPRO_STATE_VECTOR,
24
+ ReferenceFrame,
25
+ StateFlags,
26
+ } from "../generated/orbpro/propagator-abi.js";
27
+
28
+ export { ORBPRO_OMM_RECORD, ORBPRO_STATE_VECTOR, ReferenceFrame, StateFlags };
29
+
30
+ /**
31
+ * Documented error codes — the ABI "Error codes" table in
32
+ * docs/propagator-abi.md. Every failure returns its OWN code; a propagator
33
+ * that answers -1 for everything is unconformable because the host cannot
34
+ * place the failure on the degradation ladder.
35
+ */
36
+ export const ErrorCode = Object.freeze({
37
+ OK: 0,
38
+ NOT_INITIALIZED: -1,
39
+ BAD_ENTITY_INDEX: -2,
40
+ NULL_OUTPUT: -3,
41
+ BAD_INPUT: -4,
42
+ NOT_CONVERGED: -5,
43
+ UNPHYSICAL: -6,
44
+ });
45
+
46
+ /**
47
+ * The export set the propagator harness requires. Structural absence of any
48
+ * of these is a Tier 0 conformance failure with the missing name in the
49
+ * report — never "not found".
50
+ */
51
+ export const REQUIRED_ABI_EXPORTS = Object.freeze([
52
+ "memory",
53
+ "plugin_alloc",
54
+ "plugin_free",
55
+ "plugin_init_omm",
56
+ "plugin_ingest_omm_one",
57
+ "plugin_propagate",
58
+ "plugin_propagate_batch",
59
+ "plugin_entity_count",
60
+ "plugin_destroy",
61
+ ]);
62
+
63
+ /**
64
+ * Instantiate an artifact under WASI preview1 and wrap it in the driver
65
+ * interface the conformance suite consumes.
66
+ *
67
+ * Deliberately NOT calling wasi.start(): SDK-built artifacts declare the
68
+ * `command` invoke surface, so `_start` is the stdin-driven invoke loop and
69
+ * blocks forever when driven from a test. The propagator ABI is a set of
70
+ * directly-callable exports — which is exactly how the engine calls it — so
71
+ * conformance calls them directly. The command surface is exercised by the
72
+ * parity gate, which stays the Tier 0 cross-runtime authority.
73
+ */
74
+ export async function loadPropagatorArtifact(artifactPath) {
75
+ const { WASI } = await import("node:wasi");
76
+ const bytes = await fs.readFile(artifactPath);
77
+ const wasi = new WASI({ version: "preview1", args: ["module"], env: {} });
78
+ const module = await WebAssembly.compile(bytes);
79
+ const instance = await WebAssembly.instantiate(module, wasi.getImportObject());
80
+ return new PropagatorAbiDriver(instance);
81
+ }
82
+
83
+ export class PropagatorAbiDriver {
84
+ constructor(instance) {
85
+ this.instance = instance;
86
+ this.exports = instance.exports;
87
+ }
88
+
89
+ /** Export names, for the Tier 0 structural check. */
90
+ exportNames() {
91
+ return Object.keys(this.exports);
92
+ }
93
+
94
+ get memory() {
95
+ return this.exports.memory;
96
+ }
97
+
98
+ /** Linear-memory size, the leak test's only honest metric. */
99
+ memoryBytes() {
100
+ return this.exports.memory.buffer.byteLength;
101
+ }
102
+
103
+ alloc(byteLength) {
104
+ const pointer = this.exports.plugin_alloc(byteLength);
105
+ if (pointer === 0) throw new Error(`plugin_alloc(${byteLength}) returned 0`);
106
+ return pointer;
107
+ }
108
+
109
+ free(pointer) {
110
+ this.exports.plugin_free(pointer);
111
+ }
112
+
113
+ /** Pack element sets into the ABI's OrbProOMMRecord layout. */
114
+ packOmmRecords(records) {
115
+ const { size, offsets } = ORBPRO_OMM_RECORD;
116
+ const buffer = new ArrayBuffer(size * records.length);
117
+ const view = new DataView(buffer);
118
+ records.forEach((record, index) => {
119
+ const base = index * size;
120
+ view.setFloat64(base + offsets.epoch_jd, record.epochJd, true);
121
+ view.setFloat64(base + offsets.mean_motion, record.meanMotionRevPerDay, true);
122
+ view.setFloat64(base + offsets.eccentricity, record.eccentricity, true);
123
+ view.setFloat64(base + offsets.inclination, record.inclinationDeg, true);
124
+ view.setFloat64(base + offsets.ra_of_asc_node, record.raOfAscNodeDeg, true);
125
+ view.setFloat64(base + offsets.arg_of_pericenter, record.argOfPericenterDeg, true);
126
+ view.setFloat64(base + offsets.mean_anomaly, record.meanAnomalyDeg, true);
127
+ view.setFloat64(base + offsets.bstar, record.bstar ?? 0, true);
128
+ view.setFloat64(base + offsets.mean_motion_dot, record.meanMotionDot ?? 0, true);
129
+ view.setFloat64(base + offsets.mean_motion_ddot, record.meanMotionDdot ?? 0, true);
130
+ view.setUint32(base + offsets.norad_cat_id, record.noradCatId ?? 0, true);
131
+ });
132
+ return new Uint8Array(buffer);
133
+ }
134
+
135
+ initFromOmm(records) {
136
+ const packed = this.packOmmRecords(records);
137
+ const pointer = this.alloc(packed.byteLength);
138
+ new Uint8Array(this.memory.buffer).set(packed, pointer);
139
+ try {
140
+ return this.exports.plugin_init_omm(pointer, records.length);
141
+ } finally {
142
+ this.free(pointer);
143
+ }
144
+ }
145
+
146
+ ingestOne(record) {
147
+ const packed = this.packOmmRecords([record]);
148
+ const pointer = this.alloc(packed.byteLength);
149
+ new Uint8Array(this.memory.buffer).set(packed, pointer);
150
+ try {
151
+ return this.exports.plugin_ingest_omm_one(pointer);
152
+ } finally {
153
+ this.free(pointer);
154
+ }
155
+ }
156
+
157
+ /** Returns {status, state, bytes} — bytes for the byte-identity checks. */
158
+ propagate(julianDate, entityIndex) {
159
+ const pointer = this.alloc(ORBPRO_STATE_VECTOR.size);
160
+ try {
161
+ const status = this.exports.plugin_propagate(julianDate, entityIndex, pointer);
162
+ const bytes = new Uint8Array(
163
+ this.memory.buffer.slice(pointer, pointer + ORBPRO_STATE_VECTOR.size),
164
+ );
165
+ return { status, state: decodeStateVector(bytes), bytes };
166
+ } finally {
167
+ this.free(pointer);
168
+ }
169
+ }
170
+
171
+ propagateBatch(julianDate, count) {
172
+ const pointer = this.alloc(ORBPRO_STATE_VECTOR.size * count);
173
+ try {
174
+ const status = this.exports.plugin_propagate_batch(julianDate, pointer, count);
175
+ const states = [];
176
+ for (let index = 0; index < count; index += 1) {
177
+ const start = pointer + index * ORBPRO_STATE_VECTOR.size;
178
+ states.push(
179
+ decodeStateVector(
180
+ new Uint8Array(this.memory.buffer.slice(start, start + ORBPRO_STATE_VECTOR.size)),
181
+ ),
182
+ );
183
+ }
184
+ return { status, states };
185
+ } finally {
186
+ this.free(pointer);
187
+ }
188
+ }
189
+
190
+ entityCount() {
191
+ return this.exports.plugin_entity_count();
192
+ }
193
+
194
+ destroy() {
195
+ this.exports.plugin_destroy();
196
+ }
197
+ }
198
+
199
+ /** Decode a state vector using the GENERATED offsets. */
200
+ export function decodeStateVector(bytes) {
201
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
202
+ const { offsets } = ORBPRO_STATE_VECTOR;
203
+ return {
204
+ epoch: view.getFloat64(offsets.epoch, true),
205
+ position: [
206
+ view.getFloat64(offsets.position + 0, true),
207
+ view.getFloat64(offsets.position + 8, true),
208
+ view.getFloat64(offsets.position + 16, true),
209
+ ],
210
+ velocity: [
211
+ view.getFloat64(offsets.velocity + 0, true),
212
+ view.getFloat64(offsets.velocity + 8, true),
213
+ view.getFloat64(offsets.velocity + 16, true),
214
+ ],
215
+ referenceFrame: view.getUint8(offsets.reference_frame),
216
+ // The three IDL-reserved bytes. Read them explicitly: they are part of the
217
+ // contract, and a writer that skips them leaves the previous call behind.
218
+ reserved: [
219
+ view.getUint8(offsets.reference_frame + 1),
220
+ view.getUint8(offsets.reference_frame + 2),
221
+ view.getUint8(offsets.reference_frame + 3),
222
+ ],
223
+ flags: view.getUint32(offsets.flags, true),
224
+ };
225
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `space-data-module conformance <family>` — the official-harness conformance
3
+ * runner (finding graph/findings/official-harness-shapes.md §5; W1.4 of the
4
+ * harness program).
5
+ *
6
+ * WASM artifacts ONLY (owner ruling 2026-08-10: "No JS propagator!!!! WASM
7
+ * ONLY") — the runner instantiates a compiled module and drives the family's
8
+ * ABI; there is no path that certifies a JS object, because JS registries are
9
+ * internal engine plumbing, never a public contract.
10
+ *
11
+ * Verdict vocabulary matches the gauntlet's: PASS / PASS-WITH-GAPS / FAIL.
12
+ * A gap is a check that could not be adjudicated HERE (no corpus, or a lane
13
+ * another command owns) — named, never silent.
14
+ */
15
+
16
+ import crypto from "node:crypto";
17
+ import fs from "node:fs/promises";
18
+ import path from "node:path";
19
+
20
+ import { loadPropagatorArtifact } from "./abiDriver.js";
21
+ import { computeVerdict, runPropagatorSuite } from "./propagatorSuite.js";
22
+
23
+ export { ErrorCode, REQUIRED_ABI_EXPORTS } from "./abiDriver.js";
24
+ export {
25
+ computeVerdict,
26
+ runPropagatorSuite,
27
+ DEFAULT_LEAK_OPTIONS,
28
+ } from "./propagatorSuite.js";
29
+ export { runPropagatorSelfTest, formatSelfTestReport } from "./selfTest.js";
30
+
31
+ /**
32
+ * Families with a conformance kit. The vocabulary is the SDS pluginCategory
33
+ * projection (W0.3): unknown families are refused BY NAME with the known set,
34
+ * never coerced — the ANALYSIS fallback was the namespace corruption the
35
+ * finding killed.
36
+ */
37
+ export const CONFORMANCE_FAMILIES = Object.freeze(["propagator"]);
38
+
39
+ export class UnknownConformanceFamilyError extends Error {
40
+ constructor(family) {
41
+ super(
42
+ `no conformance kit for family "${family}" — kits exist for: ` +
43
+ `${CONFORMANCE_FAMILIES.join(", ")}. A family with no kit can never be CORE ` +
44
+ "(finding §5); maneuver is Wave 2 (EXPERIMENTAL), OD is deferred by ruling.",
45
+ );
46
+ this.name = "UnknownConformanceFamilyError";
47
+ this.family = family;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Locate a module's own corpus: <package-root>/vectors/vectors.json, walking
53
+ * up from the artifact (dist/isomorphic/module.wasm -> package root).
54
+ */
55
+ export async function resolveCorpusPath(artifactPath) {
56
+ let dir = path.dirname(path.resolve(artifactPath));
57
+ for (let depth = 0; depth < 5; depth += 1) {
58
+ const candidate = path.join(dir, "vectors", "vectors.json");
59
+ try {
60
+ await fs.access(candidate);
61
+ return candidate;
62
+ } catch {
63
+ // keep walking
64
+ }
65
+ const parent = path.dirname(dir);
66
+ if (parent === dir) break;
67
+ dir = parent;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ export async function loadCorpus(vectorsPath) {
73
+ const raw = await fs.readFile(vectorsPath, "utf8");
74
+ const corpus = JSON.parse(raw);
75
+ if (!Array.isArray(corpus.cases)) {
76
+ throw new Error(`${vectorsPath} has no cases[] — not a conformance corpus`);
77
+ }
78
+ return corpus;
79
+ }
80
+
81
+ /**
82
+ * Run conformance for one family against one WASM artifact.
83
+ *
84
+ * @param {object} options
85
+ * @param {string} options.family e.g. "propagator"
86
+ * @param {string} options.artifactPath dist/isomorphic/module.wasm
87
+ * @param {string} [options.vectorsPath] corpus override; default = the
88
+ * module's own vectors/vectors.json, found by walking up from the artifact
89
+ * @param {object} [options.leak] {warmupCycles, measureCycles, entities}
90
+ */
91
+ export async function runConformance(options) {
92
+ const family = String(options.family ?? "").trim().toLowerCase();
93
+ if (!CONFORMANCE_FAMILIES.includes(family)) {
94
+ throw new UnknownConformanceFamilyError(options.family);
95
+ }
96
+ const artifactPath = path.resolve(options.artifactPath);
97
+ const artifactBytes = await fs.readFile(artifactPath);
98
+ const artifactSha256 = crypto
99
+ .createHash("sha256")
100
+ .update(artifactBytes)
101
+ .digest("hex");
102
+
103
+ let vectorsPath = options.vectorsPath
104
+ ? path.resolve(options.vectorsPath)
105
+ : await resolveCorpusPath(artifactPath);
106
+ let corpus = null;
107
+ if (vectorsPath) {
108
+ corpus = await loadCorpus(vectorsPath);
109
+ }
110
+
111
+ const checks = await runPropagatorSuite(
112
+ () => loadPropagatorArtifact(artifactPath),
113
+ { corpus, leak: options.leak },
114
+ );
115
+
116
+ return {
117
+ family,
118
+ artifact: { path: artifactPath, sha256: artifactSha256 },
119
+ corpus: vectorsPath
120
+ ? {
121
+ path: vectorsPath,
122
+ schemaVersion: corpus.schemaVersion ?? null,
123
+ cases: corpus.cases.length,
124
+ model: corpus.conformance?.model ?? null,
125
+ }
126
+ : null,
127
+ checks,
128
+ verdict: computeVerdict(checks),
129
+ };
130
+ }
131
+
132
+ export function formatConformanceReport(report) {
133
+ const lines = [];
134
+ lines.push(`conformance ${report.family} — ${report.verdict}`);
135
+ lines.push(` artifact ${report.artifact.path}`);
136
+ lines.push(` sha256 ${report.artifact.sha256}`);
137
+ if (report.corpus) {
138
+ lines.push(
139
+ ` corpus ${report.corpus.path} (${report.corpus.cases} cases` +
140
+ `${report.corpus.model ? `, model: ${report.corpus.model}` : ""})`,
141
+ );
142
+ } else {
143
+ lines.push(" corpus none supplied");
144
+ }
145
+ for (const check of report.checks) {
146
+ const marker =
147
+ check.status === "pass" ? "PASS" : check.status === "gap" ? "GAP " : "FAIL";
148
+ lines.push(` [${marker}] ${check.id} — ${check.detail}`);
149
+ }
150
+ return lines.join("\n");
151
+ }