space-data-module-sdk 0.8.7 → 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.
@@ -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
+ }
@@ -1,62 +1,32 @@
1
+ /**
2
+ * Standards catalog — NODE leg (the `default` condition of `./standards`).
3
+ *
4
+ * Reads the pinned `spacedatastandards.org` manifest off disk. Everything the
5
+ * catalog is then USED for lives in `catalogCore.js` and is shared with the
6
+ * browser leg, so a manifest validated here and a manifest validated in a
7
+ * browser produce the same issues in the same order.
8
+ */
9
+
1
10
  import { readFile } from "node:fs/promises";
2
11
  import path from "node:path";
3
12
  import { createRequire } from "node:module";
4
13
 
5
- import { sharedModuleCatalog } from "./sharedCatalog.js";
6
- import { normalizePayloadSchemaHash } from "../manifest/typeRefs.js";
14
+ import {
15
+ buildStandardsCatalog,
16
+ validateManifestAgainstCatalog,
17
+ withSharedModuleCatalog,
18
+ } from "./catalogCore.js";
19
+
20
+ export {
21
+ buildStandardsCatalog,
22
+ resolveStandardsTypeRef,
23
+ validateManifestAgainstCatalog,
24
+ withSharedModuleCatalog,
25
+ } from "./catalogCore.js";
7
26
 
8
27
  const require = createRequire(import.meta.url);
9
28
  const standardsCatalogPromises = new Map();
10
29
  const knownTypeCatalogPromises = new Map();
11
- const requiredCurrentScvTokens = Object.freeze([
12
- "SENSOR_LOCAL",
13
- "table SCVResult",
14
- "table SCVSensorShapeContract",
15
- "SAR_ANNULAR_SECTOR",
16
- "enum scvSensorRangeBoundaryKind",
17
- "table SCVAggregateStatistics",
18
- "AGGREGATE_STATISTICS:SCVAggregateStatistics",
19
- "table SCVPackedRasterProducts",
20
- "table SCVPackedRasterBand",
21
- "RASTER_PRODUCTS:SCVPackedRasterProducts",
22
- "enum scvRasterProductKind",
23
- ]);
24
- const currentScvResultFields = Object.freeze(
25
- new Set([
26
- "JOB_ID",
27
- "TRACE_ID",
28
- "STATUS",
29
- "TIME_GRID",
30
- "TARGET_BODY",
31
- "TOTAL_SENSORS",
32
- "TOTAL_WINDOWS",
33
- "CELL_STATS",
34
- "INTERVALS",
35
- "LATITUDE_BANDS",
36
- "TIME_SERIES",
37
- "HISTOGRAMS",
38
- "CONTRIBUTIONS",
39
- "GEOMETRY",
40
- "RASTER_PRODUCTS",
41
- "MESSAGE",
42
- "AGGREGATE_STATISTICS",
43
- "TARGET_RESULTS",
44
- ]),
45
- );
46
-
47
- function collectFlatbufferTableFields(idl, tableName) {
48
- const match = String(idl ?? "").match(
49
- new RegExp(`\\btable\\s+${tableName}\\s*\\{([\\s\\S]*?)\\n\\}`, "m"),
50
- );
51
- if (!match) {
52
- return [];
53
- }
54
- return match[1]
55
- .split("\n")
56
- .map((line) => line.replace(/\/\/.*$/, "").trim())
57
- .map((line) => line.match(/^([A-Z0-9_]+)\s*:/)?.[1] ?? null)
58
- .filter(Boolean);
59
- }
60
30
 
61
31
  function resolveStandardsManifestPath(options = {}) {
62
32
  const standardsRoot =
@@ -69,81 +39,15 @@ function resolveStandardsManifestPath(options = {}) {
69
39
  return path.join(path.dirname(packageEntry), "dist", "manifest.json");
70
40
  }
71
41
 
72
- function normalizeSchemaName(value) {
73
- return value === undefined || value === null ? "" : String(value);
74
- }
75
-
76
- function normalizeFileIdentifier(value) {
77
- return value === undefined || value === null ? "" : String(value);
78
- }
79
-
80
- function parseStandardsEntry([schemaCode, entry]) {
81
- const idl = String(entry?.IDL ?? "");
82
- const fileIdentifierMatch = idl.match(/file_identifier\s+"([^"]+)"/i);
83
- const hashMatch = idl.match(/\/\/ Hash:\s*([a-f0-9]+)/i);
84
- const versionMatch = idl.match(/\/\/ Version:\s*([^\n]+)/i);
85
- const rootTypeMatch = idl.match(/root_type\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/i);
86
- return {
87
- schemaCode: schemaCode.toUpperCase(),
88
- schemaName: `${schemaCode.toUpperCase()}.fbs`,
89
- fileIdentifier: fileIdentifierMatch
90
- ? normalizeFileIdentifier(fileIdentifierMatch[1])
91
- : null,
92
- hash: hashMatch ? hashMatch[1].toLowerCase() : null,
93
- rootTypeName: rootTypeMatch ? rootTypeMatch[1] : null,
94
- idl,
95
- version: versionMatch ? versionMatch[1].trim() : null,
96
- files: Array.isArray(entry?.files) ? entry.files : [],
97
- };
98
- }
99
-
100
- function validateStandardsCatalogFreshness(catalog, sourceName) {
101
- const issues = [];
102
- const scvEntry = catalog.find((entry) => entry.schemaCode === "SCV");
103
- if (!scvEntry) {
104
- return issues;
105
- }
106
- const missingTokens = requiredCurrentScvTokens.filter(
107
- (token) => !scvEntry.idl.includes(token),
108
- );
109
- if (missingTokens.length > 0) {
110
- issues.push({
111
- severity: "error",
112
- code: "stale-scv-contract",
113
- message:
114
- "The loaded spacedatastandards.org SCV catalog is stale and does not " +
115
- `include required current coverage fields: ${missingTokens.join(", ")}.`,
116
- location: `${sourceName}.standards.SCV`,
117
- });
118
- }
119
- const unsupportedResultFields = collectFlatbufferTableFields(
120
- scvEntry.idl,
121
- "SCVResult",
122
- ).filter((field) => !currentScvResultFields.has(field));
123
- if (unsupportedResultFields.length > 0) {
124
- issues.push({
125
- severity: "error",
126
- code: "stale-scv-contract",
127
- message:
128
- "The loaded spacedatastandards.org SCV catalog contains unsupported " +
129
- `SCVResult fields that are not part of the current contract: ${unsupportedResultFields.join(", ")}.`,
130
- location: `${sourceName}.standards.SCV`,
131
- });
132
- }
133
- return issues;
134
- }
135
-
136
42
  export async function loadStandardsCatalog(options = {}) {
137
43
  const manifestPath = resolveStandardsManifestPath(options);
138
44
  if (!standardsCatalogPromises.has(manifestPath)) {
139
45
  standardsCatalogPromises.set(
140
46
  manifestPath,
141
- (async () => {
142
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
143
- return Object.entries(manifest?.STANDARDS ?? {})
144
- .map(parseStandardsEntry)
145
- .sort((left, right) => left.schemaCode.localeCompare(right.schemaCode));
146
- })(),
47
+ (async () =>
48
+ buildStandardsCatalog(
49
+ JSON.parse(await readFile(manifestPath, "utf8")),
50
+ ))(),
147
51
  );
148
52
  }
149
53
  return standardsCatalogPromises.get(manifestPath);
@@ -154,144 +58,16 @@ export async function loadKnownTypeCatalog(options = {}) {
154
58
  if (!knownTypeCatalogPromises.has(manifestPath)) {
155
59
  knownTypeCatalogPromises.set(
156
60
  manifestPath,
157
- loadStandardsCatalog(options).then((catalog) => [
158
- ...catalog,
159
- ...sharedModuleCatalog,
160
- ]),
61
+ loadStandardsCatalog(options).then(withSharedModuleCatalog),
161
62
  );
162
63
  }
163
64
  return knownTypeCatalogPromises.get(manifestPath);
164
65
  }
165
66
 
166
- export function resolveStandardsTypeRef(typeRef, catalog = []) {
167
- const schemaName = normalizeSchemaName(typeRef?.schemaName);
168
- const fileIdentifier = normalizeFileIdentifier(typeRef?.fileIdentifier);
169
- if (!schemaName && !fileIdentifier) {
170
- return null;
171
- }
172
- return (
173
- catalog.find(
174
- (entry) =>
175
- (!schemaName || normalizeSchemaName(entry.schemaName) === schemaName) &&
176
- (!fileIdentifier ||
177
- normalizeFileIdentifier(entry.fileIdentifier) === fileIdentifier),
178
- ) ?? null
179
- );
180
- }
181
-
182
- function schemaHashMatchesCatalog(typeRefHash, catalogHash) {
183
- if (typeRefHash === undefined || typeRefHash === null) return true;
184
- const declared = normalizePayloadSchemaHash(typeRefHash);
185
- if (!declared) return true;
186
- const canonical = normalizePayloadSchemaHash(catalogHash);
187
- if (!canonical || declared.length !== canonical.length) {
188
- return false;
189
- }
190
- return declared.every((byte, index) => byte === canonical[index]);
191
- }
192
-
193
- function collectTypeRefs(manifest) {
194
- const refs = [];
195
- for (const method of Array.isArray(manifest?.methods) ? manifest.methods : []) {
196
- for (const portsKey of ["inputPorts", "outputPorts"]) {
197
- for (const port of Array.isArray(method?.[portsKey]) ? method[portsKey] : []) {
198
- for (const typeSet of Array.isArray(port?.acceptedTypeSets)
199
- ? port.acceptedTypeSets
200
- : []) {
201
- for (const allowedType of Array.isArray(typeSet?.allowedTypes)
202
- ? typeSet.allowedTypes
203
- : []) {
204
- refs.push({
205
- source: `${method?.methodId ?? "method"}.${port?.portId ?? "port"}`,
206
- typeRef: allowedType,
207
- });
208
- }
209
- }
210
- }
211
- }
212
- }
213
- for (const typeRef of Array.isArray(manifest?.schemasUsed)
214
- ? manifest.schemasUsed
215
- : []) {
216
- refs.push({
217
- source: "schemasUsed",
218
- typeRef,
219
- });
220
- }
221
- return refs;
222
- }
223
-
224
67
  export async function validateManifestAgainstStandardsCatalog(
225
68
  manifest,
226
69
  options = {},
227
70
  ) {
228
71
  const catalog = options.catalog ?? (await loadKnownTypeCatalog(options));
229
- const sourceName = options.sourceName ?? "manifest";
230
- const issues = validateStandardsCatalogFreshness(catalog, sourceName);
231
- for (const { source, typeRef } of collectTypeRefs(manifest)) {
232
- if (typeRef?.acceptsAnyFlatbuffer === true) {
233
- continue;
234
- }
235
- const resolved = resolveStandardsTypeRef(typeRef, catalog);
236
- if (!resolved) {
237
- const schemaName = normalizeSchemaName(typeRef?.schemaName);
238
- const fileIdentifier = normalizeFileIdentifier(typeRef?.fileIdentifier);
239
- const partiallyKnown = catalog.some(
240
- (entry) =>
241
- (schemaName && normalizeSchemaName(entry.schemaName) === schemaName) ||
242
- (fileIdentifier &&
243
- normalizeFileIdentifier(entry.fileIdentifier) === fileIdentifier),
244
- );
245
- issues.push({
246
- severity: "error",
247
- code: partiallyKnown
248
- ? "standards-type-identity-mismatch"
249
- : "unresolved-standards-type",
250
- message: partiallyKnown
251
- ? `Type reference from ${source} mixes a known schemaName or exact four-byte fileIdentifier with a different standards entry.`
252
- : `Type reference from ${source} does not resolve to a known shared-module or \`spacedatastandards.org\` schema by its exact schemaName and fileIdentifier.`,
253
- location: `${sourceName}.${source}`,
254
- });
255
- continue;
256
- }
257
- if (
258
- typeRef?.rootTypeName &&
259
- resolved.rootTypeName &&
260
- typeRef.rootTypeName !== resolved.rootTypeName
261
- ) {
262
- issues.push({
263
- severity: "error",
264
- code: "standards-root-type-mismatch",
265
- message: `Type reference from ${source} declares rootTypeName ${JSON.stringify(typeRef.rootTypeName)} but the canonical SDS root is ${JSON.stringify(resolved.rootTypeName)}.`,
266
- location: `${sourceName}.${source}`,
267
- });
268
- }
269
- if (
270
- typeRef?.schemaVersion &&
271
- resolved.version &&
272
- typeRef.schemaVersion !== resolved.version
273
- ) {
274
- issues.push({
275
- severity: "error",
276
- code: "standards-schema-version-mismatch",
277
- message: `Type reference from ${source} declares schemaVersion ${JSON.stringify(typeRef.schemaVersion)} but the canonical SDS version is ${JSON.stringify(resolved.version)}.`,
278
- location: `${sourceName}.${source}`,
279
- });
280
- }
281
- if (
282
- resolved.hash &&
283
- !schemaHashMatchesCatalog(typeRef?.schemaHash, resolved.hash)
284
- ) {
285
- issues.push({
286
- severity: "error",
287
- code: "standards-schema-hash-mismatch",
288
- message: `Type reference from ${source} declares a schemaHash that differs from the canonical SDS schema hash.`,
289
- location: `${sourceName}.${source}`,
290
- });
291
- }
292
- }
293
- return {
294
- catalog,
295
- issues,
296
- };
72
+ return validateManifestAgainstCatalog(catalog, manifest, options);
297
73
  }
@@ -22,10 +22,33 @@ author-facing harnesses and streaming helpers.
22
22
  - Avoid hiding stateful behavior inside one-off demos; if a harness contract is
23
23
  real, test it here.
24
24
 
25
+ ## Browser-Facing Code Does NOT Live Here (ruling 2026-08-07)
26
+
27
+ `src/testing/**` is HARNESS surface: it spawns WasmEdge, shells out, and opens
28
+ files. NOTHING browser-facing may resolve into it, because a browser bundler
29
+ resolves every branch statically and emits `node:` specifiers the browser then
30
+ tries to FETCH — that is how all 275 OrbPro gallery demos went dark
31
+ (`orbpro-engine-bundle-ships-node-builtins`).
32
+
33
+ The browser runtime surfaces moved OUT of here and into `src/host/`:
34
+
35
+ | was | is |
36
+ | --- | --- |
37
+ | `testing/browserModuleHarness.js` | `host/browserModuleHarness.js` (`./host/browser-module`) |
38
+ | `testing/workerModuleHarness.js` | `host/workerModuleHarness.js` (`./host/worker-module`) |
39
+ | `testing/moduleFlatbufferStreamPump.js` | `host/moduleFlatbufferStreamPump.js` |
40
+ | `toLoadableWasmBytes` | `bundle/artifactBytes.js` (`./bundle`) |
41
+
42
+ `./testing/browser` survives as `browser.js`, a pure re-export shim of those
43
+ runtime surfaces and nothing else. Both guards
44
+ (`test/browser-reachable-node-builtins.test.js`,
45
+ `test/browser-bundle-node-builtins.test.js`) enforce this with no exception
46
+ list; the second one bundles the artifact and greps it.
47
+
25
48
  ## Key Files To Read
26
49
 
27
- - `browserModuleHarness.js`
28
- - `moduleFlatbufferStreamPump.js`
50
+ - `moduleHarness.js`
51
+ - `parityHarness.js`
29
52
  - `processInvoke.js`
30
53
 
31
54
  ## Note
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `space-data-module-sdk/testing/browser` — browser-safe alias, kept honest.
3
+ *
4
+ * RULING 2026-08-07 (`orbpro-engine-bundle-ships-node-builtins`): `testing/*`
5
+ * is HARNESS surface. Production browser code must NOT reach for it to load a
6
+ * module. The two things callers actually wanted moved to runtime surfaces:
7
+ *
8
+ * toLoadableWasmBytes, ... -> space-data-module-sdk/bundle
9
+ * createBrowserModuleHarness,
10
+ * createWorkerModuleHarness,
11
+ * detectArtifactProfile,
12
+ * zeroWasmBytes -> space-data-module-sdk/host/browser-module
13
+ *
14
+ * This file exists so that subpath stays browser-SAFE for the tests and
15
+ * consumers still pointed at it: it re-exports the runtime surfaces and nothing
16
+ * else. It can never reach a Node harness again — the whole `src/testing/`
17
+ * neighbourhood is off the browser graph now, and
18
+ * test/browser-reachable-node-builtins.test.js plus
19
+ * test/browser-bundle-node-builtins.test.js enforce that at module level AND at
20
+ * bundled-artifact level, with no exception list.
21
+ *
22
+ * New browser code: import from the runtime subpaths above.
23
+ */
24
+
25
+ export {
26
+ createBrowserModuleHarness,
27
+ detectArtifactProfile,
28
+ toLoadableWasmBytes,
29
+ zeroWasmBytes,
30
+ isSharedArrayBufferLike,
31
+ } from "../host/browserModuleHarness.js";
32
+ export { createWorkerModuleHarness } from "../host/workerModuleHarness.js";
@@ -10,8 +10,8 @@ export {
10
10
  export {
11
11
  createBrowserModuleHarness,
12
12
  detectArtifactProfile,
13
- } from "./browserModuleHarness.js";
14
- export { createWorkerModuleHarness } from "./workerModuleHarness.js";
13
+ } from "../host/browserModuleHarness.js";
14
+ export { createWorkerModuleHarness } from "../host/workerModuleHarness.js";
15
15
  export {
16
16
  buildWasmEdgeSpawnEnv,
17
17
  createPluginInvokeProcessClient,
@@ -27,7 +27,7 @@ export {
27
27
  createModuleHarness,
28
28
  resolveModuleHarnessLaunchPlan,
29
29
  } from "./moduleHarness.js";
30
- export { createModuleFlatBufferStreamPump } from "./moduleFlatbufferStreamPump.js";
30
+ export { createModuleFlatBufferStreamPump } from "../host/moduleFlatbufferStreamPump.js";
31
31
  export {
32
32
  PARITY_LANES,
33
33
  ExitClass as ParityExitClass,