space-data-module-sdk 0.8.6 → 0.8.8

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 (51) hide show
  1. package/README.md +13 -0
  2. package/docs/AGENTS.md +5 -0
  3. package/docs/browser-wasmedge-isomorphic.md +65 -1
  4. package/docs/language-runtime-matrix.md +2 -2
  5. package/docs/provider-access-abi.md +600 -0
  6. package/package.json +15 -6
  7. package/schemas/orbpro/Propagator.fbs +19 -3
  8. package/src/browser.js +14 -4
  9. package/src/bundle/artifactBytes.js +44 -0
  10. package/src/bundle/index.js +1 -0
  11. package/src/compiler/compileModule.js +24 -1
  12. package/src/flow/flowCompiler.js +141 -2
  13. package/src/flow/flowRuntimeHost.js +7 -1
  14. package/src/flow/isomorphicFlowHost.js +1 -1
  15. package/src/flow/vendor/sdn-flow/MethodRegistry.js +6 -1
  16. package/src/generated/orbpro/propagator/propagator-source-description.js +2 -2
  17. package/src/generated/orbpro/propagator/propagator-source-description.ts +2 -2
  18. package/src/generated/orbpro/propagator/propagator-source-kind.js +13 -2
  19. package/src/generated/orbpro/propagator/propagator-source-kind.ts +13 -2
  20. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +18 -1
  21. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
  22. package/src/generated/spacedatastandards/plg/pluginCategory.js +17 -0
  23. package/src/generated/spacedatastandards/plg/pluginCategory.ts +21 -1
  24. package/src/{testing → host}/browserModuleHarness.js +69 -34
  25. package/src/host/index.js +6 -0
  26. package/src/host/isomorphicLoader.js +17 -45
  27. package/src/host/isomorphicLoaderBrowser.js +47 -0
  28. package/src/host/isomorphicLoaderCore.js +58 -0
  29. package/src/host/nodeBuiltinSpecifier.js +43 -0
  30. package/src/host/providerAccess.js +727 -0
  31. package/src/host/providerAccessAbi.js +403 -0
  32. package/src/host/providerAccessEngineAdapter.js +338 -0
  33. package/src/host/providerAccessFixtureAdapter.js +444 -0
  34. package/src/host/providerAccessTileStoreAdapter.js +366 -0
  35. package/src/host/sabHostcallChannel.js +1 -1
  36. package/src/host/terrainSourceSeam.js +205 -0
  37. package/src/host/wasiThreadHost.js +11 -1
  38. package/src/{testing → host}/workerModuleHarness.js +45 -12
  39. package/src/{testing → host}/workerModuleHarnessWorker.js +6 -5
  40. package/src/index.d.ts +29 -2
  41. package/src/standards/browser.js +95 -0
  42. package/src/standards/catalogCore.js +290 -0
  43. package/src/standards/index.js +27 -251
  44. package/src/testing/AGENTS.md +25 -2
  45. package/src/testing/browser.js +32 -0
  46. package/src/testing/index.d.ts +12 -1
  47. package/src/testing/index.js +3 -3
  48. package/src/testing/parityBrowserRunner.js +9 -2
  49. package/src/testing/parityHarness.js +1 -1
  50. package/templates/provider-access-module/include/space_data_provider_abi.h +227 -0
  51. /package/src/{testing → host}/moduleFlatbufferStreamPump.js +0 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Deterministic fixture providers — the parity instrument.
3
+ *
4
+ * Live network providers cannot be byte-parity-tested: two runtimes reading a
5
+ * world terrain service are not obliged to hold the same tiles at the same
6
+ * instant, and asserting so would be false rigor. These fixtures are the
7
+ * source that IS inside the byte envelope, bound identically in the browser,
8
+ * in native WasmEdge and in the Docker WasmEdge container.
9
+ *
10
+ * Every value is produced with IEEE-754 basic operations only: integer
11
+ * arithmetic, +, -, *, and division by powers of two. No transcendental is
12
+ * used anywhere. `Math.sin` and friends are NOT bit-reproducible across
13
+ * engines, and a fixture whose bytes depend on a libm implementation would be
14
+ * testing the libm, not the ABI.
15
+ */
16
+
17
+ import {
18
+ PROVIDER_NO_DATA_F64,
19
+ ProviderCost,
20
+ ProviderEncoding,
21
+ ProviderKind,
22
+ normalizeRequestPositions,
23
+ resolveRequestLevel,
24
+ } from "./providerAccessAbi.js";
25
+
26
+ const TWO_PI = 6.283185307179586;
27
+ const PI_OVER_TWO = 1.5707963267948966;
28
+
29
+ /** FNV-1a over a fixed integer tuple. Exact, order-dependent, no floats. */
30
+ function hashInts(...values) {
31
+ let hash = 0x811c9dc5;
32
+ for (const value of values) {
33
+ let word = value >>> 0;
34
+ for (let byte = 0; byte < 4; byte += 1) {
35
+ hash ^= word & 0xff;
36
+ hash = Math.imul(hash, 0x01000193) >>> 0;
37
+ word >>>= 8;
38
+ }
39
+ }
40
+ return hash >>> 0;
41
+ }
42
+
43
+ /** Exact: a u32 divided by 2^32 is representable with no rounding. */
44
+ function unitOf(hash) {
45
+ return hash / 4294967296;
46
+ }
47
+
48
+ /**
49
+ * Closed-form height field, metres. Deterministic in (level, x, y, i, j) alone
50
+ * — never in wall-clock, iteration order, or thread count, which is what makes
51
+ * "byte-identical at ANY thread count" testable rather than hopeful.
52
+ *
53
+ * A coarse ridge term plus a fine detail term, so a profile across it has real
54
+ * ridges to occlude a radio path.
55
+ */
56
+ export function fixtureHeight(level, tileX, tileY, i, j) {
57
+ const ridge = unitOf(hashInts(level, tileX, tileY, i >>> 3, j >>> 3));
58
+ const detail = unitOf(hashInts(level ^ 0x5a5a, tileX, tileY, i, j));
59
+ // 0..8500 m from the ridge term, 0..250 m of detail, floor at -420 m.
60
+ return ridge * 8500 - 420 + detail * 250;
61
+ }
62
+
63
+ /** Closed-form RGBA8 pixel field. */
64
+ export function fixturePixel(level, tileX, tileY, i, j) {
65
+ const hash = hashInts(level, tileX, tileY, i, j);
66
+ return [
67
+ hash & 0xff,
68
+ (hash >>> 8) & 0xff,
69
+ (hash >>> 16) & 0xff,
70
+ 255,
71
+ ];
72
+ }
73
+
74
+ function tileRectangle(level, x, y) {
75
+ const tiles = 1 << level;
76
+ const width = TWO_PI / (tiles * 2);
77
+ const height = Math.PI / tiles;
78
+ const west = -Math.PI + x * width;
79
+ const north = PI_OVER_TWO - y * height;
80
+ return { west, south: north - height, east: west + width, north };
81
+ }
82
+
83
+ /**
84
+ * Level resolution goes through the SHARED helper so the fixture and any host
85
+ * tile store pick the same level for the same request. Divergence here would
86
+ * be invisible in a diff and fatal in a parity run.
87
+ */
88
+ function resolveLevelInfo(request, options) {
89
+ return resolveRequestLevel(request, options);
90
+ }
91
+
92
+ function resolveLevel(request, fallback, options = {}) {
93
+ return resolveLevelInfo(request, { defaultLevel: fallback, ...options });
94
+ }
95
+
96
+ /**
97
+ * Great-circle-shaped position interpolation, done HOST-side so that two
98
+ * runtimes sampling the same start/end agree byte-for-byte. Linear in radians:
99
+ * exact under IEEE-754 basic ops, and the fixture's job is reproducibility,
100
+ * not geodesy.
101
+ */
102
+ function interpolatePositions(request) {
103
+ const explicit = normalizeRequestPositions(request);
104
+ if (explicit) return explicit;
105
+ const [lon0, lat0] = request.start ?? [0, 0];
106
+ const [lon1, lat1] = request.end ?? [0, 0];
107
+ const samples = Math.max(2, Math.min(request.samples | 0 || 2, 1 << 20));
108
+ const positions = new Array(samples);
109
+ const last = samples - 1;
110
+ for (let index = 0; index < samples; index += 1) {
111
+ const t = index / last;
112
+ positions[index] = [
113
+ lon0 + (lon1 - lon0) * t,
114
+ lat0 + (lat1 - lat0) * t,
115
+ ];
116
+ }
117
+ return positions;
118
+ }
119
+
120
+ function positionToTileSample(lon, lat, level, tileWidth, tileHeight) {
121
+ const tiles = 1 << level;
122
+ const u = (lon + Math.PI) / TWO_PI;
123
+ const v = (PI_OVER_TWO - lat) / Math.PI;
124
+ const gx = u * tiles * 2;
125
+ const gy = v * tiles;
126
+ const tileX = Math.min(Math.max(Math.floor(gx), 0), tiles * 2 - 1);
127
+ const tileY = Math.min(Math.max(Math.floor(gy), 0), tiles - 1);
128
+ const i = Math.min(Math.max(Math.floor((gx - tileX) * tileWidth), 0), tileWidth - 1);
129
+ const j = Math.min(Math.max(Math.floor((gy - tileY) * tileHeight), 0), tileHeight - 1);
130
+ return { tileX, tileY, i, j };
131
+ }
132
+
133
+ export function createFixtureTerrainProvider(options = {}) {
134
+ const id = options.id ?? "terrain.fixture";
135
+ const tileWidth = options.tileWidth ?? 65;
136
+ const tileHeight = options.tileHeight ?? 65;
137
+ const maxLevel = options.maxLevel ?? 12;
138
+ const defaultLevel = options.defaultLevel ?? 9;
139
+ // A hole so FLAG_PARTIAL and the no-data sentinel are exercised, not assumed.
140
+ const voidTile = options.voidTile ?? { level: 9, x: 3, y: 3 };
141
+
142
+ function isVoid(level, x, y) {
143
+ return (
144
+ voidTile &&
145
+ level === voidTile.level &&
146
+ x === voidTile.x &&
147
+ y === voidTile.y
148
+ );
149
+ }
150
+
151
+ return {
152
+ id,
153
+ kind: "terrain",
154
+ name: options.name ?? "Deterministic fixture terrain",
155
+ ready: true,
156
+ minLevel: 0,
157
+ maxLevel,
158
+ tileWidth,
159
+ tileHeight,
160
+ encoding: ProviderEncoding.HEIGHT_F32,
161
+ costClass: ProviderCost.RESIDENT,
162
+ credit: "fixture",
163
+ fixture: true,
164
+
165
+ availability({ level, x, y }) {
166
+ return level <= maxLevel && !isVoid(level, x, y);
167
+ },
168
+
169
+ prefetch({ level }) {
170
+ // A tile store has no camera: it can genuinely prefetch.
171
+ return { requested: 1 << Math.min(level ?? 0, 8), pending: 0 };
172
+ },
173
+
174
+ awaitReady() {
175
+ return { ready: true, pending: 0 };
176
+ },
177
+
178
+ acquireTile(request) {
179
+ const { level, strategyCode } = resolveLevel(request, defaultLevel);
180
+ const x = request.x | 0;
181
+ const y = request.y | 0;
182
+ if (isVoid(level, x, y)) {
183
+ const error = new Error(`Fixture tile ${level}/${x}/${y} has no data.`);
184
+ error.code = -5; // NOT_AVAILABLE
185
+ throw error;
186
+ }
187
+ const heights = new Float32Array(tileWidth * tileHeight);
188
+ let min = Infinity;
189
+ let max = -Infinity;
190
+ for (let j = 0; j < tileHeight; j += 1) {
191
+ for (let i = 0; i < tileWidth; i += 1) {
192
+ const height = fixtureHeight(level, x, y, i, j);
193
+ heights[j * tileWidth + i] = height;
194
+ if (height < min) min = height;
195
+ if (height > max) max = height;
196
+ }
197
+ }
198
+ return {
199
+ planes: [heights],
200
+ descriptor: {
201
+ encoding: ProviderEncoding.HEIGHT_F32,
202
+ width: tileWidth,
203
+ height: tileHeight,
204
+ level,
205
+ tileX: x,
206
+ tileY: y,
207
+ minValue: min,
208
+ maxValue: max,
209
+ costClass: ProviderCost.RESIDENT,
210
+ strategy: strategyCode,
211
+ ...tileRectangle(level, x, y),
212
+ },
213
+ };
214
+ },
215
+
216
+ acquireProfile(request) {
217
+ const { level, strategyCode } = resolveLevel(request, defaultLevel, {
218
+ tileWidth,
219
+ maxLevel,
220
+ mostDetailedLevel: maxLevel,
221
+ });
222
+ const positions = interpolatePositions(request);
223
+ const heights = new Float64Array(positions.length);
224
+ let min = Infinity;
225
+ let max = -Infinity;
226
+ let partial = false;
227
+ for (let index = 0; index < positions.length; index += 1) {
228
+ const [lon, lat] = positions[index];
229
+ const sample = positionToTileSample(lon, lat, level, tileWidth, tileHeight);
230
+ if (isVoid(level, sample.tileX, sample.tileY)) {
231
+ heights[index] = PROVIDER_NO_DATA_F64;
232
+ partial = true;
233
+ continue;
234
+ }
235
+ // Round through f32 EXACTLY as the source tile stores it. A profile is
236
+ // a sampling OF the source, not a re-derivation at higher precision:
237
+ // without this, reading a tile and interpolating it yourself gives
238
+ // different numbers than asking for a profile through the same point,
239
+ // and any host whose tiles really are f32 diverges from this fixture.
240
+ const height = Math.fround(
241
+ fixtureHeight(level, sample.tileX, sample.tileY, sample.i, sample.j),
242
+ );
243
+ heights[index] = height;
244
+ if (height < min) min = height;
245
+ if (height > max) max = height;
246
+ }
247
+ const first = positions[0];
248
+ const last = positions[positions.length - 1];
249
+ return {
250
+ planes: [heights],
251
+ descriptor: {
252
+ encoding: ProviderEncoding.HEIGHT_F64,
253
+ width: positions.length,
254
+ height: 1,
255
+ level,
256
+ minValue: Number.isFinite(min) ? min : 0,
257
+ maxValue: Number.isFinite(max) ? max : 0,
258
+ flags: partial ? 1 << 2 : 0,
259
+ west: Math.min(first[0], last[0]),
260
+ east: Math.max(first[0], last[0]),
261
+ south: Math.min(first[1], last[1]),
262
+ north: Math.max(first[1], last[1]),
263
+ costClass: ProviderCost.RESIDENT,
264
+ strategy: strategyCode,
265
+ },
266
+ };
267
+ },
268
+
269
+ acquireRegion(request) {
270
+ const { level, strategyCode } = resolveLevel(request, defaultLevel, {
271
+ tileWidth,
272
+ maxLevel,
273
+ mostDetailedLevel: maxLevel,
274
+ });
275
+ const [west, south, east, north] = request.rectangle ?? [0, 0, 0, 0];
276
+ const width = Math.max(1, request.width | 0);
277
+ const height = Math.max(1, request.height | 0);
278
+ const heights = new Float32Array(width * height);
279
+ let min = Infinity;
280
+ let max = -Infinity;
281
+ for (let row = 0; row < height; row += 1) {
282
+ const lat = north + ((south - north) * row) / height;
283
+ for (let column = 0; column < width; column += 1) {
284
+ const lon = west + ((east - west) * column) / width;
285
+ const sample = positionToTileSample(lon, lat, level, tileWidth, tileHeight);
286
+ const value = fixtureHeight(
287
+ level,
288
+ sample.tileX,
289
+ sample.tileY,
290
+ sample.i,
291
+ sample.j,
292
+ );
293
+ heights[row * width + column] = value;
294
+ if (value < min) min = value;
295
+ if (value > max) max = value;
296
+ }
297
+ }
298
+ return {
299
+ planes: [heights],
300
+ descriptor: {
301
+ encoding: ProviderEncoding.HEIGHT_F32,
302
+ width,
303
+ height,
304
+ level,
305
+ minValue: min,
306
+ maxValue: max,
307
+ west,
308
+ south,
309
+ east,
310
+ north,
311
+ costClass: ProviderCost.RESIDENT,
312
+ strategy: strategyCode,
313
+ },
314
+ };
315
+ },
316
+ };
317
+ }
318
+
319
+ export function createFixtureImageryProvider(options = {}) {
320
+ const id = options.id ?? "imagery.fixture";
321
+ const tileWidth = options.tileWidth ?? 64;
322
+ const tileHeight = options.tileHeight ?? 64;
323
+ const maxLevel = options.maxLevel ?? 12;
324
+ const defaultLevel = options.defaultLevel ?? 9;
325
+
326
+ function renderTile(level, x, y, width, height) {
327
+ const pixels = new Uint8Array(width * height * 4);
328
+ for (let j = 0; j < height; j += 1) {
329
+ for (let i = 0; i < width; i += 1) {
330
+ const [r, g, b, a] = fixturePixel(level, x, y, i, j);
331
+ const offset = (j * width + i) * 4;
332
+ pixels[offset] = r;
333
+ pixels[offset + 1] = g;
334
+ pixels[offset + 2] = b;
335
+ pixels[offset + 3] = a;
336
+ }
337
+ }
338
+ return pixels;
339
+ }
340
+
341
+ return {
342
+ id,
343
+ kind: "imagery",
344
+ name: options.name ?? "Deterministic fixture imagery",
345
+ ready: true,
346
+ minLevel: 0,
347
+ maxLevel,
348
+ tileWidth,
349
+ tileHeight,
350
+ encoding: ProviderEncoding.RGBA8,
351
+ costClass: ProviderCost.RESIDENT,
352
+ credit: "fixture",
353
+ fixture: true,
354
+ // A fixture imagery source keeps its pixels resident, which a real browser
355
+ // imagery layer does not. That asymmetry is the ABI's cost class doing its
356
+ // job, not a fixture cheating: parity is asserted on THIS source in every
357
+ // lane, and the engine adapter reports its own (higher) cost honestly.
358
+
359
+ availability({ level }) {
360
+ return level <= maxLevel;
361
+ },
362
+
363
+ configure(settings = {}) {
364
+ const applied = [];
365
+ const rejected = [];
366
+ for (const key of Object.keys(settings)) {
367
+ if (["alpha", "brightness", "show"].includes(key)) applied.push(key);
368
+ else rejected.push(key);
369
+ }
370
+ return { applied, rejected };
371
+ },
372
+
373
+ acquireTile(request) {
374
+ const { level, strategyCode } = resolveLevel(request, defaultLevel);
375
+ const x = request.x | 0;
376
+ const y = request.y | 0;
377
+ return {
378
+ planes: [renderTile(level, x, y, tileWidth, tileHeight)],
379
+ descriptor: {
380
+ encoding: ProviderEncoding.RGBA8,
381
+ width: tileWidth,
382
+ height: tileHeight,
383
+ level,
384
+ tileX: x,
385
+ tileY: y,
386
+ costClass: ProviderCost.RESIDENT,
387
+ strategy: strategyCode,
388
+ ...tileRectangle(level, x, y),
389
+ },
390
+ };
391
+ },
392
+
393
+ acquireRegion(request) {
394
+ const { level, strategyCode } = resolveLevel(request, defaultLevel);
395
+ const [west, south, east, north] = request.rectangle ?? [0, 0, 0, 0];
396
+ const width = Math.max(1, request.width | 0);
397
+ const height = Math.max(1, request.height | 0);
398
+ const pixels = new Uint8Array(width * height * 4);
399
+ for (let row = 0; row < height; row += 1) {
400
+ const lat = north + ((south - north) * row) / height;
401
+ for (let column = 0; column < width; column += 1) {
402
+ const lon = west + ((east - west) * column) / width;
403
+ const sample = positionToTileSample(lon, lat, level, tileWidth, tileHeight);
404
+ const [r, g, b, a] = fixturePixel(
405
+ level,
406
+ sample.tileX,
407
+ sample.tileY,
408
+ sample.i,
409
+ sample.j,
410
+ );
411
+ const offset = (row * width + column) * 4;
412
+ pixels[offset] = r;
413
+ pixels[offset + 1] = g;
414
+ pixels[offset + 2] = b;
415
+ pixels[offset + 3] = a;
416
+ }
417
+ }
418
+ return {
419
+ planes: [pixels],
420
+ descriptor: {
421
+ encoding: ProviderEncoding.RGBA8,
422
+ width,
423
+ height,
424
+ level,
425
+ west,
426
+ south,
427
+ east,
428
+ north,
429
+ costClass: ProviderCost.RESIDENT,
430
+ strategy: strategyCode,
431
+ },
432
+ };
433
+ },
434
+ };
435
+ }
436
+
437
+ export function createFixtureProviderAdapters(options = {}) {
438
+ return [
439
+ createFixtureTerrainProvider(options.terrain ?? {}),
440
+ createFixtureImageryProvider(options.imagery ?? {}),
441
+ ];
442
+ }
443
+
444
+ export const FixtureProviderKinds = ProviderKind;