space-data-module-sdk 0.8.11 → 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.
Files changed (40) hide show
  1. package/README.md +92 -0
  2. package/bin/space-data-module.js +85 -1
  3. package/docs/module-publication-standard.md +7 -3
  4. package/docs/propagator-abi.md +477 -0
  5. package/include/orbpro/orbpro_propagator_abi.h +312 -0
  6. package/package.json +7 -1
  7. package/schemas/PluginManifest.fbs +46 -1
  8. package/schemas/orbpro/Propagator.fbs +161 -3
  9. package/src/browser.js +11 -0
  10. package/src/bundle/index.js +1 -0
  11. package/src/bundle/sigdomain.js +22 -0
  12. package/src/capabilities.js +91 -0
  13. package/src/compliance/index.js +8 -0
  14. package/src/compliance/pluginCompliance.js +76 -32
  15. package/src/flow/flowCompiler.js +231 -3
  16. package/src/flow/flowRuntimeHost.js +26 -0
  17. package/src/flow/isomorphicFlowHost.js +8 -0
  18. package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
  19. package/src/generated/orbpro/manifest/plugin-family.js +8 -0
  20. package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
  21. package/src/generated/orbpro/propagator-abi.js +118 -0
  22. package/src/generated/orbpro/propagator-abi.ts +199 -0
  23. package/src/host/browserModuleHarness.js +26 -0
  24. package/src/host/isomorphicLoader.js +57 -11
  25. package/src/host/runtimeTargetGate.js +256 -0
  26. package/src/host/workerModuleHarness.js +7 -0
  27. package/src/index.d.ts +47 -0
  28. package/src/index.js +11 -0
  29. package/src/manifest/normalize.js +113 -4
  30. package/src/scaffold/copyTemplate.js +71 -0
  31. package/src/scaffold/index.js +150 -0
  32. package/src/scaffold/tokens.js +90 -0
  33. package/src/testing/parityBrowserRunner.js +11 -0
  34. package/src/testing/parityGate.js +287 -27
  35. package/templates/propagator-module/README.md +99 -0
  36. package/templates/propagator-module/build.js +103 -0
  37. package/templates/propagator-module/package.json +19 -0
  38. package/templates/propagator-module/plugin-manifest.json +66 -0
  39. package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
  40. package/templates/propagator-module/tests/module.build.test.mjs +103 -0
@@ -0,0 +1,66 @@
1
+ {
2
+ "pluginId": "__PLUGIN_ID__",
3
+ "name": "__MODULE_NAME__ propagator",
4
+ "version": "0.1.0",
5
+ "description": "TODO: describe __MODULE_NAME__'s propagation model. Scaffolded from space-data-module-sdk's propagator-module template — see README.md for the ABI obligations this manifest and src/__MODULE_NAME_SNAKE__.cpp must keep meeting as you fill in real physics.",
6
+ "pluginFamily": "propagator",
7
+ "capabilities": [],
8
+ "externalInterfaces": [],
9
+ "invokeSurfaces": ["direct", "command"],
10
+ "runtimeTargets": ["browser", "wasmedge"],
11
+ "threadModel": "wasi-sequential",
12
+ "sequentialJustification": {
13
+ "kind": "caller-level-parallelism",
14
+ "detail": "Propagation is embarrassingly parallel ACROSS entities and strictly sequential WITHIN one. The propagator ABI (docs/propagator-abi.md §9) puts the sharding on the HOST: the frame-worker pool hands each worker the same output base pointer and a disjoint index range. This module therefore spawns no threads of its own and holds no cross-row state, which is what makes it safe under ANY sharding the host chooses. A module that spawned its own pool here would contend with the pool already scheduling it. Both thread models compile through the same clang wasm32-wasip1-threads toolchain; this one links no thread-spawn contract because it genuinely never spawns."
15
+ },
16
+ "methods": [
17
+ {
18
+ "methodId": "ingest_omm",
19
+ "displayName": "Ingest OMM",
20
+ "description": "Adopts mean elements from SDS $OMM records and replaces the module's element set. Propagation itself is reached through the propagator ABI exports (plugin_propagate / plugin_propagate_batch), not through this invoke surface.",
21
+ "inputPorts": [
22
+ {
23
+ "portId": "omm",
24
+ "acceptedTypeSets": [
25
+ {
26
+ "setId": "omm",
27
+ "allowedTypes": [
28
+ {
29
+ "schemaName": "OMM.fbs",
30
+ "fileIdentifier": "$OMM",
31
+ "rootTypeName": "OMM",
32
+ "wireFormat": "flatbuffer"
33
+ },
34
+ {
35
+ "schemaName": "OMM.fbs",
36
+ "fileIdentifier": "$OMM",
37
+ "rootTypeName": "OMM",
38
+ "wireFormat": "aligned-binary",
39
+ "requiredAlignment": 8
40
+ }
41
+ ],
42
+ "description": "Ratified SDS $OMM mean elements, unpacked to the ABI's 88-byte OrbProOMMRecord layout. A bare (non-$) 4-byte identifier is a vendor invention and is deliberately not accepted here."
43
+ }
44
+ ],
45
+ "minStreams": 1,
46
+ "maxStreams": 65535,
47
+ "required": true,
48
+ "description": "OMM mean-element records."
49
+ }
50
+ ],
51
+ "outputPorts": [],
52
+ "maxBatch": 1024,
53
+ "drainPolicy": "drain-to-empty"
54
+ }
55
+ ],
56
+ "schemasUsed": [],
57
+ "buildArtifacts": [
58
+ {
59
+ "artifactId": "__MODULE_NAME_SNAKE__-isomorphic",
60
+ "kind": "wasm",
61
+ "path": "dist/isomorphic/module.wasm",
62
+ "target": "browser,wasmedge"
63
+ }
64
+ ],
65
+ "abiVersion": 1
66
+ }
@@ -0,0 +1,450 @@
1
+ // =============================================================================
2
+ // __MODULE_NAME__ — SDN propagator module
3
+ // =============================================================================
4
+ //
5
+ // Scaffolded from space-data-module-sdk's propagator-module template. This
6
+ // file implements every OBLIGATION of the official OrbPro WASM propagator
7
+ // ABI — exports, wire layout, units, frames, identity, threading discipline,
8
+ // error codes, and lifetime — and leaves exactly ONE thing for you to fill
9
+ // in: the orbital mechanics inside `propagate_entity()`, marked below.
10
+ //
11
+ // See README.md in this directory for the full obligation checklist, and
12
+ // `orbpro/orbpro_propagator_abi.h` (generated from the pinned
13
+ // space-data-module-sdk and inlined into this build by build.js) for the
14
+ // normative wire-level contract.
15
+ //
16
+ // Build: `npm run build` -> dist/isomorphic/module.wasm
17
+ // (SDK compiler lane, clang wasm32-wasip1-threads per the
18
+ // isomorphic-pthreads law; NEVER emcc -pthread)
19
+ // =============================================================================
20
+
21
+ #include "space_data_module_invoke.h"
22
+
23
+ // The ONE source of the ABI. Generated from schemas/orbpro/Propagator.fbs in
24
+ // space-data-module-sdk and inlined here by build.js. Do not retype these
25
+ // structs into a local copy — that is exactly the drift this generated
26
+ // header exists to end.
27
+ #include "orbpro/orbpro_propagator_abi.h"
28
+
29
+ #include <cmath>
30
+ #include <cstdint>
31
+ #include <cstring>
32
+ #include <vector>
33
+
34
+ // -----------------------------------------------------------------------------
35
+ // ABI §4 — export macro. `export_name` puts the symbol in the module's
36
+ // export table directly. The SDK compiler exports the invoke-surface symbol
37
+ // (`ingest_omm` below) for you from plugin-manifest.json; the PROPAGATOR ABI
38
+ // entry points are not methodIds, so they announce themselves here.
39
+ // -----------------------------------------------------------------------------
40
+ #define ORBPRO_ABI_EXPORT(name) __attribute__((export_name(name))) extern "C"
41
+
42
+ namespace {
43
+
44
+ constexpr double kTwoPi = 2.0 * 3.14159265358979323846;
45
+ constexpr double kDegToRad = 3.14159265358979323846 / 180.0;
46
+ constexpr double kSecondsPerDay = 86400.0;
47
+
48
+ // -----------------------------------------------------------------------------
49
+ // ABI §10 — error codes. Every one of these is a NAMED, documented, negative
50
+ // return. A propagator that returns -1 for everything is unconformable: the
51
+ // host cannot tell a bad entity index from an uninitialized module, so it
52
+ // cannot decide whether to retry, skip, or latch.
53
+ // -----------------------------------------------------------------------------
54
+ constexpr int32_t kOk = 0;
55
+ constexpr int32_t kErrNotInitialized = -1; // no elements ingested yet
56
+ constexpr int32_t kErrBadEntityIndex = -2; // index >= entity count
57
+ constexpr int32_t kErrNullOutput = -3; // caller passed a null out pointer
58
+ constexpr int32_t kErrBadInput = -4; // malformed / short input buffer
59
+ constexpr int32_t kErrNotConverged = -5; // reserved for iterative solvers
60
+ constexpr int32_t kErrUnphysical = -6; // elements describe no orbit
61
+
62
+ // -----------------------------------------------------------------------------
63
+ // ABI §8 — identity. NORAD_CAT_ID is the identity authority and it is
64
+ // carried, not invented. The entity index is a LOCAL handle into this
65
+ // module's own array; it is returned by ingest and is meaningless outside
66
+ // this module instance.
67
+ //
68
+ // Note what ingest does NOT do: it never derives "the entity I just
69
+ // created" as `count - 1`. Ingest RETURNS the handle it assigned
70
+ // (`plugin_ingest_omm_one` below) — the create-returns-handle primitive a
71
+ // caller can rely on under a threaded host.
72
+ // -----------------------------------------------------------------------------
73
+ struct Entity {
74
+ uint32_t norad_cat_id = 0;
75
+ double epoch_jd = 0.0;
76
+ double mean_motion_rad_s = 0.0;
77
+ double semi_major_axis_m = 0.0;
78
+ double eccentricity = 0.0;
79
+ double inclination_rad = 0.0;
80
+ double raan_rad = 0.0;
81
+ double arg_pericenter_rad = 0.0;
82
+ double mean_anomaly_rad = 0.0;
83
+ };
84
+
85
+ // The module's entire mutable state. `plugin_destroy` returns this to
86
+ // exactly the shape it has at load time — see ABI §11.
87
+ std::vector<Entity>* g_entities = nullptr;
88
+
89
+ std::vector<Entity>& entities() {
90
+ if (g_entities == nullptr) {
91
+ g_entities = new std::vector<Entity>();
92
+ }
93
+ return *g_entities;
94
+ }
95
+
96
+ /// Adopt one binary OMM record into an entity. Angles arrive in DEGREES and
97
+ /// mean motion in REV/DAY (ABI §5, OrbProOMMRecord) and are converted once,
98
+ /// here, on the way in — never on the way out.
99
+ ///
100
+ /// TODO: your propagation goes here (part 1 of 2).
101
+ ///
102
+ /// This derives only what the placeholder in `propagate_entity()` needs
103
+ /// (mean motion + epoch, for the `semi_major_axis_m` placeholder below). A
104
+ /// real propagator will typically also want a genuine semi-major axis from
105
+ /// mean motion — `a = (mu / n^2)^(1/3)` under whichever gravity model your
106
+ /// element source assumes (WGS-72 for TLE-derived mean elements) — and it
107
+ /// will use eccentricity and the three orientation angles converted here,
108
+ /// exactly as shown, rather than re-deriving them in `propagate_entity()`.
109
+ bool adopt_omm(const OrbProOMMRecord& record, Entity* out) {
110
+ const double mean_motion_rad_s = record.mean_motion * kTwoPi / kSecondsPerDay;
111
+ if (!(mean_motion_rad_s > 0.0) || !std::isfinite(mean_motion_rad_s)) {
112
+ return false;
113
+ }
114
+ if (!(record.eccentricity >= 0.0) || record.eccentricity >= 1.0) {
115
+ return false;
116
+ }
117
+ if (!std::isfinite(record.epoch_jd)) {
118
+ return false;
119
+ }
120
+
121
+ out->mean_motion_rad_s = mean_motion_rad_s;
122
+ out->epoch_jd = record.epoch_jd;
123
+ out->eccentricity = record.eccentricity;
124
+ out->inclination_rad = record.inclination * kDegToRad;
125
+ out->raan_rad = record.ra_of_asc_node * kDegToRad;
126
+ out->arg_pericenter_rad = record.arg_of_pericenter * kDegToRad;
127
+ out->mean_anomaly_rad = record.mean_anomaly * kDegToRad;
128
+ out->norad_cat_id = record.norad_cat_id;
129
+
130
+ // --- BEGIN TODO: your propagation goes here (semi-major axis) -----------
131
+ // Placeholder: a fixed LEO-ish radius so the module has SOMETHING physical
132
+ // to hold still at. Replace with a = (mu / n^2)^(1/3) or your model's
133
+ // equivalent.
134
+ out->semi_major_axis_m = 7000000.0;
135
+ // --- END TODO -------------------------------------------------------------
136
+ return true;
137
+ }
138
+
139
+ /// Propagate one entity to `julian_date` and write an ABI state vector.
140
+ ///
141
+ /// =============================================================================
142
+ /// TODO: your propagation goes here (part 2 of 2).
143
+ /// =============================================================================
144
+ /// Everything above and below this function's TODO block is ABI plumbing you
145
+ /// should not need to touch. The block below currently holds the entity
146
+ /// motionless at a fixed position — enough to exercise every byte of the
147
+ /// wire contract (frame, units, flags) end to end, but not a real orbit.
148
+ /// Replace it with real orbital mechanics (SGP4, a numerical integrator,
149
+ /// two-body Kepler, whatever your module's family is) and keep writing
150
+ /// through `out` exactly as shown below the TODO block: zero the struct
151
+ /// first, set position/velocity in METERS and METERS/SECOND, set the frame
152
+ /// explicitly, set VALID last.
153
+ int32_t propagate_entity(const Entity& entity, double julian_date, OrbProStateVector* out) {
154
+ if (entity.semi_major_axis_m <= 0.0 || entity.eccentricity < 0.0 ||
155
+ entity.eccentricity >= 1.0) {
156
+ return kErrUnphysical;
157
+ }
158
+
159
+ // --- BEGIN TODO: your propagation goes here ------------------------------
160
+ const double x_m = entity.semi_major_axis_m;
161
+ const double y_m = 0.0;
162
+ const double z_m = 0.0;
163
+ const double vx_m_s = 0.0;
164
+ const double vy_m_s = 0.0;
165
+ const double vz_m_s = 0.0;
166
+ // --- END TODO --------------------------------------------------------------
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // ABI §5/§6/§7/§11 — WRITING THE STATE VECTOR.
170
+ //
171
+ // orbpro_state_init() zeroes the WHOLE struct — including the three
172
+ // reserved bytes at offsets 57..59 the IDL requires to be zero. The host
173
+ // may reuse one scratch buffer across calls, so a partial write leaves the
174
+ // previous call's bytes behind; start from the initializer, always.
175
+ //
176
+ // Units are METERS and METERS/SECOND. There is no km variant.
177
+ // ---------------------------------------------------------------------------
178
+ orbpro_state_init(out);
179
+ out->epoch = julian_date;
180
+ out->position[0] = x_m;
181
+ out->position[1] = y_m;
182
+ out->position[2] = z_m;
183
+ out->velocity[0] = vx_m_s;
184
+ out->velocity[1] = vy_m_s;
185
+ out->velocity[2] = vz_m_s;
186
+ // ABI §7 — FRAMES. Set explicitly, never left implicit, and with the
187
+ // GENERATED setter — not a bare assignment: it clears the three padding
188
+ // bytes a consumer reading offset 56 as a 32-bit word would otherwise see
189
+ // as garbage. Change ORBPRO_FRAME_ECEF if your propagator's native output
190
+ // frame differs — see OrbProReferenceFrame in orbpro_propagator_abi.h for
191
+ // the full vocabulary.
192
+ orbpro_state_set_reference_frame(out, ORBPRO_FRAME_ECEF);
193
+ out->flags |= (uint32_t)ORBPRO_STATE_VALID;
194
+ return kOk;
195
+ }
196
+
197
+ const plugin_input_frame_t* find_frame(const char* port_id) {
198
+ const uint32_t input_count = plugin_get_input_count();
199
+ for (uint32_t index = 0; index < input_count; ++index) {
200
+ const plugin_input_frame_t* frame = plugin_get_input_frame(index);
201
+ if (frame != nullptr && frame->port_id != nullptr &&
202
+ std::strcmp(frame->port_id, port_id) == 0) {
203
+ return frame;
204
+ }
205
+ }
206
+ return nullptr;
207
+ }
208
+
209
+ } // namespace
210
+
211
+ // =============================================================================
212
+ // ABI §4 — THE EXPORTED SURFACE
213
+ //
214
+ // Every export below must exist with exactly this name and signature. None
215
+ // of them are TODOs — they are the contract a host program links against.
216
+ // =============================================================================
217
+
218
+ /// ABI §4.2 — `plugin_init_omm(records, count)`: the typed ingest. REPLACES
219
+ /// the existing element set. Returns the number of entities now held, or a
220
+ /// negative error code.
221
+ ORBPRO_ABI_EXPORT("plugin_init_omm")
222
+ int32_t plugin_init_omm(const OrbProOMMRecord* records, uint32_t count) {
223
+ if (records == nullptr) {
224
+ return kErrBadInput;
225
+ }
226
+ std::vector<Entity>& store = entities();
227
+ store.clear();
228
+ store.reserve(count);
229
+ for (uint32_t index = 0; index < count; ++index) {
230
+ Entity entity{};
231
+ if (!adopt_omm(records[index], &entity)) {
232
+ return kErrBadInput;
233
+ }
234
+ store.push_back(entity);
235
+ }
236
+ return static_cast<int32_t>(store.size());
237
+ }
238
+
239
+ /// ABI §4.1 — `plugin_init(data, len)`. The generic initializer: accepts a
240
+ /// packed array of OrbProOMMRecord and refuses anything that is not a whole
241
+ /// number of records rather than silently truncating.
242
+ ///
243
+ /// Returns the number of entities initialized (>0), or a negative error code.
244
+ ORBPRO_ABI_EXPORT("plugin_init")
245
+ int32_t plugin_init(const uint8_t* data, size_t len) {
246
+ if (data == nullptr || len == 0) {
247
+ return kErrBadInput;
248
+ }
249
+ if (len % sizeof(OrbProOMMRecord) != 0) {
250
+ // A partial trailing record means the caller and this module disagree
251
+ // about the struct size. Refusing is the only safe answer.
252
+ return kErrBadInput;
253
+ }
254
+ const uint32_t count = static_cast<uint32_t>(len / sizeof(OrbProOMMRecord));
255
+ return plugin_init_omm(reinterpret_cast<const OrbProOMMRecord*>(data), count);
256
+ }
257
+
258
+ /// ABI §8 — create-returns-handle. Appends ONE record and RETURNS THE HANDLE
259
+ /// IT ASSIGNED. A caller never has to derive "the entity I just created"
260
+ /// from the count, which is race-unsafe under a threaded host.
261
+ ORBPRO_ABI_EXPORT("plugin_ingest_omm_one")
262
+ int32_t plugin_ingest_omm_one(const OrbProOMMRecord* record) {
263
+ if (record == nullptr) {
264
+ return kErrBadInput;
265
+ }
266
+ Entity entity{};
267
+ if (!adopt_omm(*record, &entity)) {
268
+ return kErrBadInput;
269
+ }
270
+ std::vector<Entity>& store = entities();
271
+ store.push_back(entity);
272
+ return static_cast<int32_t>(store.size() - 1);
273
+ }
274
+
275
+ /// ABI §4.3 — `plugin_propagate(julian_date, entity_index, out)`.
276
+ ORBPRO_ABI_EXPORT("plugin_propagate")
277
+ int32_t plugin_propagate(double julian_date, uint32_t entity_index, OrbProStateVector* out) {
278
+ if (out == nullptr) {
279
+ return kErrNullOutput;
280
+ }
281
+ std::vector<Entity>& store = entities();
282
+ if (store.empty()) {
283
+ return kErrNotInitialized;
284
+ }
285
+ if (entity_index >= store.size()) {
286
+ return kErrBadEntityIndex;
287
+ }
288
+ return propagate_entity(store[entity_index], julian_date, out);
289
+ }
290
+
291
+ /// ABI §4.4 / §9 — `plugin_propagate_batch(julian_date, out, count)`.
292
+ ///
293
+ /// SHARD-WRITE DISCIPLINE. This module declares `threadModel:
294
+ /// "wasi-sequential"` (see plugin-manifest.json's `sequentialJustification`)
295
+ /// — it never spawns a thread of its own. That is the STRONG DEFAULT for a
296
+ /// propagator, not a shortcut: propagation is embarrassingly parallel ACROSS
297
+ /// entities and strictly sequential WITHIN one, and the ABI puts the
298
+ /// sharding decision on the HOST (e.g. a frame-worker pool), never on the
299
+ /// module. A module that spawned its own pool here would contend with the
300
+ /// pool that is already scheduling it. See docs/propagator-abi.md
301
+ /// "Threading".
302
+ ///
303
+ /// Declaring no threading of your own does NOT relax the discipline below —
304
+ /// it is exactly what MAKES this plain loop safe to run under ANY sharding a
305
+ /// host chooses, including calling it concurrently from multiple workers
306
+ /// each with a disjoint `[begin, end)` slice of a larger array:
307
+ ///
308
+ /// - Write ONLY the rows in `[0, count)` you were given here. Never write
309
+ /// outside that range, and never assume you own the whole array — a
310
+ /// host sharding this call gives every worker the SAME base `out`
311
+ /// pointer and a disjoint index range.
312
+ /// - Never READ a neighbour's row. This function's output for row `i`
313
+ /// must depend only on `store[i]` and `julian_date`, never on any other
314
+ /// row or on the order rows are visited in.
315
+ /// - Hold NO state across rows (no running totals, no "last entity"
316
+ /// cache) — each iteration must be independently correct.
317
+ /// - On failure, zero the offending row with `orbpro_state_init()` before
318
+ /// returning, so a host that ignores the return value still reads a
319
+ /// state marked not-valid rather than stale bytes from a previous call.
320
+ /// A partially-written batch with no signal is exactly the
321
+ /// silent-wrong-numbers failure this ABI exists to prevent.
322
+ ///
323
+ /// If your propagator genuinely needs its OWN internal parallelism (rare —
324
+ /// most do not; the host pool is where batch parallelism belongs), declare
325
+ /// `threadModel: "emscripten-pthreads"` instead. Be aware the SDK's
326
+ /// post-link artifact guard validates the EMITTED wasm, not the manifest's
327
+ /// claim: the `-pthread` compiler flag alone does not satisfy it, because a
328
+ /// static link drops the thread-spawn runtime unless your code actually
329
+ /// references it (e.g. spawns a `std::thread`). See
330
+ /// docs/isomorphic-pthreads.md.
331
+ ORBPRO_ABI_EXPORT("plugin_propagate_batch")
332
+ int32_t plugin_propagate_batch(double julian_date, OrbProStateVector* out, uint32_t count) {
333
+ if (out == nullptr) {
334
+ return kErrNullOutput;
335
+ }
336
+ std::vector<Entity>& store = entities();
337
+ if (store.empty()) {
338
+ return kErrNotInitialized;
339
+ }
340
+ if (count > store.size()) {
341
+ return kErrBadEntityIndex;
342
+ }
343
+ for (uint32_t index = 0; index < count; ++index) {
344
+ const int32_t status = propagate_entity(store[index], julian_date, &out[index]);
345
+ if (status != kOk) {
346
+ orbpro_state_init(&out[index]);
347
+ return status;
348
+ }
349
+ }
350
+ return kOk;
351
+ }
352
+
353
+ /// ABI §4.5 — how many entities are currently held.
354
+ ORBPRO_ABI_EXPORT("plugin_entity_count")
355
+ int32_t plugin_entity_count(void) {
356
+ return static_cast<int32_t>(entities().size());
357
+ }
358
+
359
+ /// ABI §11 — `plugin_destroy()`. THIS ONE MUST BE REAL. It releases this
360
+ /// module's storage and returns it to exactly its load-time shape, so N
361
+ /// cycles of ingest/propagate/destroy return to baseline. A `{}` body here
362
+ /// is a leak, not a stub — do not simplify it away.
363
+ ORBPRO_ABI_EXPORT("plugin_destroy")
364
+ void plugin_destroy(void) {
365
+ delete g_entities;
366
+ g_entities = nullptr;
367
+ }
368
+
369
+ // =============================================================================
370
+ // The SDN invoke surface.
371
+ //
372
+ // One method, `ingest_omm`, declared in plugin-manifest.json with a TYPED
373
+ // input port carrying the ratified SDS `$OMM` file identifier. Note what is
374
+ // absent: no `acceptsAnyFlatbuffer` wildcard, and no invented 4-byte type. A
375
+ // bare (non-`$`) identifier is a vendor invention and a harness must refuse
376
+ // it.
377
+ //
378
+ // This scaffold's ingest_omm ONLY accepts the port's `aligned-binary` peer
379
+ // (records already unpacked to the ABI's binary OrbProOMMRecord layout, the
380
+ // same shape `plugin_init` takes) — not the canonical FlatBuffer wire form
381
+ // the manifest also declares. That is a deliberate simplification to keep
382
+ // this file readable as a starting point; a host that only ever sends the
383
+ // canonical FlatBuffer form will get "invalid-omm" from this code as
384
+ // written. If you need to honor both wire formats, decode the FlatBuffer
385
+ // table yourself — the reference propagator this template is derived from
386
+ // (space-data-network/propagator/keplerian-reference) is the worked example
387
+ // of a small, hand-rolled, dependency-free vtable reader for `$OMM`.
388
+ // =============================================================================
389
+
390
+ extern "C" int ingest_omm(void) {
391
+ plugin_reset_output_state();
392
+
393
+ const plugin_input_frame_t* frame = find_frame("omm");
394
+ if (frame == nullptr || frame->payload == nullptr) {
395
+ plugin_set_error("missing-omm", "An 'omm' input frame is required.");
396
+ return 3;
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // TODO(you): decode the CANONICAL $OMM FlatBuffer.
401
+ //
402
+ // The manifest declares this port as ratified SDS `$OMM` in BOTH its
403
+ // canonical FlatBuffer form and its aligned-binary peer, because the
404
+ // validator requires the pair. This scaffold only implements the
405
+ // aligned-binary peer.
406
+ //
407
+ // So it REFUSES the canonical form explicitly, rather than accepting bytes
408
+ // it cannot read. Declaring a typed port and then not honouring it is the
409
+ // defect this harness exists to prevent — a propagator that guesses at
410
+ // unknown bytes produces confident, silently wrong numbers, and no runtime
411
+ // check can catch it.
412
+ //
413
+ // The reference implementation shows the decode:
414
+ // space-data-network-modules/propagator/keplerian-reference/
415
+ // src/keplerian_reference_module.cpp (FlatTable + decode_omm_flatbuffer)
416
+ // build.js (vtable slots derived from the
417
+ // PINNED SDS schema, never hand-typed)
418
+ //
419
+ // Copy it, or drop the canonical entry from your manifest and accept the
420
+ // narrower contract. Do not delete this refusal and hope.
421
+ // ---------------------------------------------------------------------------
422
+ if (frame->payload_length >= 8 && frame->payload[4] == '$' &&
423
+ frame->payload[5] == 'O' && frame->payload[6] == 'M' &&
424
+ frame->payload[7] == 'M') {
425
+ plugin_set_error(
426
+ "unimplemented-omm-flatbuffer",
427
+ "This module declares the canonical $OMM FlatBuffer on its 'omm' port but "
428
+ "does not decode it yet. See the TODO in ingest_omm and the reference "
429
+ "implementation's decode_omm_flatbuffer().");
430
+ return 3;
431
+ }
432
+
433
+ if (frame->payload_length == 0 ||
434
+ frame->payload_length % sizeof(OrbProOMMRecord) != 0) {
435
+ plugin_set_error(
436
+ "invalid-omm",
437
+ "The 'omm' payload must be a whole number of 88-byte OrbProOMMRecord entries.");
438
+ return 3;
439
+ }
440
+
441
+ const uint32_t count =
442
+ static_cast<uint32_t>(frame->payload_length / sizeof(OrbProOMMRecord));
443
+ const int32_t ingested =
444
+ plugin_init_omm(reinterpret_cast<const OrbProOMMRecord*>(frame->payload), count);
445
+ if (ingested < 0) {
446
+ plugin_set_error("unphysical-omm", "One or more OMM records describe no closed orbit.");
447
+ return 3;
448
+ }
449
+ return 0;
450
+ }
@@ -0,0 +1,103 @@
1
+ // __MODULE_NAME__ — scaffolded tests.
2
+ //
3
+ // These check the two things every propagator module must get right before
4
+ // any physics is real: the manifest is well-formed, and the compiled
5
+ // artifact actually exposes the ABI surface a host will link against. Run
6
+ // `npm run build` first — the compliance/export checks SKIP (not fail) if
7
+ // dist/isomorphic/module.wasm has not been built yet, matching how
8
+ // space-data-module-sdk's own first-party modules test themselves.
9
+ import assert from "node:assert/strict";
10
+ import fs from "node:fs";
11
+ import test from "node:test";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const MANIFEST_PATH = new URL("../plugin-manifest.json", import.meta.url);
15
+ const ISOMORPHIC_WASM_PATH = new URL("../dist/isomorphic/module.wasm", import.meta.url);
16
+
17
+ const EXPECTED_ABI_EXPORTS = [
18
+ "plugin_init",
19
+ "plugin_init_omm",
20
+ "plugin_ingest_omm_one",
21
+ "plugin_propagate",
22
+ "plugin_propagate_batch",
23
+ "plugin_entity_count",
24
+ "plugin_destroy",
25
+ "ingest_omm",
26
+ ];
27
+
28
+ function readManifest() {
29
+ return JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
30
+ }
31
+
32
+ function wasmBuilt() {
33
+ return fs.existsSync(fileURLToPath(ISOMORPHIC_WASM_PATH));
34
+ }
35
+
36
+ test("__MODULE_NAME__ manifest declares the shared browser/WasmEdge artifact", () => {
37
+ const manifest = readManifest();
38
+ assert.equal(manifest.pluginId, "__PLUGIN_ID__");
39
+ assert.equal(manifest.pluginFamily, "propagator");
40
+ assert.deepEqual(manifest.runtimeTargets, ["browser", "wasmedge"]);
41
+ assert.equal(manifest.buildArtifacts?.[0]?.path, "dist/isomorphic/module.wasm");
42
+ assert.ok(Array.isArray(manifest.methods) && manifest.methods.length > 0);
43
+ });
44
+
45
+ test("__MODULE_NAME__ declares wasi-sequential with a substantive justification", () => {
46
+ // This module never spawns a thread of its own — see the SHARD-WRITE
47
+ // DISCIPLINE comment on plugin_propagate_batch() in
48
+ // src/__MODULE_NAME_SNAKE__.cpp for why that's the strong default for a
49
+ // propagator, not a shortcut. build.js passes threadModel: manifest.threadModel
50
+ // explicitly (resolveThreadModel does NOT read this field on its own, and
51
+ // "wasmedge" in runtimeTargets otherwise infers the OTHER model) and
52
+ // asserts the compiler agreed — this test guards the manifest side of that
53
+ // same invariant.
54
+ const manifest = readManifest();
55
+ assert.equal(manifest.threadModel, "wasi-sequential");
56
+ assert.equal(typeof manifest.sequentialJustification?.kind, "string");
57
+ assert.ok(manifest.sequentialJustification.kind.length > 0);
58
+ assert.ok(
59
+ (manifest.sequentialJustification.detail ?? "").length >= 40,
60
+ "sequentialJustification.detail must be a substantive explanation, not a placeholder",
61
+ );
62
+ });
63
+
64
+ test("__MODULE_NAME__ artifact passes SDK compliance checks", async (t) => {
65
+ if (!wasmBuilt()) {
66
+ t.skip("dist/isomorphic/module.wasm not built yet — run `npm run build` first.");
67
+ return;
68
+ }
69
+ const { validateArtifactWithStandards } = await import("space-data-module-sdk/compliance");
70
+ const report = await validateArtifactWithStandards({
71
+ manifest: readManifest(),
72
+ wasmPath: fileURLToPath(ISOMORPHIC_WASM_PATH),
73
+ });
74
+ assert.equal(report.ok, true, JSON.stringify(report.issues, null, 2));
75
+ });
76
+
77
+ test("__MODULE_NAME__ artifact exposes the propagator ABI exports", async (t) => {
78
+ if (!wasmBuilt()) {
79
+ t.skip("dist/isomorphic/module.wasm not built yet — run `npm run build` first.");
80
+ return;
81
+ }
82
+ const { inspectModule } = await import("space-data-module-sdk/host/isomorphic");
83
+ const inspection = await inspectModule(fs.readFileSync(fileURLToPath(ISOMORPHIC_WASM_PATH)));
84
+ for (const name of EXPECTED_ABI_EXPORTS) {
85
+ assert.ok(inspection.exports.includes(name), `missing export ${name}`);
86
+ }
87
+ // This module declares wasi-sequential and must not link the wasi-threads
88
+ // contract — its absence here is a POSITIVE assertion, not an oversight,
89
+ // and catches an accidental regression to emscripten-pthreads just as
90
+ // loudly as a missing export would.
91
+ assert.ok(
92
+ !inspection.exports.includes("wasi_thread_start"),
93
+ "wasi_thread_start must be absent for a wasi-sequential artifact",
94
+ );
95
+ });
96
+
97
+ // TODO: your propagation goes here (part 3 of 3) — once propagate_entity()
98
+ // holds real physics, replace this with an assertion on an actual computed
99
+ // state vector (e.g. ingest a known OMM, propagate to its epoch, and check
100
+ // position/velocity against an independent reference).
101
+ test("__MODULE_NAME__ TODO: assert real propagated output once physics lands", (t) => {
102
+ t.skip("placeholder — see propagate_entity() in src/__MODULE_NAME_SNAKE__.cpp");
103
+ });