space-data-module-sdk 0.8.12 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/bin/space-data-module.js +52 -0
- package/docs/flatsql-host-contract.md +34 -1
- 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 +9 -3
- 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
|
@@ -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
|
+
});
|