gitnexus 1.3.10 → 1.4.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 (97) hide show
  1. package/README.md +22 -2
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +33 -6
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +20 -2
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/setup.js +17 -19
  8. package/dist/cli/skill-gen.d.ts +26 -0
  9. package/dist/cli/skill-gen.js +549 -0
  10. package/dist/core/graph/types.d.ts +5 -2
  11. package/dist/core/ingestion/call-processor.d.ts +5 -5
  12. package/dist/core/ingestion/call-processor.js +173 -260
  13. package/dist/core/ingestion/constants.d.ts +16 -0
  14. package/dist/core/ingestion/constants.js +16 -0
  15. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  16. package/dist/core/ingestion/entry-point-scoring.js +81 -22
  17. package/dist/core/ingestion/export-detection.d.ts +18 -0
  18. package/dist/core/ingestion/export-detection.js +230 -0
  19. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  20. package/dist/core/ingestion/framework-detection.js +39 -8
  21. package/dist/core/ingestion/heritage-processor.d.ts +13 -4
  22. package/dist/core/ingestion/heritage-processor.js +92 -28
  23. package/dist/core/ingestion/import-processor.d.ts +17 -19
  24. package/dist/core/ingestion/import-processor.js +170 -695
  25. package/dist/core/ingestion/language-config.d.ts +46 -0
  26. package/dist/core/ingestion/language-config.js +167 -0
  27. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  28. package/dist/core/ingestion/mro-processor.js +369 -0
  29. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  30. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  31. package/dist/core/ingestion/parsing-processor.d.ts +1 -10
  32. package/dist/core/ingestion/parsing-processor.js +41 -177
  33. package/dist/core/ingestion/pipeline.js +41 -26
  34. package/dist/core/ingestion/process-processor.js +2 -1
  35. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  36. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  37. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  38. package/dist/core/ingestion/resolvers/go.js +42 -0
  39. package/dist/core/ingestion/resolvers/index.d.ts +16 -0
  40. package/dist/core/ingestion/resolvers/index.js +11 -0
  41. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  42. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  43. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  44. package/dist/core/ingestion/resolvers/php.js +35 -0
  45. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  46. package/dist/core/ingestion/resolvers/rust.js +73 -0
  47. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  48. package/dist/core/ingestion/resolvers/standard.js +145 -0
  49. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  50. package/dist/core/ingestion/resolvers/utils.js +120 -0
  51. package/dist/core/ingestion/symbol-resolver.d.ts +32 -0
  52. package/dist/core/ingestion/symbol-resolver.js +83 -0
  53. package/dist/core/ingestion/symbol-table.d.ts +12 -1
  54. package/dist/core/ingestion/symbol-table.js +19 -12
  55. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
  56. package/dist/core/ingestion/tree-sitter-queries.js +114 -9
  57. package/dist/core/ingestion/type-env.d.ts +27 -0
  58. package/dist/core/ingestion/type-env.js +86 -0
  59. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  60. package/dist/core/ingestion/type-extractors/c-cpp.js +60 -0
  61. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  62. package/dist/core/ingestion/type-extractors/csharp.js +89 -0
  63. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  64. package/dist/core/ingestion/type-extractors/go.js +105 -0
  65. package/dist/core/ingestion/type-extractors/index.d.ts +21 -0
  66. package/dist/core/ingestion/type-extractors/index.js +29 -0
  67. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  68. package/dist/core/ingestion/type-extractors/jvm.js +121 -0
  69. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  70. package/dist/core/ingestion/type-extractors/php.js +31 -0
  71. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  72. package/dist/core/ingestion/type-extractors/python.js +41 -0
  73. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  74. package/dist/core/ingestion/type-extractors/rust.js +39 -0
  75. package/dist/core/ingestion/type-extractors/shared.d.ts +17 -0
  76. package/dist/core/ingestion/type-extractors/shared.js +97 -0
  77. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  78. package/dist/core/ingestion/type-extractors/swift.js +43 -0
  79. package/dist/core/ingestion/type-extractors/types.d.ts +14 -0
  80. package/dist/core/ingestion/type-extractors/types.js +1 -0
  81. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  82. package/dist/core/ingestion/type-extractors/typescript.js +46 -0
  83. package/dist/core/ingestion/utils.d.ts +67 -0
  84. package/dist/core/ingestion/utils.js +691 -4
  85. package/dist/core/ingestion/workers/parse-worker.d.ts +20 -3
  86. package/dist/core/ingestion/workers/parse-worker.js +84 -345
  87. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  88. package/dist/core/kuzu/csv-generator.js +19 -3
  89. package/dist/core/kuzu/kuzu-adapter.js +5 -2
  90. package/dist/core/kuzu/schema.d.ts +3 -3
  91. package/dist/core/kuzu/schema.js +16 -1
  92. package/dist/core/search/bm25-index.js +2 -1
  93. package/dist/mcp/core/kuzu-adapter.js +6 -18
  94. package/dist/mcp/tools.js +12 -3
  95. package/hooks/claude/gitnexus-hook.cjs +149 -66
  96. package/package.json +1 -1
  97. package/skills/gitnexus-cli.md +1 -1
@@ -0,0 +1,549 @@
1
+ /**
2
+ * Skill File Generator
3
+ *
4
+ * Generates repo-specific SKILL.md files from detected Leiden communities.
5
+ * Each significant community becomes a skill that describes a functional area
6
+ * of the codebase, including key files, entry points, execution flows, and
7
+ * cross-community connections.
8
+ */
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ // ============================================================================
12
+ // MAIN EXPORT
13
+ // ============================================================================
14
+ /**
15
+ * @brief Generate repo-specific skill files from detected communities
16
+ * @param {string} repoPath - Absolute path to the repository root
17
+ * @param {string} projectName - Human-readable project name
18
+ * @param {PipelineResult} pipelineResult - In-memory pipeline data with communities, processes, graph
19
+ * @returns {Promise<{ skills: GeneratedSkillInfo[], outputPath: string }>} Generated skill metadata
20
+ */
21
+ export const generateSkillFiles = async (repoPath, projectName, pipelineResult) => {
22
+ const { communityResult, processResult, graph } = pipelineResult;
23
+ const outputDir = path.join(repoPath, '.claude', 'skills', 'generated');
24
+ if (!communityResult || !communityResult.memberships.length) {
25
+ console.log('\n Skills: no communities detected, skipping skill generation');
26
+ return { skills: [], outputPath: outputDir };
27
+ }
28
+ console.log('\n Generating repo-specific skills...');
29
+ // Step 1: Build communities from memberships (not the filtered communities array).
30
+ // The community processor skips singletons from its communities array but memberships
31
+ // include ALL assignments. For repos with sparse CALLS edges, the communities array
32
+ // can be empty while memberships still has useful groupings.
33
+ const communities = communityResult.communities.length > 0
34
+ ? communityResult.communities
35
+ : buildCommunitiesFromMemberships(communityResult.memberships, graph, repoPath);
36
+ const aggregated = aggregateCommunities(communities);
37
+ // Step 2: Filter to significant communities
38
+ // Keep communities with >= 3 symbols after aggregation.
39
+ const significant = aggregated
40
+ .filter(c => c.symbolCount >= 3)
41
+ .sort((a, b) => b.symbolCount - a.symbolCount)
42
+ .slice(0, 20);
43
+ if (significant.length === 0) {
44
+ console.log('\n Skills: no significant communities found (all below 3-symbol threshold)');
45
+ return { skills: [], outputPath: outputDir };
46
+ }
47
+ // Step 3: Build lookup maps
48
+ const membershipsByComm = buildMembershipMap(communityResult.memberships);
49
+ const nodeIdToCommunityLabel = buildNodeCommunityLabelMap(communityResult.memberships, communities);
50
+ // Step 4: Clear and recreate output directory
51
+ try {
52
+ await fs.rm(outputDir, { recursive: true, force: true });
53
+ }
54
+ catch { /* may not exist */ }
55
+ await fs.mkdir(outputDir, { recursive: true });
56
+ // Step 5: Generate skill files
57
+ const skills = [];
58
+ const usedNames = new Set();
59
+ for (const community of significant) {
60
+ // Gather member symbols
61
+ const members = gatherMembers(community.rawIds, membershipsByComm, graph);
62
+ if (members.length === 0)
63
+ continue;
64
+ // Gather file info
65
+ const files = gatherFiles(members, repoPath);
66
+ // Gather entry points
67
+ const entryPoints = gatherEntryPoints(members);
68
+ // Gather execution flows
69
+ const flows = gatherFlows(community.rawIds, processResult?.processes || []);
70
+ // Gather cross-community connections
71
+ const connections = gatherCrossConnections(community.rawIds, community.label, membershipsByComm, nodeIdToCommunityLabel, graph);
72
+ // Generate kebab name
73
+ const kebabName = toKebabName(community.label, usedNames);
74
+ usedNames.add(kebabName);
75
+ // Generate SKILL.md content
76
+ const content = renderSkillMarkdown(community, projectName, members, files, entryPoints, flows, connections, kebabName);
77
+ // Write file
78
+ const skillDir = path.join(outputDir, kebabName);
79
+ await fs.mkdir(skillDir, { recursive: true });
80
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
81
+ const info = {
82
+ name: kebabName,
83
+ label: community.label,
84
+ symbolCount: community.symbolCount,
85
+ fileCount: files.length,
86
+ };
87
+ skills.push(info);
88
+ console.log(` \u2713 ${community.label} (${community.symbolCount} symbols, ${files.length} files)`);
89
+ }
90
+ console.log(`\n ${skills.length} skills generated \u2192 .claude/skills/generated/`);
91
+ return { skills, outputPath: outputDir };
92
+ };
93
+ // ============================================================================
94
+ // FALLBACK COMMUNITY BUILDER
95
+ // ============================================================================
96
+ /**
97
+ * @brief Build CommunityNode-like objects from raw memberships when the community
98
+ * processor's communities array is empty (all singletons were filtered out)
99
+ * @param {CommunityMembership[]} memberships - All node-to-community assignments
100
+ * @param {KnowledgeGraph} graph - The knowledge graph for resolving node metadata
101
+ * @param {string} repoPath - Repository root for path normalization
102
+ * @returns {CommunityNode[]} Synthetic community nodes built from membership data
103
+ */
104
+ const buildCommunitiesFromMemberships = (memberships, graph, repoPath) => {
105
+ // Group memberships by communityId
106
+ const groups = new Map();
107
+ for (const m of memberships) {
108
+ const arr = groups.get(m.communityId);
109
+ if (arr) {
110
+ arr.push(m.nodeId);
111
+ }
112
+ else {
113
+ groups.set(m.communityId, [m.nodeId]);
114
+ }
115
+ }
116
+ const communities = [];
117
+ for (const [commId, nodeIds] of groups) {
118
+ // Derive a heuristic label from the most common parent directory
119
+ const folderCounts = new Map();
120
+ for (const nodeId of nodeIds) {
121
+ const node = graph.getNode(nodeId);
122
+ if (!node?.properties.filePath)
123
+ continue;
124
+ const normalized = node.properties.filePath.replace(/\\/g, '/');
125
+ const parts = normalized.split('/').filter(Boolean);
126
+ if (parts.length >= 2) {
127
+ const folder = parts[parts.length - 2];
128
+ if (!['src', 'lib', 'core', 'utils', 'common', 'shared', 'helpers'].includes(folder.toLowerCase())) {
129
+ folderCounts.set(folder, (folderCounts.get(folder) || 0) + 1);
130
+ }
131
+ }
132
+ }
133
+ let bestFolder = '';
134
+ let bestCount = 0;
135
+ for (const [folder, count] of folderCounts) {
136
+ if (count > bestCount) {
137
+ bestCount = count;
138
+ bestFolder = folder;
139
+ }
140
+ }
141
+ const label = bestFolder
142
+ ? bestFolder.charAt(0).toUpperCase() + bestFolder.slice(1)
143
+ : `Cluster_${commId.replace('comm_', '')}`;
144
+ // Compute cohesion as internal-edge ratio (matches backend calculateCohesion).
145
+ // For each member node, count edges that stay inside the community vs total.
146
+ const nodeSet = new Set(nodeIds);
147
+ let internalEdges = 0;
148
+ let totalEdges = 0;
149
+ graph.forEachRelationship(rel => {
150
+ if (nodeSet.has(rel.sourceId)) {
151
+ totalEdges++;
152
+ if (nodeSet.has(rel.targetId))
153
+ internalEdges++;
154
+ }
155
+ });
156
+ const cohesion = totalEdges > 0 ? Math.min(1.0, internalEdges / totalEdges) : 1.0;
157
+ communities.push({
158
+ id: commId,
159
+ label,
160
+ heuristicLabel: label,
161
+ cohesion,
162
+ symbolCount: nodeIds.length,
163
+ });
164
+ }
165
+ return communities.sort((a, b) => b.symbolCount - a.symbolCount);
166
+ };
167
+ // ============================================================================
168
+ // AGGREGATION
169
+ // ============================================================================
170
+ /**
171
+ * @brief Aggregate raw Leiden communities by heuristicLabel
172
+ * @param {CommunityNode[]} communities - Raw community nodes from Leiden detection
173
+ * @returns {AggregatedCommunity[]} Aggregated communities grouped by label
174
+ */
175
+ const aggregateCommunities = (communities) => {
176
+ const groups = new Map();
177
+ for (const c of communities) {
178
+ const label = c.heuristicLabel || c.label || 'Unknown';
179
+ const symbols = c.symbolCount || 0;
180
+ const cohesion = c.cohesion || 0;
181
+ const existing = groups.get(label);
182
+ if (!existing) {
183
+ groups.set(label, {
184
+ rawIds: [c.id],
185
+ totalSymbols: symbols,
186
+ weightedCohesion: cohesion * symbols,
187
+ });
188
+ }
189
+ else {
190
+ existing.rawIds.push(c.id);
191
+ existing.totalSymbols += symbols;
192
+ existing.weightedCohesion += cohesion * symbols;
193
+ }
194
+ }
195
+ return Array.from(groups.entries()).map(([label, g]) => ({
196
+ label,
197
+ rawIds: g.rawIds,
198
+ symbolCount: g.totalSymbols,
199
+ cohesion: g.totalSymbols > 0 ? g.weightedCohesion / g.totalSymbols : 0,
200
+ }));
201
+ };
202
+ // ============================================================================
203
+ // LOOKUP MAP BUILDERS
204
+ // ============================================================================
205
+ /**
206
+ * @brief Build a map from communityId to member nodeIds
207
+ * @param {CommunityMembership[]} memberships - All membership records
208
+ * @returns {Map<string, string[]>} Map of communityId -> nodeId[]
209
+ */
210
+ const buildMembershipMap = (memberships) => {
211
+ const map = new Map();
212
+ for (const m of memberships) {
213
+ const arr = map.get(m.communityId);
214
+ if (arr) {
215
+ arr.push(m.nodeId);
216
+ }
217
+ else {
218
+ map.set(m.communityId, [m.nodeId]);
219
+ }
220
+ }
221
+ return map;
222
+ };
223
+ /**
224
+ * @brief Build a map from nodeId to aggregated community label
225
+ * @param {CommunityMembership[]} memberships - All membership records
226
+ * @param {CommunityNode[]} communities - Community nodes with labels
227
+ * @returns {Map<string, string>} Map of nodeId -> community label
228
+ */
229
+ const buildNodeCommunityLabelMap = (memberships, communities) => {
230
+ const commIdToLabel = new Map();
231
+ for (const c of communities) {
232
+ commIdToLabel.set(c.id, c.heuristicLabel || c.label || 'Unknown');
233
+ }
234
+ const map = new Map();
235
+ for (const m of memberships) {
236
+ const label = commIdToLabel.get(m.communityId);
237
+ if (label) {
238
+ map.set(m.nodeId, label);
239
+ }
240
+ }
241
+ return map;
242
+ };
243
+ // ============================================================================
244
+ // DATA GATHERING
245
+ // ============================================================================
246
+ /**
247
+ * @brief Gather member symbols for an aggregated community
248
+ * @param {string[]} rawIds - Raw community IDs belonging to this aggregated community
249
+ * @param {Map<string, string[]>} membershipsByComm - communityId -> nodeIds
250
+ * @param {KnowledgeGraph} graph - The knowledge graph
251
+ * @returns {MemberSymbol[]} Array of member symbol information
252
+ */
253
+ const gatherMembers = (rawIds, membershipsByComm, graph) => {
254
+ const seen = new Set();
255
+ const members = [];
256
+ for (const commId of rawIds) {
257
+ const nodeIds = membershipsByComm.get(commId) || [];
258
+ for (const nodeId of nodeIds) {
259
+ if (seen.has(nodeId))
260
+ continue;
261
+ seen.add(nodeId);
262
+ const node = graph.getNode(nodeId);
263
+ if (!node)
264
+ continue;
265
+ members.push({
266
+ id: node.id,
267
+ name: node.properties.name,
268
+ label: node.label,
269
+ filePath: node.properties.filePath || '',
270
+ startLine: node.properties.startLine || 0,
271
+ isExported: node.properties.isExported === true,
272
+ });
273
+ }
274
+ }
275
+ return members;
276
+ };
277
+ /**
278
+ * @brief Gather deduplicated file info with per-file symbol names
279
+ * @param {MemberSymbol[]} members - Member symbols
280
+ * @param {string} repoPath - Repository root for relative path computation
281
+ * @returns {FileInfo[]} Sorted by symbol count descending
282
+ */
283
+ const gatherFiles = (members, repoPath) => {
284
+ const fileMap = new Map();
285
+ for (const m of members) {
286
+ if (!m.filePath)
287
+ continue;
288
+ const rel = toRelativePath(m.filePath, repoPath);
289
+ const arr = fileMap.get(rel);
290
+ if (arr) {
291
+ arr.push(m.name);
292
+ }
293
+ else {
294
+ fileMap.set(rel, [m.name]);
295
+ }
296
+ }
297
+ return Array.from(fileMap.entries())
298
+ .map(([relativePath, symbols]) => ({ relativePath, symbols }))
299
+ .sort((a, b) => b.symbols.length - a.symbols.length);
300
+ };
301
+ /**
302
+ * @brief Gather exported entry points prioritized by type
303
+ * @param {MemberSymbol[]} members - Member symbols
304
+ * @returns {MemberSymbol[]} Exported symbols sorted by type priority
305
+ */
306
+ const gatherEntryPoints = (members) => {
307
+ const typePriority = {
308
+ Function: 0,
309
+ Class: 1,
310
+ Method: 2,
311
+ Interface: 3,
312
+ };
313
+ return members
314
+ .filter(m => m.isExported)
315
+ .sort((a, b) => {
316
+ const pa = typePriority[a.label] ?? 99;
317
+ const pb = typePriority[b.label] ?? 99;
318
+ return pa - pb;
319
+ });
320
+ };
321
+ /**
322
+ * @brief Gather execution flows touching this community
323
+ * @param {string[]} rawIds - Raw community IDs for this aggregated community
324
+ * @param {ProcessNode[]} processes - All detected processes
325
+ * @returns {ProcessNode[]} Processes whose communities intersect rawIds, sorted by stepCount
326
+ */
327
+ const gatherFlows = (rawIds, processes) => {
328
+ const rawIdSet = new Set(rawIds);
329
+ return processes
330
+ .filter(proc => proc.communities.some(cid => rawIdSet.has(cid)))
331
+ .sort((a, b) => b.stepCount - a.stepCount);
332
+ };
333
+ /**
334
+ * @brief Gather cross-community call connections
335
+ * @param {string[]} rawIds - Raw community IDs for this aggregated community
336
+ * @param {string} ownLabel - This community's aggregated label
337
+ * @param {Map<string, string[]>} membershipsByComm - communityId -> nodeIds
338
+ * @param {Map<string, string>} nodeIdToCommunityLabel - nodeId -> community label
339
+ * @param {KnowledgeGraph} graph - The knowledge graph
340
+ * @returns {CrossConnection[]} Aggregated cross-community connections sorted by count
341
+ */
342
+ const gatherCrossConnections = (rawIds, ownLabel, membershipsByComm, nodeIdToCommunityLabel, graph) => {
343
+ // Collect all node IDs in this aggregated community
344
+ const ownNodeIds = new Set();
345
+ for (const commId of rawIds) {
346
+ const nodeIds = membershipsByComm.get(commId) || [];
347
+ for (const nid of nodeIds) {
348
+ ownNodeIds.add(nid);
349
+ }
350
+ }
351
+ // Count outgoing CALLS to nodes in different communities
352
+ const targetCounts = new Map();
353
+ graph.forEachRelationship(rel => {
354
+ if (rel.type !== 'CALLS')
355
+ return;
356
+ if (!ownNodeIds.has(rel.sourceId))
357
+ return;
358
+ if (ownNodeIds.has(rel.targetId))
359
+ return; // same community
360
+ const targetLabel = nodeIdToCommunityLabel.get(rel.targetId);
361
+ if (!targetLabel || targetLabel === ownLabel)
362
+ return;
363
+ targetCounts.set(targetLabel, (targetCounts.get(targetLabel) || 0) + 1);
364
+ });
365
+ return Array.from(targetCounts.entries())
366
+ .map(([targetLabel, count]) => ({ targetLabel, count }))
367
+ .sort((a, b) => b.count - a.count);
368
+ };
369
+ // ============================================================================
370
+ // MARKDOWN RENDERING
371
+ // ============================================================================
372
+ /**
373
+ * @brief Render SKILL.md content for a single community
374
+ * @param {AggregatedCommunity} community - The aggregated community data
375
+ * @param {string} projectName - Project name for the description
376
+ * @param {MemberSymbol[]} members - All member symbols
377
+ * @param {FileInfo[]} files - File info with symbol names
378
+ * @param {MemberSymbol[]} entryPoints - Exported entry point symbols
379
+ * @param {ProcessNode[]} flows - Execution flows touching this community
380
+ * @param {CrossConnection[]} connections - Cross-community connections
381
+ * @param {string} kebabName - Kebab-case name for the skill
382
+ * @returns {string} Full SKILL.md content
383
+ */
384
+ const renderSkillMarkdown = (community, projectName, members, files, entryPoints, flows, connections, kebabName) => {
385
+ const cohesionPct = Math.round(community.cohesion * 100);
386
+ // Dominant directory: most common top-level directory
387
+ const dominantDir = getDominantDirectory(files);
388
+ // Top symbol names for "When to Use"
389
+ const topNames = entryPoints.slice(0, 3).map(e => e.name);
390
+ if (topNames.length === 0) {
391
+ // Fallback to any members
392
+ topNames.push(...members.slice(0, 3).map(m => m.name));
393
+ }
394
+ const lines = [];
395
+ // Frontmatter
396
+ lines.push('---');
397
+ lines.push(`name: ${kebabName}`);
398
+ lines.push(`description: "Skill for the ${community.label} area of ${projectName}. ${community.symbolCount} symbols across ${files.length} files."`);
399
+ lines.push('---');
400
+ lines.push('');
401
+ // Title
402
+ lines.push(`# ${community.label}`);
403
+ lines.push('');
404
+ lines.push(`${community.symbolCount} symbols | ${files.length} files | Cohesion: ${cohesionPct}%`);
405
+ lines.push('');
406
+ // When to Use
407
+ lines.push('## When to Use');
408
+ lines.push('');
409
+ if (dominantDir) {
410
+ lines.push(`- Working with code in \`${dominantDir}/\``);
411
+ }
412
+ if (topNames.length > 0) {
413
+ lines.push(`- Understanding how ${topNames.join(', ')} work`);
414
+ }
415
+ lines.push(`- Modifying ${community.label.toLowerCase()}-related functionality`);
416
+ lines.push('');
417
+ // Key Files (top 10)
418
+ lines.push('## Key Files');
419
+ lines.push('');
420
+ lines.push('| File | Symbols |');
421
+ lines.push('|------|---------|');
422
+ for (const f of files.slice(0, 10)) {
423
+ const symbolList = f.symbols.slice(0, 5).join(', ');
424
+ const suffix = f.symbols.length > 5 ? ` (+${f.symbols.length - 5})` : '';
425
+ lines.push(`| \`${f.relativePath}\` | ${symbolList}${suffix} |`);
426
+ }
427
+ lines.push('');
428
+ // Entry Points (top 5)
429
+ if (entryPoints.length > 0) {
430
+ lines.push('## Entry Points');
431
+ lines.push('');
432
+ lines.push('Start here when exploring this area:');
433
+ lines.push('');
434
+ for (const ep of entryPoints.slice(0, 5)) {
435
+ lines.push(`- **\`${ep.name}\`** (${ep.label}) \u2014 \`${ep.filePath}:${ep.startLine}\``);
436
+ }
437
+ lines.push('');
438
+ }
439
+ // Key Symbols (top 20, exported first, then by type)
440
+ lines.push('## Key Symbols');
441
+ lines.push('');
442
+ lines.push('| Symbol | Type | File | Line |');
443
+ lines.push('|--------|------|------|------|');
444
+ const sortedMembers = [...members].sort((a, b) => {
445
+ if (a.isExported !== b.isExported)
446
+ return a.isExported ? -1 : 1;
447
+ return a.label.localeCompare(b.label);
448
+ });
449
+ for (const m of sortedMembers.slice(0, 20)) {
450
+ lines.push(`| \`${m.name}\` | ${m.label} | \`${m.filePath}\` | ${m.startLine} |`);
451
+ }
452
+ lines.push('');
453
+ // Execution Flows
454
+ if (flows.length > 0) {
455
+ lines.push('## Execution Flows');
456
+ lines.push('');
457
+ lines.push('| Flow | Type | Steps |');
458
+ lines.push('|------|------|-------|');
459
+ for (const f of flows.slice(0, 10)) {
460
+ lines.push(`| \`${f.heuristicLabel}\` | ${f.processType} | ${f.stepCount} |`);
461
+ }
462
+ lines.push('');
463
+ }
464
+ // Connected Areas
465
+ if (connections.length > 0) {
466
+ lines.push('## Connected Areas');
467
+ lines.push('');
468
+ lines.push('| Area | Connections |');
469
+ lines.push('|------|-------------|');
470
+ for (const c of connections.slice(0, 8)) {
471
+ lines.push(`| ${c.targetLabel} | ${c.count} calls |`);
472
+ }
473
+ lines.push('');
474
+ }
475
+ // How to Explore
476
+ const firstEntry = entryPoints.length > 0 ? entryPoints[0].name : (members.length > 0 ? members[0].name : community.label);
477
+ lines.push('## How to Explore');
478
+ lines.push('');
479
+ lines.push(`1. \`gitnexus_context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
480
+ lines.push(`2. \`gitnexus_query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`);
481
+ lines.push('3. Read key files listed above for implementation details');
482
+ lines.push('');
483
+ return lines.join('\n');
484
+ };
485
+ // ============================================================================
486
+ // UTILITY HELPERS
487
+ // ============================================================================
488
+ /**
489
+ * @brief Convert a community label to a kebab-case directory name
490
+ * @param {string} label - The community label
491
+ * @param {Set<string>} usedNames - Already-used names for collision detection
492
+ * @returns {string} Unique kebab-case name capped at 50 characters
493
+ */
494
+ const toKebabName = (label, usedNames) => {
495
+ let name = label
496
+ .toLowerCase()
497
+ .replace(/[^a-z0-9]+/g, '-')
498
+ .replace(/^-+|-+$/g, '')
499
+ .slice(0, 50);
500
+ if (!name)
501
+ name = 'skill';
502
+ let candidate = name;
503
+ let counter = 2;
504
+ while (usedNames.has(candidate)) {
505
+ candidate = `${name}-${counter}`;
506
+ counter++;
507
+ }
508
+ return candidate;
509
+ };
510
+ /**
511
+ * @brief Convert an absolute or repo-relative file path to a clean relative path
512
+ * @param {string} filePath - The file path from the graph node
513
+ * @param {string} repoPath - Repository root path
514
+ * @returns {string} Relative path using forward slashes
515
+ */
516
+ const toRelativePath = (filePath, repoPath) => {
517
+ // Normalize to forward slashes for cross-platform consistency
518
+ const normalizedFile = filePath.replace(/\\/g, '/');
519
+ const normalizedRepo = repoPath.replace(/\\/g, '/');
520
+ if (normalizedFile.startsWith(normalizedRepo)) {
521
+ return normalizedFile.slice(normalizedRepo.length).replace(/^\//, '');
522
+ }
523
+ // Already relative or different root
524
+ return normalizedFile.replace(/^\//, '');
525
+ };
526
+ /**
527
+ * @brief Find the dominant (most common) top-level directory across files
528
+ * @param {FileInfo[]} files - File info entries
529
+ * @returns {string | null} Most common directory or null
530
+ */
531
+ const getDominantDirectory = (files) => {
532
+ const dirCounts = new Map();
533
+ for (const f of files) {
534
+ const parts = f.relativePath.split('/');
535
+ if (parts.length >= 2) {
536
+ const dir = parts[0];
537
+ dirCounts.set(dir, (dirCounts.get(dir) || 0) + f.symbols.length);
538
+ }
539
+ }
540
+ let best = null;
541
+ let bestCount = 0;
542
+ for (const [dir, count] of dirCounts) {
543
+ if (count > bestCount) {
544
+ bestCount = count;
545
+ best = dir;
546
+ }
547
+ }
548
+ return best;
549
+ };
@@ -1,10 +1,11 @@
1
1
  export type NodeLabel = 'Project' | 'Package' | 'Module' | 'Folder' | 'File' | 'Class' | 'Function' | 'Method' | 'Variable' | 'Interface' | 'Enum' | 'Decorator' | 'Import' | 'Type' | 'CodeElement' | 'Community' | 'Process' | 'Struct' | 'Macro' | 'Typedef' | 'Union' | 'Namespace' | 'Trait' | 'Impl' | 'TypeAlias' | 'Const' | 'Static' | 'Property' | 'Record' | 'Delegate' | 'Annotation' | 'Constructor' | 'Template';
2
+ import { SupportedLanguages } from '../../config/supported-languages.js';
2
3
  export type NodeProperties = {
3
4
  name: string;
4
5
  filePath: string;
5
6
  startLine?: number;
6
7
  endLine?: number;
7
- language?: string;
8
+ language?: SupportedLanguages;
8
9
  isExported?: boolean;
9
10
  astFrameworkMultiplier?: number;
10
11
  astFrameworkReason?: string;
@@ -21,8 +22,10 @@ export type NodeProperties = {
21
22
  terminalId?: string;
22
23
  entryPointScore?: number;
23
24
  entryPointReason?: string;
25
+ parameterCount?: number;
26
+ returnType?: string;
24
27
  };
25
- export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'OVERRIDES' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'MEMBER_OF' | 'STEP_IN_PROCESS';
28
+ export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'OVERRIDES' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' | 'MEMBER_OF' | 'STEP_IN_PROCESS';
26
29
  export interface GraphNode {
27
30
  id: string;
28
31
  label: NodeLabel;
@@ -1,19 +1,19 @@
1
1
  import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { ASTCache } from './ast-cache.js';
3
- import { SymbolTable } from './symbol-table.js';
4
- import { ImportMap } from './import-processor.js';
3
+ import type { SymbolTable } from './symbol-table.js';
4
+ import { ImportMap, PackageMap, NamedImportMap } from './import-processor.js';
5
5
  import type { ExtractedCall, ExtractedRoute } from './workers/parse-worker.js';
6
6
  export declare const processCalls: (graph: KnowledgeGraph, files: {
7
7
  path: string;
8
8
  content: string;
9
- }[], astCache: ASTCache, symbolTable: SymbolTable, importMap: ImportMap, onProgress?: (current: number, total: number) => void) => Promise<void>;
9
+ }[], astCache: ASTCache, symbolTable: SymbolTable, importMap: ImportMap, packageMap?: PackageMap, onProgress?: (current: number, total: number) => void, namedImportMap?: NamedImportMap) => Promise<void>;
10
10
  /**
11
11
  * Fast path: resolve pre-extracted call sites from workers.
12
12
  * No AST parsing — workers already extracted calledName + sourceId.
13
13
  * This function only does symbol table lookups + graph mutations.
14
14
  */
15
- export declare const processCallsFromExtracted: (graph: KnowledgeGraph, extractedCalls: ExtractedCall[], symbolTable: SymbolTable, importMap: ImportMap, onProgress?: (current: number, total: number) => void) => Promise<void>;
15
+ export declare const processCallsFromExtracted: (graph: KnowledgeGraph, extractedCalls: ExtractedCall[], symbolTable: SymbolTable, importMap: ImportMap, packageMap?: PackageMap, onProgress?: (current: number, total: number) => void, namedImportMap?: NamedImportMap) => Promise<void>;
16
16
  /**
17
17
  * Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
18
18
  */
19
- export declare const processRoutesFromExtracted: (graph: KnowledgeGraph, extractedRoutes: ExtractedRoute[], symbolTable: SymbolTable, importMap: ImportMap, onProgress?: (current: number, total: number) => void) => Promise<void>;
19
+ export declare const processRoutesFromExtracted: (graph: KnowledgeGraph, extractedRoutes: ExtractedRoute[], symbolTable: SymbolTable, importMap: ImportMap, packageMap?: PackageMap, onProgress?: (current: number, total: number) => void) => Promise<void>;