gitnexus 1.6.12-rc.26 → 1.6.12-rc.28
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/embeddings-sync.d.ts +2 -0
- package/dist/cli/embeddings-sync.js +127 -0
- package/dist/cli/i18n/en.d.ts +2 -1
- package/dist/cli/i18n/en.js +2 -1
- package/dist/cli/i18n/resources.d.ts +3 -1
- package/dist/cli/i18n/zh-CN.d.ts +1 -0
- package/dist/cli/i18n/zh-CN.js +2 -1
- package/dist/cli/index.js +9 -5
- package/dist/core/embeddings/http-client.js +45 -13
- package/dist/core/ingestion/languages/ruby/import-target.d.ts +9 -7
- package/dist/core/ingestion/languages/ruby/import-target.js +69 -8
- package/dist/core/ingestion/languages/ruby/resolution-config.d.ts +28 -0
- package/dist/core/ingestion/languages/ruby/resolution-config.js +311 -0
- package/dist/core/ingestion/languages/ruby/scope-resolver.js +2 -0
- package/dist/core/lbug/schema.d.ts +4 -2
- package/dist/core/lbug/schema.js +4 -1
- package/dist/mcp/local/local-backend.js +13 -5
- package/dist/storage/git.d.ts +17 -0
- package/dist/storage/git.js +221 -4
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cliInfo } from './cli-message.js';
|
|
4
|
+
import { getGitRoot } from '../storage/git.js';
|
|
5
|
+
import { acquireIndexLock } from '../storage/index-lock.js';
|
|
6
|
+
import { getStoragePaths, loadMeta, saveMeta } from '../storage/repo-manager.js';
|
|
7
|
+
import { closeLbug, executeQuery, executeWithReusedStatement, fetchExistingEmbeddingHashes, initLbug, } from '../core/lbug/lbug-adapter.js';
|
|
8
|
+
import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js';
|
|
9
|
+
import { resolveEmbeddingIdentity } from '../core/embeddings/embedding-identity.js';
|
|
10
|
+
import { checkpointKind, decideEmbeddingResume, mintInterruptedCheckpoint, mintPartialCheckpoint, mintUnverifiedCountCheckpoint, } from '../core/embedding-checkpoint.js';
|
|
11
|
+
import { measurePersistedEmbeddingCount, persistedEmbeddingCountOrUndefined, } from '../core/embedding-count.js';
|
|
12
|
+
/** Add missing embeddings directly to a healthy index, checkpointing periodically. */
|
|
13
|
+
export const embeddingsSyncCommand = async (inputPath) => {
|
|
14
|
+
const repoPath = inputPath ? path.resolve(inputPath) : getGitRoot(process.cwd());
|
|
15
|
+
if (!repoPath)
|
|
16
|
+
throw new Error('Not inside a git repository. Pass a repository path.');
|
|
17
|
+
const { lbugPath, metaPath } = getStoragePaths(repoPath);
|
|
18
|
+
const metaDir = path.dirname(metaPath);
|
|
19
|
+
const lock = await acquireIndexLock(metaDir);
|
|
20
|
+
try {
|
|
21
|
+
const meta = await loadMeta(metaDir);
|
|
22
|
+
if (!meta)
|
|
23
|
+
throw new Error(`No GitNexus index found for ${repoPath}. Run gitnexus analyze first.`);
|
|
24
|
+
if (meta.incrementalInProgress) {
|
|
25
|
+
throw new Error('The structural index is incomplete. Run gitnexus analyze --force first.');
|
|
26
|
+
}
|
|
27
|
+
let lbugStat;
|
|
28
|
+
try {
|
|
29
|
+
lbugStat = await lstat(lbugPath);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error(`The LadybugDB graph store at ${lbugPath} is missing. Run gitnexus analyze first.`);
|
|
33
|
+
}
|
|
34
|
+
if (!lbugStat.isFile()) {
|
|
35
|
+
throw new Error(`The LadybugDB graph store at ${lbugPath} is not a usable database file. Run gitnexus analyze first.`);
|
|
36
|
+
}
|
|
37
|
+
const identity = resolveEmbeddingIdentity();
|
|
38
|
+
let forceReembedNodeIds;
|
|
39
|
+
let resumedFrom;
|
|
40
|
+
if (meta.embeddingCheckpoint) {
|
|
41
|
+
const checkpoint = meta.embeddingCheckpoint;
|
|
42
|
+
const decision = decideEmbeddingResume(checkpoint, identity);
|
|
43
|
+
if (decision.action === 'abort')
|
|
44
|
+
throw new Error(decision.error);
|
|
45
|
+
const identityDiffers = checkpoint.provider !== identity.provider ||
|
|
46
|
+
checkpoint.model !== identity.model ||
|
|
47
|
+
checkpoint.dimensions !== identity.dimensions;
|
|
48
|
+
// `abandon` on a non-interrupted foreign identity drops the pending set
|
|
49
|
+
// only. Existing rows stay; sync would then embed the holes under the new
|
|
50
|
+
// identity and mix vector spaces. Fail closed — rebuild via analyze.
|
|
51
|
+
if (identityDiffers && checkpointKind(checkpoint) !== 'unverified-count') {
|
|
52
|
+
throw new Error(`Cannot sync embeddings: the index checkpoint was written by ${checkpoint.model} ` +
|
|
53
|
+
`(${checkpoint.provider}) at ${checkpoint.dimensions} dimensions, but this run ` +
|
|
54
|
+
`resolves ${identity.model} (${identity.provider}) at ${identity.dimensions}. ` +
|
|
55
|
+
'Run `gitnexus analyze --embeddings --force` to rebuild under the new identity.');
|
|
56
|
+
}
|
|
57
|
+
cliInfo(decision.log);
|
|
58
|
+
if (decision.action === 'resume') {
|
|
59
|
+
forceReembedNodeIds = decision.pendingNodeIds;
|
|
60
|
+
resumedFrom = decision.resumedFrom;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await initLbug(lbugPath);
|
|
64
|
+
try {
|
|
65
|
+
const existing = await fetchExistingEmbeddingHashes(executeQuery);
|
|
66
|
+
let lastPercent = -1;
|
|
67
|
+
const countEmbeddings = async () => persistedEmbeddingCountOrUndefined(await measurePersistedEmbeddingCount(executeQuery));
|
|
68
|
+
const saveCheckpoint = async (checkpoint, pendingNodeIds, embeddings) => {
|
|
69
|
+
const latest = (await loadMeta(metaDir)) ?? meta;
|
|
70
|
+
await saveMeta(metaDir, {
|
|
71
|
+
...latest,
|
|
72
|
+
...(embeddings === undefined ? {} : { stats: { ...latest.stats, embeddings } }),
|
|
73
|
+
embeddingCheckpoint: mintInterruptedCheckpoint(identity, checkpoint, pendingNodeIds),
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
cliInfo(`Embedding ${repoPath}`);
|
|
77
|
+
cliInfo(`Checkpointed nodes already present: ${existing?.size ?? 0}`);
|
|
78
|
+
const result = await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
|
|
79
|
+
const percent = Math.floor(progress.percent);
|
|
80
|
+
if (percent !== lastPercent && (percent % 5 === 0 || percent === 100)) {
|
|
81
|
+
lastPercent = percent;
|
|
82
|
+
cliInfo(` ${percent}% — ${progress.nodesProcessed ?? 0}/${progress.totalNodes ?? '?'} nodes`);
|
|
83
|
+
}
|
|
84
|
+
}, {}, undefined, existing && existing.size ? existing : undefined, {
|
|
85
|
+
forceReembedNodeIds,
|
|
86
|
+
onCheckpointWindowStart: async ({ nodeIds, ...checkpoint }) => {
|
|
87
|
+
await saveCheckpoint(checkpoint, nodeIds);
|
|
88
|
+
},
|
|
89
|
+
onCheckpoint: async (checkpoint) => {
|
|
90
|
+
await saveCheckpoint(checkpoint, [], await countEmbeddings());
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const embeddings = await countEmbeddings();
|
|
94
|
+
const latest = (await loadMeta(metaDir)) ?? meta;
|
|
95
|
+
if (embeddings === undefined) {
|
|
96
|
+
// Keep last-known stats.embeddings. An interrupted window marker would
|
|
97
|
+
// fail the identity gate on the next run even though this run finished;
|
|
98
|
+
// unverified-count is the recovery kind that forces a recount (#2790).
|
|
99
|
+
await saveMeta(metaDir, {
|
|
100
|
+
...latest,
|
|
101
|
+
embeddingCheckpoint: result.failedNodeIds.length
|
|
102
|
+
? mintPartialCheckpoint(identity, result, resumedFrom)
|
|
103
|
+
: mintUnverifiedCountCheckpoint(identity, {
|
|
104
|
+
nodesProcessed: result.nodesProcessed,
|
|
105
|
+
totalNodes: result.nodesProcessed,
|
|
106
|
+
chunksProcessed: result.chunksProcessed,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
throw new Error('Could not verify persisted embedding count.');
|
|
110
|
+
}
|
|
111
|
+
await saveMeta(metaDir, {
|
|
112
|
+
...latest,
|
|
113
|
+
stats: { ...latest.stats, embeddings },
|
|
114
|
+
embeddingCheckpoint: result.failedNodeIds.length
|
|
115
|
+
? mintPartialCheckpoint(identity, result, resumedFrom)
|
|
116
|
+
: undefined,
|
|
117
|
+
});
|
|
118
|
+
cliInfo(`Embeddings ready: ${embeddings}`);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await closeLbug().catch(() => { });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
lock.release();
|
|
126
|
+
}
|
|
127
|
+
};
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -140,6 +140,7 @@ export declare const en: {
|
|
|
140
140
|
readonly 'help.watch.details': "\n`gitnexus watch` does not start a watcher.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n";
|
|
141
141
|
readonly 'error.watch.ambiguous': "`gitnexus watch` is ambiguous.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n";
|
|
142
142
|
readonly 'help.command.analyze.description': "Index a repository (full analysis)";
|
|
143
|
+
readonly 'help.command.embeddings.sync.description': "Add missing embeddings to an existing index, checkpointing periodically for safe resume";
|
|
143
144
|
readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
|
|
144
145
|
readonly 'help.command.serve.description': "Start local HTTP server for web UI connection";
|
|
145
146
|
readonly 'help.command.mcp.description': "Start MCP server. Default: stdio. Use --http for a remote HTTP server (Streamable HTTP at POST /mcp + legacy SSE at GET /sse, POST /messages).";
|
|
@@ -277,5 +278,5 @@ export declare const en: {
|
|
|
277
278
|
readonly 'help.option.group.contracts.repo': "Filter by repo";
|
|
278
279
|
readonly 'help.option.group.contracts.unmatched': "Show only unmatched contracts";
|
|
279
280
|
readonly 'help.identityCache.environment': "\nAnalyzer identity cache:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n Operator-trusted persistent cache for warm cross-process status. The directory must pre-exist, be outside the GitNexus package/build roots, and contain no symlink or junction components. Defaults remain fail-closed on platforms without POSIX ownership APIs.";
|
|
280
|
-
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
281
|
+
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
281
282
|
};
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -142,6 +142,7 @@ export const en = {
|
|
|
142
142
|
'help.watch.details': '\n`gitnexus watch` does not start a watcher.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n',
|
|
143
143
|
'error.watch.ambiguous': '`gitnexus watch` is ambiguous.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n',
|
|
144
144
|
'help.command.analyze.description': 'Index a repository (full analysis)',
|
|
145
|
+
'help.command.embeddings.sync.description': 'Add missing embeddings to an existing index, checkpointing periodically for safe resume',
|
|
145
146
|
'help.command.index.description': 'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)',
|
|
146
147
|
'help.command.serve.description': 'Start local HTTP server for web UI connection',
|
|
147
148
|
'help.command.mcp.description': 'Start MCP server. Default: stdio. Use --http for a remote HTTP server (Streamable HTTP at POST /mcp + legacy SSE at GET /sse, POST /messages).',
|
|
@@ -279,5 +280,5 @@ export const en = {
|
|
|
279
280
|
'help.option.group.contracts.repo': 'Filter by repo',
|
|
280
281
|
'help.option.group.contracts.unmatched': 'Show only unmatched contracts',
|
|
281
282
|
'help.identityCache.environment': '\nAnalyzer identity cache:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n Operator-trusted persistent cache for warm cross-process status. The directory must pre-exist, be outside the GitNexus package/build roots, and contain no symlink or junction components. Defaults remain fail-closed on platforms without POSIX ownership APIs.',
|
|
282
|
-
'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).',
|
|
283
|
+
'help.analyze.environment': '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).',
|
|
283
284
|
};
|
|
@@ -141,6 +141,7 @@ export declare const cliResources: {
|
|
|
141
141
|
readonly 'help.watch.details': "\n`gitnexus watch` does not start a watcher.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n";
|
|
142
142
|
readonly 'error.watch.ambiguous': "`gitnexus watch` is ambiguous.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n";
|
|
143
143
|
readonly 'help.command.analyze.description': "Index a repository (full analysis)";
|
|
144
|
+
readonly 'help.command.embeddings.sync.description': "Add missing embeddings to an existing index, checkpointing periodically for safe resume";
|
|
144
145
|
readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
|
|
145
146
|
readonly 'help.command.serve.description': "Start local HTTP server for web UI connection";
|
|
146
147
|
readonly 'help.command.mcp.description': "Start MCP server. Default: stdio. Use --http for a remote HTTP server (Streamable HTTP at POST /mcp + legacy SSE at GET /sse, POST /messages).";
|
|
@@ -278,7 +279,7 @@ export declare const cliResources: {
|
|
|
278
279
|
readonly 'help.option.group.contracts.repo': "Filter by repo";
|
|
279
280
|
readonly 'help.option.group.contracts.unmatched': "Show only unmatched contracts";
|
|
280
281
|
readonly 'help.identityCache.environment': "\nAnalyzer identity cache:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n Operator-trusted persistent cache for warm cross-process status. The directory must pre-exist, be outside the GitNexus package/build roots, and contain no symlink or junction components. Defaults remain fail-closed on platforms without POSIX ownership APIs.";
|
|
281
|
-
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
282
|
+
readonly 'help.analyze.environment': "\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir Operator-trusted persistent analyzer identity cache; must pre-exist, be outside package/build roots, and contain no symlink/junction components.\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL auto-checkpoint threshold in bytes (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N Max wait at pool shutdown for a retired worker still inside native code (terminated at its next safe point instead of aborting the process). Default 30000.\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N Per-file wall-clock budget for C++ capture extraction; on breach the file keeps partial captures with a warning. Default 20000.\n GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 Retry per-attempt HTTP embedding timeouts through GITNEXUS_EMBEDDING_MAX_ATTEMPTS (default off; timeouts stay terminal).\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n GITNEXUS_VECTOR_MAX_DISTANCE=N Max accepted semantic/vector cosine distance (0 < N <= 2; higher values clamp to 2). Default 0.6 for MCP, 0.5 elsewhere.\n\nFlags override the corresponding env vars when both are provided.\n\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n `!__tests__/` to index a directory that is auto-filtered by default (#771).";
|
|
282
283
|
};
|
|
283
284
|
readonly 'zh-CN': {
|
|
284
285
|
'common.notIndexed': string;
|
|
@@ -422,6 +423,7 @@ export declare const cliResources: {
|
|
|
422
423
|
'help.watch.details': string;
|
|
423
424
|
'error.watch.ambiguous': string;
|
|
424
425
|
'help.command.analyze.description': string;
|
|
426
|
+
'help.command.embeddings.sync.description': string;
|
|
425
427
|
'help.command.index.description': string;
|
|
426
428
|
'help.command.serve.description': string;
|
|
427
429
|
'help.command.mcp.description': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -140,6 +140,7 @@ export declare const zhCN: {
|
|
|
140
140
|
'help.watch.details': string;
|
|
141
141
|
'error.watch.ambiguous': string;
|
|
142
142
|
'help.command.analyze.description': string;
|
|
143
|
+
'help.command.embeddings.sync.description': string;
|
|
143
144
|
'help.command.index.description': string;
|
|
144
145
|
'help.command.serve.description': string;
|
|
145
146
|
'help.command.mcp.description': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -140,6 +140,7 @@ export const zhCN = {
|
|
|
140
140
|
'help.watch.details': '\n`gitnexus watch` 不会启动监视器。\n 本地工作区增量索引:gitnexus analyze --watch\n 定时远程 clone/pull 并分析:gitnexus auto-sync start\n',
|
|
141
141
|
'error.watch.ambiguous': '`gitnexus watch` 含义不明确。\n 本地工作区增量索引:gitnexus analyze --watch\n 定时远程 clone/pull 并分析:gitnexus auto-sync start\n',
|
|
142
142
|
'help.command.analyze.description': '索引仓库(完整分析)',
|
|
143
|
+
'help.command.embeddings.sync.description': '向现有索引添加缺失的嵌入,并定期保存检查点以安全续跑',
|
|
143
144
|
'help.command.index.description': '将现有 .gitnexus/ 文件夹注册到全局注册表(无需重新分析)',
|
|
144
145
|
'help.command.serve.description': '启动供 Web UI 连接的本地 HTTP 服务器',
|
|
145
146
|
'help.command.mcp.description': '启动 MCP 服务器。默认为 stdio。使用 --http 启动远程 HTTP 服务器(Streamable HTTP: POST /mcp + 遗留 SSE: GET /sse, POST /messages)。',
|
|
@@ -277,5 +278,5 @@ export const zhCN = {
|
|
|
277
278
|
'help.option.group.contracts.repo': '按仓库过滤',
|
|
278
279
|
'help.option.group.contracts.unmatched': '仅显示未匹配契约',
|
|
279
280
|
'help.identityCache.environment': '\n分析器身份缓存:\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir\n 由操作员明确信任的持久缓存,用于跨进程快速查询状态。目录必须预先存在、位于 GitNexus 包/构建根目录之外,且路径中不得包含符号链接或 junction。缺少 POSIX 所有权 API 的平台默认保持故障关闭。',
|
|
280
|
-
'help.analyze.environment': '\n环境变量:\n GITNEXUS_NO_GITIGNORE=1 跳过 .gitignore 解析(仍读取 .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N 覆盖大文件跳过阈值(KB)。默认 512,最大 32768。\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir 由操作员明确信任的持久分析器身份缓存;目录必须预先存在、位于包/构建根目录之外,且路径中不得包含符号链接或 junction。\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker 空闲超时(毫秒)。默认 30000。\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL 自动 checkpoint 阈值(字节,默认 67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker 作业字节预算。默认 8388608。\n GITNEXUS_WORKER_POOL_SIZE=N 解析 worker 数量覆盖值。默认 cores-1,最多 16。\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N 并发进行中的解析分块数。默认 2。\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N 每个 slot 丢弃前允许的最大替换进程数。默认 3。\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N 每个作业的总重试墙钟时间。默认 5 倍子批次超时。\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N 每个 slot 触发熔断的死亡次数。默认 max(3, poolSize)。\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N 线程池关闭时等待仍在原生代码中的已退役 worker 的最长时间(到达安全点后再终止,避免进程级 abort)。默认 30000。\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N C++ 捕获提取的每文件墙钟预算;超出后该文件保留部分捕获并输出警告。默认 20000。\n GITNEXUS_EMBEDDING_THREADS=N 限制 --embeddings 的本地 ONNX CPU 线程数。\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N exact-scan 回退的最大嵌入分块数。默认 10000。\n GITNEXUS_VECTOR_MAX_DISTANCE=N 语义/向量搜索接受的最大余弦距离(0 < N <= 2;超出则钳制为 2)。MCP 默认 0.6,其他路径默认 0.5。\n\n当参数和对应环境变量同时提供时,参数优先。\n\n提示:`.gitnexusignore` 支持 `.gitignore` 风格的取反。比如添加\n `!__tests__/` 可以索引默认自动过滤的目录(#771)。',
|
|
281
|
+
'help.analyze.environment': '\n环境变量:\n GITNEXUS_NO_GITIGNORE=1 跳过 .gitignore 解析(仍读取 .gitnexusignore)\n GITNEXUS_MAX_FILE_SIZE=N 覆盖大文件跳过阈值(KB)。默认 512,最大 32768。\n GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/absolute/protected/dir 由操作员明确信任的持久分析器身份缓存;目录必须预先存在、位于包/构建根目录之外,且路径中不得包含符号链接或 junction。\n GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker 空闲超时(毫秒)。默认 30000。\n GITNEXUS_WAL_CHECKPOINT_THRESHOLD=N LadybugDB WAL 自动 checkpoint 阈值(字节,默认 67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。\n GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker 作业字节预算。默认 8388608。\n GITNEXUS_WORKER_POOL_SIZE=N 解析 worker 数量覆盖值。默认 cores-1,最多 16。\n GITNEXUS_PARSE_CHUNK_CONCURRENCY=N 并发进行中的解析分块数。默认 2。\n GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N 每个 slot 丢弃前允许的最大替换进程数。默认 3。\n GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N 每个作业的总重试墙钟时间。默认 5 倍子批次超时。\n GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N 每个 slot 触发熔断的死亡次数。默认 max(3, poolSize)。\n GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS=N 线程池关闭时等待仍在原生代码中的已退役 worker 的最长时间(到达安全点后再终止,避免进程级 abort)。默认 30000。\n GITNEXUS_CPP_CAPTURE_BUDGET_MS=N C++ 捕获提取的每文件墙钟预算;超出后该文件保留部分捕获并输出警告。默认 20000。\n GITNEXUS_EMBEDDING_THREADS=N 限制 --embeddings 的本地 ONNX CPU 线程数。\n GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1 将单次 HTTP 嵌入超时纳入 GITNEXUS_EMBEDDING_MAX_ATTEMPTS 重试(默认关闭,超时仍为终止错误)。\n GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N exact-scan 回退的最大嵌入分块数。默认 10000。\n GITNEXUS_VECTOR_MAX_DISTANCE=N 语义/向量搜索接受的最大余弦距离(0 < N <= 2;超出则钳制为 2)。MCP 默认 0.6,其他路径默认 0.5。\n\n当参数和对应环境变量同时提供时,参数优先。\n\n提示:`.gitnexusignore` 支持 `.gitignore` 风格的取反。比如添加\n `!__tests__/` 可以索引默认自动过滤的目录(#771)。',
|
|
281
282
|
};
|
package/dist/cli/index.js
CHANGED
|
@@ -184,17 +184,21 @@ program
|
|
|
184
184
|
.command('update')
|
|
185
185
|
.description('Install the latest published GitNexus globally (`npm i -g gitnexus@<x.y.z>`).')
|
|
186
186
|
.action(createLazyAction(() => import('./update.js'), 'updateCommand'));
|
|
187
|
-
program
|
|
187
|
+
const embeddings = program
|
|
188
188
|
.command('embeddings')
|
|
189
|
-
.description('
|
|
189
|
+
.description(t('help.command.embeddings.description'));
|
|
190
|
+
embeddings
|
|
190
191
|
.command('install')
|
|
191
|
-
.description('
|
|
192
|
-
'Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). ' +
|
|
193
|
-
'Downloads only from your configured npm registry — mirrors and proxies apply.')
|
|
192
|
+
.description(t('help.command.embeddings.install.description'))
|
|
194
193
|
.option('--cuda', "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; " +
|
|
195
194
|
'set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)')
|
|
196
195
|
.option('--force', 'Install into the runtime prefix even when the stack already resolves')
|
|
197
196
|
.action(createLazyAction(() => import('./embeddings.js'), 'embeddingsInstallCommand'));
|
|
197
|
+
embeddings
|
|
198
|
+
.command('sync [path]')
|
|
199
|
+
.description(t('help.command.embeddings.sync.description'))
|
|
200
|
+
.addHelpText('after', () => t('help.analyze.environment'))
|
|
201
|
+
.action(createLbugLazyAction(() => import('./embeddings-sync.js'), 'embeddingsSyncCommand'));
|
|
198
202
|
program
|
|
199
203
|
.command('clean')
|
|
200
204
|
.description('Delete GitNexus index for current repo')
|
|
@@ -157,6 +157,7 @@ const readConfig = () => {
|
|
|
157
157
|
retryCapMs: parsePositiveIntegerEnv('GITNEXUS_EMBEDDING_RETRY_CAP_MS', HTTP_RETRY_CAP_MS, 300_000),
|
|
158
158
|
minIntervalMs: parseNonNegativeIntegerEnv('GITNEXUS_EMBEDDING_MIN_INTERVAL_MS', 0, 300_000),
|
|
159
159
|
timeoutMs: parsePositiveIntegerEnv(HTTP_TIMEOUT_ENV, DEFAULT_HTTP_TIMEOUT_MS, MAX_HTTP_TIMEOUT_MS),
|
|
160
|
+
retryTimeouts: parseNonNegativeIntegerEnv('GITNEXUS_EMBEDDING_RETRY_TIMEOUTS', 0, 1) === 1,
|
|
160
161
|
requestDimensions,
|
|
161
162
|
};
|
|
162
163
|
};
|
|
@@ -277,6 +278,23 @@ class RetryableEmbeddingBodyError extends Error {
|
|
|
277
278
|
this.name = 'RetryableEmbeddingBodyError';
|
|
278
279
|
}
|
|
279
280
|
}
|
|
281
|
+
class RetryableEmbeddingTimeoutError extends Error {
|
|
282
|
+
timeoutMs;
|
|
283
|
+
constructor(timeoutMs, options) {
|
|
284
|
+
super(`Embedding request timed out after ${timeoutMs}ms`, options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
285
|
+
this.timeoutMs = timeoutMs;
|
|
286
|
+
this.name = 'RetryableEmbeddingTimeoutError';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Re-wrap an opt-in TimeoutError so `resilientFetch` retries it. Abort stays terminal. */
|
|
290
|
+
const throwIfRetryableTimeout = (err, retryTimeouts, callerAborted, timeoutMs) => {
|
|
291
|
+
if (retryTimeouts &&
|
|
292
|
+
!callerAborted &&
|
|
293
|
+
isTerminalNetworkError(err) &&
|
|
294
|
+
err.name === 'TimeoutError') {
|
|
295
|
+
throw new RetryableEmbeddingTimeoutError(timeoutMs, { cause: err });
|
|
296
|
+
}
|
|
297
|
+
};
|
|
280
298
|
/**
|
|
281
299
|
* Build the message for a 2xx body carrying the wrong number of vectors.
|
|
282
300
|
*
|
|
@@ -304,7 +322,7 @@ const countMismatchMessage = (received, expected, safeEndpoint, batchIndex) => `
|
|
|
304
322
|
* `GITNEXUS_EMBEDDING_REQUEST_DIMS=omit` for strict backends while keeping
|
|
305
323
|
* `GITNEXUS_EMBEDDING_DIMS` set to the returned vector size.
|
|
306
324
|
*/
|
|
307
|
-
const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0, dimensions, requestOptions = {}, maxAttempts = HTTP_MAX_RETRIES + 1, retryCapMs = HTTP_RETRY_CAP_MS, minIntervalMs = 0, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) => {
|
|
325
|
+
const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0, dimensions, requestOptions = {}, maxAttempts = HTTP_MAX_RETRIES + 1, retryCapMs = HTTP_RETRY_CAP_MS, minIntervalMs = 0, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS, retryTimeouts = false) => {
|
|
308
326
|
const requestBody = {
|
|
309
327
|
input: batch,
|
|
310
328
|
model,
|
|
@@ -341,7 +359,14 @@ const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0, dimensi
|
|
|
341
359
|
const signal = requestOptions.signal
|
|
342
360
|
? AbortSignal.any([requestOptions.signal, timeoutSignal])
|
|
343
361
|
: timeoutSignal;
|
|
344
|
-
|
|
362
|
+
let attemptResp;
|
|
363
|
+
try {
|
|
364
|
+
attemptResp = await globalThis.fetch(input, { ...init, signal });
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
throwIfRetryableTimeout(err, retryTimeouts, requestOptions.signal?.aborted, timeoutMs);
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
345
370
|
// Non-OK bodies are none of our business: hand the response straight
|
|
346
371
|
// back so `resilientFetch` keeps classifying 4xx/5xx/429 unchanged.
|
|
347
372
|
if (!attemptResp.ok)
|
|
@@ -361,15 +386,19 @@ const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0, dimensi
|
|
|
361
386
|
// Not every `.json()` rejection is a parse error: the per-attempt
|
|
362
387
|
// signal (`AbortSignal.any([caller, AbortSignal.timeout(...)])`) is
|
|
363
388
|
// wired to the body stream, so a stalled body rejects with the abort
|
|
364
|
-
// reason. Re-raise
|
|
365
|
-
// `resilientFetch`'s own
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
389
|
+
// reason. Re-raise AbortError (and TimeoutError when retry is off)
|
|
390
|
+
// untouched — `isTerminalNetworkError` is `resilientFetch`'s own
|
|
391
|
+
// predicate, so this test agrees with `classifyOutcome` by
|
|
392
|
+
// construction. Wrapping one would flip its verdict from
|
|
393
|
+
// `terminal-network` (returned without retry AND without touching
|
|
394
|
+
// the breaker, via `recordNeutral()`) to `retryable-network`
|
|
395
|
+
// (retried, then `breaker.recordFailure()`): the same timeout would
|
|
396
|
+
// take 3 attempts instead of 1, count toward the process-global
|
|
397
|
+
// `embeddings-http` breaker, and reach the operator as "unparseable
|
|
398
|
+
// response" so they never reach for the timeout knob.
|
|
399
|
+
// Opt-in `GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1` is the exception:
|
|
400
|
+
// TimeoutError is re-wrapped so the existing retry loop can retry it.
|
|
401
|
+
throwIfRetryableTimeout(err, retryTimeouts, requestOptions.signal?.aborted, timeoutMs);
|
|
373
402
|
if (isTerminalNetworkError(err))
|
|
374
403
|
throw err;
|
|
375
404
|
throw new RetryableEmbeddingBodyError(unparseableMessage(), { cause: err });
|
|
@@ -411,6 +440,9 @@ const httpEmbedBatch = async (url, batch, model, apiKey, batchIndex = 0, dimensi
|
|
|
411
440
|
if (err instanceof RetryableEmbeddingBodyError) {
|
|
412
441
|
throw new HttpEmbeddingError(err.terminalMessage, { cause: err.cause });
|
|
413
442
|
}
|
|
443
|
+
if (err instanceof RetryableEmbeddingTimeoutError) {
|
|
444
|
+
throw new HttpEmbeddingError(`${err.message} after ${maxAttempts} attempt(s) (${safeUrl(url)}, batch ${batchIndex})`, { cause: err.cause });
|
|
445
|
+
}
|
|
414
446
|
if (err instanceof CircuitOpenError) {
|
|
415
447
|
throw new HttpEmbeddingError(`Embedding endpoint circuit open (${safeUrl(url)}, batch ${batchIndex}): retry in ${Math.ceil(err.retryAfterMs / 1000)}s`, { cause: err });
|
|
416
448
|
}
|
|
@@ -455,7 +487,7 @@ export const httpEmbed = async (texts, requestOptions = {}) => {
|
|
|
455
487
|
const url = `${config.baseUrl}/embeddings`;
|
|
456
488
|
const allVectors = [];
|
|
457
489
|
for (const [batchIndex, batch] of chunk(texts, HTTP_BATCH_SIZE).entries()) {
|
|
458
|
-
const items = await httpEmbedBatch(url, batch, config.model, config.apiKey, batchIndex, config.requestDimensions, requestOptions, config.maxAttempts, config.retryCapMs, config.minIntervalMs, config.timeoutMs);
|
|
490
|
+
const items = await httpEmbedBatch(url, batch, config.model, config.apiKey, batchIndex, config.requestDimensions, requestOptions, config.maxAttempts, config.retryCapMs, config.minIntervalMs, config.timeoutMs, config.retryTimeouts);
|
|
459
491
|
// Defensive backstop, deliberately kept: `httpEmbedBatch` now rejects a
|
|
460
492
|
// short body from *inside* the retry loop (through the same
|
|
461
493
|
// `countMismatchMessage`), so in practice this branch is unreachable. It
|
|
@@ -506,7 +538,7 @@ export const httpEmbedQuery = async (text, requestOptions = {}) => {
|
|
|
506
538
|
if (!config)
|
|
507
539
|
throw new Error('HTTP embedding not configured');
|
|
508
540
|
const url = `${config.baseUrl}/embeddings`;
|
|
509
|
-
const items = await httpEmbedBatch(url, [text], config.model, config.apiKey, 0, config.requestDimensions, requestOptions, config.maxAttempts, config.retryCapMs, config.minIntervalMs, config.timeoutMs);
|
|
541
|
+
const items = await httpEmbedBatch(url, [text], config.model, config.apiKey, 0, config.requestDimensions, requestOptions, config.maxAttempts, config.retryCapMs, config.minIntervalMs, config.timeoutMs, config.retryTimeouts);
|
|
510
542
|
// Defensive backstop like the `httpEmbed` one above: an empty `data` array is
|
|
511
543
|
// now a cardinality mismatch (0 vectors for 1 text) rejected and retried
|
|
512
544
|
// inside `httpEmbedBatch`, so this branch is unreachable in practice.
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ruby import resolution rules:
|
|
5
5
|
* - `require_relative './foo'` → resolve relative to the importing file's dir
|
|
6
|
-
* - `require 'foo'`
|
|
7
|
-
* -
|
|
6
|
+
* - `require 'foo'` → use scoped gem metadata before legacy suffix matching
|
|
7
|
+
* - Known local gems → resolve only within their declared load roots
|
|
8
|
+
* - Known external gems → null, even if an unrelated repo file suffix matches
|
|
8
9
|
*/
|
|
9
10
|
export interface RubyResolveContext {
|
|
10
11
|
readonly fromFile: string;
|
|
@@ -18,9 +19,10 @@ export interface RubyResolveContext {
|
|
|
18
19
|
* against the importing file's directory, trying `.rb` and `/index.rb`
|
|
19
20
|
* suffixes.
|
|
20
21
|
*
|
|
21
|
-
* For bare requires
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* For bare requires, scoped manifest metadata takes precedence: known local
|
|
23
|
+
* gems resolve only within their declared load roots (a miss returns `null`),
|
|
24
|
+
* and known external gem prefixes return `null` even if a repo suffix matches.
|
|
25
|
+
* Only requires without matching gem evidence delegate to the existing
|
|
26
|
+
* `resolveRubyImportInternal` suffix matcher.
|
|
25
27
|
*/
|
|
26
|
-
export declare function resolveRubyImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>,
|
|
28
|
+
export declare function resolveRubyImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, resolutionConfig?: unknown): string | readonly string[] | null;
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ruby import resolution rules:
|
|
5
5
|
* - `require_relative './foo'` → resolve relative to the importing file's dir
|
|
6
|
-
* - `require 'foo'`
|
|
7
|
-
* -
|
|
6
|
+
* - `require 'foo'` → use scoped gem metadata before legacy suffix matching
|
|
7
|
+
* - Known local gems → resolve only within their declared load roots
|
|
8
|
+
* - Known external gems → null, even if an unrelated repo file suffix matches
|
|
8
9
|
*/
|
|
9
10
|
import { resolveRubyImportInternal } from '../../import-resolvers/ruby.js';
|
|
10
11
|
import { getWorkspaceFileIndex } from '../../import-resolvers/workspace-file-index.js';
|
|
11
12
|
import { isHeritageMarker } from '../../utils/heritage-marker.js';
|
|
13
|
+
import { findRubyResolutionScope, } from './resolution-config.js';
|
|
12
14
|
// ─── resolveRubyImportTarget ──────────────────────────────────────────────
|
|
13
15
|
/**
|
|
14
16
|
* ScopeResolver-shaped adapter:
|
|
@@ -18,12 +20,13 @@ import { isHeritageMarker } from '../../utils/heritage-marker.js';
|
|
|
18
20
|
* against the importing file's directory, trying `.rb` and `/index.rb`
|
|
19
21
|
* suffixes.
|
|
20
22
|
*
|
|
21
|
-
* For bare requires
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* For bare requires, scoped manifest metadata takes precedence: known local
|
|
24
|
+
* gems resolve only within their declared load roots (a miss returns `null`),
|
|
25
|
+
* and known external gem prefixes return `null` even if a repo suffix matches.
|
|
26
|
+
* Only requires without matching gem evidence delegate to the existing
|
|
27
|
+
* `resolveRubyImportInternal` suffix matcher.
|
|
25
28
|
*/
|
|
26
|
-
export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths,
|
|
29
|
+
export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig) {
|
|
27
30
|
if (!targetRaw)
|
|
28
31
|
return null;
|
|
29
32
|
if (isHeritageMarker(targetRaw))
|
|
@@ -37,7 +40,16 @@ export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, _reso
|
|
|
37
40
|
const resolved = resolveRelative(targetRaw, fromDir, allFilePaths);
|
|
38
41
|
return resolved;
|
|
39
42
|
}
|
|
40
|
-
// ── require:
|
|
43
|
+
// ── require: scoped gem evidence before repository-wide fallback ────
|
|
44
|
+
const config = resolutionConfig;
|
|
45
|
+
const scope = config === null || config === undefined ? undefined : findRubyResolutionScope(config, fromFile);
|
|
46
|
+
if (scope !== undefined) {
|
|
47
|
+
const localGemTarget = resolveLocalGemTarget(targetRaw, scope, allFilePaths);
|
|
48
|
+
if (localGemTarget !== undefined)
|
|
49
|
+
return localGemTarget;
|
|
50
|
+
if (matchesRequirePrefix(targetRaw, scope.externalRequirePrefixes))
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
41
53
|
return resolveBare(targetRaw, allFilePaths);
|
|
42
54
|
}
|
|
43
55
|
// ─── internal helpers ─────────────────────────────────────────────────────
|
|
@@ -73,6 +85,55 @@ function resolveRelative(targetRaw, fromDir, allFilePaths) {
|
|
|
73
85
|
return resolvedPath;
|
|
74
86
|
return null;
|
|
75
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Yield the require stem, then each slash-delimited ancestor. This makes both
|
|
90
|
+
* local-root and external-gem lookup O(require path depth), not O(gem count).
|
|
91
|
+
* A trailing `.rb` is stripped first so `require 'my_engine.rb'` matches the
|
|
92
|
+
* configured prefix `my_engine`, matching Ruby's optional-suffix require.
|
|
93
|
+
*/
|
|
94
|
+
function requirePrefixCandidates(targetRaw) {
|
|
95
|
+
const stem = targetRaw.endsWith('.rb') ? targetRaw.slice(0, -3) : targetRaw;
|
|
96
|
+
if (stem.length === 0)
|
|
97
|
+
return [];
|
|
98
|
+
const candidates = [stem];
|
|
99
|
+
let slash = stem.lastIndexOf('/');
|
|
100
|
+
while (slash !== -1) {
|
|
101
|
+
candidates.push(stem.slice(0, slash));
|
|
102
|
+
slash = stem.lastIndexOf('/', slash - 1);
|
|
103
|
+
}
|
|
104
|
+
return candidates;
|
|
105
|
+
}
|
|
106
|
+
function matchesRequirePrefix(targetRaw, prefixes) {
|
|
107
|
+
return requirePrefixCandidates(targetRaw).some((candidate) => prefixes.has(candidate));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a path/gemspec-backed gem against its declared Ruby load roots.
|
|
111
|
+
* `undefined` means no local-gem prefix matched; `null` means one matched but
|
|
112
|
+
* none of its declared load roots contained the target.
|
|
113
|
+
*/
|
|
114
|
+
function resolveLocalGemTarget(targetRaw, scope, allFilePaths) {
|
|
115
|
+
for (const prefix of requirePrefixCandidates(targetRaw)) {
|
|
116
|
+
const loadRoots = scope.localLoadRootsByPrefix.get(prefix);
|
|
117
|
+
if (loadRoots === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
for (const loadRoot of loadRoots) {
|
|
120
|
+
const base = loadRoot ? `${loadRoot}/${targetRaw}` : targetRaw;
|
|
121
|
+
if (base.endsWith('.rb')) {
|
|
122
|
+
if (allFilePaths.has(base))
|
|
123
|
+
return base;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const rbFile = `${base}.rb`;
|
|
127
|
+
if (allFilePaths.has(rbFile))
|
|
128
|
+
return rbFile;
|
|
129
|
+
}
|
|
130
|
+
// The prefix is owned by a known local gem. If its declared roots do not
|
|
131
|
+
// contain the target, repository-wide suffix matching would fabricate an
|
|
132
|
+
// edge to an unrelated file.
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
76
137
|
/**
|
|
77
138
|
* Resolve a bare require path (`'serializable'`, `'json'`, `'net/http'`)
|
|
78
139
|
* via suffix matching using the existing Ruby import resolver.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RubyResolutionScope {
|
|
2
|
+
/** Require prefixes provided by gems whose source is outside this repository. */
|
|
3
|
+
readonly externalRequirePrefixes: ReadonlySet<string>;
|
|
4
|
+
/** Require prefix -> repository-relative Ruby load roots for local gems. */
|
|
5
|
+
readonly localLoadRootsByPrefix: ReadonlyMap<string, readonly string[]>;
|
|
6
|
+
}
|
|
7
|
+
export interface RubyResolutionConfig {
|
|
8
|
+
/** Manifest directory (repository-relative POSIX path) -> dependency scope. */
|
|
9
|
+
readonly scopesByDirectory: ReadonlyMap<string, RubyResolutionScope>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Select the nearest manifest directory that owns `fromFile`.
|
|
13
|
+
*
|
|
14
|
+
* Walking ancestors makes lookup O(path depth), independent of how many
|
|
15
|
+
* sibling Gemfiles a monorepo contains.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findRubyResolutionScope(config: RubyResolutionConfig, fromFile: string): RubyResolutionScope | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Statically collect Ruby dependency sources without evaluating Gemfile or
|
|
20
|
+
* gemspec code. Scopes stay separate by manifest directory so sibling projects
|
|
21
|
+
* in a monorepo cannot suppress one another's local imports. Lockfiles
|
|
22
|
+
* contribute remote GEM/GIT specs and local PATH/GEMSPEC load roots only when
|
|
23
|
+
* an adjacent Gemfile or gemspec establishes a Bundler/RubyGems project.
|
|
24
|
+
*
|
|
25
|
+
* No declarative manifest means no safe gate, so return null and preserve the
|
|
26
|
+
* resolver's existing fail-open behavior for loose script directories.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadRubyResolutionConfig(repoPath: string): RubyResolutionConfig | null;
|