sparda-mcp 0.9.0 → 0.12.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
@@ -84,6 +84,83 @@ To output raw JSON for integration:
84
84
  npx sparda-mcp report --json
85
85
  ```
86
86
 
87
+ ## Deployment Proof: Apocalypse
88
+
89
+ SPARDA's Behavior Graph is a formal model of your system. Instead of waiting for runtime failures or relying on static analysis vibes, you can statically prove the safety of your backend before any deployment:
90
+ ```bash
91
+ npx sparda-mcp apocalypse
92
+ ```
93
+ This command reads the compiled `.sparda/ubg.json` (with zero source code parsing at runtime) and discharges five static correctness obligations:
94
+ * **Unguarded Mutation (Critical)**: Flags any mutation path that does not cross a security `guard`.
95
+ * **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 declared invariants (CHECK, NOT NULL, UNIQUE — parsed from your `.sql` DDL **or `schema.prisma`**, Prisma enums included) without prior validation (Zod/Pydantic).
97
+ * **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
+ * **Aggregate Member Bypass (Info)**: Flags mutating a member table directly without routing through the aggregate root.
99
+
100
+ To save your current graph as a safe baseline:
101
+ ```bash
102
+ npx sparda-mcp apocalypse --save-baseline
103
+ ```
104
+ Subsequent runs will diff the candidate graph against this baseline to detect regression vectors:
105
+ * Deletion of any security `guard` (Critical).
106
+ * Deletion of a database SQL invariant (High).
107
+ * API blast radius expansion (Medium).
108
+
109
+ If any Critical or High finding is found, `apocalypse` exits with a non-zero code to block your CI pipeline.
110
+
111
+ **One step in your workflow — findings land in the GitHub Security tab (SARIF):**
112
+ ```yaml
113
+ - uses: zyx77550/sparda@main
114
+ with:
115
+ sarif: 'true'
116
+ ```
117
+
118
+ ## Time Travel: Timeless
119
+
120
+ 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:
121
+
122
+ ```bash
123
+ npx sparda-mcp timeless # list recorded flights
124
+ npx sparda-mcp timeless replay <id> # re-fly it — byte-identical or loud divergence
125
+ npx sparda-mcp timeless export <id> # the production bug is now a vitest test
126
+ ```
127
+
128
+ Recording is two lines in your app (ESM), with deterministic sampling and GDPR redaction built in:
129
+ ```js
130
+ import { getFlightBox } from 'sparda-mcp/src/flight/box.js';
131
+ const box = getFlightBox(); box.arm();
132
+ app.use(box.middleware({ sample: 100 })); // 1 request in 100; passwords/tokens redacted by default
133
+ const db = box.wrapClient(pgPool); // your query client, tapped
134
+ ```
135
+
136
+ 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).
137
+
138
+ ## Any Backend On Earth: OpenAPI Lowering
139
+
140
+ 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.
141
+
142
+ ```bash
143
+ npx sparda-mcp ubg --openapi openapi.json
144
+ ```
145
+
146
+ 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.)
147
+
148
+ ## The Mirror VM: delete the framework, the app still answers
149
+
150
+ The graph is not a diagram — it executes:
151
+
152
+ ```bash
153
+ npx sparda-mcp mirror
154
+ ```
155
+
156
+ ```
157
+ MIRROR — the graph is serving. 3 entrypoint(s) on http://127.0.0.1:4477
158
+ GET /orders/{orderId} → {amount, id, status}
159
+ POST /orders 🔒 bearerAuth → {amount, id, status}
160
+ ```
161
+
162
+ 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.
163
+
87
164
  To undo everything: **`npx sparda-mcp remove`** restores your code byte-for-byte.
88
165
 
89
166
  ## The promise — every word is backed by a test in CI
package/SKILL.md CHANGED
@@ -1,40 +1,30 @@
1
1
  ---
2
2
  name: sparda-mcp
3
3
  description: >-
4
- Drive a SPARDA-generated MCP server to its full potential. Use this whenever
5
- you are connected to an MCP server built by SPARDA (sparda-mcp) recognizable
6
- by the tools `sparda_get_context`, `sparda_info`, `sparda_confirm`, or composite
7
- tools labelled "[Labs circuit]". It teaches the call-context-first workflow, how
8
- to exploit the response-recycling flywheel, the circuit-breaker, crystallized
9
- circuits and adaptive immunity, and the mandatory two-phase confirm protocol for
10
- write tools. Reach for it before calling raw endpoints, before any write, or when
11
- a tool returns 202 (awaiting confirmation) or 503 (quarantined).
4
+ Drive a SPARDA compiled Behavior Runtime to its full potential. Use this whenever
5
+ you are connected to a backend running the SPARDA Runtime (compiled into a SPARDA
6
+ Behavior Graph - SBG) — recognizable by the tools `sparda_get_context`, `sparda_info`,
7
+ `sparda_confirm`, or composite tools. It teaches the graph-context-first workflow,
8
+ how to exploit the response-recycling flywheel, the circuit-breaker, crystallized
9
+ circuits, offline Twin simulations, and the two-phase confirm protocol for writes.
12
10
  ---
13
11
 
14
- # Driving a SPARDA MCP server
12
+ # Driving a SPARDA Behavior Runtime
15
13
 
16
- A SPARDA server is **not** a flat list of HTTP endpoints. SPARDA injected a live
17
- `/mcp` router *inside a real running app*, so its tools are that app's actual
18
- routes — wrapped in an intelligence layer that makes repeated use **cheaper** (it
19
- recycles stable answers from memory) and **safer** (it quarantines failing tools
20
- and gates writes behind human confirmation). Used naively it's just an API. Used
21
- well, it gets faster and safer the more you call it. This skill is how to use it
22
- well.
14
+ A SPARDA server is driven by a compiled **SPARDA Behavior Graph (SBG)**. Instead of exposing raw, disconnected endpoints, SPARDA has compiled the host application's states, transitions, permissions, and side-effects into a deterministic behavioral model. The local **SPARDA Runtime** dynamically executes this graph inside the live host process, powering the MCP interface, the Twin simulation clone, and the Immune system offline.
23
15
 
24
16
  ## Rule 0 — call `sparda_get_context` first, every session
25
17
 
26
- Before anything else, call **`sparda_get_context`** (no params). It returns the
27
- *live* picture, so you never guess the surface:
18
+ Before anything else, call **`sparda_get_context`** (no params). It returns the *live* state of the SPARDA Behavior Graph:
28
19
 
29
- - the enabled tools + their descriptions, and SPARDA's suggested **workflows**;
30
- - `runtime` — current `/mcp/stats` (per-tool call/error counts, tool "purity",
31
- quarantine state);
32
- - the last ~20 **events** (errors, latency anomalies, immune diagnoses);
33
- - **immune memory** (known failure signatures + cached fixes);
34
- - **recycling** stats (how many calls were served from memory);
35
- - a `behavior` snapshot (which response fields are stable).
20
+ - the active routes/tools, workflows, and type-propagated schemas;
21
+ - `runtime` — current stats (calls, errors, quarantine states, Twin mode active);
22
+ - the last ~20 **events** (errors, latency anomalies, immune alerts);
23
+ - **immune memory** (cached antibodies and error diagnoses);
24
+ - **recycling** metrics (flywheel memory hit rates);
25
+ - a `behavior` snapshot of stable variables.
36
26
 
37
- Read it before acting. `sparda_info` gives a lighter summary if you only need counts.
27
+ Read it to orient yourself inside the graph. `sparda_info` gives a lighter summary of counts.
38
28
 
39
29
  ## The tools you'll see
40
30
 
@@ -146,6 +136,7 @@ Writes are **disabled by default**. The protocol is not optional:
146
136
  - **General health** → `sparda doctor` checks Node version, framework detection,
147
137
  manifest validity, the semantic/immune cache, host reachability, and quarantine;
148
138
  it exits non-zero so it can gate CI.
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.
149
140
  - **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.
150
141
  - **Learn exemplars** → Start your live app and run `sparda twin --learn` to fetch actual response data and construct `.sparda/twin.json` locally.
151
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparda-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.12.0",
4
4
  "mcpName": "io.github.zyx77550/sparda-mcp",
5
5
  "description": "Turn any codebase into an MCP server with one command. Express & FastAPI → Claude-ready in 3 minutes.",
6
6
  "type": "module",
@@ -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,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;
@@ -0,0 +1,187 @@
1
+ // commands/timeless.js — production bugs become unit tests.
2
+ // sparda timeless list recorded flights
3
+ // sparda timeless replay <id> re-fly the request against current code
4
+ // sparda timeless export <id> generate a vitest test that replays it
5
+ // Recording side: mount the recorder in the app (2 lines, documented in the
6
+ // README) — `const flight = createFlightBox(); app.use(flight.middleware())`.
7
+ // Replay imports the real entry file with `listen()` suppressed, feeds every
8
+ // recorded tap back in strict order, and compares the response byte-for-byte.
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import http from 'node:http';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { detectStack } from '../detect.js';
14
+ import { getFlightBox } from '../flight/box.js';
15
+ import { replayFlight } from '../flight/replayer.js';
16
+ import { listFlights, loadFlight } from '../flight/format.js';
17
+ import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
18
+
19
+ const err = (message, hint) => Object.assign(new Error(message), { code: 'USER', hint });
20
+
21
+ export async function runTimeless(opts, args) {
22
+ const [sub, id] = args.filter((a) => !a.startsWith('--'));
23
+
24
+ if (!sub || sub === 'list') return list(opts);
25
+ if (sub === 'replay') return replay(opts, requireId(id));
26
+ if (sub === 'export') return exportTest(opts, requireId(id));
27
+ throw err(
28
+ `unknown timeless subcommand "${sub}"`,
29
+ 'use: list | replay <id> | export <id>',
30
+ );
31
+ }
32
+
33
+ const requireId = (id) => {
34
+ if (!id) throw err('flight id required', 'run `sparda timeless` to list flights');
35
+ return id;
36
+ };
37
+
38
+ function list(opts) {
39
+ const flights = listFlights(opts.cwd);
40
+ if (!flights.length) {
41
+ console.log(
42
+ 'No flights recorded. Mount the recorder in your app (ESM):\n' +
43
+ " import { getFlightBox } from 'sparda-mcp/src/flight/box.js';\n" +
44
+ ' const box = getFlightBox(); box.arm();\n' +
45
+ ' app.use(box.middleware());\n' +
46
+ ' const db = box.wrapClient(rawDb); // your query client\n' +
47
+ " (CJS: const { getFlightBox } = await import('sparda-mcp/src/flight/box.js'))",
48
+ );
49
+ return { flights };
50
+ }
51
+ console.log(`${flights.length} flight(s) in .sparda/flight/`);
52
+ for (const f of flights) {
53
+ const { request, response } = loadFlight(opts.cwd, f);
54
+ console.log(
55
+ ` ${f} ${request.method} ${request.url} → ${response.status} (${response.body.length}b, ${loadFlight(opts.cwd, f).taps.length} taps)`,
56
+ );
57
+ }
58
+ return { flights };
59
+ }
60
+
61
+ async function replay(opts, id) {
62
+ const flight = loadFlight(opts.cwd, id);
63
+ // the SHARED box: if the app integrates the recorder, its wrapClient rides
64
+ // the same AsyncLocalStorage this replay store lands on — load it BEFORE
65
+ // the app so both resolve the singleton
66
+ const box = getFlightBox();
67
+ const app = await loadApp(opts.cwd);
68
+ box.arm();
69
+ let result;
70
+ try {
71
+ result = await replayFlight(app, flight, box);
72
+ } finally {
73
+ box.disarm();
74
+ }
75
+
76
+ console.log(
77
+ `TIMELESS — replaying ${flight.request.method} ${flight.request.url} (${flight.taps.length} taps virtualized)`,
78
+ );
79
+ if (result.match) {
80
+ console.log(
81
+ `✓ byte-identical replay — status ${result.actual.status}, every tap consumed, zero divergence`,
82
+ );
83
+ } else {
84
+ if (!result.statusMatch)
85
+ console.log(
86
+ ` ✗ status: expected ${result.expected.status}, got ${result.actual.status}`,
87
+ );
88
+ if (!result.bodyMatch)
89
+ console.log(
90
+ ` ✗ body differs\n expected: ${result.expected.body.slice(0, 200)}\n actual: ${result.actual.body.slice(0, 200)}`,
91
+ );
92
+ for (const d of result.divergences)
93
+ console.log(
94
+ ` ✗ divergence: expected ${d.kind} "${d.expected}", code asked "${d.got}"`,
95
+ );
96
+ for (const l of result.leftover)
97
+ console.log(
98
+ ` ✗ ${l.unconsumed} unconsumed ${l.kind} tap(s) — the code skipped step(s)`,
99
+ );
100
+ console.log(
101
+ '✗ NOT identical — the code no longer does what it did during the flight',
102
+ );
103
+ process.exitCode = 1;
104
+ }
105
+ return { result };
106
+ }
107
+
108
+ async function exportTest(opts, id) {
109
+ const flight = loadFlight(opts.cwd, id);
110
+ const stack = detectStack(opts.cwd);
111
+ if (stack.framework !== 'express')
112
+ throw err(
113
+ 'timeless export supports Express apps in v1',
114
+ 'FastAPI replay is a later round',
115
+ );
116
+
117
+ const testDir = path.join(opts.cwd, 'tests');
118
+ fs.mkdirSync(testDir, { recursive: true });
119
+ const testFile = path.join(testDir, `flight-${id}.test.js`);
120
+ const relEntry = path
121
+ .relative(testDir, path.resolve(opts.cwd, stack.entryFile))
122
+ .split(path.sep)
123
+ .join('/');
124
+
125
+ atomicWrite(
126
+ testFile,
127
+ `// flight-${id}.test.js — generated by \`sparda timeless export ${id}\`
128
+ // Replays a recorded production request against the current code. A failure
129
+ // means the behavior changed: if the change is the intended fix, re-record
130
+ // the flight and re-export — this file asserts the PAST, on purpose.
131
+ import { describe, it, expect } from 'vitest';
132
+ import { getFlightBox } from 'sparda-mcp/src/flight/box.js';
133
+ import { replayFlight } from 'sparda-mcp/src/flight/replayer.js';
134
+ import { loadFlight } from 'sparda-mcp/src/flight/format.js';
135
+ import appModule from '${relEntry.startsWith('.') ? relEntry : './' + relEntry}';
136
+
137
+ describe('flight ${id} — ${flight.request.method} ${flight.request.url}', () => {
138
+ it('replays byte-identically', async () => {
139
+ const app = appModule.default ?? appModule;
140
+ const flight = loadFlight(process.cwd(), '${id}');
141
+ // the SHARED box — the app's own wrapClient must see the replay store
142
+ const box = getFlightBox();
143
+ box.arm();
144
+ try {
145
+ const result = await replayFlight(app, flight, box);
146
+ expect(result.divergences).toEqual([]);
147
+ expect(result.actual.status).toBe(result.expected.status);
148
+ expect(result.bodyMatch).toBe(true);
149
+ expect(result.leftover).toEqual([]);
150
+ } finally {
151
+ box.disarm();
152
+ }
153
+ });
154
+ });
155
+ `,
156
+ );
157
+ console.log(`✓ Test written: tests/flight-${id}.test.js — the bug is now a unit test`);
158
+ return { testFile };
159
+ }
160
+
161
+ // import the real entry with listen() suppressed — replay never binds ports
162
+ async function loadApp(cwd) {
163
+ const stack = detectStack(cwd);
164
+ if (stack.framework !== 'express')
165
+ throw err(
166
+ 'timeless replay supports Express apps in v1',
167
+ 'FastAPI replay is a later round',
168
+ );
169
+ const origListen = http.Server.prototype.listen;
170
+ http.Server.prototype.listen = function suppressedListen(...args) {
171
+ const cb = args.find((a) => typeof a === 'function');
172
+ if (cb) setImmediate(cb);
173
+ return this;
174
+ };
175
+ try {
176
+ const mod = await import(pathToFileURL(path.resolve(cwd, stack.entryFile)).href);
177
+ const app = mod.default ?? mod;
178
+ if (typeof app !== 'function')
179
+ throw err(
180
+ 'the entry file does not export the Express app',
181
+ 'add `module.exports = app` (CJS) or `export default app` (ESM) to your entry',
182
+ );
183
+ return app;
184
+ } finally {
185
+ http.Server.prototype.listen = origListen;
186
+ }
187
+ }
@@ -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) {
@@ -44,7 +45,16 @@ export async function runUbg(opts) {
44
45
  console.log(
45
46
  ` ${p.pass}: ${names.length} domain(s) — ${names.join(', ')}${p.cycles.length ? ` (⚠ FK cycle: ${p.cycles.join(', ')})` : ''}`,
46
47
  );
47
- }
48
+ } else if (p.pass === 'CapabilityExtraction' && p.capabilities)
49
+ console.log(
50
+ ` ${p.pass}: ${p.capabilities} capability(ies), ${p.guardsAnnotated} guard(s) annotated`,
51
+ );
52
+ else if (p.pass === 'ResourceLifetimes' && p.annotated)
53
+ console.log(
54
+ ` ${p.pass}: ${p.annotated} state(s)${p.immortal.length ? ` — immortal: ${p.immortal.join(', ')}` : ''}`,
55
+ );
56
+ else if (p.pass === 'StateMachineInference' && p.machines)
57
+ console.log(` ${p.pass}: ${p.machines} machine(s) inferred`);
48
58
  }
49
59
 
50
60
  if (report.link.inferredTables.length)
@@ -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',