gitnexus 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -214
- package/dist/cli/ai-context.d.ts +1 -2
- package/dist/cli/ai-context.js +90 -117
- package/dist/cli/analyze.d.ts +0 -2
- package/dist/cli/analyze.js +2 -20
- package/dist/cli/index.js +25 -17
- package/dist/cli/setup.js +19 -17
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/graph/types.d.ts +2 -5
- package/dist/core/ingestion/ast-cache.js +2 -3
- package/dist/core/ingestion/call-processor.d.ts +5 -5
- package/dist/core/ingestion/call-processor.js +258 -173
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -2
- package/dist/core/ingestion/entry-point-scoring.js +22 -81
- package/dist/core/ingestion/framework-detection.d.ts +1 -5
- package/dist/core/ingestion/framework-detection.js +8 -39
- package/dist/core/ingestion/heritage-processor.d.ts +4 -13
- package/dist/core/ingestion/heritage-processor.js +28 -92
- package/dist/core/ingestion/import-processor.d.ts +19 -17
- package/dist/core/ingestion/import-processor.js +695 -170
- package/dist/core/ingestion/parsing-processor.d.ts +10 -1
- package/dist/core/ingestion/parsing-processor.js +177 -41
- package/dist/core/ingestion/pipeline.js +26 -49
- package/dist/core/ingestion/process-processor.js +1 -2
- package/dist/core/ingestion/symbol-table.d.ts +1 -12
- package/dist/core/ingestion/symbol-table.js +12 -19
- package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
- package/dist/core/ingestion/tree-sitter-queries.js +485 -590
- package/dist/core/ingestion/utils.d.ts +0 -67
- package/dist/core/ingestion/utils.js +9 -692
- package/dist/core/ingestion/workers/parse-worker.d.ts +3 -20
- package/dist/core/ingestion/workers/parse-worker.js +345 -84
- package/dist/core/ingestion/workers/worker-pool.js +0 -8
- package/dist/core/kuzu/csv-generator.js +3 -19
- package/dist/core/kuzu/kuzu-adapter.js +19 -14
- package/dist/core/kuzu/schema.d.ts +3 -3
- package/dist/core/kuzu/schema.js +288 -303
- package/dist/core/search/bm25-index.js +6 -7
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/diagrams.d.ts +27 -0
- package/dist/core/wiki/diagrams.js +163 -0
- package/dist/core/wiki/generator.d.ts +50 -2
- package/dist/core/wiki/generator.js +548 -49
- package/dist/core/wiki/graph-queries.d.ts +42 -0
- package/dist/core/wiki/graph-queries.js +276 -97
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/llm-client.js +73 -11
- package/dist/core/wiki/prompts.d.ts +52 -8
- package/dist/core/wiki/prompts.js +200 -86
- package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
- package/dist/mcp/core/kuzu-adapter.js +44 -13
- package/dist/mcp/local/local-backend.js +128 -128
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +19 -18
- package/dist/mcp/tools.js +104 -103
- package/hooks/claude/gitnexus-hook.cjs +155 -238
- package/hooks/claude/pre-tool-use.sh +79 -79
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +96 -96
- package/scripts/patch-tree-sitter-swift.cjs +74 -74
- package/skills/gitnexus-cli.md +82 -82
- package/skills/gitnexus-debugging.md +89 -89
- package/skills/gitnexus-exploring.md +78 -78
- package/skills/gitnexus-guide.md +64 -64
- package/skills/gitnexus-impact-analysis.md +97 -97
- package/skills/gitnexus-pr-review.md +163 -163
- package/skills/gitnexus-refactoring.md +121 -121
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
- package/dist/cli/lazy-action.d.ts +0 -6
- package/dist/cli/lazy-action.js +0 -18
- package/dist/cli/skill-gen.d.ts +0 -26
- package/dist/cli/skill-gen.js +0 -549
- package/dist/core/ingestion/constants.d.ts +0 -16
- package/dist/core/ingestion/constants.js +0 -16
- package/dist/core/ingestion/export-detection.d.ts +0 -18
- package/dist/core/ingestion/export-detection.js +0 -230
- package/dist/core/ingestion/language-config.d.ts +0 -46
- package/dist/core/ingestion/language-config.js +0 -167
- package/dist/core/ingestion/mro-processor.d.ts +0 -45
- package/dist/core/ingestion/mro-processor.js +0 -369
- package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
- package/dist/core/ingestion/named-binding-extraction.js +0 -363
- package/dist/core/ingestion/resolvers/csharp.d.ts +0 -22
- package/dist/core/ingestion/resolvers/csharp.js +0 -109
- package/dist/core/ingestion/resolvers/go.d.ts +0 -19
- package/dist/core/ingestion/resolvers/go.js +0 -42
- package/dist/core/ingestion/resolvers/index.d.ts +0 -16
- package/dist/core/ingestion/resolvers/index.js +0 -11
- package/dist/core/ingestion/resolvers/jvm.d.ts +0 -23
- package/dist/core/ingestion/resolvers/jvm.js +0 -87
- package/dist/core/ingestion/resolvers/php.d.ts +0 -15
- package/dist/core/ingestion/resolvers/php.js +0 -35
- package/dist/core/ingestion/resolvers/rust.d.ts +0 -15
- package/dist/core/ingestion/resolvers/rust.js +0 -73
- package/dist/core/ingestion/resolvers/standard.d.ts +0 -28
- package/dist/core/ingestion/resolvers/standard.js +0 -145
- package/dist/core/ingestion/resolvers/utils.d.ts +0 -33
- package/dist/core/ingestion/resolvers/utils.js +0 -120
- package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
- package/dist/core/ingestion/symbol-resolver.js +0 -83
- package/dist/core/ingestion/type-env.d.ts +0 -27
- package/dist/core/ingestion/type-env.js +0 -86
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/c-cpp.js +0 -60
- package/dist/core/ingestion/type-extractors/csharp.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/csharp.js +0 -89
- package/dist/core/ingestion/type-extractors/go.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/go.js +0 -105
- package/dist/core/ingestion/type-extractors/index.d.ts +0 -21
- package/dist/core/ingestion/type-extractors/index.js +0 -29
- package/dist/core/ingestion/type-extractors/jvm.d.ts +0 -3
- package/dist/core/ingestion/type-extractors/jvm.js +0 -121
- package/dist/core/ingestion/type-extractors/php.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/php.js +0 -31
- package/dist/core/ingestion/type-extractors/python.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/python.js +0 -41
- package/dist/core/ingestion/type-extractors/rust.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/rust.js +0 -39
- package/dist/core/ingestion/type-extractors/shared.d.ts +0 -17
- package/dist/core/ingestion/type-extractors/shared.js +0 -97
- package/dist/core/ingestion/type-extractors/swift.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/swift.js +0 -43
- package/dist/core/ingestion/type-extractors/types.d.ts +0 -14
- package/dist/core/ingestion/type-extractors/types.js +0 -1
- package/dist/core/ingestion/type-extractors/typescript.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/typescript.js +0 -46
- package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
- package/dist/mcp/compatible-stdio-transport.js +0 -200
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a lazy-loaded CLI action that defers module import until invocation.
|
|
3
|
-
* The generic constraints ensure the export name is a valid key of the module
|
|
4
|
-
* at compile time — catching typos when used with concrete module imports.
|
|
5
|
-
*/
|
|
6
|
-
export declare function createLazyAction<TModule extends Record<string, unknown>, TKey extends string & keyof TModule>(loader: () => Promise<TModule>, exportName: TKey): (...args: unknown[]) => Promise<void>;
|
package/dist/cli/lazy-action.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a lazy-loaded CLI action that defers module import until invocation.
|
|
3
|
-
* The generic constraints ensure the export name is a valid key of the module
|
|
4
|
-
* at compile time — catching typos when used with concrete module imports.
|
|
5
|
-
*/
|
|
6
|
-
function isCallable(value) {
|
|
7
|
-
return typeof value === 'function';
|
|
8
|
-
}
|
|
9
|
-
export function createLazyAction(loader, exportName) {
|
|
10
|
-
return async (...args) => {
|
|
11
|
-
const module = await loader();
|
|
12
|
-
const action = module[exportName];
|
|
13
|
-
if (!isCallable(action)) {
|
|
14
|
-
throw new Error(`Lazy action export not found: ${exportName}`);
|
|
15
|
-
}
|
|
16
|
-
await action(...args);
|
|
17
|
-
};
|
|
18
|
-
}
|
package/dist/cli/skill-gen.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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 { PipelineResult } from '../types/pipeline.js';
|
|
10
|
-
export interface GeneratedSkillInfo {
|
|
11
|
-
name: string;
|
|
12
|
-
label: string;
|
|
13
|
-
symbolCount: number;
|
|
14
|
-
fileCount: number;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* @brief Generate repo-specific skill files from detected communities
|
|
18
|
-
* @param {string} repoPath - Absolute path to the repository root
|
|
19
|
-
* @param {string} projectName - Human-readable project name
|
|
20
|
-
* @param {PipelineResult} pipelineResult - In-memory pipeline data with communities, processes, graph
|
|
21
|
-
* @returns {Promise<{ skills: GeneratedSkillInfo[], outputPath: string }>} Generated skill metadata
|
|
22
|
-
*/
|
|
23
|
-
export declare const generateSkillFiles: (repoPath: string, projectName: string, pipelineResult: PipelineResult) => Promise<{
|
|
24
|
-
skills: GeneratedSkillInfo[];
|
|
25
|
-
outputPath: string;
|
|
26
|
-
}>;
|
package/dist/cli/skill-gen.js
DELETED
|
@@ -1,549 +0,0 @@
|
|
|
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,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default minimum buffer size for tree-sitter parsing (512 KB).
|
|
3
|
-
* tree-sitter requires bufferSize >= file size in bytes.
|
|
4
|
-
*/
|
|
5
|
-
export declare const TREE_SITTER_BUFFER_SIZE: number;
|
|
6
|
-
/**
|
|
7
|
-
* Maximum buffer size cap (32 MB) to prevent OOM on huge files.
|
|
8
|
-
* Also used as the file-size skip threshold — files larger than this are not parsed.
|
|
9
|
-
*/
|
|
10
|
-
export declare const TREE_SITTER_MAX_BUFFER: number;
|
|
11
|
-
/**
|
|
12
|
-
* Compute adaptive buffer size for tree-sitter parsing.
|
|
13
|
-
* Uses 2× file size, clamped between 512 KB and 32 MB.
|
|
14
|
-
* Previous 256 KB fixed limit silently skipped files > ~200 KB (e.g., imgui.h at 411 KB).
|
|
15
|
-
*/
|
|
16
|
-
export declare const getTreeSitterBufferSize: (contentLength: number) => number;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default minimum buffer size for tree-sitter parsing (512 KB).
|
|
3
|
-
* tree-sitter requires bufferSize >= file size in bytes.
|
|
4
|
-
*/
|
|
5
|
-
export const TREE_SITTER_BUFFER_SIZE = 512 * 1024;
|
|
6
|
-
/**
|
|
7
|
-
* Maximum buffer size cap (32 MB) to prevent OOM on huge files.
|
|
8
|
-
* Also used as the file-size skip threshold — files larger than this are not parsed.
|
|
9
|
-
*/
|
|
10
|
-
export const TREE_SITTER_MAX_BUFFER = 32 * 1024 * 1024;
|
|
11
|
-
/**
|
|
12
|
-
* Compute adaptive buffer size for tree-sitter parsing.
|
|
13
|
-
* Uses 2× file size, clamped between 512 KB and 32 MB.
|
|
14
|
-
* Previous 256 KB fixed limit silently skipped files > ~200 KB (e.g., imgui.h at 411 KB).
|
|
15
|
-
*/
|
|
16
|
-
export const getTreeSitterBufferSize = (contentLength) => Math.min(Math.max(contentLength * 2, TREE_SITTER_BUFFER_SIZE), TREE_SITTER_MAX_BUFFER);
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Export Detection
|
|
3
|
-
*
|
|
4
|
-
* Determines whether a symbol (function, class, etc.) is exported/public
|
|
5
|
-
* in its language. This is a pure function — safe for use in worker threads.
|
|
6
|
-
*
|
|
7
|
-
* Shared between parse-worker.ts (worker pool) and parsing-processor.ts (sequential fallback).
|
|
8
|
-
*/
|
|
9
|
-
import { SyntaxNode } from './utils.js';
|
|
10
|
-
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
11
|
-
/**
|
|
12
|
-
* Check if a tree-sitter node is exported/public in its language.
|
|
13
|
-
* @param node - The tree-sitter AST node
|
|
14
|
-
* @param name - The symbol name
|
|
15
|
-
* @param language - The programming language
|
|
16
|
-
* @returns true if the symbol is exported/public
|
|
17
|
-
*/
|
|
18
|
-
export declare const isNodeExported: (node: SyntaxNode, name: string, language: SupportedLanguages) => boolean;
|