@veewo/gitnexus 1.3.11 → 1.4.7-rc
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 +37 -80
- package/dist/benchmark/agent-context/tool-runner.js +2 -2
- package/dist/benchmark/neonspark-candidates.js +3 -3
- package/dist/benchmark/tool-runner.js +2 -2
- package/dist/cli/ai-context.d.ts +2 -1
- package/dist/cli/ai-context.js +16 -12
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +68 -48
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +8 -1
- package/dist/cli/eval-server.js +30 -13
- package/dist/cli/index.js +28 -82
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/mcp.js +3 -1
- package/dist/cli/setup.js +87 -48
- package/dist/cli/setup.test.js +18 -13
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +50 -16
- package/dist/cli/wiki.js +8 -4
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +4 -1
- package/dist/config/supported-languages.js +3 -2
- package/dist/core/augmentation/engine.js +94 -67
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +52 -25
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +7 -2
- package/dist/core/ingestion/ast-cache.js +3 -2
- package/dist/core/ingestion/call-processor.d.ts +8 -6
- package/dist/core/ingestion/call-processor.js +468 -206
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
- package/dist/core/ingestion/entry-point-scoring.js +116 -23
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.d.ts +19 -4
- package/dist/core/ingestion/framework-detection.js +182 -6
- package/dist/core/ingestion/heritage-processor.d.ts +13 -5
- package/dist/core/ingestion/heritage-processor.js +109 -55
- package/dist/core/ingestion/import-processor.d.ts +16 -20
- package/dist/core/ingestion/import-processor.js +199 -579
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +4 -1
- package/dist/core/ingestion/parsing-processor.js +107 -109
- package/dist/core/ingestion/pipeline.d.ts +6 -3
- package/dist/core/ingestion/pipeline.js +208 -114
- package/dist/core/ingestion/process-processor.js +8 -2
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/symbol-table.d.ts +21 -1
- package/dist/core/ingestion/symbol-table.js +40 -12
- package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
- package/dist/core/ingestion/tree-sitter-queries.js +297 -7
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +611 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +406 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +449 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
- package/dist/core/ingestion/type-extractors/shared.js +703 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +103 -0
- package/dist/core/ingestion/utils.js +1085 -4
- package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
- package/dist/core/ingestion/workers/parse-worker.js +634 -222
- package/dist/core/ingestion/workers/worker-pool.js +8 -0
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
- package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
- package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
- package/dist/core/{kuzu → lbug}/schema.js +23 -22
- package/dist/core/lbug/schema.test.d.ts +1 -0
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +12 -11
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +6 -6
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
- package/dist/core/tree-sitter/parser-loader.js +19 -0
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +6 -6
- package/dist/core/wiki/graph-queries.d.ts +4 -4
- package/dist/core/wiki/graph-queries.js +7 -7
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
- package/dist/mcp/core/lbug-adapter.js +327 -0
- package/dist/mcp/local/local-backend.d.ts +21 -16
- package/dist/mcp/local/local-backend.js +306 -706
- package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
- package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
- package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
- package/dist/mcp/resources.js +2 -2
- package/dist/mcp/server.js +28 -13
- package/dist/mcp/staleness.js +2 -2
- package/dist/mcp/tools.js +12 -3
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/git.js +4 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +74 -4
- package/dist/types/pipeline.d.ts +1 -1
- package/hooks/claude/gitnexus-hook.cjs +149 -46
- package/hooks/claude/pre-tool-use.sh +2 -1
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +20 -4
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/skills/gitnexus-cli.md +8 -8
- package/skills/gitnexus-debugging.md +1 -1
- package/skills/gitnexus-exploring.md +1 -1
- package/skills/gitnexus-guide.md +1 -1
- package/skills/gitnexus-impact-analysis.md +1 -1
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +1 -1
- package/dist/cli/claude-hooks.d.ts +0 -22
- package/dist/cli/claude-hooks.js +0 -97
- package/dist/mcp/core/kuzu-adapter.js +0 -231
- /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
- /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
- /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { UnityParitySeed } from '../../core/ingestion/unity-parity-seed.js';
|
|
2
|
-
|
|
2
|
+
interface LoadUnityParitySeedOptions {
|
|
3
|
+
indexedCommit?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function loadUnityParitySeed(storagePath: string, options?: LoadUnityParitySeedOptions): Promise<UnityParitySeed | null>;
|
|
6
|
+
export declare function __resetUnityParitySeedLoaderCacheForTest(): void;
|
|
7
|
+
export {};
|
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const SEED_FILENAME = 'unity-parity-seed.json';
|
|
4
|
-
|
|
4
|
+
const DEFAULT_IDLE_MS = 30_000;
|
|
5
|
+
const DEFAULT_MAX_ENTRIES = 2;
|
|
6
|
+
const seedCache = new Map();
|
|
7
|
+
const inFlightLoads = new Map();
|
|
8
|
+
export async function loadUnityParitySeed(storagePath, options) {
|
|
5
9
|
const seedPath = path.join(storagePath, SEED_FILENAME);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
const cacheKey = await buildSeedCacheKey(seedPath, storagePath, options?.indexedCommit);
|
|
11
|
+
if (!cacheKey) {
|
|
12
|
+
return null;
|
|
9
13
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const cached = seedCache.get(cacheKey);
|
|
15
|
+
if (cached) {
|
|
16
|
+
touchCacheEntry(cacheKey, cached);
|
|
17
|
+
return cached.value;
|
|
18
|
+
}
|
|
19
|
+
const pending = inFlightLoads.get(cacheKey);
|
|
20
|
+
if (pending) {
|
|
21
|
+
return pending;
|
|
15
22
|
}
|
|
23
|
+
const loadPromise = (async () => {
|
|
24
|
+
let raw = '';
|
|
25
|
+
try {
|
|
26
|
+
raw = await fs.readFile(seedPath, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const parsed = parseSeed(raw);
|
|
35
|
+
setCacheEntry(cacheKey, parsed);
|
|
36
|
+
return parsed;
|
|
37
|
+
})().finally(() => {
|
|
38
|
+
inFlightLoads.delete(cacheKey);
|
|
39
|
+
});
|
|
40
|
+
inFlightLoads.set(cacheKey, loadPromise);
|
|
41
|
+
return loadPromise;
|
|
42
|
+
}
|
|
43
|
+
function parseSeed(raw) {
|
|
16
44
|
try {
|
|
17
45
|
const parsed = JSON.parse(raw);
|
|
18
46
|
if (!parsed
|
|
@@ -28,3 +56,85 @@ export async function loadUnityParitySeed(storagePath) {
|
|
|
28
56
|
return null;
|
|
29
57
|
}
|
|
30
58
|
}
|
|
59
|
+
async function buildSeedCacheKey(seedPath, storagePath, indexedCommit) {
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.stat(seedPath);
|
|
62
|
+
const commitKey = String(indexedCommit || '').trim() || 'no-commit';
|
|
63
|
+
return `${storagePath}::${commitKey}::${Math.trunc(stat.mtimeMs)}`;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code === 'ENOENT') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function setCacheEntry(cacheKey, value) {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const existing = seedCache.get(cacheKey);
|
|
75
|
+
if (existing?.idleTimer) {
|
|
76
|
+
clearTimeout(existing.idleTimer);
|
|
77
|
+
}
|
|
78
|
+
const entry = {
|
|
79
|
+
value,
|
|
80
|
+
lastAccessMs: now,
|
|
81
|
+
};
|
|
82
|
+
seedCache.set(cacheKey, entry);
|
|
83
|
+
scheduleEviction(cacheKey, entry);
|
|
84
|
+
pruneOldestEntries(resolveMaxEntries());
|
|
85
|
+
}
|
|
86
|
+
function touchCacheEntry(cacheKey, entry) {
|
|
87
|
+
entry.lastAccessMs = Date.now();
|
|
88
|
+
if (entry.idleTimer) {
|
|
89
|
+
clearTimeout(entry.idleTimer);
|
|
90
|
+
}
|
|
91
|
+
scheduleEviction(cacheKey, entry);
|
|
92
|
+
}
|
|
93
|
+
function scheduleEviction(cacheKey, entry) {
|
|
94
|
+
const idleMs = resolveIdleMs();
|
|
95
|
+
entry.idleTimer = setTimeout(() => {
|
|
96
|
+
const current = seedCache.get(cacheKey);
|
|
97
|
+
if (!current || current !== entry) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
seedCache.delete(cacheKey);
|
|
101
|
+
}, idleMs);
|
|
102
|
+
entry.idleTimer.unref?.();
|
|
103
|
+
}
|
|
104
|
+
function pruneOldestEntries(maxEntries) {
|
|
105
|
+
if (seedCache.size <= maxEntries) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const rows = [...seedCache.entries()].sort((left, right) => left[1].lastAccessMs - right[1].lastAccessMs);
|
|
109
|
+
const removeCount = rows.length - maxEntries;
|
|
110
|
+
for (let index = 0; index < removeCount; index += 1) {
|
|
111
|
+
const [key, entry] = rows[index];
|
|
112
|
+
if (entry.idleTimer) {
|
|
113
|
+
clearTimeout(entry.idleTimer);
|
|
114
|
+
}
|
|
115
|
+
seedCache.delete(key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function resolveIdleMs() {
|
|
119
|
+
const parsed = Number.parseInt(String(process.env.GITNEXUS_UNITY_PARITY_SEED_CACHE_IDLE_MS || '').trim(), 10);
|
|
120
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
return DEFAULT_IDLE_MS;
|
|
124
|
+
}
|
|
125
|
+
function resolveMaxEntries() {
|
|
126
|
+
const parsed = Number.parseInt(String(process.env.GITNEXUS_UNITY_PARITY_SEED_CACHE_MAX_ENTRIES || '').trim(), 10);
|
|
127
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
return DEFAULT_MAX_ENTRIES;
|
|
131
|
+
}
|
|
132
|
+
export function __resetUnityParitySeedLoaderCacheForTest() {
|
|
133
|
+
for (const entry of seedCache.values()) {
|
|
134
|
+
if (entry.idleTimer) {
|
|
135
|
+
clearTimeout(entry.idleTimer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
seedCache.clear();
|
|
139
|
+
inFlightLoads.clear();
|
|
140
|
+
}
|
|
@@ -3,23 +3,111 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
|
-
import { loadUnityParitySeed } from './unity-parity-seed-loader.js';
|
|
6
|
+
import { __resetUnityParitySeedLoaderCacheForTest, loadUnityParitySeed } from './unity-parity-seed-loader.js';
|
|
7
|
+
const baseSeed = {
|
|
8
|
+
version: 1,
|
|
9
|
+
symbolToScriptPath: { DoorObj: 'Assets/Code/DoorObj.cs' },
|
|
10
|
+
scriptPathToGuid: { 'Assets/Code/DoorObj.cs': 'abc123abc123abc123abc123abc123ab' },
|
|
11
|
+
guidToResourcePaths: { abc123abc123abc123abc123abc123ab: ['Assets/Prefabs/Door.prefab'] },
|
|
12
|
+
};
|
|
13
|
+
async function writeSeed(storagePath, symbol = 'DoorObj') {
|
|
14
|
+
await fs.writeFile(path.join(storagePath, 'unity-parity-seed.json'), JSON.stringify({
|
|
15
|
+
...baseSeed,
|
|
16
|
+
symbolToScriptPath: { [symbol]: `Assets/Code/${symbol}.cs` },
|
|
17
|
+
scriptPathToGuid: { [`Assets/Code/${symbol}.cs`]: 'abc123abc123abc123abc123abc123ab' },
|
|
18
|
+
}), 'utf-8');
|
|
19
|
+
}
|
|
7
20
|
test('loadUnityParitySeed returns null on missing file and parsed object on valid file', async () => {
|
|
8
21
|
const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
|
|
9
22
|
try {
|
|
10
23
|
const missing = await loadUnityParitySeed(storagePath);
|
|
11
24
|
assert.equal(missing, null);
|
|
12
|
-
await
|
|
13
|
-
version: 1,
|
|
14
|
-
symbolToScriptPath: { DoorObj: 'Assets/Code/DoorObj.cs' },
|
|
15
|
-
scriptPathToGuid: { 'Assets/Code/DoorObj.cs': 'abc123abc123abc123abc123abc123ab' },
|
|
16
|
-
guidToResourcePaths: { abc123abc123abc123abc123abc123ab: ['Assets/Prefabs/Door.prefab'] },
|
|
17
|
-
}), 'utf-8');
|
|
25
|
+
await writeSeed(storagePath, 'DoorObj');
|
|
18
26
|
const loaded = await loadUnityParitySeed(storagePath);
|
|
19
27
|
assert.equal(loaded?.version, 1);
|
|
20
28
|
assert.equal(loaded?.symbolToScriptPath.DoorObj, 'Assets/Code/DoorObj.cs');
|
|
21
29
|
}
|
|
22
30
|
finally {
|
|
31
|
+
__resetUnityParitySeedLoaderCacheForTest();
|
|
32
|
+
await fs.rm(storagePath, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
test('loadUnityParitySeed deduplicates concurrent requests for same storage key', async (t) => {
|
|
36
|
+
const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
|
|
37
|
+
try {
|
|
38
|
+
await writeSeed(storagePath, 'ConcurrentSymbol');
|
|
39
|
+
const readFileOriginal = fs.readFile.bind(fs);
|
|
40
|
+
let readFileCalls = 0;
|
|
41
|
+
t.mock.method(fs, 'readFile', async (...args) => {
|
|
42
|
+
readFileCalls += 1;
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
44
|
+
return readFileOriginal(...args);
|
|
45
|
+
});
|
|
46
|
+
const results = await Promise.all(Array.from({ length: 10 }, () => loadUnityParitySeed(storagePath)));
|
|
47
|
+
assert.equal(results.every((row) => row?.symbolToScriptPath.ConcurrentSymbol), true);
|
|
48
|
+
assert.equal(readFileCalls, 1);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
__resetUnityParitySeedLoaderCacheForTest();
|
|
52
|
+
await fs.rm(storagePath, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
test('loadUnityParitySeed evicts idle cache entry after ttl', async (t) => {
|
|
56
|
+
const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
|
|
57
|
+
const idleEnvKey = 'GITNEXUS_UNITY_PARITY_SEED_CACHE_IDLE_MS';
|
|
58
|
+
const previousIdle = process.env[idleEnvKey];
|
|
59
|
+
process.env[idleEnvKey] = '15';
|
|
60
|
+
try {
|
|
61
|
+
await writeSeed(storagePath, 'IdleSymbol');
|
|
62
|
+
const readFileOriginal = fs.readFile.bind(fs);
|
|
63
|
+
let readFileCalls = 0;
|
|
64
|
+
t.mock.method(fs, 'readFile', async (...args) => {
|
|
65
|
+
readFileCalls += 1;
|
|
66
|
+
return readFileOriginal(...args);
|
|
67
|
+
});
|
|
68
|
+
await loadUnityParitySeed(storagePath);
|
|
69
|
+
await loadUnityParitySeed(storagePath);
|
|
70
|
+
assert.equal(readFileCalls, 1);
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
72
|
+
await loadUnityParitySeed(storagePath);
|
|
73
|
+
assert.equal(readFileCalls, 2);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
__resetUnityParitySeedLoaderCacheForTest();
|
|
77
|
+
if (previousIdle === undefined) {
|
|
78
|
+
delete process.env[idleEnvKey];
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
process.env[idleEnvKey] = previousIdle;
|
|
82
|
+
}
|
|
83
|
+
await fs.rm(storagePath, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
test('loadUnityParitySeed invalidates cache when seed mtime changes', async (t) => {
|
|
87
|
+
const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-seed-loader-'));
|
|
88
|
+
const seedPath = path.join(storagePath, 'unity-parity-seed.json');
|
|
89
|
+
try {
|
|
90
|
+
await writeSeed(storagePath, 'VersionA');
|
|
91
|
+
const readFileOriginal = fs.readFile.bind(fs);
|
|
92
|
+
let readFileCalls = 0;
|
|
93
|
+
t.mock.method(fs, 'readFile', async (...args) => {
|
|
94
|
+
readFileCalls += 1;
|
|
95
|
+
return readFileOriginal(...args);
|
|
96
|
+
});
|
|
97
|
+
const first = await loadUnityParitySeed(storagePath);
|
|
98
|
+
const second = await loadUnityParitySeed(storagePath);
|
|
99
|
+
assert.equal(first?.symbolToScriptPath.VersionA, 'Assets/Code/VersionA.cs');
|
|
100
|
+
assert.equal(second?.symbolToScriptPath.VersionA, 'Assets/Code/VersionA.cs');
|
|
101
|
+
assert.equal(readFileCalls, 1);
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
103
|
+
await writeSeed(storagePath, 'VersionB');
|
|
104
|
+
await fs.utimes(seedPath, new Date(), new Date());
|
|
105
|
+
const third = await loadUnityParitySeed(storagePath);
|
|
106
|
+
assert.equal(third?.symbolToScriptPath.VersionB, 'Assets/Code/VersionB.cs');
|
|
107
|
+
assert.equal(readFileCalls, 2);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
__resetUnityParitySeedLoaderCacheForTest();
|
|
23
111
|
await fs.rm(storagePath, { recursive: true, force: true });
|
|
24
112
|
}
|
|
25
113
|
});
|
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: 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.');
|
|
189
|
+
lines.push('re_index: If data is stale, ask user whether to run `npx -y @veewo/gitnexus@latest 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');
|
|
@@ -372,7 +372,7 @@ async function getProcessDetailResource(name, backend, repoName) {
|
|
|
372
372
|
async function getSetupResource(backend) {
|
|
373
373
|
const repos = await backend.listRepos();
|
|
374
374
|
if (repos.length === 0) {
|
|
375
|
-
return '# GitNexus\n\nNo repositories indexed. Run: `npx gitnexus analyze` in a repository.';
|
|
375
|
+
return '# GitNexus\n\nNo repositories indexed. Run: `npx -y @veewo/gitnexus@latest analyze` in a repository.';
|
|
376
376
|
}
|
|
377
377
|
const sections = [];
|
|
378
378
|
for (const repo of repos) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* Tools: list_repos, query, cypher, context, impact, detect_changes, rename
|
|
11
11
|
* Resources: repos, repo/{name}/context, repo/{name}/clusters, ...
|
|
12
12
|
*/
|
|
13
|
+
import { createRequire } from 'module';
|
|
13
14
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
14
|
-
import {
|
|
15
|
+
import { CompatibleStdioServerTransport } from './compatible-stdio-transport.js';
|
|
15
16
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
16
17
|
import { GITNEXUS_TOOLS } from './tools.js';
|
|
17
18
|
import { getResourceDefinitions, getResourceTemplates, readResource } from './resources.js';
|
|
@@ -59,9 +60,11 @@ function getNextStepHint(toolName, args) {
|
|
|
59
60
|
* Transport-agnostic — caller connects the desired transport.
|
|
60
61
|
*/
|
|
61
62
|
export function createMCPServer(backend) {
|
|
63
|
+
const require = createRequire(import.meta.url);
|
|
64
|
+
const pkgVersion = require('../../package.json').version;
|
|
62
65
|
const server = new Server({
|
|
63
66
|
name: 'gitnexus',
|
|
64
|
-
version:
|
|
67
|
+
version: pkgVersion,
|
|
65
68
|
}, {
|
|
66
69
|
capabilities: {
|
|
67
70
|
tools: {},
|
|
@@ -235,17 +238,29 @@ Follow these steps:
|
|
|
235
238
|
export async function startMCPServer(backend) {
|
|
236
239
|
const server = createMCPServer(backend);
|
|
237
240
|
// Connect to stdio transport
|
|
238
|
-
const transport = new
|
|
241
|
+
const transport = new CompatibleStdioServerTransport();
|
|
239
242
|
await server.connect(transport);
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
243
|
+
// Graceful shutdown helper
|
|
244
|
+
let shuttingDown = false;
|
|
245
|
+
const shutdown = async () => {
|
|
246
|
+
if (shuttingDown)
|
|
247
|
+
return;
|
|
248
|
+
shuttingDown = true;
|
|
249
|
+
try {
|
|
250
|
+
await backend.disconnect();
|
|
251
|
+
}
|
|
252
|
+
catch { }
|
|
253
|
+
try {
|
|
254
|
+
await server.close();
|
|
255
|
+
}
|
|
256
|
+
catch { }
|
|
249
257
|
process.exit(0);
|
|
250
|
-
}
|
|
258
|
+
};
|
|
259
|
+
// Handle graceful shutdown
|
|
260
|
+
process.on('SIGINT', shutdown);
|
|
261
|
+
process.on('SIGTERM', shutdown);
|
|
262
|
+
// Handle stdio errors — stdin close means the parent process is gone
|
|
263
|
+
process.stdin.on('end', shutdown);
|
|
264
|
+
process.stdin.on('error', () => shutdown());
|
|
265
|
+
process.stdout.on('error', () => shutdown());
|
|
251
266
|
}
|
package/dist/mcp/staleness.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* Checks if the GitNexus index is behind the current git HEAD.
|
|
5
5
|
* Returns a hint for the LLM to call analyze if stale.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { execFileSync } from 'child_process';
|
|
8
8
|
/**
|
|
9
9
|
* Check how many commits the index is behind HEAD
|
|
10
10
|
*/
|
|
11
11
|
export function checkStaleness(repoPath, lastCommit) {
|
|
12
12
|
try {
|
|
13
13
|
// Get count of commits between lastCommit and HEAD
|
|
14
|
-
const result =
|
|
14
|
+
const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
15
15
|
const commitsBehind = parseInt(result, 10) || 0;
|
|
16
16
|
if (commitsBehind > 0) {
|
|
17
17
|
return {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -78,7 +78,7 @@ SCHEMA:
|
|
|
78
78
|
- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
|
|
79
79
|
- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
|
|
80
80
|
- All edges via single CodeRelation table with 'type' property
|
|
81
|
-
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
|
|
81
|
+
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
|
|
82
82
|
- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
|
|
83
83
|
|
|
84
84
|
EXAMPLES:
|
|
@@ -91,6 +91,15 @@ EXAMPLES:
|
|
|
91
91
|
• Trace a process:
|
|
92
92
|
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
|
|
93
93
|
|
|
94
|
+
• Find all methods of a class:
|
|
95
|
+
MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
|
|
96
|
+
|
|
97
|
+
• Find method overrides (MRO resolution):
|
|
98
|
+
MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
|
|
99
|
+
|
|
100
|
+
• Detect diamond inheritance:
|
|
101
|
+
MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name
|
|
102
|
+
|
|
94
103
|
OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading.
|
|
95
104
|
|
|
96
105
|
TIPS:
|
|
@@ -208,7 +217,7 @@ Depth groups:
|
|
|
208
217
|
- d=2: LIKELY AFFECTED (indirect)
|
|
209
218
|
- d=3: MAY NEED TESTING (transitive)
|
|
210
219
|
|
|
211
|
-
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS
|
|
220
|
+
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES
|
|
212
221
|
Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
|
213
222
|
inputSchema: {
|
|
214
223
|
type: 'object',
|
|
@@ -218,7 +227,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
|
|
218
227
|
file_path: { type: 'string', description: 'Optional file path filter to disambiguate target name' },
|
|
219
228
|
direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
|
|
220
229
|
maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
|
|
221
|
-
relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS (default: usage-based)' },
|
|
230
|
+
relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES (default: usage-based)' },
|
|
222
231
|
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
|
|
223
232
|
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.3)' },
|
|
224
233
|
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
package/dist/server/api.js
CHANGED
|
@@ -12,9 +12,9 @@ import cors from 'cors';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import { loadMeta, listRegisteredRepos } from '../storage/repo-manager.js';
|
|
15
|
-
import { executeQuery,
|
|
16
|
-
import { NODE_TABLES } from '../core/
|
|
17
|
-
import {
|
|
15
|
+
import { executeQuery, closeLbug, withLbugDb } from '../core/lbug/lbug-adapter.js';
|
|
16
|
+
import { NODE_TABLES } from '../core/lbug/schema.js';
|
|
17
|
+
import { searchFTSFromLbug } from '../core/search/bm25-index.js';
|
|
18
18
|
import { hybridSearch } from '../core/search/hybrid-search.js';
|
|
19
19
|
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
20
20
|
// at server startup — crashes on unsupported Node ABI versions (#89)
|
|
@@ -171,8 +171,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
171
171
|
res.status(404).json({ error: 'Repository not found' });
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
174
|
-
const
|
|
175
|
-
const graph = await
|
|
174
|
+
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
175
|
+
const graph = await withLbugDb(lbugPath, async () => buildGraph());
|
|
176
176
|
res.json(graph);
|
|
177
177
|
}
|
|
178
178
|
catch (err) {
|
|
@@ -192,8 +192,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
192
192
|
res.status(404).json({ error: 'Repository not found' });
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
|
-
const
|
|
196
|
-
const result = await
|
|
195
|
+
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
196
|
+
const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
|
|
197
197
|
res.json({ result });
|
|
198
198
|
}
|
|
199
199
|
catch (err) {
|
|
@@ -213,19 +213,19 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
213
213
|
res.status(404).json({ error: 'Repository not found' });
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
|
-
const
|
|
216
|
+
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
217
217
|
const parsedLimit = Number(req.body.limit ?? 10);
|
|
218
218
|
const limit = Number.isFinite(parsedLimit)
|
|
219
219
|
? Math.max(1, Math.min(100, Math.trunc(parsedLimit)))
|
|
220
220
|
: 10;
|
|
221
|
-
const results = await
|
|
221
|
+
const results = await withLbugDb(lbugPath, async () => {
|
|
222
222
|
const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
|
|
223
223
|
if (isEmbedderReady()) {
|
|
224
224
|
const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js');
|
|
225
225
|
return hybridSearch(query, limit, executeQuery, semanticSearch);
|
|
226
226
|
}
|
|
227
227
|
// FTS-only fallback when embeddings aren't loaded
|
|
228
|
-
return
|
|
228
|
+
return searchFTSFromLbug(query, limit);
|
|
229
229
|
});
|
|
230
230
|
res.json({ results });
|
|
231
231
|
}
|
|
@@ -331,11 +331,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
331
331
|
const server = app.listen(port, host, () => {
|
|
332
332
|
console.log(`GitNexus server running on http://${host}:${port}`);
|
|
333
333
|
});
|
|
334
|
-
// Graceful shutdown — close Express +
|
|
334
|
+
// Graceful shutdown — close Express + LadybugDB cleanly
|
|
335
335
|
const shutdown = async () => {
|
|
336
336
|
server.close();
|
|
337
337
|
await cleanupMcp();
|
|
338
|
-
await
|
|
338
|
+
await closeLbug();
|
|
339
339
|
await backend.disconnect();
|
|
340
340
|
process.exit(0);
|
|
341
341
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mounts the GitNexus MCP server on Express using StreamableHTTP transport.
|
|
5
5
|
* Each connecting client gets its own stateful session; the LocalBackend
|
|
6
|
-
* is shared across all sessions (thread-safe — lazy
|
|
6
|
+
* is shared across all sessions (thread-safe — lazy LadybugDB per repo).
|
|
7
7
|
*
|
|
8
8
|
* Sessions are cleaned up on explicit close or after SESSION_TTL_MS of inactivity
|
|
9
9
|
* (guards against network drops that never trigger onclose).
|
package/dist/server/mcp-http.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mounts the GitNexus MCP server on Express using StreamableHTTP transport.
|
|
5
5
|
* Each connecting client gets its own stateful session; the LocalBackend
|
|
6
|
-
* is shared across all sessions (thread-safe — lazy
|
|
6
|
+
* is shared across all sessions (thread-safe — lazy LadybugDB per repo).
|
|
7
7
|
*
|
|
8
8
|
* Sessions are cleaned up on explicit close or after SESSION_TTL_MS of inactivity
|
|
9
9
|
* (guards against network drops that never trigger onclose).
|
package/dist/storage/git.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
// Git utilities for repository detection, commit tracking, and diff analysis
|
|
3
4
|
export const isGitRepo = (repoPath) => {
|
|
4
5
|
try {
|
|
@@ -22,9 +23,11 @@ export const getCurrentCommit = (repoPath) => {
|
|
|
22
23
|
*/
|
|
23
24
|
export const getGitRoot = (fromPath) => {
|
|
24
25
|
try {
|
|
25
|
-
|
|
26
|
+
const raw = execSync('git rev-parse --show-toplevel', { cwd: fromPath })
|
|
26
27
|
.toString()
|
|
27
28
|
.trim();
|
|
29
|
+
// On Windows, git returns /d/Projects/Foo — path.resolve normalizes to D:\Projects\Foo
|
|
30
|
+
return path.resolve(raw);
|
|
28
31
|
}
|
|
29
32
|
catch {
|
|
30
33
|
return null;
|
|
@@ -27,7 +27,7 @@ export interface RepoMeta {
|
|
|
27
27
|
export interface IndexedRepo {
|
|
28
28
|
repoPath: string;
|
|
29
29
|
storagePath: string;
|
|
30
|
-
|
|
30
|
+
lbugPath: string;
|
|
31
31
|
metaPath: string;
|
|
32
32
|
meta: RepoMeta;
|
|
33
33
|
}
|
|
@@ -56,9 +56,27 @@ export declare const getStoragePath: (repoPath: string) => string;
|
|
|
56
56
|
*/
|
|
57
57
|
export declare const getStoragePaths: (repoPath: string) => {
|
|
58
58
|
storagePath: string;
|
|
59
|
-
|
|
59
|
+
lbugPath: string;
|
|
60
60
|
metaPath: string;
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Check whether a KuzuDB index exists in the given storage path.
|
|
64
|
+
* Non-destructive — safe to call from status commands.
|
|
65
|
+
*/
|
|
66
|
+
export declare const hasKuzuIndex: (storagePath: string) => Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Clean up stale KuzuDB files after migration to LadybugDB.
|
|
69
|
+
*
|
|
70
|
+
* Returns:
|
|
71
|
+
* found — true if .gitnexus/kuzu existed and was deleted
|
|
72
|
+
* needsReindex — true if kuzu existed but lbug does not (re-analyze required)
|
|
73
|
+
*
|
|
74
|
+
* Callers own the user-facing messaging; this function only deletes files.
|
|
75
|
+
*/
|
|
76
|
+
export declare const cleanupOldKuzuFiles: (storagePath: string) => Promise<{
|
|
77
|
+
found: boolean;
|
|
78
|
+
needsReindex: boolean;
|
|
79
|
+
}>;
|
|
62
80
|
/**
|
|
63
81
|
* Load metadata from an indexed repo
|
|
64
82
|
*/
|