@tyroneross/navgator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +486 -0
- package/agents/architecture-advisor.md +109 -0
- package/commands/nav-check.md +64 -0
- package/commands/nav-connections.md +58 -0
- package/commands/nav-diagram.md +106 -0
- package/commands/nav-export.md +71 -0
- package/commands/nav-impact.md +58 -0
- package/commands/nav-scan.md +46 -0
- package/commands/nav-status.md +44 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +627 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config.d.ts +95 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +262 -0
- package/dist/config.js.map +1 -0
- package/dist/diagram.d.ts +36 -0
- package/dist/diagram.d.ts.map +1 -0
- package/dist/diagram.js +333 -0
- package/dist/diagram.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner.d.ts +57 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +282 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scanners/connections/ast-scanner.d.ts +26 -0
- package/dist/scanners/connections/ast-scanner.d.ts.map +1 -0
- package/dist/scanners/connections/ast-scanner.js +430 -0
- package/dist/scanners/connections/ast-scanner.js.map +1 -0
- package/dist/scanners/connections/service-calls.d.ts +14 -0
- package/dist/scanners/connections/service-calls.d.ts.map +1 -0
- package/dist/scanners/connections/service-calls.js +719 -0
- package/dist/scanners/connections/service-calls.js.map +1 -0
- package/dist/scanners/infrastructure/index.d.ts +27 -0
- package/dist/scanners/infrastructure/index.d.ts.map +1 -0
- package/dist/scanners/infrastructure/index.js +233 -0
- package/dist/scanners/infrastructure/index.js.map +1 -0
- package/dist/scanners/packages/npm.d.ts +18 -0
- package/dist/scanners/packages/npm.d.ts.map +1 -0
- package/dist/scanners/packages/npm.js +256 -0
- package/dist/scanners/packages/npm.js.map +1 -0
- package/dist/scanners/packages/pip.d.ts +14 -0
- package/dist/scanners/packages/pip.d.ts.map +1 -0
- package/dist/scanners/packages/pip.js +228 -0
- package/dist/scanners/packages/pip.js.map +1 -0
- package/dist/scanners/prompts/detector.d.ts +119 -0
- package/dist/scanners/prompts/detector.d.ts.map +1 -0
- package/dist/scanners/prompts/detector.js +617 -0
- package/dist/scanners/prompts/detector.js.map +1 -0
- package/dist/scanners/prompts/index.d.ts +51 -0
- package/dist/scanners/prompts/index.d.ts.map +1 -0
- package/dist/scanners/prompts/index.js +340 -0
- package/dist/scanners/prompts/index.js.map +1 -0
- package/dist/scanners/prompts/types.d.ts +127 -0
- package/dist/scanners/prompts/types.d.ts.map +1 -0
- package/dist/scanners/prompts/types.js +37 -0
- package/dist/scanners/prompts/types.js.map +1 -0
- package/dist/setup.d.ts +65 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +261 -0
- package/dist/setup.js.map +1 -0
- package/dist/storage.d.ts +147 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +931 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +55 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-server.d.ts +17 -0
- package/dist/ui-server.d.ts.map +1 -0
- package/dist/ui-server.js +815 -0
- package/dist/ui-server.js.map +1 -0
- package/hooks/hooks.json +57 -0
- package/package.json +80 -0
- package/scripts/ibr-ui-test.mjs +359 -0
- package/scripts/postinstall.cjs +35 -0
- package/skills/architecture-awareness/SKILL.md +141 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Call Scanner
|
|
3
|
+
* Detects connections to external services (Stripe, OpenAI, Claude, etc.)
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { glob } from 'glob';
|
|
8
|
+
import { generateConnectionId, generateComponentId, } from '../../types.js';
|
|
9
|
+
const SERVICE_PATTERNS = [
|
|
10
|
+
// AI/LLM Services - These get their own 'llm' type for visibility
|
|
11
|
+
{
|
|
12
|
+
serviceName: 'Claude (Anthropic)',
|
|
13
|
+
patterns: [
|
|
14
|
+
/anthropic\.messages\.create/,
|
|
15
|
+
/anthropic\.completions\.create/,
|
|
16
|
+
/new Anthropic\(/,
|
|
17
|
+
/from anthropic import/,
|
|
18
|
+
/AnthropicAI/,
|
|
19
|
+
/import\s+Anthropic\s+from\s+['"]@anthropic-ai\/sdk['"]/,
|
|
20
|
+
/require\(['"]@anthropic-ai\/sdk['"]\)/,
|
|
21
|
+
/ChatAnthropic\(/,
|
|
22
|
+
],
|
|
23
|
+
componentType: 'llm',
|
|
24
|
+
layer: 'external',
|
|
25
|
+
purpose: 'Claude AI API',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
serviceName: 'OpenAI',
|
|
29
|
+
patterns: [
|
|
30
|
+
/openai\.chat\.completions\.create/,
|
|
31
|
+
/openai\.completions\.create/,
|
|
32
|
+
/openai\.embeddings\.create/,
|
|
33
|
+
/new OpenAI\(/,
|
|
34
|
+
/from openai import/,
|
|
35
|
+
/OpenAIApi\(/,
|
|
36
|
+
/import\s+OpenAI\s+from\s+['"]openai['"]/,
|
|
37
|
+
/require\(['"]openai['"]\)/,
|
|
38
|
+
/ChatOpenAI\(/,
|
|
39
|
+
/wrapOpenAI\(/,
|
|
40
|
+
],
|
|
41
|
+
componentType: 'llm',
|
|
42
|
+
layer: 'external',
|
|
43
|
+
purpose: 'OpenAI API',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
serviceName: 'Groq',
|
|
47
|
+
patterns: [
|
|
48
|
+
/new Groq\(/,
|
|
49
|
+
/groq\.chat\.completions\.create/,
|
|
50
|
+
/from groq import/,
|
|
51
|
+
/import\s+Groq\s+from\s+['"]groq-sdk['"]/,
|
|
52
|
+
/require\(['"]groq-sdk['"]\)/,
|
|
53
|
+
/ChatGroq\(/,
|
|
54
|
+
/from\s+['"]@langchain\/groq['"]/,
|
|
55
|
+
],
|
|
56
|
+
componentType: 'llm',
|
|
57
|
+
layer: 'external',
|
|
58
|
+
purpose: 'Groq LLM API',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
serviceName: 'Cohere',
|
|
62
|
+
patterns: [
|
|
63
|
+
/new Cohere\(/,
|
|
64
|
+
/cohere\.generate/,
|
|
65
|
+
/cohere\.chat/,
|
|
66
|
+
/from cohere import/,
|
|
67
|
+
],
|
|
68
|
+
componentType: 'llm',
|
|
69
|
+
layer: 'external',
|
|
70
|
+
purpose: 'Cohere API',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
serviceName: 'Gemini (Google)',
|
|
74
|
+
patterns: [
|
|
75
|
+
/GenerativeModel\(/,
|
|
76
|
+
/gemini-pro/,
|
|
77
|
+
/from google\.generativeai/,
|
|
78
|
+
/ChatGoogleGenerativeAI\(/,
|
|
79
|
+
/from\s+['"]@langchain\/google-genai['"]/,
|
|
80
|
+
],
|
|
81
|
+
componentType: 'llm',
|
|
82
|
+
layer: 'external',
|
|
83
|
+
purpose: 'Google Gemini API',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
serviceName: 'Vercel AI SDK',
|
|
87
|
+
patterns: [
|
|
88
|
+
/from\s+['"]ai['"]/,
|
|
89
|
+
/from\s+['"]@ai-sdk\//,
|
|
90
|
+
/import\s+\{[^}]*generateText[^}]*\}/,
|
|
91
|
+
/import\s+\{[^}]*streamText[^}]*\}/,
|
|
92
|
+
/import\s+\{[^}]*generateObject[^}]*\}/,
|
|
93
|
+
/import\s+\{[^}]*useChat[^}]*\}/,
|
|
94
|
+
/import\s+\{[^}]*useCompletion[^}]*\}/,
|
|
95
|
+
],
|
|
96
|
+
componentType: 'llm',
|
|
97
|
+
layer: 'external',
|
|
98
|
+
purpose: 'Vercel AI SDK',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
serviceName: 'LangChain',
|
|
102
|
+
patterns: [
|
|
103
|
+
/from\s+['"]langchain/,
|
|
104
|
+
/from\s+['"]@langchain\//,
|
|
105
|
+
/require\(['"]langchain/,
|
|
106
|
+
/require\(['"]@langchain\//,
|
|
107
|
+
/ChatPromptTemplate\./,
|
|
108
|
+
/StructuredOutputParser\./,
|
|
109
|
+
/RunnableSequence\./,
|
|
110
|
+
],
|
|
111
|
+
componentType: 'llm',
|
|
112
|
+
layer: 'external',
|
|
113
|
+
purpose: 'LangChain framework',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
serviceName: 'LangSmith',
|
|
117
|
+
patterns: [
|
|
118
|
+
/from\s+['"]langsmith/,
|
|
119
|
+
/require\(['"]langsmith/,
|
|
120
|
+
/traceable\(/,
|
|
121
|
+
/LANGCHAIN_TRACING/,
|
|
122
|
+
],
|
|
123
|
+
componentType: 'llm',
|
|
124
|
+
layer: 'external',
|
|
125
|
+
purpose: 'LangSmith observability',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
serviceName: 'Mistral',
|
|
129
|
+
patterns: [
|
|
130
|
+
/new MistralClient\(/,
|
|
131
|
+
/import\s+.*from\s+['"]@mistralai/,
|
|
132
|
+
/from mistralai import/,
|
|
133
|
+
],
|
|
134
|
+
componentType: 'llm',
|
|
135
|
+
layer: 'external',
|
|
136
|
+
purpose: 'Mistral AI API',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
serviceName: 'Replicate',
|
|
140
|
+
patterns: [
|
|
141
|
+
/new Replicate\(/,
|
|
142
|
+
/import\s+Replicate\s+from\s+['"]replicate['"]/,
|
|
143
|
+
/replicate\.run\(/,
|
|
144
|
+
],
|
|
145
|
+
componentType: 'llm',
|
|
146
|
+
layer: 'external',
|
|
147
|
+
purpose: 'Replicate API',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
serviceName: 'HuggingFace',
|
|
151
|
+
patterns: [
|
|
152
|
+
/HfInference\(/,
|
|
153
|
+
/from\s+['"]@huggingface\/inference['"]/,
|
|
154
|
+
/huggingface\.co\/api/,
|
|
155
|
+
],
|
|
156
|
+
componentType: 'llm',
|
|
157
|
+
layer: 'external',
|
|
158
|
+
purpose: 'HuggingFace Inference API',
|
|
159
|
+
},
|
|
160
|
+
// Payment Services
|
|
161
|
+
{
|
|
162
|
+
serviceName: 'Stripe',
|
|
163
|
+
patterns: [
|
|
164
|
+
/stripe\.customers\./,
|
|
165
|
+
/stripe\.paymentIntents\./,
|
|
166
|
+
/stripe\.subscriptions\./,
|
|
167
|
+
/stripe\.invoices\./,
|
|
168
|
+
/stripe\.checkout\./,
|
|
169
|
+
/new Stripe\(/,
|
|
170
|
+
],
|
|
171
|
+
componentType: 'service',
|
|
172
|
+
layer: 'external',
|
|
173
|
+
purpose: 'Stripe payments',
|
|
174
|
+
},
|
|
175
|
+
// Database Services
|
|
176
|
+
{
|
|
177
|
+
serviceName: 'Supabase',
|
|
178
|
+
patterns: [
|
|
179
|
+
/supabase\.from\(/,
|
|
180
|
+
/createClient\(\s*process\.env\.SUPABASE/,
|
|
181
|
+
/supabase\.auth\./,
|
|
182
|
+
/supabase\.storage\./,
|
|
183
|
+
],
|
|
184
|
+
componentType: 'database',
|
|
185
|
+
layer: 'database',
|
|
186
|
+
purpose: 'Supabase backend',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
serviceName: 'Firebase',
|
|
190
|
+
patterns: [
|
|
191
|
+
/firebase\.firestore\(/,
|
|
192
|
+
/firebase\.auth\(/,
|
|
193
|
+
/initializeApp\(/,
|
|
194
|
+
/getFirestore\(/,
|
|
195
|
+
],
|
|
196
|
+
componentType: 'database',
|
|
197
|
+
layer: 'database',
|
|
198
|
+
purpose: 'Firebase backend',
|
|
199
|
+
},
|
|
200
|
+
// Queue Services
|
|
201
|
+
{
|
|
202
|
+
serviceName: 'BullMQ',
|
|
203
|
+
patterns: [
|
|
204
|
+
/new Queue\(/,
|
|
205
|
+
/new Worker\(/,
|
|
206
|
+
/Queue\.add\(/,
|
|
207
|
+
/from 'bullmq'/,
|
|
208
|
+
],
|
|
209
|
+
componentType: 'queue',
|
|
210
|
+
layer: 'queue',
|
|
211
|
+
purpose: 'BullMQ job queue',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
serviceName: 'Celery',
|
|
215
|
+
patterns: [
|
|
216
|
+
/@celery\.task/,
|
|
217
|
+
/celery\.send_task/,
|
|
218
|
+
/delay\(\)/,
|
|
219
|
+
/apply_async\(/,
|
|
220
|
+
],
|
|
221
|
+
componentType: 'queue',
|
|
222
|
+
layer: 'queue',
|
|
223
|
+
purpose: 'Celery task queue',
|
|
224
|
+
},
|
|
225
|
+
// Communication Services
|
|
226
|
+
{
|
|
227
|
+
serviceName: 'Twilio',
|
|
228
|
+
patterns: [
|
|
229
|
+
/twilio\.messages\.create/,
|
|
230
|
+
/new Twilio\(/,
|
|
231
|
+
/twilio\.calls\./,
|
|
232
|
+
],
|
|
233
|
+
componentType: 'service',
|
|
234
|
+
layer: 'external',
|
|
235
|
+
purpose: 'Twilio SMS/Voice',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
serviceName: 'SendGrid',
|
|
239
|
+
patterns: [
|
|
240
|
+
/sgMail\.send/,
|
|
241
|
+
/@sendgrid\/mail/,
|
|
242
|
+
/sendgrid\.send/,
|
|
243
|
+
],
|
|
244
|
+
componentType: 'service',
|
|
245
|
+
layer: 'external',
|
|
246
|
+
purpose: 'SendGrid email',
|
|
247
|
+
},
|
|
248
|
+
// Cloud Storage
|
|
249
|
+
{
|
|
250
|
+
serviceName: 'AWS S3',
|
|
251
|
+
patterns: [
|
|
252
|
+
/s3\.putObject/,
|
|
253
|
+
/s3\.getObject/,
|
|
254
|
+
/S3Client\(/,
|
|
255
|
+
/PutObjectCommand/,
|
|
256
|
+
],
|
|
257
|
+
componentType: 'service',
|
|
258
|
+
layer: 'external',
|
|
259
|
+
purpose: 'AWS S3 storage',
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// FALSE POSITIVE DETECTION
|
|
264
|
+
// =============================================================================
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// ACCURACY GUARDRAILS
|
|
267
|
+
// =============================================================================
|
|
268
|
+
//
|
|
269
|
+
// Strategy: Context-aware confidence scoring instead of LLM post-processing.
|
|
270
|
+
// Inspired by ZeroFalse (arxiv:2510.02534) approach of enriching static analysis
|
|
271
|
+
// with flow-sensitive context, but without the LLM dependency.
|
|
272
|
+
//
|
|
273
|
+
// Three layers:
|
|
274
|
+
// 1. Line-level: Is the match in a comment, string literal, or example code?
|
|
275
|
+
// 2. File-level: Is this a test, mock, docs, or generated file?
|
|
276
|
+
// 3. Corroboration: Does an import/require for this service exist in the file?
|
|
277
|
+
//
|
|
278
|
+
// Each layer adjusts confidence. Only results >= 0.5 confidence are surfaced.
|
|
279
|
+
// =============================================================================
|
|
280
|
+
const MIN_CONFIDENCE = 0.5;
|
|
281
|
+
/**
|
|
282
|
+
* Check if a match is inside a comment
|
|
283
|
+
*/
|
|
284
|
+
function isInComment(line, matchIndex) {
|
|
285
|
+
const trimmed = line.trimStart();
|
|
286
|
+
// Single-line comment (JS/TS/Python)
|
|
287
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*')) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
// Inline comment: check if // appears before the match
|
|
291
|
+
const commentStart = line.indexOf('//');
|
|
292
|
+
if (commentStart >= 0 && commentStart < matchIndex) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Check if a match is inside a string literal (not actual code)
|
|
299
|
+
*/
|
|
300
|
+
function isInStringLiteral(line, matchStart) {
|
|
301
|
+
// Count unescaped quotes before the match position
|
|
302
|
+
let inSingle = false;
|
|
303
|
+
let inDouble = false;
|
|
304
|
+
for (let i = 0; i < matchStart && i < line.length; i++) {
|
|
305
|
+
const c = line[i];
|
|
306
|
+
const prev = i > 0 ? line[i - 1] : '';
|
|
307
|
+
if (c === "'" && prev !== '\\' && !inDouble)
|
|
308
|
+
inSingle = !inSingle;
|
|
309
|
+
if (c === '"' && prev !== '\\' && !inSingle)
|
|
310
|
+
inDouble = !inDouble;
|
|
311
|
+
}
|
|
312
|
+
return inSingle || inDouble;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Check if a line is example/mock code (string literal containing code)
|
|
316
|
+
*/
|
|
317
|
+
function isExampleCode(line) {
|
|
318
|
+
const examplePatterns = [
|
|
319
|
+
/code:\s*["'`].*["'`]/,
|
|
320
|
+
/example:\s*["'`]/,
|
|
321
|
+
/snippet:\s*["'`]/,
|
|
322
|
+
/sample:\s*["'`]/,
|
|
323
|
+
/mock:\s*["'`]/,
|
|
324
|
+
/["'`]await\s+\w+\.\w+\.\w+\([^)]*\)["'`]/,
|
|
325
|
+
/["'`][^"'`]*\.\.\.[^"'`]*["'`]/,
|
|
326
|
+
];
|
|
327
|
+
return examplePatterns.some(pattern => pattern.test(line));
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Check if a file should be excluded from scanning
|
|
331
|
+
*/
|
|
332
|
+
function shouldExcludeFile(file, projectRoot) {
|
|
333
|
+
const excludePatterns = [
|
|
334
|
+
/NavGator\/src\//,
|
|
335
|
+
/NavGator\/web\//,
|
|
336
|
+
/\/__tests__\//,
|
|
337
|
+
/\/test\//,
|
|
338
|
+
/\/tests\//,
|
|
339
|
+
/\/mocks?\//,
|
|
340
|
+
/\/fixtures?\//,
|
|
341
|
+
/\.test\.(ts|tsx|js|jsx)$/,
|
|
342
|
+
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
343
|
+
/\.mock\.(ts|tsx|js|jsx)$/,
|
|
344
|
+
];
|
|
345
|
+
const fullPath = path.join(projectRoot, file);
|
|
346
|
+
return excludePatterns.some(pattern => pattern.test(fullPath) || pattern.test(file));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Check if a file is documentation, config, or generated code (lower confidence)
|
|
350
|
+
*/
|
|
351
|
+
function getFileConfidenceModifier(file) {
|
|
352
|
+
// Documentation / non-code files — lower confidence
|
|
353
|
+
if (/\.(md|mdx|txt|rst|adoc)$/.test(file))
|
|
354
|
+
return -0.4;
|
|
355
|
+
if (/README|CHANGELOG|LICENSE/i.test(file))
|
|
356
|
+
return -0.4;
|
|
357
|
+
// Generated / compiled
|
|
358
|
+
if (/\.(d\.ts|map|min\.js)$/.test(file))
|
|
359
|
+
return -0.3;
|
|
360
|
+
if (/\/dist\/|\/build\/|\/generated\//.test(file))
|
|
361
|
+
return -0.3;
|
|
362
|
+
// Config files — sometimes legitimate (e.g., docker-compose)
|
|
363
|
+
if (/\.(json|ya?ml|toml|ini)$/.test(file))
|
|
364
|
+
return -0.1;
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Check if the file contains a corroborating import/require for a service.
|
|
369
|
+
* An import + a call site = high confidence. A call site without import = suspicious.
|
|
370
|
+
*/
|
|
371
|
+
function hasCorroboratingImport(fileContent, serviceName) {
|
|
372
|
+
const importPatterns = {
|
|
373
|
+
'Claude (Anthropic)': [/@anthropic-ai\/sdk/, /anthropic/],
|
|
374
|
+
'OpenAI': [/['"]openai['"]/, /@langchain\/openai/],
|
|
375
|
+
'Groq': [/groq-sdk/, /@langchain\/groq/],
|
|
376
|
+
'Stripe': [/['"]stripe['"]/],
|
|
377
|
+
'Supabase': [/@supabase\/supabase-js/],
|
|
378
|
+
'Firebase': [/firebase\//],
|
|
379
|
+
'BullMQ': [/['"]bullmq['"]/],
|
|
380
|
+
'Twilio': [/['"]twilio['"]/],
|
|
381
|
+
'SendGrid': [/@sendgrid\//],
|
|
382
|
+
'AWS S3': [/@aws-sdk\/client-s3/],
|
|
383
|
+
'Vercel AI SDK': [/['"]ai['"]/, /@ai-sdk\//],
|
|
384
|
+
'LangChain': [/langchain/, /@langchain\//],
|
|
385
|
+
'LangSmith': [/langsmith/],
|
|
386
|
+
'Cohere': [/['"]cohere['"]/],
|
|
387
|
+
'Gemini (Google)': [/google\.generativeai/, /@langchain\/google/],
|
|
388
|
+
'Mistral': [/@mistralai/],
|
|
389
|
+
'Replicate': [/['"]replicate['"]/],
|
|
390
|
+
'HuggingFace': [/@huggingface\//],
|
|
391
|
+
};
|
|
392
|
+
const patterns = importPatterns[serviceName];
|
|
393
|
+
if (!patterns)
|
|
394
|
+
return true; // No import check available, don't penalize
|
|
395
|
+
return patterns.some(p => p.test(fileContent));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Compute final confidence for a match, applying all guardrail layers.
|
|
399
|
+
* Returns 0 if the match should be discarded entirely.
|
|
400
|
+
*/
|
|
401
|
+
function computeConfidence(line, matchIndex, file, fileContent, serviceName, baseConfidence = 0.9) {
|
|
402
|
+
let confidence = baseConfidence;
|
|
403
|
+
// Layer 1: Line-level checks
|
|
404
|
+
if (isInComment(line, matchIndex))
|
|
405
|
+
return 0;
|
|
406
|
+
if (isExampleCode(line))
|
|
407
|
+
return 0;
|
|
408
|
+
if (isInStringLiteral(line, matchIndex)) {
|
|
409
|
+
confidence -= 0.3;
|
|
410
|
+
}
|
|
411
|
+
// Layer 2: File-level checks
|
|
412
|
+
confidence += getFileConfidenceModifier(file);
|
|
413
|
+
// Layer 3: Corroboration — does the file import this service?
|
|
414
|
+
if (!hasCorroboratingImport(fileContent, serviceName)) {
|
|
415
|
+
confidence -= 0.2;
|
|
416
|
+
}
|
|
417
|
+
return Math.max(0, Math.min(1, confidence));
|
|
418
|
+
}
|
|
419
|
+
// =============================================================================
|
|
420
|
+
// SCANNING
|
|
421
|
+
// =============================================================================
|
|
422
|
+
/**
|
|
423
|
+
* Scan for service calls in the codebase
|
|
424
|
+
*/
|
|
425
|
+
export async function scanServiceCalls(projectRoot) {
|
|
426
|
+
const components = [];
|
|
427
|
+
const connections = [];
|
|
428
|
+
const timestamp = Date.now();
|
|
429
|
+
// Find all source files
|
|
430
|
+
const sourceFiles = await glob('**/*.{ts,tsx,js,jsx,py}', {
|
|
431
|
+
cwd: projectRoot,
|
|
432
|
+
ignore: [
|
|
433
|
+
'node_modules/**',
|
|
434
|
+
'dist/**',
|
|
435
|
+
'build/**',
|
|
436
|
+
'.next/**',
|
|
437
|
+
'__pycache__/**',
|
|
438
|
+
'venv/**',
|
|
439
|
+
'**/node_modules/**',
|
|
440
|
+
'**/.git/**',
|
|
441
|
+
],
|
|
442
|
+
});
|
|
443
|
+
// Track which services we've found
|
|
444
|
+
const foundServices = new Map();
|
|
445
|
+
for (const file of sourceFiles) {
|
|
446
|
+
// Skip files that should be excluded (NavGator's own code, test files, etc.)
|
|
447
|
+
if (shouldExcludeFile(file, projectRoot)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const filePath = path.join(projectRoot, file);
|
|
451
|
+
// Skip if not a file (could be a directory matching the glob pattern)
|
|
452
|
+
try {
|
|
453
|
+
const stat = await fs.promises.stat(filePath);
|
|
454
|
+
if (!stat.isFile())
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
continue; // Skip if we can't stat the file
|
|
459
|
+
}
|
|
460
|
+
let content;
|
|
461
|
+
try {
|
|
462
|
+
content = await fs.promises.readFile(filePath, 'utf-8');
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
continue; // Skip files we can't read
|
|
466
|
+
}
|
|
467
|
+
const lines = content.split('\n');
|
|
468
|
+
for (const pattern of SERVICE_PATTERNS) {
|
|
469
|
+
for (let i = 0; i < lines.length; i++) {
|
|
470
|
+
const line = lines[i];
|
|
471
|
+
for (const regex of pattern.patterns) {
|
|
472
|
+
const match = regex.exec(line);
|
|
473
|
+
if (match) {
|
|
474
|
+
// Compute confidence with all guardrail layers
|
|
475
|
+
const confidence = computeConfidence(line, match.index, file, content, pattern.serviceName);
|
|
476
|
+
// Skip low-confidence matches
|
|
477
|
+
if (confidence < MIN_CONFIDENCE) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
// Create service component if not exists (use highest confidence seen)
|
|
481
|
+
if (!foundServices.has(pattern.serviceName)) {
|
|
482
|
+
const component = {
|
|
483
|
+
component_id: generateComponentId(pattern.componentType, pattern.serviceName),
|
|
484
|
+
name: pattern.serviceName,
|
|
485
|
+
type: pattern.componentType,
|
|
486
|
+
role: {
|
|
487
|
+
purpose: pattern.purpose,
|
|
488
|
+
layer: pattern.layer,
|
|
489
|
+
critical: true,
|
|
490
|
+
},
|
|
491
|
+
source: {
|
|
492
|
+
detection_method: 'auto',
|
|
493
|
+
config_files: [],
|
|
494
|
+
confidence,
|
|
495
|
+
},
|
|
496
|
+
connects_to: [],
|
|
497
|
+
connected_from: [],
|
|
498
|
+
status: 'active',
|
|
499
|
+
tags: [pattern.componentType, pattern.layer],
|
|
500
|
+
timestamp,
|
|
501
|
+
last_updated: timestamp,
|
|
502
|
+
};
|
|
503
|
+
foundServices.set(pattern.serviceName, component);
|
|
504
|
+
components.push(component);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// Update confidence if this match is higher
|
|
508
|
+
const existing = foundServices.get(pattern.serviceName);
|
|
509
|
+
if (confidence > existing.source.confidence) {
|
|
510
|
+
existing.source.confidence = confidence;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Create connection with computed confidence
|
|
514
|
+
const serviceComponent = foundServices.get(pattern.serviceName);
|
|
515
|
+
const functionName = extractFunctionName(lines, i);
|
|
516
|
+
const connection = {
|
|
517
|
+
connection_id: generateConnectionId('service-call'),
|
|
518
|
+
from: {
|
|
519
|
+
component_id: `FILE:${file}`,
|
|
520
|
+
location: {
|
|
521
|
+
file,
|
|
522
|
+
line: i + 1,
|
|
523
|
+
function: functionName,
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
to: {
|
|
527
|
+
component_id: serviceComponent.component_id,
|
|
528
|
+
},
|
|
529
|
+
connection_type: 'service-call',
|
|
530
|
+
code_reference: {
|
|
531
|
+
file,
|
|
532
|
+
symbol: functionName || `anonymous_${i + 1}`,
|
|
533
|
+
symbol_type: functionName ? 'function' : undefined,
|
|
534
|
+
line_start: i + 1,
|
|
535
|
+
code_snippet: line.trim().slice(0, 100),
|
|
536
|
+
},
|
|
537
|
+
description: `Calls ${pattern.serviceName}`,
|
|
538
|
+
detected_from: `Pattern: ${regex.source}`,
|
|
539
|
+
confidence,
|
|
540
|
+
timestamp,
|
|
541
|
+
last_verified: timestamp,
|
|
542
|
+
};
|
|
543
|
+
connections.push(connection);
|
|
544
|
+
break; // Only match once per line per pattern
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return { components, connections, warnings: [] };
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Extract function name from surrounding context
|
|
554
|
+
*/
|
|
555
|
+
function extractFunctionName(lines, lineIndex) {
|
|
556
|
+
// Look backwards for function definition
|
|
557
|
+
for (let i = lineIndex; i >= Math.max(0, lineIndex - 20); i--) {
|
|
558
|
+
const line = lines[i];
|
|
559
|
+
// JavaScript/TypeScript function patterns
|
|
560
|
+
const jsMatch = line.match(/(?:async\s+)?(?:function\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|[\(:])/);
|
|
561
|
+
if (jsMatch)
|
|
562
|
+
return jsMatch[1];
|
|
563
|
+
// Arrow function assignment
|
|
564
|
+
const arrowMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/);
|
|
565
|
+
if (arrowMatch)
|
|
566
|
+
return arrowMatch[1];
|
|
567
|
+
// Method definition
|
|
568
|
+
const methodMatch = line.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
|
|
569
|
+
if (methodMatch)
|
|
570
|
+
return methodMatch[1];
|
|
571
|
+
// Python function
|
|
572
|
+
const pyMatch = line.match(/(?:async\s+)?def\s+(\w+)\s*\(/);
|
|
573
|
+
if (pyMatch)
|
|
574
|
+
return pyMatch[1];
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Specifically scan for AI prompt locations
|
|
580
|
+
*/
|
|
581
|
+
export async function scanPromptLocations(projectRoot) {
|
|
582
|
+
const components = [];
|
|
583
|
+
const connections = [];
|
|
584
|
+
const timestamp = Date.now();
|
|
585
|
+
// Patterns that indicate a prompt definition
|
|
586
|
+
const promptPatterns = [
|
|
587
|
+
/messages:\s*\[\s*\{[^}]*role:\s*['"](?:system|user|assistant)['"]/s,
|
|
588
|
+
/prompt\s*[:=]\s*[`'"]/,
|
|
589
|
+
/system_prompt\s*[:=]\s*[`'"]/,
|
|
590
|
+
/SYSTEM_PROMPT\s*[:=]\s*[`'"]/,
|
|
591
|
+
/content:\s*[`'"][^`'"]{50,}/,
|
|
592
|
+
];
|
|
593
|
+
const sourceFiles = await glob('**/*.{ts,tsx,js,jsx,py}', {
|
|
594
|
+
cwd: projectRoot,
|
|
595
|
+
ignore: [
|
|
596
|
+
'node_modules/**',
|
|
597
|
+
'dist/**',
|
|
598
|
+
'build/**',
|
|
599
|
+
'.next/**',
|
|
600
|
+
'__pycache__/**',
|
|
601
|
+
'venv/**',
|
|
602
|
+
'**/node_modules/**',
|
|
603
|
+
'**/.git/**',
|
|
604
|
+
],
|
|
605
|
+
});
|
|
606
|
+
for (const file of sourceFiles) {
|
|
607
|
+
// Skip files that should be excluded (NavGator's own code, test files, etc.)
|
|
608
|
+
if (shouldExcludeFile(file, projectRoot)) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const filePath = path.join(projectRoot, file);
|
|
612
|
+
// Skip if not a file (could be a directory matching the glob pattern)
|
|
613
|
+
try {
|
|
614
|
+
const stat = await fs.promises.stat(filePath);
|
|
615
|
+
if (!stat.isFile())
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
continue; // Skip if we can't stat the file
|
|
620
|
+
}
|
|
621
|
+
let content;
|
|
622
|
+
try {
|
|
623
|
+
content = await fs.promises.readFile(filePath, 'utf-8');
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
continue; // Skip files we can't read
|
|
627
|
+
}
|
|
628
|
+
const lines = content.split('\n');
|
|
629
|
+
for (let i = 0; i < lines.length; i++) {
|
|
630
|
+
const line = lines[i];
|
|
631
|
+
const context = lines.slice(Math.max(0, i - 2), i + 3).join('\n');
|
|
632
|
+
// Skip example/mock code patterns
|
|
633
|
+
if (isExampleCode(line)) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
for (const pattern of promptPatterns) {
|
|
637
|
+
if (pattern.test(context)) {
|
|
638
|
+
const functionName = extractFunctionName(lines, i);
|
|
639
|
+
const promptName = extractPromptName(lines, i, file);
|
|
640
|
+
// Create prompt component
|
|
641
|
+
const component = {
|
|
642
|
+
component_id: generateComponentId('prompt', promptName),
|
|
643
|
+
name: promptName,
|
|
644
|
+
type: 'prompt',
|
|
645
|
+
role: {
|
|
646
|
+
purpose: 'AI prompt definition',
|
|
647
|
+
layer: 'backend',
|
|
648
|
+
critical: true,
|
|
649
|
+
},
|
|
650
|
+
source: {
|
|
651
|
+
detection_method: 'auto',
|
|
652
|
+
config_files: [file],
|
|
653
|
+
confidence: 0.8,
|
|
654
|
+
},
|
|
655
|
+
connects_to: [],
|
|
656
|
+
connected_from: [],
|
|
657
|
+
status: 'active',
|
|
658
|
+
tags: ['prompt', 'ai'],
|
|
659
|
+
timestamp,
|
|
660
|
+
last_updated: timestamp,
|
|
661
|
+
};
|
|
662
|
+
components.push(component);
|
|
663
|
+
// Create connection showing where prompt is defined
|
|
664
|
+
const connection = {
|
|
665
|
+
connection_id: generateConnectionId('prompt-location'),
|
|
666
|
+
from: {
|
|
667
|
+
component_id: component.component_id,
|
|
668
|
+
location: {
|
|
669
|
+
file,
|
|
670
|
+
line: i + 1,
|
|
671
|
+
function: functionName,
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
to: {
|
|
675
|
+
component_id: component.component_id,
|
|
676
|
+
},
|
|
677
|
+
connection_type: 'prompt-location',
|
|
678
|
+
code_reference: {
|
|
679
|
+
file,
|
|
680
|
+
symbol: promptName,
|
|
681
|
+
symbol_type: 'variable',
|
|
682
|
+
line_start: i + 1,
|
|
683
|
+
code_snippet: line.trim().slice(0, 100),
|
|
684
|
+
},
|
|
685
|
+
description: `Prompt defined: ${promptName}`,
|
|
686
|
+
detected_from: 'Prompt pattern detection',
|
|
687
|
+
confidence: 0.75,
|
|
688
|
+
timestamp,
|
|
689
|
+
last_verified: timestamp,
|
|
690
|
+
};
|
|
691
|
+
connections.push(connection);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return { components, connections, warnings: [] };
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Extract a meaningful prompt name from context
|
|
701
|
+
*/
|
|
702
|
+
function extractPromptName(lines, lineIndex, file) {
|
|
703
|
+
// Look for variable assignment
|
|
704
|
+
for (let i = lineIndex; i >= Math.max(0, lineIndex - 5); i--) {
|
|
705
|
+
const line = lines[i];
|
|
706
|
+
// Variable names like SYSTEM_PROMPT, summarizePrompt, etc.
|
|
707
|
+
const varMatch = line.match(/(?:const|let|var|PROMPT|prompt)\s*[:=]\s*(\w*[Pp]rompt\w*)/i);
|
|
708
|
+
if (varMatch)
|
|
709
|
+
return varMatch[1];
|
|
710
|
+
// Function names
|
|
711
|
+
const funcMatch = line.match(/(?:function|def|async)\s+(\w+)/);
|
|
712
|
+
if (funcMatch)
|
|
713
|
+
return `${funcMatch[1]}_prompt`;
|
|
714
|
+
}
|
|
715
|
+
// Fallback to file-based name
|
|
716
|
+
const baseName = path.basename(file, path.extname(file));
|
|
717
|
+
return `${baseName}_prompt_L${lineIndex + 1}`;
|
|
718
|
+
}
|
|
719
|
+
//# sourceMappingURL=service-calls.js.map
|