sparda-mcp 0.5.2 → 0.5.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.
@@ -6,7 +6,15 @@ import traverseModule from '@babel/traverse';
6
6
  const traverse = traverseModule.default ?? traverseModule;
7
7
 
8
8
  const HTTP = new Set(['get', 'post', 'put', 'patch', 'delete']);
9
- const EXCLUDE = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.sparda']);
9
+ const EXCLUDE = new Set([
10
+ 'node_modules',
11
+ '.git',
12
+ 'dist',
13
+ 'build',
14
+ '.next',
15
+ 'coverage',
16
+ '.sparda',
17
+ ]);
10
18
 
11
19
  export function parseExpressProject(cwd, entryFile) {
12
20
  const routes = [];
@@ -29,7 +37,11 @@ export function parseExpressProject(cwd, entryFile) {
29
37
  if (depth > 2 || visited.has(absFile + '::' + prefix)) return;
30
38
  visited.add(absFile + '::' + prefix);
31
39
  let src;
32
- try { src = fs.readFileSync(absFile, 'utf8'); } catch { return; }
40
+ try {
41
+ src = fs.readFileSync(absFile, 'utf8');
42
+ } catch {
43
+ return;
44
+ }
33
45
  let ast;
34
46
  try {
35
47
  ast = parse(src, {
@@ -38,13 +50,16 @@ export function parseExpressProject(cwd, entryFile) {
38
50
  attachComment: true,
39
51
  });
40
52
  } catch (e) {
41
- skipped.push({ reason: `parse error in ${rel(absFile)}: ${e.message.slice(0, 80)}`, file: rel(absFile) });
53
+ skipped.push({
54
+ reason: `parse error in ${rel(absFile)}: ${e.message.slice(0, 80)}`,
55
+ file: rel(absFile),
56
+ });
42
57
  return;
43
58
  }
44
59
 
45
- const appVars = new Set(); // vars = express()
60
+ const appVars = new Set(); // vars = express()
46
61
  const routerVars = new Set(); // vars = express.Router() / Router()
47
- const importMap = new Map(); // localName -> absolute file path (relative imports only)
62
+ const importMap = new Map(); // localName -> absolute file path (relative imports only)
48
63
  const exprStarts = new Set(); // line numbers where express() assigned (for injector)
49
64
 
50
65
  traverse(ast, {
@@ -53,7 +68,11 @@ export function parseExpressProject(cwd, entryFile) {
53
68
  if (!init) return;
54
69
  if (init.type === 'CallExpression') {
55
70
  const callee = init.callee;
56
- if (callee.type === 'Identifier' && callee.name === 'require' && init.arguments[0]?.type === 'StringLiteral') {
71
+ if (
72
+ callee.type === 'Identifier' &&
73
+ callee.name === 'require' &&
74
+ init.arguments[0]?.type === 'StringLiteral'
75
+ ) {
57
76
  const resolved = resolveRel(absFile, init.arguments[0].value);
58
77
  if (resolved) {
59
78
  const id = p.node.id;
@@ -61,7 +80,10 @@ export function parseExpressProject(cwd, entryFile) {
61
80
  importMap.set(id.name, resolved);
62
81
  } else if (id.type === 'ObjectPattern') {
63
82
  for (const prop of id.properties) {
64
- if (prop.type === 'ObjectProperty' && prop.value.type === 'Identifier') {
83
+ if (
84
+ prop.type === 'ObjectProperty' &&
85
+ prop.value.type === 'Identifier'
86
+ ) {
65
87
  importMap.set(prop.value.name, resolved);
66
88
  }
67
89
  }
@@ -70,9 +92,14 @@ export function parseExpressProject(cwd, entryFile) {
70
92
  }
71
93
  const name = p.node.id?.name;
72
94
  if (name) {
73
- if (callee.type === 'Identifier' && callee.name === 'express') { appVars.add(name); exprStarts.add(p.node.loc.end.line); }
74
- if (callee.type === 'Identifier' && callee.name === 'Router') routerVars.add(name);
75
- if (callee.type === 'MemberExpression' && callee.property?.name === 'Router') routerVars.add(name);
95
+ if (callee.type === 'Identifier' && callee.name === 'express') {
96
+ appVars.add(name);
97
+ exprStarts.add(p.node.loc.end.line);
98
+ }
99
+ if (callee.type === 'Identifier' && callee.name === 'Router')
100
+ routerVars.add(name);
101
+ if (callee.type === 'MemberExpression' && callee.property?.name === 'Router')
102
+ routerVars.add(name);
76
103
  }
77
104
  }
78
105
  },
@@ -83,7 +110,8 @@ export function parseExpressProject(cwd, entryFile) {
83
110
  },
84
111
  CallExpression(p) {
85
112
  const callee = p.node.callee;
86
- if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier') return;
113
+ if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier')
114
+ return;
87
115
  const objName = callee.object.type === 'Identifier' ? callee.object.name : null;
88
116
  const method = callee.property.name;
89
117
  const args = p.node.arguments;
@@ -92,8 +120,17 @@ export function parseExpressProject(cwd, entryFile) {
92
120
  if (method === 'use' && objName && appVars.has(objName) && args.length >= 2) {
93
121
  const [a0, a1] = args;
94
122
  if (a0.type === 'StringLiteral' && a1.type === 'Identifier') {
95
- mounts.push({ prefix: joinPath(prefix, a0.value), file: importMap.get(a1.name) ?? null, ident: a1.name });
96
- if (!importMap.get(a1.name)) skipped.push({ reason: `router "${a1.name}" mounted at ${a0.value} — source file not resolved`, file: rel(absFile), line: p.node.loc.start.line });
123
+ mounts.push({
124
+ prefix: joinPath(prefix, a0.value),
125
+ file: importMap.get(a1.name) ?? null,
126
+ ident: a1.name,
127
+ });
128
+ if (!importMap.get(a1.name))
129
+ skipped.push({
130
+ reason: `router "${a1.name}" mounted at ${a0.value} — source file not resolved`,
131
+ file: rel(absFile),
132
+ line: p.node.loc.start.line,
133
+ });
97
134
  }
98
135
  return;
99
136
  }
@@ -105,26 +142,50 @@ export function parseExpressProject(cwd, entryFile) {
105
142
 
106
143
  const pathArg = args[0];
107
144
  if (!pathArg || pathArg.type !== 'StringLiteral') {
108
- skipped.push({ reason: `dynamic path on ${method.toUpperCase()} (non-literal first arg)`, file: rel(absFile), line: p.node.loc?.start.line });
145
+ skipped.push({
146
+ reason: `dynamic path on ${method.toUpperCase()} (non-literal first arg)`,
147
+ file: rel(absFile),
148
+ line: p.node.loc?.start.line,
149
+ });
109
150
  return;
110
151
  }
111
152
 
112
153
  const fullPath = joinPath(prefix, pathArg.value);
113
154
  if (fullPath === '/mcp' || fullPath.startsWith('/mcp/')) {
114
- skipped.push({ reason: `self-referential path ${fullPath} blocked`, file: rel(absFile), line: p.node.loc?.start.line });
155
+ skipped.push({
156
+ reason: `self-referential path ${fullPath} blocked`,
157
+ file: rel(absFile),
158
+ line: p.node.loc?.start.line,
159
+ });
115
160
  return;
116
161
  }
117
162
 
118
163
  const handler = args[args.length - 1];
119
- const handlerName = handler?.type === 'Identifier' ? handler.name
120
- : handler?.id?.name ?? `anonymous_${routes.length + 1}`;
164
+ const handlerName =
165
+ handler?.type === 'Identifier'
166
+ ? handler.name
167
+ : (handler?.id?.name ?? `anonymous_${routes.length + 1}`);
121
168
 
122
- const leading = (p.parentPath.node.leadingComments ?? p.node.leadingComments ?? [])
123
- .map((c) => c.value.replace(/^\*+/gm, '').replace(/^\s*\*\s?/gm, '').trim())
124
- .filter(Boolean).join(' ');
169
+ const leading = (
170
+ p.parentPath.node.leadingComments ??
171
+ p.node.leadingComments ??
172
+ []
173
+ )
174
+ .map((c) =>
175
+ c.value
176
+ .replace(/^\*+/gm, '')
177
+ .replace(/^\s*\*\s?/gm, '')
178
+ .trim(),
179
+ )
180
+ .filter(Boolean)
181
+ .join(' ');
125
182
 
126
183
  const pathParams = [...fullPath.matchAll(/:(\w+)/g)].map((mm) => ({
127
- name: mm[1], in: 'path', type: 'string', required: true, description: 'path parameter',
184
+ name: mm[1],
185
+ in: 'path',
186
+ type: 'string',
187
+ required: true,
188
+ description: 'path parameter',
128
189
  }));
129
190
 
130
191
  const mutating = method !== 'get';
@@ -135,18 +196,36 @@ export function parseExpressProject(cwd, entryFile) {
135
196
  for (const q of queryParamsOf(p)) {
136
197
  if (taken.has(q)) continue;
137
198
  taken.add(q);
138
- params.push({ name: q, in: 'query', type: 'string', required: false, description: 'query parameter' });
199
+ params.push({
200
+ name: q,
201
+ in: 'query',
202
+ type: 'string',
203
+ required: false,
204
+ description: 'query parameter',
205
+ });
139
206
  }
140
207
  let confidence = 'high';
141
208
  if (mutating) {
142
- params.push({ name: 'body', in: 'body', type: 'object', required: false, description: 'JSON body — schema not statically detected' });
209
+ params.push({
210
+ name: 'body',
211
+ in: 'body',
212
+ type: 'object',
213
+ required: false,
214
+ description: 'JSON body — schema not statically detected',
215
+ });
143
216
  confidence = 'low';
144
217
  }
145
218
 
146
219
  routes.push({
147
- method, path: fullPath, handlerName,
148
- sourceFile: rel(absFile), sourceLine: p.node.loc?.start.line ?? 0,
149
- params, description: leading.slice(0, 400), mutating, confidence,
220
+ method,
221
+ path: fullPath,
222
+ handlerName,
223
+ sourceFile: rel(absFile),
224
+ sourceLine: p.node.loc?.start.line ?? 0,
225
+ params,
226
+ description: leading.slice(0, 400),
227
+ mutating,
228
+ confidence,
150
229
  });
151
230
  },
152
231
  });
@@ -163,16 +242,26 @@ export function parseExpressProject(cwd, entryFile) {
163
242
  }
164
243
  }
165
244
 
166
- function rel(abs) { return path.relative(cwd, abs).split(path.sep).join('/'); }
245
+ function rel(abs) {
246
+ return path.relative(cwd, abs).split(path.sep).join('/');
247
+ }
167
248
 
168
249
  function resolveRel(fromFile, spec) {
169
250
  if (!spec.startsWith('.')) return null;
170
251
  const cleanSpec = spec.replace(/\.(m?[jt]s|cjs)$/, '');
171
252
  const base = path.resolve(path.dirname(fromFile), cleanSpec);
172
- for (const cand of [base, `${base}.ts`, `${base}.js`, `${base}.mjs`, `${base}.cjs`,
173
- path.join(base, 'index.ts'), path.join(base, 'index.js')]) {
253
+ for (const cand of [
254
+ base,
255
+ `${base}.ts`,
256
+ `${base}.js`,
257
+ `${base}.mjs`,
258
+ `${base}.cjs`,
259
+ path.join(base, 'index.ts'),
260
+ path.join(base, 'index.js'),
261
+ ]) {
174
262
  if (fs.existsSync(cand) && fs.statSync(cand).isFile()) {
175
- if ([...EXCLUDE].some((x) => cand.includes(`${path.sep}${x}${path.sep}`))) return null;
263
+ if ([...EXCLUDE].some((x) => cand.includes(`${path.sep}${x}${path.sep}`)))
264
+ return null;
176
265
  return cand;
177
266
  }
178
267
  }
@@ -187,27 +276,36 @@ export function parseExpressProject(cwd, entryFile) {
187
276
  function queryParamsOf(callPath) {
188
277
  const reqNames = new Set();
189
278
  for (const a of callPath.node.arguments) {
190
- if ((a.type === 'ArrowFunctionExpression' || a.type === 'FunctionExpression') && a.params[0]?.type === 'Identifier') {
279
+ if (
280
+ (a.type === 'ArrowFunctionExpression' || a.type === 'FunctionExpression') &&
281
+ a.params[0]?.type === 'Identifier'
282
+ ) {
191
283
  reqNames.add(a.params[0].name);
192
284
  }
193
285
  }
194
286
  const found = [];
195
287
  if (!reqNames.size) return found;
196
- const isReqQuery = (n) => n?.type === 'MemberExpression' && !n.computed &&
197
- n.object?.type === 'Identifier' && reqNames.has(n.object.name) &&
198
- n.property?.type === 'Identifier' && n.property.name === 'query';
288
+ const isReqQuery = (n) =>
289
+ n?.type === 'MemberExpression' &&
290
+ !n.computed &&
291
+ n.object?.type === 'Identifier' &&
292
+ reqNames.has(n.object.name) &&
293
+ n.property?.type === 'Identifier' &&
294
+ n.property.name === 'query';
199
295
  callPath.traverse({
200
296
  MemberExpression(q) {
201
297
  const n = q.node;
202
298
  if (!isReqQuery(n.object) || found.length >= 15) return;
203
299
  if (!n.computed && n.property.type === 'Identifier') found.push(n.property.name);
204
- else if (n.computed && n.property.type === 'StringLiteral') found.push(n.property.value);
300
+ else if (n.computed && n.property.type === 'StringLiteral')
301
+ found.push(n.property.value);
205
302
  },
206
303
  VariableDeclarator(q) {
207
304
  if (q.node.id.type !== 'ObjectPattern' || !isReqQuery(q.node.init)) return;
208
305
  for (const prop of q.node.id.properties) {
209
306
  if (found.length >= 15) break;
210
- if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') found.push(prop.key.name);
307
+ if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier')
308
+ found.push(prop.key.name);
211
309
  }
212
310
  },
213
311
  });
@@ -230,14 +328,16 @@ function dedupe(routes) {
230
328
  }
231
329
 
232
330
  export function toolNameFor(route, taken) {
233
- let name = `${route.method}_${route.path}`
234
- .toLowerCase()
235
- .replace(/:(\w+)/g, 'by_$1')
236
- .replace(/\{(\w+)\}/g, 'by_$1')
237
- .replace(/[^a-z0-9]+/g, '_')
238
- .replace(/^_+|_+$/g, '')
239
- .slice(0, 60) || 'tool';
240
- let final = name; let i = 2;
331
+ let name =
332
+ `${route.method}_${route.path}`
333
+ .toLowerCase()
334
+ .replace(/:(\w+)/g, 'by_$1')
335
+ .replace(/\{(\w+)\}/g, 'by_$1')
336
+ .replace(/[^a-z0-9]+/g, '_')
337
+ .replace(/^_+|_+$/g, '')
338
+ .slice(0, 60) || 'tool';
339
+ let final = name;
340
+ let i = 2;
241
341
  while (taken.has(final)) final = `${name}_${i++}`;
242
342
  taken.add(final);
243
343
  return final;
@@ -7,8 +7,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
 
8
8
  export function parseFastAPIProject(cwd, entryFile, pythonCmd = 'python') {
9
9
  const extractorScript = path.resolve(__dirname, 'fastapi_extract.py');
10
- const res = spawnSync(pythonCmd, [extractorScript, entryFile, cwd], { cwd, encoding: 'utf8' });
11
-
10
+ const res = spawnSync(pythonCmd, [extractorScript, entryFile, cwd], {
11
+ cwd,
12
+ encoding: 'utf8',
13
+ });
14
+
12
15
  if (res.status !== 0) {
13
16
  const errorMsg = (res.stderr || res.stdout || '').trim();
14
17
  throw new Error(`FastAPI extraction process failed: ${errorMsg}`);
@@ -19,8 +22,14 @@ export function parseFastAPIProject(cwd, entryFile, pythonCmd = 'python') {
19
22
  if (payload.error) {
20
23
  throw new Error(payload.error);
21
24
  }
22
- return { routes: payload.routes || [], skipped: payload.skipped || [], entryAppVars: payload.entryAppVars || [] };
25
+ return {
26
+ routes: payload.routes || [],
27
+ skipped: payload.skipped || [],
28
+ entryAppVars: payload.entryAppVars || [],
29
+ };
23
30
  } catch (err) {
24
- throw new Error(`Failed to parse Python extractor output: ${err.message}. Raw output: ${res.stdout}`);
31
+ throw new Error(
32
+ `Failed to parse Python extractor output: ${err.message}. Raw output: ${res.stdout}`,
33
+ );
25
34
  }
26
35
  }
@@ -18,10 +18,10 @@
18
18
 
19
19
  import { createRequire } from 'node:module';
20
20
  import { fileURLToPath } from 'node:url';
21
- import { dirname, resolve } from 'node:path';
21
+ import { dirname, resolve } from 'node:path';
22
22
 
23
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
- const require = createRequire(import.meta.url);
24
+ const require = createRequire(import.meta.url);
25
25
 
26
26
  // Load the CJS shim — patches Module._load globally in the CJS loader
27
27
  require(resolve(__dirname, 'express-shim.cjs'));
@@ -31,16 +31,16 @@ if (!process.env.SPARDA_PROBE) {
31
31
  }
32
32
 
33
33
  function installShim() {
34
- const net = require('net');
34
+ const net = require('net');
35
35
  const Module = require('module');
36
36
 
37
37
  const IPC_PORT = parseInt(process.env.SPARDA_IPC_PORT, 10) || 0;
38
38
 
39
39
  // ── Transport: fork IPC or TCP fallback ────────────────────────────────────
40
40
 
41
- let socket = null;
41
+ let socket = null;
42
42
  let socketReady = false;
43
- const pending = [];
43
+ const pending = [];
44
44
 
45
45
  function connectTcp() {
46
46
  if (socket || !IPC_PORT) return;
@@ -50,23 +50,33 @@ function installShim() {
50
50
  for (const line of pending) socket.write(line);
51
51
  pending.length = 0;
52
52
  });
53
- socket.on('error', () => { socket = null; socketReady = false; });
53
+ socket.on('error', () => {
54
+ socket = null;
55
+ socketReady = false;
56
+ });
54
57
  }
55
58
 
56
59
  function sendLine(line) {
57
60
  if (typeof process.send === 'function') {
58
- try { process.send(JSON.parse(line.trimEnd())); return; } catch {}
61
+ try {
62
+ process.send(JSON.parse(line.trimEnd()));
63
+ return;
64
+ } catch {}
59
65
  }
60
66
  connectTcp();
61
67
  if (socketReady && socket) socket.write(line);
62
68
  else pending.push(line);
63
69
  }
64
70
 
65
- function sendMsg(obj) { sendLine(JSON.stringify(obj) + '\n'); }
71
+ function sendMsg(obj) {
72
+ sendLine(JSON.stringify(obj) + '\n');
73
+ }
66
74
 
67
75
  function sendDone() {
68
76
  if (typeof process.send === 'function') {
69
- try { process.send({ type: '__done__' }); } catch {}
77
+ try {
78
+ process.send({ type: '__done__' });
79
+ } catch {}
70
80
  return;
71
81
  }
72
82
  const finish = () => {
@@ -93,7 +103,17 @@ function installShim() {
93
103
 
94
104
  // ── HTTP method list ───────────────────────────────────────────────────────
95
105
 
96
- const HTTP_METHODS = ['get','post','put','patch','delete','del','head','options','all'];
106
+ const HTTP_METHODS = [
107
+ 'get',
108
+ 'post',
109
+ 'put',
110
+ 'patch',
111
+ 'delete',
112
+ 'del',
113
+ 'head',
114
+ 'options',
115
+ 'all',
116
+ ];
97
117
 
98
118
  // ── Core wrapper — works on any target object ──────────────────────────────
99
119
  //
@@ -117,7 +137,10 @@ function installShim() {
117
137
  };
118
138
  }
119
139
  try {
120
- Object.defineProperty(target, '__sparda_wrapped__', { value: true, configurable: true });
140
+ Object.defineProperty(target, '__sparda_wrapped__', {
141
+ value: true,
142
+ configurable: true,
143
+ });
121
144
  } catch {}
122
145
  }
123
146
 
@@ -133,20 +156,30 @@ function installShim() {
133
156
  if (!appProto || appProto.__sparda_listen_patched__) return;
134
157
  if (typeof appProto.listen !== 'function') return;
135
158
  try {
136
- Object.defineProperty(appProto, '__sparda_listen_patched__', { value: true, configurable: true });
159
+ Object.defineProperty(appProto, '__sparda_listen_patched__', {
160
+ value: true,
161
+ configurable: true,
162
+ });
137
163
  } catch {}
138
- const origListen = appProto.listen;
139
164
  appProto.listen = function spardaListen(...args) {
140
165
  clearTimeout(idleTimer);
141
166
  // §ANALYSE §2: call the listen callback so routes registered inside it are captured
142
- const cb = args.find(a => typeof a === 'function');
143
- if (cb) { try { cb(); } catch {} }
167
+ const cb = args.find((a) => typeof a === 'function');
168
+ if (cb) {
169
+ try {
170
+ cb();
171
+ } catch {}
172
+ }
144
173
  sendDone();
145
174
  // Do NOT call origListen — no real socket in probe mode
146
175
  return {
147
- on() { return this; },
148
- close() {},
149
- address() { return { port: 0, address: '127.0.0.1', family: 'IPv4' }; },
176
+ on() {
177
+ return this;
178
+ },
179
+ close() {},
180
+ address() {
181
+ return { port: 0, address: '127.0.0.1', family: 'IPv4' };
182
+ },
150
183
  };
151
184
  };
152
185
  }
@@ -156,7 +189,10 @@ function installShim() {
156
189
  function patchExpress(exp) {
157
190
  if (!exp || exp.__sparda_factory_patched__) return;
158
191
  try {
159
- Object.defineProperty(exp, '__sparda_factory_patched__', { value: true, configurable: true });
192
+ Object.defineProperty(exp, '__sparda_factory_patched__', {
193
+ value: true,
194
+ configurable: true,
195
+ });
160
196
  } catch {}
161
197
 
162
198
  // Surface 1: express.application — catches app.get/post/... (Express 4 & 5)
@@ -179,7 +215,7 @@ function installShim() {
179
215
  // ── Intercept require('express') via Module._load ─────────────────────────
180
216
 
181
217
  const originalLoad = Module._load;
182
- Module._load = function spardaLoad(request, parent, isMain) {
218
+ Module._load = function spardaLoad(request) {
183
219
  const result = originalLoad.apply(this, arguments);
184
220
  if (request === 'express') patchExpress(result);
185
221
  return result;
@@ -190,8 +226,8 @@ function installShim() {
190
226
  // this shim loaded, Module._load hook fires too late. Patch the cached export.
191
227
 
192
228
  try {
193
- const expressPaths = Object.keys(require.cache).filter(
194
- k => /[/\\]express[/\\]index\.js$/.test(k)
229
+ const expressPaths = Object.keys(require.cache).filter((k) =>
230
+ /[/\\]express[/\\]index\.js$/.test(k),
195
231
  );
196
232
  for (const p of expressPaths) {
197
233
  const cached = require.cache[p];
@@ -201,8 +237,15 @@ function installShim() {
201
237
 
202
238
  // ── Safety nets ────────────────────────────────────────────────────────────
203
239
 
204
- process.on('exit', () => { try { sendDone(); } catch {} });
205
- process.on('SIGTERM', () => { sendDone(); setTimeout(() => process.exit(0), 200); });
240
+ process.on('exit', () => {
241
+ try {
242
+ sendDone();
243
+ } catch {}
244
+ });
245
+ process.on('SIGTERM', () => {
246
+ sendDone();
247
+ setTimeout(() => process.exit(0), 200);
248
+ });
206
249
 
207
250
  module.exports = { record, sendDone };
208
251
  }
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { probeRoutes } from './probe.js';
20
- import { reconcile } from './reconcile.js';
20
+ import { reconcile } from './reconcile.js';
21
21
 
22
22
  const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
23
23
 
@@ -40,21 +40,31 @@ const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
40
40
  */
41
41
  export function gapToStaticRoute(gap, framework) {
42
42
  const method = (gap.method ?? 'GET').toUpperCase();
43
- const lower = method.toLowerCase();
44
- const mutating = gap.writeClass ? gap.writeClass === 'write' : WRITE_METHODS.has(method);
43
+ const lower = method.toLowerCase();
44
+ const mutating = gap.writeClass
45
+ ? gap.writeClass === 'write'
46
+ : WRITE_METHODS.has(method);
45
47
 
46
- const path = framework === 'fastapi'
47
- ? (gap.path ?? '/').replace(/:([a-zA-Z_]\w*)/g, '{$1}')
48
- : (gap.path ?? '/');
48
+ const path =
49
+ framework === 'fastapi'
50
+ ? (gap.path ?? '/').replace(/:([a-zA-Z_]\w*)/g, '{$1}')
51
+ : (gap.path ?? '/');
49
52
 
50
53
  // Path params, in the exact param object the static parser uses.
51
54
  const params = (gap.pathParams ?? []).map((name) => ({
52
- name, in: 'path', type: 'string', required: true, description: 'path parameter',
55
+ name,
56
+ in: 'path',
57
+ type: 'string',
58
+ required: true,
59
+ description: 'path parameter',
53
60
  }));
54
61
  // Mirror the static parser: mutating routes get a body param (schema unknown).
55
62
  if (mutating) {
56
63
  params.push({
57
- name: 'body', in: 'body', type: 'object', required: false,
64
+ name: 'body',
65
+ in: 'body',
66
+ type: 'object',
67
+ required: false,
58
68
  description: 'JSON body — schema not statically detected',
59
69
  });
60
70
  }
@@ -68,7 +78,7 @@ export function gapToStaticRoute(gap, framework) {
68
78
  params,
69
79
  description: 'Discovered at runtime via --probe; not found by static analysis.',
70
80
  mutating,
71
- confidence: 'low', // never statically verified → always low
81
+ confidence: 'low', // never statically verified → always low
72
82
  source: 'dynamic',
73
83
  };
74
84
  }
@@ -84,9 +94,15 @@ export function gapToStaticRoute(gap, framework) {
84
94
  * gaps — the raw minimal gaps (stored in the scan-report for provenance)
85
95
  * probedCount — how many routes the probe observed (0 ⇒ probe degraded / nothing seen)
86
96
  */
87
- export async function discoverDynamicRoutes({ framework, entryFile, projectRoot, staticRoutes, timeoutMs = 8000 }) {
97
+ export async function discoverDynamicRoutes({
98
+ framework,
99
+ entryFile,
100
+ projectRoot,
101
+ staticRoutes,
102
+ timeoutMs = 8000,
103
+ }) {
88
104
  const probed = await probeRoutes({ framework, entryFile, projectRoot, timeoutMs });
89
- const { gaps, staticCount, dynamicCount } = reconcile(staticRoutes, probed);
105
+ const { gaps, staticCount } = reconcile(staticRoutes, probed);
90
106
 
91
107
  // Defensive: the probe should not emit duplicate (method, path) pairs, but
92
108
  // reconcile does not dedup gaps among themselves. Collapse here so we never
@@ -100,5 +116,11 @@ export async function discoverDynamicRoutes({ framework, entryFile, projectRoot,
100
116
  });
101
117
 
102
118
  const added = uniqueGaps.map((g) => gapToStaticRoute(g, framework));
103
- return { added, gaps: uniqueGaps, staticCount, dynamicCount: added.length, probedCount: probed.length };
119
+ return {
120
+ added,
121
+ gaps: uniqueGaps,
122
+ staticCount,
123
+ dynamicCount: added.length,
124
+ probedCount: probed.length,
125
+ };
104
126
  }