@kadabra-rs/spec-mcp 0.0.0-stage → 0.2.1

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/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # @kadabra-rs/spec-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Self-contained [MCP](https://modelcontextprotocol.io) server for the **OMG KerML 1.0** and
4
+ **SysML v2** specifications — an authoritative oracle agents consult *before* writing model
5
+ code, so syntax and semantics come from the spec instead of being invented.
6
+
7
+ Everything is embedded in a single binary (no repo checkout needed at runtime):
8
+
9
+ - full clause text of both specs (Markdown conversions of the OMG PDFs)
10
+ - the official textual-notation grammars (`.kebnf`)
11
+ - every spec figure/diagram as an inline image
12
+ - the OMG worked-example model corpus (KerML examples + SysML training/validation models)
13
+
14
+ ## Run with npx / bunx
15
+
16
+ ```bash
17
+ npx -y @kadabra-rs/spec-mcp # serve over stdio (downloads + SHA-256 verifies the binary)
18
+ bunx @kadabra-rs/spec-mcp # same under bun
19
+ npx -y @kadabra-rs/spec-mcp install # prefetch the binary only
20
+ npx -y @kadabra-rs/spec-mcp path # print the cached binary path
21
+ ```
22
+
23
+ MCP client config (`.mcp.json`, `.omp/mcp.json`, Claude Desktop, …):
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "spec": { "command": "npx", "args": ["-y", "@kadabra-rs/spec-mcp"] }
29
+ }
30
+ }
31
+ ```
32
+
33
+ First run caches the platform binary under `~/.cache/spec-mcp/<version>-<platform>/`;
34
+ subsequent runs start instantly.
35
+
36
+ ## Tools
37
+
38
+ Every tool takes `spec: "kerml" | "sysml"`.
39
+
40
+ | Tool | Purpose |
41
+ | --- | --- |
42
+ | `search_sections` | full-text clause search (title hits first, body hits with snippets) |
43
+ | `get_section` | clause body as Markdown + defined BNF productions + figures (images inline) |
44
+ | `get_syntax` | official `.kebnf` production bodies by name, with defining clause |
45
+ | `get_figure` | one captioned diagram by printed figure number, inline |
46
+ | `list_examples` | index of the official worked-example model corpus |
47
+ | `get_example` | full source of one example `.kerml`/`.sysml` file |
48
+ | `follow_link` | resolve `#anchor` cross-references between clauses |
49
+ | `spec_toc` | nested clause outline (chapters → sections → clauses), optional max_depth |
50
+ | `find_implementation` | map a clause to compiler code (only inside the forge repo) |
51
+
52
+ ## Bundled agent skill
53
+
54
+ The package ships a `sysml-modeling` skill teaching agents the two-surface workflow
55
+ (**spec-mcp = documentation oracle**, **spec42 CLI = validator**): ground every construct in a
56
+ BNF production / clause, learn shape from the official examples, then validate with
57
+ `spec42 check`.
58
+
59
+ ```bash
60
+ npx -y @kadabra-rs/spec-mcp skill install # -> ~/.omp/agent/skills/sysml-modeling
61
+ npx -y @kadabra-rs/spec-mcp skill install --agent all # omp + claude + codex + agents dirs
62
+ npx -y @kadabra-rs/spec-mcp skill install --project # -> ./.omp/skills (commit it)
63
+ npx -y @kadabra-rs/spec-mcp skill remove
64
+ ```
65
+
66
+ ## Environment
67
+
68
+ | Variable | Effect |
69
+ | --- | --- |
70
+ | `SPEC_MCP_BINARY` | use a prebuilt binary, skip download |
71
+ | `SPEC_MCP_VERSION` | override the release version |
72
+ | `SPEC_MCP_BASE_URL` | override the download base URL (mirrors/tests) |
73
+ | `SPEC_MCP_CACHE_DIR` | override the cache directory |
74
+ | `SPEC_MCP_CRATES_DIR` | passed to the server for `find_implementation` |
75
+
76
+ Binaries are released on [GitHub](https://github.com/kadabra-rs/forge/releases) with
77
+ `SHA256SUMS.txt`; the launcher refuses unverified downloads.
78
+
79
+ ## License
80
+
81
+ AGPL-3.0-only (same as the spec-mcp source in
82
+ [`kadabra-rs/forge`](https://github.com/kadabra-rs/forge/tree/main/tools/spec-mcp)).
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+ // spec-mcp launcher: resolves the self-contained spec MCP binary for this
3
+ // platform (download + SHA-256 verify + cache, or SPEC_MCP_BINARY), runs it
4
+ // over stdio, and can install/remove the bundled `sysml-modeling` agent skill.
5
+ //
6
+ // npx @kadabra-rs/spec-mcp # serve MCP on stdio
7
+ // npx @kadabra-rs/spec-mcp install # fetch binary only
8
+ // npx @kadabra-rs/spec-mcp skill install # skill -> agent dirs (user-level)
9
+ // npx @kadabra-rs/spec-mcp skill install --project --agent omp
10
+ // npx @kadabra-rs/spec-mcp skill remove
11
+ // npx @kadabra-rs/spec-mcp path # print binary path
12
+ //
13
+ // Environment:
14
+ // SPEC_MCP_BINARY absolute path to a prebuilt spec-mcp (skips download)
15
+ // SPEC_MCP_VERSION override the release tag version (e.g. 0.2.0)
16
+ // SPEC_MCP_BASE_URL override the download base URL (mirrors/tests)
17
+ // SPEC_MCP_CACHE_DIR override the cache dir (default ~/.cache/spec-mcp)
18
+
19
+ import { spawn } from "node:child_process";
20
+ import { createHash } from "node:crypto";
21
+ import {
22
+ existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync,
23
+ realpathSync, renameSync,
24
+ } from "node:fs";
25
+ import { dirname, join } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ import { homedir } from "node:os";
28
+
29
+ const HERE = dirname(fileURLToPath(import.meta.url));
30
+ const PKG = JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8"));
31
+ const SKILL_NAME = "sysml-modeling";
32
+ const GITHUB_REPO = "kadabra-rs/forge";
33
+
34
+ const err = (msg) => process.stderr.write(`${msg}\n`);
35
+ const fail = (msg) => { err(`spec-mcp: ${msg}`); process.exit(1); };
36
+
37
+ export function detectPlatform() {
38
+ const arch = process.arch === "arm64" ? "arm64"
39
+ : process.arch === "x64" ? "x64" : null;
40
+ const os = process.platform === "darwin" ? "darwin"
41
+ : process.platform === "linux" ? "linux" : null;
42
+ if (!os || !arch) {
43
+ fail(`no prebuilt spec-mcp binary for ${process.platform}/${process.arch}; \
44
+ build it (cargo build --release -p spec-mcp in the forge repo, tools/spec-mcp) \
45
+ and set SPEC_MCP_BINARY, or run: cargo install --git https://github.com/${GITHUB_REPO} --bin spec-mcp`);
46
+ }
47
+ // Intel macOS is not released: the semantic engine's onnxruntime
48
+ // (ort-sys 2.0.0-rc.13) publishes no x86_64-apple-darwin prebuilt, so the
49
+ // release workflow has no darwin-x64 leg.
50
+ if (os === "darwin" && arch === "x64") {
51
+ fail(`no prebuilt spec-mcp binary for darwin/x64 (Intel macOS is not released); \
52
+ build it (cargo build --release -p spec-mcp in the forge repo, tools/spec-mcp) and set SPEC_MCP_BINARY, or run: cargo install --git https://github.com/${GITHUB_REPO} --bin spec-mcp`);
53
+ }
54
+ if (os === "linux" && !process.report?.getReport?.().header?.glibcVersionRuntime) {
55
+ fail(`no prebuilt spec-mcp binary for linux/${arch}; build it \
56
+ (cargo build --release -p spec-mcp in the forge repo, tools/spec-mcp) and set SPEC_MCP_BINARY`);
57
+ }
58
+ return `${os}-${arch}`;
59
+ }
60
+
61
+ export function releaseVersion() {
62
+ return process.env.SPEC_MCP_VERSION ?? PKG.version;
63
+ }
64
+
65
+ export function assetName(version, platform) {
66
+ return `spec-mcp-${version}-${platform}.tar.gz`;
67
+ }
68
+
69
+ export function cacheDir() {
70
+ return process.env.SPEC_MCP_CACHE_DIR ?? join(homedir(), ".cache", "spec-mcp");
71
+ }
72
+
73
+ function fetchBuffer(url) {
74
+ return (async () => {
75
+ const res = await fetch(url);
76
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
77
+ return Buffer.from(await res.arrayBuffer());
78
+ })();
79
+ }
80
+
81
+ function sha256(buf) {
82
+ return createHash("sha256").update(buf).digest("hex");
83
+ }
84
+
85
+ // Extract one named member from a tar.gz using only node builtins: gzip via
86
+ // zlib, then a minimal USTAR/PAX reader (release archives hold one regular
87
+ // file, but pax size/longname overrides are honored defensively).
88
+ import { gunzipSync } from "node:zlib";
89
+
90
+ // PAX extended-header body, one record per line. GNU tar length-prefixes
91
+ // every record (`206 path=./…`, `29 mtime=…`); libarchive omits the length.
92
+ // Strip the optional leading `<len> ` and parse `keyword=value`.
93
+ function paxRecords(body) {
94
+ const out = {};
95
+ for (const line of body.split("\n")) {
96
+ if (!line) continue;
97
+ const rec = line.replace(/^\d+\s/, "");
98
+ const m = /^(\w+)=([\s\S]*)$/.exec(rec);
99
+ if (!m) continue;
100
+ out[m[1]] = m[2];
101
+ }
102
+ return out;
103
+ }
104
+
105
+ export function untarOne(gz, memberName) {
106
+ const tar = gunzipSync(gz);
107
+ let off = 0;
108
+ let paxName = null;
109
+ let paxSize = null;
110
+ while (off + 512 <= tar.length) {
111
+ const h = tar.subarray(off, off + 512);
112
+ if (h[0] === 0) break; // end-of-archive zero block
113
+ const field = (s, e) => h.toString("utf8", s, e).split("\0")[0].trim();
114
+ const rawName = field(0, 100);
115
+ const prefix = field(345, 500);
116
+ const size = parseInt(field(124, 136), 8) || 0;
117
+ const type = String.fromCharCode(h[156]);
118
+ off += 512;
119
+ const dataStart = off;
120
+ // GNU longname and pax extended headers describe the *next* entry.
121
+ if (type === "L") {
122
+ paxName = tar.subarray(dataStart, dataStart + size).toString("utf8").split("\0")[0];
123
+ off += Math.ceil(size / 512) * 512;
124
+ continue;
125
+ }
126
+ if (type === "x" || type === "g") {
127
+ const kv = paxRecords(tar.subarray(dataStart, dataStart + size).toString("utf8"));
128
+ if (kv.path !== undefined) paxName = kv.path;
129
+ if (kv.size !== undefined) paxSize = parseInt(kv.size, 10);
130
+ else paxSize = null;
131
+ off += Math.ceil(size / 512) * 512;
132
+ continue;
133
+ }
134
+ const entryName = paxName ?? (prefix ? `${prefix}/` : "") + rawName;
135
+ const normalized = entryName.replace(/^\.\//, "");
136
+ const isFile = type === "0" || type === "\0";
137
+ const effectiveSize = paxSize ?? size;
138
+ paxName = null;
139
+ paxSize = null;
140
+ if (isFile && normalized === memberName) {
141
+ return tar.subarray(dataStart, dataStart + effectiveSize);
142
+ }
143
+ off += Math.ceil(size / 512) * 512;
144
+ }
145
+ throw new Error(`member ${memberName} not found in archive`);
146
+ }
147
+
148
+ export async function ensureBinary({ quiet = false } = {}) {
149
+ if (process.env.SPEC_MCP_BINARY) {
150
+ const p = process.env.SPEC_MCP_BINARY;
151
+ if (!existsSync(p)) fail(`SPEC_MCP_BINARY points at a missing file: ${p}`);
152
+ return p;
153
+ }
154
+ const version = releaseVersion();
155
+ const platform = detectPlatform();
156
+ const dir = join(cacheDir(), `${version}-${platform}`);
157
+ const bin = join(dir, "spec-mcp");
158
+ if (existsSync(bin)) return bin;
159
+
160
+ const base = process.env.SPEC_MCP_BASE_URL ??
161
+ `https://github.com/${GITHUB_REPO}/releases/download/spec-mcp-v${version}`;
162
+ const asset = assetName(version, platform);
163
+ err(`spec-mcp: downloading ${asset} (v${version})`);
164
+ const [archive, sums] = await Promise.all([
165
+ fetchBuffer(`${base}/${asset}`),
166
+ fetchBuffer(`${base}/SHA256SUMS.txt`).catch(() => null),
167
+ ]);
168
+ if (sums) {
169
+ const expected = sums.toString("utf8").split("\n")
170
+ .map((l) => l.trim().split(/\s+/))
171
+ .filter(([h, f]) => h && f && (f === asset || f.endsWith("/" + asset)))
172
+ .map(([h]) => h)[0];
173
+ if (!expected) fail(`${asset} is not listed in the published SHA256SUMS.txt`);
174
+ const got = sha256(archive);
175
+ if (got !== expected) fail(`checksum mismatch for ${asset}: expected ${expected}, got ${got}`);
176
+ if (!quiet) err(`spec-mcp: checksum OK`);
177
+ } else {
178
+ fail("could not fetch SHA256SUMS.txt from the release; refusing unverified download");
179
+ }
180
+
181
+ const data = untarOne(archive, "spec-mcp");
182
+ mkdirSync(dir, { recursive: true });
183
+ const tmp = `${bin}.tmp-${process.pid}`;
184
+ try {
185
+ writeFileSync(tmp, data);
186
+ chmodSync(tmp, 0o755);
187
+ renameSync(tmp, bin);
188
+ } catch (e) {
189
+ rmSync(tmp, { force: true });
190
+ throw e;
191
+ }
192
+ if (!quiet) err(`spec-mcp: cached at ${bin}`);
193
+ return bin;
194
+ }
195
+
196
+ // -- skill install ------------------------------------------------------------
197
+
198
+ export function skillSource() {
199
+ const packaged = join(HERE, "..", "skill", "SKILL.md");
200
+ if (existsSync(packaged)) return packaged;
201
+ // Inside the forge repo checkout, fall back to the canonical skill location.
202
+ const canonical = join(HERE, "..", "..", "..", "..", "skills", SKILL_NAME, "SKILL.md");
203
+ if (existsSync(canonical)) return canonical;
204
+ fail(`bundled skill missing from package (${packaged})`);
205
+ }
206
+
207
+ export function skillTargets(scope, agents) {
208
+ const home = homedir();
209
+ const roots = {
210
+ omp: scope === "project" ? ".omp/skills" : join(home, ".omp/agent/skills"),
211
+ claude: scope === "project" ? ".claude/skills" : join(home, ".claude/skills"),
212
+ codex: scope === "project" ? ".codex/skills" : join(home, ".codex/skills"),
213
+ agents: scope === "project" ? ".agents/skills" : join(home, ".agents/skills"),
214
+ };
215
+ const list = agents.includes("all")
216
+ ? ["omp", "claude", "codex", "agents"] : agents;
217
+ for (const a of list) if (!Object.hasOwn(roots, a)) fail(`unknown agent '${a}' (omp|claude|codex|agents|all)`);
218
+ return list.map((a) => ({ agent: a, dir: join(roots[a], SKILL_NAME) }));
219
+ }
220
+
221
+ export function installSkill({ scope = "user", agents = ["omp"], force = false } = {}) {
222
+ const src = skillSource();
223
+ const content = readFileSync(src);
224
+ const results = [];
225
+ for (const { agent, dir } of skillTargets(scope, agents)) {
226
+ const target = join(dir, "SKILL.md");
227
+ let state = "installed";
228
+ if (existsSync(target)) {
229
+ const cur = readFileSync(target);
230
+ if (cur.equals(content)) { state = "up-to-date"; }
231
+ else if (!force) { state = "skipped (modified locally; --force to overwrite)"; }
232
+ else { writeFileSync(target, content); state = "overwritten"; }
233
+ } else {
234
+ mkdirSync(dir, { recursive: true });
235
+ writeFileSync(target, content);
236
+ state = "installed";
237
+ }
238
+ results.push({ agent, scope, path: target, state });
239
+ }
240
+ return results;
241
+ }
242
+
243
+ export function removeSkill({ scope = "user", agents = ["omp"] } = {}) {
244
+ const results = [];
245
+ for (const { agent, dir } of skillTargets(scope, agents)) {
246
+ const existed = existsSync(join(dir, "SKILL.md"));
247
+ if (existed) rmSync(dir, { recursive: true, force: true });
248
+ results.push({ agent, scope, path: dir, state: existed ? "removed" : "absent" });
249
+ }
250
+ return results;
251
+ }
252
+
253
+ // -- arg parsing ---------------------------------------------------------------
254
+
255
+ function parseArgs(argv) {
256
+ const opts = { scope: "user", agents: [], force: false };
257
+ const positional = [];
258
+ for (let i = 0; i < argv.length; i++) {
259
+ const a = argv[i];
260
+ if (a === "--project") opts.scope = "project";
261
+ else if (a === "--user") opts.scope = "user";
262
+ else if (a === "--force") opts.force = true;
263
+ else if (a === "--agent") {
264
+ const v = argv[++i];
265
+ if (!v) fail("--agent requires a value (omp|claude|codex|agents|all, repeatable)");
266
+ if (v === "all") opts.agents = ["all"];
267
+ else opts.agents.push(...v.split(","));
268
+ } else if (a === "--help" || a === "-h") {
269
+ positional.length = 0;
270
+ positional.push("help");
271
+ }
272
+ else if (a.startsWith("-")) fail(`unknown option: ${a} (see --help)`);
273
+ else positional.push(a);
274
+ }
275
+ if (opts.agents.length === 0) opts.agents = ["omp"];
276
+ return { positional, opts };
277
+ }
278
+
279
+ const USAGE = `spec-mcp ${PKG.version} — OMG KerML/SysML v2 specification MCP server
280
+
281
+ usage: spec-mcp [command] [options]
282
+
283
+ commands
284
+ (default) serve the MCP server over stdio (for MCP client config)
285
+ install download + verify the platform binary only
286
+ skill install install the '${SKILL_NAME}' agent skill
287
+ skill remove remove the '${SKILL_NAME}' skill
288
+ path print the resolved binary path
289
+ version print the launcher version
290
+
291
+ options (skill commands)
292
+ --user install to user-level agent dirs (default)
293
+ --project install to <cwd>/.<agent>/skills (checked in with your repo)
294
+ --agent <a> omp|claude|codex|agents|all (default omp; repeatable/comma list)
295
+ --force overwrite locally modified SKILL.md
296
+
297
+ MCP client config (project .mcp.json / .omp/mcp.json):
298
+ { "mcpServers": { "spec": { "command": "npx", "args": ["-y", "@kadabra-rs/spec-mcp"] } } }
299
+ bun: use "command": "bunx", same args.
300
+ `;
301
+
302
+ async function main() {
303
+ const { positional, opts } = parseArgs(process.argv.slice(2));
304
+ const cmd = positional.join(" ") || "serve";
305
+ switch (cmd) {
306
+ case "help":
307
+ process.stdout.write(USAGE);
308
+ return;
309
+ case "version":
310
+ process.stdout.write(`spec-mcp launcher ${PKG.version}\n`);
311
+ return;
312
+ case "path":
313
+ process.stdout.write(`${await ensureBinary({ quiet: true })}\n`);
314
+ return;
315
+ case "install":
316
+ await ensureBinary();
317
+ return;
318
+ case "skill install":
319
+ for (const r of installSkill(opts)) err(`spec-mcp: skill ${r.state}: ${r.path}`);
320
+ err(`spec-mcp: skill '${SKILL_NAME}' ready for ${opts.agents.join(", ")} (${opts.scope}-level)`);
321
+ return;
322
+ case "skill remove":
323
+ for (const r of removeSkill(opts)) err(`spec-mcp: skill ${r.state}: ${r.path}`);
324
+ return;
325
+ case "serve": {
326
+ const bin = await ensureBinary({ quiet: true });
327
+ const args = process.env.SPEC_MCP_CRATES_DIR ? [process.env.SPEC_MCP_CRATES_DIR] : [];
328
+ const child = spawn(bin, args, { stdio: "inherit" });
329
+ child.on("error", (e) => fail(`failed to exec ${bin}: ${e.message}`));
330
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
331
+ process.on(sig, () => {
332
+ if (child.exitCode === null && child.signalCode === null) child.kill(sig);
333
+ else {
334
+ // Re-raised from the exit handler below: restore default and die.
335
+ process.removeAllListeners(sig);
336
+ process.kill(process.pid, sig);
337
+ }
338
+ });
339
+ }
340
+ child.on("exit", (code, signal) => {
341
+ if (signal) process.kill(process.pid, signal);
342
+ else process.exit(code ?? 1);
343
+ });
344
+ return;
345
+ }
346
+ default:
347
+ fail(`unknown command: ${cmd} (see --help)`);
348
+ }
349
+ }
350
+
351
+ // Run only when executed as a program; importers (tests) get the functions.
352
+ // npm exposes bins through symlinks while Node realpath-resolves the main
353
+ // module, so compare realpaths rather than the URL of the invoked path.
354
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
355
+ await main();
356
+ }
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@kadabra-rs/spec-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Self-contained MCP server for the OMG KerML 1.0 and SysML v2 specifications: clause text, official BNF grammars, figures, and worked-example models, embedded in one binary.",
5
+ "license": "AGPL-3.0-only",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/kadabra-rs/forge.git",
13
+ "directory": "tools/spec-mcp/npm"
14
+ },
15
+ "homepage": "https://github.com/kadabra-rs/forge/tree/main/tools/spec-mcp/npm",
16
+ "keywords": ["mcp", "sysml", "sysml-v2", "kerml", "omg", "specification", "mbse"],
17
+ "bin": {
18
+ "spec-mcp": "bin/spec-mcp.mjs"
19
+ },
20
+ "files": [
21
+ "bin/spec-mcp.mjs",
22
+ "skill",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "prepare": "node scripts/copy-skill.mjs",
27
+ "test": "node --test"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ }
32
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: sysml-modeling
3
+ description: Use when creating, extending, reviewing, validating, or exploring SysML v2 and KerML models, or when asked how something should be modeled per the OMG specification. Grounds syntax and semantics in the spec-mcp oracle (clauses, BNF productions, figures, official worked examples) and validates models with the spec42 CLI.
4
+ ---
5
+
6
+ # SysML v2 / KerML modeling with the spec oracle and spec42
7
+
8
+ Two distinct surfaces, never interchangeable:
9
+
10
+ | Surface | What it is | Use it for |
11
+ | --- | --- | --- |
12
+ | **`spec` MCP server** (package `@kadabra-rs/spec-mcp`) | The OMG KerML 1.0 + SysML v2 specifications embedded in a binary: clause text, official BNF grammars, diagrams, and the OMG worked-example model corpus. It is *documentation*, not a validator. | How something is modeled: normative rules, concrete syntax (BNF), figures, real example models, clause citations. |
13
+ | **`spec42` CLI** | SysML v2 / KerML language tooling (parser, resolver, validator). It *compiles* model files. | Does my model parse/resolve/validate; what do diagnostics mean; resolved structure (`model-export`). |
14
+
15
+ Typical loop: learn from the oracle → write the smallest slice → validate with spec42 → repair root causes → cite the oracle when uncertain.
16
+
17
+ ## Ground rules
18
+
19
+ - Never invent concrete syntax. If you are not sure a construct exists or how it is written, ask the oracle: `get_syntax` (BNF production bodies) and `get_section` (the clause that defines it). Every syntax claim you write down should trace to a BNF production or a clause ID; when reporting, cite it (e.g. "§8.2.2.21.2").
20
+ - Before modeling an unfamiliar concept (requirements, ports, successions, variance, metadata, views…), look at how the OMG themselves modeled it: `list_examples` then `get_example`. The corpus is the official SysML v2 release material — training examples (`NN. <Topic>/…`), validation suites, KerML examples. Copy *shape* from examples only after confirming the governing syntax with `get_syntax`.
21
+ - Semantics questions ("may a RequirementUsage specialize…", "what does conjugates do to directions") go to `search_sections` + `get_section`, never to memory.
22
+ - Validation is what `spec42` says, not what the oracle says, and not what the source text looks like. Never claim a model is valid without an observed `spec42 check` run; never claim the oracle validated anything — it does not compile.
23
+
24
+ ## spec oracle tools (MCP, all take `spec: "kerml" | "sysml"`)
25
+
26
+ | Tool | Returns |
27
+ | --- | --- |
28
+ | `search_sections {spec, query}` | Clauses whose id/title/body match, body hits with snippets. Entry point for any question. |
29
+ | `semantic_search {spec, query, limit?}` | Meaning-based lookup when a concept is phrased differently than the spec's terms (lexical search misses it); ranked by cosine similarity. First call downloads the embedding model and caches clause embeddings. |
30
+ | `get_section {spec, clause_id}` | Clause body as Markdown + BNF production names defined there + figures; diagram images arrive inline (uncaptioned diagrams have `number: null`, captioned ones carry their printed number). |
31
+ | `get_syntax {spec, production}` | `.kebnf` production bodies (exact or substring match, exact first) with the defining clause. |
32
+ | `get_figure {spec, figure_number}` | One captioned diagram, inline, with title and owning clause. |
33
+ | `list_examples {spec}` | Index of the official worked-example corpus (paths grouped by topic). |
34
+ | `get_example {spec, path}` | Full source of one example `.kerml`/`.sysml` file from the corpus. |
35
+ | `follow_link {spec, link}` | Resolves `#anchor` cross-references found in clause text to their clause. |
36
+ | `spec_toc {spec, max_depth?}` | Nested clause outline (id, title, anchor, depth); max_depth 1–2 to orient, omit for the full tree. |
37
+ | `find_implementation {spec, clause_id}` | Maps a clause to compiler code in the forge repo (only meaningful there). |
38
+
39
+ The KerML spec holds the kernel semantics (clause 7.x), textual + abstract syntax (8.x); SysML v2 builds the systems layer on top. When a SysML clause says "see KerML 8.4.x", switch `spec` and follow it.
40
+
41
+ ## spec42 CLI contract
42
+
43
+ ```bash
44
+ command -v spec42 || echo "spec42 missing: in the forge repo run: sh tools/install-sysml-tooling.sh; elsewhere download a platform archive from https://github.com/elan8/spec42/releases and verify it against that release's SHA256SUMS.txt"
45
+
46
+ spec42 doctor --format json # environment/stdlib first on suspect setups
47
+ spec42 check <path-or-dir> --format json # validation report
48
+ spec42 check file.sysml --workspace-root <dir> --format json # single file inside a multi-file project
49
+ spec42 explain-diagnostic --code <code> --format json # stable meaning of a diagnostic code
50
+ spec42 model-export <dir> --format json # resolved structure (elements, relationships, connectors)
51
+ spec42 model-export <dir> --max-nodes 500 --format json # bounded; ALWAYS inspect projection.truncation
52
+ ```
53
+
54
+ Reading `check`: gate on `summary.error_count == 0`; report `summary.warning_count` separately; per-document diagnostics carry `code`, `message`, LSP `range`; `advice` flags environment problems (missing standard library). Many `unresolved_*` codes → run `doctor` before rewriting imports or types. A clean check proves the file compiles — not that the model satisfies what the user intended.
55
+
56
+ Reading `model-export`: it is resolved truth (specialization, typing, connector topology, metadata), the source text is not. Check `projection.schema_version`, state when a bounded export truncated.
57
+
58
+ There is no spec42 MCP server or HTTP API — upstream removed both in v0.50.0; the CLI is the only surface. If the binary is missing, do not claim validation happened: write "Not validated with Spec42" and offer an unvalidated draft.
59
+
60
+ ## Modeling workflow (small steps, always)
61
+
62
+ 1. **Frame.** System boundary, owning package, exact requested change. Inspect existing model conventions first (imports, package layout, naming).
63
+ 2. **Ground.** `search_sections` → `get_section` for the governing clauses; `get_syntax` for every construct you intend to emit; `get_example` on one or two official examples of that construct. Collect: normative rules, production bodies, example shapes.
64
+ 3. **Separate.** Facts from the conversation vs assumptions vs open questions. Do not turn every utterance into a requirement; attribute claims and preserve ambiguity.
65
+ 4. **Slice.** Smallest coherent change; definitions/usages stay in their owning packages; one authoritative definition per concept, reference it instead of duplicating.
66
+ 5. **Edit.** Minimal diff, existing conventions preserved.
67
+ 6. **Validate.** `spec42 check` with correct workspace scope. New error → root cause first, in this order: toolchain/stdlib → syntax → names/imports → typing/specialization → relations/behavior → traceability. Re-run after each fix; stop a speculative chain the moment it introduces new diagnostics; keep a baseline count so preexisting errors aren't blamed on you.
68
+ 7. **Verify structure.** When resolved relationships matter, `model-export` and cite the projection — with truncation status.
69
+ 8. **Report.** What changed; exact check scope and counts; clause/BNF citations backing the syntax; remaining warnings; assumptions; one next question when a material decision lacks evidence.
70
+
71
+ ## Repair and review specifics
72
+
73
+ - Reviewing a model or diff: read it, `check` it, then verify constructs against `get_syntax` — a construct can compile under spec42 yet be modelled against the spec's intent; the clause text is the arbiter of intent, cite it.
74
+ - Unusual syntax in someone's model is not a license to copy it: confirm in the grammar before reusing.
75
+ - Never suppress diagnostics by deleting content, disabling the standard library, or rewriting imports to hide errors; fix the root cause or report it.
76
+
77
+ ## Anti-fabrication checklist
78
+
79
+ - Syntax invented without a `get_syntax`/`get_section` citation → do not emit it.
80
+ - "Validated" without an observed `spec42 check` JSON → say "Not validated with Spec42".
81
+ - "Complete structure" from a `--max-nodes` export → check `projection.truncation` first.
82
+ - Relationship not present in `model-export` projection → report it as missing/inferred, never as verified.