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.
- package/bin/space-data-module.js +91 -0
- package/docs/conformance.md +92 -0
- package/docs/propagator-abi.md +28 -9
- package/package.json +5 -2
- package/src/conformance/abiDriver.js +225 -0
- package/src/conformance/index.js +151 -0
- package/src/conformance/propagatorSuite.js +526 -0
- package/src/conformance/selfTest.js +275 -0
- package/src/conformance/selfTestCorpus.js +68 -0
- package/src/conformance/twoBodyReference.js +132 -0
- package/src/testing/isomorphicHarness.js +233 -0
package/bin/space-data-module.js
CHANGED
|
@@ -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.
|
package/docs/propagator-abi.md
CHANGED
|
@@ -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
|
-
> **
|
|
343
|
-
> `
|
|
344
|
-
>
|
|
345
|
-
>
|
|
346
|
-
>
|
|
347
|
-
>
|
|
348
|
-
>
|
|
349
|
-
>
|
|
350
|
-
>
|
|
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.
|
|
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
|
+
}
|