space-data-module-sdk 0.8.13 → 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/README.md +18 -1
- package/bin/space-data-module.js +91 -0
- package/docs/conformance.md +92 -0
- package/docs/flatsql-host-contract.md +34 -1
- package/docs/propagator-abi.md +28 -9
- package/package.json +6 -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/templates/propagator-module/package.json +1 -1
package/README.md
CHANGED
|
@@ -779,6 +779,8 @@ inspect the parsed `REC`, `PNM`, and `ENC` records produced by the real
|
|
|
779
779
|
|
|
780
780
|
- [`spacedatastandards.org`](https://spacedatastandards.org)
|
|
781
781
|
- [`hd-wallet-wasm`](https://github.com/nicktj-dev/hd-wallet-wasm)
|
|
782
|
+
- [`flatsql`](https://github.com/DigitalArsenal/flatsql) — the FlatBuffer-native
|
|
783
|
+
query engine behind `createFlatSqlRuntimeStore()`
|
|
782
784
|
|
|
783
785
|
## Development
|
|
784
786
|
|
|
@@ -817,4 +819,19 @@ helpers for serialized command execution and virtual filesystem access.
|
|
|
817
819
|
|
|
818
820
|
## License
|
|
819
821
|
|
|
820
|
-
[
|
|
822
|
+
[Apache-2.0](./LICENSE)
|
|
823
|
+
|
|
824
|
+
### Dependency licenses
|
|
825
|
+
|
|
826
|
+
The SDK's own license (above) does not change, but one runtime dependency is
|
|
827
|
+
no longer permissive: as of `flatsql` **2.0.0** the FlatSQL engine is licensed
|
|
828
|
+
under the [PolyForm Noncommercial License
|
|
829
|
+
1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/) — source-available
|
|
830
|
+
and free for any noncommercial purpose, with commercial use requiring a separate
|
|
831
|
+
license from DigitalArsenal.io, Inc.
|
|
832
|
+
([tj@digitalarsenal.io](mailto:tj@digitalarsenal.io)). Installing this package
|
|
833
|
+
installs `flatsql`, so a commercial deployment of anything built on the SDK's
|
|
834
|
+
FlatSQL-backed runtime-host storage needs that license. Every `flatsql` release
|
|
835
|
+
before 2.0.0 has been withdrawn from npm, so `^2.0.0` is the only resolvable
|
|
836
|
+
floor — see [`docs/flatsql-host-contract.md`](./docs/flatsql-host-contract.md)
|
|
837
|
+
for why the major bump changes no API.
|
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.
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
# The FlatSQL host contract (why the floor moved to ^1.4.4)
|
|
1
|
+
# The FlatSQL host contract (why the floor moved to ^1.4.4, then to ^2.0.0)
|
|
2
|
+
|
|
3
|
+
The current floor is **`^2.0.0`**. The 1.x history below is what made the
|
|
4
|
+
contract, and it still describes the contract 2.0.0 ships — see
|
|
5
|
+
[The 2.0.0 floor is a licence change, not a port](#the-200-floor-is-a-licence-change-not-a-port)
|
|
6
|
+
at the end for why the major bump costs nothing here.
|
|
2
7
|
|
|
3
8
|
## The defect the bump repairs
|
|
4
9
|
|
|
@@ -93,3 +98,31 @@ consumer would actually load. At 1.4.4 it reports `in-surface (WASI + declared
|
|
|
93
98
|
capabilities: 7)`, `satisfied` in the real-browser lane and
|
|
94
99
|
`runner-cannot-supply-declared-capability` under the bare WasmEdge CLI, naming
|
|
95
100
|
`env.flatsql_io_open` as the single blocker.
|
|
101
|
+
|
|
102
|
+
## The 2.0.0 floor is a licence change, not a port
|
|
103
|
+
|
|
104
|
+
`flatsql` 2.0.0 is 1.4.5's code republished under the [PolyForm Noncommercial
|
|
105
|
+
License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/).
|
|
106
|
+
The major is semver signalling the licence, not an API break. Measured on the
|
|
107
|
+
two published tarballs, every file is byte-identical except three:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
LICENSE Apache-2.0 -> PolyForm Noncommercial 1.0.0
|
|
111
|
+
package.json "version" and "license" only
|
|
112
|
+
README.md licence + contact section
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The host contract above therefore carries over unchanged. On
|
|
116
|
+
`wasm/flatsql-wasi.wasm`, 2.0.0 imports the same **13** — the seven
|
|
117
|
+
`flatsql_io_*` on `env` plus six WASI preview1 — and exports the same **102**
|
|
118
|
+
as 1.4.4, so the parity gate classifies the 2.0.0 engine exactly as it
|
|
119
|
+
classified 1.4.4.
|
|
120
|
+
|
|
121
|
+
Two consequences for anyone reading this floor:
|
|
122
|
+
|
|
123
|
+
- **Every pre-2.0.0 `flatsql` is withdrawn from npm.** A pin below `^2.0.0`
|
|
124
|
+
resolves to nothing. There is no supported lower floor.
|
|
125
|
+
- **Commercial use of the engine needs a licence** from DigitalArsenal.io, Inc.
|
|
126
|
+
([tj@digitalarsenal.io](mailto:tj@digitalarsenal.io)). The SDK stays MIT and
|
|
127
|
+
consumes flatsql through its JS API only, but installing the SDK installs the
|
|
128
|
+
engine, so the obligation reaches the SDK's consumers.
|
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,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "space-data-module-sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.15",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"types": "./src/index.d.ts",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"./include/*": "./include/*",
|
|
33
34
|
"./generated/propagator-abi": "./src/generated/orbpro/propagator-abi.js",
|
|
35
|
+
"./conformance": "./src/conformance/index.js",
|
|
34
36
|
"./compiler": "./src/compiler/index.js",
|
|
35
37
|
"./compiler/emception": "./src/compiler/emception.js",
|
|
36
38
|
"./bundle": "./src/bundle/index.js",
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
"default": "./src/host/moduleFlatbufferStreamPump.js"
|
|
67
69
|
},
|
|
68
70
|
"./testing": "./src/testing/index.js",
|
|
71
|
+
"./testing/isomorphic": "./src/testing/isomorphicHarness.js",
|
|
69
72
|
"./standards": {
|
|
70
73
|
"browser": "./src/standards/browser.js",
|
|
71
74
|
"default": "./src/standards/index.js"
|
|
@@ -93,6 +96,7 @@
|
|
|
93
96
|
"test:parity-gate-lanes": "SPACE_DATA_MODULE_SDK_ENABLE_PARITY_GATE=1 node --test test/parity-gate-lanes.test.js",
|
|
94
97
|
"gate:parity": "node ./bin/space-data-module.js parity-gate",
|
|
95
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",
|
|
96
100
|
"test:stream-ingest": "node --test test/runtime-host-stream-ingest.test.js",
|
|
97
101
|
"test:module-stream": "node --test test/module-flatbuffer-stream-pump.test.js",
|
|
98
102
|
"benchmark:stream-1gib": "SPACE_DATA_MODULE_SDK_ENABLE_1GB_STREAM_TEST=1 node --test test/runtime-host-stream-ingest.test.js",
|
|
@@ -109,7 +113,7 @@
|
|
|
109
113
|
"dependencies": {
|
|
110
114
|
"flatbuffers": "^25.9.23",
|
|
111
115
|
"flatc-wasm": "^26.1.32",
|
|
112
|
-
"flatsql": "^
|
|
116
|
+
"flatsql": "^2.0.0",
|
|
113
117
|
"hd-wallet-wasm": "2.0.28",
|
|
114
118
|
"sdn-emception": "1.0.0",
|
|
115
119
|
"spacedatastandards.org": "https://github.com/DigitalArsenal/spacedatastandards.org/archive/06deda5079204a46bd97a7ce6ac2868e991c6b8f.tar.gz"
|
|
@@ -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
|
+
}
|