sparda-mcp 0.71.1 → 0.71.4
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 +2 -2
- package/package.json +5 -5
- package/src/commands/apocalypse.js +1 -1
- package/src/commands/prove.js +1 -1
- package/src/commands/ubg.js +1 -1
- package/src/detect.js +1 -1
- package/src/index.js +1 -1
- package/src/probe/express-shim.cjs +126 -6
- package/src/probe/probe.js +99 -2
- package/src/server/stdio.js +2 -2
- package/src/ubg/premise.js +9 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
**The trust layer for AI-written backends.** SPARDA compiles your backend — routes, database queries, state mutations, guards, side-effects — into one deterministic behavior graph, then **statically proves what can and can't break before you ship**: no unguarded mutation, no broken invariant, no non-atomic aggregate write.
|
|
17
17
|
|
|
18
18
|
[](https://www.npmjs.com/package/sparda-mcp)
|
|
19
|
-
[](https://github.com/zakariagharzouli/sparda/actions/workflows/ci.yml)
|
|
20
20
|

|
|
21
21
|

|
|
22
22
|
[](./LICENSE)
|
|
@@ -184,7 +184,7 @@ If any Critical or High finding is found, `apocalypse` exits with a non-zero cod
|
|
|
184
184
|
**One step in your workflow — findings land in the GitHub Security tab (SARIF):**
|
|
185
185
|
|
|
186
186
|
```yaml
|
|
187
|
-
- uses:
|
|
187
|
+
- uses: zakariagharzouli/sparda@main
|
|
188
188
|
with:
|
|
189
189
|
sarif: 'true'
|
|
190
190
|
```
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparda-mcp",
|
|
3
|
-
"version": "0.71.
|
|
4
|
-
"mcpName": "io.github.
|
|
3
|
+
"version": "0.71.4",
|
|
4
|
+
"mcpName": "io.github.zakariagharzouli/sparda-mcp",
|
|
5
5
|
"description": "AI writes. SPARDA proves. A deterministic, offline gate that catches when an AI edit removes a guard, exposes a route, or breaks an invariant \u2014 no API key, right in the agent edit loop.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/zakariagharzouli/sparda.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/
|
|
11
|
+
"homepage": "https://github.com/zakariagharzouli/sparda#readme",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "https://github.com/zakariagharzouli/sparda/issues"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
16
|
"sparda": "src/index.js"
|
|
@@ -205,7 +205,7 @@ function toSarif(findings, canonical) {
|
|
|
205
205
|
tool: {
|
|
206
206
|
driver: {
|
|
207
207
|
name: 'sparda-apocalypse',
|
|
208
|
-
informationUri: 'https://github.com/
|
|
208
|
+
informationUri: 'https://github.com/zakariagharzouli/sparda',
|
|
209
209
|
rules: [...new Set(findings.map((f) => f.rule))].sort().map((id) => ({ id })),
|
|
210
210
|
},
|
|
211
211
|
},
|
package/src/commands/prove.js
CHANGED
|
@@ -275,7 +275,7 @@ function proveMarkdown({ verdict, report, findings, coverage, seal }) {
|
|
|
275
275
|
}
|
|
276
276
|
lines.push(
|
|
277
277
|
'',
|
|
278
|
-
`<sub>Proven locally by [SPARDA](https://github.com/
|
|
278
|
+
`<sub>Proven locally by [SPARDA](https://github.com/zakariagharzouli/sparda) — deterministic, no keys · seal \`${seal}\`</sub>`,
|
|
279
279
|
);
|
|
280
280
|
return lines.join('\n');
|
|
281
281
|
}
|
package/src/commands/ubg.js
CHANGED
|
@@ -40,7 +40,7 @@ export async function runUbg(opts) {
|
|
|
40
40
|
` ◐ 0 routes — SPARDA saw the framework but no route handlers. Run with --verbose,`,
|
|
41
41
|
);
|
|
42
42
|
console.log(
|
|
43
|
-
` or open an issue with your layout: github.com/
|
|
43
|
+
` or open an issue with your layout: github.com/zakariagharzouli/sparda/issues`,
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/detect.js
CHANGED
|
@@ -106,7 +106,7 @@ export function detectStack(cwd) {
|
|
|
106
106
|
if (known)
|
|
107
107
|
throw err(
|
|
108
108
|
`${known} detected — not supported yet. Express, NestJS, FastAPI & Flask in v0.`,
|
|
109
|
-
'+1 the framework vote: github.com/
|
|
109
|
+
'+1 the framework vote: github.com/zakariagharzouli/sparda/issues/1',
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
for (const f of ['requirements.txt', 'pyproject.toml']) {
|
package/src/index.js
CHANGED
|
@@ -334,7 +334,7 @@ By Residual Labs — residual-labs.fr`);
|
|
|
334
334
|
if (opts.verbose && err?.stack) console.error(err.stack);
|
|
335
335
|
else
|
|
336
336
|
console.error(
|
|
337
|
-
' (run with --verbose for details — or open an issue: github.com/
|
|
337
|
+
' (run with --verbose for details — or open an issue: github.com/zakariagharzouli/sparda/issues)',
|
|
338
338
|
);
|
|
339
339
|
process.exit(err?.code === 'USER' ? 1 : 2);
|
|
340
340
|
}
|
|
@@ -34,6 +34,12 @@ function installShim() {
|
|
|
34
34
|
const net = require('net');
|
|
35
35
|
const Module = require('module');
|
|
36
36
|
|
|
37
|
+
// Did we ever get our hands on the express module? The probe used to report "the app did not
|
|
38
|
+
// boot" for this, which is a WRONG diagnosis — sending the user to debug their app when the
|
|
39
|
+
// app was fine and the SHIM never hooked (E-109). Reported to the parent so the reason it
|
|
40
|
+
// prints is the true one.
|
|
41
|
+
let patched = false;
|
|
42
|
+
|
|
37
43
|
const IPC_PORT = parseInt(process.env.SPARDA_IPC_PORT, 10) || 0;
|
|
38
44
|
|
|
39
45
|
// ── Transport: fork IPC or TCP fallback ────────────────────────────────────
|
|
@@ -73,6 +79,7 @@ function installShim() {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
function sendDone() {
|
|
82
|
+
flush(); // the staged routes must go out BEFORE the parent is told there are no more
|
|
76
83
|
if (typeof process.send === 'function') {
|
|
77
84
|
try {
|
|
78
85
|
process.send({ type: '__done__' });
|
|
@@ -96,11 +103,65 @@ function installShim() {
|
|
|
96
103
|
idleTimer = setTimeout(sendDone, IDLE_MS);
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
// ── Routes are STAGED, not emitted — the mount point is not known yet ──────
|
|
107
|
+
//
|
|
108
|
+
// E-110. `usersRouter.get('/:id')` runs at import time; `app.use('/api/users', usersRouter)`
|
|
109
|
+
// runs later. Emitting at registration therefore reported `GET /:id`, and reconcile compared
|
|
110
|
+
// that against the compiler's (correct) `/api/users/:id` and called it a route the app serves
|
|
111
|
+
// and the compiler never saw. On demo-app: two FALSE premise gaps out of three.
|
|
112
|
+
//
|
|
113
|
+
// False gaps are in the SAFE direction for a verdict — they only make SPARDA refuse to claim
|
|
114
|
+
// PROVEN — which is exactly why this could sit unnoticed. They are in the WRONG direction for
|
|
115
|
+
// anything that consumes gaps as findings: every real Express app uses routers, so the most
|
|
116
|
+
// load-bearing signal the runtime oracle produces was also its noisiest.
|
|
117
|
+
//
|
|
118
|
+
// So: buffer each route with the OBJECT it was registered on, record the mount edges, and
|
|
119
|
+
// resolve full paths once the app is fully wired (at `listen`, or on idle).
|
|
120
|
+
const staged = [];
|
|
121
|
+
const mounts = []; // { parent, child, path }
|
|
122
|
+
let flushed = false;
|
|
123
|
+
|
|
124
|
+
function stage(owner, method, path) {
|
|
125
|
+
staged.push({ owner, method, path });
|
|
101
126
|
resetIdle();
|
|
102
127
|
}
|
|
103
128
|
|
|
129
|
+
const joinPaths = (prefix, path) => {
|
|
130
|
+
const a = String(prefix || '').replace(/\/+$/, '');
|
|
131
|
+
const b = String(path || '');
|
|
132
|
+
if (b === '/' || b === '') return a || '/';
|
|
133
|
+
return a + (b.startsWith('/') ? b : '/' + b) || '/';
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Every full path this owner is reachable at. A router CAN be mounted more than once, and then
|
|
137
|
+
// its routes genuinely exist at several paths — so this returns a list, never a single answer.
|
|
138
|
+
function prefixesOf(owner, depth = 0) {
|
|
139
|
+
if (depth > 12) return ['']; // pathological nesting or a cycle — stop, do not hang
|
|
140
|
+
const edges = mounts.filter((m) => m.child === owner);
|
|
141
|
+
if (edges.length === 0) return ['']; // a root (the app itself, or an unmounted router)
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const e of edges) {
|
|
144
|
+
for (const up of prefixesOf(e.parent, depth + 1)) out.push(joinPaths(up, e.path));
|
|
145
|
+
}
|
|
146
|
+
return out.length ? out : [''];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function flush() {
|
|
150
|
+
if (flushed) return;
|
|
151
|
+
flushed = true;
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
for (const r of staged) {
|
|
154
|
+
for (const prefix of prefixesOf(r.owner)) {
|
|
155
|
+
const full = joinPaths(prefix, r.path);
|
|
156
|
+
const key = `${r.method} ${full}`;
|
|
157
|
+
if (seen.has(key)) continue; // the same route reached twice is one route
|
|
158
|
+
seen.add(key);
|
|
159
|
+
sendMsg({ type: 'route', method: r.method, path: full });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
staged.length = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
104
165
|
// ── HTTP method list ───────────────────────────────────────────────────────
|
|
105
166
|
|
|
106
167
|
const HTTP_METHODS = [
|
|
@@ -131,11 +192,34 @@ function installShim() {
|
|
|
131
192
|
// Only record route registrations (path + at least one handler/middleware).
|
|
132
193
|
if (typeof path === 'string' && rest.length > 0) {
|
|
133
194
|
const verb = m === 'del' ? 'DELETE' : m.toUpperCase();
|
|
134
|
-
|
|
195
|
+
stage(this, verb, path);
|
|
135
196
|
}
|
|
136
197
|
return orig.call(this, path, ...rest);
|
|
137
198
|
};
|
|
138
199
|
}
|
|
200
|
+
|
|
201
|
+
// `use` is what makes a path mean something. Recorded, never altered: we only need to know
|
|
202
|
+
// WHICH object was mounted WHERE, so a route registered on it can be reported at the address
|
|
203
|
+
// the framework actually serves it from.
|
|
204
|
+
if (typeof target.use === 'function' && !target.__sparda_use_wrapped__) {
|
|
205
|
+
const origUse = target.use;
|
|
206
|
+
target.use = function spardaUse(...args) {
|
|
207
|
+
const mountPath = typeof args[0] === 'string' ? args[0] : '/';
|
|
208
|
+
for (const h of args) {
|
|
209
|
+
// A mounted router is a function carrying a middleware `stack` — that is what tells it
|
|
210
|
+
// apart from a plain handler like `express.json()`.
|
|
211
|
+
if (typeof h === 'function' && Array.isArray(h.stack))
|
|
212
|
+
mounts.push({ parent: this, child: h, path: mountPath });
|
|
213
|
+
}
|
|
214
|
+
return origUse.apply(this, args);
|
|
215
|
+
};
|
|
216
|
+
try {
|
|
217
|
+
Object.defineProperty(target, '__sparda_use_wrapped__', {
|
|
218
|
+
value: true,
|
|
219
|
+
configurable: true,
|
|
220
|
+
});
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
139
223
|
try {
|
|
140
224
|
Object.defineProperty(target, '__sparda_wrapped__', {
|
|
141
225
|
value: true,
|
|
@@ -217,7 +301,10 @@ function installShim() {
|
|
|
217
301
|
const originalLoad = Module._load;
|
|
218
302
|
Module._load = function spardaLoad(request) {
|
|
219
303
|
const result = originalLoad.apply(this, arguments);
|
|
220
|
-
if (request === 'express')
|
|
304
|
+
if (request === 'express') {
|
|
305
|
+
patched = true;
|
|
306
|
+
patchExpress(result);
|
|
307
|
+
}
|
|
221
308
|
return result;
|
|
222
309
|
};
|
|
223
310
|
|
|
@@ -231,10 +318,43 @@ function installShim() {
|
|
|
231
318
|
);
|
|
232
319
|
for (const p of expressPaths) {
|
|
233
320
|
const cached = require.cache[p];
|
|
234
|
-
if (cached && cached.exports)
|
|
321
|
+
if (cached && cached.exports) {
|
|
322
|
+
patched = true;
|
|
323
|
+
patchExpress(cached.exports);
|
|
324
|
+
}
|
|
235
325
|
}
|
|
236
326
|
} catch {}
|
|
237
327
|
|
|
328
|
+
// ── ESM entries: the hook above CANNOT fire, so pull express in ourselves ──
|
|
329
|
+
//
|
|
330
|
+
// E-109. `Module._load` interception is a CJS-loader mechanism. On Node 22 an ESM
|
|
331
|
+
// `import express from 'express'` does NOT go through `Module._load` — measured, and it
|
|
332
|
+
// contradicts the comment this shim's ESM wrapper used to carry. So for every Express app
|
|
333
|
+
// written in ESM (the modern default: `.mjs`, or `.js` under `"type": "module"`) the shim
|
|
334
|
+
// installed itself and then intercepted nothing, forever. The probe reported a timeout, the
|
|
335
|
+
// premise stayed `unmeasured`, and an ESM Express app could never reach PROVEN.
|
|
336
|
+
//
|
|
337
|
+
// The fix does not need a loader hook. express is CJS, and a CJS module loaded through the
|
|
338
|
+
// ESM bridge comes from the SAME `require.cache`: so if we require it FIRST, the app's later
|
|
339
|
+
// `import` receives the already-patched instance. Verified — the marker survives the import.
|
|
340
|
+
//
|
|
341
|
+
// Resolved from the ENTRY FILE, never from this shim's own directory: SPARDA carries express
|
|
342
|
+
// as a devDependency, and patching SPARDA's copy while the app imports its own would hook a
|
|
343
|
+
// module nobody uses — the same instance-identity trap, one level down.
|
|
344
|
+
if (!patched) {
|
|
345
|
+
try {
|
|
346
|
+
const path = require('path');
|
|
347
|
+
const from = process.argv[1] || path.join(process.cwd(), 'index.js');
|
|
348
|
+
const appRequire = Module.createRequire(path.resolve(from));
|
|
349
|
+
appRequire('express'); // goes through Module._load → sets `patched`, runs patchExpress
|
|
350
|
+
} catch {
|
|
351
|
+
// no express resolvable from the app — `patched` stays false and the parent is told so,
|
|
352
|
+
// which is a different statement from "the app did not boot"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
sendMsg({ type: '__shim__', patched });
|
|
357
|
+
|
|
238
358
|
// ── Safety nets ────────────────────────────────────────────────────────────
|
|
239
359
|
|
|
240
360
|
process.on('exit', () => {
|
|
@@ -247,5 +367,5 @@ function installShim() {
|
|
|
247
367
|
setTimeout(() => process.exit(0), 200);
|
|
248
368
|
});
|
|
249
369
|
|
|
250
|
-
module.exports = {
|
|
370
|
+
module.exports = { stage, flush, sendDone };
|
|
251
371
|
}
|
package/src/probe/probe.js
CHANGED
|
@@ -52,12 +52,28 @@ async function probeExpress({ entryFile, projectRoot, timeoutMs }) {
|
|
|
52
52
|
return [];
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Which shim FLAG is used turns out not to matter, and that is worth stating because the
|
|
56
|
+
// obvious "fix" for E-109 was to detect `"type": "module"` here and switch to `--import`. It
|
|
57
|
+
// changes nothing measurable: `--require` preloads the CJS shim fine ahead of an ESM entry, and
|
|
58
|
+
// the shim's own eager `require('express')` is what actually closes the hole. A second
|
|
59
|
+
// mechanism that no test can distinguish is dead weight — E-106, one week old, is what happens
|
|
60
|
+
// when such a line is kept.
|
|
55
61
|
const isEsm = ext === '.mjs';
|
|
56
62
|
const shimFlag = isEsm ? ['--import', SHIM_ESM] : ['--require', SHIM_CJS];
|
|
57
63
|
|
|
58
64
|
const routes = [];
|
|
59
65
|
let child;
|
|
60
66
|
|
|
67
|
+
// WHY the probe saw nothing is a different fact from THAT it saw nothing, and the old code
|
|
68
|
+
// could only say the second (E-109). It reported every silence as "the app did not boot",
|
|
69
|
+
// which sent users to debug a healthy app while the real cause was the shim never hooking.
|
|
70
|
+
// `shimPatched` stays null until the child says; the child's stderr is KEPT rather than
|
|
71
|
+
// dropped on the floor, because it is the only place an app's own crash is written down.
|
|
72
|
+
let shimPatched = null;
|
|
73
|
+
let timedOut = false;
|
|
74
|
+
let exitCode = null;
|
|
75
|
+
let stderrTail = '';
|
|
76
|
+
|
|
61
77
|
return new Promise((resolve_) => {
|
|
62
78
|
let settled = false;
|
|
63
79
|
let killTimer;
|
|
@@ -69,10 +85,23 @@ async function probeExpress({ entryFile, projectRoot, timeoutMs }) {
|
|
|
69
85
|
try {
|
|
70
86
|
child && child.kill('SIGKILL');
|
|
71
87
|
} catch {}
|
|
88
|
+
// Non-enumerable so `probeRoutes` keeps returning exactly an array of routes: every
|
|
89
|
+
// existing caller and assertion is untouched, and the reason travels with it.
|
|
90
|
+
Object.defineProperty(result, 'diagnostic', {
|
|
91
|
+
value: diagnose({
|
|
92
|
+
count: result.length,
|
|
93
|
+
shimPatched,
|
|
94
|
+
timedOut,
|
|
95
|
+
exitCode,
|
|
96
|
+
stderrTail,
|
|
97
|
+
}),
|
|
98
|
+
enumerable: false,
|
|
99
|
+
});
|
|
72
100
|
resolve_(result);
|
|
73
101
|
}
|
|
74
102
|
|
|
75
103
|
killTimer = setTimeout(() => {
|
|
104
|
+
timedOut = true;
|
|
76
105
|
process.stderr.write(
|
|
77
106
|
'[sparda] --probe: timeout waiting for Express routes; using static floor.\n',
|
|
78
107
|
);
|
|
@@ -95,10 +124,19 @@ async function probeExpress({ entryFile, projectRoot, timeoutMs }) {
|
|
|
95
124
|
return;
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
|
|
127
|
+
// KEPT, capped. The target's own boot error ("cannot connect to postgres") is written
|
|
128
|
+
// nowhere else, and discarding it is what made a shim that hooked nothing look identical
|
|
129
|
+
// to an app that refused to start.
|
|
130
|
+
child.stderr?.on('data', (chunk) => {
|
|
131
|
+
if (stderrTail.length < 4000) stderrTail += String(chunk);
|
|
132
|
+
});
|
|
99
133
|
|
|
100
134
|
child.on('message', (msg) => {
|
|
101
135
|
if (!msg || typeof msg !== 'object') return;
|
|
136
|
+
if (msg.type === '__shim__') {
|
|
137
|
+
shimPatched = msg.patched === true;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
102
140
|
if (msg.type === '__done__') {
|
|
103
141
|
settle(routes);
|
|
104
142
|
return;
|
|
@@ -121,10 +159,69 @@ async function probeExpress({ entryFile, projectRoot, timeoutMs }) {
|
|
|
121
159
|
settle(routes);
|
|
122
160
|
});
|
|
123
161
|
|
|
124
|
-
|
|
162
|
+
// `exit` gives the code; `close` is the one that means the stdio streams have been fully
|
|
163
|
+
// drained. Settling on `exit` races the last chunk of the child's stderr — under load the
|
|
164
|
+
// diagnostic came out without the very error it exists to carry.
|
|
165
|
+
child.on('exit', (code) => {
|
|
166
|
+
exitCode = code;
|
|
167
|
+
});
|
|
168
|
+
child.on('close', () => settle(routes));
|
|
125
169
|
});
|
|
126
170
|
}
|
|
127
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Why did the probe see what it saw? Four distinguishable states, and the point of naming them
|
|
174
|
+
* is that three of them used to print as the fourth (E-109):
|
|
175
|
+
*
|
|
176
|
+
* observed routes came back — nothing to explain
|
|
177
|
+
* not-instrumented the shim never got hold of express. NOT a boot failure: the app may be
|
|
178
|
+
* running perfectly. This is the ESM case, and the one that hid for a release.
|
|
179
|
+
* no-routes express WAS instrumented and the app registered none before we stopped.
|
|
180
|
+
* did-not-start the child never reported in at all, or exited non-zero — the app itself.
|
|
181
|
+
*
|
|
182
|
+
* `reason` is the sentence a user acts on, so it must never assert the app is broken when what
|
|
183
|
+
* actually happened is that SPARDA could not look.
|
|
184
|
+
*/
|
|
185
|
+
export function diagnose({ count, shimPatched, timedOut, exitCode, stderrTail }) {
|
|
186
|
+
const tail = String(stderrTail || '')
|
|
187
|
+
.trim()
|
|
188
|
+
.split('\n')
|
|
189
|
+
.slice(-3)
|
|
190
|
+
.join(' | ')
|
|
191
|
+
.slice(0, 300);
|
|
192
|
+
if (count > 0)
|
|
193
|
+
return { state: 'observed', reason: `${count} route(s) observed at runtime` };
|
|
194
|
+
// A non-zero exit wins over everything below: whatever we managed to instrument, the app
|
|
195
|
+
// itself died, and that is the fact its author needs. (Our own timeout SIGKILLs the child,
|
|
196
|
+
// which leaves exitCode null — so this cannot swallow the timeout cases.)
|
|
197
|
+
if (exitCode != null && exitCode !== 0)
|
|
198
|
+
return {
|
|
199
|
+
state: 'did-not-start',
|
|
200
|
+
reason: `the app exited ${exitCode} before serving anything`,
|
|
201
|
+
stderrTail: tail,
|
|
202
|
+
};
|
|
203
|
+
if (shimPatched === false)
|
|
204
|
+
return {
|
|
205
|
+
state: 'not-instrumented',
|
|
206
|
+
reason:
|
|
207
|
+
'the probe never got hold of the express module, so nothing could be observed — the app may well be running fine (this is not a boot failure)',
|
|
208
|
+
stderrTail: tail,
|
|
209
|
+
};
|
|
210
|
+
if (shimPatched === true)
|
|
211
|
+
return {
|
|
212
|
+
state: 'no-routes',
|
|
213
|
+
reason: timedOut
|
|
214
|
+
? 'express was instrumented but no route was registered before the timeout — the app may boot slowly or block on a dependency'
|
|
215
|
+
: 'express was instrumented and the app registered no routes',
|
|
216
|
+
stderrTail: tail,
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
state: 'did-not-start',
|
|
220
|
+
reason: `the probe child never reported in${exitCode != null ? ` (exit ${exitCode})` : ''} — the app did not start`,
|
|
221
|
+
stderrTail: tail,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
128
225
|
// ── FastAPI probe ─────────────────────────────────────────────────────────────
|
|
129
226
|
|
|
130
227
|
async function probeFastAPI({ entryFile, projectRoot, timeoutMs }) {
|
package/src/server/stdio.js
CHANGED
|
@@ -359,7 +359,7 @@ export async function startStdioBridge({ cwd, portOverride }) {
|
|
|
359
359
|
{
|
|
360
360
|
name: 'sparda_info',
|
|
361
361
|
description:
|
|
362
|
-
'Info about this SPARDA endpoint. SPARDA is the trust layer for AI-written code — AI writes, SPARDA proves: deterministic, offline behavior proofs (guards, invariants, irreversible effects) plus a live gate for AI edits, no API key. By Residual Labs (residual-labs.fr) — github.com/
|
|
362
|
+
'Info about this SPARDA endpoint. SPARDA is the trust layer for AI-written code — AI writes, SPARDA proves: deterministic, offline behavior proofs (guards, invariants, irreversible effects) plus a live gate for AI edits, no API key. By Residual Labs (residual-labs.fr) — github.com/zakariagharzouli/sparda',
|
|
363
363
|
inputSchema: { type: 'object', properties: {} },
|
|
364
364
|
},
|
|
365
365
|
{
|
|
@@ -477,7 +477,7 @@ export async function startStdioBridge({ cwd, portOverride }) {
|
|
|
477
477
|
circuits_observed: Object.keys(manifest.labs?.circuits ?? {}).length,
|
|
478
478
|
composite_tools: composites.size,
|
|
479
479
|
generated_by:
|
|
480
|
-
'SPARDA by Residual Labs (residual-labs.fr) — npx sparda-mcp init — github.com/
|
|
480
|
+
'SPARDA by Residual Labs (residual-labs.fr) — npx sparda-mcp init — github.com/zakariagharzouli/sparda',
|
|
481
481
|
},
|
|
482
482
|
null,
|
|
483
483
|
2,
|
package/src/ubg/premise.js
CHANGED
|
@@ -108,12 +108,20 @@ export async function verifyPremise(
|
|
|
108
108
|
// "an app with no routes". Treating that as a clean bill of health would be the
|
|
109
109
|
// worst possible reading — a broken oracle would silently CONFIRM every proof. So
|
|
110
110
|
// an empty probe is "unavailable", never "verified".
|
|
111
|
+
//
|
|
112
|
+
// The reason, however, must be the TRUE one. This used to read "the app did not boot, or
|
|
113
|
+
// exposes none" for every silence, including the case where the app was running perfectly and
|
|
114
|
+
// the shim had simply never hooked express — a wrong diagnosis that pointed the user at their
|
|
115
|
+
// own code (E-109). `probeRoutes` now attaches which of the four states actually happened.
|
|
111
116
|
if (!Array.isArray(probed) || probed.length === 0)
|
|
112
117
|
return {
|
|
113
118
|
available: false,
|
|
114
119
|
gaps: [],
|
|
115
120
|
probed: 0,
|
|
116
|
-
reason:
|
|
121
|
+
reason:
|
|
122
|
+
probed?.diagnostic?.reason ??
|
|
123
|
+
'probe returned no routes — the app did not boot, or exposes none',
|
|
124
|
+
...(probed?.diagnostic ? { probeState: probed.diagnostic.state } : {}),
|
|
117
125
|
};
|
|
118
126
|
|
|
119
127
|
const { gaps } = reconcile(entrypointsAsRoutes(graph), probed);
|