@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.
Files changed (84) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +486 -0
  4. package/agents/architecture-advisor.md +109 -0
  5. package/commands/nav-check.md +64 -0
  6. package/commands/nav-connections.md +58 -0
  7. package/commands/nav-diagram.md +106 -0
  8. package/commands/nav-export.md +71 -0
  9. package/commands/nav-impact.md +58 -0
  10. package/commands/nav-scan.md +46 -0
  11. package/commands/nav-status.md +44 -0
  12. package/dist/cli/index.d.ts +7 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +627 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/config.d.ts +95 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +262 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/diagram.d.ts +36 -0
  21. package/dist/diagram.d.ts.map +1 -0
  22. package/dist/diagram.js +333 -0
  23. package/dist/diagram.js.map +1 -0
  24. package/dist/index.d.ts +16 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +18 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/scanner.d.ts +57 -0
  29. package/dist/scanner.d.ts.map +1 -0
  30. package/dist/scanner.js +282 -0
  31. package/dist/scanner.js.map +1 -0
  32. package/dist/scanners/connections/ast-scanner.d.ts +26 -0
  33. package/dist/scanners/connections/ast-scanner.d.ts.map +1 -0
  34. package/dist/scanners/connections/ast-scanner.js +430 -0
  35. package/dist/scanners/connections/ast-scanner.js.map +1 -0
  36. package/dist/scanners/connections/service-calls.d.ts +14 -0
  37. package/dist/scanners/connections/service-calls.d.ts.map +1 -0
  38. package/dist/scanners/connections/service-calls.js +719 -0
  39. package/dist/scanners/connections/service-calls.js.map +1 -0
  40. package/dist/scanners/infrastructure/index.d.ts +27 -0
  41. package/dist/scanners/infrastructure/index.d.ts.map +1 -0
  42. package/dist/scanners/infrastructure/index.js +233 -0
  43. package/dist/scanners/infrastructure/index.js.map +1 -0
  44. package/dist/scanners/packages/npm.d.ts +18 -0
  45. package/dist/scanners/packages/npm.d.ts.map +1 -0
  46. package/dist/scanners/packages/npm.js +256 -0
  47. package/dist/scanners/packages/npm.js.map +1 -0
  48. package/dist/scanners/packages/pip.d.ts +14 -0
  49. package/dist/scanners/packages/pip.d.ts.map +1 -0
  50. package/dist/scanners/packages/pip.js +228 -0
  51. package/dist/scanners/packages/pip.js.map +1 -0
  52. package/dist/scanners/prompts/detector.d.ts +119 -0
  53. package/dist/scanners/prompts/detector.d.ts.map +1 -0
  54. package/dist/scanners/prompts/detector.js +617 -0
  55. package/dist/scanners/prompts/detector.js.map +1 -0
  56. package/dist/scanners/prompts/index.d.ts +51 -0
  57. package/dist/scanners/prompts/index.d.ts.map +1 -0
  58. package/dist/scanners/prompts/index.js +340 -0
  59. package/dist/scanners/prompts/index.js.map +1 -0
  60. package/dist/scanners/prompts/types.d.ts +127 -0
  61. package/dist/scanners/prompts/types.d.ts.map +1 -0
  62. package/dist/scanners/prompts/types.js +37 -0
  63. package/dist/scanners/prompts/types.js.map +1 -0
  64. package/dist/setup.d.ts +65 -0
  65. package/dist/setup.d.ts.map +1 -0
  66. package/dist/setup.js +261 -0
  67. package/dist/setup.js.map +1 -0
  68. package/dist/storage.d.ts +147 -0
  69. package/dist/storage.d.ts.map +1 -0
  70. package/dist/storage.js +931 -0
  71. package/dist/storage.js.map +1 -0
  72. package/dist/types.d.ts +296 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +55 -0
  75. package/dist/types.js.map +1 -0
  76. package/dist/ui-server.d.ts +17 -0
  77. package/dist/ui-server.d.ts.map +1 -0
  78. package/dist/ui-server.js +815 -0
  79. package/dist/ui-server.js.map +1 -0
  80. package/hooks/hooks.json +57 -0
  81. package/package.json +80 -0
  82. package/scripts/ibr-ui-test.mjs +359 -0
  83. package/scripts/postinstall.cjs +35 -0
  84. 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