space-data-module-sdk 0.8.14 → 0.8.16

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 (125) hide show
  1. package/bin/space-data-module.js +91 -0
  2. package/docs/_shell/consumer-assets.html +5 -0
  3. package/docs/browser-wasmedge-isomorphic.html +261 -0
  4. package/docs/byo-wasm-quickstart.html +226 -0
  5. package/docs/byo-wasm-quickstart.md +204 -0
  6. package/docs/conformance.html +156 -0
  7. package/docs/conformance.md +92 -0
  8. package/docs/emception-shared-memory-verdict.md +209 -0
  9. package/docs/events-abi.html +313 -0
  10. package/docs/events-abi.md +530 -0
  11. package/docs/families/analytics.html +178 -0
  12. package/docs/families/analytics.md +104 -0
  13. package/docs/families/attitude.html +157 -0
  14. package/docs/families/attitude.md +36 -0
  15. package/docs/families/behavior.html +157 -0
  16. package/docs/families/behavior.md +36 -0
  17. package/docs/families/breakup.html +157 -0
  18. package/docs/families/breakup.md +36 -0
  19. package/docs/families/conjunction.html +186 -0
  20. package/docs/families/conjunction.md +121 -0
  21. package/docs/families/data-source.html +318 -0
  22. package/docs/families/effects.html +157 -0
  23. package/docs/families/effects.md +36 -0
  24. package/docs/families/environment.html +170 -0
  25. package/docs/families/environment.md +73 -0
  26. package/docs/families/estimation.html +190 -0
  27. package/docs/families/estimation.md +128 -0
  28. package/docs/families/gnc.html +157 -0
  29. package/docs/families/gnc.md +36 -0
  30. package/docs/families/maneuver.html +209 -0
  31. package/docs/families/maneuver.md +206 -0
  32. package/docs/families/obstruction.html +191 -0
  33. package/docs/families/obstruction.md +96 -0
  34. package/docs/families/propagator.html +333 -0
  35. package/docs/families/propulsion.html +157 -0
  36. package/docs/families/propulsion.md +36 -0
  37. package/docs/families/reentry.html +157 -0
  38. package/docs/families/reentry.md +36 -0
  39. package/docs/families/rf.html +193 -0
  40. package/docs/families/rf.md +136 -0
  41. package/docs/families/scheduler.html +157 -0
  42. package/docs/families/scheduler.md +36 -0
  43. package/docs/families/sensor.html +157 -0
  44. package/docs/families/sensor.md +36 -0
  45. package/docs/families/signature.html +157 -0
  46. package/docs/families/signature.md +36 -0
  47. package/docs/flatsql-host-contract.html +176 -0
  48. package/docs/flatsql-streaming-standard.html +375 -0
  49. package/docs/gpu-module-abi.html +233 -0
  50. package/docs/harness-family-matrix.md +135 -0
  51. package/docs/index.html +216 -85
  52. package/docs/isomorphic-pthreads.html +228 -0
  53. package/docs/isomorphic-pthreads.md +84 -0
  54. package/docs/isomorphic-sdn-runtime-plan.html +274 -0
  55. package/docs/language-runtime-matrix.html +182 -0
  56. package/docs/llms.txt +86 -0
  57. package/docs/module-bundle-runtime-plan.html +146 -0
  58. package/docs/module-publication-standard.html +503 -0
  59. package/docs/node-red-default-node-parity.html +201 -0
  60. package/docs/propagator-abi.md +73 -21
  61. package/docs/protect-and-sign.html +181 -0
  62. package/docs/protect-and-sign.md +120 -0
  63. package/docs/protocol-installation.html +296 -0
  64. package/docs/publication-submission.html +158 -0
  65. package/docs/publication-submission.md +78 -0
  66. package/docs/secrets-capability.html +206 -0
  67. package/docs/styles.css +860 -167
  68. package/docs/testing-harness.html +408 -0
  69. package/docs/tri-runtime-parity-gate.html +182 -0
  70. package/docs/tri-runtime-parity.html +203 -0
  71. package/include/orbpro/orbpro_event_runner.h +951 -0
  72. package/include/orbpro/orbpro_events_abi.h +565 -0
  73. package/include/orbpro/orbpro_propagator_abi.h +17 -0
  74. package/package.json +15 -5
  75. package/schemas/orbpro/Events.fbs +501 -0
  76. package/schemas/orbpro/Propagator.fbs +72 -9
  77. package/schemas/orbpro/reference-frame-crosswalk.json +108 -0
  78. package/schemas/orbpro/reference-frame.lock.json +98 -0
  79. package/src/browser.js +13 -0
  80. package/src/conformance/abiDriver.js +225 -0
  81. package/src/conformance/index.js +151 -0
  82. package/src/conformance/propagatorSuite.js +526 -0
  83. package/src/conformance/selfTest.js +275 -0
  84. package/src/conformance/selfTestCorpus.js +68 -0
  85. package/src/conformance/twoBodyReference.js +132 -0
  86. package/src/generated/orbpro/events-abi.js +206 -0
  87. package/src/generated/orbpro/events-abi.ts +319 -0
  88. package/src/generated/orbpro/propagator/reference-frame.js +17 -0
  89. package/src/generated/orbpro/propagator/reference-frame.ts +91 -1
  90. package/src/generated/orbpro/propagator-abi.js +17 -0
  91. package/src/generated/orbpro/propagator-abi.ts +17 -0
  92. package/src/generated/spacedatastandards/plg/CCT.d.ts +94 -0
  93. package/src/generated/spacedatastandards/plg/CCT.d.ts.map +1 -0
  94. package/src/generated/spacedatastandards/plg/CCT.js +208 -0
  95. package/src/generated/spacedatastandards/plg/CCT.ts +281 -0
  96. package/src/generated/spacedatastandards/plg/CCTCategory.d.ts +98 -0
  97. package/src/generated/spacedatastandards/plg/CCTCategory.d.ts.map +1 -0
  98. package/src/generated/spacedatastandards/plg/CCTCategory.js +177 -0
  99. package/src/generated/spacedatastandards/plg/CCTCategory.ts +269 -0
  100. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.d.ts +54 -0
  101. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.d.ts.map +1 -0
  102. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.js +106 -0
  103. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.ts +144 -0
  104. package/src/generated/spacedatastandards/plg/PLG.d.ts +46 -2
  105. package/src/generated/spacedatastandards/plg/PLG.d.ts.map +1 -1
  106. package/src/generated/spacedatastandards/plg/PLG.js +78 -5
  107. package/src/generated/spacedatastandards/plg/PLG.ts +90 -5
  108. package/src/generated/spacedatastandards/plg/capabilityClass.d.ts +181 -0
  109. package/src/generated/spacedatastandards/plg/capabilityClass.d.ts.map +1 -0
  110. package/src/generated/spacedatastandards/plg/capabilityClass.js +183 -0
  111. package/src/generated/spacedatastandards/plg/capabilityClass.ts +208 -0
  112. package/src/generated/spacedatastandards/plg/main.d.ts +4 -0
  113. package/src/generated/spacedatastandards/plg/main.d.ts.map +1 -1
  114. package/src/generated/spacedatastandards/plg/main.js +4 -0
  115. package/src/generated/spacedatastandards/plg/main.ts +4 -0
  116. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +40 -2
  117. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
  118. package/src/generated/spacedatastandards/plg/pluginCategory.js +40 -2
  119. package/src/generated/spacedatastandards/plg/pluginCategory.ts +40 -2
  120. package/src/host/browserModuleHarness.js +8 -0
  121. package/src/host/wasiThreadHost.js +189 -14
  122. package/src/index.d.ts +18 -1
  123. package/src/testing/isomorphicHarness.js +233 -0
  124. package/src/transport/index.js +16 -0
  125. package/src/transport/records.js +1015 -3
@@ -0,0 +1,530 @@
1
+ # Event locator ABI
2
+
3
+ **Status: EXPERIMENTAL.** The schema, the generated header, the drift gate and
4
+ the runner have landed and are measured. There is no conformance kit and no
5
+ reference module yet, so this is not `Shipped` and a commercial module should
6
+ not be sold against it. It is the **event-location sub-harness of the
7
+ [Analytics](families/analytics.html) family** — not a twentieth harness family.
8
+ The nineteen-family taxonomy is owner-ratified and this ABI does not amend it.
9
+
10
+ ## Table of contents
11
+
12
+ - [Doctrine](#doctrine)
13
+ - [Capability](#capability)
14
+ - [The export set](#the-export-set)
15
+ - [The pull protocol](#the-pull-protocol)
16
+ - [Wire layout](#wire-layout)
17
+ - [Units, frames and epochs](#units-frames-and-epochs)
18
+ - [Sentinels](#sentinels)
19
+ - [Identity](#identity)
20
+ - [Threading](#threading)
21
+ - [Error codes](#error-codes)
22
+ - [Interval pairing](#interval-pairing)
23
+ - [Propagate-to-condition](#propagate-to-condition)
24
+ - [Lifetime](#lifetime)
25
+ - [Versioning](#versioning)
26
+ - [Parity envelope](#parity-envelope)
27
+ - [Conformance](#conformance)
28
+ - [Consumer seam](#consumer-seam)
29
+ - [Guest usage](#guest-usage)
30
+ - [Regenerating](#regenerating)
31
+
32
+ ## Doctrine
33
+
34
+ **One ABI, N locators.** Eclipse (umbra / penumbra / antumbra, any number of
35
+ occulting bodies), station contact with masks and light time, sensor-FOV
36
+ intrusion, apsides, node crossings and a propagate-to-condition stop are the
37
+ SAME computation: a vector of scalar event functions `g_i(t)` whose sign
38
+ changes are refined to roots. They differ only in `g`.
39
+
40
+ **The locator supplies `g`. It supplies nothing else.** The bracketing scan,
41
+ the root refinement, the direction filtering, the occurrence counting, the
42
+ endpoint bookkeeping and the epoch ordering live in
43
+ `include/orbpro/orbpro_event_runner.h`, which every locator compiles in
44
+ unchanged. "Adding a new locator requires no change to the runner" is therefore
45
+ a structural fact and not a promise: there is no runner to change, only a `g`
46
+ to write. A locator that needed the runner to change would be evidence that
47
+ this ABI is wrong.
48
+
49
+ **The state source is PULLED, never imported.** A locator needs states at
50
+ epochs only it can choose — Brent picks the next abscissa from the last three.
51
+ It does not import a host function to fetch them. `plugin_event_next` reports
52
+ the epochs it wants, the consumer evaluates them through whatever module is
53
+ wired to the propagator port, and `plugin_event_supply` feeds the states back.
54
+ Three consequences, and they are the reason the design is this way:
55
+
56
+ - The propagator stays a **port**, per the pluggable-propagation law (owner,
57
+ 2026-07-29). No provider is named anywhere in this ABI.
58
+ - **No new host capability and no new import.** The browser, native-WasmEdge
59
+ and Docker-WasmEdge lanes are byte-identical by construction, because the
60
+ consumer shuttles buffers and decides nothing.
61
+ - The refinement stays **inside the guest**. A host-side root finder would be
62
+ physics in JavaScript and its arithmetic would differ per lane.
63
+
64
+ **A hit is a crossing, not an interval.** Apsides and node crossings are
65
+ instants; eclipse and contact are intervals. Emitting crossings and pairing
66
+ them is total over both; an interval-shaped hit would put a sentinel in half
67
+ the family. See [Interval pairing](#interval-pairing).
68
+
69
+ ## Capability
70
+
71
+ **None.** An event locator declares no capability at all: it computes, and
72
+ every byte it consumes arrives through its own exports. This is not an
73
+ oversight to be corrected later — the pull protocol exists precisely so that
74
+ locating an eclipse does not require a module to reach outside itself.
75
+
76
+ A locator that also wants to *emit* an SDS `$EVL` record through the invoke
77
+ surface declares whatever that surface already requires, and nothing new.
78
+
79
+ ## The export set
80
+
81
+ Three exports are the locator's. Seven are the runner's, and
82
+ `ORBPRO_EVENT_RUNNER_EXPORTS(runner)` emits all seven.
83
+
84
+ ### The locator writes these
85
+
86
+ | Export | Signature | Returns |
87
+ | --- | --- | --- |
88
+ | `plugin_event_describe` | `int32_t(uint8_t* out, uint32_t capacity)` | bytes of the `EventLocatorDescription` FlatBuffer written, the required size if `capacity` is short, or a negative [error code](#error-codes) |
89
+ | `plugin_event_configure` | `int32_t(const uint8_t* config, uint32_t len)` | components configured (>0), or negative |
90
+ | `plugin_event_eval` | `int32_t(double epoch_jd_day, double epoch_seconds, const OrbProStateVector* states, uint32_t object_count, double* g_out, uint32_t component_count)` | `0`, or negative |
91
+
92
+ `plugin_event_eval` **must be a pure function of its arguments.** A `g` that
93
+ depends on call order or on a cached previous epoch breaks bracketing: Brent
94
+ evaluates epochs out of chronological order by design. The conformance
95
+ obligation is that `plugin_event_eval` at an epoch and the `g` the runner used
96
+ to find a root at that epoch agree exactly.
97
+
98
+ It is vector-valued on purpose: one state fetch feeds every component, so a
99
+ ten-component locator costs one propagation per epoch rather than ten. That is
100
+ what makes "eclipse against three occulting bodies" and "contact with eight
101
+ stations" ordinary rather than special.
102
+
103
+ ### The runner provides these
104
+
105
+ | Export | Signature | Returns |
106
+ | --- | --- | --- |
107
+ | `plugin_event_begin` | `int32_t(const OrbProEventInterval*, const OrbProRootPolicy*)` | `0`, or negative |
108
+ | `plugin_event_next` | `int32_t(OrbProEventStateRequest*, double* epochs, uint32_t epoch_capacity)` | epochs written (`2 * n` doubles), `0` when finished, or negative |
109
+ | `plugin_event_supply` | `int32_t(const OrbProStateVector*, uint32_t count)` | `0`, or negative |
110
+ | `plugin_event_hit_count` | `int32_t(void)` | hits held |
111
+ | `plugin_event_hits` | `int32_t(OrbProEventHit*, uint32_t capacity)` | hits written, or negative |
112
+ | `plugin_event_summaries` | `int32_t(OrbProEventScanSummary*, uint32_t capacity)` | summaries written, or negative |
113
+ | `plugin_event_destroy` | `void(void)` | — |
114
+
115
+ Plus `memory`, `plugin_alloc` and `plugin_free`, as every family requires.
116
+
117
+ ## The pull protocol
118
+
119
+ ```
120
+ configure ──► begin ──► next ──┬──► (n > 0) consumer propagates ──► supply ──┐
121
+ │ │
122
+ └──► (n == 0) ──► hits / summaries │
123
+ ▲ │
124
+ └─────────────────────────────────────────────┘
125
+ ```
126
+
127
+ - `next` writes `2 * n` doubles: `(jd_day, seconds)` **pairs**, in scan order,
128
+ and fills the `OrbProEventStateRequest` with the object count and the frame
129
+ the states must be in.
130
+ - The consumer evaluates **every** requested epoch for **every** object.
131
+ - `supply` takes `epoch_count * object_count` states, ordered **epoch-major**:
132
+ `states[e * object_count + o]` is object `o` at epoch `e`. A count that is
133
+ not exactly that is `SUPPLY_COUNT_MISMATCH` — never a partial evaluation,
134
+ because a locator that accepted a short supply would report events computed
135
+ from another object's trajectory.
136
+ - The **coarse scan** batches (up to `ORBPRO_EVENT_SCAN_BATCH`, default 64), so
137
+ a consumer drives it through `plugin_propagate_batch`. The **refinement**
138
+ asks for exactly one epoch: it is inherently sequential.
139
+ - Brackets found in a scan batch are refined **before** the scan advances, so
140
+ occurrence counting is chronological and a `max_events` stop lands on the
141
+ right event.
142
+
143
+ Calling `next` twice without an intervening `supply`, or `supply` with nothing
144
+ outstanding, is `PROTOCOL_ORDER`.
145
+
146
+ ## Wire layout
147
+
148
+ Generated header: `include/orbpro/orbpro_events_abi.h`. Source of truth:
149
+ `schemas/orbpro/Events.fbs`. Drift gate: `node scripts/check-events-abi.mjs`.
150
+
151
+ | Struct | Size | Align |
152
+ | --- | ---: | ---: |
153
+ | `OrbProRootPolicy` | 32 | 8 |
154
+ | `OrbProEventInterval` | 56 | 8 |
155
+ | `OrbProEventHit` | 48 | 8 |
156
+ | `OrbProEventScanSummary` | 32 | 8 |
157
+ | `OrbProEventStateRequest` | 16 | 4 |
158
+
159
+ `OrbProStateVector` (64 bytes) and `OrbProReferenceFrame` are **not**
160
+ redeclared here — they come from `orbpro/orbpro_propagator_abi.h`, which the
161
+ generated header includes. A second `ReferenceFrame` typedef is refused by
162
+ `scripts/check-reference-frame-uniqueness.mjs` C1, and it would fail to compile
163
+ the moment both headers met in one translation unit, which they always do.
164
+
165
+ Every struct has a generated `_init` that zeroes the padding, and a generated
166
+ `_set_<field>` for every enum field that clears the padding behind it. Use
167
+ them. A partial write leaves the previous call's bytes in the padding the IDL
168
+ requires to be zero.
169
+
170
+ ## Units, frames and epochs
171
+
172
+ - `scan_step_seconds`, `epoch_tolerance_seconds`, `state_epoch_resolution_seconds`
173
+ and `epoch_seconds`: **SECONDS**.
174
+ - `value`, `goalValue`, `value_tolerance`, `initial_value`, `final_value`: the
175
+ component's own declared `unit` string. The ABI does not convert.
176
+ - `reference_frame`: an `OrbProReferenceFrame` value. A consumer whose
177
+ propagator emits another frame converts through the frames port; a consumer
178
+ that cannot **fails loudly** rather than supplying a differently-framed
179
+ state.
180
+
181
+ ### Epochs are split pairs, and that is not decoration
182
+
183
+ Every epoch here is `(jd_day, seconds)`: an exactly-representable Julian day
184
+ plus an offset in seconds.
185
+
186
+ A Julian date carried in one `float64` resolves to `ulp(2460000.5) = 2^-31 d =`
187
+ **4.02e-5 s** — forty microseconds. A single-double JD therefore *cannot*
188
+ express a stop epoch to 1e-6 s, and a locator that reported one would be
189
+ reporting rounding noise. The split pair resolves 1.5e-11 s. Internally the
190
+ runner works in seconds from the interval start, and never collapses the pair.
191
+
192
+ `epoch_seconds` may exceed 86400 and is **not normalized**. The fine coordinate
193
+ stays fine and two epochs from one scan stay directly comparable.
194
+
195
+ ### The state source bounds the answer, and says so
196
+
197
+ `OrbProStateVector.epoch` is a single `float64` and is **frozen** — 64 bytes,
198
+ on the wire, in the field. So a propagator whose ABI takes a single Julian date
199
+ quantizes `g` at about 4.02e-5 s, and no refinement can resolve a root below
200
+ that no matter what tolerance it is handed.
201
+
202
+ This is declared, not assumed. `EventLocatorConfig.stateEpochResolutionSeconds`
203
+ carries the source's resolution; the runner clamps
204
+ `epoch_tolerance_seconds` up to it and reports
205
+ `ORBPRO_ROOT_STATUS_EPOCH_RESOLUTION_LIMITED` on every root that hit the clamp.
206
+ `orbpro_event_runner_effective_tolerance()` reports the tolerance actually
207
+ used.
208
+
209
+ **Consequence for acceptance criteria.** A "stop epoch reproducible to 1e-6 s"
210
+ claim is only meaningful against a state source that resolves better than
211
+ 1e-6 s. Against a single-double-JD propagator the honest bar is
212
+ `max(requested, source resolution)`, and the receipt must record the
213
+ resolution. Extending the propagator ABI to a split epoch would be a breaking
214
+ change to a frozen 64-byte struct and is an **owner** decision, not an oracle
215
+ call.
216
+
217
+ ## Sentinels
218
+
219
+ | Value | Meaning |
220
+ | --- | --- |
221
+ | `EventInterval.component == 0xFFFFFFFF` | every component |
222
+ | `EventInterval.max_events == 0` | unbounded (still bounded by `max_evaluations` and the caller's buffer). A nonzero cap returns the first N IN SCAN ORDER and sets `truncated`, so "these are the events" and "these are the first N" are distinguishable |
223
+ | `EventInterval.max_evaluations == 0` | unbounded |
224
+ | `EventInterval.occurrence == 0` | report every qualifying crossing |
225
+ | `RootPolicy.value_tolerance == 0` | converge on the bracket width alone |
226
+ | `EventLocatorConfig.stateEpochResolutionSeconds == 0` | not declared; the runner trusts the tolerance it was given |
227
+ | `EventLocatorDescription.maxSafeScanStepSeconds == 0` | the locator declines to bound it — a NAMED GAP, never a pass |
228
+
229
+ `RootPolicy.scan_step_seconds == 0` is **not** a sentinel. It is refused with
230
+ `BAD_INPUT`: a silent default step is how a scan misses every event shorter
231
+ than it, and the miss looks exactly like "there was no event".
232
+
233
+ ### The scan start is not a bracket
234
+
235
+ An event exactly at the interval **start** has no preceding sample to bracket
236
+ it and is therefore not a crossing. One exactly at the interval **end** is.
237
+ The asymmetry is real and deliberate; `EventScanSummary.initial_sign == ZERO`
238
+ is how a consumer learns an event sits on the start.
239
+
240
+ ## Identity
241
+
242
+ A component is a dense 0-based index, stable for the lifetime of one
243
+ configuration, and it is the ONLY handle the ABI structs carry. Names live in
244
+ `EventComponent.name` and resolve through the SDS parameter-catalog record —
245
+ this schema deliberately carries no parameter roster of its own, because a
246
+ second roster is a second source of truth.
247
+
248
+ Objects are positional: object `o` is the `o`-th object the consumer supplies
249
+ states for, in the order `EventLocatorDescription.objectCount` declares.
250
+
251
+ ## Threading
252
+
253
+ Locators compile `clang --target=wasm32-wasip1-threads`, per the
254
+ isomorphic-pthreads law. **Never `emcc -pthread`.**
255
+
256
+ **Declare `wasi-sequential`, with `sequentialJustification.kind =
257
+ "inherently-sequential-algorithm"`.** Root refinement carries state from
258
+ iteration `k-1` into iteration `k` — that is the literal definition of that
259
+ justification kind, and it is also the strongest available guarantee of the
260
+ step-independence and byte-identity properties above: a threaded refinement
261
+ would evaluate abscissae in a nondeterministic order and would not be
262
+ reproducible.
263
+
264
+ A **parameter-catalog** module, which evaluates named quantities from a state
265
+ and has nothing to fan out inside one call, declares `wasi-sequential` with
266
+ `kind = "caller-level-parallelism"` when it exposes a batch export the host
267
+ shards, or `"pure-transform"` when it does not.
268
+
269
+ > **Known defect, still live:** `resolveThreadModel` reads the compile
270
+ > OPTION, not `manifest.threadModel`, and otherwise infers the model from
271
+ > `runtimeTargets`. Pass `threadModel` explicitly to
272
+ > `compileModuleFromSource` and assert `result.threadModel` came back as
273
+ > declared, until `sdk-manifest-threadmodel-silently-ignored` lands.
274
+ > `threadModel: "single-thread"` is the **legacy Emscripten** model and routes
275
+ > to `em++`, not to the sanctioned clang toolchain — it is not a synonym for
276
+ > "does not thread".
277
+
278
+ The runner never allocates, never reads a clock and never spawns. Its state is
279
+ a fixed arena sized by `ORBPRO_EVENT_MAX_COMPONENTS`, `ORBPRO_EVENT_MAX_OBJECTS`,
280
+ `ORBPRO_EVENT_MAX_HITS`, `ORBPRO_EVENT_SCAN_BATCH` and
281
+ `ORBPRO_EVENT_MAX_PENDING`, each overridable by the locator before the include.
282
+
283
+ ## Error codes
284
+
285
+ Every failure returns its OWN code. A locator that answers `-1` for everything
286
+ is unconformable: the consumer cannot place the failure on the degradation
287
+ ladder, so it cannot decide between retrying, widening the interval and
288
+ refusing. Codes `-1..-6` are deliberately the same numbers and meanings as the
289
+ propagator family's; the events-specific codes start at `-20` so no consumer
290
+ can confuse the two tables by value.
291
+
292
+ | Code | Name | When |
293
+ | ---: | --- | --- |
294
+ | `-1` | `NOT_CONFIGURED` | `plugin_event_configure` has not been called, or it failed |
295
+ | `-2` | `BAD_OBJECT_INDEX` | an object index outside `[0, object_count)` |
296
+ | `-3` | `NULL_OUTPUT` | a required output pointer was null |
297
+ | `-4` | `BAD_INPUT` | malformed policy or interval (zero step, zero span, NaN tolerance, zero `max_iterations`) |
298
+ | `-5` | `NOT_CONVERGED` | strict convergence was demanded and not reached |
299
+ | `-6` | `UNPHYSICAL` | geometry outside the locator's domain |
300
+ | `-20` | `NOT_STARTED` | `plugin_event_begin` has not been called |
301
+ | `-21` | `UNKNOWN_COMPONENT` | a component, parameter or object this locator does not implement |
302
+ | `-22` | `BUFFER_TOO_SMALL` | a caller buffer was short; nothing is truncated silently |
303
+ | `-23` | `PROTOCOL_ORDER` | `next`/`supply` out of order |
304
+ | `-24` | `SUPPLY_COUNT_MISMATCH` | `count != epoch_count * object_count` |
305
+ | `-25` | `INTERNAL` | never returned by a conformant locator |
306
+
307
+ Non-convergence of one root is normally reported per hit in
308
+ `EventHit.status`, not as a call failure. The statuses are `CONVERGED`,
309
+ `MAX_ITERATIONS`, `FLAT_BRACKET`, `DISCONTINUOUS`, `TRUNCATED` and
310
+ `EPOCH_RESOLUTION_LIMITED`, and they are distinct because a root that stopped
311
+ on the iteration cap and a root that converged are different answers.
312
+
313
+ ## Interval pairing
314
+
315
+ Hits are crossings. An interval is a `FALLING` hit followed by the next
316
+ `RISING` hit on the same component (for the usual "`g > 0` means outside"
317
+ convention). The pairing is total because `EventScanSummary` carries the
318
+ endpoint signs:
319
+
320
+ | `initial_sign` | `final_sign` | Reading |
321
+ | --- | --- | --- |
322
+ | `POSITIVE` | `POSITIVE` | intervals fully inside the scan; pair `FALLING`→`RISING` |
323
+ | `NEGATIVE` | … | the first interval was **already open** at the scan start; its opening epoch is before the interval, and the first `RISING` hit closes it (`INTERVAL_OPEN_AT_START`) |
324
+ | … | `NEGATIVE` | the last interval is still open at the scan end (`INTERVAL_OPEN_AT_END`) |
325
+
326
+ Without the endpoint signs, "no hits" is ambiguous between "never in eclipse"
327
+ and "in eclipse the entire time", and those are opposite answers. This is the
328
+ classic event-scan defect and the summary struct exists to make it
329
+ unrepresentable.
330
+
331
+ Hits are reported in **scan order** — increasing epoch forward, decreasing
332
+ backward — regardless of the order refinement completed them.
333
+
334
+ ## Propagate-to-condition
335
+
336
+ A GMAT-style `Propagate ... {Sat.Altitude = 400}` is this ABI with one
337
+ component whose `isStopCondition` is true and whose `g = parameter - goalValue`.
338
+ Nothing is added to the propagator ABI, and that is an engineering decision,
339
+ not a scoping convenience:
340
+
341
+ 1. **It is the same algorithm.** A stop condition is
342
+ `(event function, root refinement)`. Putting a second copy of Brent behind
343
+ `plugin_propagate` would be two implementations of one algorithm that must
344
+ agree — exactly the drift the generated-ABI lane exists to end.
345
+ 2. **It would break every shipped propagator.** The propagator export set is a
346
+ per-epoch state evaluation. Adding stop conditions to it makes root
347
+ refinement a family-wide obligation on `sgp4`, `hpop` and
348
+ `keplerian-reference`, all in the field. That is a breaking ABI change and
349
+ an owner decision.
350
+ 3. **A propagator is a state SOURCE.** It has no opinion about what is
351
+ interesting, and giving it one couples every propagator to the parameter
352
+ catalog.
353
+
354
+ **Backward propagation is not a mode.** It is `stop` earlier than `start`. The
355
+ runner steps negatively, the consumer's propagator is asked for earlier epochs,
356
+ and every hit carries `BACKWARD`. There is no second code path to keep in
357
+ agreement with the first.
358
+
359
+ **Synchronized multi-spacecraft (`Formation`) stops** are `object_count > 1`.
360
+ Every configured object is evaluated at **every** requested epoch, so the
361
+ states are aligned by construction rather than by the consumer remembering to
362
+ align them.
363
+
364
+ ## Lifetime
365
+
366
+ `plugin_event_destroy` is **required** and must actually release: it resets the
367
+ runner to its post-`init` state while keeping the `eval` wiring, so a second
368
+ `configure`/`begin` cycle starts clean. It is idempotent, and after it
369
+ `plugin_event_hit_count()` is `0`.
370
+
371
+ Because the runner's arena is static and fixed, a locator's page count after N
372
+ scan cycles must equal its page count after one. That is the leak invariant,
373
+ and it is free here by construction.
374
+
375
+ ## Versioning
376
+
377
+ Additive-only within a MAJOR. New enum members are **appended**; existing
378
+ values are never renumbered. New struct fields are appended only where they do
379
+ not move an existing offset — in practice that means a new struct, because
380
+ every struct in this ABI is packed to its alignment. The size and offset locks
381
+ in the generated header are what make a violation a compile error rather than a
382
+ silently-wrong-numbers defect.
383
+
384
+ ## Parity envelope
385
+
386
+ Byte-identical across browser, native WasmEdge and Docker WasmEdge, for
387
+ identical inputs:
388
+
389
+ - the hit list: every `epoch_jd_day`, `epoch_seconds`, `value`, `component`,
390
+ `direction`, `status`, `iterations`, `evaluations` and `flags`;
391
+ - the summaries: every endpoint value, sign and count;
392
+ - the epoch sequence `plugin_event_next` produces, and the evaluation count;
393
+ - the trap class of every refusal.
394
+
395
+ This is not a hope. The runner's arithmetic is IEEE-754 `+ - * /` and
396
+ comparison, which WebAssembly specifies exactly and which has no
397
+ fused-multiply-add form in the MVP. There is nothing for the lanes to disagree
398
+ about, and a divergence is a **P1 SDK defect**.
399
+
400
+ **A native reference build must use `-ffp-contract=off`.** On arm64, clang
401
+ contracts `a*b + c` into a single fused multiply-add; wasm has no `fma`, so the
402
+ same source computes a different (both correct) double. Measured on this ABI:
403
+ the Illinois regula-falsi step diverged by 1 ulp in the root epoch, which is
404
+ 2.6e-10 s — inside every tolerance, and still a byte difference. A native build
405
+ that leaves contraction on is not comparing the same arithmetic.
406
+
407
+ ### Measured, on this candidate
408
+
409
+ `test/events-abi.test.js` compiles `test/fixtures/event-runner-harness.c`
410
+ twice — natively and to `wasm32-wasip1-threads` — and diffs the raw IEEE-754
411
+ bit patterns of every reported number. Result: **identical**.
412
+
413
+ Against the closed form (`g0 = z`, roots at `kT/2`; `g1 = x`, roots at
414
+ `T/4 + kT/2`), worst `|root - analytic|` over two orbits:
415
+
416
+ | Scan | Method / step | Worst residual | g evaluations |
417
+ | --- | --- | ---: | ---: |
418
+ | A | Brent, 60 s | **7.3e-12 s** | 227 |
419
+ | B | Brent, 137 s | **5.5e-12 s** | 118 |
420
+ | C | Brent, 300 s | **2.4e-11 s** | 70 |
421
+ | D | Brent, 60 s, BACKWARD over the same arc | **4.5e-12 s** | 226 |
422
+ | E | Bisection, 60 s | **8.2e-10 s** | 484 |
423
+ | F | Illinois, 60 s | **3.3e-10 s** | 388 |
424
+ | G | Brent, source resolution 4.0233e-5 s | **2.1e-7 s** | 111 |
425
+
426
+ All eight events, in the same order, at all three scan steps; the backward scan
427
+ returns the same eight; the three methods agree to better than 1e-6 s while
428
+ costing 227 / 484 / 388 evaluations. Scan G is the clamp working: it reports
429
+ `EPOCH_RESOLUTION_LIMITED` and its residual is bounded by the source resolution
430
+ it was told about, not by the 1e-9 s it was asked for.
431
+
432
+ **The ordering defect this measurement caught.** Hits were originally reported
433
+ in *discovery* order, which is chronological per component but interleaved
434
+ across components. With a 60 s step two components' events fell in separate
435
+ scan samples and came out in epoch order; with a 137 s step they fell in the
436
+ same sample and came out in component order — the same eight events, two of
437
+ them transposed by half an orbit. Step-independence is a property of the LIST,
438
+ not only of each epoch, so the runner now inserts in scan order.
439
+
440
+ ## Conformance
441
+
442
+ There is **no events conformance kit yet**, and `space-data-module conformance`
443
+ refuses the family by name rather than coercing it — a family with no kit can
444
+ never be `CORE`. What exists today and is wired into `npm test`:
445
+
446
+ ```
447
+ node scripts/check-events-abi.mjs # the drift gate
448
+ node --test test/events-abi.test.js # the both-harness proof
449
+ ```
450
+
451
+ The kit that would make this family `Shipped` owes: Tier 0 (the export set),
452
+ Tier C (`plugin_event_eval` agrees exactly with the `g` the runner used;
453
+ determinism as bytes across destroy/reconfigure; typed refusals;
454
+ step-independence; forward-backward closure; root convergence on a
455
+ known-analytic event), Tier 4 (lifecycle), and a self-test with planted
456
+ defects — including a locator whose `g` is impure, which is the failure mode
457
+ this ABI is most exposed to.
458
+
459
+ ## Consumer seam
460
+
461
+ Per the pluggable-propagation law, the consumer resolves the propagator ONCE
462
+ and feeds it to the locator's supply loop. The locator never learns which
463
+ propagator it was. Concretely:
464
+
465
+ ```js
466
+ const request = new OrbProEventStateRequest(); // from events-abi.js offsets
467
+ for (;;) {
468
+ const n = wasm.plugin_event_next(requestPtr, epochsPtr, capacity);
469
+ if (n < 0) throw new Error(eventErrorName(n));
470
+ if (n === 0) break;
471
+ for (let e = 0; e < n; e += 1) {
472
+ const jd = epochs[2 * e] + epochs[2 * e + 1] / 86400.0;
473
+ for (let o = 0; o < objectCount; o += 1) {
474
+ propagator.propagate(jd, o, statesPtr + (e * objectCount + o) * 64);
475
+ }
476
+ }
477
+ const rc = wasm.plugin_event_supply(statesPtr, n * objectCount);
478
+ if (rc < 0) throw new Error(eventErrorName(rc));
479
+ }
480
+ ```
481
+
482
+ That loop is the whole host obligation. It contains no physics, no tolerance,
483
+ no decision about which epochs matter, and it is identical in all three
484
+ runtimes.
485
+
486
+ ## Guest usage
487
+
488
+ ```c
489
+ #include "orbpro/orbpro_event_runner.h" /* pulls in the generated ABI */
490
+
491
+ static OrbProEventRunner g_runner;
492
+
493
+ /* THE locator. Everything else is the runner. */
494
+ static int32_t eclipse_g(double jd_day, double seconds,
495
+ const OrbProStateVector* states, uint32_t objects,
496
+ double* g, uint32_t components, void* user) {
497
+ (void)jd_day; (void)seconds; (void)user;
498
+ if (objects < 1u || components < 1u) return ORBPRO_EVENT_E_BAD_INPUT;
499
+ /* g > 0 outside the shadow, g < 0 inside. A FALLING crossing is entry. */
500
+ g[0] = shadow_function(&states[0]);
501
+ return 0;
502
+ }
503
+
504
+ ORBPRO_EVENT_EXPORT("plugin_event_eval")
505
+ int32_t plugin_event_eval(double jd_day, double seconds,
506
+ const OrbProStateVector* states, uint32_t objects,
507
+ double* g, uint32_t components) {
508
+ return eclipse_g(jd_day, seconds, states, objects, g, components, NULL);
509
+ }
510
+
511
+ /* plugin_event_begin / next / supply / hit_count / hits / summaries / destroy */
512
+ ORBPRO_EVENT_RUNNER_EXPORTS(g_runner)
513
+ ```
514
+
515
+ `plugin_event_describe` and `plugin_event_configure` are the locator's too:
516
+ they encode and decode the `EventLocatorDescription` / `EventLocatorConfig`
517
+ FlatBuffers and end in `orbpro_event_runner_configure` plus one
518
+ `orbpro_event_runner_set_component` per component.
519
+
520
+ ## Regenerating
521
+
522
+ ```
523
+ node scripts/generate-events-abi.mjs # regenerate the header + bindings
524
+ node scripts/check-events-abi.mjs # the drift gate, wired into npm test
525
+ ```
526
+
527
+ Edit `schemas/orbpro/Events.fbs` — it is the single source of truth. A hand
528
+ edit to the generated header is erased by the next run and failed by the gate
529
+ in between. `include/orbpro/orbpro_event_runner.h` is **not** generated: it is
530
+ algorithm, not layout, and there is exactly one copy of it.