sparda-mcp 0.14.1 → 0.15.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
@@ -6,115 +6,39 @@
6
6
 
7
7
  <br/>
8
8
 
9
- <div align="center">
10
-
11
- ## **AI writes. SPARDA proves.**
12
-
13
- </div>
14
-
15
- AI made writing code free. It made *trusting* code the bottleneck. Your agents open
16
- pull requests faster than any human can truly review them, and the industry's answer
17
- is to have another LLM skim the diff — an opinion grading an opinion.
18
-
19
- SPARDA is not an opinion. It's a **behavior compiler**: it compiles your backend
20
- (code + schema) into a deterministic behavior graph, and *proves* things against it —
21
- which guard a PR removed, which invariant a deploy dropped, whether a fix broke
22
- nothing else. Every finding is a **counterexample**, never a vibe. Zero spec to
23
- write, zero config, zero API key, nothing leaves your machine.
24
-
25
- One engine, four moves:
26
-
27
- | | Command | What it proves |
28
- |---|---|---|
29
- | 🔍 | `sparda review` | the **behavior diff** of a PR — guards dropped, blast radius grown, new endpoints |
30
- | 🛡️ | `sparda apocalypse` | a deploy can't break any declared guard, invariant, transaction or aggregate |
31
- | 🪞 | `sparda mirror` | a live mock **derived from the code** — enforces the real state machine, can never drift |
32
- | ⏪ | `sparda timeless` / `heal` | replay a production bug deterministically; gate the fix with proof, not hope |
33
-
34
- And because trust must extend to *runtime*, the same engine turns your app into a
35
- safe set of MCP tools for Claude & friends — writes gated, failures quarantined
36
- (see [Give your AI safe hands](#give-your-ai-safe-hands-the-mcp-layer)).
37
-
38
- ## Review every PR's *behavior* — one file, zero config
9
+ > 🇫🇷 **Français** : Pour comprendre SPARDA en 10 minutes (douleur, architecture, vision), lisez le document du fondateur : [SPARDA-EXPLIQUE.md](docs/SPARDA-EXPLIQUE.md).
39
10
 
40
- Every code-review tool diffs your **text**. None diff your **behavior**. SPARDA
41
- compiles the PR's base branch and your changes to the behavior graph and comments the
42
- difference — as one sticky comment that updates on every push:
11
+ ---
43
12
 
44
- <div align="center">
45
- <img src="assets/pr-review-demo.svg" alt="SPARDA review bot commenting the behavior diff on a pull request: NOT PROVEN — GUARD_REMOVED on POST /pay, BLAST_RADIUS_GREW on POST /transfer, new endpoint POST /refund" width="740" />
46
- </div>
13
+ **A compiler for backend behavior. The LLVM of web applications.**
47
14
 
48
- <br/>
15
+ For twenty years software communicated through APIs. Then AI agents arrived, and the industry's answer was to expose more endpoints (MCP). But an agent doesn't *understand* an application from a list of disconnected tools — and neither can a linter, a debugger, or a deploy gate reason about a codebase they only read line by line.
49
16
 
50
- Drop this one file into `.github/workflows/sparda-review.yml` no key, no account,
51
- no spec to write:
52
-
53
- ```yaml
54
- name: SPARDA behavior review
55
- on: pull_request
56
- permissions:
57
- contents: read
58
- pull-requests: write
59
- jobs:
60
- review:
61
- runs-on: ubuntu-latest
62
- steps:
63
- - uses: actions/checkout@v4
64
- with: { fetch-depth: 0 }
65
- - uses: zyx77550/sparda@main
66
- with:
67
- mode: review # comment-only by default — never blocks a merge
68
- # fail-on-severity: high # uncomment to also gate the check
69
- ```
17
+ SPARDA compiles your backend — routes, database queries, state mutations, permissions, side-effects — into one language-agnostic, mathematical graph: the **Unified Behavior Graph (UBG)**, serialized as `.sparda/ubg.json` under the **SBIR** specification ([SPARDA Behavior IR](docs/SBIR_SPEC_V1.1.md)). Compile once; then every tool is a simple pass over that graph.
70
18
 
71
- Every finding is a **counterexample from the code + schema**, not a pattern-match — so
72
- no false-positive noise to train your team to ignore. Same engine, as a hard CI gate:
73
- `mode: apocalypse` (the default) fails the job on any critical/high and can upload
74
- SARIF to the Security tab.
19
+ **What the graph unlocks 100% local, deterministic, zero runtime dependencies, zero API key:**
75
20
 
76
- ## Prove the deploy
21
+ | Command | What it does |
22
+ |---|---|
23
+ | **`ubg`** | Compile the codebase to its behavior graph (Express · FastAPI · Next.js natively; **any** stack via OpenAPI) |
24
+ | **`apocalypse`** | *Prove the deploy* — no guard, invariant, transaction or aggregate boundary can be broken (SARIF + CI gate) |
25
+ | **`timeless`** | *Time-travel* — record a production request, replay it byte-identically, export the bug as a test |
26
+ | **`heal`** | *Self-heal, proven* — bug → fix → the machine proves the fix is correct and breaks nothing |
27
+ | **`mirror`** | *Execute the graph* — serve the compiled behavior over HTTP with no framework and no source |
28
+ | **`openapi`** | *Emit the standard* — produce an OpenAPI 3.1 spec from the graph |
29
+ | **`verify`** | *Prove the compiler's own laws* (determinism, soundness, round-trip) on your app |
30
+ | **`init` / `dev`** | Expose the graph to AI clients as a live MCP server (+ Twin, Immune, Evolution runtime layer) |
77
31
 
78
32
  ```bash
79
- npx sparda-mcp apocalypse # PROVEN, RISKY, or NOT PROVEN — with counterexamples
80
- npx sparda-mcp apocalypse --sarif # findings land in GitHub's Security tab
33
+ npx sparda-mcp ubg # compile your backend .sparda/ubg.json
34
+ npx sparda-mcp apocalypse # prove the current tree is safe to deploy
81
35
  ```
82
36
 
83
- Five proof obligations discharged over the compiled graphunguarded mutations,
84
- unvalidated constrained writes, non-atomic aggregate writes, irreversible external
85
- calls, aggregate bypasses — plus a diff against your saved baseline: no entrypoint
86
- silently removed, no guard dropped, no blast radius grown. Exit 1 gates the pipeline.
87
-
88
- ## A mock that can never lie
89
-
90
- ```bash
91
- npx sparda-mcp mirror # the graph IS the server — no framework, no source
92
- ```
37
+ No cloud account. No server to host. Exposing raw APIs to AI is the old waySPARDA compiles the whole system's behavior, then proves, replays, heals, and serves it.
93
38
 
94
- The mirror serves your compiled behavior over HTTP: guards actually deny (401),
95
- responses come out typed, and the **inferred state machine is enforced** — `POST
96
- /orders` seeds `pending`, paying it moves it to `paid`, paying it *again* is refused
97
- `409`. Hand-written mocks drift; this one is derived from your code + schema, so it's
98
- synchronized by construction. Front-ends develop against backends that aren't
99
- deployed — or written — yet.
39
+ **Nomenclature:** **SBIR** is the specification (the format, like "JSON"); **UBG** is the compiled graph itself (the artifact, `ubg.json`). The MCP server is one *output* of the graph, not the product.
100
40
 
101
- ## Give your AI safe hands (the MCP layer)
102
-
103
- Your AI can write code. It still can't *operate* your app — and giving it raw access
104
- usually means days of glue code and one prayer per `DELETE`. SPARDA deletes that work:
105
-
106
- ```bash
107
- npx sparda-mcp init # scan your Express/FastAPI/Next.js app, inject the MCP router — 3 minutes
108
- npx sparda-mcp dev # connect Claude Desktop / Claude Code. Done.
109
- ```
110
-
111
- No OpenAPI spec. No account. No API key. No server to host. And the same trust rules
112
- apply at runtime: **writes are disabled by default**, an enabled write needs an
113
- explicit human confirmation (two-phase, single-use token), and every write is followed
114
- by a proof-after-write read-back.
115
-
116
- <details>
117
- <summary><b>Quickstart — three steps</b></summary>
41
+ ## Quickstart
118
42
 
119
43
  1. **Scan + inject** — run once, from your app's directory:
120
44
  ```bash
@@ -144,24 +68,130 @@ by a proof-after-write read-back.
144
68
  Claude Code connects to the same bridge. That's it — your running app is now a set
145
69
  of MCP tools your AI can call.
146
70
 
147
- </details>
148
-
149
71
  ## Try the Standalone Demo
150
72
 
151
73
  To see SPARDA in action instantly without modifying your codebase:
152
74
  ```bash
153
75
  npx sparda-mcp demo
154
76
  ```
155
- This runs the entire lifecycle (detect → parse → generate → inject → remove) on a bundled demo app in a temporary folder, illustrating all six guarantees in 10 seconds.
77
+ This runs the entire MCP lifecycle (detect → parse → generate → inject → remove) on a bundled demo app in a temporary folder, in about 10 seconds. For the compiler itself, run `npx sparda-mcp ubg` then `apocalypse` on any Express/FastAPI app.
156
78
 
157
79
  ## Black Box Report
158
80
 
159
81
  SPARDA is designed as a local organism. To see what it remembers and how much compute it has recycled:
160
82
  ```bash
161
- npx sparda-mcp report # terminal dashboard
162
- npx sparda-mcp report --html # self-contained offline dashboard at .sparda/report.html
163
- npx sparda-mcp report --json # raw JSON for integration
83
+ npx sparda-mcp report
84
+ ```
85
+ This prints a terminal dashboard aggregating your exposed tools, write opt-ins, proof journal decisions, and crystallized composite tools.
86
+
87
+ To write a self-contained, offline HTML dashboard at `.sparda/report.html`, append the `--html` flag:
88
+ ```bash
89
+ npx sparda-mcp report --html
90
+ ```
91
+
92
+ To output raw JSON for integration:
93
+ ```bash
94
+ npx sparda-mcp report --json
95
+ ```
96
+
97
+ ## Deployment Proof: Apocalypse
98
+
99
+ 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:
100
+ ```bash
101
+ npx sparda-mcp apocalypse
102
+ ```
103
+ This command reads the compiled `.sparda/ubg.json` (with zero source code parsing at runtime) and discharges five static correctness obligations:
104
+ * **Unguarded Mutation (Critical)**: Flags any mutation path that does not cross a security `guard`.
105
+ * **Non-Atomic Aggregate Write (High)**: Flags when an API writes to multiple tables of the same Consistency Domain (Aggregate) outside a single transaction scope.
106
+ * **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).
107
+ * **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).
108
+ * **Aggregate Member Bypass (Info)**: Flags mutating a member table directly without routing through the aggregate root.
109
+
110
+ To save your current graph as a safe baseline:
111
+ ```bash
112
+ npx sparda-mcp apocalypse --save-baseline
164
113
  ```
114
+ Subsequent runs will diff the candidate graph against this baseline to detect regression vectors:
115
+ * Deletion of any security `guard` (Critical).
116
+ * Deletion of a database SQL invariant (High).
117
+ * API blast radius expansion (Medium).
118
+
119
+ If any Critical or High finding is found, `apocalypse` exits with a non-zero code to block your CI pipeline.
120
+
121
+ **One step in your workflow — findings land in the GitHub Security tab (SARIF):**
122
+ ```yaml
123
+ - uses: zyx77550/sparda@main
124
+ with:
125
+ sarif: 'true'
126
+ ```
127
+
128
+ ## Time Travel: Timeless
129
+
130
+ 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:
131
+
132
+ ```bash
133
+ npx sparda-mcp timeless # list recorded flights
134
+ npx sparda-mcp timeless replay <id> # re-fly it — byte-identical or loud divergence
135
+ npx sparda-mcp timeless export <id> # the production bug is now a vitest test
136
+ ```
137
+
138
+ Recording is two lines in your app (ESM), with deterministic sampling and GDPR redaction built in:
139
+ ```js
140
+ import { getFlightBox } from 'sparda-mcp/src/flight/box.js';
141
+ const box = getFlightBox(); box.arm();
142
+ app.use(box.middleware({ sample: 100 })); // 1 request in 100; passwords/tokens redacted by default
143
+ const db = box.wrapClient(pgPool); // your query client, tapped
144
+ ```
145
+
146
+ 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).
147
+
148
+ ## Self-Healing, Proven: `sparda heal`
149
+
150
+ The loop above, as **one gesture** — and the machine judges the fix, whoever wrote it:
151
+
152
+ ```bash
153
+ npx sparda-mcp heal <flightId> # diagnose + write the fix brief
154
+ # ...apply the fix (a human, or --agent "your-ai-cli")...
155
+ npx sparda-mcp heal <flightId> --check --expect '{"status":404}'
156
+ ```
157
+
158
+ The brief is built from the graph itself — it hands the fixer the handler's `file:line`, the capabilities the fix must not grow, and the guards it must not remove. Then the **gate** — the actual product — proves the fix on three axes at once:
159
+
160
+ 1. **Behavior** — lenient replay of the recorded flight (same deterministic inputs) now produces the *expected* response, not the recorded bug. The fix may reformulate a query (the tap is relabeled, allowed); it may **not** change the effect order or kinds.
161
+ 2. **Compiler laws** — `verify` still passes: the graph is still sound and deterministic.
162
+ 3. **No regression** — `apocalypse` diff against the frozen pre-fix graph: zero new critical/high findings, no guard removed, no blast radius grown.
163
+
164
+ ```
165
+ ✓ HEALED & PROVEN — same recorded inputs, correct output, zero law broken, zero protection lost. Ship it.
166
+ ```
167
+
168
+ The gate is honest in both directions: an unfixed bug, or a "fix" that silently drops a guard, keeps it **closed** (exit 1). This is the difference between an AI that writes plausible code and a system that *proves* the code is correct — the trust layer the agent era is missing.
169
+
170
+ ## Any Backend On Earth: OpenAPI Lowering
171
+
172
+ 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.
173
+
174
+ ```bash
175
+ npx sparda-mcp ubg --openapi openapi.json
176
+ ```
177
+
178
+ 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.)
179
+
180
+ ## The Mirror VM: delete the framework, the app still answers
181
+
182
+ The graph is not a diagram — it executes:
183
+
184
+ ```bash
185
+ npx sparda-mcp mirror
186
+ ```
187
+
188
+ ```
189
+ MIRROR — the graph is serving. 3 entrypoint(s) on http://127.0.0.1:4477
190
+ GET /orders/{orderId} → {amount, id, status}
191
+ POST /orders 🔒 bearerAuth → {amount, id, status}
192
+ ```
193
+
194
+ 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.
165
195
 
166
196
  To undo everything: **`npx sparda-mcp remove`** restores your code byte-for-byte.
167
197
 
@@ -173,10 +203,10 @@ To undo everything: **`npx sparda-mcp remove`** restores your code byte-for-byte
173
203
 
174
204
  <br/>
175
205
 
176
- 1. **Proof, not opinion.** Every review/apocalypse finding is a counterexample derived from your code + schema deterministic, byte-identical run after run, machine after machine.
177
- 2. **Three minutes, one command.** AST scan, router generation, reversible injection no config.
178
- 3. **Try it for free, leave for free.** `npx sparda-mcp remove` restores your code **byte-for-byte** (tested on JS, TS, Python, even Windows CRLF files). No trace, no lock-in.
179
- 4. **The AI cannot write until you say so.** Every POST/PUT/DELETE is disabled by default; you enable per tool, and your choice survives every re-run.
206
+ 1. **Three minutes, one command.** AST scan, router generation, reversible injectionno config.
207
+ 2. **Try it for free, leave for free.** `npx sparda-mcp remove` restores your code **byte-for-byte** (tested on JS, TS, Python, even Windows CRLF files). No trace, no lock-in.
208
+ 3. **The AI cannot write until you say so.** Every POST/PUT/DELETE is disabled by default; you enable per tool, and your choice survives every re-run.
209
+ 4. **Your app defends itself.** A route failing 3 times in a row is quarantined the AI can't hammer your broken production. Latency anomalies are flagged. Zero LLM needed.
180
210
  5. **Nothing leaves your machine.** No telemetry to us, no cloud, local key auth, 4 exact-pinned dependencies.
181
211
  6. **What it learns is never lost.** Diagnoses, descriptions, settings — versioned with your git, surviving every re-init.
182
212
 
@@ -184,14 +214,23 @@ What we *don't* promise: the honest limits in [docs/SECURITY.md](./docs/SECURITY
184
214
 
185
215
  ## How it works
186
216
 
187
- 1. **The compiler.** `sparda ubg` (run implicitly by `review`/`apocalypse`/`mirror`) parses your codebase and SQL/Prisma schema (AST 100% local, zero LLM) into a deterministic behavior graph: entrypoints, guards, effects, state, invariants, state machines. `sparda verify` proves the compiler's own laws (determinism, soundness, round-trip) on your input.
188
- 2. **The proofs.** `review` and `apocalypse` discharge proof obligations over that graph; `mirror` executes it; `timeless` records/replays real requests against it.
189
- 3. **The MCP layer.** `init` injects a tiny marked router (`/mcp`) into your app — fully reversible with `remove`. Tool calls run **inside your live app process** warm DB pools, real auth chain, real data. Zero infrastructure: compute from your host process, intelligence from your AI client's own model (MCP sampling), storage from `sparda.json` + git.
217
+ 1. `npx sparda-mcp init` parses your codebase (AST), extracts every route, and injects a tiny marked router (`/mcp`) into your app fully reversible with `npx sparda-mcp remove`.
218
+ 2. Tool calls run **inside your live app process** warm DB pools, real auth chain, real data. SPARDA adds no infrastructure: compute comes from your host process, intelligence from your AI client's own model (MCP sampling), storage from `sparda.json` + git.
219
+ 3. Write tools (POST/PUT/DELETE) are **disabled by default**. You opt in per tool in `sparda.json` your choices survive re-runs.
190
220
  4. Suspicious docstrings are sanitized before they ever reach the AI (prompt-injection defense).
221
+ 5. `npx sparda-mcp doctor --app` audits your codebase for drift: it detects stale tools (IA seeing ghosts), unsynced routes, schema drift via fingerprints, and zombie configurations. High severity issues trigger a non-zero exit code for your CI pipeline.
222
+ 6. `npx sparda-mcp seed export/import` lets you package and share your app's "genome" (semantic memory, workflows, antibodies) securely, transferring immune memory between environments or across similar stacks with zero data leak.
223
+ 7. `npx sparda-mcp twin` starts a safe, simulated mock server of your backend on the original port. It serves GET calls from learned exemplars (observed response shapes & mock data) and returns simulated 202 writes without ever touching your real database or production APIs. Learn exemplars by running `npx sparda-mcp twin --learn`.
224
+ 8. `npx sparda-mcp grammar` maps the graph of valid sequences of tool calls (observed circuits and candidate hypotheses) to prevent LLM hallucination of routes.
225
+ 9. `npx sparda-mcp evolve` mutates candidate chains and tests them against the twin in-memory, promoting successful chains to evolved workflow suggestions.
191
226
 
192
- ## The living organism what runtime trust looks like
227
+ ## What SPARDA gives your AI
193
228
 
194
- The proof gate covers what AI *writes*. These organs cover what AI *does*, live:
229
+ ### Operate, not just read
230
+ Every route becomes a tool that runs against your live process — real auth, real data,
231
+ warm connections. One call to **`sparda_get_context`** hands the AI the whole living
232
+ picture: enabled tools, suggested workflows, runtime telemetry, quarantine state, and
233
+ immune memory — so every session resumes where the last one stopped.
195
234
 
196
235
  ### Write-safety: the AI can't write until you say so
197
236
  - Writes (POST/PUT/DELETE) ship **disabled**. Enable them per tool in `sparda.json`; your choice survives every re-init.
@@ -223,10 +262,9 @@ step's real response. Write routes are never absorbed — their per-call confirm
223
262
  always stands.
224
263
 
225
264
  ### Living context & telemetry
226
- One call to **`sparda_get_context`** hands the AI the whole living picture: enabled
227
- tools, suggested workflows, runtime telemetry, quarantine state, and immune memory —
228
- so every session resumes where the last one stopped. `GET /mcp/stats` and
229
- `GET /mcp/events` expose the same live picture over HTTP.
265
+ `GET /mcp/stats` (per-tool calls/errors, tool "purity", quarantine state) and
266
+ `GET /mcp/events` (errors, latency anomalies, cached diagnoses) expose exactly what
267
+ your app is doing surfaced to the AI as live notifications.
230
268
 
231
269
  ## Built for AI clients: the bundled Skill
232
270
  SPARDA ships with an Agent Skill ([`SKILL.md`](./SKILL.md)) that teaches any compatible
@@ -241,10 +279,10 @@ runtime, so the guidance never goes stale.
241
279
  - **Next.js App Router (13/14/15)** — file-based injection. Since Next.js uses file-system routing, SPARDA simply creates a catch-all route handler under `app/mcp/[...sparda]/route.js`. **Nothing in your existing codebase's code is touched**; running `remove` simply deletes the generated file.
242
280
  - **Express 4/5** (JS/TS, ESM/CJS) — AST-based router injection.
243
281
  - **FastAPI** (Python >= 3.9) — AST-based router injection.
244
- - **Any backend with an OpenAPI spec** — `--openapi api.json` lowers the spec into the same behavior graph, so `review`, `apocalypse`, `mirror` and `openapi` work on Go, Rails, Java, .NET… without a parser.
245
282
 
246
283
  ## Security posture (honest)
247
284
  - 4 runtime dependencies, exact-pinned.
285
+ - **Dynamic Local Key Resolution.** The generated router contains no baked secrets. It resolves authorization keys at runtime from the `SPARDA_LOCAL_KEY` environment variable or the local gitignored `.sparda/key` file, and fails closed (503) when neither is found. For custom production or staging setups, you can override this behavior by exposing `SPARDA_LOCAL_KEY` in your environment.
248
286
  - Local key on every router call; self-reference loop protection; 30s timeouts; 8 KB output truncation.
249
287
  - AST-positioned injection with backup and post-injection re-parse; `npx sparda-mcp remove` leaves a clean git diff.
250
288
  - Persistence is **value-free**: SPARDA records structure (tool names, field names, fingerprints), never your payloads.
package/SKILL.md CHANGED
@@ -1,42 +1,32 @@
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 Unified
6
+ Behavior Graph - UBG) — 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
- routeswrapped 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). This is the runtime half of SPARDA's
21
- trust layer — "AI writes. SPARDA proves." — the same engine that proves PRs and
22
- deploys (`sparda review` / `apocalypse`) also guards what you *do* to the live app.
23
- Used naively it's just an API. Used well, it gets faster and safer the more you
24
- call it. This skill is how to use it well.
14
+ A SPARDA server is driven by a compiled **Unified Behavior Graph (UBG)** the graph SPARDA's compiler produces from the host application's states, transitions, permissions, and side-effects, serialized under the **SBIR** specification. Instead of exposing raw, disconnected endpoints, SPARDA compiles the app 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.
15
+
16
+ > This skill covers the **runtime** (driving a live MCP server) the live half of SPARDA's trust layer: *"AI writes. SPARDA proves."* The same graph also powers dev-time proof commands you run in the app's repo — `sparda review` (the behavior diff of a PR), `apocalypse` (prove the deploy), `timeless` (record/replay a request), `heal` (prove a fix), `mirror` (serve the graph), `ubg` (compile), `verify` (prove the compiler's laws). Those are CLI, not MCP tools; see the project README.
25
17
 
26
18
  ## Rule 0 — call `sparda_get_context` first, every session
27
19
 
28
- Before anything else, call **`sparda_get_context`** (no params). It returns the
29
- *live* picture, so you never guess the surface:
20
+ Before anything else, call **`sparda_get_context`** (no params). It returns the *live* state of the SPARDA Behavior Graph:
30
21
 
31
- - the enabled tools + their descriptions, and SPARDA's suggested **workflows**;
32
- - `runtime` — current `/mcp/stats` (per-tool call/error counts, tool "purity",
33
- quarantine state);
34
- - the last ~20 **events** (errors, latency anomalies, immune diagnoses);
35
- - **immune memory** (known failure signatures + cached fixes);
36
- - **recycling** stats (how many calls were served from memory);
37
- - a `behavior` snapshot (which response fields are stable).
22
+ - the active routes/tools, workflows, and type-propagated schemas;
23
+ - `runtime` — current stats (calls, errors, quarantine states, Twin mode active);
24
+ - the last ~20 **events** (errors, latency anomalies, immune alerts);
25
+ - **immune memory** (cached antibodies and error diagnoses);
26
+ - **recycling** metrics (flywheel memory hit rates);
27
+ - a `behavior` snapshot of stable variables.
38
28
 
39
- Read it before acting. `sparda_info` gives a lighter summary if you only need counts.
29
+ Read it to orient yourself inside the graph. `sparda_info` gives a lighter summary of counts.
40
30
 
41
31
  ## The tools you'll see
42
32
 
@@ -86,6 +76,16 @@ adapt — don't blindly retry the same call.
86
76
  **5. Latency anomalies.** A call far slower than a tool's own baseline surfaces as
87
77
  an `immune` event in `/mcp/events`. Treat it as a hint to back off or warn the user.
88
78
 
79
+ **6. Twin Simulation Mode — practice safely on a clone.**
80
+ When `/mcp/stats` or `sparda_get_context.runtime` contains `"twin": true`, you are connected to a safe, in-memory mock clone of the application.
81
+ - All GET reads return learned exemplars (observed response shapes and mock values).
82
+ - All write tools return simulated `202` echoes but do not write to database or external APIs.
83
+ - Use this twin mode to practice multi-step workflows, debug tool sequences, and test your plans without touching the live production backend.
84
+
85
+ **7. Grammar & Evolution — discover optimal workflows.**
86
+ - You can query or contribute to the app's grammar (`.sparda/grammar.json`). The grammar maps valid sequences of tool calls (edges).
87
+ - Running `sparda evolve` mutates and runs candidate chains against the twin. The successful evolved sequences are suggested as mid-session workflows.
88
+
89
89
  ## Writing safely — the mandatory two-phase protocol
90
90
 
91
91
  Writes are **disabled by default**. The protocol is not optional:
@@ -138,6 +138,15 @@ Writes are **disabled by default**. The protocol is not optional:
138
138
  - **General health** → `sparda doctor` checks Node version, framework detection,
139
139
  manifest validity, the semantic/immune cache, host reachability, and quarantine;
140
140
  it exits non-zero so it can gate CI.
141
+ - **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.
142
+ - **OpenAPI Ingestion** → Run `sparda ubg --openapi <openapi_spec.json>` to compile any non-JS/Python backend (Go, Java, Rails, Laravel, .NET) into a Unified Behavior Graph by mapping security schemes into guards and request/response structures. (JSON specs only — convert YAML once with `npx -y js-yaml spec.yaml > spec.json`.)
143
+ - **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.
144
+ - **Exporting OpenAPI 3.1 Spec** → Run `sparda openapi` to generate a valid, deterministic OpenAPI 3.1 spec dynamically from the compiled behavior graph.
145
+ - **Self-Verification** → Run `sparda verify` to test the compiler's own invariants (byte-determinism, soundness, and spec round-trip) to guarantee trust.
146
+ - **Time-Travel Debugging** → `sparda timeless` records a production request's nondeterminism (db, http, clock, random, uuid) and replays it byte-identically against current code. `sparda timeless replay <id>` re-flies it; `sparda timeless export <id>` turns the bug into a vitest test. Recording is opt-in in the app (deterministic sampling + GDPR redaction built in).
147
+ - **Self-Healing, Proven** → `sparda heal <id>` builds a fix brief from the graph, then `--check --expect '{"status":200}'` gates a candidate fix on three axes at once: the recorded flight now returns the expected response (not the bug), `verify` still passes, and `apocalypse` finds no new critical/high and no removed guard. The gate is honest both ways — an unfixed bug keeps it closed (exit 1).
148
+ - **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.
149
+ - **Learn exemplars** → Start your live app and run `sparda twin --learn` to fetch actual response data and construct `.sparda/twin.json` locally.
141
150
 
142
151
  ---
143
152
  *This skill ships with `sparda-mcp` and is regenerated from SPARDA's capability
@@ -0,0 +1 @@
1
+ {"v":"imm1","proven":true,"routes":[{"behaviorHash":"bh1_13969839d8a18d54aa0342618e56db6a","pol":121,"exposed":[]},{"behaviorHash":"bh1_2daf8a2b1b0ed393d443991a2b0700f6","pol":121,"exposed":[]},{"behaviorHash":"bh1_58fbf5ac38b6d71abc7204921de6b957","pol":121,"exposed":[]},{"behaviorHash":"bh1_58fbf5ac38b6d71abc7204921de6b957","pol":121,"exposed":[]},{"behaviorHash":"bh1_cc6788289c612af85b8d215b5bdf9b28","pol":121,"exposed":[]}],"posture":{"auth":{"protected":0,"exposed":0,"na":5},"atomicity":{"protected":0,"exposed":0,"na":5},"reversibility":{"protected":0,"exposed":0,"na":5},"validation":{"protected":0,"exposed":0,"na":5},"aggregate":{"protected":0,"exposed":0,"na":5}},"bytes":5}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparda-mcp",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "mcpName": "io.github.zyx77550/sparda-mcp",
5
5
  "description": "Compile backends to behavior graphs. Statically prove security, simulate APIs, and replay bugs.",
6
6
  "type": "module",
@@ -0,0 +1,47 @@
1
+ // commands/fingerprint.js — print the portable behavior fingerprint of each
2
+ // entrypoint (ADR-035, Brick 1). The hash is coordinate-free: the SAME behavioral
3
+ // shape in any other repo produces the SAME hash. This is the address a shared
4
+ // diagnosis (an antibody) is filed under — the seam where a local proof becomes
5
+ // collective memory. Read-only, offline, deterministic.
6
+ // sparda fingerprint human table
7
+ // sparda fingerprint --json raw [{ entrypoint, behaviorHash, descriptor }]
8
+ import { compileUBG } from '../ubg/compile.js';
9
+ import { canonicalizeGraph } from '../ubg/schema.js';
10
+ import { fingerprintGraph } from '../ubg/fingerprint.js';
11
+
12
+ export async function runFingerprint(opts) {
13
+ const canonical = canonicalizeGraph(
14
+ compileUBG(opts.cwd, { write: false, openapi: opts.openapi }).graph,
15
+ );
16
+ const prints = fingerprintGraph(canonical);
17
+
18
+ if (opts.json) {
19
+ console.log(JSON.stringify(prints, null, 2));
20
+ return { prints };
21
+ }
22
+
23
+ if (!prints.length) {
24
+ // Same honesty rule as apocalypse: nothing to address is not a success.
25
+ console.log(
26
+ '✗ NO FINGERPRINT — 0 routes reached. SPARDA could not see this app’s surface (a parser-coverage gap); there is nothing to address.',
27
+ );
28
+ process.exitCode = 1;
29
+ return { prints };
30
+ }
31
+
32
+ console.log(`BEHAVIOR FINGERPRINTS — ${prints.length} entrypoint(s), coordinate-free`);
33
+ for (const p of prints) {
34
+ const d = p.descriptor;
35
+ const parts = [
36
+ `${d.guards} guard${d.guards === 1 ? '' : 's'}`,
37
+ d.validated ? 'validated' : 'unvalidated',
38
+ d.effects.length ? d.effects.join(',') : 'no effects',
39
+ ];
40
+ if (d.observable) parts.push('observable');
41
+ console.log(` ${p.behaviorHash} ${short(p.entrypoint)}`);
42
+ console.log(` ${parts.join(' · ')}`);
43
+ }
44
+ return { prints };
45
+ }
46
+
47
+ const short = (id) => id.replace(/^entrypoint:/, '');
@@ -0,0 +1,57 @@
1
+ // commands/immunize.js — freeze the app's proven safety into a tiny capsule (ADR-037).
2
+ // The expensive reasoning (compile → obligations → polarity → behaviorHash) runs
3
+ // ONCE here; the output is a self-contained artifact of a few bytes per route that
4
+ // any consumer — CI, an agent, the runtime, another SPARDA install — reads by pure
5
+ // lookup, no recompile, no LLM, no network. BitNet's move applied to trust.
6
+ // sparda immunize write .sparda/immunity.json + a summary
7
+ // sparda immunize --json print the capsule to stdout
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { compileUBG } from '../ubg/compile.js';
11
+ import { canonicalizeGraph } from '../ubg/schema.js';
12
+ import { buildCapsule } from '../ubg/immunity.js';
13
+ import { AXES } from '../ubg/polarity.js';
14
+ import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
15
+
16
+ export async function runImmunize(opts) {
17
+ const canonical = canonicalizeGraph(
18
+ compileUBG(opts.cwd, { write: false, openapi: opts.openapi }).graph,
19
+ );
20
+ const capsule = buildCapsule(canonical);
21
+
22
+ if (opts.json) {
23
+ console.log(JSON.stringify(capsule, null, 2));
24
+ return { capsule };
25
+ }
26
+
27
+ if (!capsule.routes.length) {
28
+ console.log(
29
+ '✗ NO CAPSULE — 0 routes reached; nothing to immunize (a parser-coverage gap, not a pass).',
30
+ );
31
+ process.exitCode = 1;
32
+ return { capsule };
33
+ }
34
+
35
+ const dotSparda = path.join(opts.cwd, '.sparda');
36
+ fs.mkdirSync(dotSparda, { recursive: true });
37
+ const outPath = path.join(dotSparda, 'immunity.json');
38
+ atomicWrite(outPath, JSON.stringify(capsule) + '\n');
39
+ const wire = JSON.stringify(capsule).length;
40
+
41
+ console.log(
42
+ `IMMUNITY CAPSULE — ${capsule.routes.length} route(s) frozen into ${capsule.bytes} byte(s) of safety` +
43
+ ` (5 axes = 5 trits = 1 byte/route)`,
44
+ );
45
+ const exposed = AXES.filter((a) => capsule.posture[a].exposed > 0)
46
+ .map((a) => `${a}×${capsule.posture[a].exposed}`)
47
+ .join(', ');
48
+ console.log(
49
+ ` verdict: ${capsule.proven ? '✓ PROVEN' : '✗ NOT PROVEN'}` +
50
+ (exposed ? ` — exposed: ${exposed}` : ''),
51
+ );
52
+ console.log(
53
+ ` written: .sparda/immunity.json (${wire} bytes on the wire) — portable, offline, lookup by behaviorHash`,
54
+ );
55
+ if (!capsule.proven) process.exitCode = 1;
56
+ return { capsule, outPath };
57
+ }
@@ -0,0 +1,57 @@
1
+ // commands/polarity.js — show the ternary behavior matrix (ADR-036).
2
+ // Each route becomes a row of {−, ·, +} over the five safety obligations; the
3
+ // verdict is a sign check and the app posture is a column sum. This is proof as
4
+ // arithmetic — the representation that lets the collective genome compose behavior
5
+ // by adding ternary columns instead of re-running proofs. Read-only, deterministic.
6
+ // sparda polarity the matrix + posture + arithmetic verdict
7
+ // sparda polarity --json raw { polarity, posture, proven }
8
+ import { compileUBG } from '../ubg/compile.js';
9
+ import { canonicalizeGraph } from '../ubg/schema.js';
10
+ import { checkGraph } from '../ubg/apocalypse.js';
11
+ import {
12
+ AXES,
13
+ posture,
14
+ provenByPolarity,
15
+ polaritySignature,
16
+ exposedAxes,
17
+ } from '../ubg/polarity.js';
18
+
19
+ export async function runPolarity(opts) {
20
+ const canonical = canonicalizeGraph(
21
+ compileUBG(opts.cwd, { write: false, openapi: opts.openapi }).graph,
22
+ );
23
+ const { polarity } = checkGraph(canonical);
24
+ const proven = provenByPolarity(polarity);
25
+ const cols = posture(polarity);
26
+
27
+ if (opts.json) {
28
+ console.log(JSON.stringify({ proven, polarity, posture: cols }, null, 2));
29
+ return { polarity, posture: cols, proven };
30
+ }
31
+
32
+ if (!polarity.length) {
33
+ console.log(
34
+ '✗ NO POLARITY — 0 routes reached; nothing to score (a parser-coverage gap, not a pass).',
35
+ );
36
+ process.exitCode = 1;
37
+ return { polarity, posture: cols, proven };
38
+ }
39
+
40
+ console.log(`BEHAVIOR POLARITY — ${polarity.length} route(s) · ${AXES.join(' ')}`);
41
+ for (const p of polarity) {
42
+ const flag = exposedAxes(p.vector).length ? ' ✗' : '';
43
+ console.log(` ${short(p.entrypoint)}${flag}`);
44
+ console.log(` ${polaritySignature(p.vector)}`);
45
+ }
46
+ const exposed = AXES.filter((a) => cols[a].exposed > 0)
47
+ .map((a) => `${a}×${cols[a].exposed}`)
48
+ .join(', ');
49
+ console.log(
50
+ `\n${proven ? '✓ PROVEN' : '✗ NOT PROVEN'} (arithmetic: no gating −1)` +
51
+ (exposed ? ` — exposed: ${exposed}` : ''),
52
+ );
53
+ if (!proven) process.exitCode = 1;
54
+ return { polarity, posture: cols, proven };
55
+ }
56
+
57
+ const short = (id) => id.replace(/^entrypoint:/, '');
package/src/index.js CHANGED
@@ -117,6 +117,21 @@ try {
117
117
  await runReview(opts);
118
118
  break;
119
119
  }
120
+ case 'fingerprint': {
121
+ const { runFingerprint } = await import('./commands/fingerprint.js');
122
+ await runFingerprint(opts);
123
+ break;
124
+ }
125
+ case 'polarity': {
126
+ const { runPolarity } = await import('./commands/polarity.js');
127
+ await runPolarity(opts);
128
+ break;
129
+ }
130
+ case 'immunize': {
131
+ const { runImmunize } = await import('./commands/immunize.js');
132
+ await runImmunize(opts);
133
+ break;
134
+ }
120
135
  case 'timeless': {
121
136
  const { runTimeless } = await import('./commands/timeless.js');
122
137
  await runTimeless(opts, rest);
@@ -161,6 +176,9 @@ Usage:
161
176
  npx sparda-mcp ubg Compile the codebase to its Unified Behavior Graph (.sparda/ubg.json)
162
177
  npx sparda-mcp apocalypse Prove the deploy: guards, invariants, transactions, aggregates (--save-baseline)
163
178
  npx sparda-mcp review Semantic diff of a PR vs a base ref (--base main / --json / --markdown)
179
+ npx sparda-mcp fingerprint Portable behavior hash per route — the address for shared diagnoses (--json)
180
+ npx sparda-mcp polarity Ternary safety matrix per route — proof as arithmetic (--json)
181
+ npx sparda-mcp immunize Freeze proven safety into a tiny capsule (1 byte/route) — .sparda/immunity.json
164
182
  npx sparda-mcp timeless Replay production requests (list | replay <id> | export <id> → vitest)
165
183
  npx sparda-mcp mirror Serve the compiled graph over HTTP — no framework, no source (--port)
166
184
  npx sparda-mcp openapi Emit an OpenAPI 3.1 spec FROM the graph (--out / --json)
@@ -15,7 +15,7 @@ const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, info: 3 };
15
15
  const CONSTRAINING = new Set(['check', 'not_null', 'unique']);
16
16
 
17
17
  // canonical serialized graph ({ nodes: [], edges: [] }) → indexed view
18
- function indexGraph(graph) {
18
+ export function indexGraph(graph) {
19
19
  const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
20
20
  const cfOut = new Map();
21
21
  const mutOut = new Map();
@@ -40,7 +40,7 @@ function indexGraph(graph) {
40
40
  return { nodes, cfOut, mutOut, gateTargets, compensators, entrypoints };
41
41
  }
42
42
 
43
- function reachOf(epId, cfOut) {
43
+ export function reachOf(epId, cfOut) {
44
44
  const seen = new Set();
45
45
  const queue = [epId];
46
46
  while (queue.length) {
@@ -66,6 +66,12 @@ export function checkGraph(graph) {
66
66
  const g = indexGraph(graph);
67
67
  const findings = [];
68
68
  let obligations = 0;
69
+ // Behavior polarity (ADR-036): per entrypoint, a ternary vector over the same
70
+ // obligations checked below — +1 protection present, 0 not applicable, -1
71
+ // violated. Built HERE so a -1 is literally the same condition as the finding
72
+ // (one source of truth, no drift). The algebra over these vectors lives in
73
+ // ubg/polarity.js; this function just records them.
74
+ const polarity = [];
69
75
 
70
76
  for (const ep of g.entrypoints) {
71
77
  const reach = reachOf(ep.id, g.cfOut);
@@ -77,9 +83,11 @@ export function checkGraph(graph) {
77
83
  for (const e of g.mutOut.get(n.id) ?? []) writes.push({ effect: n, stateId: e.to });
78
84
  }
79
85
  const observables = reached.filter((n) => n?.kind === 'effect' && n.meta.observable);
86
+ const vec = { auth: 0, validation: 0, atomicity: 0, reversibility: 0, aggregate: 0 };
80
87
 
81
88
  // O1 — every mutation path must pass a guard
82
89
  obligations++;
90
+ if (writes.length) vec.auth = guards.length ? 1 : -1;
83
91
  if (writes.length && guards.length === 0) {
84
92
  findings.push({
85
93
  rule: 'UNGUARDED_MUTATION',
@@ -92,21 +100,20 @@ export function checkGraph(graph) {
92
100
 
93
101
  // O2 — writes into constrained tables need validated input
94
102
  obligations++;
95
- if (!ep.meta.inputValidated) {
96
- const constrained = writes.filter((w) =>
97
- (g.nodes.get(w.stateId)?.meta.invariants ?? []).some((i) =>
98
- CONSTRAINING.has(i.type),
99
- ),
100
- );
101
- if (constrained.length) {
102
- findings.push({
103
- rule: 'UNVALIDATED_CONSTRAINED_WRITE',
104
- severity: 'medium',
105
- entrypoint: ep.id,
106
- message: `${ep.label} writes ${fmtStates(constrained)} whose declared invariants (CHECK/NOT NULL/UNIQUE) can be violated by unvalidated input`,
107
- evidence: constrained.map((w) => w.stateId),
108
- });
109
- }
103
+ const constrained = writes.filter((w) =>
104
+ (g.nodes.get(w.stateId)?.meta.invariants ?? []).some((i) =>
105
+ CONSTRAINING.has(i.type),
106
+ ),
107
+ );
108
+ if (constrained.length) vec.validation = ep.meta.inputValidated ? 1 : -1;
109
+ if (!ep.meta.inputValidated && constrained.length) {
110
+ findings.push({
111
+ rule: 'UNVALIDATED_CONSTRAINED_WRITE',
112
+ severity: 'medium',
113
+ entrypoint: ep.id,
114
+ message: `${ep.label} writes ${fmtStates(constrained)} whose declared invariants (CHECK/NOT NULL/UNIQUE) can be violated by unvalidated input`,
115
+ evidence: constrained.map((w) => w.stateId),
116
+ });
110
117
  }
111
118
 
112
119
  // O3 — multi-table writes inside one aggregate must share a transaction
@@ -122,7 +129,9 @@ export function checkGraph(graph) {
122
129
  const states = new Set(ws.map((w) => w.stateId));
123
130
  if (states.size < 2) continue;
124
131
  const txIds = new Set(ws.map((w) => w.effect.meta.transaction?.id ?? null));
125
- if (txIds.size !== 1 || txIds.has(null)) {
132
+ const atomic = txIds.size === 1 && !txIds.has(null);
133
+ vec.atomicity = atomic && vec.atomicity !== -1 ? 1 : -1;
134
+ if (!atomic) {
126
135
  findings.push({
127
136
  rule: 'NON_ATOMIC_AGGREGATE_WRITE',
128
137
  severity: 'high',
@@ -137,17 +146,21 @@ export function checkGraph(graph) {
137
146
  // when the entrypoint also mutates state (the write can fail after the
138
147
  // world already saw the effect)
139
148
  obligations++;
140
- for (const obs of observables) {
141
- if (obs.meta.compensable) continue;
142
- if (g.compensators.has(obs.id)) continue; // the undo itself is not a risk
143
- if (!writes.length) continue;
144
- findings.push({
145
- rule: 'IRREVERSIBLE_OBSERVABLE',
146
- severity: 'high',
147
- entrypoint: ep.id,
148
- message: `${ep.label} makes an irreversible external call (${obs.meta.target ?? obs.label}) while also mutating state — no compensation path exists if the write fails`,
149
- evidence: [`${obs.id} (${locOf(obs)})`],
150
- });
149
+ if (observables.length && writes.length) {
150
+ let bad = false;
151
+ for (const obs of observables) {
152
+ if (obs.meta.compensable) continue;
153
+ if (g.compensators.has(obs.id)) continue; // the undo itself is not a risk
154
+ bad = true;
155
+ findings.push({
156
+ rule: 'IRREVERSIBLE_OBSERVABLE',
157
+ severity: 'high',
158
+ entrypoint: ep.id,
159
+ message: `${ep.label} makes an irreversible external call (${obs.meta.target ?? obs.label}) while also mutating state — no compensation path exists if the write fails`,
160
+ evidence: [`${obs.id} (${locOf(obs)})`],
161
+ });
162
+ }
163
+ vec.reversibility = bad ? -1 : 1;
151
164
  }
152
165
 
153
166
  // O5 — mutating an aggregate member without touching its root
@@ -160,6 +173,7 @@ export function checkGraph(graph) {
160
173
  g.nodes.get(o.stateId)?.meta.role === 'aggregate_root' &&
161
174
  g.nodes.get(o.stateId)?.meta.consistencyDomain === state.meta.consistencyDomain,
162
175
  );
176
+ vec.aggregate = touchesRoot && vec.aggregate !== -1 ? 1 : -1;
163
177
  if (!touchesRoot) {
164
178
  findings.push({
165
179
  rule: 'AGGREGATE_MEMBER_BYPASS',
@@ -170,9 +184,11 @@ export function checkGraph(graph) {
170
184
  });
171
185
  }
172
186
  }
187
+
188
+ polarity.push({ entrypoint: ep.id, vector: vec });
173
189
  }
174
190
 
175
- return { findings: sortFindings(findings), obligations };
191
+ return { findings: sortFindings(findings), obligations, polarity };
176
192
  }
177
193
 
178
194
  // ---------------------------------------------------------------------------
@@ -0,0 +1,97 @@
1
+ // ubg/fingerprint.js — portable behavior fingerprints (ADR-035, Brick 1).
2
+ //
3
+ // The UBG's node ids are content-derived but REPO-LOCAL: `logic:src/index.ts#foo:44`
4
+ // names a file, a line, a symbol. Two codebases that share the exact same behavior
5
+ // still get different ids. A *behavior fingerprint* erases those coordinates and
6
+ // keeps only the behavioral SHAPE of an entrypoint's reachable subgraph — verb,
7
+ // guard presence, validation, the kinds of effects it runs, and the invariant
8
+ // classes on the state it writes. Same shape in two different repos → same hash.
9
+ //
10
+ // That hash is an ADDRESS. It turns "a behavior" into something you can look up:
11
+ // the key under which a diagnosis (an antibody) learned once, anywhere, applies
12
+ // everywhere the same shape occurs. This file only computes the address; it makes
13
+ // no network call and stores nothing (hard rule #1). Deterministic, locale-
14
+ // independent, byte-stable machine to machine (the same promise as the graph).
15
+ import crypto from 'node:crypto';
16
+ import { indexGraph, reachOf } from './apocalypse.js';
17
+ import { cmp, stableStringify } from './schema.js';
18
+
19
+ export const FINGERPRINT_VERSION = 'bh1'; // behavior-hash v1 — bump on shape change
20
+
21
+ // entrypoint id is `entrypoint:${METHOD} ${path}`. The verb is behavioral; the
22
+ // literal path segments are repo-local, but the *arity* of its parameters
23
+ // (`:id`, `{id}`) is part of the shape.
24
+ function entrypointShape(ep) {
25
+ const m = /^entrypoint:(\S+)\s+(.*)$/.exec(ep.id);
26
+ const method = (m?.[1] ?? ep.meta?.method ?? 'GET').toUpperCase();
27
+ const path = m?.[2] ?? '';
28
+ const pathParams = (path.match(/:(\w+)|\{(\w+)\}/g) ?? []).length;
29
+ return { method, pathParams };
30
+ }
31
+
32
+ // One entrypoint → { behaviorHash, descriptor }. The descriptor is the inspectable
33
+ // pre-image (so a human/agent can see WHY two things share an address); the hash is
34
+ // sha256 over its canonical bytes. Everything here is coordinate-free by design.
35
+ export function fingerprintEntrypoint(indexed, ep) {
36
+ const { nodes, cfOut, mutOut } = indexed;
37
+ const reached = [...reachOf(ep.id, cfOut)].map((id) => nodes.get(id)).filter(Boolean);
38
+
39
+ const guards = reached.filter((n) => n.kind === 'guard').length;
40
+
41
+ const effects = []; // sorted multiset of effect atoms, e.g. "db_write:update"
42
+ const writes = []; // per-write: invariant CLASSES touched + tx/member flags
43
+ let observable = false;
44
+
45
+ for (const n of reached) {
46
+ if (n.kind !== 'effect') continue;
47
+ const type = n.meta?.effectType ?? n.id.split(':')[1] ?? 'effect';
48
+ const op = n.meta?.op ?? null;
49
+ effects.push(op ? `${type}:${op}` : type);
50
+ if (n.meta?.observable) observable = true;
51
+ for (const e of mutOut.get(n.id) ?? []) {
52
+ const state = nodes.get(e.to);
53
+ const invariants = [
54
+ ...new Set((state?.meta?.invariants ?? []).map((i) => i.type)),
55
+ ].sort(cmp); // classes only (check/unique/notnull) — never the table or expr
56
+ writes.push({
57
+ op: op ?? 'write',
58
+ invariants,
59
+ tx: Boolean(n.meta?.transaction),
60
+ member: state?.meta?.role === 'member',
61
+ });
62
+ }
63
+ }
64
+
65
+ effects.sort(cmp);
66
+ writes.sort((a, b) => cmp(stableStringify(a), stableStringify(b)));
67
+
68
+ const { method, pathParams } = entrypointShape(ep);
69
+ const descriptor = {
70
+ v: FINGERPRINT_VERSION,
71
+ method,
72
+ pathParams,
73
+ guards,
74
+ validated: Boolean(ep.meta?.inputValidated),
75
+ observable,
76
+ effects,
77
+ writes,
78
+ };
79
+ return { behaviorHash: hashOf(descriptor), descriptor };
80
+ }
81
+
82
+ // Whole graph → one fingerprint per entrypoint, sorted by id (deterministic).
83
+ export function fingerprintGraph(graph) {
84
+ const indexed = indexGraph(graph);
85
+ return indexed.entrypoints
86
+ .map((ep) => ({ entrypoint: ep.id, ...fingerprintEntrypoint(indexed, ep) }))
87
+ .sort((a, b) => cmp(a.entrypoint, b.entrypoint));
88
+ }
89
+
90
+ function hashOf(descriptor) {
91
+ const digest = crypto
92
+ .createHash('sha256')
93
+ .update(stableStringify(descriptor))
94
+ .digest('hex')
95
+ .slice(0, 32);
96
+ return `${FINGERPRINT_VERSION}_${digest}`;
97
+ }
@@ -0,0 +1,65 @@
1
+ // ubg/immunity.js — the immunity capsule (ADR-037).
2
+ //
3
+ // The capsule is the "mini-intelligence that costs nothing": SPARDA does the
4
+ // expensive static reasoning ONCE at compile time (graph → obligations → polarity
5
+ // + a portable behaviorHash per route), then freezes the result into a tiny,
6
+ // self-contained artifact. Each route reduces to (behaviorHash, one polarity byte).
7
+ // The capsule needs nothing to "run" — it is a frozen judgment consulted by a pure
8
+ // lookup: no recompile, no LLM, no network. That is BitNet's move applied to trust —
9
+ // amortize the thinking, ship a cheap representation that acts on its own.
10
+ //
11
+ // It is also the atom of the world genome (ADR-035): one app's capsule is its
12
+ // contribution; capsules compose (posture column-sums stack app → fleet → world).
13
+ import { fingerprintGraph } from './fingerprint.js';
14
+ import { checkGraph } from './apocalypse.js';
15
+ import { cmp } from './schema.js';
16
+ import { AXES, posture, provenByPolarity, packVector, exposedAxes } from './polarity.js';
17
+
18
+ export const CAPSULE_VERSION = 'imm1';
19
+
20
+ // canonical graph → a compact, portable, deterministic safety capsule.
21
+ export function buildCapsule(graph) {
22
+ const prints = new Map(fingerprintGraph(graph).map((p) => [p.entrypoint, p]));
23
+ const { polarity } = checkGraph(graph);
24
+
25
+ const routes = polarity
26
+ .map(({ entrypoint, vector }) => ({
27
+ behaviorHash: prints.get(entrypoint)?.behaviorHash ?? null,
28
+ pol: packVector(vector), // one byte: the route's whole safety character
29
+ exposed: exposedAxes(vector), // convenience; derivable from pol
30
+ }))
31
+ .sort((a, b) => cmp(a.behaviorHash ?? '', b.behaviorHash ?? ''));
32
+
33
+ return {
34
+ v: CAPSULE_VERSION,
35
+ proven: provenByPolarity(polarity),
36
+ routes,
37
+ posture: posture(polarity),
38
+ bytes: routes.length, // the whole app's safety, one byte per route
39
+ };
40
+ }
41
+
42
+ // The lookup that makes the capsule act on its own: given a behavior shape, what
43
+ // does this capsule already know about it? Pure, O(routes), no compute. This is the
44
+ // seam an agent (or the runtime) consults BEFORE doing expensive work — the local,
45
+ // offline face of collective immunity.
46
+ export function judge(capsule, behaviorHash) {
47
+ const hit = capsule.routes.find((r) => r.behaviorHash === behaviorHash);
48
+ if (!hit) return { known: false };
49
+ return {
50
+ known: true,
51
+ pol: hit.pol,
52
+ exposed: hit.exposed,
53
+ safe: hit.exposed.length === 0,
54
+ };
55
+ }
56
+
57
+ // Merge capsules into a fleet posture — composition is a column sum, nothing more.
58
+ export function mergePosture(capsules) {
59
+ const cols = {};
60
+ for (const a of AXES) cols[a] = { protected: 0, exposed: 0, na: 0 };
61
+ for (const c of capsules)
62
+ for (const a of AXES)
63
+ for (const k of ['protected', 'exposed', 'na']) cols[a][k] += c.posture[a][k];
64
+ return cols;
65
+ }
@@ -0,0 +1,120 @@
1
+ // ubg/polarity.js — the ternary behavior algebra (ADR-036).
2
+ //
3
+ // Inspiration: BitNet reduces every network weight to {-1, 0, +1} so matrix
4
+ // multiplication collapses into addition. We do the analogue for VERIFICATION.
5
+ // Each entrypoint, against each safety obligation, is reduced to one ternary
6
+ // digit: +1 the protection is present, 0 the obligation does not apply, -1 it is
7
+ // violated. The vector is produced in checkGraph (one source of truth with the
8
+ // findings — a -1 IS a finding). This file is the algebra over those vectors:
9
+ //
10
+ // • a verdict is a sign check — PROVEN ⇔ no gating axis is -1
11
+ // • a PR review is a SUBTRACTION — candidate − base; a negative delta on an
12
+ // axis means the change REMOVED a protection
13
+ // • a posture is a COLUMN SUM — count the -1/+1/0 per axis across routes;
14
+ // stack routes → app, stack apps → fleet.
15
+ //
16
+ // That closure — verification as add/subtract over a tiny alphabet — is what lets
17
+ // the collective genome (ADR-035) compose behavior at scale: merging knowledge is
18
+ // adding ternary columns, not re-running proofs. No floats, no drift, deterministic.
19
+ import { cmp } from './schema.js';
20
+
21
+ // The five obligations apocalypse discharges, in gating order. Severity is the
22
+ // same mapping the findings use, so the arithmetic verdict matches the worded one.
23
+ export const AXES = Object.freeze([
24
+ 'auth', // O1 UNGUARDED_MUTATION
25
+ 'atomicity', // O3 NON_ATOMIC_AGGREGATE_WRITE
26
+ 'reversibility', // O4 IRREVERSIBLE_OBSERVABLE
27
+ 'validation', // O2 UNVALIDATED_CONSTRAINED_WRITE
28
+ 'aggregate', // O5 AGGREGATE_MEMBER_BYPASS
29
+ ]);
30
+
31
+ export const AXIS_SEVERITY = Object.freeze({
32
+ auth: 'critical',
33
+ atomicity: 'high',
34
+ reversibility: 'high',
35
+ validation: 'medium',
36
+ aggregate: 'info',
37
+ });
38
+
39
+ const GATING = new Set(['critical', 'high']); // a -1 here means NOT PROVEN
40
+ export const POLARITY_SYMBOL = Object.freeze({ '-1': '−', 0: '·', 1: '+' });
41
+
42
+ // A route is "exposed" on an axis when its digit is -1. Gating exposure (auth /
43
+ // atomicity / reversibility) is what flips a verdict to NOT PROVEN.
44
+ export function exposedAxes(vector) {
45
+ return AXES.filter((a) => vector[a] === -1);
46
+ }
47
+
48
+ // PROVEN ⇔ no gating axis is -1, across every entrypoint. Pure sign check — the
49
+ // arithmetic twin of verdictOf's `safe`.
50
+ export function provenByPolarity(polarity) {
51
+ return polarity.every((p) =>
52
+ exposedAxes(p.vector).every((a) => !GATING.has(AXIS_SEVERITY[a])),
53
+ );
54
+ }
55
+
56
+ // Column sums: the app's exposure profile. protected/exposed/na per axis — the
57
+ // ternary matrix collapsed to one row you can stack across apps into a fleet view.
58
+ export function posture(polarity) {
59
+ const cols = {};
60
+ for (const a of AXES) cols[a] = { protected: 0, exposed: 0, na: 0 };
61
+ for (const { vector } of polarity) {
62
+ for (const a of AXES) {
63
+ const d = vector[a];
64
+ cols[a][d === 1 ? 'protected' : d === -1 ? 'exposed' : 'na']++;
65
+ }
66
+ }
67
+ return cols;
68
+ }
69
+
70
+ // review as subtraction: candidate − base, per shared entrypoint, per axis. A
71
+ // negative delta means this change WEAKENED that axis (a protection removed); a
72
+ // positive delta means it strengthened it. `regressed` is the headline: the axes
73
+ // a PR made worse — the same thing diffGraphs reports in prose, as arithmetic.
74
+ export function polarityDelta(baseList, candList) {
75
+ const base = new Map(baseList.map((p) => [p.entrypoint, p.vector]));
76
+ const out = [];
77
+ for (const { entrypoint, vector } of candList) {
78
+ const b = base.get(entrypoint);
79
+ if (!b) continue; // new entrypoint — surfaced by the endpoint diff, not here
80
+ const deltas = {};
81
+ const regressed = [];
82
+ for (const a of AXES) {
83
+ const d = (vector[a] ?? 0) - (b[a] ?? 0);
84
+ deltas[a] = d;
85
+ if (d < 0) regressed.push(a);
86
+ }
87
+ if (regressed.length || Object.values(deltas).some((d) => d !== 0)) {
88
+ out.push({ entrypoint, deltas, regressed });
89
+ }
90
+ }
91
+ return out.sort((x, y) => cmp(x.entrypoint, y.entrypoint));
92
+ }
93
+
94
+ // Compact printable signature of a vector, e.g. "auth:− validation:+ …" — order
95
+ // fixed by AXES so it is stable across machines.
96
+ export function polaritySignature(vector) {
97
+ return AXES.map((a) => `${a}:${POLARITY_SYMBOL[vector[a]]}`).join(' ');
98
+ }
99
+
100
+ // --- BitNet-style packing: a route's whole safety character in ONE byte -------
101
+ // The five axes are five trits {-1,0,+1}. Five trits = 3^5 = 243 states < 256, so
102
+ // the entire polarity vector packs into a single byte (base-3, digit = trit+1).
103
+ // This is the atom of the immunity capsule (ADR-037): frozen judgment, near-zero
104
+ // storage, decoded by a pure table lookup — no recompile, no LLM, no network.
105
+ export function packVector(vector) {
106
+ let byte = 0;
107
+ for (let i = AXES.length - 1; i >= 0; i--)
108
+ byte = byte * 3 + ((vector[AXES[i]] ?? 0) + 1);
109
+ return byte; // 0..242
110
+ }
111
+
112
+ export function unpackVector(byte) {
113
+ const vector = {};
114
+ let b = byte;
115
+ for (const a of AXES) {
116
+ vector[a] = (b % 3) - 1;
117
+ b = Math.floor(b / 3);
118
+ }
119
+ return vector;
120
+ }
package/src/ubg/schema.js CHANGED
Binary file