space-data-module-sdk 0.8.12 → 0.8.13
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 +52 -0
- package/docs/module-publication-standard.md +7 -3
- package/docs/propagator-abi.md +477 -0
- package/include/orbpro/orbpro_propagator_abi.h +312 -0
- package/package.json +6 -1
- package/schemas/PluginManifest.fbs +46 -1
- package/schemas/orbpro/Propagator.fbs +161 -3
- package/src/bundle/index.js +1 -0
- package/src/bundle/sigdomain.js +22 -0
- package/src/capabilities.js +53 -1
- package/src/compliance/index.js +6 -0
- package/src/compliance/pluginCompliance.js +76 -16
- package/src/flow/flowCompiler.js +30 -14
- package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
- package/src/generated/orbpro/manifest/plugin-family.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
- package/src/generated/orbpro/propagator-abi.js +118 -0
- package/src/generated/orbpro/propagator-abi.ts +199 -0
- package/src/manifest/normalize.js +113 -4
- package/src/scaffold/copyTemplate.js +71 -0
- package/src/scaffold/index.js +150 -0
- package/src/scaffold/tokens.js +90 -0
- package/templates/propagator-module/README.md +99 -0
- package/templates/propagator-module/build.js +103 -0
- package/templates/propagator-module/package.json +19 -0
- package/templates/propagator-module/plugin-manifest.json +66 -0
- package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
- package/templates/propagator-module/tests/module.build.test.mjs +103 -0
package/bin/space-data-module.js
CHANGED
|
@@ -34,6 +34,8 @@ async function main(argv) {
|
|
|
34
34
|
return runCheck(rest);
|
|
35
35
|
case "compile":
|
|
36
36
|
return runCompile(rest);
|
|
37
|
+
case "init":
|
|
38
|
+
return runInit(rest);
|
|
37
39
|
case "flow":
|
|
38
40
|
return runFlow(rest);
|
|
39
41
|
case "parity":
|
|
@@ -98,6 +100,18 @@ function parseArgs(argv) {
|
|
|
98
100
|
case "--out":
|
|
99
101
|
options.outputPath = path.resolve(requireValue(argv, ++index, value));
|
|
100
102
|
break;
|
|
103
|
+
case "--family":
|
|
104
|
+
options.family = requireValue(argv, ++index, value);
|
|
105
|
+
break;
|
|
106
|
+
case "--name":
|
|
107
|
+
options.name = requireValue(argv, ++index, value);
|
|
108
|
+
break;
|
|
109
|
+
case "--plugin-id":
|
|
110
|
+
options.pluginId = requireValue(argv, ++index, value);
|
|
111
|
+
break;
|
|
112
|
+
case "--force":
|
|
113
|
+
options.force = true;
|
|
114
|
+
break;
|
|
101
115
|
case "--recipient-public-key":
|
|
102
116
|
options.recipientPublicKeyHex = requireValue(argv, ++index, value);
|
|
103
117
|
break;
|
|
@@ -219,6 +233,8 @@ function printUsage() {
|
|
|
219
233
|
space-data-module check --repo-root .
|
|
220
234
|
space-data-module check --manifest ./manifest.json --wasm ./dist/module.wasm
|
|
221
235
|
space-data-module compile --manifest ./manifest.json --source ./src/module.c --out ./dist/module.wasm
|
|
236
|
+
space-data-module init --family propagator --name my-propagator
|
|
237
|
+
space-data-module init --family propagator --name my-propagator --out ./modules/my-propagator [--plugin-id com.orbpro.my.propagator] [--force] [--json]
|
|
222
238
|
space-data-module parity --wasm ./dist/isomorphic/module.wasm --fixture ./fixtures/parity/basic.json
|
|
223
239
|
space-data-module parity --wasm ./dist/isomorphic/module.wasm --fixture ./f.json --lanes browser,wasmedge,docker-wasmedge [--json]
|
|
224
240
|
space-data-module parity ... --self-test-divergence docker-wasmedge (fire drill: prove the diff fails loudly)
|
|
@@ -532,6 +548,42 @@ async function runCompile(argv) {
|
|
|
532
548
|
return result.report.ok ? 0 : 1;
|
|
533
549
|
}
|
|
534
550
|
|
|
551
|
+
// space-data-module init --family propagator --name <module-name>
|
|
552
|
+
// [--out <dir>] [--plugin-id <id>] [--force] [--json]
|
|
553
|
+
//
|
|
554
|
+
// Scaffolds a new SDN WASM module skeleton from templates/<family>-module/.
|
|
555
|
+
// An unrecognized --family FAILS LOUDLY (see src/scaffold/index.js) — there
|
|
556
|
+
// is no generic fallback template.
|
|
557
|
+
async function runInit(argv) {
|
|
558
|
+
const options = parseArgs(argv);
|
|
559
|
+
if (!options.family) {
|
|
560
|
+
throw new Error("init requires --family <family> (e.g. --family propagator).");
|
|
561
|
+
}
|
|
562
|
+
if (!options.name) {
|
|
563
|
+
throw new Error("init requires --name <module-name>.");
|
|
564
|
+
}
|
|
565
|
+
const { scaffoldModule } = await import("../src/scaffold/index.js");
|
|
566
|
+
const result = await scaffoldModule({
|
|
567
|
+
family: options.family,
|
|
568
|
+
name: options.name,
|
|
569
|
+
outDir: options.outputPath ?? undefined,
|
|
570
|
+
pluginId: options.pluginId,
|
|
571
|
+
force: options.force === true,
|
|
572
|
+
});
|
|
573
|
+
if (options.json) {
|
|
574
|
+
console.log(JSON.stringify(result, null, 2));
|
|
575
|
+
} else {
|
|
576
|
+
console.log(
|
|
577
|
+
`Scaffolded ${result.family} module "${result.name}" into ${result.outDir}`,
|
|
578
|
+
);
|
|
579
|
+
console.log(` pluginId=${result.pluginId}`);
|
|
580
|
+
for (const file of result.files) {
|
|
581
|
+
console.log(` created ${file}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return result.ok ? 0 : 1;
|
|
585
|
+
}
|
|
586
|
+
|
|
535
587
|
async function runProtect(argv) {
|
|
536
588
|
const options = parseArgs(argv);
|
|
537
589
|
if (!options.manifestPath || !options.wasmPath) {
|
|
@@ -128,7 +128,9 @@ instantiates and the node's capability policy identifies by content hash.
|
|
|
128
128
|
|
|
129
129
|
The domain prefix exists because the node's publisher key is the NODE key, and
|
|
130
130
|
that one bonded key signs several unrelated kinds of statement (dataset
|
|
131
|
-
publications, module artifacts,
|
|
131
|
+
publications, module artifacts, update SIGNALS — the advisory pub/sub nudge a
|
|
132
|
+
publisher pushes so every install upgrades itself in place, which has a live
|
|
133
|
+
producer — and, still reserved, update manifests). If every
|
|
132
134
|
statement were a bare SHA-256 digest, a caller who could reach the node's
|
|
133
135
|
signing endpoint could submit the bytes of one kind of document and staple the
|
|
134
136
|
returned signature onto another. An ASCII domain label, a `NUL` that cannot
|
|
@@ -137,8 +139,10 @@ in a disjoint message space with no length ambiguity anywhere in the preimage.
|
|
|
137
139
|
|
|
138
140
|
The domain registry is **closed**: a verifier refuses any label that is not
|
|
139
141
|
registered, and a module verifier additionally refuses any registered label
|
|
140
|
-
other than `SDN-MODULE-PUBLICATION-V1` — so a signature minted for
|
|
141
|
-
update can never be replayed into a module trailer
|
|
142
|
+
other than `SDN-MODULE-PUBLICATION-V1` — so a signature minted for an update
|
|
143
|
+
manifest or an update signal can never be replayed into a module trailer, which
|
|
144
|
+
the shared vectors pin as `foreign-registered-domain` and
|
|
145
|
+
`update-signal-domain-is-refused-for-a-module`. Adding a domain is a
|
|
142
146
|
reviewed change in every implementation at once
|
|
143
147
|
(`src/bundle/sigdomain.js` here; `internal/sigdomain` and its kubo twin in the
|
|
144
148
|
node), never a request parameter.
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Propagator ABI
|
|
2
|
+
|
|
3
|
+
**Status:** v1 — owner ruling 2026-08-10 ("No JS propagator!!!! WASM ONLY"),
|
|
4
|
+
graph task `harness-w1-propagator-abi-and-reference`, ratified by
|
|
5
|
+
`graph/findings/official-harness-shapes.md` §7 SHIP-1.
|
|
6
|
+
|
|
7
|
+
This is **the** official third-party propagator harness. A propagator that
|
|
8
|
+
touches OrbPro primitives ships as a signed WASM module implementing the
|
|
9
|
+
exports below. The JavaScript `Propagators` registry is INTERNAL engine
|
|
10
|
+
plumbing — how the engine dispatches to compiled modules — and is never
|
|
11
|
+
offered as a public extension point.
|
|
12
|
+
|
|
13
|
+
Nothing in this ABI names Cesium, OrbPro, or a vendor. The same exports serve
|
|
14
|
+
the browser frame-worker pool, a WasmEdge flow runtime, and a deterministic
|
|
15
|
+
test fixture.
|
|
16
|
+
|
|
17
|
+
## Table of contents
|
|
18
|
+
|
|
19
|
+
- [Doctrine](#doctrine)
|
|
20
|
+
- [Capability](#capability)
|
|
21
|
+
- [The export set](#the-export-set)
|
|
22
|
+
- [Wire layout](#wire-layout)
|
|
23
|
+
- [Units](#units)
|
|
24
|
+
- [Frames](#frames)
|
|
25
|
+
- [Identity](#identity)
|
|
26
|
+
- [Threading](#threading)
|
|
27
|
+
- [Error codes](#error-codes)
|
|
28
|
+
- [Lifetime](#lifetime)
|
|
29
|
+
- [Versioning](#versioning)
|
|
30
|
+
- [Parity envelope](#parity-envelope)
|
|
31
|
+
- [Consumer seam](#consumer-seam)
|
|
32
|
+
- [Guest usage](#guest-usage)
|
|
33
|
+
|
|
34
|
+
## Doctrine
|
|
35
|
+
|
|
36
|
+
Five rules govern every line below.
|
|
37
|
+
|
|
38
|
+
1. **One source, generated everywhere.** The structs, enums, size locks and
|
|
39
|
+
offset locks in this ABI are GENERATED from
|
|
40
|
+
`schemas/orbpro/Propagator.fbs`. No hand-written mirror is legitimate
|
|
41
|
+
anywhere in the stack. Before W1.1 there were five, they disagreed, and the
|
|
42
|
+
disagreement was a units contradiction inside a single file — kilometres in
|
|
43
|
+
the field comments, METERS in the layout block three lines below.
|
|
44
|
+
2. **The wire is bytes at offsets, and the offsets are locked.** Every ABI
|
|
45
|
+
struct carries `_Static_assert` on its size AND on every field offset. This
|
|
46
|
+
is not decoration: JavaScript reads these structs out of linear memory at
|
|
47
|
+
byte offsets, and no runtime check can catch a shifted field — `6778` and
|
|
48
|
+
`6778000` are both finite doubles.
|
|
49
|
+
3. **Refuse rather than approximate.** A propagator that cannot answer returns
|
|
50
|
+
a documented negative code. It never returns a plausible number it does not
|
|
51
|
+
stand behind, and it never traps on input it was given the chance to
|
|
52
|
+
validate. A `converged` flag is never trusted by a consumer; it is
|
|
53
|
+
adjudicated by verify-by-propagation.
|
|
54
|
+
4. **Declare what you are.** Frame, validity flags and reserved bytes are
|
|
55
|
+
WRITTEN, every call. A state vector that leaves `reference_frame` at its
|
|
56
|
+
default is unreadable by a host that honours the field, and one that leaves
|
|
57
|
+
the padding bytes alone is handing back the previous call's data.
|
|
58
|
+
5. **The engine owns the schedule; the module owns one row.** Sharding a batch
|
|
59
|
+
across workers is the host's decision. A module writes only the rows it was
|
|
60
|
+
given and holds no cross-row state, which is what makes it safe under any
|
|
61
|
+
sharding the host chooses.
|
|
62
|
+
|
|
63
|
+
## Capability
|
|
64
|
+
|
|
65
|
+
A propagator module declares family `propagator` in its
|
|
66
|
+
`plugin-manifest.json`. The family vocabulary is authoritative and
|
|
67
|
+
fail-closed: `normalizePluginFamily` throws `UnknownPluginFamilyError` naming
|
|
68
|
+
the value and the vocabulary (W0.3). There is no silent `ANALYSIS` fallback —
|
|
69
|
+
that fallback silently mislabelled 22 modules.
|
|
70
|
+
|
|
71
|
+
**The namespace rule.** A `$`-prefixed four-byte file identifier is ratified
|
|
72
|
+
SDS. A bare four-byte identifier is a vendor invention, and **a harness MUST
|
|
73
|
+
refuse it**. A port declaring `acceptsAnyFlatbuffer` is unconformable and is
|
|
74
|
+
not admissible on a harnessed family: a wildcard cannot be
|
|
75
|
+
conformance-tested, and six first-party manifests currently declare one on
|
|
76
|
+
both faces.
|
|
77
|
+
|
|
78
|
+
The reference module's ingest port is the worked example: it declares exactly
|
|
79
|
+
`$OMM`, in both its canonical FlatBuffer form and its aligned-binary peer, and
|
|
80
|
+
nothing else.
|
|
81
|
+
|
|
82
|
+
## The export set
|
|
83
|
+
|
|
84
|
+
Exports are announced with `__attribute__((export_name(...)))`. The SDK
|
|
85
|
+
compiler exports the invoke-surface symbols and every declared `methodId`;
|
|
86
|
+
the propagator ABI entry points are not `methodId`s, so they announce
|
|
87
|
+
themselves.
|
|
88
|
+
|
|
89
|
+
### Required
|
|
90
|
+
|
|
91
|
+
| Export | Signature | Returns |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `plugin_init` | `int32_t(const uint8_t* data, size_t len)` | entities initialized (>0), or a negative [error code](#error-codes) |
|
|
94
|
+
| `plugin_propagate` | `int32_t(double julian_date, uint32_t entity_index, OrbProStateVector* out)` | `0`, or a negative error code |
|
|
95
|
+
| `plugin_destroy` | `void(void)` | — |
|
|
96
|
+
|
|
97
|
+
At least one of `plugin_propagate` or `plugin_propagate_batch` must exist;
|
|
98
|
+
shipping both is expected, and they must agree exactly (see
|
|
99
|
+
[parity envelope](#parity-envelope)).
|
|
100
|
+
|
|
101
|
+
### Typed ingest
|
|
102
|
+
|
|
103
|
+
| Export | Signature | Returns |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `plugin_init_omm` | `int32_t(const OrbProOMMRecord* records, uint32_t count)` | entities now held, or negative |
|
|
106
|
+
| `plugin_ingest_omm_one` | `int32_t(const OrbProOMMRecord* record)` | **the handle it assigned**, or negative |
|
|
107
|
+
| `plugin_init_elements` | `int32_t(const OrbProOrbitalElements* elements, uint32_t count)` | entities initialized, or negative |
|
|
108
|
+
|
|
109
|
+
`plugin_init_omm` REPLACES the element set. `plugin_ingest_omm_one` APPENDS
|
|
110
|
+
and returns its handle — see [identity](#identity).
|
|
111
|
+
|
|
112
|
+
### Batch and introspection
|
|
113
|
+
|
|
114
|
+
| Export | Signature | Returns |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `plugin_propagate_batch` | `int32_t(double julian_date, OrbProStateVector* out, uint32_t count)` | `0`, or negative |
|
|
117
|
+
| `plugin_entity_count` | `int32_t(void)` | entities currently held |
|
|
118
|
+
|
|
119
|
+
`plugin_init` must accept a packed array of `OrbProOMMRecord` and MUST refuse
|
|
120
|
+
a length that is not a whole multiple of `sizeof(OrbProOMMRecord)`. A partial
|
|
121
|
+
trailing record means the caller and the module disagree about the struct
|
|
122
|
+
size, and the size lock cannot see across the boundary.
|
|
123
|
+
|
|
124
|
+
## Wire layout
|
|
125
|
+
|
|
126
|
+
Generated header: `include/orbpro/orbpro_propagator_abi.h`.
|
|
127
|
+
Generated TS byte offsets: `space-data-module-sdk/generated/propagator-abi`.
|
|
128
|
+
**Read offsets from the generated bindings. Never write a literal `8`.**
|
|
129
|
+
|
|
130
|
+
### `OrbProStateVector` — 64 bytes, 8-byte aligned
|
|
131
|
+
|
|
132
|
+
| Offset | Size | Type | Field |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| 0 | 8 | float64 | `epoch` — Julian date |
|
|
135
|
+
| 8 | 24 | float64×3 | `position` — **METERS** |
|
|
136
|
+
| 32 | 24 | float64×3 | `velocity` — **METERS/SECOND** |
|
|
137
|
+
| 56 | 1 | uint8 | `reference_frame` |
|
|
138
|
+
| 57 | 3 | uint8×3 | padding — **MUST be written as zero** |
|
|
139
|
+
| 60 | 4 | uint32 | `flags` |
|
|
140
|
+
|
|
141
|
+
The one-byte frame plus three reserved bytes is a DECLARED layout, not an
|
|
142
|
+
accident. The C header formerly declared a `uint32_t` at offset 56, which is
|
|
143
|
+
wire-identical only by little-endian accident (W0.2).
|
|
144
|
+
|
|
145
|
+
### `OrbProOMMRecord` — 88 bytes, 8-byte aligned
|
|
146
|
+
|
|
147
|
+
| Offset | Size | Field | Units |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| 0 | 8 | `epoch_jd` | Julian date |
|
|
150
|
+
| 8 | 8 | `mean_motion` | REV/DAY |
|
|
151
|
+
| 16 | 8 | `eccentricity` | — |
|
|
152
|
+
| 24 | 8 | `inclination` | DEGREES |
|
|
153
|
+
| 32 | 8 | `ra_of_asc_node` | DEGREES |
|
|
154
|
+
| 40 | 8 | `arg_of_pericenter` | DEGREES |
|
|
155
|
+
| 48 | 8 | `mean_anomaly` | DEGREES |
|
|
156
|
+
| 56 | 8 | `bstar` | 1/earth-radii |
|
|
157
|
+
| 64 | 8 | `mean_motion_dot` | REV/DAY² |
|
|
158
|
+
| 72 | 8 | `mean_motion_ddot` | REV/DAY³ |
|
|
159
|
+
| 80 | 4 | `norad_cat_id` | uint32 |
|
|
160
|
+
| 84 | 4 | padding | MUST be zero |
|
|
161
|
+
|
|
162
|
+
**This struct is also an on-disk format.** The first-party SGP4 module
|
|
163
|
+
persists it verbatim as a SQLite BLOB
|
|
164
|
+
(`sqlite3_bind_blob(..., &omm, sizeof(OrbProOMMRecord), ...)`). Until W1.1 it
|
|
165
|
+
carried no size or offset lock anywhere in the stack, so the layout every
|
|
166
|
+
stored blob depends on was held only by the field order of one hand-written
|
|
167
|
+
C struct in one module.
|
|
168
|
+
|
|
169
|
+
The layout above is that layout, exactly as it has already been written to
|
|
170
|
+
disk, trailing padding included. Locking it revealed **no ambiguity**: the
|
|
171
|
+
IDL-derived layout reproduces the hand-written struct byte for byte, so the
|
|
172
|
+
lock pins the existing wire rather than changing it. Migration is out of
|
|
173
|
+
scope and would invalidate every stored blob.
|
|
174
|
+
|
|
175
|
+
### `OrbProOrbitalElements` — 64 bytes, 8-byte aligned
|
|
176
|
+
|
|
177
|
+
Eight float64 in declaration order: `semi_major_axis` (**KILOMETRES**),
|
|
178
|
+
`eccentricity`, `inclination`, `raan`, `arg_periapsis`, `true_anomaly`
|
|
179
|
+
(all RADIANS), `epoch` (Julian date), `reserved` (MUST be 0).
|
|
180
|
+
|
|
181
|
+
## Units
|
|
182
|
+
|
|
183
|
+
> `OrbProStateVector.position` IS IN METERS.
|
|
184
|
+
> `OrbProStateVector.velocity` IS IN METERS PER SECOND.
|
|
185
|
+
|
|
186
|
+
There is no km variant, no per-plugin unit flag, and no negotiation. A plugin
|
|
187
|
+
that writes kilometres here is off by 1000× and its satellites render inside
|
|
188
|
+
the Earth.
|
|
189
|
+
|
|
190
|
+
The ONE place kilometres survive is `OrbProOrbitalElements.semi_major_axis`,
|
|
191
|
+
an INPUT struct that is not the state vector. It is kilometres there and
|
|
192
|
+
stays kilometres. Do not "unify" them.
|
|
193
|
+
|
|
194
|
+
Ruling: finding §4.1 / §8.1; landed as W0.1.
|
|
195
|
+
|
|
196
|
+
## Frames
|
|
197
|
+
|
|
198
|
+
`reference_frame` is `OrbProReferenceFrame`:
|
|
199
|
+
`TEME=0 J2000=1 ICRF=2 ECEF=3 MCI=4 MCMF=5`.
|
|
200
|
+
|
|
201
|
+
**Plugins output ECEF.** The frame transform happens INSIDE the module. A
|
|
202
|
+
plugin that emits an inertial frame and expects the host to rotate it is
|
|
203
|
+
relying on a host path that is still unimplemented — `PropagatorPlugin.toICRF`
|
|
204
|
+
carries a live TEME≈ICRF approximation, and an ECEF input there is wrong by up
|
|
205
|
+
to a full Earth rotation (`orbpro-toicrf-frame-transform-unimplemented`).
|
|
206
|
+
|
|
207
|
+
**Never let a raw integer frame value cross a boundary unqualified.** Four
|
|
208
|
+
incompatible `ReferenceFrame` vocabularies are live on this seam:
|
|
209
|
+
|
|
210
|
+
| Vocabulary | Values |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `orbpro.propagator` (**this ABI**) | TEME=0 J2000=1 ICRF=2 ECEF=3 MCI=4 MCMF=5 |
|
|
213
|
+
| `orbpro.plugins` (`PropagatorState.fbs`) | ECI=0 ECEF=1 TEME=2 ICRF=3 |
|
|
214
|
+
| `Cesium.ReferenceFrame` | FIXED=0 INERTIAL=1 |
|
|
215
|
+
| `ConjunctionCommon.fbs` | ECI=1 |
|
|
216
|
+
|
|
217
|
+
`ECI==0`, `TEME==0` and `FIXED==0` all collide, and ECEF is 1 in the second
|
|
218
|
+
but 3 here. The second vocabulary's values are frozen by compiled WASM
|
|
219
|
+
artifacts already in the field, so collapsing them is a wire break, tracked as
|
|
220
|
+
`sdk-reference-frame-enum-unification`. Until it lands, **translate by named
|
|
221
|
+
token at every seam**.
|
|
222
|
+
|
|
223
|
+
Use the generated setter `orbpro_state_set_reference_frame()`, never a bare
|
|
224
|
+
assignment: it clears the three padding bytes a consumer reading offset 56 as
|
|
225
|
+
a 32-bit word would otherwise see as garbage.
|
|
226
|
+
|
|
227
|
+
## Identity
|
|
228
|
+
|
|
229
|
+
**`NORAD_CAT_ID` is the identity authority.** It is carried through, never
|
|
230
|
+
invented, never synthesized to make a lookup succeed.
|
|
231
|
+
|
|
232
|
+
**The entity index is a local handle** into one module instance's own array.
|
|
233
|
+
It is meaningless outside that instance and must never be persisted as an
|
|
234
|
+
identity.
|
|
235
|
+
|
|
236
|
+
### Creating engine state RETURNS its handle
|
|
237
|
+
|
|
238
|
+
This is the single highest-value primitive the harness adds.
|
|
239
|
+
|
|
240
|
+
```c
|
|
241
|
+
int32_t handle = plugin_ingest_omm_one(&record); /* -> the handle assigned */
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A caller must never derive "the entity I just created" as `count − 1`. Three
|
|
245
|
+
families in this stack independently reinvented that derivation; all three are
|
|
246
|
+
race-unsafe, undeclared and untested, and it is the root of defect B3 (the
|
|
247
|
+
maneuver marker renders from a buffer the seam never writes).
|
|
248
|
+
|
|
249
|
+
The engine-side implementation of this primitive for the FIRST-PARTY
|
|
250
|
+
propagators is **W1.5** (`graph/tasks/official-harness-shapes-program.md`).
|
|
251
|
+
This document states the contract now; third-party modules are expected to
|
|
252
|
+
honour it from day one, and the reference module does.
|
|
253
|
+
|
|
254
|
+
## Threading
|
|
255
|
+
|
|
256
|
+
Modules compile to `wasm32-wasip1-threads` (clang), per the isomorphic-pthreads
|
|
257
|
+
law. **Never `emcc -pthread`** — that emits the browser-only Web Worker +
|
|
258
|
+
postMessage thread model and has no wasi thread-spawn contract, so it cannot
|
|
259
|
+
thread under WasmEdge.
|
|
260
|
+
|
|
261
|
+
Two thread models are legitimate, and both use that same toolchain:
|
|
262
|
+
|
|
263
|
+
- **`emscripten-pthreads`** — despite the name, the real wasi-threads contract:
|
|
264
|
+
the guest imports `wasi.thread-spawn` and exports `wasi_thread_start` over an
|
|
265
|
+
imported shared memory. The post-link artifact guard fails the build if the
|
|
266
|
+
emitted wasm does not actually carry shared memory and atomics.
|
|
267
|
+
- **`wasi-sequential`** — the module provably never spawns a thread. Requires
|
|
268
|
+
`manifest.sequentialJustification` with a `kind` and a substantive `detail`;
|
|
269
|
+
a mirror guard fails the build if the artifact is not what was claimed.
|
|
270
|
+
|
|
271
|
+
**A propagator is normally `wasi-sequential`, and that is the strong default.**
|
|
272
|
+
Propagation is embarrassingly parallel ACROSS entities and strictly sequential
|
|
273
|
+
WITHIN one, and the sharding belongs to the host. A module that spawns its own
|
|
274
|
+
pool contends with the pool already scheduling it.
|
|
275
|
+
|
|
276
|
+
> **Known defect:** `resolveThreadModel` reads the compile OPTION, not
|
|
277
|
+
> `manifest.threadModel`, and otherwise infers the model from
|
|
278
|
+
> `runtimeTargets` — where `"wasmedge"` infers pthreads. Pass
|
|
279
|
+
> `threadModel: manifest.threadModel` explicitly until
|
|
280
|
+
> `sdk-manifest-threadmodel-silently-ignored` lands, and assert the compiler
|
|
281
|
+
> agreed.
|
|
282
|
+
|
|
283
|
+
### Shard write discipline
|
|
284
|
+
|
|
285
|
+
When the host runs `plugin_propagate_batch` across a worker pool it hands each
|
|
286
|
+
worker the SAME output base pointer and a DISJOINT index range.
|
|
287
|
+
|
|
288
|
+
- Write **only** rows in your own range. Never write outside your stride.
|
|
289
|
+
- Never READ a neighbour's row. Your output must not depend on rows you were
|
|
290
|
+
not given.
|
|
291
|
+
- Hold no cross-row state between rows of one batch.
|
|
292
|
+
- On failure, zero the offending row before returning, so a host that ignores
|
|
293
|
+
the return value still reads a state marked not-valid rather than stale
|
|
294
|
+
bytes. A partially written batch with no signal is the silent-wrong-numbers
|
|
295
|
+
failure this ABI exists to prevent.
|
|
296
|
+
|
|
297
|
+
A module that satisfies these is safe under any sharding the host chooses,
|
|
298
|
+
which is the property the ABI actually requires — not a particular thread
|
|
299
|
+
count.
|
|
300
|
+
|
|
301
|
+
## Error codes
|
|
302
|
+
|
|
303
|
+
Every failure returns its OWN documented negative code. A propagator that
|
|
304
|
+
returns `-1` for everything is unconformable: the host cannot tell a bad
|
|
305
|
+
entity index from an uninitialized module, so it cannot place the failure on
|
|
306
|
+
the degradation ladder (transient → skip; fatal → respawn; exhausted → latch).
|
|
307
|
+
|
|
308
|
+
| Code | Name | Meaning |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| `0` | OK | success |
|
|
311
|
+
| `-1` | NOT_INITIALIZED | no elements ingested yet |
|
|
312
|
+
| `-2` | BAD_ENTITY_INDEX | index ≥ entity count |
|
|
313
|
+
| `-3` | NULL_OUTPUT | caller passed a null output pointer |
|
|
314
|
+
| `-4` | BAD_INPUT | malformed or short input buffer |
|
|
315
|
+
| `-5` | NOT_CONVERGED | the solve failed to converge |
|
|
316
|
+
| `-6` | UNPHYSICAL | the elements describe no closed orbit |
|
|
317
|
+
|
|
318
|
+
Rules that are not negotiable:
|
|
319
|
+
|
|
320
|
+
- **Validated input can never trap.** Malformed input is a code, not a crash.
|
|
321
|
+
- **NaN is its own failure class.** It is never "a number that happened".
|
|
322
|
+
- **A physically impossible result is a refusal**, not an output.
|
|
323
|
+
- **Error classes are identical across runtimes.** A code that differs between
|
|
324
|
+
browser and WasmEdge is a P1 SDK defect.
|
|
325
|
+
|
|
326
|
+
## Lifetime
|
|
327
|
+
|
|
328
|
+
`plugin_destroy` is **required**, and it must actually release.
|
|
329
|
+
|
|
330
|
+
The test is mechanical: N × ingest / propagate / destroy must reach a steady
|
|
331
|
+
memory baseline. WebAssembly linear memory never shrinks, so "memory went back
|
|
332
|
+
down" is not available and a test asserting it would assert something
|
|
333
|
+
impossible. What a non-leaking module gives you is that growth STOPS: after a
|
|
334
|
+
warm-up that pays for every allocation the cycle will ever need, further
|
|
335
|
+
identical cycles add ZERO pages.
|
|
336
|
+
|
|
337
|
+
`destroy` must also be idempotent, must leave `plugin_entity_count()` at zero,
|
|
338
|
+
and must leave the module usable — a destroyed module refuses to propagate
|
|
339
|
+
(`NOT_INITIALIZED`) rather than reading freed state, and comes back cleanly on
|
|
340
|
+
the next ingest.
|
|
341
|
+
|
|
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.
|
|
351
|
+
|
|
352
|
+
## Versioning
|
|
353
|
+
|
|
354
|
+
`abi_version` gates at register/load. `ORBPRO_ABI_VERSION` is declared in
|
|
355
|
+
`orbpro_plugin.h`; a module declares `abiVersion` in its manifest, and a
|
|
356
|
+
mismatch is refused with `ORBPRO_ERROR_ABI_MISMATCH`, never coerced.
|
|
357
|
+
|
|
358
|
+
Shape versions are `SHAPE_MAJOR.SHAPE_MINOR`, independent of the SDS wave
|
|
359
|
+
counter.
|
|
360
|
+
|
|
361
|
+
- **Additive-only within a MAJOR.** Enforced by the drift gate, not by prose:
|
|
362
|
+
`npm run check:propagator-abi` regenerates every artifact from the IDL and
|
|
363
|
+
byte-diffs it against what is committed. Any difference fails.
|
|
364
|
+
- **Consumers declare a FLOOR plus a MAJOR, never exact equality.**
|
|
365
|
+
Exact-equality resolution caused three build outages and a P1 in one week.
|
|
366
|
+
Floors only advance.
|
|
367
|
+
- **Stability promise:** additive-only for two minors; a breaking change
|
|
368
|
+
requires a one-minor deprecation notice.
|
|
369
|
+
- **Deprecation, never deletion.**
|
|
370
|
+
- **Refusals are legible.** A mismatch names the shape, what was required and
|
|
371
|
+
what was offered — never "not found".
|
|
372
|
+
|
|
373
|
+
## Parity envelope
|
|
374
|
+
|
|
375
|
+
**Inside the envelope** — byte-identical across browser, native WasmEdge and
|
|
376
|
+
Docker WasmEdge, at thread counts 1/2/4/8:
|
|
377
|
+
|
|
378
|
+
- every byte of every `OrbProStateVector` a module writes for given inputs
|
|
379
|
+
- `plugin_propagate` and `plugin_propagate_batch` for the same entity and epoch
|
|
380
|
+
- every error code, for every malformed and unsatisfiable input
|
|
381
|
+
- the handle `plugin_ingest_omm_one` assigns, for a given ingest order
|
|
382
|
+
- the steady-state verdict of the lifecycle leak test
|
|
383
|
+
|
|
384
|
+
Determinism is compared as **bytes, not as numbers**, and must survive a
|
|
385
|
+
destroy / re-ingest cycle: output that changes after a lifecycle round trip is
|
|
386
|
+
state leaking across it.
|
|
387
|
+
|
|
388
|
+
**Outside the envelope**, stated so it is never mistaken for a defect: results
|
|
389
|
+
from a propagator whose physics depends on live external data (space weather,
|
|
390
|
+
EOP) at different acquisition instants. Their ABI BEHAVIOUR — codes, struct
|
|
391
|
+
shapes, frame declaration, padding, copy accounting — is fully inside.
|
|
392
|
+
|
|
393
|
+
**Divergence in anything listed as inside the envelope is a P1 SDK defect.**
|
|
394
|
+
Not a platform quirk. File, block, fix.
|
|
395
|
+
|
|
396
|
+
Run it:
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
space-data-module parity-gate --artifact <id>=./dist/isomorphic/module.wasm:module
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Consumer seam
|
|
403
|
+
|
|
404
|
+
Per the pluggable-propagation law (owner, 2026-07-29), **every surface that
|
|
405
|
+
consumes a propagator takes it as a parameter or port** — never hardwired to
|
|
406
|
+
SGP4 or any single provider. A Sandcastle demo resolves it once from
|
|
407
|
+
`?propagator=NAME` and feeds that single value to every consumer downstream.
|
|
408
|
+
|
|
409
|
+
Engine-side, `PropagatedPositionProperty` accepts a propagator instance OR a
|
|
410
|
+
registered name/id, and orbits are drawn by the regular path visualizer.
|
|
411
|
+
Third-party modules reach it through the storefront ADD path —
|
|
412
|
+
`Cesium.registerPlugin(name, source)` then `initPlugins({ plugins: [name] })`.
|
|
413
|
+
Built-in bundle names are refused there (`E92`); a third-party module uses its
|
|
414
|
+
own name.
|
|
415
|
+
|
|
416
|
+
## Guest usage
|
|
417
|
+
|
|
418
|
+
The complete, buildable reference implementation lives at
|
|
419
|
+
`space-data-network-modules/propagator/keplerian-reference/`. Every export in
|
|
420
|
+
it is annotated with the section of this document it implements, and
|
|
421
|
+
`space-data-module init --family propagator` scaffolds its skeleton.
|
|
422
|
+
|
|
423
|
+
```c
|
|
424
|
+
#include "space_data_module_invoke.h"
|
|
425
|
+
#include "orbpro/orbpro_propagator_abi.h" /* the ONE generated ABI */
|
|
426
|
+
|
|
427
|
+
#define ORBPRO_ABI_EXPORT(name) __attribute__((export_name(name))) extern "C"
|
|
428
|
+
|
|
429
|
+
ORBPRO_ABI_EXPORT("plugin_propagate")
|
|
430
|
+
int32_t plugin_propagate(double julian_date, uint32_t entity_index,
|
|
431
|
+
OrbProStateVector* out) {
|
|
432
|
+
if (out == NULL) return -3; /* NULL_OUTPUT */
|
|
433
|
+
if (entity_count == 0) return -1; /* NOT_INITIALIZED */
|
|
434
|
+
if (entity_index >= entity_count) return -2; /* BAD_ENTITY_INDEX */
|
|
435
|
+
|
|
436
|
+
/* Start from the initializer: it zeroes the WHOLE struct, including the
|
|
437
|
+
* three reserved bytes the IDL requires to be zero. The host reuses one
|
|
438
|
+
* scratch buffer across every call, so a partial write hands back the
|
|
439
|
+
* previous call's bytes. */
|
|
440
|
+
orbpro_state_init(out);
|
|
441
|
+
out->epoch = julian_date;
|
|
442
|
+
out->position[0] = x_metres; /* METERS. Not kilometres. */
|
|
443
|
+
out->position[1] = y_metres;
|
|
444
|
+
out->position[2] = z_metres;
|
|
445
|
+
out->velocity[0] = vx_metres_per_second;
|
|
446
|
+
out->velocity[1] = vy_metres_per_second;
|
|
447
|
+
out->velocity[2] = vz_metres_per_second;
|
|
448
|
+
|
|
449
|
+
/* Use the generated setter — it clears the padding bytes. */
|
|
450
|
+
orbpro_state_set_reference_frame(out, ORBPRO_FRAME_ECEF);
|
|
451
|
+
out->flags |= (uint32_t)ORBPRO_STATE_VALID;
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Reading a state vector from JavaScript, without a single literal offset:
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
import { ORBPRO_STATE_VECTOR } from "space-data-module-sdk/generated/propagator-abi";
|
|
460
|
+
|
|
461
|
+
const { offsets, size } = ORBPRO_STATE_VECTOR;
|
|
462
|
+
const view = new DataView(memory.buffer, pointer, size);
|
|
463
|
+
const epoch = view.getFloat64(offsets.epoch, true);
|
|
464
|
+
const x = view.getFloat64(offsets.position, true);
|
|
465
|
+
const frame = view.getUint8(offsets.reference_frame);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Regenerating
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
node scripts/generate-propagator-abi.mjs # regenerate from the IDL
|
|
472
|
+
node scripts/check-propagator-abi.mjs # the drift gate (runs in npm test)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The gate ships its own negative control: a test corrupts a copy of the
|
|
476
|
+
generated tree and requires the gate to name the corruption. A gate never
|
|
477
|
+
observed to fail is indistinguishable from one that cannot fail.
|