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 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
- [MIT](./LICENSE)
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.
@@ -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.
@@ -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,7 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.8.13",
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": "^1.4.4",
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
+ }