code-graph-context 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.
@@ -0,0 +1,832 @@
1
+ /**
2
+ * FairSquare Custom Framework Schema
3
+ *
4
+ * Detects custom patterns:
5
+ * - @Injectable([deps]) with dependency array
6
+ * - Controller with HTTP method conventions (get, post, put, delete)
7
+ * - Repository pattern (extends Repository)
8
+ * - Custom permission managers
9
+ */
10
+ import { Node } from 'ts-morph';
11
+ import { CoreNodeType } from './schema.js';
12
+ // ============================================================================
13
+ // FAIRSQUARE SEMANTIC TYPES
14
+ // ============================================================================
15
+ export var FairSquareSemanticNodeType;
16
+ (function (FairSquareSemanticNodeType) {
17
+ // Core FairSquare Types
18
+ FairSquareSemanticNodeType["FS_CONTROLLER"] = "Controller";
19
+ FairSquareSemanticNodeType["FS_SERVICE"] = "Service";
20
+ FairSquareSemanticNodeType["FS_REPOSITORY"] = "Repository";
21
+ FairSquareSemanticNodeType["FS_DAL"] = "DAL";
22
+ FairSquareSemanticNodeType["FS_PERMISSION_MANAGER"] = "PermissionManager";
23
+ FairSquareSemanticNodeType["FS_VENDOR_CLIENT"] = "VendorClient";
24
+ // HTTP & Routing
25
+ FairSquareSemanticNodeType["FS_ROUTE_DEFINITION"] = "RouteDefinition";
26
+ })(FairSquareSemanticNodeType || (FairSquareSemanticNodeType = {}));
27
+ export var FairSquareSemanticEdgeType;
28
+ (function (FairSquareSemanticEdgeType) {
29
+ // Dependency Injection
30
+ FairSquareSemanticEdgeType["FS_INJECTS"] = "INJECTS";
31
+ // Repository Pattern
32
+ FairSquareSemanticEdgeType["FS_REPOSITORY_USES_DAL"] = "USES_DAL";
33
+ // HTTP Routing
34
+ FairSquareSemanticEdgeType["FS_ROUTES_TO"] = "ROUTES_TO";
35
+ FairSquareSemanticEdgeType["FS_ROUTES_TO_HANDLER"] = "ROUTES_TO_HANDLER";
36
+ // Permissions
37
+ FairSquareSemanticEdgeType["FS_PROTECTED_BY"] = "PROTECTED_BY";
38
+ FairSquareSemanticEdgeType["FS_INTERNAL_API_CALL"] = "INTERNAL_API_CALL";
39
+ })(FairSquareSemanticEdgeType || (FairSquareSemanticEdgeType = {}));
40
+ // Common labels used across FairSquare schema
41
+ export var FairSquareLabel;
42
+ (function (FairSquareLabel) {
43
+ FairSquareLabel["FAIRSQUARE"] = "FairSquare";
44
+ FairSquareLabel["BUSINESS_LOGIC"] = "BusinessLogic";
45
+ FairSquareLabel["DATA_ACCESS"] = "DataAccess";
46
+ FairSquareLabel["DATABASE"] = "Database";
47
+ FairSquareLabel["SECURITY"] = "Security";
48
+ FairSquareLabel["EXTERNAL_INTEGRATION"] = "ExternalIntegration";
49
+ FairSquareLabel["HTTP_ENDPOINT"] = "HttpEndpoint";
50
+ })(FairSquareLabel || (FairSquareLabel = {}));
51
+ // ============================================================================
52
+ // CONTEXT EXTRACTORS
53
+ // ============================================================================
54
+ /**
55
+ * Extract Injectable decorator dependencies
56
+ * @Injectable([Dep1, Dep2]) → ['Dep1', 'Dep2']
57
+ */
58
+ const extractInjectableDependencies = (parsedNode, _allNodes, _sharedContext) => {
59
+ const node = parsedNode.sourceNode;
60
+ if (!node || !Node.isClassDeclaration(node))
61
+ return {};
62
+ const decorators = node.getDecorators();
63
+ for (const decorator of decorators) {
64
+ if (decorator.getName() === 'Injectable') {
65
+ const args = decorator.getArguments();
66
+ if (args.length > 0 && Node.isArrayLiteralExpression(args[0])) {
67
+ const elements = args[0].getElements();
68
+ const dependencies = elements.map((el) => el.getText());
69
+ return {
70
+ dependencies,
71
+ dependencyCount: dependencies.length,
72
+ hasInjection: dependencies.length > 0,
73
+ };
74
+ }
75
+ }
76
+ }
77
+ return {};
78
+ };
79
+ /**
80
+ * Extract repository DAL dependencies
81
+ * Uses the dependencies array from @Injectable([...]) decorator
82
+ */
83
+ const extractRepositoryDals = (parsedNode, _allNodes, _sharedContext) => {
84
+ const dependencies = parsedNode.properties.context?.dependencies ?? [];
85
+ // Filter for DAL-related dependencies
86
+ const dalDeps = dependencies.filter((dep) => dep.toLowerCase().includes('dal') || dep.toLowerCase().endsWith('dal'));
87
+ return {
88
+ dals: dalDeps,
89
+ dalCount: dalDeps.length,
90
+ usesDALPattern: dalDeps.length > 0,
91
+ };
92
+ };
93
+ /**
94
+ * Extract permission manager usage
95
+ * Uses the dependencies array from @Injectable([...]) decorator
96
+ */
97
+ const extractPermissionManager = (parsedNode, _allNodes, _sharedContext) => {
98
+ const dependencies = parsedNode.properties.context?.dependencies ?? [];
99
+ // Find permission manager dependency
100
+ const permissionDep = dependencies.find((dep) => dep.toLowerCase().includes('permission'));
101
+ if (permissionDep) {
102
+ return {
103
+ permissionManager: permissionDep,
104
+ hasPermissionManager: true,
105
+ };
106
+ }
107
+ return {};
108
+ };
109
+ /**
110
+ * Extract monorepo project name from file path
111
+ * Supports patterns like: packages/project-name/..., apps/project-name/...
112
+ */
113
+ const extractMonorepoProject = (parsedNode, _allNodes, _sharedContext) => {
114
+ const node = parsedNode.sourceNode;
115
+ if (!node || !Node.isClassDeclaration(node))
116
+ return {};
117
+ const filePath = node.getSourceFile().getFilePath();
118
+ // Match common monorepo patterns
119
+ const monorepoPatterns = [
120
+ /\/packages\/([^/]+)\//,
121
+ /\/apps\/([^/]+)\//,
122
+ /\/libs\/([^/]+)\//,
123
+ /\/components\/([^/]+)\//,
124
+ ];
125
+ for (const pattern of monorepoPatterns) {
126
+ const match = filePath.match(pattern);
127
+ if (match?.[1]) {
128
+ return {
129
+ monorepoProject: match[1],
130
+ isMonorepoPackage: true,
131
+ };
132
+ }
133
+ }
134
+ return {};
135
+ };
136
+ /**
137
+ * Extract route definitions from .routes.ts files
138
+ * Parses ModuleRoute[] arrays to get explicit route mappings
139
+ */
140
+ const extractRouteDefinitions = (parsedNode, _allNodes, _sharedContext) => {
141
+ const node = parsedNode.sourceNode;
142
+ if (!node || !Node.isVariableDeclaration(node))
143
+ return {};
144
+ // Get the initializer (the array)
145
+ const initializer = node.getInitializer();
146
+ if (!initializer || !Node.isArrayLiteralExpression(initializer)) {
147
+ return {};
148
+ }
149
+ const routes = [];
150
+ // Loop through each object in the array
151
+ for (const element of initializer.getElements()) {
152
+ if (!Node.isObjectLiteralExpression(element))
153
+ continue;
154
+ const routeData = {};
155
+ // Extract each property from the route object
156
+ for (const prop of element.getProperties()) {
157
+ if (!Node.isPropertyAssignment(prop))
158
+ continue;
159
+ const propName = prop.getName();
160
+ const propValue = prop.getInitializer();
161
+ if (!propValue)
162
+ continue;
163
+ // Extract based on property type
164
+ switch (propName) {
165
+ case 'method':
166
+ case 'path':
167
+ case 'handler':
168
+ // String values
169
+ routeData[propName] = propValue.getText().replace(/['"]/g, '');
170
+ break;
171
+ case 'authenticated':
172
+ // Boolean value
173
+ routeData[propName] = propValue.getText() === 'true';
174
+ break;
175
+ case 'controller':
176
+ // Identifier (class reference)
177
+ routeData.controllerName = propValue.getText();
178
+ break;
179
+ }
180
+ }
181
+ // Only add if we got meaningful data
182
+ if (routeData.method && routeData.path) {
183
+ routes.push(routeData);
184
+ }
185
+ }
186
+ return {
187
+ routes,
188
+ routeCount: routes.length,
189
+ isRouteFile: true,
190
+ fileName: node.getSourceFile().getBaseName(),
191
+ };
192
+ };
193
+ // ============================================================================
194
+ // FRAMEWORK ENHANCEMENTS
195
+ // ============================================================================
196
+ export const FAIRSQUARE_FRAMEWORK_SCHEMA = {
197
+ name: 'FairSquare Custom Framework',
198
+ version: '1.0.0',
199
+ description: 'Custom FairSquare dependency injection and repository patterns',
200
+ enhances: [CoreNodeType.CLASS_DECLARATION, CoreNodeType.METHOD_DECLARATION],
201
+ metadata: {
202
+ targetLanguages: ['typescript'],
203
+ dependencies: ['@fairsquare/core', '@fairsquare/server'],
204
+ parseVariablesFrom: ['**/*.routes.ts', '**/*.route.ts'],
205
+ },
206
+ // ============================================================================
207
+ // GLOBAL CONTEXT EXTRACTORS (run on all nodes)
208
+ // ============================================================================
209
+ contextExtractors: [
210
+ {
211
+ nodeType: CoreNodeType.CLASS_DECLARATION,
212
+ extractor: extractInjectableDependencies,
213
+ priority: 10,
214
+ },
215
+ ],
216
+ // ============================================================================
217
+ // NODE ENHANCEMENTS
218
+ // ============================================================================
219
+ enhancements: {
220
+ // FairSquare Controller
221
+ fairsquareController: {
222
+ name: 'FairSquare Controller',
223
+ targetCoreType: CoreNodeType.CLASS_DECLARATION,
224
+ semanticType: FairSquareSemanticNodeType.FS_CONTROLLER,
225
+ priority: 100,
226
+ detectionPatterns: [
227
+ {
228
+ type: 'classname',
229
+ pattern: /Controller$/,
230
+ confidence: 0.7,
231
+ priority: 5,
232
+ },
233
+ {
234
+ type: 'function',
235
+ pattern: (parsedNode) => {
236
+ const node = parsedNode.sourceNode;
237
+ if (!node || !Node.isClassDeclaration(node))
238
+ return false;
239
+ const baseClass = node.getExtends();
240
+ const result = baseClass?.getText() === 'Controller';
241
+ return result;
242
+ },
243
+ confidence: 1.0,
244
+ priority: 10,
245
+ },
246
+ ],
247
+ contextExtractors: [
248
+ {
249
+ nodeType: CoreNodeType.CLASS_DECLARATION,
250
+ extractor: extractInjectableDependencies,
251
+ priority: 10,
252
+ },
253
+ {
254
+ nodeType: CoreNodeType.CLASS_DECLARATION,
255
+ extractor: extractPermissionManager,
256
+ priority: 8,
257
+ },
258
+ {
259
+ nodeType: CoreNodeType.CLASS_DECLARATION,
260
+ extractor: (parsedNode, allNodes, sharedContext) => {
261
+ // Store vendor controllers in shared context for later lookup
262
+ const controllerName = parsedNode.properties.name;
263
+ // Check if this is a vendor controller
264
+ if (controllerName.includes('Vendor') || parsedNode.properties.filePath.includes('modules/vendor')) {
265
+ // Extract vendor name: ExperianVendorController → experian
266
+ let vendorName = '';
267
+ if (controllerName.endsWith('VendorController')) {
268
+ vendorName = controllerName.replace('VendorController', '').toLowerCase();
269
+ }
270
+ else if (controllerName.endsWith('Controller')) {
271
+ vendorName = controllerName.replace('Controller', '').toLowerCase();
272
+ }
273
+ if (vendorName) {
274
+ // Initialize map if not exists
275
+ if (!sharedContext?.has('vendorControllers')) {
276
+ sharedContext?.set('vendorControllers', new Map());
277
+ }
278
+ const vendorControllerMap = sharedContext?.get('vendorControllers');
279
+ vendorControllerMap.set(vendorName, parsedNode);
280
+ }
281
+ }
282
+ return {};
283
+ },
284
+ priority: 5,
285
+ },
286
+ ],
287
+ additionalRelationships: [
288
+ FairSquareSemanticEdgeType.FS_INJECTS,
289
+ FairSquareSemanticEdgeType.FS_PROTECTED_BY,
290
+ ],
291
+ neo4j: {
292
+ additionalLabels: [FairSquareLabel.FAIRSQUARE, FairSquareLabel.BUSINESS_LOGIC],
293
+ primaryLabel: FairSquareSemanticNodeType.FS_CONTROLLER,
294
+ },
295
+ },
296
+ // FairSquare Service
297
+ fairsquareService: {
298
+ name: 'FairSquare Service',
299
+ targetCoreType: CoreNodeType.CLASS_DECLARATION,
300
+ semanticType: FairSquareSemanticNodeType.FS_SERVICE,
301
+ priority: 90,
302
+ detectionPatterns: [
303
+ {
304
+ type: 'classname',
305
+ pattern: /Service$/,
306
+ confidence: 0.8,
307
+ priority: 5,
308
+ },
309
+ {
310
+ type: 'decorator',
311
+ pattern: 'Injectable',
312
+ confidence: 0.9,
313
+ priority: 8,
314
+ },
315
+ {
316
+ type: 'function',
317
+ pattern: (parsedNode) => {
318
+ const node = parsedNode.sourceNode;
319
+ if (!node || !Node.isClassDeclaration(node))
320
+ return false;
321
+ const name = node.getName() ?? '';
322
+ const hasInjectable = node.getDecorators().some((d) => d.getName() === 'Injectable');
323
+ return name.endsWith('Service') && hasInjectable;
324
+ },
325
+ confidence: 1.0,
326
+ priority: 10,
327
+ },
328
+ ],
329
+ contextExtractors: [
330
+ {
331
+ nodeType: CoreNodeType.CLASS_DECLARATION,
332
+ extractor: extractInjectableDependencies,
333
+ priority: 10,
334
+ },
335
+ ],
336
+ additionalRelationships: [FairSquareSemanticEdgeType.FS_INJECTS],
337
+ neo4j: {
338
+ additionalLabels: [FairSquareLabel.FAIRSQUARE, FairSquareLabel.BUSINESS_LOGIC],
339
+ primaryLabel: FairSquareSemanticNodeType.FS_SERVICE,
340
+ },
341
+ },
342
+ // FairSquare Repository
343
+ fairsquareRepository: {
344
+ name: 'FairSquare Repository',
345
+ targetCoreType: CoreNodeType.CLASS_DECLARATION,
346
+ semanticType: FairSquareSemanticNodeType.FS_REPOSITORY,
347
+ priority: 95,
348
+ detectionPatterns: [
349
+ {
350
+ type: 'classname',
351
+ pattern: /Repository$/,
352
+ confidence: 0.7,
353
+ priority: 5,
354
+ },
355
+ {
356
+ type: 'function',
357
+ pattern: (parsedNode) => {
358
+ const node = parsedNode.sourceNode;
359
+ if (!node || !Node.isClassDeclaration(node))
360
+ return false;
361
+ const baseClass = node.getExtends();
362
+ return baseClass?.getText() === 'Repository';
363
+ },
364
+ confidence: 1.0,
365
+ priority: 10,
366
+ },
367
+ ],
368
+ contextExtractors: [
369
+ {
370
+ nodeType: CoreNodeType.CLASS_DECLARATION,
371
+ extractor: extractInjectableDependencies,
372
+ priority: 10,
373
+ },
374
+ {
375
+ nodeType: CoreNodeType.CLASS_DECLARATION,
376
+ extractor: extractRepositoryDals,
377
+ priority: 9,
378
+ },
379
+ ],
380
+ additionalRelationships: [FairSquareSemanticEdgeType.FS_REPOSITORY_USES_DAL],
381
+ neo4j: {
382
+ additionalLabels: [FairSquareLabel.FAIRSQUARE, FairSquareLabel.DATA_ACCESS],
383
+ primaryLabel: FairSquareSemanticNodeType.FS_REPOSITORY,
384
+ },
385
+ },
386
+ // FairSquare DAL (Data Access Layer)
387
+ fairsquareDAL: {
388
+ name: 'FairSquare DAL',
389
+ targetCoreType: CoreNodeType.CLASS_DECLARATION,
390
+ semanticType: FairSquareSemanticNodeType.FS_DAL,
391
+ priority: 85,
392
+ detectionPatterns: [
393
+ {
394
+ type: 'classname',
395
+ pattern: /DAL$/,
396
+ confidence: 1.0,
397
+ priority: 10,
398
+ },
399
+ ],
400
+ contextExtractors: [],
401
+ additionalRelationships: [],
402
+ neo4j: {
403
+ additionalLabels: [FairSquareLabel.FAIRSQUARE, FairSquareLabel.DATA_ACCESS, FairSquareLabel.DATABASE],
404
+ primaryLabel: FairSquareSemanticNodeType.FS_DAL,
405
+ },
406
+ },
407
+ // FairSquare Permission Manager
408
+ fairsquarePermissionManager: {
409
+ name: 'FairSquare Permission Manager',
410
+ targetCoreType: CoreNodeType.CLASS_DECLARATION,
411
+ semanticType: FairSquareSemanticNodeType.FS_PERMISSION_MANAGER,
412
+ priority: 80,
413
+ detectionPatterns: [
414
+ {
415
+ type: 'classname',
416
+ pattern: /PermissionManager$/,
417
+ confidence: 1.0,
418
+ priority: 10,
419
+ },
420
+ ],
421
+ contextExtractors: [],
422
+ additionalRelationships: [],
423
+ neo4j: {
424
+ additionalLabels: [FairSquareLabel.FAIRSQUARE, FairSquareLabel.SECURITY],
425
+ primaryLabel: FairSquareSemanticNodeType.FS_PERMISSION_MANAGER,
426
+ },
427
+ },
428
+ // FairSquare Vendor Client
429
+ fairsquareVendorClient: {
430
+ name: 'FairSquare Vendor Client',
431
+ targetCoreType: CoreNodeType.CLASS_DECLARATION,
432
+ semanticType: FairSquareSemanticNodeType.FS_VENDOR_CLIENT,
433
+ priority: 75,
434
+ detectionPatterns: [
435
+ {
436
+ type: 'classname',
437
+ pattern: /Client$/,
438
+ confidence: 0.6,
439
+ priority: 3,
440
+ },
441
+ {
442
+ type: 'filename',
443
+ pattern: /vendor-client/,
444
+ confidence: 0.9,
445
+ priority: 8,
446
+ },
447
+ {
448
+ type: 'function',
449
+ pattern: (parsedNode) => {
450
+ const node = parsedNode.sourceNode;
451
+ if (!node || !Node.isClassDeclaration(node))
452
+ return false;
453
+ const filePath = node.getSourceFile().getFilePath();
454
+ return filePath.includes('vendor-client') || filePath.includes('component-vendor');
455
+ },
456
+ confidence: 1.0,
457
+ priority: 10,
458
+ },
459
+ ],
460
+ contextExtractors: [
461
+ {
462
+ nodeType: CoreNodeType.CLASS_DECLARATION,
463
+ extractor: extractMonorepoProject,
464
+ priority: 10,
465
+ },
466
+ ],
467
+ additionalRelationships: [],
468
+ neo4j: {
469
+ additionalLabels: [FairSquareLabel.FAIRSQUARE, FairSquareLabel.EXTERNAL_INTEGRATION],
470
+ primaryLabel: FairSquareSemanticNodeType.FS_VENDOR_CLIENT,
471
+ },
472
+ },
473
+ // FairSquare Route Definition
474
+ fairsquareRouteDefinition: {
475
+ name: 'FairSquare Route Definition',
476
+ targetCoreType: CoreNodeType.VARIABLE_DECLARATION,
477
+ semanticType: FairSquareSemanticNodeType.FS_ROUTE_DEFINITION,
478
+ priority: 110,
479
+ detectionPatterns: [
480
+ {
481
+ type: 'function',
482
+ pattern: (parsedNode) => {
483
+ const node = parsedNode.sourceNode;
484
+ if (!node || !Node.isVariableDeclaration(node))
485
+ return false;
486
+ const name = node.getName();
487
+ const typeNode = node.getTypeNode();
488
+ // Check if variable name ends with "Routes" AND has type ModuleRoute[]
489
+ return !!name.endsWith('Routes') && !!typeNode?.getText().includes('ModuleRoute');
490
+ },
491
+ confidence: 1.0,
492
+ priority: 10,
493
+ },
494
+ ],
495
+ contextExtractors: [
496
+ {
497
+ nodeType: CoreNodeType.VARIABLE_DECLARATION,
498
+ extractor: extractRouteDefinitions,
499
+ priority: 10,
500
+ },
501
+ ],
502
+ additionalRelationships: [
503
+ FairSquareSemanticEdgeType.FS_ROUTES_TO,
504
+ FairSquareSemanticEdgeType.FS_ROUTES_TO_HANDLER,
505
+ ],
506
+ neo4j: {
507
+ additionalLabels: [FairSquareLabel.FAIRSQUARE],
508
+ primaryLabel: FairSquareSemanticNodeType.FS_ROUTE_DEFINITION,
509
+ },
510
+ },
511
+ // HTTP Endpoint (Controller methods)
512
+ // fairsquareHttpEndpoint: {
513
+ // name: 'FairSquare HTTP Endpoint',
514
+ // targetCoreType: CoreNodeType.METHOD_DECLARATION,
515
+ // semanticType: FairSquareSemanticNodeType.FS_HTTP_ENDPOINT as any,
516
+ // priority: 100,
517
+ //
518
+ // detectionPatterns: [
519
+ // {
520
+ // type: 'function',
521
+ // pattern: (node: Node) => {
522
+ // if (!Node.isMethodDeclaration(node)) return false;
523
+ // const methodName = node.getName().toLowerCase();
524
+ // const httpMethods = ['get', 'post', 'put', 'delete', 'patch'];
525
+ //
526
+ // // Check if method is HTTP verb AND parent is Controller
527
+ // const parent = node.getParent();
528
+ // const isController =
529
+ // Node.isClassDeclaration(parent) &&
530
+ // (parent.getName()?.endsWith('Controller') || parent.getExtends()?.getText() === 'Controller');
531
+ //
532
+ // return httpMethods.includes(methodName) && isController;
533
+ // },
534
+ // confidence: 1.0,
535
+ // priority: 10,
536
+ // },
537
+ // ],
538
+ //
539
+ // contextExtractors: [
540
+ // {
541
+ // nodeType: CoreNodeType.METHOD_DECLARATION,
542
+ // extractor: extractHttpEndpoint,
543
+ // priority: 10,
544
+ // },
545
+ // ],
546
+ //
547
+ // additionalRelationships: [FairSquareSemanticEdgeType.FS_EXPOSES_HTTP as any],
548
+ //
549
+ // neo4j: {
550
+ // additionalLabels: ['FairSquare', 'HttpEndpoint', 'API'],
551
+ // primaryLabel: 'FairSquareHttpEndpoint',
552
+ // },
553
+ // },
554
+ },
555
+ // ============================================================================
556
+ // EDGE ENHANCEMENTS (Relationship detection)
557
+ // ============================================================================
558
+ edgeEnhancements: {
559
+ // @Injectable([Dep1, Dep2]) creates INJECTS edges
560
+ injectableDependencies: {
561
+ name: 'Injectable Dependencies',
562
+ semanticType: FairSquareSemanticEdgeType.FS_INJECTS,
563
+ relationshipWeight: 0.95, // Critical - FairSquare DI is core architecture
564
+ detectionPattern: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
565
+ // FILTER: Only create INJECTS edges between ClassDeclarations
566
+ if (parsedSourceNode.coreType !== CoreNodeType.CLASS_DECLARATION ||
567
+ parsedTargetNode.coreType !== CoreNodeType.CLASS_DECLARATION) {
568
+ return false;
569
+ }
570
+ // Source has @Injectable([Target])
571
+ const sourceContext = parsedSourceNode.properties.context;
572
+ const targetName = parsedTargetNode.properties.name;
573
+ if (!sourceContext?.dependencies)
574
+ return false;
575
+ // Use exact match to avoid false positives from substring matching
576
+ return sourceContext.dependencies.some((dep) => {
577
+ // Remove quotes and whitespace from dependency string
578
+ const cleanDep = dep.replace(/['"]/g, '').trim();
579
+ return cleanDep === targetName;
580
+ });
581
+ },
582
+ contextExtractor: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => ({
583
+ injectionType: 'constructor',
584
+ framework: 'fairsquare',
585
+ targetDependency: parsedTargetNode.properties.name,
586
+ }),
587
+ neo4j: {
588
+ relationshipType: 'INJECTS',
589
+ direction: 'OUTGOING',
590
+ },
591
+ },
592
+ // Repository uses DAL
593
+ repositoryUsesDAL: {
594
+ name: 'Repository Uses DAL',
595
+ semanticType: FairSquareSemanticEdgeType.FS_REPOSITORY_USES_DAL,
596
+ relationshipWeight: 0.85, // High - data access layer relationships
597
+ detectionPattern: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
598
+ const isSourceRepo = parsedSourceNode.semanticType === FairSquareSemanticNodeType.FS_REPOSITORY;
599
+ const isTargetDAL = parsedTargetNode.semanticType === FairSquareSemanticNodeType.FS_DAL;
600
+ if (!isSourceRepo || !isTargetDAL)
601
+ return false;
602
+ // Check if Repository injects this DAL
603
+ const sourceDals = parsedSourceNode.properties.context?.dals ?? [];
604
+ const targetName = parsedTargetNode.properties.name;
605
+ // Use exact match to avoid false positives
606
+ return sourceDals.some((dal) => {
607
+ const cleanDal = dal.replace(/['"]/g, '').trim();
608
+ return cleanDal === targetName;
609
+ });
610
+ },
611
+ contextExtractor: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => ({
612
+ dalName: parsedTargetNode.properties.name,
613
+ repositoryName: parsedSourceNode.properties.name,
614
+ }),
615
+ neo4j: {
616
+ relationshipType: 'USES_DAL',
617
+ direction: 'OUTGOING',
618
+ },
619
+ },
620
+ // Controller uses PermissionManager
621
+ controllerProtectedBy: {
622
+ name: 'Controller Protected By Permission Manager',
623
+ semanticType: FairSquareSemanticEdgeType.FS_PROTECTED_BY,
624
+ relationshipWeight: 0.88, // High - security/authorization is critical
625
+ detectionPattern: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
626
+ const isSourceController = parsedSourceNode.semanticType === FairSquareSemanticNodeType.FS_CONTROLLER;
627
+ const isTargetPermissionManager = parsedTargetNode.semanticType === FairSquareSemanticNodeType.FS_PERMISSION_MANAGER;
628
+ if (!isSourceController || !isTargetPermissionManager)
629
+ return false;
630
+ const sourcePermManager = parsedSourceNode.properties.context?.permissionManager;
631
+ const targetName = parsedTargetNode.properties.name;
632
+ if (!sourcePermManager)
633
+ return false;
634
+ // Use exact match to avoid false positives
635
+ const cleanPermManager = sourcePermManager.replace(/['"]/g, '').trim();
636
+ return cleanPermManager === targetName;
637
+ },
638
+ contextExtractor: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => ({
639
+ permissionManagerName: parsedTargetNode.properties.name,
640
+ controllerName: parsedSourceNode.properties.name,
641
+ }),
642
+ neo4j: {
643
+ relationshipType: 'PROTECTED_BY',
644
+ direction: 'OUTGOING',
645
+ },
646
+ },
647
+ // Route definition routes to Controller
648
+ routeToController: {
649
+ name: 'Route To Controller',
650
+ semanticType: FairSquareSemanticEdgeType.FS_ROUTES_TO,
651
+ relationshipWeight: 0.92, // Critical - HTTP routing is primary entry point
652
+ detectionPattern: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
653
+ const isSourceRoute = parsedSourceNode.semanticType === FairSquareSemanticNodeType.FS_ROUTE_DEFINITION;
654
+ const isTargetController = parsedTargetNode.semanticType === FairSquareSemanticNodeType.FS_CONTROLLER;
655
+ if (!isSourceRoute || !isTargetController)
656
+ return false;
657
+ // Check if any route in the definition references this controller
658
+ const routes = parsedSourceNode.properties.context?.routes ?? [];
659
+ const targetName = parsedTargetNode.properties.name;
660
+ return routes.some((route) => route.controllerName === targetName);
661
+ },
662
+ contextExtractor: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
663
+ const routes = parsedSourceNode.properties.context?.routes ?? [];
664
+ const targetName = parsedTargetNode.properties.name;
665
+ const relevantRoutes = routes.filter((r) => r.controllerName === targetName);
666
+ return {
667
+ routeCount: relevantRoutes.length,
668
+ routes: relevantRoutes,
669
+ methods: relevantRoutes.map((r) => r.method),
670
+ paths: relevantRoutes.map((r) => r.path),
671
+ routeFile: parsedSourceNode.properties.context?.fileName,
672
+ };
673
+ },
674
+ neo4j: {
675
+ relationshipType: 'ROUTES_TO',
676
+ direction: 'OUTGOING',
677
+ },
678
+ },
679
+ // Route definition routes to Handler method
680
+ routeToHandlerMethod: {
681
+ name: 'Route To Handler Method',
682
+ semanticType: FairSquareSemanticEdgeType.FS_ROUTES_TO_HANDLER,
683
+ relationshipWeight: 0.9, // Critical - direct route to handler method
684
+ detectionPattern: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
685
+ const isSourceRoute = parsedSourceNode.semanticType === FairSquareSemanticNodeType.FS_ROUTE_DEFINITION;
686
+ const isTargetMethod = parsedTargetNode.coreType === CoreNodeType.METHOD_DECLARATION;
687
+ if (!isSourceRoute || !isTargetMethod)
688
+ return false;
689
+ // Check if any route in the definition references this method as handler
690
+ const routes = parsedSourceNode.properties.context?.routes ?? [];
691
+ const targetMethodName = parsedTargetNode.properties.name;
692
+ // Find routes that match this method name
693
+ const matchingRoutes = routes.filter((route) => route.handler === targetMethodName);
694
+ if (matchingRoutes.length === 0)
695
+ return false;
696
+ // CRITICAL FIX: Verify the method belongs to the correct controller
697
+ // Find the parent class of this method by checking the AST node
698
+ const targetNode = parsedTargetNode.sourceNode;
699
+ if (!targetNode || !Node.isMethodDeclaration(targetNode))
700
+ return false;
701
+ const parentClass = targetNode.getParent();
702
+ if (!parentClass || !Node.isClassDeclaration(parentClass))
703
+ return false;
704
+ const parentClassName = parentClass.getName();
705
+ if (!parentClassName)
706
+ return false;
707
+ // Check if any matching route's controller name matches the parent class
708
+ const isHandler = matchingRoutes.some((route) => route.controllerName === parentClassName);
709
+ // If this method is a route handler AND is public, add HttpEndpoint label to the target node
710
+ if (isHandler) {
711
+ // Only add HttpEndpoint label to public methods (not private/protected)
712
+ const isPublicMethod = parsedTargetNode.properties.visibility === 'public';
713
+ if (isPublicMethod &&
714
+ !parsedTargetNode.labels.includes(FairSquareLabel.HTTP_ENDPOINT) &&
715
+ parsedTargetNode.properties) {
716
+ parsedTargetNode.labels.push(FairSquareLabel.HTTP_ENDPOINT);
717
+ }
718
+ }
719
+ return isHandler;
720
+ },
721
+ contextExtractor: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
722
+ const routes = parsedSourceNode.properties.context?.routes ?? [];
723
+ const targetMethodName = parsedTargetNode.properties.name;
724
+ const matchingRoute = routes.find((r) => r.handler === targetMethodName);
725
+ return {
726
+ method: matchingRoute?.method,
727
+ path: matchingRoute?.path,
728
+ authenticated: matchingRoute?.authenticated,
729
+ handler: targetMethodName,
730
+ controllerName: matchingRoute?.controllerName,
731
+ routeFile: parsedSourceNode.properties.context?.fileName,
732
+ };
733
+ },
734
+ neo4j: {
735
+ relationshipType: 'ROUTES_TO_HANDLER',
736
+ direction: 'OUTGOING',
737
+ },
738
+ },
739
+ internalApiCall: {
740
+ name: 'Internal API Call',
741
+ semanticType: FairSquareSemanticEdgeType.FS_INTERNAL_API_CALL,
742
+ relationshipWeight: 0.82, // High - internal service communication
743
+ detectionPattern: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
744
+ // Service → VendorController (through VendorClient)
745
+ const isSourceService = parsedSourceNode.semanticType === FairSquareSemanticNodeType.FS_SERVICE;
746
+ const isTargetController = parsedTargetNode.semanticType === FairSquareSemanticNodeType.FS_CONTROLLER;
747
+ if (!isSourceService || !isTargetController)
748
+ return false;
749
+ // Get vendor controller map
750
+ const vendorControllerMap = sharedContext?.get('vendorControllers');
751
+ if (!vendorControllerMap)
752
+ return false;
753
+ // Check if target is a vendor controller
754
+ let vendorName = '';
755
+ for (const [name, controllerNode] of vendorControllerMap) {
756
+ if (controllerNode.id === parsedTargetNode.id) {
757
+ vendorName = name;
758
+ break;
759
+ }
760
+ }
761
+ if (!vendorName)
762
+ return false;
763
+ // Check if service uses the corresponding VendorClient
764
+ const sourceNode = parsedSourceNode.sourceNode;
765
+ if (!sourceNode || !Node.isClassDeclaration(sourceNode))
766
+ return false;
767
+ const expectedClientName = `${vendorName.charAt(0).toUpperCase() + vendorName.slice(1)}Client`;
768
+ const properties = sourceNode.getProperties();
769
+ for (const prop of properties) {
770
+ const typeNode = prop.getTypeNode();
771
+ if (typeNode?.getText() === expectedClientName) {
772
+ return true;
773
+ }
774
+ const initializer = prop.getInitializer();
775
+ if (initializer && Node.isNewExpression(initializer)) {
776
+ if (initializer.getExpression().getText() === expectedClientName) {
777
+ return true;
778
+ }
779
+ }
780
+ }
781
+ return false;
782
+ },
783
+ contextExtractor: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
784
+ const vendorControllerMap = sharedContext?.get('vendorControllers');
785
+ let vendorName = '';
786
+ for (const [name, controllerNode] of vendorControllerMap) {
787
+ if (controllerNode.id === parsedTargetNode.id) {
788
+ vendorName = name;
789
+ break;
790
+ }
791
+ }
792
+ return {
793
+ serviceName: parsedSourceNode.properties.name,
794
+ vendorController: parsedTargetNode.properties.name,
795
+ vendorClient: `${vendorName.charAt(0).toUpperCase() + vendorName.slice(1)}Client`,
796
+ };
797
+ },
798
+ neo4j: {
799
+ relationshipType: 'INTERNAL_API_CALL',
800
+ direction: 'OUTGOING',
801
+ },
802
+ },
803
+ usesRepository: {
804
+ name: 'Uses Repository',
805
+ semanticType: 'USES_REPOSITORY',
806
+ relationshipWeight: 0.8, // High - service to repository data access
807
+ detectionPattern: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => {
808
+ // Service → Repository
809
+ const isSourceService = parsedSourceNode.semanticType === FairSquareSemanticNodeType.FS_SERVICE;
810
+ const isTargetRepository = parsedTargetNode.semanticType === FairSquareSemanticNodeType.FS_REPOSITORY;
811
+ if (!isSourceService || !isTargetRepository)
812
+ return false;
813
+ // Check if Service injects this Repository
814
+ const sourceDependencies = parsedSourceNode.properties.context?.dependencies ?? [];
815
+ const targetName = parsedTargetNode.properties.name;
816
+ // Use exact match to avoid false positives
817
+ return sourceDependencies.some((dep) => {
818
+ const cleanDep = dep.replace(/['"]/g, '').trim();
819
+ return cleanDep === targetName;
820
+ });
821
+ },
822
+ contextExtractor: (parsedSourceNode, parsedTargetNode, allParsedNodes, sharedContext) => ({
823
+ repositoryName: parsedTargetNode.properties.name,
824
+ serviceName: parsedSourceNode.properties.name,
825
+ }),
826
+ neo4j: {
827
+ relationshipType: 'USES_REPOSITORY',
828
+ direction: 'OUTGOING',
829
+ },
830
+ },
831
+ },
832
+ };