sparda-mcp 0.10.1 β†’ 0.13.0

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
@@ -6,6 +6,10 @@
6
6
 
7
7
  <br/>
8
8
 
9
+ > πŸ‡«πŸ‡· **FranΓ§ais** : Pour comprendre SPARDA en 10 minutes (douleur, architecture, vision), lisez le document du fondateur : [SPARDA-EXPLIQUE.md](docs/SPARDA-EXPLIQUE.md).
10
+
11
+ ---
12
+
9
13
  **Compile your backend into a deterministic behavior runtime.**
10
14
 
11
15
  For twenty years, software communicated via APIs. Then AI agents arrived. The industry's response was simply to expose more endpoints (MCP). But an agent doesn't understand your application; it only sees a list of disconnected tools. That is enough for a human, but not for an autonomous machine.
@@ -93,7 +97,7 @@ npx sparda-mcp apocalypse
93
97
  This command reads the compiled `.sparda/ubg.json` (with zero source code parsing at runtime) and discharges five static correctness obligations:
94
98
  * **Unguarded Mutation (Critical)**: Flags any mutation path that does not cross a security `guard`.
95
99
  * **Non-Atomic Aggregate Write (High)**: Flags when an API writes to multiple tables of the same Consistency Domain (Aggregate) outside a single transaction scope.
96
- * **Unvalidated Constrained Write (Medium)**: Flags writes into columns with SQL invariants (CHECK, NOT NULL, UNIQUE) without prior validation (Zod/Pydantic).
100
+ * **Unvalidated Constrained Write (Medium)**: Flags writes into columns with declared invariants (CHECK, NOT NULL, UNIQUE β€” parsed from your `.sql` DDL **or `schema.prisma`**, Prisma enums included) without prior validation (Zod/Pydantic).
97
101
  * **Irreversible Observable Effect (High)**: Flags out-of-process actions (like Stripe charges) that happen alongside state writes without a structural compensation path (like a catch-refund).
98
102
  * **Aggregate Member Bypass (Info)**: Flags mutating a member table directly without routing through the aggregate root.
99
103
 
@@ -108,6 +112,59 @@ Subsequent runs will diff the candidate graph against this baseline to detect re
108
112
 
109
113
  If any Critical or High finding is found, `apocalypse` exits with a non-zero code to block your CI pipeline.
110
114
 
115
+ **One step in your workflow β€” findings land in the GitHub Security tab (SARIF):**
116
+ ```yaml
117
+ - uses: zyx77550/sparda@main
118
+ with:
119
+ sarif: 'true'
120
+ ```
121
+
122
+ ## Time Travel: Timeless
123
+
124
+ Every production request is deterministic between its effects β€” the compiler knows exactly where the nondeterminism lives (db, http, clock, random, uuid: the effect nodes of the graph). Timeless records only those points (a few KB per request) and replays the request **byte-identically** against your current code, with the database, webhooks and clock virtualized from the recording:
125
+
126
+ ```bash
127
+ npx sparda-mcp timeless # list recorded flights
128
+ npx sparda-mcp timeless replay <id> # re-fly it β€” byte-identical or loud divergence
129
+ npx sparda-mcp timeless export <id> # the production bug is now a vitest test
130
+ ```
131
+
132
+ Recording is two lines in your app (ESM), with deterministic sampling and GDPR redaction built in:
133
+ ```js
134
+ import { getFlightBox } from 'sparda-mcp/src/flight/box.js';
135
+ const box = getFlightBox(); box.arm();
136
+ app.use(box.middleware({ sample: 100 })); // 1 request in 100; passwords/tokens redacted by default
137
+ const db = box.wrapClient(pgPool); // your query client, tapped
138
+ ```
139
+
140
+ The closed loop nobody else has: **production bug β†’ recorded flight β†’ failing test β†’ AI writes the fix β†’ `apocalypse` proves the fix breaks no guard, invariant or transaction β†’ deploy.** Replay is per-request (concurrent-race capture is out of scope for v1 β€” stated, not hidden).
141
+
142
+ ## Any Backend On Earth: OpenAPI Lowering
143
+
144
+ SPARDA parses Express, FastAPI and Next.js natively β€” and **every other stack through the format the industry already agreed on**. Go, Java, Rails, Laravel, .NET: if it has an OpenAPI spec, it compiles.
145
+
146
+ ```bash
147
+ npx sparda-mcp ubg --openapi openapi.json
148
+ ```
149
+
150
+ Security schemes become gating `guard` nodes, response schemas become typed returns, declared request bodies count as validated input. Pair the spec with your `.sql` or `schema.prisma` files and the full state layer β€” invariants, aggregates, state machines β€” fills in from declared truth. (JSON specs in v1; we refuse to half-parse YAML with zero dependencies.)
151
+
152
+ ## The Mirror VM: delete the framework, the app still answers
153
+
154
+ The graph is not a diagram β€” it executes:
155
+
156
+ ```bash
157
+ npx sparda-mcp mirror
158
+ ```
159
+
160
+ ```
161
+ MIRROR β€” the graph is serving. 3 entrypoint(s) on http://127.0.0.1:4477
162
+ GET /orders/{orderId} β†’ {amount, id, status}
163
+ POST /orders πŸ”’ bearerAuth β†’ {amount, id, status}
164
+ ```
165
+
166
+ No Express. No FastAPI. No source code β€” just `ubg.json` answering HTTP: guards actually deny (401), responses render the compiled return schemas, unknown paths 404 with the full route table. Front-end teams develop against backends that aren't deployed yet β€” or aren't written yet (point `mirror` at an OpenAPI spec). Every response carries `x-sparda-mirror: true`; the mirror serves declared behavior, it never invents business values.
167
+
111
168
  To undo everything: **`npx sparda-mcp remove`** restores your code byte-for-byte.
112
169
 
113
170
  ## The promise β€” every word is backed by a test in CI
package/SKILL.md CHANGED
@@ -137,6 +137,10 @@ Writes are **disabled by default**. The protocol is not optional:
137
137
  manifest validity, the semantic/immune cache, host reachability, and quarantine;
138
138
  it exits non-zero so it can gate CI.
139
139
  - **Formal Deployment Proof** β†’ `sparda apocalypse` reads the compiled graph (`ubg.json`) and proves five correctness obligations: catches unguarded mutations, non-atomic aggregate writes, unvalidated writes to constrained tables, uncompensated observable effects, and aggregate root bypasses. Run `sparda apocalypse --save-baseline` to store the reference graph; subsequent runs diff against the baseline to catch dropped guards, dropped SQL invariants, or grown blast radiuses.
140
+ - **OpenAPI Ingestion** β†’ Run `sparda ubg --openapi <openapi_spec.json_or_yaml>` to compile any non-JS/Python backend (Go, Java, Rails, Laravel, .NET) into a SPARDA Behavior Graph by mapping security schemes into guards and request/response structures.
141
+ - **Executing the Graph (No code mock)** β†’ Run `sparda mirror` to host a mock HTTP simulation server directly from `ubg.json` without any backend code. Enforces authentication guards, returns typed responses, and acts as a contract sandbox.
142
+ - **Exporting OpenAPI 3.1 Spec** β†’ Run `sparda openapi` to generate a valid, deterministic OpenAPI 3.1 spec dynamically from the compiled behavior graph.
143
+ - **Self-Verification** β†’ Run `sparda verify` to test the compiler's own invariants (byte-determinism, soundness, and spec round-trip) to guarantee trust.
140
144
  - **Clone learning / Transfer sΓ©mantique** β†’ Use `sparda seed export` to package your app's descriptions, workflows, and antibodies. Then `sparda seed import --germinate` in another clone to import the structure and germinate simulated twin instances.
141
145
  - **Learn exemplars** β†’ Start your live app and run `sparda twin --learn` to fetch actual response data and construct `.sparda/twin.json` locally.
142
146
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "sparda-mcp",
3
- "version": "0.10.1",
3
+ "version": "0.13.0",
4
4
  "mcpName": "io.github.zyx77550/sparda-mcp",
5
- "description": "Turn any codebase into an MCP server with one command. Express & FastAPI β†’ Claude-ready in 3 minutes.",
5
+ "description": "Compile any backend into a deterministic behavior graph (SBG). Statically prove security, simulate APIs without code, and replay bugs locally.",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -0,0 +1,123 @@
1
+ // commands/apocalypse.js β€” prove a deployment can't break the declared rules.
2
+ // Compiles the tree to SBIR, discharges the static proof obligations, and β€”
3
+ // when a baseline exists β€” proves the deploy removed no protection the
4
+ // baseline had. Exit code 1 on any critical/high finding: this command is
5
+ // built to sit in CI between "tests pass" and "deploy".
6
+ // sparda apocalypse check current tree (+ diff vs baseline if saved)
7
+ // sparda apocalypse --save-baseline record the current graph as the reference
8
+ // sparda apocalypse --json raw findings for tooling
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { compileUBG } from '../ubg/compile.js';
12
+ import { canonicalizeGraph } from '../ubg/schema.js';
13
+ import { checkGraph, diffGraphs, verdictOf } from '../ubg/apocalypse.js';
14
+ import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
15
+
16
+ const ICONS = { critical: 'βœ—', high: 'βœ—', medium: '⚠', info: 'Β·' };
17
+
18
+ export async function runApocalypse(opts) {
19
+ const { graph } = compileUBG(opts.cwd, { write: false });
20
+ const canonical = canonicalizeGraph(graph);
21
+ const baselinePath = path.join(opts.cwd, '.sparda', 'ubg.baseline.json');
22
+
23
+ if (opts.saveBaseline) {
24
+ fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
25
+ atomicWrite(baselinePath, JSON.stringify(canonical, null, 2) + '\n');
26
+ console.log(
27
+ `βœ“ Baseline saved: .sparda/ubg.baseline.json β€” future deploys are proven against it`,
28
+ );
29
+ }
30
+
31
+ const { findings: staticFindings, obligations } = checkGraph(canonical);
32
+ let diffFindings = [];
33
+ let hasBaseline = false;
34
+ if (!opts.saveBaseline && fs.existsSync(baselinePath)) {
35
+ hasBaseline = true;
36
+ try {
37
+ const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
38
+ diffFindings = diffGraphs(baseline, canonical).findings;
39
+ } catch (err) {
40
+ console.error(`⚠ baseline unreadable (${err.message}) β€” static checks only`);
41
+ }
42
+ }
43
+
44
+ const findings = [...staticFindings, ...diffFindings];
45
+ const verdict = verdictOf(findings);
46
+
47
+ if (opts.sarif) {
48
+ const sarifPath = path.join(opts.cwd, '.sparda', 'apocalypse.sarif');
49
+ fs.mkdirSync(path.dirname(sarifPath), { recursive: true });
50
+ atomicWrite(sarifPath, JSON.stringify(toSarif(findings, canonical), null, 2) + '\n');
51
+ console.log(
52
+ `βœ“ SARIF written: .sparda/apocalypse.sarif (${findings.length} result(s))`,
53
+ );
54
+ }
55
+
56
+ if (opts.json) {
57
+ console.log(JSON.stringify({ verdict, obligations, findings }, null, 2));
58
+ } else {
59
+ console.log(
60
+ `APOCALYPSE β€” deployment proof over ${canonical.nodes.length} nodes, ${canonical.edges.length} edges` +
61
+ (hasBaseline ? ' (baseline diff included)' : ''),
62
+ );
63
+ for (const f of findings) {
64
+ console.log(` ${ICONS[f.severity]} [${f.severity}] ${f.rule} β€” ${f.message}`);
65
+ if (opts.verbose) for (const ev of f.evidence) console.log(` evidence: ${ev}`);
66
+ }
67
+ if (verdict.clean) {
68
+ console.log(
69
+ `βœ“ PROVEN β€” ${obligations} obligation(s) discharged, zero violations. No declared guard, invariant, transaction or aggregate boundary can be broken by this tree.`,
70
+ );
71
+ } else {
72
+ const c = verdict.counts;
73
+ console.log(
74
+ `${verdict.safe ? '⚠ RISKY' : 'βœ— NOT PROVEN'} β€” ${c.critical} critical, ${c.high} high, ${c.medium} medium, ${c.info} info`,
75
+ );
76
+ }
77
+ }
78
+
79
+ if (!verdict.safe) process.exitCode = 1; // CI gates on this
80
+ return { verdict, findings, obligations };
81
+ }
82
+
83
+ // SARIF 2.1.0 β€” GitHub code scanning eats this directly
84
+ const SARIF_LEVEL = { critical: 'error', high: 'error', medium: 'warning', info: 'note' };
85
+
86
+ function toSarif(findings, canonical) {
87
+ const nodeById = new Map(canonical.nodes.map((n) => [n.id, n]));
88
+ return {
89
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
90
+ version: '2.1.0',
91
+ runs: [
92
+ {
93
+ tool: {
94
+ driver: {
95
+ name: 'sparda-apocalypse',
96
+ informationUri: 'https://github.com/zyx77550/sparda',
97
+ rules: [...new Set(findings.map((f) => f.rule))].sort().map((id) => ({ id })),
98
+ },
99
+ },
100
+ results: findings.map((f) => {
101
+ const loc = nodeById.get(f.entrypoint)?.loc;
102
+ return {
103
+ ruleId: f.rule,
104
+ level: SARIF_LEVEL[f.severity] ?? 'warning',
105
+ message: { text: f.message },
106
+ ...(loc
107
+ ? {
108
+ locations: [
109
+ {
110
+ physicalLocation: {
111
+ artifactLocation: { uri: loc.file },
112
+ region: { startLine: loc.line || 1 },
113
+ },
114
+ },
115
+ ],
116
+ }
117
+ : {}),
118
+ };
119
+ }),
120
+ },
121
+ ],
122
+ };
123
+ }
@@ -0,0 +1,182 @@
1
+ // commands/heal.js β€” the closed loop as one gesture.
2
+ // sparda heal <flightId> diagnose + write the fix brief
3
+ // sparda heal <flightId> --agent "claude -p" let an AI attempt the fix, then gate
4
+ // sparda heal <flightId> --check [--expect '{"status":200}'] gate a fix (human or AI)
5
+ // The gate is the product: lenient replay against the recorded taps must meet
6
+ // the EXPECTATION (not the recorded bug), compiler laws must hold (verify),
7
+ // and apocalypse must find no new critical/high and no removed guard. SPARDA
8
+ // orchestrates and proves β€” whoever writes the fix, the machine judges it.
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { loadFlight } from '../flight/format.js';
13
+ import { getFlightBox } from '../flight/box.js';
14
+ import { replayFlight } from '../flight/replayer.js';
15
+ import { evaluateHealing, buildBrief } from '../flight/heal.js';
16
+ import { compileUBG } from '../ubg/compile.js';
17
+ import { canonicalizeGraph } from '../ubg/schema.js';
18
+ import { checkGraph, diffGraphs, verdictOf } from '../ubg/apocalypse.js';
19
+ import { verifyProject } from '../ubg/verify.js';
20
+ import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
21
+
22
+ const err = (message, hint) => Object.assign(new Error(message), { code: 'USER', hint });
23
+
24
+ export async function runHeal(opts, args) {
25
+ const id = args.filter((a) => !a.startsWith('--'))[0];
26
+ if (!id) throw err('flight id required', 'run `sparda timeless` to list flights');
27
+ const flight = loadFlight(opts.cwd, id);
28
+ const healDir = path.join(opts.cwd, '.sparda', 'heal', id);
29
+ fs.mkdirSync(healDir, { recursive: true });
30
+
31
+ if (opts.check) return gate(opts, id, flight, healDir);
32
+
33
+ // ---- phase 1: diagnose + brief -----------------------------------------
34
+ const compiled = compileUBG(opts.cwd, { write: false });
35
+ const graph = canonicalizeGraph(compiled.graph);
36
+ const app = await loadAppFor(opts.cwd);
37
+ const box = getFlightBox();
38
+ box.arm();
39
+ let strict;
40
+ try {
41
+ strict = await replayFlight(app, flight, box);
42
+ } finally {
43
+ box.disarm();
44
+ }
45
+
46
+ // freeze the pre-fix graph: the gate diffs the fixed tree against THIS
47
+ atomicWrite(path.join(healDir, 'baseline.json'), JSON.stringify(graph, null, 2) + '\n');
48
+ const brief = buildBrief(flight, graph, strict, id);
49
+ const briefPath = path.join(healDir, 'BRIEF.md');
50
+ atomicWrite(briefPath, brief);
51
+
52
+ console.log(`HEAL β€” flight ${id}: ${flight.request.method} ${flight.request.url}`);
53
+ console.log(
54
+ strict.match
55
+ ? ' βœ“ bug reproduced byte-identically against current code'
56
+ : ' ⚠ current code already answers differently β€” the bug may be partially fixed',
57
+ );
58
+ console.log(` βœ“ brief written: .sparda/heal/${id}/BRIEF.md`);
59
+ console.log(` βœ“ pre-fix graph frozen: .sparda/heal/${id}/baseline.json`);
60
+
61
+ if (opts.agent) {
62
+ console.log(` β–Έ handing the brief to: ${opts.agent}`);
63
+ const run = spawnSync(opts.agent, {
64
+ input: brief,
65
+ shell: true,
66
+ cwd: opts.cwd,
67
+ encoding: 'utf8',
68
+ timeout: 600_000,
69
+ });
70
+ if (run.status !== 0)
71
+ throw err(`agent exited ${run.status}`, (run.stderr ?? '').slice(0, 200));
72
+ console.log(' βœ“ agent finished β€” gating the fix now');
73
+ return gate(opts, id, flight, healDir);
74
+ }
75
+
76
+ console.log('\nNext: apply the fix (yourself or an agent), then:');
77
+ console.log(` sparda heal ${id} --check --expect '{"status":200}'`);
78
+ return { brief: briefPath };
79
+ }
80
+
81
+ // ---- phase 2: the gate ----------------------------------------------------
82
+ async function gate(opts, id, flight, healDir) {
83
+ const expectation = opts.expect ? JSON.parse(opts.expect) : null;
84
+
85
+ // 1. lenient replay: recorded inputs, candidate code, expected outcome
86
+ const app = await loadAppFor(opts.cwd);
87
+ const box = getFlightBox();
88
+ box.arm();
89
+ let replay;
90
+ try {
91
+ replay = await replayFlight(app, flight, box, { lenient: true });
92
+ } finally {
93
+ box.disarm();
94
+ }
95
+ const healing = evaluateHealing(flight, replay, expectation);
96
+
97
+ // 2. compiler laws still hold on the fixed tree
98
+ const laws = verifyProject(opts.cwd);
99
+
100
+ // 3. apocalypse: no new critical/high, nothing protected got removed
101
+ const fixedGraph = canonicalizeGraph(compileUBG(opts.cwd, { write: false }).graph);
102
+ const staticFindings = checkGraph(fixedGraph).findings;
103
+ let regressions = [];
104
+ const baselinePath = path.join(healDir, 'baseline.json');
105
+ if (fs.existsSync(baselinePath)) {
106
+ const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
107
+ regressions = diffGraphs(baseline, fixedGraph).findings;
108
+ // pre-existing static findings are not the fix's fault β€” only NEW ones gate
109
+ const before = new Set(
110
+ checkGraph(baseline).findings.map((f) => `${f.rule}:${f.entrypoint}`),
111
+ );
112
+ regressions = [
113
+ ...regressions,
114
+ ...staticFindings.filter((f) => !before.has(`${f.rule}:${f.entrypoint}`)),
115
+ ];
116
+ } else {
117
+ regressions = staticFindings;
118
+ }
119
+ const apocalypse = verdictOf(regressions);
120
+
121
+ // ---- verdict -------------------------------------------------------------
122
+ console.log(`HEAL GATE β€” flight ${id}`);
123
+ console.log(
124
+ ` ${healing.healed ? 'βœ“' : 'βœ—'} behavior: ${healing.healed ? `bug gone (${healing.before.status} β†’ ${healing.after.status})` : healing.reasons.join('; ')}`,
125
+ );
126
+ if (healing.relabels.length)
127
+ console.log(
128
+ ` Β· ${healing.relabels.length} tap(s) relabeled by the fix (allowed): ${healing.relabels.map((r) => r.kind).join(', ')}`,
129
+ );
130
+ console.log(` ${laws.ok ? 'βœ“' : 'βœ—'} compiler laws: ${laws.passed}/${laws.total}`);
131
+ console.log(
132
+ ` ${apocalypse.safe ? 'βœ“' : 'βœ—'} apocalypse: ${regressions.length === 0 ? 'no new findings' : regressions.map((f) => f.rule).join(', ')}`,
133
+ );
134
+
135
+ const ok = healing.healed && laws.ok && apocalypse.safe;
136
+ const report = { id, healed: healing, laws: laws.checks, regressions, ok };
137
+ atomicWrite(path.join(healDir, 'report.json'), JSON.stringify(report, null, 2) + '\n');
138
+
139
+ if (ok) {
140
+ console.log(
141
+ `βœ“ HEALED & PROVEN β€” same recorded inputs, correct output, zero law broken, zero protection lost. Ship it.`,
142
+ );
143
+ } else {
144
+ console.log(
145
+ 'βœ— NOT PROVEN β€” the gate stays closed. Details: .sparda/heal/' +
146
+ id +
147
+ '/report.json',
148
+ );
149
+ process.exitCode = 1;
150
+ }
151
+ return report;
152
+ }
153
+
154
+ // same entry-loading discipline as timeless replay (listen suppressed)
155
+ async function loadAppFor(cwd) {
156
+ const { detectStack } = await import('../detect.js');
157
+ const http = await import('node:http');
158
+ const { pathToFileURL } = await import('node:url');
159
+ const stack = detectStack(cwd);
160
+ if (stack.framework !== 'express')
161
+ throw err('heal supports Express apps in v1', 'FastAPI replay is a later round');
162
+ const origListen = http.default.Server.prototype.listen;
163
+ http.default.Server.prototype.listen = function suppressed(...a) {
164
+ const cb = a.find((x) => typeof x === 'function');
165
+ if (cb) setImmediate(cb);
166
+ return this;
167
+ };
168
+ try {
169
+ // cache-bust: the gate must load the FIXED code, not the pre-fix module
170
+ const entry = pathToFileURL(path.resolve(cwd, stack.entryFile)).href;
171
+ const mod = await import(`${entry}?sparda-heal=${Date.now()}`);
172
+ const app = mod.default ?? mod;
173
+ if (typeof app !== 'function')
174
+ throw err(
175
+ 'the entry file does not export the Express app',
176
+ 'add `module.exports = app`',
177
+ );
178
+ return app;
179
+ } finally {
180
+ http.default.Server.prototype.listen = origListen;
181
+ }
182
+ }
@@ -0,0 +1,42 @@
1
+ // commands/mirror.js β€” serve the compiled behavior, delete the framework.
2
+ // sparda mirror compile the tree (or --openapi spec) and serve it
3
+ // sparda mirror --port 5050 pick the port
4
+ // sparda ubg --openapi api.json && sparda mirror any backend on earth
5
+ // The graph answers HTTP: guards deny (401), responses come out typed from
6
+ // the compiled return schemas, unknown paths list what the graph knows.
7
+ // Front-ends develop against backends that aren't deployed β€” or written.
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { compileUBG } from '../ubg/compile.js';
11
+ import { createMirrorServer } from '../ubg/mirror.js';
12
+
13
+ export async function runMirror(opts) {
14
+ // prefer the already-compiled artifact; compile fresh when absent
15
+ const artifact = path.join(opts.cwd, '.sparda', 'ubg.json');
16
+ let graph;
17
+ if (fs.existsSync(artifact) && !opts.openapi) {
18
+ graph = JSON.parse(fs.readFileSync(artifact, 'utf8'));
19
+ } else {
20
+ graph = JSON.parse(
21
+ compileUBG(opts.cwd, { write: false, openapi: opts.openapi ?? null }).json,
22
+ );
23
+ }
24
+
25
+ const { server, routes } = createMirrorServer(graph);
26
+ const port = Number(opts.port ?? 4477);
27
+ await new Promise((resolve, reject) => {
28
+ server.once('error', reject);
29
+ server.listen(port, '127.0.0.1', resolve);
30
+ });
31
+
32
+ console.log(
33
+ `MIRROR β€” the graph is serving. ${routes.length} entrypoint(s) on http://127.0.0.1:${port}`,
34
+ );
35
+ for (const r of routes) {
36
+ console.log(
37
+ ` ${r.method.toUpperCase().padEnd(6)} ${r.path}${r.guarded ? ' πŸ”’ ' + r.guards.join(',') : ''}${r.returns ? ' β†’ {' + Object.keys(r.returns).join(', ') + '}' : ''}`,
38
+ );
39
+ }
40
+ console.log(' (every response carries x-sparda-mirror: true β€” Ctrl+C to stop)');
41
+ return { server, routes, port };
42
+ }
@@ -0,0 +1,61 @@
1
+ // commands/openapi.js β€” emit an OpenAPI 3.1 spec FROM the behavior graph.
2
+ // We produce the standard the rest of the industry consumes: any backend
3
+ // SPARDA can compile (Express, FastAPI, Next.js β€” or another spec via
4
+ // --openapi) gets a valid, deterministic spec, richer than most hand-written
5
+ // ones because it comes from what the code actually declares.
6
+ // sparda openapi write .sparda/openapi.json
7
+ // sparda openapi --out api.json pick the path
8
+ // sparda openapi --json print to stdout
9
+ import path from 'node:path';
10
+ import fs from 'node:fs';
11
+ import { compileUBG } from '../ubg/compile.js';
12
+ import { canonicalizeGraph } from '../ubg/schema.js';
13
+ import { emitOpenAPI } from '../ubg/openapi-emit.js';
14
+ import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
15
+
16
+ export async function runOpenapi(opts) {
17
+ const { graph } = compileUBG(opts.cwd, {
18
+ write: false,
19
+ openapi: opts.openapi ?? null,
20
+ });
21
+ const title = packageNameOf(opts.cwd);
22
+ const spec = emitOpenAPI(canonicalizeGraph(graph), { title });
23
+ const body = JSON.stringify(spec, null, 2) + '\n';
24
+
25
+ if (opts.json) {
26
+ console.log(body);
27
+ return { spec };
28
+ }
29
+ const target = opts.out
30
+ ? path.resolve(opts.cwd, opts.out)
31
+ : path.join(opts.cwd, '.sparda', 'openapi.json');
32
+ fs.mkdirSync(path.dirname(target), { recursive: true });
33
+ atomicWrite(target, body);
34
+
35
+ const pathCount = Object.keys(spec.paths).length;
36
+ const opCount = Object.values(spec.paths).reduce(
37
+ (n, p) => n + Object.keys(p).length,
38
+ 0,
39
+ );
40
+ console.log(
41
+ `βœ“ OpenAPI 3.1 emitted: ${rel(target, opts.cwd)} β€” ${pathCount} path(s), ${opCount} operation(s)`,
42
+ );
43
+ console.log(
44
+ ' SPARDA produces the standard others consume β€” feed it to Swagger UI or a codegen.',
45
+ );
46
+ return { spec, target };
47
+ }
48
+
49
+ function packageNameOf(cwd) {
50
+ try {
51
+ return (
52
+ JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).name ??
53
+ 'sparda-compiled-api'
54
+ );
55
+ } catch {
56
+ return 'sparda-compiled-api';
57
+ }
58
+ }
59
+
60
+ const rel = (abs, cwd) =>
61
+ abs.startsWith(cwd) ? abs.slice(cwd.length + 1).replaceAll('\\', '/') : abs;
@@ -9,6 +9,7 @@ export async function runUbg(opts) {
9
9
  const { report, json, outPath } = compileUBG(opts.cwd, {
10
10
  write: true,
11
11
  out: opts.out,
12
+ openapi: opts.openapi ?? null,
12
13
  });
13
14
 
14
15
  if (opts.json) {
@@ -0,0 +1,36 @@
1
+ // commands/verify.js β€” prove the compiler's own laws on THIS project.
2
+ // Runs the SBIR compiler-law audit and reports each check. Exit code 1 if any
3
+ // law fails: run it in CI to guarantee the graph your tools trust was built
4
+ // under determinism and soundness β€” not just asserted to be.
5
+ // sparda verify audit the detected app
6
+ // sparda verify --openapi s.json audit an OpenAPI-lowered backend
7
+ import { verifyProject } from '../ubg/verify.js';
8
+
9
+ export async function runVerify(opts) {
10
+ const { checks, passed, total, ok } = verifyProject(opts.cwd, {
11
+ openapi: opts.openapi ?? null,
12
+ });
13
+
14
+ console.log(`VERIFY β€” SBIR compiler laws on this project (${passed}/${total} checks)`);
15
+ let lastLaw = null;
16
+ for (const c of checks) {
17
+ if (c.law !== lastLaw) {
18
+ console.log(` ${c.law}`);
19
+ lastLaw = c.law;
20
+ }
21
+ console.log(
22
+ ` ${c.pass ? 'βœ“' : 'βœ—'} ${c.name}${c.pass || !c.detail ? '' : ` β€” ${c.detail}`}`,
23
+ );
24
+ }
25
+ if (ok) {
26
+ console.log(
27
+ 'βœ“ PROVEN β€” every compiler law holds on this input. The graph is trustworthy.',
28
+ );
29
+ } else {
30
+ console.log(
31
+ 'βœ— LAW VIOLATED β€” the compiler broke its own contract on this input. This is a bug; please open an issue.',
32
+ );
33
+ process.exitCode = 1;
34
+ }
35
+ return { ok, checks };
36
+ }
package/src/detect.js CHANGED
@@ -199,6 +199,8 @@ function findExpressEntry(cwd, pkg) {
199
199
  'src/app.ts',
200
200
  'src/server.ts',
201
201
  'src/index.ts',
202
+ 'src/main.ts', // Nx / NestJS-style monorepo layout
203
+ 'main.ts',
202
204
  'app.ts',
203
205
  'server.ts',
204
206
  'index.ts',