@veewo/gitnexus 1.3.9 → 1.3.10
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/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze-multi-scope-regression.test.js +1 -1
- package/dist/cli/analyze-options.d.ts +19 -0
- package/dist/cli/analyze-options.js +35 -0
- package/dist/cli/analyze-options.test.js +42 -1
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +53 -26
- package/dist/cli/index.js +1 -0
- package/dist/cli/repo-manager-alias.test.js +24 -1
- package/dist/mcp/resources.js +1 -1
- package/dist/mcp/staleness.js +1 -1
- package/dist/storage/repo-manager.d.ts +6 -0
- package/package.json +3 -3
- package/dist/cli/analyze-custom-modules-regression.test.d.ts +0 -1
- package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
- package/dist/cli/analyze-modules-diagnostics.test.d.ts +0 -1
- package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
- package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
- package/dist/core/ingestion/modules/assignment-engine.js +0 -179
- package/dist/core/ingestion/modules/assignment-engine.test.d.ts +0 -1
- package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
- package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
- package/dist/core/ingestion/modules/config-loader.js +0 -186
- package/dist/core/ingestion/modules/config-loader.test.d.ts +0 -1
- package/dist/core/ingestion/modules/config-loader.test.js +0 -57
- package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
- package/dist/core/ingestion/modules/rule-matcher.js +0 -63
- package/dist/core/ingestion/modules/rule-matcher.test.d.ts +0 -1
- package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
- package/dist/core/ingestion/modules/types.d.ts +0 -44
- package/dist/core/ingestion/modules/types.js +0 -2
- package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
- package/dist/mcp/local/cluster-aggregation.js +0 -48
- package/dist/mcp/local/cluster-aggregation.test.d.ts +0 -1
- package/dist/mcp/local/cluster-aggregation.test.js +0 -22
package/dist/cli/ai-context.js
CHANGED
|
@@ -38,7 +38,7 @@ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} s
|
|
|
38
38
|
2. **Match your task to a skill below** and **read that skill file**
|
|
39
39
|
3. **Follow the skill's workflow and checklist**
|
|
40
40
|
|
|
41
|
-
> If step 1 warns the index is stale,
|
|
41
|
+
> If step 1 warns the index is stale, ask user whether to rebuild index via \`npx -y gitnexus analyze\` first (it reuses previous analyze scope/options by default; add \`--no-reuse-options\` to reset). If user declines, explicitly warn that retrieval may not reflect current codebase. For build/analyze/test commands, use a 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently fall back to glob/grep.
|
|
42
42
|
|
|
43
43
|
## Skills
|
|
44
44
|
|
|
@@ -27,6 +27,6 @@ test('pipeline forwards extension-filtered scoped paths to unity enrich', { time
|
|
|
27
27
|
includeExtensions: ['.cs'],
|
|
28
28
|
scopeRules: ['Assets'],
|
|
29
29
|
});
|
|
30
|
-
assert.
|
|
30
|
+
assert.ok((result.unityResult?.bindingCount || 0) > 0);
|
|
31
31
|
assert.ok(result.unityResult?.diagnostics.some((message) => message.includes('scanContext:')));
|
|
32
32
|
});
|
|
@@ -2,6 +2,25 @@ export interface AnalyzeScopeOptions {
|
|
|
2
2
|
scopeManifest?: string;
|
|
3
3
|
scopePrefix?: string[] | string;
|
|
4
4
|
}
|
|
5
|
+
export interface StoredAnalyzeOptions {
|
|
6
|
+
includeExtensions?: string[];
|
|
7
|
+
scopeRules?: string[];
|
|
8
|
+
repoAlias?: string;
|
|
9
|
+
embeddings?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ResolveAnalyzeOptionsInput extends AnalyzeScopeOptions {
|
|
12
|
+
extensions?: string;
|
|
13
|
+
repoAlias?: string;
|
|
14
|
+
embeddings?: boolean;
|
|
15
|
+
reuseOptions?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface EffectiveAnalyzeOptions {
|
|
18
|
+
includeExtensions: string[];
|
|
19
|
+
scopeRules: string[];
|
|
20
|
+
repoAlias?: string;
|
|
21
|
+
embeddings: boolean;
|
|
22
|
+
}
|
|
5
23
|
export declare function parseExtensionList(rawExtensions?: string): string[];
|
|
6
24
|
export declare function normalizeRepoAlias(repoAlias?: string): string | undefined;
|
|
7
25
|
export declare function resolveAnalyzeScopeRules(options?: AnalyzeScopeOptions): Promise<string[]>;
|
|
26
|
+
export declare function resolveEffectiveAnalyzeOptions(options?: ResolveAnalyzeOptionsInput, stored?: StoredAnalyzeOptions): Promise<EffectiveAnalyzeOptions>;
|
|
@@ -54,3 +54,38 @@ export async function resolveAnalyzeScopeRules(options) {
|
|
|
54
54
|
}
|
|
55
55
|
return normalizedRules;
|
|
56
56
|
}
|
|
57
|
+
function parseScopePrefixCount(scopePrefix) {
|
|
58
|
+
if (Array.isArray(scopePrefix))
|
|
59
|
+
return scopePrefix.length;
|
|
60
|
+
if (typeof scopePrefix === 'string')
|
|
61
|
+
return scopePrefix.trim() ? 1 : 0;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
export async function resolveEffectiveAnalyzeOptions(options, stored) {
|
|
65
|
+
const includeExtensionsFromCli = parseExtensionList(options?.extensions);
|
|
66
|
+
const scopeRulesFromCli = await resolveAnalyzeScopeRules({
|
|
67
|
+
scopeManifest: options?.scopeManifest,
|
|
68
|
+
scopePrefix: options?.scopePrefix,
|
|
69
|
+
});
|
|
70
|
+
const repoAliasFromCli = normalizeRepoAlias(options?.repoAlias);
|
|
71
|
+
const hasCliExtensions = options?.extensions !== undefined;
|
|
72
|
+
const hasCliScope = Boolean(options?.scopeManifest) || parseScopePrefixCount(options?.scopePrefix) > 0;
|
|
73
|
+
const hasCliRepoAlias = options?.repoAlias !== undefined;
|
|
74
|
+
const canReuse = options?.reuseOptions !== false;
|
|
75
|
+
const includeExtensions = hasCliExtensions
|
|
76
|
+
? includeExtensionsFromCli
|
|
77
|
+
: (canReuse ? (stored?.includeExtensions || []) : []);
|
|
78
|
+
const scopeRules = hasCliScope
|
|
79
|
+
? scopeRulesFromCli
|
|
80
|
+
: (canReuse ? (stored?.scopeRules || []) : []);
|
|
81
|
+
const repoAlias = hasCliRepoAlias
|
|
82
|
+
? repoAliasFromCli
|
|
83
|
+
: (canReuse ? normalizeRepoAlias(stored?.repoAlias) : undefined);
|
|
84
|
+
const embeddings = options?.embeddings ?? (canReuse ? Boolean(stored?.embeddings) : false);
|
|
85
|
+
return {
|
|
86
|
+
includeExtensions: [...includeExtensions],
|
|
87
|
+
scopeRules: [...scopeRules],
|
|
88
|
+
repoAlias,
|
|
89
|
+
embeddings,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { normalizeRepoAlias, parseExtensionList, resolveAnalyzeScopeRules } from './analyze-options.js';
|
|
6
|
+
import { normalizeRepoAlias, parseExtensionList, resolveAnalyzeScopeRules, resolveEffectiveAnalyzeOptions, } from './analyze-options.js';
|
|
7
7
|
test('parseExtensionList normalizes dot prefixes', () => {
|
|
8
8
|
const exts = parseExtensionList('cs,.ts, go ');
|
|
9
9
|
assert.deepEqual(exts, ['.cs', '.ts', '.go']);
|
|
@@ -34,3 +34,44 @@ test('resolveAnalyzeScopeRules fails when manifest has no usable rule', async ()
|
|
|
34
34
|
await fs.writeFile(manifestPath, '# only comments\n\n', 'utf-8');
|
|
35
35
|
await assert.rejects(resolveAnalyzeScopeRules({ scopeManifest: manifestPath }), /no valid scope rules/i);
|
|
36
36
|
});
|
|
37
|
+
test('resolveEffectiveAnalyzeOptions reuses stored settings when CLI omits them', async () => {
|
|
38
|
+
const resolved = await resolveEffectiveAnalyzeOptions({}, {
|
|
39
|
+
includeExtensions: ['.cs'],
|
|
40
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
41
|
+
repoAlias: 'neonspark-v1-subset',
|
|
42
|
+
embeddings: true,
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(resolved.includeExtensions, ['.cs']);
|
|
45
|
+
assert.deepEqual(resolved.scopeRules, ['Assets/NEON/Code']);
|
|
46
|
+
assert.equal(resolved.repoAlias, 'neonspark-v1-subset');
|
|
47
|
+
assert.equal(resolved.embeddings, true);
|
|
48
|
+
});
|
|
49
|
+
test('resolveEffectiveAnalyzeOptions disables reuse via reuseOptions=false', async () => {
|
|
50
|
+
const resolved = await resolveEffectiveAnalyzeOptions({ reuseOptions: false }, {
|
|
51
|
+
includeExtensions: ['.cs'],
|
|
52
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
53
|
+
repoAlias: 'neonspark-v1-subset',
|
|
54
|
+
embeddings: true,
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(resolved.includeExtensions, []);
|
|
57
|
+
assert.deepEqual(resolved.scopeRules, []);
|
|
58
|
+
assert.equal(resolved.repoAlias, undefined);
|
|
59
|
+
assert.equal(resolved.embeddings, false);
|
|
60
|
+
});
|
|
61
|
+
test('resolveEffectiveAnalyzeOptions prefers explicit CLI values over stored settings', async () => {
|
|
62
|
+
const resolved = await resolveEffectiveAnalyzeOptions({
|
|
63
|
+
extensions: '.ts',
|
|
64
|
+
scopePrefix: ['src'],
|
|
65
|
+
repoAlias: 'new-alias',
|
|
66
|
+
embeddings: false,
|
|
67
|
+
}, {
|
|
68
|
+
includeExtensions: ['.cs'],
|
|
69
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
70
|
+
repoAlias: 'old-alias',
|
|
71
|
+
embeddings: true,
|
|
72
|
+
});
|
|
73
|
+
assert.deepEqual(resolved.includeExtensions, ['.ts']);
|
|
74
|
+
assert.deepEqual(resolved.scopeRules, ['src']);
|
|
75
|
+
assert.equal(resolved.repoAlias, 'new-alias');
|
|
76
|
+
assert.equal(resolved.embeddings, false);
|
|
77
|
+
});
|
package/dist/cli/analyze.d.ts
CHANGED
package/dist/cli/analyze.js
CHANGED
|
@@ -18,7 +18,7 @@ import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
|
18
18
|
import { generateAIContextFiles } from './ai-context.js';
|
|
19
19
|
import fs from 'fs/promises';
|
|
20
20
|
import { registerClaudeHook } from './claude-hooks.js';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveEffectiveAnalyzeOptions } from './analyze-options.js';
|
|
22
22
|
import { formatFallbackSummary, formatUnityDiagnosticsSummary } from './analyze-summary.js';
|
|
23
23
|
const HEAP_MB = 8192;
|
|
24
24
|
const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
|
|
@@ -62,22 +62,6 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
62
62
|
if (ensureHeap())
|
|
63
63
|
return;
|
|
64
64
|
console.log('\n GitNexus Analyzer\n');
|
|
65
|
-
let includeExtensions = [];
|
|
66
|
-
let scopeRules = [];
|
|
67
|
-
let repoAlias;
|
|
68
|
-
try {
|
|
69
|
-
includeExtensions = parseExtensionList(options?.extensions);
|
|
70
|
-
scopeRules = await resolveAnalyzeScopeRules({
|
|
71
|
-
scopeManifest: options?.scopeManifest,
|
|
72
|
-
scopePrefix: options?.scopePrefix,
|
|
73
|
-
});
|
|
74
|
-
repoAlias = normalizeRepoAlias(options?.repoAlias);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
console.log(` ${error?.message || String(error)}\n`);
|
|
78
|
-
process.exitCode = 1;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
65
|
let repoPath;
|
|
82
66
|
if (inputPath) {
|
|
83
67
|
repoPath = path.resolve(inputPath);
|
|
@@ -99,15 +83,52 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
99
83
|
const { storagePath, kuzuPath } = getStoragePaths(repoPath);
|
|
100
84
|
const currentCommit = getCurrentCommit(repoPath);
|
|
101
85
|
const existingMeta = await loadMeta(storagePath);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
86
|
+
let includeExtensions = [];
|
|
87
|
+
let scopeRules = [];
|
|
88
|
+
let repoAlias;
|
|
89
|
+
let embeddingsEnabled = false;
|
|
90
|
+
try {
|
|
91
|
+
const effectiveOptions = await resolveEffectiveAnalyzeOptions({
|
|
92
|
+
extensions: options?.extensions,
|
|
93
|
+
scopeManifest: options?.scopeManifest,
|
|
94
|
+
scopePrefix: options?.scopePrefix,
|
|
95
|
+
repoAlias: options?.repoAlias,
|
|
96
|
+
embeddings: options?.embeddings,
|
|
97
|
+
reuseOptions: options?.reuseOptions,
|
|
98
|
+
}, existingMeta?.analyzeOptions);
|
|
99
|
+
includeExtensions = effectiveOptions.includeExtensions;
|
|
100
|
+
scopeRules = effectiveOptions.scopeRules;
|
|
101
|
+
repoAlias = effectiveOptions.repoAlias;
|
|
102
|
+
embeddingsEnabled = effectiveOptions.embeddings;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.log(` ${error?.message || String(error)}\n`);
|
|
106
|
+
process.exitCode = 1;
|
|
109
107
|
return;
|
|
110
108
|
}
|
|
109
|
+
if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit) {
|
|
110
|
+
const hasScopePrefixInput = Array.isArray(options?.scopePrefix)
|
|
111
|
+
? options.scopePrefix.length > 0
|
|
112
|
+
: Boolean(options?.scopePrefix);
|
|
113
|
+
const hasCliOverrides = options?.extensions !== undefined ||
|
|
114
|
+
Boolean(options?.scopeManifest) ||
|
|
115
|
+
hasScopePrefixInput ||
|
|
116
|
+
options?.repoAlias !== undefined ||
|
|
117
|
+
options?.embeddings !== undefined ||
|
|
118
|
+
options?.reuseOptions === false;
|
|
119
|
+
if (!hasCliOverrides) {
|
|
120
|
+
console.log(' Already up to date\n');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (options?.reuseOptions !== false &&
|
|
124
|
+
includeExtensions.length === 0 &&
|
|
125
|
+
scopeRules.length === 0 &&
|
|
126
|
+
!repoAlias &&
|
|
127
|
+
!embeddingsEnabled) {
|
|
128
|
+
console.log(' Already up to date\n');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
111
132
|
// Single progress bar for entire pipeline
|
|
112
133
|
const bar = new cliProgress.SingleBar({
|
|
113
134
|
format: ' {bar} {percentage}% | {phase}',
|
|
@@ -171,7 +192,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
171
192
|
// ── Cache embeddings from existing index before rebuild ────────────
|
|
172
193
|
let cachedEmbeddingNodeIds = new Set();
|
|
173
194
|
let cachedEmbeddings = [];
|
|
174
|
-
if (
|
|
195
|
+
if (embeddingsEnabled && existingMeta && !options?.force) {
|
|
175
196
|
try {
|
|
176
197
|
updateBar(0, 'Caching embeddings...');
|
|
177
198
|
await initKuzu(kuzuPath);
|
|
@@ -259,7 +280,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
259
280
|
let embeddingTime = '0.0';
|
|
260
281
|
let embeddingSkipped = true;
|
|
261
282
|
let embeddingSkipReason = 'off (use --embeddings to enable)';
|
|
262
|
-
if (
|
|
283
|
+
if (embeddingsEnabled) {
|
|
263
284
|
if (stats.nodes > EMBEDDING_NODE_LIMIT) {
|
|
264
285
|
embeddingSkipReason = `skipped (${stats.nodes.toLocaleString()} nodes > ${EMBEDDING_NODE_LIMIT.toLocaleString()} limit)`;
|
|
265
286
|
}
|
|
@@ -284,6 +305,12 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
284
305
|
repoPath,
|
|
285
306
|
lastCommit: currentCommit,
|
|
286
307
|
indexedAt: new Date().toISOString(),
|
|
308
|
+
analyzeOptions: {
|
|
309
|
+
includeExtensions,
|
|
310
|
+
scopeRules,
|
|
311
|
+
repoAlias,
|
|
312
|
+
embeddings: embeddingsEnabled,
|
|
313
|
+
},
|
|
287
314
|
stats: {
|
|
288
315
|
files: pipelineResult.totalFileCount,
|
|
289
316
|
nodes: stats.nodes,
|
package/dist/cli/index.js
CHANGED
|
@@ -69,6 +69,7 @@ program
|
|
|
69
69
|
.command('analyze [path]')
|
|
70
70
|
.description('Index a repository (full analysis)')
|
|
71
71
|
.option('-f, --force', 'Force full re-index even if up to date')
|
|
72
|
+
.option('--no-reuse-options', 'Do not reuse stored analyze options from previous index')
|
|
72
73
|
.option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
|
|
73
74
|
.option('--extensions <list>', 'Comma-separated file extensions to include (e.g. .cs,.ts)')
|
|
74
75
|
.option('--repo-alias <name>', 'Override indexed repository name with a stable alias')
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { readRegistry, registerRepo } from '../storage/repo-manager.js';
|
|
6
|
+
import { loadMeta, readRegistry, registerRepo, saveMeta } from '../storage/repo-manager.js';
|
|
7
7
|
function makeMeta(repoPath, lastCommit) {
|
|
8
8
|
return {
|
|
9
9
|
repoPath,
|
|
@@ -38,3 +38,26 @@ test('registerRepo stores alias and rejects collisions on different paths', asyn
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
|
+
test('saveMeta/loadMeta persists analyzeOptions for future re-index reuse', async () => {
|
|
42
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-meta-'));
|
|
43
|
+
const storagePath = path.join(tmpDir, '.gitnexus');
|
|
44
|
+
await saveMeta(storagePath, {
|
|
45
|
+
repoPath: tmpDir,
|
|
46
|
+
lastCommit: 'abc1234',
|
|
47
|
+
indexedAt: '2026-03-12T00:00:00.000Z',
|
|
48
|
+
analyzeOptions: {
|
|
49
|
+
includeExtensions: ['.cs'],
|
|
50
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
51
|
+
repoAlias: 'neonspark-v1-subset',
|
|
52
|
+
embeddings: true,
|
|
53
|
+
},
|
|
54
|
+
stats: { files: 1, nodes: 2, edges: 3 },
|
|
55
|
+
});
|
|
56
|
+
const meta = await loadMeta(storagePath);
|
|
57
|
+
assert.deepEqual(meta?.analyzeOptions, {
|
|
58
|
+
includeExtensions: ['.cs'],
|
|
59
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
60
|
+
repoAlias: 'neonspark-v1-subset',
|
|
61
|
+
embeddings: true,
|
|
62
|
+
});
|
|
63
|
+
});
|
package/dist/mcp/resources.js
CHANGED
|
@@ -186,7 +186,7 @@ async function getContextResource(backend, repoName) {
|
|
|
186
186
|
lines.push(' - cypher: Raw graph queries');
|
|
187
187
|
lines.push(' - list_repos: Discover all indexed repositories');
|
|
188
188
|
lines.push('');
|
|
189
|
-
lines.push('re_index:
|
|
189
|
+
lines.push('re_index: If data is stale, ask user whether to run `npx -y gitnexus analyze` (reuses previous analyze scope/options unless `--no-reuse-options` is passed). If user declines, clearly state retrieval may not reflect current code. For build/analyze/test commands, use 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently switch to glob/grep fallback.');
|
|
190
190
|
lines.push('');
|
|
191
191
|
lines.push('resources_available:');
|
|
192
192
|
lines.push(' - gitnexus://repos: All indexed repositories');
|
package/dist/mcp/staleness.js
CHANGED
|
@@ -17,7 +17,7 @@ export function checkStaleness(repoPath, lastCommit) {
|
|
|
17
17
|
return {
|
|
18
18
|
isStale: true,
|
|
19
19
|
commitsBehind,
|
|
20
|
-
hint: `⚠️ Index is ${commitsBehind} commit${commitsBehind > 1 ? 's' : ''} behind HEAD.
|
|
20
|
+
hint: `⚠️ Index is ${commitsBehind} commit${commitsBehind > 1 ? 's' : ''} behind HEAD. Ask user whether to run analyze to rebuild index; if not rebuilt, retrieval may not match current code.`,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
return { isStale: false, commitsBehind: 0 };
|
|
@@ -9,6 +9,12 @@ export interface RepoMeta {
|
|
|
9
9
|
repoPath: string;
|
|
10
10
|
lastCommit: string;
|
|
11
11
|
indexedAt: string;
|
|
12
|
+
analyzeOptions?: {
|
|
13
|
+
includeExtensions?: string[];
|
|
14
|
+
scopeRules?: string[];
|
|
15
|
+
repoAlias?: string;
|
|
16
|
+
embeddings?: boolean;
|
|
17
|
+
};
|
|
12
18
|
stats?: {
|
|
13
19
|
files?: number;
|
|
14
20
|
nodes?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veewo/gitnexus",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"vendor"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"build": "tsc",
|
|
39
|
+
"build": "rm -rf dist && tsc",
|
|
40
40
|
"dev": "tsx watch src/cli/index.ts",
|
|
41
41
|
"prepare": "npm run build",
|
|
42
42
|
"check:release-paths": "node hooks/check-release-path-hygiene.mjs",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"check:u2-e2e-target": "node -e \"const p=(process.env.GITNEXUS_U2_E2E_TARGET_PATH||'').trim();if(!p){console.error('Missing env: GITNEXUS_U2_E2E_TARGET_PATH');process.exit(1);}\"",
|
|
45
45
|
"test:unity": "npm run build && node --test dist/core/unity/*.test.js",
|
|
46
46
|
"test:u3:gates": "npm run check:release-paths && npm run build && node --test dist/benchmark/u2-e2e/*.test.js dist/mcp/local/unity-enrichment.test.js dist/core/ingestion/unity-resource-processor.test.js",
|
|
47
|
-
"test:benchmark": "npm run build && node --test dist/benchmark/*.test.js dist/cli/*.test.js",
|
|
47
|
+
"test:benchmark": "npm run build && node --test dist/benchmark/*.test.js && node --test dist/cli/*.test.js",
|
|
48
48
|
"benchmark:quick": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/v1 --profile quick --target-path ../benchmarks/fixtures/unity-mini",
|
|
49
49
|
"benchmark:full": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/v1 --profile full --target-path ../benchmarks/fixtures/unity-mini",
|
|
50
50
|
"benchmark:neonspark:full": "npm run check:neonspark-target && npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v1 --profile full --target-path \"$GITNEXUS_NEONSPARK_TARGET_PATH\" --repo-alias neonspark-v1-subset --scope-manifest ../benchmarks/unity-baseline/neonspark-v1/sync-manifest.txt",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
|
7
|
-
const FIXTURE_REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../benchmarks/fixtures/unity-mini');
|
|
8
|
-
async function copyFixtureRepo() {
|
|
9
|
-
const tmpRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-custom-modules-'));
|
|
10
|
-
await fs.cp(FIXTURE_REPO, tmpRepo, { recursive: true });
|
|
11
|
-
await fs.mkdir(path.join(tmpRepo, '.gitnexus'), { recursive: true });
|
|
12
|
-
return tmpRepo;
|
|
13
|
-
}
|
|
14
|
-
test('pipeline mixed mode writes config module + auto fallback memberships', { timeout: 120_000 }, async () => {
|
|
15
|
-
const repoPath = await copyFixtureRepo();
|
|
16
|
-
const configPath = path.join(repoPath, '.gitnexus', 'modules.json');
|
|
17
|
-
await fs.writeFile(configPath, JSON.stringify({
|
|
18
|
-
version: 1,
|
|
19
|
-
mode: 'mixed',
|
|
20
|
-
modules: [
|
|
21
|
-
{
|
|
22
|
-
name: 'Factory',
|
|
23
|
-
defaultPriority: 100,
|
|
24
|
-
rules: [
|
|
25
|
-
{
|
|
26
|
-
id: 'factory-file-rule',
|
|
27
|
-
when: {
|
|
28
|
-
all: [{ field: 'file.path', op: 'contains', value: 'MinionFactory.cs' }],
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: 'Battle',
|
|
35
|
-
defaultPriority: 100,
|
|
36
|
-
rules: [],
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
}), 'utf-8');
|
|
40
|
-
const result = await runPipelineFromRepo(repoPath, () => { }, { includeExtensions: ['.cs'] });
|
|
41
|
-
const communities = [...result.graph.iterNodes()].filter((n) => n.label === 'Community');
|
|
42
|
-
const memberships = [...result.graph.iterRelationships()].filter((r) => r.type === 'MEMBER_OF');
|
|
43
|
-
const commById = new Map(communities.map((c) => [c.id, c]));
|
|
44
|
-
const labels = communities.map((c) => String(c.properties.heuristicLabel || c.properties.name || c.id));
|
|
45
|
-
assert.ok(labels.includes('Factory'));
|
|
46
|
-
assert.ok(labels.includes('Battle'));
|
|
47
|
-
const createMembership = memberships.find((m) => m.sourceId.includes('MinionFactory.cs:Create'));
|
|
48
|
-
assert.ok(createMembership);
|
|
49
|
-
const targetCommunity = commById.get(createMembership.targetId);
|
|
50
|
-
assert.ok(targetCommunity);
|
|
51
|
-
assert.equal(String(targetCommunity.properties.heuristicLabel), 'Factory');
|
|
52
|
-
const uniquePerSymbol = new Set(memberships.map((m) => m.sourceId));
|
|
53
|
-
assert.equal(uniquePerSymbol.size, memberships.length);
|
|
54
|
-
const battleCommunity = communities.find((c) => String(c.properties.heuristicLabel) === 'Battle');
|
|
55
|
-
assert.ok(battleCommunity);
|
|
56
|
-
assert.ok(!memberships.some((m) => m.targetId === battleCommunity.id));
|
|
57
|
-
const membershipCommunities = new Set(memberships.map((m) => m.targetId));
|
|
58
|
-
const processCommunities = new Set((result.processResult?.processes || []).flatMap((p) => p.communities || []));
|
|
59
|
-
for (const processCommunity of processCommunities) {
|
|
60
|
-
assert.ok(membershipCommunities.has(processCommunity));
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
test('pipeline mixed + invalid modules.json fails fast', { timeout: 120_000 }, async () => {
|
|
64
|
-
const repoPath = await copyFixtureRepo();
|
|
65
|
-
const configPath = path.join(repoPath, '.gitnexus', 'modules.json');
|
|
66
|
-
await fs.writeFile(configPath, JSON.stringify({
|
|
67
|
-
version: 1,
|
|
68
|
-
mode: 'mixed',
|
|
69
|
-
modules: [
|
|
70
|
-
{ name: 'Dup', defaultPriority: 100, rules: [] },
|
|
71
|
-
{ name: 'Dup', defaultPriority: 100, rules: [] },
|
|
72
|
-
],
|
|
73
|
-
}), 'utf-8');
|
|
74
|
-
await assert.rejects(runPipelineFromRepo(repoPath, () => { }, { includeExtensions: ['.cs'] }), /duplicate module name/i);
|
|
75
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { formatModuleDiagnostics } from './analyze.js';
|
|
7
|
-
test('prints one-time fallback warning for mixed + missing modules.json', () => {
|
|
8
|
-
const lines = formatModuleDiagnostics({
|
|
9
|
-
mode: 'mixed',
|
|
10
|
-
usedFallbackAuto: true,
|
|
11
|
-
warnings: ['modules.json missing in mixed mode, fallback to auto'],
|
|
12
|
-
emptyModules: [],
|
|
13
|
-
configuredModuleCount: 0,
|
|
14
|
-
finalModuleCount: 3,
|
|
15
|
-
});
|
|
16
|
-
assert.ok(lines.some((line) => /fallback to auto/i.test(line)));
|
|
17
|
-
});
|
|
18
|
-
test('prints warning for empty modules', () => {
|
|
19
|
-
const lines = formatModuleDiagnostics({
|
|
20
|
-
mode: 'mixed',
|
|
21
|
-
usedFallbackAuto: false,
|
|
22
|
-
warnings: [],
|
|
23
|
-
emptyModules: ['Battle'],
|
|
24
|
-
configuredModuleCount: 1,
|
|
25
|
-
finalModuleCount: 2,
|
|
26
|
-
});
|
|
27
|
-
assert.ok(lines.some((line) => /empty module/i.test(line)));
|
|
28
|
-
assert.ok(lines.some((line) => /Battle/.test(line)));
|
|
29
|
-
});
|
|
30
|
-
test('README mentions modules.json mode semantics and fallback behavior', async () => {
|
|
31
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
const readmePath = path.resolve(here, '../../README.md');
|
|
33
|
-
const readme = await fs.readFile(readmePath, 'utf-8');
|
|
34
|
-
assert.match(readme, /\.gitnexus\/modules\.json/);
|
|
35
|
-
assert.match(readme, /mixed.*fallback.*auto/i);
|
|
36
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { CommunityMembership, CommunityNode } from '../community-processor.js';
|
|
2
|
-
import type { ModuleConfig, ModuleMode } from './types.js';
|
|
3
|
-
import { type MatchableSymbol } from './rule-matcher.js';
|
|
4
|
-
export type AssignmentSource = 'config-rule' | 'auto-fallback';
|
|
5
|
-
export type AssignmentResolvedBy = 'priority' | 'specificity' | 'rule-order' | 'module-lexicographic' | 'fallback-auto';
|
|
6
|
-
export interface AssignmentInput {
|
|
7
|
-
mode: ModuleMode;
|
|
8
|
-
symbols: MatchableSymbol[];
|
|
9
|
-
autoCommunities: CommunityNode[];
|
|
10
|
-
autoMemberships: CommunityMembership[];
|
|
11
|
-
config: ModuleConfig | null;
|
|
12
|
-
}
|
|
13
|
-
export interface SymbolAssignment {
|
|
14
|
-
symbolId: string;
|
|
15
|
-
moduleName: string;
|
|
16
|
-
communityId: string;
|
|
17
|
-
assignmentSource: AssignmentSource;
|
|
18
|
-
matchedRuleId?: string;
|
|
19
|
-
resolvedBy: AssignmentResolvedBy;
|
|
20
|
-
}
|
|
21
|
-
export interface AssignmentDiagnostics {
|
|
22
|
-
mode: ModuleMode;
|
|
23
|
-
configuredModuleCount: number;
|
|
24
|
-
finalModuleCount: number;
|
|
25
|
-
emptyModules: string[];
|
|
26
|
-
}
|
|
27
|
-
export interface AssignmentOutput {
|
|
28
|
-
finalModules: CommunityNode[];
|
|
29
|
-
finalMemberships: CommunityMembership[];
|
|
30
|
-
membershipsBySymbol: Map<string, SymbolAssignment>;
|
|
31
|
-
diagnostics: AssignmentDiagnostics;
|
|
32
|
-
}
|
|
33
|
-
export declare function assignModules(input: AssignmentInput): AssignmentOutput;
|