space-data-module-sdk 0.8.12 → 0.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/space-data-module.js +52 -0
- package/docs/module-publication-standard.md +7 -3
- package/docs/propagator-abi.md +477 -0
- package/include/orbpro/orbpro_propagator_abi.h +312 -0
- package/package.json +6 -1
- package/schemas/PluginManifest.fbs +46 -1
- package/schemas/orbpro/Propagator.fbs +161 -3
- package/src/bundle/index.js +1 -0
- package/src/bundle/sigdomain.js +22 -0
- package/src/capabilities.js +53 -1
- package/src/compliance/index.js +6 -0
- package/src/compliance/pluginCompliance.js +76 -16
- package/src/flow/flowCompiler.js +30 -14
- package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
- package/src/generated/orbpro/manifest/plugin-family.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
- package/src/generated/orbpro/propagator-abi.js +118 -0
- package/src/generated/orbpro/propagator-abi.ts +199 -0
- package/src/manifest/normalize.js +113 -4
- package/src/scaffold/copyTemplate.js +71 -0
- package/src/scaffold/index.js +150 -0
- package/src/scaffold/tokens.js +90 -0
- package/templates/propagator-module/README.md +99 -0
- package/templates/propagator-module/build.js +103 -0
- package/templates/propagator-module/package.json +19 -0
- package/templates/propagator-module/plugin-manifest.json +66 -0
- package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
- package/templates/propagator-module/tests/module.build.test.mjs +103 -0
|
@@ -15,7 +15,16 @@ import {
|
|
|
15
15
|
import { FlatBufferTypeRefT } from "../generated/orbpro/stream/flat-buffer-type-ref.js";
|
|
16
16
|
import { ProtocolRole, ProtocolTransportKind } from "../runtime/constants.js";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The manifest-string → PluginFamily vocabulary. This is the WHOLE vocabulary:
|
|
20
|
+
* anything not in here is refused by name, never coerced.
|
|
21
|
+
*
|
|
22
|
+
* `datasource` is the ONE alias, kept because manifests in the field spell it
|
|
23
|
+
* both ways. Do not add aliases casually — an alias is a second spelling of a
|
|
24
|
+
* family, and every one of them is a place two modules can disagree about what
|
|
25
|
+
* they are.
|
|
26
|
+
*/
|
|
27
|
+
export const pluginFamilyByName = Object.freeze({
|
|
19
28
|
sensor: PluginFamily.SENSOR,
|
|
20
29
|
propagator: PluginFamily.PROPAGATOR,
|
|
21
30
|
renderer: PluginFamily.RENDERER,
|
|
@@ -28,8 +37,89 @@ const pluginFamilyByName = Object.freeze({
|
|
|
28
37
|
infrastructure: PluginFamily.INFRASTRUCTURE,
|
|
29
38
|
flow: PluginFamily.FLOW,
|
|
30
39
|
bridge: PluginFamily.BRIDGE,
|
|
40
|
+
maneuver: PluginFamily.MANEUVER,
|
|
41
|
+
orbit_determination: PluginFamily.ORBIT_DETERMINATION,
|
|
42
|
+
foundation: PluginFamily.FOUNDATION,
|
|
43
|
+
parser: PluginFamily.PARSER,
|
|
44
|
+
validator: PluginFamily.VALIDATOR,
|
|
45
|
+
exporter: PluginFamily.EXPORTER,
|
|
46
|
+
publisher: PluginFamily.PUBLISHER,
|
|
47
|
+
basilisk: PluginFamily.BASILISK,
|
|
31
48
|
});
|
|
32
49
|
|
|
50
|
+
/** Every accepted family string, sorted — the vocabulary a refusal names. */
|
|
51
|
+
export const PluginFamilyNames = Object.freeze(
|
|
52
|
+
Object.keys(pluginFamilyByName).sort(),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* SDK family → authoritative SDS `pluginCategory` member name.
|
|
57
|
+
*
|
|
58
|
+
* SDS (spacedatastandards.org `schema/PLG/main.fbs`) is the source of truth for
|
|
59
|
+
* what families exist; this SDK enum is a projection of it. The two DO NOT
|
|
60
|
+
* share ordinals — they diverge from index 5 (SDK `COMMS = 5`, SDS `EW = 5`) —
|
|
61
|
+
* so this mapping is BY NAME and must stay explicit. Never convert one enum to
|
|
62
|
+
* the other numerically.
|
|
63
|
+
*
|
|
64
|
+
* Two entries have no 1:1 SDS member and are recorded honestly rather than
|
|
65
|
+
* hidden. Both are filed in graph/tasks/sds-plugin-category-projection-gaps.md:
|
|
66
|
+
* - ORBIT_DETERMINATION → Analysis (SDS has no OD category yet)
|
|
67
|
+
* - SDF, BRIDGE → Analysis / Infrastructure (SDK-local concepts)
|
|
68
|
+
*/
|
|
69
|
+
export const sdsPluginCategoryByFamily = Object.freeze({
|
|
70
|
+
[PluginFamily.SENSOR]: "Sensor",
|
|
71
|
+
[PluginFamily.PROPAGATOR]: "Propagator",
|
|
72
|
+
[PluginFamily.RENDERER]: "Renderer",
|
|
73
|
+
[PluginFamily.ANALYSIS]: "Analysis",
|
|
74
|
+
[PluginFamily.DATA_SOURCE]: "DataSource",
|
|
75
|
+
[PluginFamily.COMMS]: "Comms",
|
|
76
|
+
[PluginFamily.SHADER]: "Shader",
|
|
77
|
+
[PluginFamily.SDF]: "Analysis",
|
|
78
|
+
[PluginFamily.INFRASTRUCTURE]: "Infrastructure",
|
|
79
|
+
[PluginFamily.FLOW]: "Flow",
|
|
80
|
+
[PluginFamily.BRIDGE]: "Infrastructure",
|
|
81
|
+
[PluginFamily.MANEUVER]: "Maneuver",
|
|
82
|
+
[PluginFamily.ORBIT_DETERMINATION]: "Analysis",
|
|
83
|
+
[PluginFamily.FOUNDATION]: "Foundation",
|
|
84
|
+
[PluginFamily.PARSER]: "Parser",
|
|
85
|
+
[PluginFamily.VALIDATOR]: "Validator",
|
|
86
|
+
[PluginFamily.EXPORTER]: "Exporter",
|
|
87
|
+
[PluginFamily.PUBLISHER]: "Publisher",
|
|
88
|
+
[PluginFamily.BASILISK]: "Basilisk",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const PluginFamilyValues = Object.freeze(
|
|
92
|
+
new Set(Object.values(pluginFamilyByName)),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Thrown when a manifest declares a family the SDK does not know.
|
|
97
|
+
*
|
|
98
|
+
* It is a THROW and not a fallback on purpose. The old code returned
|
|
99
|
+
* `PluginFamily.ANALYSIS` for anything unrecognized, which is why 19
|
|
100
|
+
* first-party modules shipped mislabelled and family-typed resolution only
|
|
101
|
+
* ever worked for propagators: a typo and a deliberate new family were
|
|
102
|
+
* indistinguishable, and both were silent.
|
|
103
|
+
*
|
|
104
|
+
* Ruling: graph/findings/official-harness-shapes.md §4.7 / §8.3
|
|
105
|
+
*/
|
|
106
|
+
export class UnknownPluginFamilyError extends Error {
|
|
107
|
+
constructor(value) {
|
|
108
|
+
super(
|
|
109
|
+
`Unknown pluginFamily ${JSON.stringify(value)}. ` +
|
|
110
|
+
`The manifest vocabulary is: ${PluginFamilyNames.join(", ")}. ` +
|
|
111
|
+
`Families are NOT invented in a manifest — the SDK enum ` +
|
|
112
|
+
`(schemas/PluginManifest.fbs PluginFamily) is a projection of the ` +
|
|
113
|
+
`authoritative SDS pluginCategory vocabulary, and a new family is ` +
|
|
114
|
+
`appended there first.`,
|
|
115
|
+
);
|
|
116
|
+
this.name = "UnknownPluginFamilyError";
|
|
117
|
+
this.code = "unknown-plugin-family";
|
|
118
|
+
this.value = value;
|
|
119
|
+
this.validFamilies = PluginFamilyNames;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
33
123
|
const drainPolicyByName = Object.freeze({
|
|
34
124
|
"single-shot": ManifestDrainPolicy.SINGLE_SHOT,
|
|
35
125
|
"drain-until-yield": ManifestDrainPolicy.DRAIN_UNTIL_YIELD,
|
|
@@ -145,14 +235,33 @@ function normalizeUnsignedInteger(value, fallback = 0) {
|
|
|
145
235
|
return Math.max(0, Math.trunc(normalized));
|
|
146
236
|
}
|
|
147
237
|
|
|
148
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Resolve a manifest `pluginFamily` to its enum value, or REFUSE.
|
|
240
|
+
*
|
|
241
|
+
* There is no fallback. See {@link UnknownPluginFamilyError} for why.
|
|
242
|
+
*
|
|
243
|
+
* @param {string|number} value
|
|
244
|
+
* @returns {number} a PluginFamily member
|
|
245
|
+
* @throws {UnknownPluginFamilyError} on an unknown string, an out-of-range
|
|
246
|
+
* number, or a missing/blank value.
|
|
247
|
+
*/
|
|
248
|
+
export function normalizePluginFamily(value) {
|
|
149
249
|
if (typeof value === "number") {
|
|
250
|
+
// A numeric family still has to BE one. An out-of-range ordinal is the
|
|
251
|
+
// same defect as an unknown string, and used to sail straight through.
|
|
252
|
+
if (!PluginFamilyValues.has(value)) {
|
|
253
|
+
throw new UnknownPluginFamilyError(value);
|
|
254
|
+
}
|
|
150
255
|
return value;
|
|
151
256
|
}
|
|
152
|
-
const normalized = String(value ?? "
|
|
257
|
+
const normalized = String(value ?? "")
|
|
153
258
|
.trim()
|
|
154
259
|
.toLowerCase();
|
|
155
|
-
|
|
260
|
+
const resolved = pluginFamilyByName[normalized];
|
|
261
|
+
if (resolved === undefined) {
|
|
262
|
+
throw new UnknownPluginFamilyError(value);
|
|
263
|
+
}
|
|
264
|
+
return resolved;
|
|
156
265
|
}
|
|
157
266
|
|
|
158
267
|
function normalizeDrainPolicy(value) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { substituteTokens } from "./tokens.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Refuse to scaffold into a non-empty directory unless `force` is set. Never
|
|
8
|
+
* deletes anything — `force` only lifts the refusal, it does not clear the
|
|
9
|
+
* directory first, so pre-existing unrelated files are left alone and
|
|
10
|
+
* template files land on top of (overwrite) any same-named files.
|
|
11
|
+
*/
|
|
12
|
+
export async function ensureWritableOutputDir(outDir, force) {
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = await fs.readdir(outDir);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error && error.code === "ENOENT") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
if (entries.length > 0 && !force) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Refusing to scaffold into non-empty directory ${outDir} ` +
|
|
25
|
+
`(${entries.length} existing ${entries.length === 1 ? "entry" : "entries"}). ` +
|
|
26
|
+
`Pass --force to scaffold into it anyway.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copy every file under `templateDir` into `outDir`, applying token
|
|
33
|
+
* substitution to BOTH file contents and file/directory names. Every
|
|
34
|
+
* template file is treated as UTF-8 text — correct for this SDK's templates
|
|
35
|
+
* (JSON/JS/C/C++/Markdown), and deliberate: a template that ever needs a
|
|
36
|
+
* binary asset is a signal to reconsider, not something this copier should
|
|
37
|
+
* silently support.
|
|
38
|
+
*
|
|
39
|
+
* Returns the sorted list of output-relative (posix-style) file paths that
|
|
40
|
+
* were written.
|
|
41
|
+
*/
|
|
42
|
+
export async function copyTemplateTree(templateDir, outDir, tokens) {
|
|
43
|
+
const created = [];
|
|
44
|
+
|
|
45
|
+
async function walk(currentTemplateDir, currentOutDir) {
|
|
46
|
+
const entries = await fs.readdir(currentTemplateDir, {
|
|
47
|
+
withFileTypes: true,
|
|
48
|
+
});
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const destName = substituteTokens(entry.name, tokens);
|
|
51
|
+
const srcPath = path.join(currentTemplateDir, entry.name);
|
|
52
|
+
const destPath = path.join(currentOutDir, destName);
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
55
|
+
await walk(srcPath, destPath);
|
|
56
|
+
} else if (entry.isFile()) {
|
|
57
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
58
|
+
const raw = await fs.readFile(srcPath, "utf8");
|
|
59
|
+
await fs.writeFile(destPath, substituteTokens(raw, tokens), "utf8");
|
|
60
|
+
created.push(
|
|
61
|
+
path.relative(outDir, destPath).split(path.sep).join("/"),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
68
|
+
await walk(templateDir, outDir);
|
|
69
|
+
created.sort();
|
|
70
|
+
return created;
|
|
71
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `space-data-module init` — scaffold a new SDN WASM module skeleton from a
|
|
3
|
+
* family template under `templates/<family>-module/`.
|
|
4
|
+
*
|
|
5
|
+
* Family resolution deliberately mirrors `normalizePluginFamily`
|
|
6
|
+
* (`src/manifest/normalize.js`): an unrecognized value is a THROW, never a
|
|
7
|
+
* silent fallback to a generic template. Two checks apply, in order:
|
|
8
|
+
*
|
|
9
|
+
* 1. Is `--family` even a name in the SDK's plugin-family vocabulary
|
|
10
|
+
* (the same vocabulary `pluginFamily` in a manifest is validated
|
|
11
|
+
* against)? If not, `normalizePluginFamily` itself throws
|
|
12
|
+
* `UnknownPluginFamilyError`, naming the offender and the whole
|
|
13
|
+
* vocabulary.
|
|
14
|
+
* 2. Does that family have an init TEMPLATE (`templates/<family>-module/
|
|
15
|
+
* plugin-manifest.json`)? Most families do not — only `propagator`
|
|
16
|
+
* ships one today. If not, `ScaffoldFamilyTemplateError` names the
|
|
17
|
+
* offender and lists the families that DO have a template.
|
|
18
|
+
*
|
|
19
|
+
* Both refusals name the value and the valid vocabulary; neither ever
|
|
20
|
+
* degrades into "pick the closest template" or "use a blank template".
|
|
21
|
+
*/
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
import { normalizePluginFamily } from "../manifest/normalize.js";
|
|
27
|
+
import { copyTemplateTree, ensureWritableOutputDir } from "./copyTemplate.js";
|
|
28
|
+
import { buildTokenMap } from "./tokens.js";
|
|
29
|
+
|
|
30
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
export const packageRoot = path.resolve(moduleDir, "..", "..");
|
|
32
|
+
export const templatesRoot = path.join(packageRoot, "templates");
|
|
33
|
+
|
|
34
|
+
const TEMPLATE_DIR_SUFFIX = "-module";
|
|
35
|
+
/** The file every real init-scaffold template must have at its root. This is
|
|
36
|
+
* what distinguishes a scaffold template (propagator-module) from the
|
|
37
|
+
* pre-existing ABI-header-only reference dirs under templates/
|
|
38
|
+
* (gpu-module, provider-access-module) that ship no plugin-manifest.json and
|
|
39
|
+
* are not meant to be scaffolded from. */
|
|
40
|
+
const TEMPLATE_MANIFEST_FILE = "plugin-manifest.json";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Thrown when `--family` names a value that does not have an init template,
|
|
44
|
+
* even if it IS a recognized `pluginFamily` string. Distinct from
|
|
45
|
+
* `UnknownPluginFamilyError` (which fires first, for values that are not a
|
|
46
|
+
* recognized family AT ALL) — this one fires for real-but-template-less
|
|
47
|
+
* families such as "sensor" today.
|
|
48
|
+
*/
|
|
49
|
+
export class ScaffoldFamilyTemplateError extends Error {
|
|
50
|
+
constructor(value, availableFamilies) {
|
|
51
|
+
const list =
|
|
52
|
+
availableFamilies.length > 0
|
|
53
|
+
? availableFamilies.join(", ")
|
|
54
|
+
: "(none — no templates/*-module directory has a plugin-manifest.json yet)";
|
|
55
|
+
super(
|
|
56
|
+
`No init template for family ${JSON.stringify(value)}. ` +
|
|
57
|
+
`Families with an init template: ${list}.`,
|
|
58
|
+
);
|
|
59
|
+
this.name = "ScaffoldFamilyTemplateError";
|
|
60
|
+
this.code = "unknown-scaffold-family-template";
|
|
61
|
+
this.value = value;
|
|
62
|
+
this.availableFamilies = availableFamilies;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** List family names (e.g. `["propagator"]`) that have a real init template. */
|
|
67
|
+
export function listScaffoldFamilies() {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(templatesRoot, { withFileTypes: true });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error && error.code === "ENOENT") {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const families = [];
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory() || !entry.name.endsWith(TEMPLATE_DIR_SUFFIX)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const manifestPath = path.join(
|
|
83
|
+
templatesRoot,
|
|
84
|
+
entry.name,
|
|
85
|
+
TEMPLATE_MANIFEST_FILE,
|
|
86
|
+
);
|
|
87
|
+
if (fs.existsSync(manifestPath)) {
|
|
88
|
+
families.push(entry.name.slice(0, -TEMPLATE_DIR_SUFFIX.length));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return families.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve `--family` to a template directory, or throw loudly. See the
|
|
96
|
+
* module docstring for the two-stage refusal shape.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveScaffoldTemplateDir(family) {
|
|
99
|
+
// Stage 1: is this even a real plugin family? Throws UnknownPluginFamilyError
|
|
100
|
+
// (naming the value + the FULL manifest vocabulary) if not.
|
|
101
|
+
normalizePluginFamily(family);
|
|
102
|
+
|
|
103
|
+
const normalized = String(family).trim().toLowerCase();
|
|
104
|
+
const available = listScaffoldFamilies();
|
|
105
|
+
if (!available.includes(normalized)) {
|
|
106
|
+
// Stage 2: real family, but no init template ships for it (yet).
|
|
107
|
+
throw new ScaffoldFamilyTemplateError(family, available);
|
|
108
|
+
}
|
|
109
|
+
return path.join(templatesRoot, `${normalized}${TEMPLATE_DIR_SUFFIX}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Scaffold a new module skeleton.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} options
|
|
116
|
+
* @param {string} options.family - e.g. "propagator". Must have an init
|
|
117
|
+
* template (see {@link resolveScaffoldTemplateDir}).
|
|
118
|
+
* @param {string} options.name - kebab-case module name, e.g. "my-propagator".
|
|
119
|
+
* @param {string} [options.outDir] - defaults to `./<name>` under cwd.
|
|
120
|
+
* @param {string} [options.pluginId] - defaults to
|
|
121
|
+
* `com.orbpro.<name-with-dots>`.
|
|
122
|
+
* @param {boolean} [options.force] - allow scaffolding into a non-empty
|
|
123
|
+
* `outDir`.
|
|
124
|
+
* @returns {Promise<{ok: true, family: string, name: string, pluginId: string, outDir: string, files: string[]}>}
|
|
125
|
+
*/
|
|
126
|
+
export async function scaffoldModule({
|
|
127
|
+
family,
|
|
128
|
+
name,
|
|
129
|
+
outDir,
|
|
130
|
+
pluginId,
|
|
131
|
+
force = false,
|
|
132
|
+
} = {}) {
|
|
133
|
+
const templateDir = resolveScaffoldTemplateDir(family);
|
|
134
|
+
const tokens = buildTokenMap({ name, pluginId });
|
|
135
|
+
const resolvedOutDir = path.resolve(outDir ?? path.join(process.cwd(), name));
|
|
136
|
+
|
|
137
|
+
await ensureWritableOutputDir(resolvedOutDir, force === true);
|
|
138
|
+
const files = await copyTemplateTree(templateDir, resolvedOutDir, tokens);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
family: String(family).trim().toLowerCase(),
|
|
143
|
+
name,
|
|
144
|
+
pluginId: tokens.PLUGIN_ID,
|
|
145
|
+
outDir: resolvedOutDir,
|
|
146
|
+
files,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { defaultPluginId } from "./tokens.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token vocabulary for `space-data-module init`.
|
|
3
|
+
*
|
|
4
|
+
* These are the ONLY tokens the scaffold engine substitutes. They are matched
|
|
5
|
+
* as whole `__NAME__` runs so a token can never partially match inside a
|
|
6
|
+
* longer identifier that happens to share a prefix.
|
|
7
|
+
*/
|
|
8
|
+
export const SCAFFOLD_TOKEN_NAMES = Object.freeze([
|
|
9
|
+
"MODULE_NAME",
|
|
10
|
+
"PLUGIN_ID",
|
|
11
|
+
"MODULE_NAME_SNAKE",
|
|
12
|
+
"MODULE_NAME_CAMEL",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const TOKEN_PATTERN = new RegExp(
|
|
16
|
+
`__(${SCAFFOLD_TOKEN_NAMES.join("|")})__`,
|
|
17
|
+
"g",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const MODULE_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate a `--name` value against the kebab-case shape every derived token
|
|
24
|
+
* assumes. Refuses loudly rather than silently mangling an unexpected name
|
|
25
|
+
* into something that merely looks plausible.
|
|
26
|
+
*/
|
|
27
|
+
export function assertValidModuleName(name) {
|
|
28
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
29
|
+
throw new Error("--name is required and must be a non-empty string.");
|
|
30
|
+
}
|
|
31
|
+
if (!MODULE_NAME_PATTERN.test(name)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`--name ${JSON.stringify(name)} is not valid. Module names must be ` +
|
|
34
|
+
`lowercase kebab-case: start with a letter, then letters, digits, or ` +
|
|
35
|
+
`single hyphens (e.g. "keplerian-reference").`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `foo-bar` -> `foo_bar` */
|
|
41
|
+
export function toSnakeCase(name) {
|
|
42
|
+
return name.replace(/-/g, "_");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `foo-bar` -> `fooBar` */
|
|
46
|
+
export function toCamelCase(name) {
|
|
47
|
+
return name
|
|
48
|
+
.split(/[-_]+/)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map((part, index) =>
|
|
51
|
+
index === 0
|
|
52
|
+
? part.toLowerCase()
|
|
53
|
+
: part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),
|
|
54
|
+
)
|
|
55
|
+
.join("");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `com.orbpro.<module-name-with-dots>` — the documented default `--plugin-id`
|
|
60
|
+
* when the author does not supply one. Hyphens become dots so the plugin id
|
|
61
|
+
* reads as reverse-DNS-style segments, matching how the SDK's own examples
|
|
62
|
+
* namespace multi-word module ids.
|
|
63
|
+
*/
|
|
64
|
+
export function defaultPluginId(name) {
|
|
65
|
+
return `com.orbpro.${name.replace(/-/g, ".")}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the full token -> replacement-string map for one scaffold run.
|
|
70
|
+
*/
|
|
71
|
+
export function buildTokenMap({ name, pluginId }) {
|
|
72
|
+
assertValidModuleName(name);
|
|
73
|
+
const resolvedPluginId =
|
|
74
|
+
typeof pluginId === "string" && pluginId.trim().length > 0
|
|
75
|
+
? pluginId.trim()
|
|
76
|
+
: defaultPluginId(name);
|
|
77
|
+
return {
|
|
78
|
+
MODULE_NAME: name,
|
|
79
|
+
PLUGIN_ID: resolvedPluginId,
|
|
80
|
+
MODULE_NAME_SNAKE: toSnakeCase(name),
|
|
81
|
+
MODULE_NAME_CAMEL: toCamelCase(name),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Substitute every `__TOKEN__` occurrence in `text` using `tokens`. */
|
|
86
|
+
export function substituteTokens(text, tokens) {
|
|
87
|
+
return text.replace(TOKEN_PATTERN, (match, key) =>
|
|
88
|
+
key in tokens ? tokens[key] : match,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# __MODULE_NAME__
|
|
2
|
+
|
|
3
|
+
Scaffolded by `space-data-module init --family propagator --name __MODULE_NAME__`
|
|
4
|
+
from space-data-module-sdk's `templates/propagator-module/` template.
|
|
5
|
+
|
|
6
|
+
This is a **minimal but building** skeleton of an SDN propagator module: every
|
|
7
|
+
ABI obligation is already implemented — exports, wire layout, units, frames,
|
|
8
|
+
identity, threading discipline, error codes, lifetime — and the orbital
|
|
9
|
+
mechanics are a placeholder. It compiles and passes the SDK's own compliance
|
|
10
|
+
checks as-is; it just doesn't propagate anything real yet.
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
- `plugin-manifest.json` — the module manifest. `pluginId` defaults to
|
|
15
|
+
`com.orbpro.<name-with-dots>`; `pluginFamily` is `propagator`; declares one
|
|
16
|
+
invoke method (`ingest_omm`) plus the propagator ABI exports below.
|
|
17
|
+
- `src/__MODULE_NAME_SNAKE__.cpp` — the module source. Search for
|
|
18
|
+
`TODO: your propagation goes here` — there are two spots (element adoption
|
|
19
|
+
in `adopt_omm()`, and the actual propagation in `propagate_entity()`).
|
|
20
|
+
Everything else in the file is ABI plumbing; you should not need to touch
|
|
21
|
+
export names, signatures, error codes, or the state-vector write pattern.
|
|
22
|
+
- `build.js` — compiles through `compileModuleFromSource` (the SDK compiler
|
|
23
|
+
lane). Inlines the generated `orbpro_propagator_abi.h` from your pinned
|
|
24
|
+
`space-data-module-sdk` dependency — never hand-copy that header.
|
|
25
|
+
- `tests/module.build.test.mjs` — manifest shape check (always runs) plus
|
|
26
|
+
compliance/export checks that skip until you've built the module.
|
|
27
|
+
- `package.json` — `"sdn-module"` points at the canonical isomorphic
|
|
28
|
+
artifact; `space-data-module-sdk` is a normal npm dependency.
|
|
29
|
+
|
|
30
|
+
## Naming
|
|
31
|
+
|
|
32
|
+
This module was scaffolded with `--name __MODULE_NAME__`. `space-data-module
|
|
33
|
+
init` substituted four spellings of that name into this tree; if you need to
|
|
34
|
+
introduce your own file or identifier later, reuse the same shapes rather
|
|
35
|
+
than inventing a fifth:
|
|
36
|
+
|
|
37
|
+
| Spelling | This module's value | Used for |
|
|
38
|
+
| --------------------- | -------------------- | ------------------------------------------ |
|
|
39
|
+
| kebab-case | `__MODULE_NAME__` | display text, kebab-case filenames |
|
|
40
|
+
| reverse-DNS plugin id | `__PLUGIN_ID__` | `plugin-manifest.json`'s `pluginId` |
|
|
41
|
+
| snake_case | `__MODULE_NAME_SNAKE__` | C/C++ file and symbol names |
|
|
42
|
+
| camelCase | `__MODULE_NAME_CAMEL__` | a JS-safe identifier (e.g. a bindings key) |
|
|
43
|
+
|
|
44
|
+
## Next steps
|
|
45
|
+
|
|
46
|
+
1. `npm install` (pulls `space-data-module-sdk` and its `spacedatastandards.org`
|
|
47
|
+
dependency).
|
|
48
|
+
2. Fill in the physics: replace the two `TODO: your propagation goes here`
|
|
49
|
+
blocks in `src/__MODULE_NAME_SNAKE__.cpp`. Keep every export, error code,
|
|
50
|
+
and the "zero the struct, set frame explicitly, set VALID last" write
|
|
51
|
+
pattern — those are the ABI contract, not style.
|
|
52
|
+
3. `npm run build` — writes `dist/isomorphic/module.wasm` +
|
|
53
|
+
`dist/plugin-manifest.json`. The build fails loudly if the compiled
|
|
54
|
+
artifact does not pass the SDK's own manifest/artifact validation.
|
|
55
|
+
4. `npm test` — the manifest-shape test always runs; the compliance and
|
|
56
|
+
export-surface tests turn on once step 3 has produced a wasm artifact.
|
|
57
|
+
5. Replace the TODO test at the bottom of `tests/module.build.test.mjs` with
|
|
58
|
+
a real assertion once you have physics to check (ingest a known OMM,
|
|
59
|
+
propagate to a known epoch, compare against an independent reference —
|
|
60
|
+
e.g. another propagator or published ephemeris).
|
|
61
|
+
6. Update `description` in `plugin-manifest.json` and `package.json` — both
|
|
62
|
+
still say "TODO" / a generic scaffold description.
|
|
63
|
+
|
|
64
|
+
## The ABI, in one paragraph
|
|
65
|
+
|
|
66
|
+
A propagator module exports `plugin_init`, `plugin_init_omm`,
|
|
67
|
+
`plugin_ingest_omm_one`, `plugin_propagate`, `plugin_propagate_batch`,
|
|
68
|
+
`plugin_entity_count`, and `plugin_destroy` against the generated
|
|
69
|
+
`OrbProStateVector` / `OrbProOMMRecord` / `OrbProOrbitalElements` structs
|
|
70
|
+
(`orbpro/orbpro_propagator_abi.h` in your pinned `space-data-module-sdk`,
|
|
71
|
+
generated from `schemas/orbpro/Propagator.fbs` — never hand-retype these
|
|
72
|
+
structs, that is the exact drift this generated header exists to end).
|
|
73
|
+
Position/velocity output is always METERS / METERS-PER-SECOND with an
|
|
74
|
+
explicit `reference_frame`; identity is carried by `NORAD_CAT_ID`, never
|
|
75
|
+
derived from array position; every failure returns a named negative code;
|
|
76
|
+
`plugin_destroy` must actually free, not no-op.
|
|
77
|
+
|
|
78
|
+
**Threading.** This module declares `threadModel: "wasi-sequential"` — it
|
|
79
|
+
never spawns a thread of its own, which is the *strong default* for a
|
|
80
|
+
propagator: propagation is embarrassingly parallel across entities but
|
|
81
|
+
sequential within one, and the ABI puts the sharding decision on the HOST
|
|
82
|
+
(e.g. a frame-worker pool), not the module. `build.js` passes
|
|
83
|
+
`threadModel: manifest.threadModel` to the compiler EXPLICITLY — do not
|
|
84
|
+
remove that. `resolveThreadModel` reads the compile option, not
|
|
85
|
+
`manifest.threadModel`, and otherwise infers the model from
|
|
86
|
+
`runtimeTargets`, where `"wasmedge"` infers the OTHER model
|
|
87
|
+
(`emscripten-pthreads`, which in this SDK means the clang
|
|
88
|
+
`wasm32-wasip1-threads` / wasi-threads contract — never `emcc -pthread`,
|
|
89
|
+
which cannot thread under WasmEdge at all). Passing `threadModel` in
|
|
90
|
+
`build.js` sidesteps that inference and is what keeps this manifest's
|
|
91
|
+
declared model and the compiled artifact in agreement — `build.js` also
|
|
92
|
+
asserts they agree after compiling. See `docs/propagator-abi.md`
|
|
93
|
+
"Threading" if you ever need the other model.
|
|
94
|
+
|
|
95
|
+
The one SDN invoke method, `ingest_omm`, is separate from the propagator ABI
|
|
96
|
+
exports above: it is how a flow graph feeds this module SDS `$OMM` records
|
|
97
|
+
over the generic invoke surface, while `plugin_propagate` /
|
|
98
|
+
`plugin_propagate_batch` are called directly by a host that has already
|
|
99
|
+
linked this module as a propagator.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build __MODULE_NAME__ through the SDK compiler lane.
|
|
3
|
+
*
|
|
4
|
+
* `compileModuleFromSource` takes ONE translation unit, so the generated
|
|
5
|
+
* OrbPro propagator ABI header is INLINED into the source before compiling.
|
|
6
|
+
* That inlining is mechanical and one-directional: the header is read from
|
|
7
|
+
* the pinned `space-data-module-sdk` package, never copied into this repo.
|
|
8
|
+
* If the SDK's ABI changes, this build picks it up on the next `npm run
|
|
9
|
+
* build` — do not hand-vendor a copy of the header beside this file.
|
|
10
|
+
*
|
|
11
|
+
* Thread model: the manifest declares `threadModel: "wasi-sequential"` (see
|
|
12
|
+
* plugin-manifest.json's `sequentialJustification`) — this module never
|
|
13
|
+
* spawns a thread, which is the strong default for a propagator (sharding a
|
|
14
|
+
* batch belongs to the HOST, not the module; see docs/propagator-abi.md
|
|
15
|
+
* "Threading"). Both `wasi-sequential` and the threaded `emscripten-pthreads`
|
|
16
|
+
* model compile through the SAME clang `wasm32-wasip1-threads` toolchain —
|
|
17
|
+
* never `emcc -pthread`, which is browser-only and cannot thread under
|
|
18
|
+
* WasmEdge — they differ only in which link-time contract the SDK's
|
|
19
|
+
* post-link artifact guard then validates against the emitted wasm.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from "node:fs/promises";
|
|
23
|
+
import { createRequire } from "node:module";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
import { compileModuleFromSource } from "space-data-module-sdk/compiler";
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const packageRoot = fileURLToPath(new URL(".", import.meta.url));
|
|
31
|
+
const manifestPath = path.join(packageRoot, "plugin-manifest.json");
|
|
32
|
+
const sourcePath = path.join(packageRoot, "src", "__MODULE_NAME_SNAKE__.cpp");
|
|
33
|
+
const distRoot = path.join(packageRoot, "dist");
|
|
34
|
+
const outputPath = path.join(distRoot, "isomorphic", "module.wasm");
|
|
35
|
+
|
|
36
|
+
const standardsRoot = path.dirname(
|
|
37
|
+
require.resolve("spacedatastandards.org/package.json"),
|
|
38
|
+
);
|
|
39
|
+
process.env.SPACE_DATA_STANDARDS_ROOT ??= `${standardsRoot}${path.sep}`;
|
|
40
|
+
|
|
41
|
+
/** The ONE source of the ABI, resolved from the pinned SDK package. */
|
|
42
|
+
const abiHeaderPath = require.resolve(
|
|
43
|
+
"space-data-module-sdk/include/orbpro/orbpro_propagator_abi.h",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
47
|
+
const abiHeader = await fs.readFile(abiHeaderPath, "utf8");
|
|
48
|
+
const rawSource = await fs.readFile(sourcePath, "utf8");
|
|
49
|
+
|
|
50
|
+
const INCLUDE_LINE = '#include "orbpro/orbpro_propagator_abi.h"';
|
|
51
|
+
if (!rawSource.includes(INCLUDE_LINE)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${path.relative(packageRoot, sourcePath)} no longer includes the generated ABI header. ` +
|
|
54
|
+
`A propagator module must build against the ONE generated ABI, not a local copy.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sourceCode = rawSource.replace(
|
|
59
|
+
INCLUDE_LINE,
|
|
60
|
+
[
|
|
61
|
+
`// --- BEGIN INLINED ${path.basename(abiHeaderPath)} (from ${manifest.pluginId}'s pinned SDK) ---`,
|
|
62
|
+
abiHeader,
|
|
63
|
+
`// --- END INLINED ${path.basename(abiHeaderPath)} ---`,
|
|
64
|
+
].join("\n"),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await fs.rm(distRoot, { recursive: true, force: true });
|
|
68
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
69
|
+
|
|
70
|
+
const compilation = await compileModuleFromSource({
|
|
71
|
+
manifest,
|
|
72
|
+
sourceCode,
|
|
73
|
+
language: "c++",
|
|
74
|
+
outputPath,
|
|
75
|
+
// PASSED EXPLICITLY ON PURPOSE. `resolveThreadModel` reads the compile
|
|
76
|
+
// OPTION, not `manifest.threadModel`, and otherwise infers the model from
|
|
77
|
+
// `runtimeTargets` — where "wasmedge" infers pthreads. A manifest that
|
|
78
|
+
// declares `wasi-sequential` and does not pass it here would be silently
|
|
79
|
+
// compiled under the OTHER model and then rejected by the post-link
|
|
80
|
+
// artifact guard for not spawning a thread it never claimed to spawn.
|
|
81
|
+
// Filed as `sdk-manifest-threadmodel-silently-ignored`; keep this explicit
|
|
82
|
+
// until that lands.
|
|
83
|
+
threadModel: manifest.threadModel,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (compilation.threadModel !== manifest.threadModel) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`threadModel drift: the manifest declares ${manifest.threadModel} but the ` +
|
|
89
|
+
`compiler resolved ${compilation.threadModel}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await fs.copyFile(manifestPath, path.join(distRoot, "plugin-manifest.json"));
|
|
94
|
+
|
|
95
|
+
if (!compilation.report?.ok) {
|
|
96
|
+
const issues = JSON.stringify(compilation.report?.issues ?? [], null, 2);
|
|
97
|
+
throw new Error(`Compiled __MODULE_NAME__ artifact failed SDK validation:\n${issues}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(
|
|
101
|
+
`Built ${path.relative(packageRoot, outputPath)} ` +
|
|
102
|
+
`(${compilation.compiler}, threadModel=${compilation.threadModel})`,
|
|
103
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "space-data-network-module-propagator-__MODULE_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__MODULE_NAME__ — an SDN propagator module scaffolded from the space-data-module-sdk propagator-module template.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sdn-module": "./dist/isomorphic/module.wasm",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./plugin-manifest.json": "./plugin-manifest.json",
|
|
9
|
+
"./dist/*": "./dist/*"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "node build.js",
|
|
13
|
+
"test": "node --test tests/*.test.mjs"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"space-data-module-sdk": "^0.8.12"
|
|
17
|
+
},
|
|
18
|
+
"private": true
|
|
19
|
+
}
|