@vibecheckai/cli 3.1.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/registry.js +105 -105
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/analysis-core.js +271 -271
- package/bin/runners/lib/analyzers.js +579 -579
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-output.js +368 -368
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/detectors-v2.js +703 -703
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/entitlements-v2.js +490 -489
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/init-wizard.js +308 -308
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/missions/plan.js +69 -69
- package/bin/runners/lib/missions/templates.js +192 -192
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-engine.js +447 -447
- package/bin/runners/lib/report-html.js +1499 -1499
- package/bin/runners/lib/report-templates.js +969 -969
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/route-truth.js +477 -477
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/truth.js +667 -667
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runAuth.js +51 -0
- package/bin/runners/runClaimVerifier.js +483 -483
- package/bin/runners/runContext.js +56 -56
- package/bin/runners/runContextCompiler.js +385 -385
- package/bin/runners/runCtx.js +674 -674
- package/bin/runners/runCtxDiff.js +301 -301
- package/bin/runners/runCtxGuard.js +176 -176
- package/bin/runners/runCtxSync.js +116 -116
- package/bin/runners/runGate.js +17 -17
- package/bin/runners/runGraph.js +454 -454
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runInitGha.js +164 -164
- package/bin/runners/runInstall.js +277 -277
- package/bin/runners/runInteractive.js +388 -388
- package/bin/runners/runLabs.js +340 -340
- package/bin/runners/runMissionGenerator.js +282 -282
- package/bin/runners/runPR.js +255 -255
- package/bin/runners/runPermissions.js +304 -304
- package/bin/runners/runPreflight.js +580 -553
- package/bin/runners/runProve.js +1252 -1252
- package/bin/runners/runReality.js +1328 -1328
- package/bin/runners/runReplay.js +499 -499
- package/bin/runners/runReport.js +584 -584
- package/bin/runners/runShare.js +212 -212
- package/bin/runners/runStatus.js +138 -138
- package/bin/runners/runTruthpack.js +636 -636
- package/bin/runners/runVerify.js +272 -272
- package/bin/runners/runWatch.js +407 -407
- package/bin/vibecheck.js +2 -1
- package/mcp-server/consolidated-tools.js +804 -804
- package/mcp-server/package.json +1 -1
- package/mcp-server/tools/index.js +72 -72
- package/mcp-server/truth-context.js +581 -581
- package/mcp-server/truth-firewall-tools.js +1500 -1500
- package/package.json +1 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -1,477 +1,477 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route Truth v1 - JavaScript Runtime
|
|
3
|
-
*
|
|
4
|
-
* Generates a normalized route map with evidence from:
|
|
5
|
-
* - Next.js (App Router + Pages Router)
|
|
6
|
-
* - Fastify (shorthand + .route() + register prefixes)
|
|
7
|
-
*
|
|
8
|
-
* Then implements validate_claim(route_exists) on top of it.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const crypto = require('crypto');
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// CANONICALIZATION
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Canonicalize a path to standard format.
|
|
21
|
-
*/
|
|
22
|
-
function canonicalizePath(p) {
|
|
23
|
-
let s = p.trim();
|
|
24
|
-
if (!s.startsWith('/')) s = '/' + s;
|
|
25
|
-
s = s.replace(/\/+/g, '/');
|
|
26
|
-
|
|
27
|
-
// Convert Next.js dynamic segments
|
|
28
|
-
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, '*$1?'); // [[...slug]] → *slug?
|
|
29
|
-
s = s.replace(/\[\.{3}([^\]]+)\]/g, '*$1'); // [...slug] → *slug
|
|
30
|
-
s = s.replace(/\[([^\]]+)\]/g, ':$1'); // [id] → :id
|
|
31
|
-
|
|
32
|
-
if (s.length > 1) s = s.replace(/\/$/, '');
|
|
33
|
-
return s;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function canonicalizeMethod(m) {
|
|
37
|
-
const u = m.toUpperCase();
|
|
38
|
-
if (u === 'ALL' || u === 'ANY') return '*';
|
|
39
|
-
return u;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function joinPrefix(prefix, p) {
|
|
43
|
-
const cleanPrefix = prefix.replace(/\/$/, '');
|
|
44
|
-
const cleanPath = p.startsWith('/') ? p : '/' + p;
|
|
45
|
-
return canonicalizePath(cleanPrefix + cleanPath);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isParameterizedPath(path) {
|
|
49
|
-
return path.includes(':') || path.includes('*');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function matchPath(pattern, concrete) {
|
|
53
|
-
const patternParts = pattern.split('/');
|
|
54
|
-
const concreteParts = concrete.split('/');
|
|
55
|
-
|
|
56
|
-
let pIdx = 0, cIdx = 0;
|
|
57
|
-
|
|
58
|
-
while (pIdx < patternParts.length && cIdx < concreteParts.length) {
|
|
59
|
-
const pPart = patternParts[pIdx];
|
|
60
|
-
const cPart = concreteParts[cIdx];
|
|
61
|
-
|
|
62
|
-
if (pPart.startsWith('*')) return true;
|
|
63
|
-
if (pPart.startsWith(':')) { pIdx++; cIdx++; continue; }
|
|
64
|
-
if (pPart !== cPart) return false;
|
|
65
|
-
pIdx++; cIdx++;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return pIdx === patternParts.length && cIdx === concreteParts.length;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function matchMethod(pattern, concrete) {
|
|
72
|
-
if (pattern === '*') return true;
|
|
73
|
-
return pattern === concrete;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ============================================================================
|
|
77
|
-
// NEXT.JS RESOLVER
|
|
78
|
-
// ============================================================================
|
|
79
|
-
|
|
80
|
-
const NEXT_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
|
81
|
-
let evidenceCounter = 0;
|
|
82
|
-
|
|
83
|
-
function createEvidence(file, lines, reason, snippet) {
|
|
84
|
-
evidenceCounter++;
|
|
85
|
-
return {
|
|
86
|
-
id: `ev_${String(evidenceCounter).padStart(4, '0')}`,
|
|
87
|
-
file,
|
|
88
|
-
lines,
|
|
89
|
-
snippetHash: `sha256:${crypto.createHash('sha256').update(snippet || '').digest('hex').slice(0, 16)}`,
|
|
90
|
-
reason,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function findFiles(dir, include, exclude) {
|
|
95
|
-
const files = [];
|
|
96
|
-
|
|
97
|
-
function walk(d) {
|
|
98
|
-
try {
|
|
99
|
-
const entries = fs.readdirSync(d, { withFileTypes: true });
|
|
100
|
-
for (const entry of entries) {
|
|
101
|
-
const fullPath = path.join(d, entry.name);
|
|
102
|
-
if (entry.isDirectory()) {
|
|
103
|
-
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
104
|
-
walk(fullPath);
|
|
105
|
-
}
|
|
106
|
-
} else if (entry.isFile()) {
|
|
107
|
-
if (include.test(entry.name) && (!exclude || !exclude.test(entry.name))) {
|
|
108
|
-
files.push(fullPath);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch {}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
walk(dir);
|
|
116
|
-
return files;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function extractAppRouterMethods(code) {
|
|
120
|
-
const methods = [];
|
|
121
|
-
const lines = code.split('\n');
|
|
122
|
-
|
|
123
|
-
const patterns = [
|
|
124
|
-
/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(/,
|
|
125
|
-
/export\s+const\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*=/,
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < lines.length; i++) {
|
|
129
|
-
for (const pattern of patterns) {
|
|
130
|
-
const match = lines[i].match(pattern);
|
|
131
|
-
if (match && NEXT_HTTP_METHODS.includes(match[1].toUpperCase())) {
|
|
132
|
-
methods.push({
|
|
133
|
-
name: match[1].toUpperCase(),
|
|
134
|
-
line: i + 1,
|
|
135
|
-
snippet: lines[i].trim(),
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return methods;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function resolveNextRoutes(repoRoot) {
|
|
145
|
-
const routes = [];
|
|
146
|
-
|
|
147
|
-
// App Router: app/api/**/route.ts|js
|
|
148
|
-
const appDirs = ['app', 'src/app'];
|
|
149
|
-
for (const appDir of appDirs) {
|
|
150
|
-
const apiDir = path.join(repoRoot, appDir, 'api');
|
|
151
|
-
if (!fs.existsSync(apiDir)) continue;
|
|
152
|
-
|
|
153
|
-
const routeFiles = findFiles(apiDir, /route\.(ts|js)$/);
|
|
154
|
-
|
|
155
|
-
for (const file of routeFiles) {
|
|
156
|
-
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
157
|
-
const apiIdx = relPath.indexOf('/api/');
|
|
158
|
-
const sub = relPath.slice(apiIdx + '/api/'.length).replace(/\/route\.(ts|js)$/, '');
|
|
159
|
-
const routePath = canonicalizePath('/api/' + sub);
|
|
160
|
-
|
|
161
|
-
const code = fs.readFileSync(file, 'utf8');
|
|
162
|
-
const methods = extractAppRouterMethods(code);
|
|
163
|
-
|
|
164
|
-
if (methods.length === 0) {
|
|
165
|
-
routes.push({
|
|
166
|
-
method: '*',
|
|
167
|
-
path: routePath,
|
|
168
|
-
handler: relPath,
|
|
169
|
-
framework: 'next',
|
|
170
|
-
routerType: 'app',
|
|
171
|
-
confidence: 'low',
|
|
172
|
-
evidence: [createEvidence(relPath, '1', 'route file with no exports', code.slice(0, 100))],
|
|
173
|
-
});
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
for (const m of methods) {
|
|
178
|
-
routes.push({
|
|
179
|
-
method: m.name,
|
|
180
|
-
path: routePath,
|
|
181
|
-
handler: `${relPath}:${m.line}`,
|
|
182
|
-
framework: 'next',
|
|
183
|
-
routerType: 'app',
|
|
184
|
-
confidence: 'high',
|
|
185
|
-
evidence: [createEvidence(relPath, String(m.line), `export ${m.name}`, m.snippet)],
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Pages Router: pages/api/**/*.ts|js
|
|
192
|
-
const pagesDirs = ['pages', 'src/pages'];
|
|
193
|
-
for (const pagesDir of pagesDirs) {
|
|
194
|
-
const apiDir = path.join(repoRoot, pagesDir, 'api');
|
|
195
|
-
if (!fs.existsSync(apiDir)) continue;
|
|
196
|
-
|
|
197
|
-
const apiFiles = findFiles(apiDir, /\.(ts|js)$/, /\.d\.ts$/);
|
|
198
|
-
|
|
199
|
-
for (const file of apiFiles) {
|
|
200
|
-
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
201
|
-
const apiIdx = relPath.indexOf('/api/');
|
|
202
|
-
const sub = relPath
|
|
203
|
-
.slice(apiIdx + '/api/'.length)
|
|
204
|
-
.replace(/\.(ts|js)$/, '')
|
|
205
|
-
.replace(/\/index$/, '');
|
|
206
|
-
|
|
207
|
-
const routePath = canonicalizePath('/api/' + sub);
|
|
208
|
-
const code = fs.readFileSync(file, 'utf8');
|
|
209
|
-
const hasDefaultExport = /export\s+default/.test(code);
|
|
210
|
-
|
|
211
|
-
routes.push({
|
|
212
|
-
method: '*',
|
|
213
|
-
path: routePath,
|
|
214
|
-
handler: relPath,
|
|
215
|
-
framework: 'next',
|
|
216
|
-
routerType: 'pages',
|
|
217
|
-
confidence: hasDefaultExport ? 'med' : 'low',
|
|
218
|
-
evidence: [createEvidence(relPath, '1', 'Pages API route', code.slice(0, 100))],
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return routes;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ============================================================================
|
|
227
|
-
// FASTIFY RESOLVER (Simplified - regex based)
|
|
228
|
-
// ============================================================================
|
|
229
|
-
|
|
230
|
-
async function resolveFastifyRoutes(repoRoot) {
|
|
231
|
-
const routes = [];
|
|
232
|
-
const gaps = [];
|
|
233
|
-
|
|
234
|
-
const entryPoints = [
|
|
235
|
-
'src/server.ts', 'src/server.js', 'src/index.ts', 'src/index.js',
|
|
236
|
-
'server.ts', 'server.js', 'apps/api/src/server.ts', 'apps/api/src/index.ts',
|
|
237
|
-
];
|
|
238
|
-
|
|
239
|
-
const fastifyMethods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'all'];
|
|
240
|
-
|
|
241
|
-
// Find source files
|
|
242
|
-
const srcDirs = ['src', 'apps/api/src', 'server'];
|
|
243
|
-
const files = [];
|
|
244
|
-
|
|
245
|
-
for (const srcDir of srcDirs) {
|
|
246
|
-
const fullDir = path.join(repoRoot, srcDir);
|
|
247
|
-
if (fs.existsSync(fullDir)) {
|
|
248
|
-
files.push(...findFiles(fullDir, /\.(ts|js)$/, /\.d\.ts$/));
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Patterns to detect routes
|
|
253
|
-
const patterns = [
|
|
254
|
-
// fastify.get('/path', handler)
|
|
255
|
-
/(?:fastify|app|server)\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
256
|
-
// router.get('/path', handler)
|
|
257
|
-
/router\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
258
|
-
// .route({ method: 'GET', url: '/path' })
|
|
259
|
-
/\.route\s*\(\s*\{[^}]*method:\s*['"`]([^'"`]+)['"`][^}]*url:\s*['"`]([^'"`]+)['"`]/gi,
|
|
260
|
-
/\.route\s*\(\s*\{[^}]*url:\s*['"`]([^'"`]+)['"`][^}]*method:\s*['"`]([^'"`]+)['"`]/gi,
|
|
261
|
-
];
|
|
262
|
-
|
|
263
|
-
// Track prefixes from register calls
|
|
264
|
-
const prefixMap = new Map(); // file → prefix
|
|
265
|
-
|
|
266
|
-
for (const file of files) {
|
|
267
|
-
try {
|
|
268
|
-
const code = fs.readFileSync(file, 'utf8');
|
|
269
|
-
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
270
|
-
const lines = code.split('\n');
|
|
271
|
-
|
|
272
|
-
// Detect prefix from register calls
|
|
273
|
-
const registerPattern = /\.register\s*\([^,]+,\s*\{[^}]*prefix:\s*['"`]([^'"`]+)['"`]/g;
|
|
274
|
-
let match;
|
|
275
|
-
while ((match = registerPattern.exec(code)) !== null) {
|
|
276
|
-
prefixMap.set(relPath, match[1]);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Extract routes
|
|
280
|
-
for (const pattern of patterns) {
|
|
281
|
-
pattern.lastIndex = 0;
|
|
282
|
-
while ((match = pattern.exec(code)) !== null) {
|
|
283
|
-
let method, routePath;
|
|
284
|
-
|
|
285
|
-
if (match[0].includes('.route')) {
|
|
286
|
-
// Handle .route() pattern - order varies
|
|
287
|
-
if (match[0].indexOf('method') < match[0].indexOf('url')) {
|
|
288
|
-
method = match[1];
|
|
289
|
-
routePath = match[2];
|
|
290
|
-
} else {
|
|
291
|
-
routePath = match[1];
|
|
292
|
-
method = match[2];
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
method = match[1];
|
|
296
|
-
routePath = match[2];
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const prefix = prefixMap.get(relPath) || '';
|
|
300
|
-
const fullPath = joinPrefix(prefix, routePath);
|
|
301
|
-
const lineNum = code.substring(0, match.index).split('\n').length;
|
|
302
|
-
const snippet = lines[lineNum - 1] || '';
|
|
303
|
-
|
|
304
|
-
routes.push({
|
|
305
|
-
method: canonicalizeMethod(method),
|
|
306
|
-
path: fullPath,
|
|
307
|
-
handler: `${relPath}:${lineNum}`,
|
|
308
|
-
framework: 'fastify',
|
|
309
|
-
confidence: 'med',
|
|
310
|
-
evidence: [createEvidence(relPath, String(lineNum), `fastify.${method}('${routePath}')`, snippet)],
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
} catch {}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return { routes, gaps };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ============================================================================
|
|
321
|
-
// ROUTE INDEX
|
|
322
|
-
// ============================================================================
|
|
323
|
-
|
|
324
|
-
class RouteIndex {
|
|
325
|
-
constructor() {
|
|
326
|
-
this.routes = [];
|
|
327
|
-
this.byMethod = new Map();
|
|
328
|
-
this.byPath = new Map();
|
|
329
|
-
this.parameterized = [];
|
|
330
|
-
this.gaps = [];
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async build(repoRoot) {
|
|
334
|
-
// Resolve Next.js routes
|
|
335
|
-
const nextRoutes = await resolveNextRoutes(repoRoot);
|
|
336
|
-
this.routes.push(...nextRoutes);
|
|
337
|
-
|
|
338
|
-
// Resolve Fastify routes
|
|
339
|
-
const { routes: fastifyRoutes, gaps } = await resolveFastifyRoutes(repoRoot);
|
|
340
|
-
this.routes.push(...fastifyRoutes);
|
|
341
|
-
this.gaps.push(...gaps);
|
|
342
|
-
|
|
343
|
-
// Build indexes
|
|
344
|
-
for (const route of this.routes) {
|
|
345
|
-
const methodKey = route.method;
|
|
346
|
-
if (!this.byMethod.has(methodKey)) this.byMethod.set(methodKey, []);
|
|
347
|
-
this.byMethod.get(methodKey).push(route);
|
|
348
|
-
|
|
349
|
-
const pathKey = route.path;
|
|
350
|
-
if (!this.byPath.has(pathKey)) this.byPath.set(pathKey, []);
|
|
351
|
-
this.byPath.get(pathKey).push(route);
|
|
352
|
-
|
|
353
|
-
if (isParameterizedPath(route.path)) {
|
|
354
|
-
this.parameterized.push(route);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return this;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
findRoutes(method, path) {
|
|
362
|
-
const canonicalMethod = canonicalizeMethod(method);
|
|
363
|
-
const canonicalPath = canonicalizePath(path);
|
|
364
|
-
const matches = [];
|
|
365
|
-
|
|
366
|
-
// Exact path match
|
|
367
|
-
const pathMatches = this.byPath.get(canonicalPath) || [];
|
|
368
|
-
for (const route of pathMatches) {
|
|
369
|
-
if (matchMethod(route.method, canonicalMethod)) {
|
|
370
|
-
matches.push(route);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Wildcard method match
|
|
375
|
-
const wildcardMethods = this.byMethod.get('*') || [];
|
|
376
|
-
for (const route of wildcardMethods) {
|
|
377
|
-
if (route.path === canonicalPath && !matches.includes(route)) {
|
|
378
|
-
matches.push(route);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Parameterized route match
|
|
383
|
-
for (const route of this.parameterized) {
|
|
384
|
-
if (matchPath(route.path, canonicalPath) && matchMethod(route.method, canonicalMethod)) {
|
|
385
|
-
if (!matches.includes(route)) matches.push(route);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return matches;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
findClosestRoutes(path, limit = 3) {
|
|
393
|
-
const canonicalPath = canonicalizePath(path);
|
|
394
|
-
const pathParts = canonicalPath.split('/').filter(Boolean);
|
|
395
|
-
|
|
396
|
-
const scored = this.routes.map(route => {
|
|
397
|
-
const routeParts = route.path.split('/').filter(Boolean);
|
|
398
|
-
let score = 0;
|
|
399
|
-
|
|
400
|
-
for (let i = 0; i < Math.min(pathParts.length, routeParts.length); i++) {
|
|
401
|
-
if (pathParts[i] === routeParts[i] || routeParts[i].startsWith(':')) {
|
|
402
|
-
score++;
|
|
403
|
-
} else break;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (pathParts.length === routeParts.length) score += 0.5;
|
|
407
|
-
return { route, score };
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map(s => s.route);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
getRouteMap() {
|
|
414
|
-
return {
|
|
415
|
-
server: this.routes,
|
|
416
|
-
clientRefs: [],
|
|
417
|
-
gaps: this.gaps,
|
|
418
|
-
generatedAt: new Date().toISOString(),
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ============================================================================
|
|
424
|
-
// VALIDATE CLAIM
|
|
425
|
-
// ============================================================================
|
|
426
|
-
|
|
427
|
-
async function validateRouteExists(claim, repoRoot, routeIndex) {
|
|
428
|
-
const index = routeIndex || new RouteIndex();
|
|
429
|
-
if (!routeIndex) await index.build(repoRoot);
|
|
430
|
-
|
|
431
|
-
const method = claim.method || '*';
|
|
432
|
-
const routePath = claim.path;
|
|
433
|
-
|
|
434
|
-
const matches = index.findRoutes(method, routePath);
|
|
435
|
-
|
|
436
|
-
if (matches.length > 0) {
|
|
437
|
-
return {
|
|
438
|
-
result: 'true',
|
|
439
|
-
confidence: matches[0].confidence,
|
|
440
|
-
evidence: matches[0].evidence,
|
|
441
|
-
matchedRoute: matches[0],
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const closest = index.findClosestRoutes(routePath);
|
|
446
|
-
const hasGaps = index.gaps.length > 0;
|
|
447
|
-
|
|
448
|
-
if (hasGaps) {
|
|
449
|
-
return {
|
|
450
|
-
result: 'unknown',
|
|
451
|
-
confidence: 'low',
|
|
452
|
-
evidence: [],
|
|
453
|
-
closestRoutes: closest,
|
|
454
|
-
gaps: index.gaps,
|
|
455
|
-
nextSteps: ['Some routes may not be detected due to unresolved plugins'],
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
result: 'false',
|
|
461
|
-
confidence: 'high',
|
|
462
|
-
evidence: [],
|
|
463
|
-
closestRoutes: closest,
|
|
464
|
-
nextSteps: closest.length > 0
|
|
465
|
-
? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(', ')}?`]
|
|
466
|
-
: ['No similar routes found'],
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
module.exports = {
|
|
471
|
-
canonicalizePath,
|
|
472
|
-
canonicalizeMethod,
|
|
473
|
-
resolveNextRoutes,
|
|
474
|
-
resolveFastifyRoutes,
|
|
475
|
-
RouteIndex,
|
|
476
|
-
validateRouteExists,
|
|
477
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Route Truth v1 - JavaScript Runtime
|
|
3
|
+
*
|
|
4
|
+
* Generates a normalized route map with evidence from:
|
|
5
|
+
* - Next.js (App Router + Pages Router)
|
|
6
|
+
* - Fastify (shorthand + .route() + register prefixes)
|
|
7
|
+
*
|
|
8
|
+
* Then implements validate_claim(route_exists) on top of it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// CANONICALIZATION
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Canonicalize a path to standard format.
|
|
21
|
+
*/
|
|
22
|
+
function canonicalizePath(p) {
|
|
23
|
+
let s = p.trim();
|
|
24
|
+
if (!s.startsWith('/')) s = '/' + s;
|
|
25
|
+
s = s.replace(/\/+/g, '/');
|
|
26
|
+
|
|
27
|
+
// Convert Next.js dynamic segments
|
|
28
|
+
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, '*$1?'); // [[...slug]] → *slug?
|
|
29
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, '*$1'); // [...slug] → *slug
|
|
30
|
+
s = s.replace(/\[([^\]]+)\]/g, ':$1'); // [id] → :id
|
|
31
|
+
|
|
32
|
+
if (s.length > 1) s = s.replace(/\/$/, '');
|
|
33
|
+
return s;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function canonicalizeMethod(m) {
|
|
37
|
+
const u = m.toUpperCase();
|
|
38
|
+
if (u === 'ALL' || u === 'ANY') return '*';
|
|
39
|
+
return u;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function joinPrefix(prefix, p) {
|
|
43
|
+
const cleanPrefix = prefix.replace(/\/$/, '');
|
|
44
|
+
const cleanPath = p.startsWith('/') ? p : '/' + p;
|
|
45
|
+
return canonicalizePath(cleanPrefix + cleanPath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isParameterizedPath(path) {
|
|
49
|
+
return path.includes(':') || path.includes('*');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function matchPath(pattern, concrete) {
|
|
53
|
+
const patternParts = pattern.split('/');
|
|
54
|
+
const concreteParts = concrete.split('/');
|
|
55
|
+
|
|
56
|
+
let pIdx = 0, cIdx = 0;
|
|
57
|
+
|
|
58
|
+
while (pIdx < patternParts.length && cIdx < concreteParts.length) {
|
|
59
|
+
const pPart = patternParts[pIdx];
|
|
60
|
+
const cPart = concreteParts[cIdx];
|
|
61
|
+
|
|
62
|
+
if (pPart.startsWith('*')) return true;
|
|
63
|
+
if (pPart.startsWith(':')) { pIdx++; cIdx++; continue; }
|
|
64
|
+
if (pPart !== cPart) return false;
|
|
65
|
+
pIdx++; cIdx++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return pIdx === patternParts.length && cIdx === concreteParts.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function matchMethod(pattern, concrete) {
|
|
72
|
+
if (pattern === '*') return true;
|
|
73
|
+
return pattern === concrete;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// NEXT.JS RESOLVER
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
const NEXT_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
|
81
|
+
let evidenceCounter = 0;
|
|
82
|
+
|
|
83
|
+
function createEvidence(file, lines, reason, snippet) {
|
|
84
|
+
evidenceCounter++;
|
|
85
|
+
return {
|
|
86
|
+
id: `ev_${String(evidenceCounter).padStart(4, '0')}`,
|
|
87
|
+
file,
|
|
88
|
+
lines,
|
|
89
|
+
snippetHash: `sha256:${crypto.createHash('sha256').update(snippet || '').digest('hex').slice(0, 16)}`,
|
|
90
|
+
reason,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findFiles(dir, include, exclude) {
|
|
95
|
+
const files = [];
|
|
96
|
+
|
|
97
|
+
function walk(d) {
|
|
98
|
+
try {
|
|
99
|
+
const entries = fs.readdirSync(d, { withFileTypes: true });
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const fullPath = path.join(d, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
104
|
+
walk(fullPath);
|
|
105
|
+
}
|
|
106
|
+
} else if (entry.isFile()) {
|
|
107
|
+
if (include.test(entry.name) && (!exclude || !exclude.test(entry.name))) {
|
|
108
|
+
files.push(fullPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
walk(dir);
|
|
116
|
+
return files;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractAppRouterMethods(code) {
|
|
120
|
+
const methods = [];
|
|
121
|
+
const lines = code.split('\n');
|
|
122
|
+
|
|
123
|
+
const patterns = [
|
|
124
|
+
/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(/,
|
|
125
|
+
/export\s+const\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*=/,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
for (const pattern of patterns) {
|
|
130
|
+
const match = lines[i].match(pattern);
|
|
131
|
+
if (match && NEXT_HTTP_METHODS.includes(match[1].toUpperCase())) {
|
|
132
|
+
methods.push({
|
|
133
|
+
name: match[1].toUpperCase(),
|
|
134
|
+
line: i + 1,
|
|
135
|
+
snippet: lines[i].trim(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return methods;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function resolveNextRoutes(repoRoot) {
|
|
145
|
+
const routes = [];
|
|
146
|
+
|
|
147
|
+
// App Router: app/api/**/route.ts|js
|
|
148
|
+
const appDirs = ['app', 'src/app'];
|
|
149
|
+
for (const appDir of appDirs) {
|
|
150
|
+
const apiDir = path.join(repoRoot, appDir, 'api');
|
|
151
|
+
if (!fs.existsSync(apiDir)) continue;
|
|
152
|
+
|
|
153
|
+
const routeFiles = findFiles(apiDir, /route\.(ts|js)$/);
|
|
154
|
+
|
|
155
|
+
for (const file of routeFiles) {
|
|
156
|
+
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
157
|
+
const apiIdx = relPath.indexOf('/api/');
|
|
158
|
+
const sub = relPath.slice(apiIdx + '/api/'.length).replace(/\/route\.(ts|js)$/, '');
|
|
159
|
+
const routePath = canonicalizePath('/api/' + sub);
|
|
160
|
+
|
|
161
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
162
|
+
const methods = extractAppRouterMethods(code);
|
|
163
|
+
|
|
164
|
+
if (methods.length === 0) {
|
|
165
|
+
routes.push({
|
|
166
|
+
method: '*',
|
|
167
|
+
path: routePath,
|
|
168
|
+
handler: relPath,
|
|
169
|
+
framework: 'next',
|
|
170
|
+
routerType: 'app',
|
|
171
|
+
confidence: 'low',
|
|
172
|
+
evidence: [createEvidence(relPath, '1', 'route file with no exports', code.slice(0, 100))],
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const m of methods) {
|
|
178
|
+
routes.push({
|
|
179
|
+
method: m.name,
|
|
180
|
+
path: routePath,
|
|
181
|
+
handler: `${relPath}:${m.line}`,
|
|
182
|
+
framework: 'next',
|
|
183
|
+
routerType: 'app',
|
|
184
|
+
confidence: 'high',
|
|
185
|
+
evidence: [createEvidence(relPath, String(m.line), `export ${m.name}`, m.snippet)],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Pages Router: pages/api/**/*.ts|js
|
|
192
|
+
const pagesDirs = ['pages', 'src/pages'];
|
|
193
|
+
for (const pagesDir of pagesDirs) {
|
|
194
|
+
const apiDir = path.join(repoRoot, pagesDir, 'api');
|
|
195
|
+
if (!fs.existsSync(apiDir)) continue;
|
|
196
|
+
|
|
197
|
+
const apiFiles = findFiles(apiDir, /\.(ts|js)$/, /\.d\.ts$/);
|
|
198
|
+
|
|
199
|
+
for (const file of apiFiles) {
|
|
200
|
+
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
201
|
+
const apiIdx = relPath.indexOf('/api/');
|
|
202
|
+
const sub = relPath
|
|
203
|
+
.slice(apiIdx + '/api/'.length)
|
|
204
|
+
.replace(/\.(ts|js)$/, '')
|
|
205
|
+
.replace(/\/index$/, '');
|
|
206
|
+
|
|
207
|
+
const routePath = canonicalizePath('/api/' + sub);
|
|
208
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
209
|
+
const hasDefaultExport = /export\s+default/.test(code);
|
|
210
|
+
|
|
211
|
+
routes.push({
|
|
212
|
+
method: '*',
|
|
213
|
+
path: routePath,
|
|
214
|
+
handler: relPath,
|
|
215
|
+
framework: 'next',
|
|
216
|
+
routerType: 'pages',
|
|
217
|
+
confidence: hasDefaultExport ? 'med' : 'low',
|
|
218
|
+
evidence: [createEvidence(relPath, '1', 'Pages API route', code.slice(0, 100))],
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return routes;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// FASTIFY RESOLVER (Simplified - regex based)
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
async function resolveFastifyRoutes(repoRoot) {
|
|
231
|
+
const routes = [];
|
|
232
|
+
const gaps = [];
|
|
233
|
+
|
|
234
|
+
const entryPoints = [
|
|
235
|
+
'src/server.ts', 'src/server.js', 'src/index.ts', 'src/index.js',
|
|
236
|
+
'server.ts', 'server.js', 'apps/api/src/server.ts', 'apps/api/src/index.ts',
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const fastifyMethods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'all'];
|
|
240
|
+
|
|
241
|
+
// Find source files
|
|
242
|
+
const srcDirs = ['src', 'apps/api/src', 'server'];
|
|
243
|
+
const files = [];
|
|
244
|
+
|
|
245
|
+
for (const srcDir of srcDirs) {
|
|
246
|
+
const fullDir = path.join(repoRoot, srcDir);
|
|
247
|
+
if (fs.existsSync(fullDir)) {
|
|
248
|
+
files.push(...findFiles(fullDir, /\.(ts|js)$/, /\.d\.ts$/));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Patterns to detect routes
|
|
253
|
+
const patterns = [
|
|
254
|
+
// fastify.get('/path', handler)
|
|
255
|
+
/(?:fastify|app|server)\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
256
|
+
// router.get('/path', handler)
|
|
257
|
+
/router\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
258
|
+
// .route({ method: 'GET', url: '/path' })
|
|
259
|
+
/\.route\s*\(\s*\{[^}]*method:\s*['"`]([^'"`]+)['"`][^}]*url:\s*['"`]([^'"`]+)['"`]/gi,
|
|
260
|
+
/\.route\s*\(\s*\{[^}]*url:\s*['"`]([^'"`]+)['"`][^}]*method:\s*['"`]([^'"`]+)['"`]/gi,
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
// Track prefixes from register calls
|
|
264
|
+
const prefixMap = new Map(); // file → prefix
|
|
265
|
+
|
|
266
|
+
for (const file of files) {
|
|
267
|
+
try {
|
|
268
|
+
const code = fs.readFileSync(file, 'utf8');
|
|
269
|
+
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
270
|
+
const lines = code.split('\n');
|
|
271
|
+
|
|
272
|
+
// Detect prefix from register calls
|
|
273
|
+
const registerPattern = /\.register\s*\([^,]+,\s*\{[^}]*prefix:\s*['"`]([^'"`]+)['"`]/g;
|
|
274
|
+
let match;
|
|
275
|
+
while ((match = registerPattern.exec(code)) !== null) {
|
|
276
|
+
prefixMap.set(relPath, match[1]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Extract routes
|
|
280
|
+
for (const pattern of patterns) {
|
|
281
|
+
pattern.lastIndex = 0;
|
|
282
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
283
|
+
let method, routePath;
|
|
284
|
+
|
|
285
|
+
if (match[0].includes('.route')) {
|
|
286
|
+
// Handle .route() pattern - order varies
|
|
287
|
+
if (match[0].indexOf('method') < match[0].indexOf('url')) {
|
|
288
|
+
method = match[1];
|
|
289
|
+
routePath = match[2];
|
|
290
|
+
} else {
|
|
291
|
+
routePath = match[1];
|
|
292
|
+
method = match[2];
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
method = match[1];
|
|
296
|
+
routePath = match[2];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const prefix = prefixMap.get(relPath) || '';
|
|
300
|
+
const fullPath = joinPrefix(prefix, routePath);
|
|
301
|
+
const lineNum = code.substring(0, match.index).split('\n').length;
|
|
302
|
+
const snippet = lines[lineNum - 1] || '';
|
|
303
|
+
|
|
304
|
+
routes.push({
|
|
305
|
+
method: canonicalizeMethod(method),
|
|
306
|
+
path: fullPath,
|
|
307
|
+
handler: `${relPath}:${lineNum}`,
|
|
308
|
+
framework: 'fastify',
|
|
309
|
+
confidence: 'med',
|
|
310
|
+
evidence: [createEvidence(relPath, String(lineNum), `fastify.${method}('${routePath}')`, snippet)],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { routes, gaps };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// ROUTE INDEX
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
class RouteIndex {
|
|
325
|
+
constructor() {
|
|
326
|
+
this.routes = [];
|
|
327
|
+
this.byMethod = new Map();
|
|
328
|
+
this.byPath = new Map();
|
|
329
|
+
this.parameterized = [];
|
|
330
|
+
this.gaps = [];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async build(repoRoot) {
|
|
334
|
+
// Resolve Next.js routes
|
|
335
|
+
const nextRoutes = await resolveNextRoutes(repoRoot);
|
|
336
|
+
this.routes.push(...nextRoutes);
|
|
337
|
+
|
|
338
|
+
// Resolve Fastify routes
|
|
339
|
+
const { routes: fastifyRoutes, gaps } = await resolveFastifyRoutes(repoRoot);
|
|
340
|
+
this.routes.push(...fastifyRoutes);
|
|
341
|
+
this.gaps.push(...gaps);
|
|
342
|
+
|
|
343
|
+
// Build indexes
|
|
344
|
+
for (const route of this.routes) {
|
|
345
|
+
const methodKey = route.method;
|
|
346
|
+
if (!this.byMethod.has(methodKey)) this.byMethod.set(methodKey, []);
|
|
347
|
+
this.byMethod.get(methodKey).push(route);
|
|
348
|
+
|
|
349
|
+
const pathKey = route.path;
|
|
350
|
+
if (!this.byPath.has(pathKey)) this.byPath.set(pathKey, []);
|
|
351
|
+
this.byPath.get(pathKey).push(route);
|
|
352
|
+
|
|
353
|
+
if (isParameterizedPath(route.path)) {
|
|
354
|
+
this.parameterized.push(route);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
findRoutes(method, path) {
|
|
362
|
+
const canonicalMethod = canonicalizeMethod(method);
|
|
363
|
+
const canonicalPath = canonicalizePath(path);
|
|
364
|
+
const matches = [];
|
|
365
|
+
|
|
366
|
+
// Exact path match
|
|
367
|
+
const pathMatches = this.byPath.get(canonicalPath) || [];
|
|
368
|
+
for (const route of pathMatches) {
|
|
369
|
+
if (matchMethod(route.method, canonicalMethod)) {
|
|
370
|
+
matches.push(route);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Wildcard method match
|
|
375
|
+
const wildcardMethods = this.byMethod.get('*') || [];
|
|
376
|
+
for (const route of wildcardMethods) {
|
|
377
|
+
if (route.path === canonicalPath && !matches.includes(route)) {
|
|
378
|
+
matches.push(route);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Parameterized route match
|
|
383
|
+
for (const route of this.parameterized) {
|
|
384
|
+
if (matchPath(route.path, canonicalPath) && matchMethod(route.method, canonicalMethod)) {
|
|
385
|
+
if (!matches.includes(route)) matches.push(route);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return matches;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
findClosestRoutes(path, limit = 3) {
|
|
393
|
+
const canonicalPath = canonicalizePath(path);
|
|
394
|
+
const pathParts = canonicalPath.split('/').filter(Boolean);
|
|
395
|
+
|
|
396
|
+
const scored = this.routes.map(route => {
|
|
397
|
+
const routeParts = route.path.split('/').filter(Boolean);
|
|
398
|
+
let score = 0;
|
|
399
|
+
|
|
400
|
+
for (let i = 0; i < Math.min(pathParts.length, routeParts.length); i++) {
|
|
401
|
+
if (pathParts[i] === routeParts[i] || routeParts[i].startsWith(':')) {
|
|
402
|
+
score++;
|
|
403
|
+
} else break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (pathParts.length === routeParts.length) score += 0.5;
|
|
407
|
+
return { route, score };
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map(s => s.route);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getRouteMap() {
|
|
414
|
+
return {
|
|
415
|
+
server: this.routes,
|
|
416
|
+
clientRefs: [],
|
|
417
|
+
gaps: this.gaps,
|
|
418
|
+
generatedAt: new Date().toISOString(),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// VALIDATE CLAIM
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
async function validateRouteExists(claim, repoRoot, routeIndex) {
|
|
428
|
+
const index = routeIndex || new RouteIndex();
|
|
429
|
+
if (!routeIndex) await index.build(repoRoot);
|
|
430
|
+
|
|
431
|
+
const method = claim.method || '*';
|
|
432
|
+
const routePath = claim.path;
|
|
433
|
+
|
|
434
|
+
const matches = index.findRoutes(method, routePath);
|
|
435
|
+
|
|
436
|
+
if (matches.length > 0) {
|
|
437
|
+
return {
|
|
438
|
+
result: 'true',
|
|
439
|
+
confidence: matches[0].confidence,
|
|
440
|
+
evidence: matches[0].evidence,
|
|
441
|
+
matchedRoute: matches[0],
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const closest = index.findClosestRoutes(routePath);
|
|
446
|
+
const hasGaps = index.gaps.length > 0;
|
|
447
|
+
|
|
448
|
+
if (hasGaps) {
|
|
449
|
+
return {
|
|
450
|
+
result: 'unknown',
|
|
451
|
+
confidence: 'low',
|
|
452
|
+
evidence: [],
|
|
453
|
+
closestRoutes: closest,
|
|
454
|
+
gaps: index.gaps,
|
|
455
|
+
nextSteps: ['Some routes may not be detected due to unresolved plugins'],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
result: 'false',
|
|
461
|
+
confidence: 'high',
|
|
462
|
+
evidence: [],
|
|
463
|
+
closestRoutes: closest,
|
|
464
|
+
nextSteps: closest.length > 0
|
|
465
|
+
? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(', ')}?`]
|
|
466
|
+
: ['No similar routes found'],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
module.exports = {
|
|
471
|
+
canonicalizePath,
|
|
472
|
+
canonicalizeMethod,
|
|
473
|
+
resolveNextRoutes,
|
|
474
|
+
resolveFastifyRoutes,
|
|
475
|
+
RouteIndex,
|
|
476
|
+
validateRouteExists,
|
|
477
|
+
};
|