gitnexus 1.6.6-rc.21 → 1.6.6-rc.23
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 +2 -1
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +20 -0
- package/dist/cli/index.js +1 -0
- package/dist/core/ingestion/languages/cpp/conversion-rank.d.ts +19 -21
- package/dist/core/ingestion/languages/cpp/conversion-rank.js +43 -25
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +4 -2
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +2 -2
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +39 -5
- package/dist/core/lbug/lbug-adapter.js +2 -1
- package/dist/core/run-analyze.d.ts +6 -0
- package/dist/core/run-analyze.js +78 -2
- package/dist/core/search/fts-indexes.d.ts +6 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/mcp/local/local-backend.js +1 -1
- package/dist/server/api.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,7 +151,8 @@ Your AI agent gets these tools automatically:
|
|
|
151
151
|
```bash
|
|
152
152
|
gitnexus setup # Configure MCP for your editors (one-time)
|
|
153
153
|
gitnexus analyze [path] # Index a repository (or update stale index)
|
|
154
|
-
gitnexus analyze --
|
|
154
|
+
gitnexus analyze --repair-fts # Fast path: rebuild/verify only FTS indexes on existing index data
|
|
155
|
+
gitnexus analyze --force # Full rebuild: re-parse + graph rebuild + FTS rebuild
|
|
155
156
|
gitnexus analyze --embeddings # Enable embedding generation (slower, better search)
|
|
156
157
|
gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
|
|
157
158
|
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
|
package/dist/cli/analyze.d.ts
CHANGED
package/dist/cli/analyze.js
CHANGED
|
@@ -216,6 +216,12 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
216
216
|
}
|
|
217
217
|
process.env.GITNEXUS_EMBEDDING_DEVICE = options.embeddingDevice;
|
|
218
218
|
}
|
|
219
|
+
if (options?.repairFts && options?.force) {
|
|
220
|
+
cliError(' Cannot combine `--repair-fts` with `--force`. ' +
|
|
221
|
+
'Use `--repair-fts` for fast FTS-only repair, or `--force` for a full rebuild.\n');
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
219
225
|
console.log('\n GitNexus Analyzer\n');
|
|
220
226
|
// `--index-only` is the stronger contract — it suppresses every form of file
|
|
221
227
|
// injection, including community skill writes that `--skills` would normally
|
|
@@ -368,9 +374,11 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
368
374
|
// needs a fresh pipelineResult. Has no bearing on the registry
|
|
369
375
|
// collision guard (see allowDuplicateName below).
|
|
370
376
|
force: options?.force || options?.skills,
|
|
377
|
+
repairFts: options?.repairFts,
|
|
371
378
|
embeddings: embeddingsEnabled,
|
|
372
379
|
embeddingsNodeLimit,
|
|
373
380
|
dropEmbeddings: options?.dropEmbeddings,
|
|
381
|
+
verbose: options?.verbose,
|
|
374
382
|
skipGit: options?.skipGit,
|
|
375
383
|
skipAgentsMd,
|
|
376
384
|
skipSkills,
|
|
@@ -411,6 +419,18 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
411
419
|
// runFullAnalysis never opens LadybugDB, so no native handles prevent exit.
|
|
412
420
|
return;
|
|
413
421
|
}
|
|
422
|
+
if (result.ftsRepairedOnly) {
|
|
423
|
+
clearInterval(elapsedTimer);
|
|
424
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
425
|
+
console.log = origLog;
|
|
426
|
+
// eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
|
|
427
|
+
console.warn = origWarn;
|
|
428
|
+
// eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
|
|
429
|
+
console.error = origError;
|
|
430
|
+
bar.stop();
|
|
431
|
+
console.log(' FTS indexes repaired successfully\n');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
414
434
|
// Post-finalize invariant (#1169): runFullAnalysis nominally writes
|
|
415
435
|
// meta.json and registers the repo, but on Windows it has been
|
|
416
436
|
// observed to return successfully with neither artifact present
|
package/dist/cli/index.js
CHANGED
|
@@ -17,6 +17,7 @@ program
|
|
|
17
17
|
.command('analyze [path]')
|
|
18
18
|
.description('Index a repository (full analysis)')
|
|
19
19
|
.option('-f, --force', 'Force full re-index even if up to date')
|
|
20
|
+
.option('--repair-fts', 'Repair/rebuild search FTS indexes without full re-analysis')
|
|
20
21
|
.option('--embeddings [limit]', 'Enable embedding generation for semantic search (off by default). ' +
|
|
21
22
|
'Optional [limit] overrides the 50,000-node safety cap; pass 0 to disable the cap entirely.')
|
|
22
23
|
.option('--drop-embeddings', 'Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` ' +
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* C++ conversion-rank scoring for overload resolution (#1578).
|
|
2
|
+
* C++ conversion-rank scoring for overload resolution (#1578, #1637).
|
|
3
3
|
*
|
|
4
|
-
* Operates on
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - char → 'char', bool → 'bool'
|
|
9
|
-
*
|
|
10
|
-
* Because the normalizer collapses promotion pairs (int↔long,
|
|
11
|
-
* float↔double) to the same string, those promotions are invisible at
|
|
12
|
-
* this layer — they appear as exact matches (rank 0).
|
|
4
|
+
* Operates on normalized type strings (output of `normalizeCppParamType`
|
|
5
|
+
* in `arity-metadata.ts`) plus optional shape sidecars from #1630.
|
|
6
|
+
* Normalization intentionally collapses cv/ref/pointer spelling for stable
|
|
7
|
+
* graph IDs, so pointer/nullptr rules must consult `ParameterTypeClass`.
|
|
13
8
|
*
|
|
14
9
|
* Post-normalization ranking:
|
|
15
|
-
* - rank 0
|
|
16
|
-
* - rank 1
|
|
17
|
-
* - rank 2
|
|
18
|
-
*
|
|
19
|
-
* -
|
|
10
|
+
* - rank 0: exact (same normalized type)
|
|
11
|
+
* - rank 1: integral promotion (char -> int, bool -> int)
|
|
12
|
+
* - rank 2: standard conversion (arithmetic, nullptr -> T*, T* -> bool,
|
|
13
|
+
* T* -> void*)
|
|
14
|
+
* - rank 3: nullptr -> bool (kept worse than nullptr -> T*)
|
|
15
|
+
* - rank 4: ellipsis conversion (worst viable)
|
|
16
|
+
* - Infinity: mismatch (string -> int, user types, unsupported shapes)
|
|
20
17
|
*
|
|
21
|
-
* This function is intentionally C++-specific
|
|
22
|
-
*
|
|
23
|
-
* languages may define their own `ConversionRankFn` in the future.
|
|
18
|
+
* This function is intentionally C++-specific. Other languages may define
|
|
19
|
+
* their own `ConversionRankFn` in the future.
|
|
24
20
|
*/
|
|
21
|
+
import type { ParameterTypeClass } from '../../../../_shared/index.js';
|
|
25
22
|
/**
|
|
26
23
|
* Return the conversion rank from `argType` to `paramType`.
|
|
27
24
|
*
|
|
28
|
-
* @returns 0 for exact match, 1 for integral promotion
|
|
29
|
-
*
|
|
25
|
+
* @returns 0 for exact match, 1 for integral promotion, 2 for standard
|
|
26
|
+
* conversion, 3 for nullptr -> bool, 4 for ellipsis, Infinity
|
|
27
|
+
* for mismatch.
|
|
30
28
|
*/
|
|
31
|
-
export declare function cppConversionRank(argType: string, paramType: string): number;
|
|
29
|
+
export declare function cppConversionRank(argType: string, paramType: string, argTypeClass?: ParameterTypeClass, paramTypeClass?: ParameterTypeClass): number;
|
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* C++ conversion-rank scoring for overload resolution (#1578).
|
|
2
|
+
* C++ conversion-rank scoring for overload resolution (#1578, #1637).
|
|
3
3
|
*
|
|
4
|
-
* Operates on
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - char → 'char', bool → 'bool'
|
|
9
|
-
*
|
|
10
|
-
* Because the normalizer collapses promotion pairs (int↔long,
|
|
11
|
-
* float↔double) to the same string, those promotions are invisible at
|
|
12
|
-
* this layer — they appear as exact matches (rank 0).
|
|
4
|
+
* Operates on normalized type strings (output of `normalizeCppParamType`
|
|
5
|
+
* in `arity-metadata.ts`) plus optional shape sidecars from #1630.
|
|
6
|
+
* Normalization intentionally collapses cv/ref/pointer spelling for stable
|
|
7
|
+
* graph IDs, so pointer/nullptr rules must consult `ParameterTypeClass`.
|
|
13
8
|
*
|
|
14
9
|
* Post-normalization ranking:
|
|
15
|
-
* - rank 0
|
|
16
|
-
* - rank 1
|
|
17
|
-
* - rank 2
|
|
18
|
-
*
|
|
19
|
-
* -
|
|
10
|
+
* - rank 0: exact (same normalized type)
|
|
11
|
+
* - rank 1: integral promotion (char -> int, bool -> int)
|
|
12
|
+
* - rank 2: standard conversion (arithmetic, nullptr -> T*, T* -> bool,
|
|
13
|
+
* T* -> void*)
|
|
14
|
+
* - rank 3: nullptr -> bool (kept worse than nullptr -> T*)
|
|
15
|
+
* - rank 4: ellipsis conversion (worst viable)
|
|
16
|
+
* - Infinity: mismatch (string -> int, user types, unsupported shapes)
|
|
20
17
|
*
|
|
21
|
-
* This function is intentionally C++-specific
|
|
22
|
-
*
|
|
23
|
-
* languages may define their own `ConversionRankFn` in the future.
|
|
18
|
+
* This function is intentionally C++-specific. Other languages may define
|
|
19
|
+
* their own `ConversionRankFn` in the future.
|
|
24
20
|
*/
|
|
25
21
|
/** Set of normalized arithmetic types that support implicit conversion. */
|
|
26
22
|
const ARITHMETIC = new Set(['int', 'double', 'char', 'bool']);
|
|
27
|
-
/** Integral promotion targets: char
|
|
23
|
+
/** Integral promotion targets: char -> int and bool -> int are rank 1. */
|
|
28
24
|
const INTEGRAL_PROMOTION = new Map([
|
|
29
25
|
['char', 'int'],
|
|
30
26
|
['bool', 'int'],
|
|
@@ -32,16 +28,38 @@ const INTEGRAL_PROMOTION = new Map([
|
|
|
32
28
|
/**
|
|
33
29
|
* Return the conversion rank from `argType` to `paramType`.
|
|
34
30
|
*
|
|
35
|
-
* @returns 0 for exact match, 1 for integral promotion
|
|
36
|
-
*
|
|
31
|
+
* @returns 0 for exact match, 1 for integral promotion, 2 for standard
|
|
32
|
+
* conversion, 3 for nullptr -> bool, 4 for ellipsis, Infinity
|
|
33
|
+
* for mismatch.
|
|
37
34
|
*/
|
|
38
|
-
export function cppConversionRank(argType, paramType) {
|
|
39
|
-
if (argType === paramType)
|
|
40
|
-
return 0;
|
|
41
|
-
|
|
35
|
+
export function cppConversionRank(argType, paramType, argTypeClass, paramTypeClass) {
|
|
36
|
+
if (argType === paramType) {
|
|
37
|
+
return exactShapeCompatible(argTypeClass, paramTypeClass) ? 0 : Infinity;
|
|
38
|
+
}
|
|
39
|
+
if (paramType === '...')
|
|
40
|
+
return 4;
|
|
42
41
|
if (INTEGRAL_PROMOTION.get(argType) === paramType)
|
|
43
42
|
return 1;
|
|
44
43
|
if (ARITHMETIC.has(argType) && ARITHMETIC.has(paramType))
|
|
45
44
|
return 2;
|
|
45
|
+
if (argType === 'null' && isPointer(paramTypeClass))
|
|
46
|
+
return 2;
|
|
47
|
+
if (argType === 'null' && paramType === 'bool')
|
|
48
|
+
return 3;
|
|
49
|
+
if (isPointer(argTypeClass) && paramType === 'bool')
|
|
50
|
+
return 2;
|
|
51
|
+
if (isPointer(argTypeClass) && isPointer(paramTypeClass) && paramType === 'void')
|
|
52
|
+
return 2;
|
|
46
53
|
return Infinity;
|
|
47
54
|
}
|
|
55
|
+
function isPointer(typeClass) {
|
|
56
|
+
return typeClass?.indirection === 'pointer' && typeClass.pointerDepth > 0;
|
|
57
|
+
}
|
|
58
|
+
function exactShapeCompatible(argTypeClass, paramTypeClass) {
|
|
59
|
+
if (argTypeClass === undefined || paramTypeClass === undefined)
|
|
60
|
+
return true;
|
|
61
|
+
if (argTypeClass.indirection === 'unknown' || paramTypeClass.indirection === 'unknown') {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return isPointer(argTypeClass) === isPointer(paramTypeClass);
|
|
65
|
+
}
|
|
@@ -202,7 +202,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
202
202
|
callerScope: site.inScope,
|
|
203
203
|
scopes,
|
|
204
204
|
})
|
|
205
|
-
: undefined, site.argumentTypes, options.conversionRankFn);
|
|
205
|
+
: undefined, site.argumentTypes, site.argumentTypeClasses, options.conversionRankFn);
|
|
206
206
|
}
|
|
207
207
|
if (fnDef === undefined)
|
|
208
208
|
continue;
|
|
@@ -261,7 +261,7 @@ function buildGlobalCallableIndex(scopes) {
|
|
|
261
261
|
}
|
|
262
262
|
return out;
|
|
263
263
|
}
|
|
264
|
-
function pickUniqueGlobalCallable(name, model, globalCallablesBySimpleName, callerFilePath, isFileLocalDef, callArity, isCallerVisible, callArgTypes, conversionRankFn) {
|
|
264
|
+
function pickUniqueGlobalCallable(name, model, globalCallablesBySimpleName, callerFilePath, isFileLocalDef, callArity, isCallerVisible, callArgTypes, callArgTypeClasses, conversionRankFn) {
|
|
265
265
|
const scopeDefs = [];
|
|
266
266
|
const scopeSeen = new Set();
|
|
267
267
|
for (const def of globalCallablesBySimpleName.get(name) ?? []) {
|
|
@@ -300,6 +300,7 @@ function pickUniqueGlobalCallable(name, model, globalCallablesBySimpleName, call
|
|
|
300
300
|
// disambiguate (e.g., `f(int)` vs `f(double)` called with `f(2.5)`).
|
|
301
301
|
if (scopeDefs.length > 1) {
|
|
302
302
|
const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, {
|
|
303
|
+
argumentTypeClasses: callArgTypeClasses,
|
|
303
304
|
conversionRankFn,
|
|
304
305
|
});
|
|
305
306
|
if (narrowed.length === 1)
|
|
@@ -340,6 +341,7 @@ function pickUniqueGlobalCallable(name, model, globalCallablesBySimpleName, call
|
|
|
340
341
|
// Same argument-type + conversion-rank narrowing for the model pool.
|
|
341
342
|
if (defs.length > 1) {
|
|
342
343
|
const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, {
|
|
344
|
+
argumentTypeClasses: callArgTypeClasses,
|
|
343
345
|
conversionRankFn,
|
|
344
346
|
});
|
|
345
347
|
if (narrowed.length === 1)
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* (monotonicity).
|
|
38
38
|
* 5. Empty input returns empty output.
|
|
39
39
|
*/
|
|
40
|
-
import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from '../../../../_shared/index.js';
|
|
40
|
+
import type { ArityVerdict, Callsite, ConstraintContext, ParameterTypeClass, SymbolDefinition } from '../../../../_shared/index.js';
|
|
41
41
|
/**
|
|
42
42
|
* Per-slot conversion-rank function. Returns a numeric cost for
|
|
43
43
|
* converting `argType` to `paramType`:
|
|
@@ -49,7 +49,7 @@ import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from
|
|
|
49
49
|
* Each language provides its own implementation. The function operates
|
|
50
50
|
* on normalized type strings (output of the language's type normalizer).
|
|
51
51
|
*/
|
|
52
|
-
export type ConversionRankFn = (argType: string, paramType: string) => number;
|
|
52
|
+
export type ConversionRankFn = (argType: string, paramType: string, argTypeClass?: ParameterTypeClass, paramTypeClass?: ParameterTypeClass) => number;
|
|
53
53
|
/**
|
|
54
54
|
* Optional hook bundle for narrowing extension points. Threaded in
|
|
55
55
|
* from `pickOverload` / `pickImplicitThisOverload` so per-language
|
|
@@ -81,8 +81,9 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx)
|
|
|
81
81
|
for (let i = 0; i < argTypes.length && i < params.length; i++) {
|
|
82
82
|
if (argTypes[i] === '')
|
|
83
83
|
continue;
|
|
84
|
-
if (argTypes[i]
|
|
84
|
+
if (!exactTypeSlotMatches(argTypes[i], params[i], hookCtx?.argumentTypeClasses?.[i], d.parameterTypeClasses?.[i])) {
|
|
85
85
|
return false;
|
|
86
|
+
}
|
|
86
87
|
}
|
|
87
88
|
return true;
|
|
88
89
|
});
|
|
@@ -97,7 +98,7 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx)
|
|
|
97
98
|
// are returned; multiple survivors are genuinely ambiguous. When
|
|
98
99
|
// ranking also yields empty, fall through to the arity-filtered
|
|
99
100
|
// `candidates` set — matches pre-#1606 behavior.
|
|
100
|
-
const ranked = rankByConversion(candidates, argTypes, hookCtx.conversionRankFn);
|
|
101
|
+
const ranked = rankByConversion(candidates, argTypes, hookCtx.conversionRankFn, hookCtx.argumentTypeClasses);
|
|
101
102
|
if (ranked.length > 0)
|
|
102
103
|
result = ranked;
|
|
103
104
|
}
|
|
@@ -134,6 +135,22 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx)
|
|
|
134
135
|
}
|
|
135
136
|
return result;
|
|
136
137
|
}
|
|
138
|
+
function exactTypeSlotMatches(argType, paramType, argTypeClass, paramTypeClass) {
|
|
139
|
+
if (argType !== paramType)
|
|
140
|
+
return false;
|
|
141
|
+
// C++ normalizes away pointer markers (`int*` -> `int`). When both sides
|
|
142
|
+
// provide shape sidecars, do not let that collapse make `int` exactly match
|
|
143
|
+
// `int*`. Unknown sidecar evidence preserves the previous string-only path.
|
|
144
|
+
if (argTypeClass === undefined || paramTypeClass === undefined)
|
|
145
|
+
return true;
|
|
146
|
+
if (argTypeClass.indirection === 'unknown' || paramTypeClass.indirection === 'unknown') {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return isPointerShape(argTypeClass) === isPointerShape(paramTypeClass);
|
|
150
|
+
}
|
|
151
|
+
function isPointerShape(typeClass) {
|
|
152
|
+
return typeClass.indirection === 'pointer' && typeClass.pointerDepth > 0;
|
|
153
|
+
}
|
|
137
154
|
/**
|
|
138
155
|
* Pairwise dominance comparison (ISO C++ [over.ics.rank]).
|
|
139
156
|
*
|
|
@@ -146,7 +163,7 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx)
|
|
|
146
163
|
* Candidates with at least one `Infinity`-ranked slot (incompatible
|
|
147
164
|
* type) are excluded before pairwise comparison begins.
|
|
148
165
|
*/
|
|
149
|
-
function rankByConversion(candidates, argTypes, rankFn) {
|
|
166
|
+
function rankByConversion(candidates, argTypes, rankFn, argTypeClasses) {
|
|
150
167
|
// Step 1: compute per-slot ranks and exclude non-viable candidates.
|
|
151
168
|
const viable = [];
|
|
152
169
|
for (const d of candidates) {
|
|
@@ -155,12 +172,17 @@ function rankByConversion(candidates, argTypes, rankFn) {
|
|
|
155
172
|
continue;
|
|
156
173
|
const ranks = [];
|
|
157
174
|
let ok = true;
|
|
158
|
-
for (let i = 0; i < argTypes.length
|
|
175
|
+
for (let i = 0; i < argTypes.length; i++) {
|
|
176
|
+
const paramType = parameterTypeAt(params, i);
|
|
177
|
+
if (paramType === undefined) {
|
|
178
|
+
ok = false;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
159
181
|
if (argTypes[i] === '') {
|
|
160
182
|
ranks.push(0); // unknown arg → any-match (rank 0)
|
|
161
183
|
continue;
|
|
162
184
|
}
|
|
163
|
-
const r = rankFn(argTypes[i],
|
|
185
|
+
const r = rankFn(argTypes[i], paramType, argTypeClasses?.[i], parameterTypeClassAt(d.parameterTypeClasses, i));
|
|
164
186
|
if (!isFinite(r)) {
|
|
165
187
|
ok = false;
|
|
166
188
|
break;
|
|
@@ -190,6 +212,18 @@ function rankByConversion(candidates, argTypes, rankFn) {
|
|
|
190
212
|
}
|
|
191
213
|
return viable.filter((_, idx) => !dominated.has(idx)).map((v) => v.def);
|
|
192
214
|
}
|
|
215
|
+
function parameterTypeAt(params, argIndex) {
|
|
216
|
+
if (argIndex < params.length)
|
|
217
|
+
return params[argIndex];
|
|
218
|
+
return params[params.length - 1] === '...' ? '...' : undefined;
|
|
219
|
+
}
|
|
220
|
+
function parameterTypeClassAt(params, argIndex) {
|
|
221
|
+
if (params === undefined)
|
|
222
|
+
return undefined;
|
|
223
|
+
if (argIndex < params.length)
|
|
224
|
+
return params[argIndex];
|
|
225
|
+
return params[params.length - 1]?.base === '...' ? params[params.length - 1] : undefined;
|
|
226
|
+
}
|
|
193
227
|
/**
|
|
194
228
|
* Compare two per-slot rank vectors.
|
|
195
229
|
* Returns -1 if `a` dominates `b` (not worse everywhere, better somewhere),
|
|
@@ -1454,7 +1454,8 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
|
|
|
1454
1454
|
if (ensuredFTSIndexes.has(key))
|
|
1455
1455
|
return;
|
|
1456
1456
|
if (!(await loadFTSExtension())) {
|
|
1457
|
-
|
|
1457
|
+
throw new Error(`FTS extension unavailable - cannot create FTS index ${tableName}.${indexName}. ` +
|
|
1458
|
+
'Run `gitnexus doctor` and ensure the LadybugDB FTS extension is installed and loadable on this machine.');
|
|
1458
1459
|
}
|
|
1459
1460
|
const propList = properties.map((p) => `'${p}'`).join(', ');
|
|
1460
1461
|
const query = `CALL CREATE_FTS_INDEX('${tableName}', '${indexName}', [${propList}], stemmer := '${stemmer}')`;
|
|
@@ -20,6 +20,10 @@ export interface AnalyzeOptions {
|
|
|
20
20
|
* bypass. See `allowDuplicateName` below.
|
|
21
21
|
*/
|
|
22
22
|
force?: boolean;
|
|
23
|
+
/** Repair only search indexes without re-running full parsing/indexing. */
|
|
24
|
+
repairFts?: boolean;
|
|
25
|
+
/** Emit per-index FTS create logs. */
|
|
26
|
+
verbose?: boolean;
|
|
23
27
|
embeddings?: boolean;
|
|
24
28
|
/**
|
|
25
29
|
* Override the auto-skip node-count cap for embedding generation.
|
|
@@ -74,6 +78,8 @@ export interface AnalyzeResult {
|
|
|
74
78
|
alreadyUpToDate?: boolean;
|
|
75
79
|
/** The raw pipeline result — only populated when needed by callers (e.g. skill generation). */
|
|
76
80
|
pipelineResult?: any;
|
|
81
|
+
/** True when analyze only repaired FTS indexes and skipped pipeline re-analysis. */
|
|
82
|
+
ftsRepairedOnly?: boolean;
|
|
77
83
|
}
|
|
78
84
|
export { deriveEmbeddingMode, DEFAULT_EMBEDDING_NODE_LIMIT } from './embedding-mode.js';
|
|
79
85
|
export type { EmbeddingMode } from './embedding-mode.js';
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
14
|
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
15
15
|
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, loadCachedEmbeddings, deleteNodesForFile, deleteAllCommunitiesAndProcesses, queryImporters, } from './lbug/lbug-adapter.js';
|
|
16
|
-
import { createSearchFTSIndexes } from './search/fts-indexes.js';
|
|
16
|
+
import { createSearchFTSIndexes, verifySearchFTSIndexes } from './search/fts-indexes.js';
|
|
17
17
|
import { getStoragePaths, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, cleanupOldKuzuFiles, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
|
|
18
18
|
import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
|
|
19
19
|
import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
|
|
@@ -68,6 +68,70 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
68
68
|
const repoHasGit = hasGitDir(repoPath);
|
|
69
69
|
const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
|
|
70
70
|
const existingMeta = await loadMeta(storagePath);
|
|
71
|
+
// ── FTS-only repair path ────────────────────────────────────────────
|
|
72
|
+
if (options.repairFts) {
|
|
73
|
+
if (!existingMeta) {
|
|
74
|
+
throw new Error('Cannot repair FTS indexes because this repository has not been analyzed yet. ' +
|
|
75
|
+
'Run `gitnexus analyze` first to create the initial index, then retry `--repair-fts`.');
|
|
76
|
+
}
|
|
77
|
+
let lbugStat;
|
|
78
|
+
try {
|
|
79
|
+
lbugStat = await fs.lstat(lbugPath);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new Error(`Cannot repair FTS indexes: graph store at ${lbugPath} is missing. ` +
|
|
83
|
+
'Run `gitnexus analyze` (full) to rebuild from scratch.');
|
|
84
|
+
}
|
|
85
|
+
if (!lbugStat.isFile()) {
|
|
86
|
+
const foundType = lbugStat.isDirectory()
|
|
87
|
+
? 'a directory'
|
|
88
|
+
: lbugStat.isSymbolicLink()
|
|
89
|
+
? 'a symbolic link'
|
|
90
|
+
: lbugStat.isSocket()
|
|
91
|
+
? 'a socket'
|
|
92
|
+
: lbugStat.isBlockDevice()
|
|
93
|
+
? 'a block device'
|
|
94
|
+
: lbugStat.isCharacterDevice()
|
|
95
|
+
? 'a character device'
|
|
96
|
+
: lbugStat.isFIFO()
|
|
97
|
+
? 'a FIFO'
|
|
98
|
+
: 'not a regular file';
|
|
99
|
+
throw new Error(`Cannot repair FTS indexes: graph store at ${lbugPath} is ${foundType} (expected a file). ` +
|
|
100
|
+
'Run `gitnexus analyze` (full) to rebuild from scratch.');
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await initLbug(lbugPath);
|
|
104
|
+
progress('fts', 85, 'Repairing search indexes...');
|
|
105
|
+
await createSearchFTSIndexes({
|
|
106
|
+
onIndexStart: options.verbose
|
|
107
|
+
? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
|
|
108
|
+
: undefined,
|
|
109
|
+
onIndexReady: options.verbose
|
|
110
|
+
? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
|
|
111
|
+
: undefined,
|
|
112
|
+
});
|
|
113
|
+
const missing = await verifySearchFTSIndexes(executeQuery);
|
|
114
|
+
if (missing.length > 0) {
|
|
115
|
+
throw new Error(`FTS repair failed - missing indexes after rebuild: ${missing.join(', ')}. ` +
|
|
116
|
+
'Run `gitnexus analyze --force` to perform a full graph+FTS rebuild; ' +
|
|
117
|
+
'if that also fails, verify FTS extension availability via `gitnexus doctor`.');
|
|
118
|
+
}
|
|
119
|
+
await ensureGitNexusIgnored(repoPath);
|
|
120
|
+
progress('fts', 90, 'Search indexes ready');
|
|
121
|
+
progress('done', 100, 'Done');
|
|
122
|
+
return {
|
|
123
|
+
repoName: options.registryName ??
|
|
124
|
+
getInferredRepoName(repoPath) ??
|
|
125
|
+
path.basename(resolveRepoIdentityRoot(repoPath)),
|
|
126
|
+
repoPath,
|
|
127
|
+
stats: existingMeta.stats ?? {},
|
|
128
|
+
ftsRepairedOnly: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await closeLbug().catch(() => { });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
71
135
|
// ── Crash recovery: dirty flag forces full rebuild ────────────────
|
|
72
136
|
// If the previous incremental run set incrementalInProgress and didn't
|
|
73
137
|
// clear it, the on-disk index may be in a half-state. Cheapest path
|
|
@@ -420,7 +484,19 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
420
484
|
}
|
|
421
485
|
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
|
422
486
|
progress('fts', 85, 'Creating search indexes...');
|
|
423
|
-
await createSearchFTSIndexes(
|
|
487
|
+
await createSearchFTSIndexes({
|
|
488
|
+
onIndexStart: options.verbose
|
|
489
|
+
? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
|
|
490
|
+
: undefined,
|
|
491
|
+
onIndexReady: options.verbose
|
|
492
|
+
? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
|
|
493
|
+
: undefined,
|
|
494
|
+
});
|
|
495
|
+
const missingIndexNames = await verifySearchFTSIndexes(executeQuery);
|
|
496
|
+
if (missingIndexNames.length > 0) {
|
|
497
|
+
throw new Error(`FTS verification failed - missing indexes after analyze: ${missingIndexNames.join(', ')}. ` +
|
|
498
|
+
'Check FTS extension availability, then retry `gitnexus analyze --force` for a full rebuild.');
|
|
499
|
+
}
|
|
424
500
|
progress('fts', 90, 'Search indexes ready');
|
|
425
501
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
426
502
|
// Runs on BOTH the full-rebuild path and the incremental path:
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface CreateSearchFTSIndexesOptions {
|
|
2
|
+
onIndexStart?: (table: string, indexName: string) => void;
|
|
3
|
+
onIndexReady?: (table: string, indexName: string) => void;
|
|
4
|
+
}
|
|
5
|
+
export declare function createSearchFTSIndexes(options?: CreateSearchFTSIndexesOptions): Promise<void>;
|
|
6
|
+
export declare function verifySearchFTSIndexes(executeQuery: (cypher: string) => Promise<unknown[]>): Promise<string[]>;
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import { createFTSIndex } from '../lbug/lbug-adapter.js';
|
|
2
2
|
import { FTS_INDEXES } from './fts-schema.js';
|
|
3
|
-
export async function createSearchFTSIndexes() {
|
|
3
|
+
export async function createSearchFTSIndexes(options) {
|
|
4
4
|
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
5
|
+
options?.onIndexStart?.(table, indexName);
|
|
5
6
|
await createFTSIndex(table, indexName, [...properties]);
|
|
7
|
+
options?.onIndexReady?.(table, indexName);
|
|
6
8
|
}
|
|
7
9
|
}
|
|
10
|
+
export async function verifySearchFTSIndexes(executeQuery) {
|
|
11
|
+
const safeIdentifier = (value) => {
|
|
12
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
13
|
+
throw new Error(`Invalid FTS identifier: ${value}`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const missing = [];
|
|
18
|
+
for (const { table, indexName } of FTS_INDEXES) {
|
|
19
|
+
const safeTable = safeIdentifier(table);
|
|
20
|
+
const safeIndex = safeIdentifier(indexName);
|
|
21
|
+
const probe = `
|
|
22
|
+
CALL QUERY_FTS_INDEX('${safeTable}', '${safeIndex}', '__gitnexus_fts_probe__', conjunctive := false)
|
|
23
|
+
RETURN score
|
|
24
|
+
LIMIT 1
|
|
25
|
+
`;
|
|
26
|
+
try {
|
|
27
|
+
await executeQuery(probe);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
missing.push(`${table}.${indexName}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return missing;
|
|
34
|
+
}
|
|
@@ -894,7 +894,7 @@ export class LocalBackend {
|
|
|
894
894
|
definitions: definitions.slice(0, 20), // cap standalone definitions
|
|
895
895
|
timing,
|
|
896
896
|
...(!ftsUsed && {
|
|
897
|
-
warning: 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.',
|
|
897
|
+
warning: 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.',
|
|
898
898
|
}),
|
|
899
899
|
};
|
|
900
900
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -1071,7 +1071,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1071
1071
|
const response = { results: results.searchResults ?? results };
|
|
1072
1072
|
if (results.ftsAvailable === false) {
|
|
1073
1073
|
response.warning =
|
|
1074
|
-
'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.';
|
|
1074
|
+
'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.';
|
|
1075
1075
|
}
|
|
1076
1076
|
res.json(response);
|
|
1077
1077
|
}
|
package/package.json
CHANGED