@veewo/gitnexus 1.3.4
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 +234 -0
- package/dist/benchmark/agent-context/evaluators.d.ts +9 -0
- package/dist/benchmark/agent-context/evaluators.js +196 -0
- package/dist/benchmark/agent-context/evaluators.test.d.ts +1 -0
- package/dist/benchmark/agent-context/evaluators.test.js +39 -0
- package/dist/benchmark/agent-context/io.d.ts +2 -0
- package/dist/benchmark/agent-context/io.js +23 -0
- package/dist/benchmark/agent-context/io.test.d.ts +1 -0
- package/dist/benchmark/agent-context/io.test.js +19 -0
- package/dist/benchmark/agent-context/report.d.ts +2 -0
- package/dist/benchmark/agent-context/report.js +59 -0
- package/dist/benchmark/agent-context/report.test.d.ts +1 -0
- package/dist/benchmark/agent-context/report.test.js +85 -0
- package/dist/benchmark/agent-context/runner.d.ts +46 -0
- package/dist/benchmark/agent-context/runner.js +111 -0
- package/dist/benchmark/agent-context/runner.test.d.ts +1 -0
- package/dist/benchmark/agent-context/runner.test.js +79 -0
- package/dist/benchmark/agent-context/tool-runner.d.ts +7 -0
- package/dist/benchmark/agent-context/tool-runner.js +18 -0
- package/dist/benchmark/agent-context/tool-runner.test.d.ts +1 -0
- package/dist/benchmark/agent-context/tool-runner.test.js +11 -0
- package/dist/benchmark/agent-context/types.d.ts +40 -0
- package/dist/benchmark/agent-context/types.js +1 -0
- package/dist/benchmark/analyze-runner.d.ts +16 -0
- package/dist/benchmark/analyze-runner.js +51 -0
- package/dist/benchmark/analyze-runner.test.d.ts +1 -0
- package/dist/benchmark/analyze-runner.test.js +37 -0
- package/dist/benchmark/evaluators.d.ts +6 -0
- package/dist/benchmark/evaluators.js +10 -0
- package/dist/benchmark/evaluators.test.d.ts +1 -0
- package/dist/benchmark/evaluators.test.js +12 -0
- package/dist/benchmark/io.d.ts +7 -0
- package/dist/benchmark/io.js +25 -0
- package/dist/benchmark/io.test.d.ts +1 -0
- package/dist/benchmark/io.test.js +35 -0
- package/dist/benchmark/neonspark-candidates.d.ts +19 -0
- package/dist/benchmark/neonspark-candidates.js +94 -0
- package/dist/benchmark/neonspark-candidates.test.d.ts +1 -0
- package/dist/benchmark/neonspark-candidates.test.js +43 -0
- package/dist/benchmark/neonspark-materialize.d.ts +19 -0
- package/dist/benchmark/neonspark-materialize.js +111 -0
- package/dist/benchmark/neonspark-materialize.test.d.ts +1 -0
- package/dist/benchmark/neonspark-materialize.test.js +124 -0
- package/dist/benchmark/neonspark-sync.d.ts +3 -0
- package/dist/benchmark/neonspark-sync.js +53 -0
- package/dist/benchmark/neonspark-sync.test.d.ts +1 -0
- package/dist/benchmark/neonspark-sync.test.js +20 -0
- package/dist/benchmark/report.d.ts +1 -0
- package/dist/benchmark/report.js +7 -0
- package/dist/benchmark/runner.d.ts +48 -0
- package/dist/benchmark/runner.js +302 -0
- package/dist/benchmark/runner.test.d.ts +1 -0
- package/dist/benchmark/runner.test.js +50 -0
- package/dist/benchmark/scoring.d.ts +16 -0
- package/dist/benchmark/scoring.js +27 -0
- package/dist/benchmark/scoring.test.d.ts +1 -0
- package/dist/benchmark/scoring.test.js +24 -0
- package/dist/benchmark/tool-runner.d.ts +6 -0
- package/dist/benchmark/tool-runner.js +17 -0
- package/dist/benchmark/types.d.ts +36 -0
- package/dist/benchmark/types.js +1 -0
- package/dist/cli/ai-context.d.ts +22 -0
- package/dist/cli/ai-context.js +184 -0
- package/dist/cli/ai-context.test.d.ts +1 -0
- package/dist/cli/ai-context.test.js +30 -0
- package/dist/cli/analyze-multi-scope-regression.test.d.ts +1 -0
- package/dist/cli/analyze-multi-scope-regression.test.js +22 -0
- package/dist/cli/analyze-options.d.ts +7 -0
- package/dist/cli/analyze-options.js +56 -0
- package/dist/cli/analyze-options.test.d.ts +1 -0
- package/dist/cli/analyze-options.test.js +36 -0
- package/dist/cli/analyze.d.ts +14 -0
- package/dist/cli/analyze.js +384 -0
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/benchmark-agent-context.d.ts +29 -0
- package/dist/cli/benchmark-agent-context.js +61 -0
- package/dist/cli/benchmark-agent-context.test.d.ts +1 -0
- package/dist/cli/benchmark-agent-context.test.js +80 -0
- package/dist/cli/benchmark-unity.d.ts +15 -0
- package/dist/cli/benchmark-unity.js +31 -0
- package/dist/cli/benchmark-unity.test.d.ts +1 -0
- package/dist/cli/benchmark-unity.test.js +18 -0
- package/dist/cli/claude-hooks.d.ts +22 -0
- package/dist/cli/claude-hooks.js +97 -0
- package/dist/cli/clean.d.ts +10 -0
- package/dist/cli/clean.js +60 -0
- package/dist/cli/eval-server.d.ts +30 -0
- package/dist/cli/eval-server.js +372 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +182 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +33 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +34 -0
- package/dist/cli/repo-manager-alias.test.d.ts +1 -0
- package/dist/cli/repo-manager-alias.test.js +40 -0
- package/dist/cli/scope-filter.test.d.ts +1 -0
- package/dist/cli/scope-filter.test.js +49 -0
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +6 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +311 -0
- package/dist/cli/setup.test.d.ts +1 -0
- package/dist/cli/setup.test.js +31 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.js +27 -0
- package/dist/cli/tool.d.ts +40 -0
- package/dist/cli/tool.js +94 -0
- package/dist/cli/version.test.d.ts +1 -0
- package/dist/cli/version.test.js +19 -0
- package/dist/cli/wiki.d.ts +15 -0
- package/dist/cli/wiki.js +361 -0
- package/dist/config/ignore-service.d.ts +1 -0
- package/dist/config/ignore-service.js +210 -0
- package/dist/config/supported-languages.d.ts +12 -0
- package/dist/config/supported-languages.js +15 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +213 -0
- package/dist/core/embeddings/embedder.d.ts +60 -0
- package/dist/core/embeddings/embedder.js +251 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +51 -0
- package/dist/core/embeddings/embedding-pipeline.js +329 -0
- package/dist/core/embeddings/index.d.ts +9 -0
- package/dist/core/embeddings/index.js +9 -0
- package/dist/core/embeddings/text-generator.d.ts +24 -0
- package/dist/core/embeddings/text-generator.js +182 -0
- package/dist/core/embeddings/types.d.ts +87 -0
- package/dist/core/embeddings/types.js +32 -0
- package/dist/core/graph/graph.d.ts +2 -0
- package/dist/core/graph/graph.js +66 -0
- package/dist/core/graph/types.d.ts +61 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/ingestion/ast-cache.d.ts +11 -0
- package/dist/core/ingestion/ast-cache.js +34 -0
- package/dist/core/ingestion/call-processor.d.ts +15 -0
- package/dist/core/ingestion/call-processor.js +327 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
- package/dist/core/ingestion/cluster-enricher.js +170 -0
- package/dist/core/ingestion/community-processor.d.ts +39 -0
- package/dist/core/ingestion/community-processor.js +312 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +39 -0
- package/dist/core/ingestion/entry-point-scoring.js +260 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
- package/dist/core/ingestion/filesystem-walker.js +80 -0
- package/dist/core/ingestion/framework-detection.d.ts +39 -0
- package/dist/core/ingestion/framework-detection.js +235 -0
- package/dist/core/ingestion/heritage-processor.d.ts +20 -0
- package/dist/core/ingestion/heritage-processor.js +197 -0
- package/dist/core/ingestion/import-processor.d.ts +38 -0
- package/dist/core/ingestion/import-processor.js +778 -0
- package/dist/core/ingestion/parsing-processor.d.ts +15 -0
- package/dist/core/ingestion/parsing-processor.js +291 -0
- package/dist/core/ingestion/pipeline.d.ts +5 -0
- package/dist/core/ingestion/pipeline.js +323 -0
- package/dist/core/ingestion/process-processor.d.ts +51 -0
- package/dist/core/ingestion/process-processor.js +309 -0
- package/dist/core/ingestion/scope-filter.d.ts +25 -0
- package/dist/core/ingestion/scope-filter.js +100 -0
- package/dist/core/ingestion/structure-processor.d.ts +2 -0
- package/dist/core/ingestion/structure-processor.js +36 -0
- package/dist/core/ingestion/symbol-table.d.ts +33 -0
- package/dist/core/ingestion/symbol-table.js +38 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -0
- package/dist/core/ingestion/tree-sitter-queries.js +398 -0
- package/dist/core/ingestion/utils.d.ts +10 -0
- package/dist/core/ingestion/utils.js +50 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +59 -0
- package/dist/core/ingestion/workers/parse-worker.js +672 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +16 -0
- package/dist/core/ingestion/workers/worker-pool.js +120 -0
- package/dist/core/kuzu/csv-generator.d.ts +29 -0
- package/dist/core/kuzu/csv-generator.js +336 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +101 -0
- package/dist/core/kuzu/kuzu-adapter.js +753 -0
- package/dist/core/kuzu/schema.d.ts +53 -0
- package/dist/core/kuzu/schema.js +407 -0
- package/dist/core/search/bm25-index.d.ts +23 -0
- package/dist/core/search/bm25-index.js +95 -0
- package/dist/core/search/hybrid-search.d.ts +49 -0
- package/dist/core/search/hybrid-search.js +118 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +4 -0
- package/dist/core/tree-sitter/parser-loader.js +44 -0
- package/dist/core/wiki/generator.d.ts +110 -0
- package/dist/core/wiki/generator.js +786 -0
- package/dist/core/wiki/graph-queries.d.ts +80 -0
- package/dist/core/wiki/graph-queries.js +238 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +297 -0
- package/dist/core/wiki/llm-client.d.ts +40 -0
- package/dist/core/wiki/llm-client.js +162 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +174 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/mcp/core/embedder.d.ts +27 -0
- package/dist/mcp/core/embedder.js +108 -0
- package/dist/mcp/core/kuzu-adapter.d.ts +34 -0
- package/dist/mcp/core/kuzu-adapter.js +231 -0
- package/dist/mcp/local/local-backend.d.ts +160 -0
- package/dist/mcp/local/local-backend.js +1646 -0
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +407 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +251 -0
- package/dist/mcp/staleness.d.ts +15 -0
- package/dist/mcp/staleness.js +29 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +195 -0
- package/dist/server/api.d.ts +10 -0
- package/dist/server/api.js +344 -0
- package/dist/server/mcp-http.d.ts +13 -0
- package/dist/server/mcp-http.js +100 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +32 -0
- package/dist/storage/repo-manager.d.ts +125 -0
- package/dist/storage/repo-manager.js +257 -0
- package/dist/types/pipeline.d.ts +34 -0
- package/dist/types/pipeline.js +18 -0
- package/hooks/claude/gitnexus-hook.cjs +135 -0
- package/hooks/claude/pre-tool-use.sh +78 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +92 -0
- package/skills/gitnexus-cli.md +82 -0
- package/skills/gitnexus-debugging.md +89 -0
- package/skills/gitnexus-exploring.md +78 -0
- package/skills/gitnexus-guide.md +64 -0
- package/skills/gitnexus-impact-analysis.md +97 -0
- package/skills/gitnexus-refactoring.md +121 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Command
|
|
3
|
+
*
|
|
4
|
+
* Indexes a repository and stores the knowledge graph in .gitnexus/
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { execFileSync } from 'child_process';
|
|
8
|
+
import v8 from 'v8';
|
|
9
|
+
import cliProgress from 'cli-progress';
|
|
10
|
+
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
|
11
|
+
import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex, loadCachedEmbeddings } from '../core/kuzu/kuzu-adapter.js';
|
|
12
|
+
// Embedding imports are lazy (dynamic import) so onnxruntime-node is never
|
|
13
|
+
// loaded when embeddings are not requested. This avoids crashes on Node
|
|
14
|
+
// versions whose ABI is not yet supported by the native binary (#89).
|
|
15
|
+
// disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
|
|
16
|
+
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
|
|
17
|
+
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
18
|
+
import { generateAIContextFiles } from './ai-context.js';
|
|
19
|
+
import fs from 'fs/promises';
|
|
20
|
+
import { registerClaudeHook } from './claude-hooks.js';
|
|
21
|
+
import { normalizeRepoAlias, parseExtensionList, resolveAnalyzeScopeRules } from './analyze-options.js';
|
|
22
|
+
const HEAP_MB = 8192;
|
|
23
|
+
const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
|
|
24
|
+
/** Re-exec the process with an 8GB heap if we're currently below that. */
|
|
25
|
+
function ensureHeap() {
|
|
26
|
+
const nodeOpts = process.env.NODE_OPTIONS || '';
|
|
27
|
+
if (nodeOpts.includes('--max-old-space-size'))
|
|
28
|
+
return false;
|
|
29
|
+
const v8Heap = v8.getHeapStatistics().heap_size_limit;
|
|
30
|
+
if (v8Heap >= HEAP_MB * 1024 * 1024 * 0.9)
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
execFileSync(process.execPath, [HEAP_FLAG, ...process.argv.slice(1)], {
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
env: { ...process.env, NODE_OPTIONS: `${nodeOpts} ${HEAP_FLAG}`.trim() },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
process.exitCode = e.status ?? 1;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/** Threshold: auto-skip embeddings for repos with more nodes than this */
|
|
44
|
+
const EMBEDDING_NODE_LIMIT = 50_000;
|
|
45
|
+
const PHASE_LABELS = {
|
|
46
|
+
extracting: 'Scanning files',
|
|
47
|
+
structure: 'Building structure',
|
|
48
|
+
parsing: 'Parsing code',
|
|
49
|
+
imports: 'Resolving imports',
|
|
50
|
+
calls: 'Tracing calls',
|
|
51
|
+
heritage: 'Extracting inheritance',
|
|
52
|
+
communities: 'Detecting communities',
|
|
53
|
+
processes: 'Detecting processes',
|
|
54
|
+
complete: 'Pipeline complete',
|
|
55
|
+
kuzu: 'Loading into KuzuDB',
|
|
56
|
+
fts: 'Creating search indexes',
|
|
57
|
+
embeddings: 'Generating embeddings',
|
|
58
|
+
done: 'Done',
|
|
59
|
+
};
|
|
60
|
+
export const analyzeCommand = async (inputPath, options) => {
|
|
61
|
+
if (ensureHeap())
|
|
62
|
+
return;
|
|
63
|
+
console.log('\n GitNexus Analyzer\n');
|
|
64
|
+
let includeExtensions = [];
|
|
65
|
+
let scopeRules = [];
|
|
66
|
+
let repoAlias;
|
|
67
|
+
try {
|
|
68
|
+
includeExtensions = parseExtensionList(options?.extensions);
|
|
69
|
+
scopeRules = await resolveAnalyzeScopeRules({
|
|
70
|
+
scopeManifest: options?.scopeManifest,
|
|
71
|
+
scopePrefix: options?.scopePrefix,
|
|
72
|
+
});
|
|
73
|
+
repoAlias = normalizeRepoAlias(options?.repoAlias);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.log(` ${error?.message || String(error)}\n`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let repoPath;
|
|
81
|
+
if (inputPath) {
|
|
82
|
+
repoPath = path.resolve(inputPath);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const gitRoot = getGitRoot(process.cwd());
|
|
86
|
+
if (!gitRoot) {
|
|
87
|
+
console.log(' Not inside a git repository\n');
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
repoPath = gitRoot;
|
|
92
|
+
}
|
|
93
|
+
if (!isGitRepo(repoPath)) {
|
|
94
|
+
console.log(' Not a git repository\n');
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const { storagePath, kuzuPath } = getStoragePaths(repoPath);
|
|
99
|
+
const currentCommit = getCurrentCommit(repoPath);
|
|
100
|
+
const existingMeta = await loadMeta(storagePath);
|
|
101
|
+
if (existingMeta &&
|
|
102
|
+
!options?.force &&
|
|
103
|
+
existingMeta.lastCommit === currentCommit &&
|
|
104
|
+
includeExtensions.length === 0 &&
|
|
105
|
+
scopeRules.length === 0 &&
|
|
106
|
+
!repoAlias) {
|
|
107
|
+
console.log(' Already up to date\n');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Single progress bar for entire pipeline
|
|
111
|
+
const bar = new cliProgress.SingleBar({
|
|
112
|
+
format: ' {bar} {percentage}% | {phase}',
|
|
113
|
+
barCompleteChar: '\u2588',
|
|
114
|
+
barIncompleteChar: '\u2591',
|
|
115
|
+
hideCursor: true,
|
|
116
|
+
barGlue: '',
|
|
117
|
+
autopadding: true,
|
|
118
|
+
clearOnComplete: false,
|
|
119
|
+
stopOnComplete: false,
|
|
120
|
+
}, cliProgress.Presets.shades_grey);
|
|
121
|
+
bar.start(100, 0, { phase: 'Initializing...' });
|
|
122
|
+
// Graceful SIGINT handling — clean up resources and exit
|
|
123
|
+
let aborted = false;
|
|
124
|
+
const sigintHandler = () => {
|
|
125
|
+
if (aborted)
|
|
126
|
+
process.exit(1); // Second Ctrl-C: force exit
|
|
127
|
+
aborted = true;
|
|
128
|
+
bar.stop();
|
|
129
|
+
console.log('\n Interrupted — cleaning up...');
|
|
130
|
+
closeKuzu().catch(() => { }).finally(() => process.exit(130));
|
|
131
|
+
};
|
|
132
|
+
process.on('SIGINT', sigintHandler);
|
|
133
|
+
// Route all console output through bar.log() so the bar doesn't stamp itself
|
|
134
|
+
// multiple times when other code writes to stdout/stderr mid-render.
|
|
135
|
+
const origLog = console.log.bind(console);
|
|
136
|
+
const origWarn = console.warn.bind(console);
|
|
137
|
+
const origError = console.error.bind(console);
|
|
138
|
+
const barLog = (...args) => {
|
|
139
|
+
// Clear the bar line, print the message, then let the next bar.update redraw
|
|
140
|
+
process.stdout.write('\x1b[2K\r');
|
|
141
|
+
origLog(args.map(a => (typeof a === 'string' ? a : String(a))).join(' '));
|
|
142
|
+
};
|
|
143
|
+
console.log = barLog;
|
|
144
|
+
console.warn = barLog;
|
|
145
|
+
console.error = barLog;
|
|
146
|
+
// Track elapsed time per phase — both updateBar and the interval use the
|
|
147
|
+
// same format so they don't flicker against each other.
|
|
148
|
+
let lastPhaseLabel = 'Initializing...';
|
|
149
|
+
let phaseStart = Date.now();
|
|
150
|
+
/** Update bar with phase label + elapsed seconds (shown after 3s). */
|
|
151
|
+
const updateBar = (value, phaseLabel) => {
|
|
152
|
+
if (phaseLabel !== lastPhaseLabel) {
|
|
153
|
+
lastPhaseLabel = phaseLabel;
|
|
154
|
+
phaseStart = Date.now();
|
|
155
|
+
}
|
|
156
|
+
const elapsed = Math.round((Date.now() - phaseStart) / 1000);
|
|
157
|
+
const display = elapsed >= 3 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
|
|
158
|
+
bar.update(value, { phase: display });
|
|
159
|
+
};
|
|
160
|
+
// Tick elapsed seconds for phases with infrequent progress callbacks
|
|
161
|
+
// (e.g. CSV streaming, FTS indexing). Uses the same display format as
|
|
162
|
+
// updateBar so there's no flickering.
|
|
163
|
+
const elapsedTimer = setInterval(() => {
|
|
164
|
+
const elapsed = Math.round((Date.now() - phaseStart) / 1000);
|
|
165
|
+
if (elapsed >= 3) {
|
|
166
|
+
bar.update({ phase: `${lastPhaseLabel} (${elapsed}s)` });
|
|
167
|
+
}
|
|
168
|
+
}, 1000);
|
|
169
|
+
const t0Global = Date.now();
|
|
170
|
+
// ── Cache embeddings from existing index before rebuild ────────────
|
|
171
|
+
let cachedEmbeddingNodeIds = new Set();
|
|
172
|
+
let cachedEmbeddings = [];
|
|
173
|
+
if (options?.embeddings && existingMeta && !options?.force) {
|
|
174
|
+
try {
|
|
175
|
+
updateBar(0, 'Caching embeddings...');
|
|
176
|
+
await initKuzu(kuzuPath);
|
|
177
|
+
const cached = await loadCachedEmbeddings();
|
|
178
|
+
cachedEmbeddingNodeIds = cached.embeddingNodeIds;
|
|
179
|
+
cachedEmbeddings = cached.embeddings;
|
|
180
|
+
await closeKuzu();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
try {
|
|
184
|
+
await closeKuzu();
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ── Phase 1: Full Pipeline (0–60%) ─────────────────────────────────
|
|
190
|
+
let pipelineResult;
|
|
191
|
+
try {
|
|
192
|
+
pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
|
|
193
|
+
const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
|
|
194
|
+
const scaled = Math.round(progress.percent * 0.6);
|
|
195
|
+
updateBar(scaled, phaseLabel);
|
|
196
|
+
}, { includeExtensions, scopeRules });
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
clearInterval(elapsedTimer);
|
|
200
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
201
|
+
console.log = origLog;
|
|
202
|
+
console.warn = origWarn;
|
|
203
|
+
console.error = origError;
|
|
204
|
+
bar.stop();
|
|
205
|
+
console.log(`\n ${error?.message || String(error)}\n`);
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// ── Phase 2: KuzuDB (60–85%) ──────────────────────────────────────
|
|
210
|
+
updateBar(60, 'Loading into KuzuDB...');
|
|
211
|
+
await closeKuzu();
|
|
212
|
+
const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
|
|
213
|
+
for (const f of kuzuFiles) {
|
|
214
|
+
try {
|
|
215
|
+
await fs.rm(f, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
catch { }
|
|
218
|
+
}
|
|
219
|
+
const t0Kuzu = Date.now();
|
|
220
|
+
await initKuzu(kuzuPath);
|
|
221
|
+
let kuzuMsgCount = 0;
|
|
222
|
+
const kuzuResult = await loadGraphToKuzu(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
|
|
223
|
+
kuzuMsgCount++;
|
|
224
|
+
const progress = Math.min(84, 60 + Math.round((kuzuMsgCount / (kuzuMsgCount + 10)) * 24));
|
|
225
|
+
updateBar(progress, msg);
|
|
226
|
+
});
|
|
227
|
+
const kuzuTime = ((Date.now() - t0Kuzu) / 1000).toFixed(1);
|
|
228
|
+
const kuzuWarnings = kuzuResult.warnings;
|
|
229
|
+
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
|
230
|
+
updateBar(85, 'Creating search indexes...');
|
|
231
|
+
const t0Fts = Date.now();
|
|
232
|
+
try {
|
|
233
|
+
await createFTSIndex('File', 'file_fts', ['name', 'content']);
|
|
234
|
+
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
|
235
|
+
await createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
|
236
|
+
await createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
|
237
|
+
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
// Non-fatal — FTS is best-effort
|
|
241
|
+
}
|
|
242
|
+
const ftsTime = ((Date.now() - t0Fts) / 1000).toFixed(1);
|
|
243
|
+
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
244
|
+
if (cachedEmbeddings.length > 0) {
|
|
245
|
+
updateBar(88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
|
|
246
|
+
const EMBED_BATCH = 200;
|
|
247
|
+
for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
|
|
248
|
+
const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
|
|
249
|
+
const paramsList = batch.map(e => ({ nodeId: e.nodeId, embedding: e.embedding }));
|
|
250
|
+
try {
|
|
251
|
+
await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
|
|
252
|
+
}
|
|
253
|
+
catch { /* some may fail if node was removed, that's fine */ }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
|
|
257
|
+
const stats = await getKuzuStats();
|
|
258
|
+
let embeddingTime = '0.0';
|
|
259
|
+
let embeddingSkipped = true;
|
|
260
|
+
let embeddingSkipReason = 'off (use --embeddings to enable)';
|
|
261
|
+
if (options?.embeddings) {
|
|
262
|
+
if (stats.nodes > EMBEDDING_NODE_LIMIT) {
|
|
263
|
+
embeddingSkipReason = `skipped (${stats.nodes.toLocaleString()} nodes > ${EMBEDDING_NODE_LIMIT.toLocaleString()} limit)`;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
embeddingSkipped = false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!embeddingSkipped) {
|
|
270
|
+
updateBar(90, 'Loading embedding model...');
|
|
271
|
+
const t0Emb = Date.now();
|
|
272
|
+
const { runEmbeddingPipeline } = await import('../core/embeddings/embedding-pipeline.js');
|
|
273
|
+
await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
|
|
274
|
+
const scaled = 90 + Math.round((progress.percent / 100) * 8);
|
|
275
|
+
const label = progress.phase === 'loading-model' ? 'Loading embedding model...' : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
|
|
276
|
+
updateBar(scaled, label);
|
|
277
|
+
}, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
|
|
278
|
+
embeddingTime = ((Date.now() - t0Emb) / 1000).toFixed(1);
|
|
279
|
+
}
|
|
280
|
+
// ── Phase 5: Finalize (98–100%) ───────────────────────────────────
|
|
281
|
+
updateBar(98, 'Saving metadata...');
|
|
282
|
+
const meta = {
|
|
283
|
+
repoPath,
|
|
284
|
+
lastCommit: currentCommit,
|
|
285
|
+
indexedAt: new Date().toISOString(),
|
|
286
|
+
stats: {
|
|
287
|
+
files: pipelineResult.totalFileCount,
|
|
288
|
+
nodes: stats.nodes,
|
|
289
|
+
edges: stats.edges,
|
|
290
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
291
|
+
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
await saveMeta(storagePath, meta);
|
|
295
|
+
const registeredRepo = await registerRepo(repoPath, meta, { repoAlias });
|
|
296
|
+
await addToGitignore(repoPath);
|
|
297
|
+
const hookResult = await registerClaudeHook();
|
|
298
|
+
const projectName = path.basename(repoPath);
|
|
299
|
+
let aggregatedClusterCount = 0;
|
|
300
|
+
if (pipelineResult.communityResult?.communities) {
|
|
301
|
+
const groups = new Map();
|
|
302
|
+
for (const c of pipelineResult.communityResult.communities) {
|
|
303
|
+
const label = c.heuristicLabel || c.label || 'Unknown';
|
|
304
|
+
groups.set(label, (groups.get(label) || 0) + c.symbolCount);
|
|
305
|
+
}
|
|
306
|
+
aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
|
|
307
|
+
}
|
|
308
|
+
const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
309
|
+
files: pipelineResult.totalFileCount,
|
|
310
|
+
nodes: stats.nodes,
|
|
311
|
+
edges: stats.edges,
|
|
312
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
313
|
+
clusters: aggregatedClusterCount,
|
|
314
|
+
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
315
|
+
});
|
|
316
|
+
await closeKuzu();
|
|
317
|
+
// Note: we intentionally do NOT call disposeEmbedder() here.
|
|
318
|
+
// ONNX Runtime's native cleanup segfaults on macOS and some Linux configs.
|
|
319
|
+
// Since the process exits immediately after, Node.js reclaims everything.
|
|
320
|
+
const totalTime = ((Date.now() - t0Global) / 1000).toFixed(1);
|
|
321
|
+
clearInterval(elapsedTimer);
|
|
322
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
323
|
+
console.log = origLog;
|
|
324
|
+
console.warn = origWarn;
|
|
325
|
+
console.error = origError;
|
|
326
|
+
bar.update(100, { phase: 'Done' });
|
|
327
|
+
bar.stop();
|
|
328
|
+
// ── Summary ───────────────────────────────────────────────────────
|
|
329
|
+
const embeddingsCached = cachedEmbeddings.length > 0;
|
|
330
|
+
console.log(`\n Repository indexed successfully (${totalTime}s)${embeddingsCached ? ` [${cachedEmbeddings.length} embeddings cached]` : ''}\n`);
|
|
331
|
+
console.log(` Repo Name: ${registeredRepo.name}`);
|
|
332
|
+
console.log(` Repo Alias: ${registeredRepo.alias || 'none'}`);
|
|
333
|
+
console.log(` Scope Rules: ${scopeRules.length}`);
|
|
334
|
+
console.log(` Scoped Files: ${pipelineResult.totalFileCount}`);
|
|
335
|
+
if (scopeRules.length > 0 && pipelineResult.scopeDiagnostics) {
|
|
336
|
+
const diagnostics = pipelineResult.scopeDiagnostics;
|
|
337
|
+
console.log(` Scope Overlap Files: ${diagnostics.overlapFiles} (${diagnostics.dedupedMatchCount} duplicate matches removed)`);
|
|
338
|
+
if (diagnostics.normalizedCollisions.length === 0) {
|
|
339
|
+
console.log(' Scope Collisions: none');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
console.warn(` Scope Collisions: ${diagnostics.normalizedCollisions.length} normalized path conflict(s) detected`);
|
|
343
|
+
diagnostics.normalizedCollisions.slice(0, 5).forEach((collision) => {
|
|
344
|
+
console.warn(` - ${collision.normalizedPath} <= ${collision.paths.join(' | ')}`);
|
|
345
|
+
});
|
|
346
|
+
if (diagnostics.normalizedCollisions.length > 5) {
|
|
347
|
+
console.warn(` ... ${diagnostics.normalizedCollisions.length - 5} more`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineResult.processResult?.stats.totalProcesses || 0} flows`);
|
|
352
|
+
console.log(` KuzuDB ${kuzuTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
|
|
353
|
+
if (includeExtensions.length > 0) {
|
|
354
|
+
console.log(` File filter: ${includeExtensions.join(', ')}`);
|
|
355
|
+
}
|
|
356
|
+
console.log(` ${repoPath}`);
|
|
357
|
+
if (aiContext.files.length > 0) {
|
|
358
|
+
console.log(` Context: ${aiContext.files.join(', ')}`);
|
|
359
|
+
}
|
|
360
|
+
if (hookResult.registered) {
|
|
361
|
+
console.log(` Hooks: ${hookResult.message}`);
|
|
362
|
+
}
|
|
363
|
+
// Show a quiet summary if some edge types needed fallback insertion
|
|
364
|
+
if (kuzuWarnings.length > 0) {
|
|
365
|
+
const totalFallback = kuzuWarnings.reduce((sum, w) => {
|
|
366
|
+
const m = w.match(/\((\d+) edges\)/);
|
|
367
|
+
return sum + (m ? parseInt(m[1]) : 0);
|
|
368
|
+
}, 0);
|
|
369
|
+
console.log(` Note: ${totalFallback} edges across ${kuzuWarnings.length} types inserted via fallback (schema will be updated in next release)`);
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await fs.access(getGlobalRegistryPath());
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
|
|
376
|
+
}
|
|
377
|
+
console.log('');
|
|
378
|
+
// ONNX Runtime registers native atexit hooks that segfault during process
|
|
379
|
+
// shutdown on macOS (#38) and some Linux configs (#40). Force-exit to
|
|
380
|
+
// bypass them when embeddings were loaded.
|
|
381
|
+
if (!embeddingSkipped) {
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Augment CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Fast-path command for platform hooks.
|
|
5
|
+
* Shells out from Claude Code PreToolUse / Cursor beforeShellExecution hooks.
|
|
6
|
+
*
|
|
7
|
+
* Usage: gitnexus augment <pattern>
|
|
8
|
+
* Returns enriched text to stdout.
|
|
9
|
+
*
|
|
10
|
+
* Performance: Must cold-start fast (<500ms).
|
|
11
|
+
* Skips unnecessary initialization (no web server, no full DB warmup).
|
|
12
|
+
*/
|
|
13
|
+
export declare function augmentCommand(pattern: string): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Augment CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Fast-path command for platform hooks.
|
|
5
|
+
* Shells out from Claude Code PreToolUse / Cursor beforeShellExecution hooks.
|
|
6
|
+
*
|
|
7
|
+
* Usage: gitnexus augment <pattern>
|
|
8
|
+
* Returns enriched text to stdout.
|
|
9
|
+
*
|
|
10
|
+
* Performance: Must cold-start fast (<500ms).
|
|
11
|
+
* Skips unnecessary initialization (no web server, no full DB warmup).
|
|
12
|
+
*/
|
|
13
|
+
import { augment } from '../core/augmentation/engine.js';
|
|
14
|
+
export async function augmentCommand(pattern) {
|
|
15
|
+
if (!pattern || pattern.length < 3) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const result = await augment(pattern, process.cwd());
|
|
20
|
+
if (result) {
|
|
21
|
+
// IMPORTANT: Write to stderr, NOT stdout.
|
|
22
|
+
// KuzuDB's native module captures stdout fd at OS level during init,
|
|
23
|
+
// which makes stdout permanently broken in subprocess contexts.
|
|
24
|
+
// stderr is never captured, so it works reliably everywhere.
|
|
25
|
+
// The hook reads from the subprocess's stderr.
|
|
26
|
+
process.stderr.write(result + '\n');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Graceful failure — never break the calling hook
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { runAnalyze } from '../benchmark/analyze-runner.js';
|
|
2
|
+
import { writeAgentContextReports } from '../benchmark/agent-context/report.js';
|
|
3
|
+
import { runAgentContextBenchmark } from '../benchmark/agent-context/runner.js';
|
|
4
|
+
import type { AgentContextDataset } from '../benchmark/agent-context/types.js';
|
|
5
|
+
export declare function resolveAgentContextProfile(profile: string): {
|
|
6
|
+
maxScenarios: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function resolveAgentContextRepoName(options: {
|
|
9
|
+
repo?: string;
|
|
10
|
+
repoAlias?: string;
|
|
11
|
+
targetPath?: string;
|
|
12
|
+
}): string | undefined;
|
|
13
|
+
export declare function benchmarkAgentContextCommand(dataset: string, options: {
|
|
14
|
+
profile?: string;
|
|
15
|
+
repo?: string;
|
|
16
|
+
repoAlias?: string;
|
|
17
|
+
targetPath?: string;
|
|
18
|
+
reportDir?: string;
|
|
19
|
+
extensions?: string;
|
|
20
|
+
scopeManifest?: string;
|
|
21
|
+
scopePrefix?: string[];
|
|
22
|
+
skipAnalyze?: boolean;
|
|
23
|
+
}, deps?: {
|
|
24
|
+
loadDataset?: (root: string) => Promise<AgentContextDataset>;
|
|
25
|
+
runBenchmark?: typeof runAgentContextBenchmark;
|
|
26
|
+
writeReports?: typeof writeAgentContextReports;
|
|
27
|
+
writeLine?: (line: string) => void;
|
|
28
|
+
analyze?: typeof runAnalyze;
|
|
29
|
+
}): Promise<import("../benchmark/agent-context/runner.js").AgentContextBenchmarkResult>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { runAnalyze } from '../benchmark/analyze-runner.js';
|
|
3
|
+
import { loadAgentContextDataset } from '../benchmark/agent-context/io.js';
|
|
4
|
+
import { writeAgentContextReports } from '../benchmark/agent-context/report.js';
|
|
5
|
+
import { runAgentContextBenchmark } from '../benchmark/agent-context/runner.js';
|
|
6
|
+
export function resolveAgentContextProfile(profile) {
|
|
7
|
+
if (profile === 'quick') {
|
|
8
|
+
return { maxScenarios: 1 };
|
|
9
|
+
}
|
|
10
|
+
return { maxScenarios: Number.MAX_SAFE_INTEGER };
|
|
11
|
+
}
|
|
12
|
+
export function resolveAgentContextRepoName(options) {
|
|
13
|
+
return options.repo || options.repoAlias || (options.targetPath ? path.basename(path.resolve(options.targetPath)) : undefined);
|
|
14
|
+
}
|
|
15
|
+
export async function benchmarkAgentContextCommand(dataset, options, deps) {
|
|
16
|
+
const loadDataset = deps?.loadDataset || loadAgentContextDataset;
|
|
17
|
+
const runBenchmark = deps?.runBenchmark || runAgentContextBenchmark;
|
|
18
|
+
const writeReports = deps?.writeReports || writeAgentContextReports;
|
|
19
|
+
const writeLine = deps?.writeLine || ((line) => process.stderr.write(`${line}\n`));
|
|
20
|
+
const analyze = deps?.analyze || runAnalyze;
|
|
21
|
+
const profile = options.profile || 'quick';
|
|
22
|
+
const profileConfig = resolveAgentContextProfile(profile);
|
|
23
|
+
const reportDir = path.resolve(options.reportDir || '.gitnexus/benchmark-agent-context');
|
|
24
|
+
if (!(options.skipAnalyze ?? false)) {
|
|
25
|
+
if (!options.targetPath) {
|
|
26
|
+
throw new Error('targetPath is required unless skipAnalyze is true');
|
|
27
|
+
}
|
|
28
|
+
const analyzePath = path.resolve(options.targetPath);
|
|
29
|
+
const analyzeOptions = {
|
|
30
|
+
extensions: options.extensions || '.cs',
|
|
31
|
+
repoAlias: options.repoAlias,
|
|
32
|
+
scopeManifest: options.scopeManifest,
|
|
33
|
+
scopePrefix: options.scopePrefix,
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
await analyze(analyzePath, analyzeOptions);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = String(error?.message || error);
|
|
40
|
+
if (!message.includes('analyze failed: null')) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
// Retry once for transient child-process exits (observed as code=null).
|
|
44
|
+
await analyze(analyzePath, analyzeOptions);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const datasetRoot = path.resolve(dataset);
|
|
48
|
+
const ds = await loadDataset(datasetRoot);
|
|
49
|
+
const result = await runBenchmark(ds, {
|
|
50
|
+
repo: resolveAgentContextRepoName(options),
|
|
51
|
+
reportDir,
|
|
52
|
+
profile: profileConfig,
|
|
53
|
+
});
|
|
54
|
+
await writeReports(reportDir, result);
|
|
55
|
+
writeLine(`${result.pass ? 'PASS' : 'FAIL'}`);
|
|
56
|
+
writeLine(`Report: ${result.reportDir}`);
|
|
57
|
+
if (!result.pass) {
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { benchmarkAgentContextCommand, resolveAgentContextProfile } from './benchmark-agent-context.js';
|
|
4
|
+
test('benchmark-agent-context resolves profile and runs runner', async () => {
|
|
5
|
+
const quick = resolveAgentContextProfile('quick');
|
|
6
|
+
const full = resolveAgentContextProfile('full');
|
|
7
|
+
assert.equal(quick.maxScenarios, 1);
|
|
8
|
+
assert.equal(full.maxScenarios, Number.MAX_SAFE_INTEGER);
|
|
9
|
+
const calls = [];
|
|
10
|
+
const output = [];
|
|
11
|
+
await benchmarkAgentContextCommand('../benchmarks/agent-context/neonspark-refactor-v1', {
|
|
12
|
+
profile: 'quick',
|
|
13
|
+
repoAlias: 'neonspark-v1-subset',
|
|
14
|
+
reportDir: '.gitnexus/benchmark-agent-context-test',
|
|
15
|
+
skipAnalyze: true,
|
|
16
|
+
}, {
|
|
17
|
+
loadDataset: async () => ({
|
|
18
|
+
thresholds: {
|
|
19
|
+
coverage: { minPerScenario: 0.5, suiteAvgMin: 0.5 },
|
|
20
|
+
efficiency: { maxToolCallsPerScenario: 4, suiteAvgMax: 4 },
|
|
21
|
+
},
|
|
22
|
+
scenarios: [],
|
|
23
|
+
}),
|
|
24
|
+
runBenchmark: async (_dataset, options) => {
|
|
25
|
+
calls.push({ repo: options.repo, profile: options.profile });
|
|
26
|
+
return {
|
|
27
|
+
pass: true,
|
|
28
|
+
failures: [],
|
|
29
|
+
reportDir: options.reportDir || '.gitnexus/benchmark-agent-context-test',
|
|
30
|
+
metrics: { avgCoverage: 1, avgToolCalls: 1, mandatoryTargetPassRate: 1 },
|
|
31
|
+
scenarios: [],
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
writeReports: async () => { },
|
|
35
|
+
writeLine: (line) => output.push(line),
|
|
36
|
+
analyze: async () => ({ stdout: '', stderr: '' }),
|
|
37
|
+
});
|
|
38
|
+
assert.equal(calls[0].repo, 'neonspark-v1-subset');
|
|
39
|
+
assert.equal(calls[0].profile.maxScenarios, 1);
|
|
40
|
+
assert.ok(output.some((line) => line.includes('Report:')));
|
|
41
|
+
});
|
|
42
|
+
test('benchmark-agent-context retries analyze once on null exit failure', async () => {
|
|
43
|
+
let analyzeCalls = 0;
|
|
44
|
+
let runCalls = 0;
|
|
45
|
+
await benchmarkAgentContextCommand('../benchmarks/agent-context/neonspark-refactor-v1', {
|
|
46
|
+
profile: 'quick',
|
|
47
|
+
repoAlias: 'neonspark-v1-subset',
|
|
48
|
+
reportDir: '.gitnexus/benchmark-agent-context-test',
|
|
49
|
+
targetPath: '/tmp/neonspark',
|
|
50
|
+
}, {
|
|
51
|
+
loadDataset: async () => ({
|
|
52
|
+
thresholds: {
|
|
53
|
+
coverage: { minPerScenario: 0.5, suiteAvgMin: 0.5 },
|
|
54
|
+
efficiency: { maxToolCallsPerScenario: 4, suiteAvgMax: 4 },
|
|
55
|
+
},
|
|
56
|
+
scenarios: [],
|
|
57
|
+
}),
|
|
58
|
+
runBenchmark: async () => {
|
|
59
|
+
runCalls += 1;
|
|
60
|
+
return {
|
|
61
|
+
pass: true,
|
|
62
|
+
failures: [],
|
|
63
|
+
reportDir: '.gitnexus/benchmark-agent-context-test',
|
|
64
|
+
metrics: { avgCoverage: 1, avgToolCalls: 1, mandatoryTargetPassRate: 1 },
|
|
65
|
+
scenarios: [],
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
writeReports: async () => { },
|
|
69
|
+
writeLine: () => { },
|
|
70
|
+
analyze: async () => {
|
|
71
|
+
analyzeCalls += 1;
|
|
72
|
+
if (analyzeCalls === 1) {
|
|
73
|
+
throw new Error('analyze failed: null');
|
|
74
|
+
}
|
|
75
|
+
return { stdout: '', stderr: '' };
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
assert.equal(analyzeCalls, 2);
|
|
79
|
+
assert.equal(runCalls, 1);
|
|
80
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function resolveProfileConfig(profile: string): {
|
|
2
|
+
maxSymbols: number;
|
|
3
|
+
maxTasks: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function benchmarkUnityCommand(dataset: string, options: {
|
|
6
|
+
profile?: string;
|
|
7
|
+
repo?: string;
|
|
8
|
+
repoAlias?: string;
|
|
9
|
+
targetPath?: string;
|
|
10
|
+
reportDir?: string;
|
|
11
|
+
extensions?: string;
|
|
12
|
+
scopeManifest?: string;
|
|
13
|
+
scopePrefix?: string[];
|
|
14
|
+
skipAnalyze?: boolean;
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadBenchmarkDataset } from '../benchmark/io.js';
|
|
3
|
+
import { runBenchmark } from '../benchmark/runner.js';
|
|
4
|
+
export function resolveProfileConfig(profile) {
|
|
5
|
+
if (profile === 'quick') {
|
|
6
|
+
return { maxSymbols: 10, maxTasks: 5 };
|
|
7
|
+
}
|
|
8
|
+
return { maxSymbols: Number.MAX_SAFE_INTEGER, maxTasks: Number.MAX_SAFE_INTEGER };
|
|
9
|
+
}
|
|
10
|
+
export async function benchmarkUnityCommand(dataset, options) {
|
|
11
|
+
const profile = options.profile || 'quick';
|
|
12
|
+
const profileConfig = resolveProfileConfig(profile);
|
|
13
|
+
const datasetRoot = path.resolve(dataset);
|
|
14
|
+
const ds = await loadBenchmarkDataset(datasetRoot);
|
|
15
|
+
const result = await runBenchmark(ds, {
|
|
16
|
+
repo: options.repo,
|
|
17
|
+
repoAlias: options.repoAlias,
|
|
18
|
+
targetPath: options.targetPath,
|
|
19
|
+
profile: profileConfig,
|
|
20
|
+
reportDir: options.reportDir,
|
|
21
|
+
extensions: options.extensions || '.cs',
|
|
22
|
+
scopeManifest: options.scopeManifest,
|
|
23
|
+
scopePrefix: options.scopePrefix,
|
|
24
|
+
skipAnalyze: options.skipAnalyze ?? false,
|
|
25
|
+
});
|
|
26
|
+
process.stderr.write(`${result.pass ? 'PASS' : 'FAIL'}\n`);
|
|
27
|
+
process.stderr.write(`Report: ${result.reportDir}\n`);
|
|
28
|
+
if (!result.pass) {
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { resolveProfileConfig } from './benchmark-unity.js';
|
|
5
|
+
test('quick profile uses reduced sample limits', () => {
|
|
6
|
+
const c = resolveProfileConfig('quick');
|
|
7
|
+
assert.equal(c.maxSymbols, 10);
|
|
8
|
+
assert.equal(c.maxTasks, 5);
|
|
9
|
+
});
|
|
10
|
+
test('package scripts include neonspark benchmark commands', async () => {
|
|
11
|
+
const raw = await fs.readFile('package.json', 'utf-8');
|
|
12
|
+
const pkg = JSON.parse(raw);
|
|
13
|
+
const scripts = pkg.scripts || {};
|
|
14
|
+
assert.ok(scripts['benchmark:neonspark:full']);
|
|
15
|
+
assert.ok(scripts['benchmark:neonspark:quick']);
|
|
16
|
+
assert.ok(scripts['benchmark:neonspark:v2:full']);
|
|
17
|
+
assert.ok(scripts['benchmark:neonspark:v2:quick']);
|
|
18
|
+
});
|