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
@@ -18,14 +18,15 @@
18
18
  * cross-origin isolated (COOP+COEP). In Node, worker_threads is used.
19
19
  */
20
20
 
21
- import { createBrowserHost } from "../host/browserHost.js";
22
- import { createAsyncHostDispatcher } from "../host/abi.js";
21
+ import { createBrowserHost } from "./browserHost.js";
22
+ import { createAsyncHostDispatcher } from "./abi.js";
23
23
  import {
24
24
  createSabHostcallBuffer,
25
25
  createSabHostcallServer,
26
26
  DEFAULT_SAB_HOSTCALL_RESPONSE_BYTES,
27
- } from "../host/sabHostcallChannel.js";
28
- import { WASI_THREAD_HOSTCALL_MESSAGE } from "../host/wasiThreadWorkerRuntime.js";
27
+ } from "./sabHostcallChannel.js";
28
+ import { WASI_THREAD_HOSTCALL_MESSAGE } from "./wasiThreadWorkerRuntime.js";
29
+ import { importNodeBuiltin } from "./nodeBuiltinSpecifier.js";
29
30
 
30
31
  const WORKER_URL = new URL("./workerModuleHarnessWorker.js", import.meta.url);
31
32
 
@@ -51,7 +52,10 @@ function isNodeRuntime() {
51
52
 
52
53
  async function spawnWorker(workerUrl) {
53
54
  if (isNodeRuntime()) {
54
- const { Worker: NodeWorker } = await import("node:worker_threads");
55
+ // Specifier assembled at runtime on purpose — see nodeBuiltinSpecifier.js.
56
+ // A literal here is statically resolved by every browser bundler even
57
+ // though this branch is dead in a browser.
58
+ const { Worker: NodeWorker } = await importNodeBuiltin("worker_threads");
55
59
  const worker = new NodeWorker(workerUrl);
56
60
  return {
57
61
  post: (message) => worker.postMessage(message),
@@ -75,17 +79,46 @@ async function spawnWorker(workerUrl) {
75
79
  };
76
80
  }
77
81
 
78
- function toBytes(source, label) {
79
- if (source instanceof Uint8Array) return new Uint8Array(source);
80
- if (source instanceof ArrayBuffer) return new Uint8Array(source.slice(0));
82
+ /**
83
+ * Compile `source` to a `WebAssembly.Module` on THIS (controlling) thread
84
+ * before ever touching a worker.
85
+ *
86
+ * Plaintext-hygiene contract (mirrors OrbPro's `wasmModuleHygiene.js`,
87
+ * `wasm-plaintext-memory-hygiene`): a compiled `WebAssembly.Module` is
88
+ * structured-cloneable to a Worker (browser) or `node:worker_threads`
89
+ * (Node) — proven by `createWasiThreadSpawn` in `src/host/wasiThreadHost.js`,
90
+ * which hands the same compiled module to every pooled worker in both
91
+ * lanes. Shipping the MODULE instead of raw bytes means there is no second
92
+ * plaintext copy alive in the worker, and no re-compile once it arrives —
93
+ * `createBrowserModuleHarness`'s own `compileWasmModule` already accepts a
94
+ * `WebAssembly.Module` directly and returns it unchanged.
95
+ *
96
+ * `source` is never retained past this call and never zeroed here: a
97
+ * caller-supplied `Uint8Array`/`ArrayBuffer` is not this harness's buffer to
98
+ * mutate (compile does not retain a reference to it either — the caller is
99
+ * free to reuse or scrub it immediately after this resolves).
100
+ *
101
+ * @param {WebAssembly.Module|Uint8Array|ArrayBuffer} source
102
+ * @param {string} label diagnostic label
103
+ * @returns {Promise<WebAssembly.Module>}
104
+ */
105
+ async function toWasmModule(source, label) {
106
+ if (source instanceof WebAssembly.Module) {
107
+ return source;
108
+ }
109
+ if (source instanceof Uint8Array || source instanceof ArrayBuffer) {
110
+ return WebAssembly.compile(source);
111
+ }
81
112
  throw new TypeError(
82
- `${label} must be a Uint8Array or ArrayBuffer (the worker harness ships bytes to the worker).`,
113
+ `${label} must be a WebAssembly.Module, Uint8Array, or ArrayBuffer.`,
83
114
  );
84
115
  }
85
116
 
86
117
  /**
87
118
  * @param {object} options
88
- * @param {Uint8Array|ArrayBuffer} options.wasmSource module bytes
119
+ * @param {WebAssembly.Module|Uint8Array|ArrayBuffer} options.wasmSource module
120
+ * bytes OR an already-compiled module (preferred — skips a redundant
121
+ * compile and avoids ever materializing plaintext bytes in this harness)
89
122
  * @param {object} [options.host] host servicing guest hostcalls (default createBrowserHost(hostOptions))
90
123
  * @param {object} [options.hostOptions] options for the default host
91
124
  * @param {(operation: string, params: any) => Promise<any>} [options.dispatchHost] dispatch override
@@ -96,7 +129,7 @@ function toBytes(source, label) {
96
129
  * createBrowserModuleHarness (surface, args, env, allowRawInvoke, logOutput, ...)
97
130
  */
98
131
  export async function createWorkerModuleHarness(options = {}) {
99
- const wasmBytes = toBytes(options.wasmSource, "wasmSource");
132
+ const wasmModule = await toWasmModule(options.wasmSource, "wasmSource");
100
133
  const host = options.host ?? createBrowserHost(options.hostOptions);
101
134
  const dispatch = options.dispatchHost ?? createAsyncHostDispatcher(host);
102
135
  const buffer = createSabHostcallBuffer({
@@ -150,7 +183,7 @@ export async function createWorkerModuleHarness(options = {}) {
150
183
  case "worker-online":
151
184
  port.post({
152
185
  type: "init",
153
- wasmBytes,
186
+ wasmModule,
154
187
  buffer,
155
188
  hostcallTimeoutMs: options.hostcallTimeoutMs,
156
189
  harnessOptions: {
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { createBrowserModuleHarness } from "./browserModuleHarness.js";
13
- import { createSabHostcallClientDispatch } from "../host/sabHostcallChannel.js";
13
+ import { createSabHostcallClientDispatch } from "./sabHostcallChannel.js";
14
14
 
15
15
  async function resolvePort() {
16
16
  if (
@@ -90,12 +90,13 @@ async function handleInit(message) {
90
90
  timeoutMs: message.hostcallTimeoutMs,
91
91
  postRequest: (request) => port.post({ type: "hostcall", ...request }),
92
92
  });
93
+ // Plaintext-hygiene contract: the controlling thread compiles once and
94
+ // ships the WebAssembly.Module (never raw bytes) — see toWasmModule() in
95
+ // workerModuleHarness.js. createBrowserModuleHarness's compileWasmModule
96
+ // already returns a Module input unchanged, so this never re-compiles.
93
97
  const harnessOptions = {
94
98
  ...(message.harnessOptions ?? {}),
95
- wasmSource:
96
- message.wasmBytes instanceof Uint8Array
97
- ? message.wasmBytes
98
- : new Uint8Array(message.wasmBytes),
99
+ wasmSource: message.wasmModule,
99
100
  hostcallDispatch: dispatch,
100
101
  };
101
102
  harness = await createBrowserModuleHarness(harnessOptions);
package/src/index.d.ts CHANGED
@@ -1962,7 +1962,11 @@ export interface BrowserModuleHarness {
1962
1962
  }
1963
1963
  export function detectArtifactProfile(wasmModule: WebAssembly.Module): string;
1964
1964
  export function createBrowserModuleHarness(options?: {
1965
- wasmSource: Uint8Array | ArrayBuffer | string | WebAssembly.Module | unknown;
1965
+ wasmSource: Uint8Array | ArrayBuffer | string | Response | WebAssembly.Module | unknown;
1966
+ // `true` uses env/global-configured trust; an object sets it explicitly;
1967
+ // `false` disables verification. Ignored for a precompiled WebAssembly.Module
1968
+ // (unverifiable — pass bytes/URL/Response when verification is required).
1969
+ verifySignature?: boolean | { trustedPublicKeys?: string[]; requireSignature?: boolean };
1966
1970
  host?: BrowserHost | RuntimeHost | Record<string, unknown>;
1967
1971
  hostOptions?: BrowserHostOptions;
1968
1972
  args?: string[];
@@ -1979,6 +1983,25 @@ export function createBrowserModuleHarness(options?: {
1979
1983
  allowRawInvoke?: boolean;
1980
1984
  initialMemoryBytes?: number;
1981
1985
  maximumMemoryBytes?: number;
1986
+ // Upper bound on guest wasi-threads spawns; sizes the browser warm worker
1987
+ // pool (defaults to hardware concurrency). No effect on a non-threaded guest.
1988
+ maxThreads?: number;
1989
+ // Request-isolated BroadcastChannel descriptor a nested pthread hostcall
1990
+ // dispatches over; supplied by createWorkerModuleHarness for the in-worker
1991
+ // harness instance, not something a top-level caller usually sets by hand.
1992
+ threadHostcallChannel?: {
1993
+ channelName: string;
1994
+ token: string;
1995
+ maxResponseBytes?: number;
1996
+ timeoutMs?: number;
1997
+ };
1998
+ // Explicit successful cross-origin-isolation negotiation for wasi-threads
1999
+ // guests; defaults to true. See docs/browser-wasmedge-isomorphic.md's
2000
+ // cross-origin-isolation section for when this is required.
2001
+ enableBrowserWasiThreads?: boolean;
2002
+ // Capacity of the module-memory arena a direct-invoke request is written
2003
+ // into (bytes). Defaults to 64 KiB.
2004
+ directInvokeRequestArenaBytes?: number;
1982
2005
  logOutput?: boolean;
1983
2006
  hostcallDispatch?: (operation: string, params: unknown) => unknown;
1984
2007
  }): Promise<BrowserModuleHarness>;
@@ -1997,7 +2020,11 @@ export interface WorkerModuleHarness {
1997
2020
  destroy(): Promise<void>;
1998
2021
  }
1999
2022
  export function createWorkerModuleHarness(options: {
2000
- wasmSource: Uint8Array | ArrayBuffer;
2023
+ // A precompiled WebAssembly.Module is preferred: it skips a redundant
2024
+ // compile and is structured-cloned to the worker directly (no plaintext
2025
+ // bytes cross the postMessage boundary at all). Uint8Array/ArrayBuffer is
2026
+ // compiled once on the controlling thread before the worker is spawned.
2027
+ wasmSource: WebAssembly.Module | Uint8Array | ArrayBuffer;
2001
2028
  host?: unknown;
2002
2029
  hostOptions?: BrowserHostOptions;
2003
2030
  dispatchHost?: (operation: string, params: unknown) => Promise<unknown>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Standards catalog — BROWSER leg (the `browser` condition of `./standards`).
3
+ *
4
+ * Same catalog, same validation verdicts as the Node leg; only acquisition
5
+ * differs, and that difference is absorbed here rather than by a runtime check
6
+ * inside shared code. A browser has no `require.resolve` and no readFile, so
7
+ * the manifest must be handed in or fetched:
8
+ *
9
+ * loadStandardsCatalog({ manifest }) // already-parsed manifest.json
10
+ * loadStandardsCatalog({ manifestUrl }) // fetched, then parsed
11
+ *
12
+ * Neither one supplied is a caller defect, not a fallback: it throws. Silently
13
+ * returning an empty catalog would let a browser "validate" a manifest against
14
+ * nothing and agree with a Node run that actually checked it — exactly the kind
15
+ * of quiet divergence the tri-runtime contract exists to forbid.
16
+ */
17
+
18
+ import {
19
+ buildStandardsCatalog,
20
+ validateManifestAgainstCatalog,
21
+ withSharedModuleCatalog,
22
+ } from "./catalogCore.js";
23
+
24
+ export {
25
+ buildStandardsCatalog,
26
+ resolveStandardsTypeRef,
27
+ validateManifestAgainstCatalog,
28
+ withSharedModuleCatalog,
29
+ } from "./catalogCore.js";
30
+
31
+ const standardsCatalogPromises = new Map();
32
+ const knownTypeCatalogPromises = new Map();
33
+
34
+ function catalogCacheKey(options) {
35
+ if (typeof options?.manifestUrl === "string" && options.manifestUrl) {
36
+ return `url:${options.manifestUrl}`;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ async function readManifest(options) {
42
+ if (options?.manifest && typeof options.manifest === "object") {
43
+ return options.manifest;
44
+ }
45
+ if (typeof options?.manifestUrl === "string" && options.manifestUrl) {
46
+ const response = await fetch(options.manifestUrl);
47
+ if (!response.ok) {
48
+ throw new Error(
49
+ `Failed to fetch the spacedatastandards.org manifest from ${options.manifestUrl}: ${response.status}`,
50
+ );
51
+ }
52
+ return response.json();
53
+ }
54
+ throw new Error(
55
+ "The browser standards catalog needs the spacedatastandards.org manifest: " +
56
+ "pass { manifest } (already parsed) or { manifestUrl } (fetched). " +
57
+ "There is no on-disk manifest to read in a browser.",
58
+ );
59
+ }
60
+
61
+ export async function loadStandardsCatalog(options = {}) {
62
+ const key = catalogCacheKey(options);
63
+ if (key === null) {
64
+ return buildStandardsCatalog(await readManifest(options));
65
+ }
66
+ if (!standardsCatalogPromises.has(key)) {
67
+ standardsCatalogPromises.set(
68
+ key,
69
+ (async () => buildStandardsCatalog(await readManifest(options)))(),
70
+ );
71
+ }
72
+ return standardsCatalogPromises.get(key);
73
+ }
74
+
75
+ export async function loadKnownTypeCatalog(options = {}) {
76
+ const key = catalogCacheKey(options);
77
+ if (key === null) {
78
+ return withSharedModuleCatalog(await loadStandardsCatalog(options));
79
+ }
80
+ if (!knownTypeCatalogPromises.has(key)) {
81
+ knownTypeCatalogPromises.set(
82
+ key,
83
+ loadStandardsCatalog(options).then(withSharedModuleCatalog),
84
+ );
85
+ }
86
+ return knownTypeCatalogPromises.get(key);
87
+ }
88
+
89
+ export async function validateManifestAgainstStandardsCatalog(
90
+ manifest,
91
+ options = {},
92
+ ) {
93
+ const catalog = options.catalog ?? (await loadKnownTypeCatalog(options));
94
+ return validateManifestAgainstCatalog(catalog, manifest, options);
95
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Standards catalog — the runtime-independent half.
3
+ *
4
+ * Parsing, freshness validation, type-ref resolution and manifest checking are
5
+ * identical everywhere. Only HOW the `spacedatastandards.org` manifest is
6
+ * OBTAINED differs: Node reads it off disk (`./index.js`), a browser is handed
7
+ * it or fetches it (`./browser.js`). The package's `browser` export condition
8
+ * picks the leg, so a browser bundle never statically resolves
9
+ * node:fs/promises, node:path or node:module
10
+ * (`module-sdk-browser-entry-node-builtins`).
11
+ */
12
+
13
+ import { sharedModuleCatalog } from "./sharedCatalog.js";
14
+ import { normalizePayloadSchemaHash } from "../manifest/typeRefs.js";
15
+ const requiredCurrentScvTokens = Object.freeze([
16
+ "SENSOR_LOCAL",
17
+ "table SCVResult",
18
+ "table SCVSensorShapeContract",
19
+ "SAR_ANNULAR_SECTOR",
20
+ "enum scvSensorRangeBoundaryKind",
21
+ "table SCVAggregateStatistics",
22
+ "AGGREGATE_STATISTICS:SCVAggregateStatistics",
23
+ "table SCVPackedRasterProducts",
24
+ "table SCVPackedRasterBand",
25
+ "RASTER_PRODUCTS:SCVPackedRasterProducts",
26
+ "enum scvRasterProductKind",
27
+ ]);
28
+ const currentScvResultFields = Object.freeze(
29
+ new Set([
30
+ "JOB_ID",
31
+ "TRACE_ID",
32
+ "STATUS",
33
+ "TIME_GRID",
34
+ "TARGET_BODY",
35
+ "TOTAL_SENSORS",
36
+ "TOTAL_WINDOWS",
37
+ "CELL_STATS",
38
+ "INTERVALS",
39
+ "LATITUDE_BANDS",
40
+ "TIME_SERIES",
41
+ "HISTOGRAMS",
42
+ "CONTRIBUTIONS",
43
+ "GEOMETRY",
44
+ "RASTER_PRODUCTS",
45
+ "MESSAGE",
46
+ "AGGREGATE_STATISTICS",
47
+ "TARGET_RESULTS",
48
+ ]),
49
+ );
50
+
51
+ function collectFlatbufferTableFields(idl, tableName) {
52
+ const match = String(idl ?? "").match(
53
+ new RegExp(`\\btable\\s+${tableName}\\s*\\{([\\s\\S]*?)\\n\\}`, "m"),
54
+ );
55
+ if (!match) {
56
+ return [];
57
+ }
58
+ return match[1]
59
+ .split("\n")
60
+ .map((line) => line.replace(/\/\/.*$/, "").trim())
61
+ .map((line) => line.match(/^([A-Z0-9_]+)\s*:/)?.[1] ?? null)
62
+ .filter(Boolean);
63
+ }
64
+
65
+ function normalizeSchemaName(value) {
66
+ return value === undefined || value === null ? "" : String(value);
67
+ }
68
+
69
+ function normalizeFileIdentifier(value) {
70
+ return value === undefined || value === null ? "" : String(value);
71
+ }
72
+
73
+ function parseStandardsEntry([schemaCode, entry]) {
74
+ const idl = String(entry?.IDL ?? "");
75
+ const fileIdentifierMatch = idl.match(/file_identifier\s+"([^"]+)"/i);
76
+ const hashMatch = idl.match(/\/\/ Hash:\s*([a-f0-9]+)/i);
77
+ const versionMatch = idl.match(/\/\/ Version:\s*([^\n]+)/i);
78
+ const rootTypeMatch = idl.match(/root_type\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/i);
79
+ return {
80
+ schemaCode: schemaCode.toUpperCase(),
81
+ schemaName: `${schemaCode.toUpperCase()}.fbs`,
82
+ fileIdentifier: fileIdentifierMatch
83
+ ? normalizeFileIdentifier(fileIdentifierMatch[1])
84
+ : null,
85
+ hash: hashMatch ? hashMatch[1].toLowerCase() : null,
86
+ rootTypeName: rootTypeMatch ? rootTypeMatch[1] : null,
87
+ idl,
88
+ version: versionMatch ? versionMatch[1].trim() : null,
89
+ files: Array.isArray(entry?.files) ? entry.files : [],
90
+ };
91
+ }
92
+
93
+ function validateStandardsCatalogFreshness(catalog, sourceName) {
94
+ const issues = [];
95
+ const scvEntry = catalog.find((entry) => entry.schemaCode === "SCV");
96
+ if (!scvEntry) {
97
+ return issues;
98
+ }
99
+ const missingTokens = requiredCurrentScvTokens.filter(
100
+ (token) => !scvEntry.idl.includes(token),
101
+ );
102
+ if (missingTokens.length > 0) {
103
+ issues.push({
104
+ severity: "error",
105
+ code: "stale-scv-contract",
106
+ message:
107
+ "The loaded spacedatastandards.org SCV catalog is stale and does not " +
108
+ `include required current coverage fields: ${missingTokens.join(", ")}.`,
109
+ location: `${sourceName}.standards.SCV`,
110
+ });
111
+ }
112
+ const unsupportedResultFields = collectFlatbufferTableFields(
113
+ scvEntry.idl,
114
+ "SCVResult",
115
+ ).filter((field) => !currentScvResultFields.has(field));
116
+ if (unsupportedResultFields.length > 0) {
117
+ issues.push({
118
+ severity: "error",
119
+ code: "stale-scv-contract",
120
+ message:
121
+ "The loaded spacedatastandards.org SCV catalog contains unsupported " +
122
+ `SCVResult fields that are not part of the current contract: ${unsupportedResultFields.join(", ")}.`,
123
+ location: `${sourceName}.standards.SCV`,
124
+ });
125
+ }
126
+ return issues;
127
+ }
128
+
129
+ /**
130
+ * Build the standards catalog from an already-parsed manifest object. Same
131
+ * bytes in, same catalog out, in every runtime.
132
+ *
133
+ * @param {object} manifest - the parsed `spacedatastandards.org` manifest.json
134
+ * @returns {Array} sorted standards catalog
135
+ */
136
+ export function buildStandardsCatalog(manifest) {
137
+ return Object.entries(manifest?.STANDARDS ?? {})
138
+ .map(parseStandardsEntry)
139
+ .sort((left, right) => left.schemaCode.localeCompare(right.schemaCode));
140
+ }
141
+
142
+ /**
143
+ * Append the SDK's own shared-module catalog to a standards catalog.
144
+ *
145
+ * @param {Array} catalog
146
+ * @returns {Array}
147
+ */
148
+ export function withSharedModuleCatalog(catalog) {
149
+ return [...catalog, ...sharedModuleCatalog];
150
+ }
151
+
152
+ export function resolveStandardsTypeRef(typeRef, catalog = []) {
153
+ const schemaName = normalizeSchemaName(typeRef?.schemaName);
154
+ const fileIdentifier = normalizeFileIdentifier(typeRef?.fileIdentifier);
155
+ if (!schemaName && !fileIdentifier) {
156
+ return null;
157
+ }
158
+ return (
159
+ catalog.find(
160
+ (entry) =>
161
+ (!schemaName || normalizeSchemaName(entry.schemaName) === schemaName) &&
162
+ (!fileIdentifier ||
163
+ normalizeFileIdentifier(entry.fileIdentifier) === fileIdentifier),
164
+ ) ?? null
165
+ );
166
+ }
167
+
168
+ function schemaHashMatchesCatalog(typeRefHash, catalogHash) {
169
+ if (typeRefHash === undefined || typeRefHash === null) return true;
170
+ const declared = normalizePayloadSchemaHash(typeRefHash);
171
+ if (!declared) return true;
172
+ const canonical = normalizePayloadSchemaHash(catalogHash);
173
+ if (!canonical || declared.length !== canonical.length) {
174
+ return false;
175
+ }
176
+ return declared.every((byte, index) => byte === canonical[index]);
177
+ }
178
+
179
+ function collectTypeRefs(manifest) {
180
+ const refs = [];
181
+ for (const method of Array.isArray(manifest?.methods) ? manifest.methods : []) {
182
+ for (const portsKey of ["inputPorts", "outputPorts"]) {
183
+ for (const port of Array.isArray(method?.[portsKey]) ? method[portsKey] : []) {
184
+ for (const typeSet of Array.isArray(port?.acceptedTypeSets)
185
+ ? port.acceptedTypeSets
186
+ : []) {
187
+ for (const allowedType of Array.isArray(typeSet?.allowedTypes)
188
+ ? typeSet.allowedTypes
189
+ : []) {
190
+ refs.push({
191
+ source: `${method?.methodId ?? "method"}.${port?.portId ?? "port"}`,
192
+ typeRef: allowedType,
193
+ });
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ for (const typeRef of Array.isArray(manifest?.schemasUsed)
200
+ ? manifest.schemasUsed
201
+ : []) {
202
+ refs.push({
203
+ source: "schemasUsed",
204
+ typeRef,
205
+ });
206
+ }
207
+ return refs;
208
+ }
209
+
210
+ /**
211
+ * Validate a module manifest against an ALREADY-RESOLVED catalog.
212
+ *
213
+ * Both legs of `./standards` funnel into this, so the verdicts cannot diverge
214
+ * between runtimes; only catalog acquisition differs.
215
+ *
216
+ * @param {Array} catalog
217
+ * @param {object} manifest
218
+ * @param {object} [options]
219
+ * @returns {{catalog: Array, issues: Array}}
220
+ */
221
+ export function validateManifestAgainstCatalog(catalog, manifest, options = {}) {
222
+ const sourceName = options.sourceName ?? "manifest";
223
+ const issues = validateStandardsCatalogFreshness(catalog, sourceName);
224
+ for (const { source, typeRef } of collectTypeRefs(manifest)) {
225
+ if (typeRef?.acceptsAnyFlatbuffer === true) {
226
+ continue;
227
+ }
228
+ const resolved = resolveStandardsTypeRef(typeRef, catalog);
229
+ if (!resolved) {
230
+ const schemaName = normalizeSchemaName(typeRef?.schemaName);
231
+ const fileIdentifier = normalizeFileIdentifier(typeRef?.fileIdentifier);
232
+ const partiallyKnown = catalog.some(
233
+ (entry) =>
234
+ (schemaName && normalizeSchemaName(entry.schemaName) === schemaName) ||
235
+ (fileIdentifier &&
236
+ normalizeFileIdentifier(entry.fileIdentifier) === fileIdentifier),
237
+ );
238
+ issues.push({
239
+ severity: "error",
240
+ code: partiallyKnown
241
+ ? "standards-type-identity-mismatch"
242
+ : "unresolved-standards-type",
243
+ message: partiallyKnown
244
+ ? `Type reference from ${source} mixes a known schemaName or exact four-byte fileIdentifier with a different standards entry.`
245
+ : `Type reference from ${source} does not resolve to a known shared-module or \`spacedatastandards.org\` schema by its exact schemaName and fileIdentifier.`,
246
+ location: `${sourceName}.${source}`,
247
+ });
248
+ continue;
249
+ }
250
+ if (
251
+ typeRef?.rootTypeName &&
252
+ resolved.rootTypeName &&
253
+ typeRef.rootTypeName !== resolved.rootTypeName
254
+ ) {
255
+ issues.push({
256
+ severity: "error",
257
+ code: "standards-root-type-mismatch",
258
+ message: `Type reference from ${source} declares rootTypeName ${JSON.stringify(typeRef.rootTypeName)} but the canonical SDS root is ${JSON.stringify(resolved.rootTypeName)}.`,
259
+ location: `${sourceName}.${source}`,
260
+ });
261
+ }
262
+ if (
263
+ typeRef?.schemaVersion &&
264
+ resolved.version &&
265
+ typeRef.schemaVersion !== resolved.version
266
+ ) {
267
+ issues.push({
268
+ severity: "error",
269
+ code: "standards-schema-version-mismatch",
270
+ message: `Type reference from ${source} declares schemaVersion ${JSON.stringify(typeRef.schemaVersion)} but the canonical SDS version is ${JSON.stringify(resolved.version)}.`,
271
+ location: `${sourceName}.${source}`,
272
+ });
273
+ }
274
+ if (
275
+ resolved.hash &&
276
+ !schemaHashMatchesCatalog(typeRef?.schemaHash, resolved.hash)
277
+ ) {
278
+ issues.push({
279
+ severity: "error",
280
+ code: "standards-schema-hash-mismatch",
281
+ message: `Type reference from ${source} declares a schemaHash that differs from the canonical SDS schema hash.`,
282
+ location: `${sourceName}.${source}`,
283
+ });
284
+ }
285
+ }
286
+ return {
287
+ catalog,
288
+ issues,
289
+ };
290
+ }