sparda-mcp 0.14.1 → 0.15.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 +155 -117
- package/SKILL.md +37 -28
- package/package.json +1 -1
- package/src/commands/fingerprint.js +47 -0
- package/src/commands/immunize.js +54 -0
- package/src/commands/polarity.js +57 -0
- package/src/index.js +18 -0
- package/src/ubg/apocalypse.js +46 -30
- package/src/ubg/fingerprint.js +97 -0
- package/src/ubg/immunity.js +65 -0
- package/src/ubg/polarity.js +120 -0
- package/src/ubg/schema.js +0 -0
package/README.md
CHANGED
|
@@ -6,115 +6,39 @@
|
|
|
6
6
|
|
|
7
7
|
<br/>
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
npx sparda-mcp apocalypse
|
|
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
|
-
|
|
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 way — SPARDA compiles the whole system's behavior, then proves, replays, heals, and serves it.
|
|
93
38
|
|
|
94
|
-
|
|
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
|
-
##
|
|
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,
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
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. **
|
|
177
|
-
2. **
|
|
178
|
-
3. **
|
|
179
|
-
4. **
|
|
206
|
+
1. **Three minutes, one command.** AST scan, router generation, reversible injection — no 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.
|
|
188
|
-
2. **
|
|
189
|
-
3.
|
|
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
|
-
##
|
|
227
|
+
## What SPARDA gives your AI
|
|
193
228
|
|
|
194
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
5
|
-
you are connected to
|
|
6
|
-
by the tools `sparda_get_context`, `sparda_info`,
|
|
7
|
-
|
|
8
|
-
to exploit the response-recycling flywheel, the circuit-breaker, crystallized
|
|
9
|
-
circuits
|
|
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
|
|
12
|
+
# Driving a SPARDA Behavior Runtime
|
|
15
13
|
|
|
16
|
-
A SPARDA server is **
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
32
|
-
- `runtime` — current
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
- **
|
|
36
|
-
-
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -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,54 @@
|
|
|
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 path from 'node:path';
|
|
9
|
+
import { compileUBG } from '../ubg/compile.js';
|
|
10
|
+
import { canonicalizeGraph } from '../ubg/schema.js';
|
|
11
|
+
import { buildCapsule } from '../ubg/immunity.js';
|
|
12
|
+
import { AXES } from '../ubg/polarity.js';
|
|
13
|
+
import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
|
|
14
|
+
|
|
15
|
+
export async function runImmunize(opts) {
|
|
16
|
+
const canonical = canonicalizeGraph(
|
|
17
|
+
compileUBG(opts.cwd, { write: false, openapi: opts.openapi }).graph,
|
|
18
|
+
);
|
|
19
|
+
const capsule = buildCapsule(canonical);
|
|
20
|
+
|
|
21
|
+
if (opts.json) {
|
|
22
|
+
console.log(JSON.stringify(capsule, null, 2));
|
|
23
|
+
return { capsule };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!capsule.routes.length) {
|
|
27
|
+
console.log(
|
|
28
|
+
'✗ NO CAPSULE — 0 routes reached; nothing to immunize (a parser-coverage gap, not a pass).',
|
|
29
|
+
);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return { capsule };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const outPath = path.join(opts.cwd, '.sparda', 'immunity.json');
|
|
35
|
+
atomicWrite(outPath, JSON.stringify(capsule) + '\n');
|
|
36
|
+
const wire = JSON.stringify(capsule).length;
|
|
37
|
+
|
|
38
|
+
console.log(
|
|
39
|
+
`IMMUNITY CAPSULE — ${capsule.routes.length} route(s) frozen into ${capsule.bytes} byte(s) of safety` +
|
|
40
|
+
` (5 axes = 5 trits = 1 byte/route)`,
|
|
41
|
+
);
|
|
42
|
+
const exposed = AXES.filter((a) => capsule.posture[a].exposed > 0)
|
|
43
|
+
.map((a) => `${a}×${capsule.posture[a].exposed}`)
|
|
44
|
+
.join(', ');
|
|
45
|
+
console.log(
|
|
46
|
+
` verdict: ${capsule.proven ? '✓ PROVEN' : '✗ NOT PROVEN'}` +
|
|
47
|
+
(exposed ? ` — exposed: ${exposed}` : ''),
|
|
48
|
+
);
|
|
49
|
+
console.log(
|
|
50
|
+
` written: .sparda/immunity.json (${wire} bytes on the wire) — portable, offline, lookup by behaviorHash`,
|
|
51
|
+
);
|
|
52
|
+
if (!capsule.proven) process.exitCode = 1;
|
|
53
|
+
return { capsule, outPath };
|
|
54
|
+
}
|
|
@@ -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)
|
package/src/ubg/apocalypse.js
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|