gitnexus 1.6.10-rc.135 → 1.6.10-rc.136
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/core/lbug/lbug-adapter.d.ts +36 -0
- package/dist/core/lbug/lbug-adapter.js +55 -2
- package/dist/core/run-analyze.js +38 -0
- package/dist/core/search/bm25-index.d.ts +11 -0
- package/dist/core/search/bm25-index.js +57 -20
- package/dist/core/search/fts-indexes.d.ts +50 -1
- package/dist/core/search/fts-indexes.js +49 -4
- package/dist/mcp/local/local-backend.d.ts +3 -2
- package/dist/mcp/local/local-backend.js +114 -29
- package/dist/server/api.js +12 -2
- package/dist/storage/repo-manager.d.ts +6 -3
- package/package.json +1 -1
|
@@ -563,6 +563,42 @@ export declare const ensureEmbeddingRowDmlSafe: () => Promise<boolean>;
|
|
|
563
563
|
* exists or will be created on the next analyze.
|
|
564
564
|
*/
|
|
565
565
|
export declare const ensureFTSIndex: (tableName: string, indexName: string, properties: string[], stemmer?: string) => Promise<void>;
|
|
566
|
+
export type FtsQueryFailureClass = 'missing-index' | 'missing-table' | 'other';
|
|
567
|
+
/**
|
|
568
|
+
* Classify a `QUERY_FTS_INDEX` failure so a genuinely-missing index (normal —
|
|
569
|
+
* this table's FTS index hasn't been built yet) is distinguished from a real
|
|
570
|
+
* query-time error that would otherwise look identical (#2767), and from the
|
|
571
|
+
* table itself being missing (schema drift / a corrupted or partial DB — a
|
|
572
|
+
* much more serious condition than an unbuilt index).
|
|
573
|
+
*
|
|
574
|
+
* tri-review Residual-1: this used to be a second, independently-maintained
|
|
575
|
+
* classifier living in `core/search/bm25-index.ts` (re-exported from there
|
|
576
|
+
* for backward compatibility), duplicating this function's job for the
|
|
577
|
+
* IDENTICAL `QUERY_FTS_INDEX` cypher call. `queryFTS` below now uses this
|
|
578
|
+
* same classifier for its own catch instead of a bare, unanchored
|
|
579
|
+
* `.includes('does not exist')` check that could not tell "index missing"
|
|
580
|
+
* from "table missing" apart, and silently swallowed both alike.
|
|
581
|
+
*
|
|
582
|
+
* Three real message shapes were confirmed empirically against a live
|
|
583
|
+
* `CALL QUERY_FTS_INDEX(...)`:
|
|
584
|
+
* `"Prepare failed: Binder exception: Table <T> doesn't have an index with
|
|
585
|
+
* name <name>."` — the table exists, only its FTS index is missing (normal,
|
|
586
|
+
* benign — `missing-index`) — `"Prepare failed: Binder exception: Table <T>
|
|
587
|
+
* does not exist."` — the TABLE ITSELF is missing (`missing-table`) — and a
|
|
588
|
+
* `Catalog exception: function QUERY_FTS_INDEX is not defined...` when the
|
|
589
|
+
* FTS extension isn't loaded at all (`other`; mirrors the confirmed
|
|
590
|
+
* `DROP_FTS_INDEX` shape in {@link isBenignDropFtsIndexError}'s doc comment).
|
|
591
|
+
*
|
|
592
|
+
* Anchored to the exception class (after stripping the optional "Prepare
|
|
593
|
+
* failed: " wrapper LadybugDB adds for statement-preparation failures),
|
|
594
|
+
* mirroring `isBenignDropFtsIndexError`'s START-of-message anchor: a bare
|
|
595
|
+
* substring search would misclassify a genuine, differently-classed error
|
|
596
|
+
* (e.g. a `Runtime exception` from the FTS parser that echoes the user's
|
|
597
|
+
* own search text back into its message) as benign whenever that echoed
|
|
598
|
+
* text happened to contain "does not exist" — silently dropping a real
|
|
599
|
+
* error, the exact #2767 failure mode this function exists to prevent.
|
|
600
|
+
*/
|
|
601
|
+
export declare const classifyFtsQueryError: (message: string) => FtsQueryFailureClass;
|
|
566
602
|
/**
|
|
567
603
|
* Query a full-text search index
|
|
568
604
|
* @param tableName - The node table name
|
|
@@ -2721,6 +2721,54 @@ export const ensureFTSIndex = async (tableName, indexName, properties, stemmer =
|
|
|
2721
2721
|
throw e;
|
|
2722
2722
|
}
|
|
2723
2723
|
};
|
|
2724
|
+
/**
|
|
2725
|
+
* Classify a `QUERY_FTS_INDEX` failure so a genuinely-missing index (normal —
|
|
2726
|
+
* this table's FTS index hasn't been built yet) is distinguished from a real
|
|
2727
|
+
* query-time error that would otherwise look identical (#2767), and from the
|
|
2728
|
+
* table itself being missing (schema drift / a corrupted or partial DB — a
|
|
2729
|
+
* much more serious condition than an unbuilt index).
|
|
2730
|
+
*
|
|
2731
|
+
* tri-review Residual-1: this used to be a second, independently-maintained
|
|
2732
|
+
* classifier living in `core/search/bm25-index.ts` (re-exported from there
|
|
2733
|
+
* for backward compatibility), duplicating this function's job for the
|
|
2734
|
+
* IDENTICAL `QUERY_FTS_INDEX` cypher call. `queryFTS` below now uses this
|
|
2735
|
+
* same classifier for its own catch instead of a bare, unanchored
|
|
2736
|
+
* `.includes('does not exist')` check that could not tell "index missing"
|
|
2737
|
+
* from "table missing" apart, and silently swallowed both alike.
|
|
2738
|
+
*
|
|
2739
|
+
* Three real message shapes were confirmed empirically against a live
|
|
2740
|
+
* `CALL QUERY_FTS_INDEX(...)`:
|
|
2741
|
+
* `"Prepare failed: Binder exception: Table <T> doesn't have an index with
|
|
2742
|
+
* name <name>."` — the table exists, only its FTS index is missing (normal,
|
|
2743
|
+
* benign — `missing-index`) — `"Prepare failed: Binder exception: Table <T>
|
|
2744
|
+
* does not exist."` — the TABLE ITSELF is missing (`missing-table`) — and a
|
|
2745
|
+
* `Catalog exception: function QUERY_FTS_INDEX is not defined...` when the
|
|
2746
|
+
* FTS extension isn't loaded at all (`other`; mirrors the confirmed
|
|
2747
|
+
* `DROP_FTS_INDEX` shape in {@link isBenignDropFtsIndexError}'s doc comment).
|
|
2748
|
+
*
|
|
2749
|
+
* Anchored to the exception class (after stripping the optional "Prepare
|
|
2750
|
+
* failed: " wrapper LadybugDB adds for statement-preparation failures),
|
|
2751
|
+
* mirroring `isBenignDropFtsIndexError`'s START-of-message anchor: a bare
|
|
2752
|
+
* substring search would misclassify a genuine, differently-classed error
|
|
2753
|
+
* (e.g. a `Runtime exception` from the FTS parser that echoes the user's
|
|
2754
|
+
* own search text back into its message) as benign whenever that echoed
|
|
2755
|
+
* text happened to contain "does not exist" — silently dropping a real
|
|
2756
|
+
* error, the exact #2767 failure mode this function exists to prevent.
|
|
2757
|
+
*/
|
|
2758
|
+
export const classifyFtsQueryError = (message) => {
|
|
2759
|
+
const PREPARE_FAILED_PREFIX = 'Prepare failed: ';
|
|
2760
|
+
const body = message.startsWith(PREPARE_FAILED_PREFIX)
|
|
2761
|
+
? message.slice(PREPARE_FAILED_PREFIX.length)
|
|
2762
|
+
: message;
|
|
2763
|
+
if (!body.startsWith('Binder exception:') && !body.startsWith('Catalog exception:')) {
|
|
2764
|
+
return 'other';
|
|
2765
|
+
}
|
|
2766
|
+
if (body.includes("doesn't have an index"))
|
|
2767
|
+
return 'missing-index';
|
|
2768
|
+
if (body.includes('does not exist'))
|
|
2769
|
+
return 'missing-table';
|
|
2770
|
+
return 'other';
|
|
2771
|
+
};
|
|
2724
2772
|
/**
|
|
2725
2773
|
* Query a full-text search index
|
|
2726
2774
|
* @param tableName - The node table name
|
|
@@ -2755,8 +2803,13 @@ export const queryFTS = async (tableName, indexName, query, limit = 20, conjunct
|
|
|
2755
2803
|
});
|
|
2756
2804
|
}
|
|
2757
2805
|
catch (e) {
|
|
2758
|
-
// Return empty
|
|
2759
|
-
|
|
2806
|
+
// Return empty only for a genuinely-missing index — the ordinary,
|
|
2807
|
+
// expected case. A missing TABLE (schema drift) or any other real error
|
|
2808
|
+
// rethrows instead of being silently swallowed (tri-review Residual-1 /
|
|
2809
|
+
// NEW-6 — this used to be a bare `.includes('does not exist')` check
|
|
2810
|
+
// that could not tell the two apart).
|
|
2811
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2812
|
+
if (classifyFtsQueryError(message) === 'missing-index') {
|
|
2760
2813
|
return [];
|
|
2761
2814
|
}
|
|
2762
2815
|
throw e;
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -603,6 +603,44 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
603
603
|
'if that also fails, verify FTS extension availability via `gitnexus doctor`.');
|
|
604
604
|
}
|
|
605
605
|
await ensureGitNexusIgnored(repoPath);
|
|
606
|
+
// #2767: stamp ONLY capabilities.fts so a long-lived MCP session's
|
|
607
|
+
// ensureInitialized() has an explicit, correctly-scoped signal that FTS
|
|
608
|
+
// changed — indexedAt/lastCommit/runnerIdentity/stats are copied through
|
|
609
|
+
// untouched (see the "must not claim a new analyzer identity" comment
|
|
610
|
+
// below). capabilities is forensic/no-programmatic-readers-until-now, so
|
|
611
|
+
// graph/vectorSearch are backfilled with conservative, honest defaults
|
|
612
|
+
// when a legacy meta.json predates this field entirely — repair-fts
|
|
613
|
+
// never touched them and cannot claim a capability it did not verify.
|
|
614
|
+
// Best-effort: a write failure must not turn an already-successful FTS
|
|
615
|
+
// rebuild into a reported repair failure.
|
|
616
|
+
try {
|
|
617
|
+
// Re-read the on-disk meta immediately before writing, rather than
|
|
618
|
+
// reusing `existingMeta` (captured before the FTS rebuild ran, which
|
|
619
|
+
// can span real wall-clock time). Another writer to this same
|
|
620
|
+
// gitnexus.json in the interim — e.g. the HTTP server's background
|
|
621
|
+
// embedding-checkpoint job — must not have its update silently
|
|
622
|
+
// reverted by this stamp overwriting a stale snapshot. Falls back to
|
|
623
|
+
// `existingMeta` only if the file became unreadable in that window.
|
|
624
|
+
const latestMeta = (await loadMeta(metaDir)) ?? existingMeta;
|
|
625
|
+
await saveMeta(metaDir, {
|
|
626
|
+
...latestMeta,
|
|
627
|
+
capabilities: {
|
|
628
|
+
graph: latestMeta.capabilities?.graph ?? {
|
|
629
|
+
provider: 'ladybugdb',
|
|
630
|
+
status: 'available',
|
|
631
|
+
},
|
|
632
|
+
fts: { provider: 'ladybugdb-fts', status: 'available' },
|
|
633
|
+
vectorSearch: latestMeta.capabilities?.vectorSearch ?? {
|
|
634
|
+
provider: 'exact-scan',
|
|
635
|
+
status: 'unavailable',
|
|
636
|
+
exactScanLimit: 0,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
catch (err) {
|
|
642
|
+
log(`FTS capability stamp write failed (non-critical, repair itself succeeded${err instanceof Error ? `: ${err.message}` : ''}); continuing.`);
|
|
643
|
+
}
|
|
606
644
|
progress('fts', 90, 'Search indexes ready');
|
|
607
645
|
progress('done', 100, 'Done');
|
|
608
646
|
return {
|
|
@@ -14,6 +14,17 @@ export interface FTSSearchResponse {
|
|
|
14
14
|
results: BM25SearchResult[];
|
|
15
15
|
/** True when at least one FTS index query succeeded (index exists). */
|
|
16
16
|
ftsAvailable: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Redacted (via {@link redactPaths}) message(s) from per-table
|
|
19
|
+
* `QUERY_FTS_INDEX` calls that failed for a reason OTHER than "index
|
|
20
|
+
* doesn't exist" (#2767) — a real query/connection error was previously
|
|
21
|
+
* indistinguishable from a genuinely-missing index. Populated whenever ANY
|
|
22
|
+
* table hit a non-benign error, regardless of whether other tables
|
|
23
|
+
* succeeded, so a caller can always log it; whether to also surface it in
|
|
24
|
+
* a client-facing warning is a caller decision (see `LocalBackend.query()`,
|
|
25
|
+
* which only does so when every table failed).
|
|
26
|
+
*/
|
|
27
|
+
nonBenignErrors?: string[];
|
|
17
28
|
}
|
|
18
29
|
/**
|
|
19
30
|
* Search using LadybugDB's built-in FTS (always fresh, reads from disk)
|
|
@@ -4,14 +4,23 @@
|
|
|
4
4
|
* Uses LadybugDB's built-in full-text search indexes for keyword-based search.
|
|
5
5
|
* Always reads from the database (no cached state to drift).
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
// tri-review Residual-1: `classifyFtsQueryError` now lives in lbug-adapter.ts
|
|
8
|
+
// (see its doc comment) so `queryFTS`'s own catch can share the SAME
|
|
9
|
+
// classifier instead of maintaining a second, independently-drifting copy
|
|
10
|
+
// for the identical `QUERY_FTS_INDEX` cypher call.
|
|
11
|
+
import { queryFTS, classifyFtsQueryError } from '../lbug/lbug-adapter.js';
|
|
8
12
|
import { normalizeFtsText } from '../lbug/csv-generator.js';
|
|
13
|
+
import { getExtensionCapabilities } from '../lbug/extension-loader.js';
|
|
14
|
+
import { redactPaths } from './fts-indexes.js';
|
|
9
15
|
import { FTS_INDEXES } from './fts-schema.js';
|
|
10
16
|
import { applyCjkSegmentationIfEnabled, MAX_CJK_SEGMENTATION_QUERY_LENGTH, } from './cjk-segmentation.js';
|
|
11
17
|
/**
|
|
12
18
|
* Execute a single FTS query via a custom executor (for MCP connection pool).
|
|
13
|
-
* Returns
|
|
14
|
-
*
|
|
19
|
+
* Returns a benign failure when the query fails because the index doesn't
|
|
20
|
+
* exist (the normal, expected case), and a non-benign failure with the
|
|
21
|
+
* captured message for any other error, so the caller can distinguish "zero
|
|
22
|
+
* matches", "index missing", and "a real error occurred" instead of
|
|
23
|
+
* collapsing the latter two into the same silent `null`.
|
|
15
24
|
*/
|
|
16
25
|
async function queryFTSViaExecutor(executor, tableName, indexName, query, limit) {
|
|
17
26
|
const cypher = `
|
|
@@ -22,18 +31,21 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
|
|
|
22
31
|
`;
|
|
23
32
|
try {
|
|
24
33
|
const rows = await executor(cypher, { query });
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
return {
|
|
35
|
+
rows: rows.map((row) => {
|
|
36
|
+
const node = row.node || row[0] || {};
|
|
37
|
+
const score = row.score ?? row[1] ?? 0;
|
|
38
|
+
return {
|
|
39
|
+
filePath: node.filePath || '',
|
|
40
|
+
score: typeof score === 'number' ? score : parseFloat(score) || 0,
|
|
41
|
+
nodeId: node.nodeId || node.id || '',
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
34
45
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
46
|
+
catch (e) {
|
|
47
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
48
|
+
return { benign: classifyFtsQueryError(message) === 'missing-index', message };
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
/**
|
|
@@ -65,7 +77,23 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
65
77
|
: query);
|
|
66
78
|
const resultsByIndex = [];
|
|
67
79
|
let queriesSucceeded = 0;
|
|
68
|
-
|
|
80
|
+
const nonBenignErrors = [];
|
|
81
|
+
const ftsExtension = getExtensionCapabilities().find((c) => c.name === 'fts');
|
|
82
|
+
if (ftsExtension && !ftsExtension.loaded) {
|
|
83
|
+
// tri-review NEW-4 (applies to BOTH the MCP pool path and the CLI/pipeline
|
|
84
|
+
// path — a /simplify altitude pass caught the original repoId-only guard
|
|
85
|
+
// letting the CLI branch surface spurious "non-benign" errors for this
|
|
86
|
+
// exact expected state, which the pool branch correctly stayed silent on):
|
|
87
|
+
// extension-unavailable is an expected, already-diagnosed degraded-capability
|
|
88
|
+
// state (#2374/#2658), not a per-table query error — every configured table
|
|
89
|
+
// would throw the identical "function not defined" shape, which
|
|
90
|
+
// classifyFtsQueryError correctly refuses to call benign "missing-index"
|
|
91
|
+
// (it's a different, more serious condition). Skip the N redundant
|
|
92
|
+
// QUERY_FTS_INDEX round-trips and N nonBenignErrors entries; ftsAvailable
|
|
93
|
+
// stays false and ftsDegradedWarning() already reports this state
|
|
94
|
+
// accurately from the same extension-capabilities registry.
|
|
95
|
+
}
|
|
96
|
+
else if (repoId) {
|
|
69
97
|
// Use MCP connection pool via dynamic import
|
|
70
98
|
// IMPORTANT: FTS queries run sequentially to avoid connection contention.
|
|
71
99
|
// The MCP pool supports multiple connections, but FTS is best run serially.
|
|
@@ -73,23 +101,31 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
73
101
|
const { executeParameterized } = poolMod;
|
|
74
102
|
const executor = (cypher, params) => executeParameterized(repoId, cypher, params);
|
|
75
103
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
104
|
+
const outcome = await queryFTSViaExecutor(executor, table, indexName, searchQuery, limit);
|
|
105
|
+
if (outcome.rows) {
|
|
78
106
|
queriesSucceeded++;
|
|
79
|
-
resultsByIndex.push(
|
|
107
|
+
resultsByIndex.push(outcome.rows);
|
|
108
|
+
}
|
|
109
|
+
else if (!outcome.benign) {
|
|
110
|
+
nonBenignErrors.push(redactPaths(outcome.message ?? 'Unknown FTS query error'));
|
|
80
111
|
}
|
|
81
112
|
}
|
|
82
113
|
}
|
|
83
114
|
else {
|
|
84
115
|
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
|
|
116
|
+
// tri-review Residual-1: `queryFTS` itself only swallows a genuinely-missing
|
|
117
|
+
// index (via the SAME classifyFtsQueryError this module re-exports); a
|
|
118
|
+
// missing-table or real query error rethrows here — track it the same way
|
|
119
|
+
// the MCP pool path does instead of a bare `catch {}` that dropped it.
|
|
85
120
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
86
121
|
try {
|
|
87
122
|
const result = await queryFTS(table, indexName, searchQuery, limit, false);
|
|
88
123
|
queriesSucceeded++;
|
|
89
124
|
resultsByIndex.push(result);
|
|
90
125
|
}
|
|
91
|
-
catch {
|
|
92
|
-
|
|
126
|
+
catch (e) {
|
|
127
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
128
|
+
nonBenignErrors.push(redactPaths(message));
|
|
93
129
|
}
|
|
94
130
|
}
|
|
95
131
|
}
|
|
@@ -129,5 +165,6 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
129
165
|
nodeIds: r.nodeIds,
|
|
130
166
|
})),
|
|
131
167
|
ftsAvailable,
|
|
168
|
+
...(nonBenignErrors.length > 0 && { nonBenignErrors }),
|
|
132
169
|
};
|
|
133
170
|
};
|
|
@@ -1,10 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip filesystem paths from a LadybugDB error before it reaches the HTTP
|
|
3
|
+
* `/api/search` and MCP query surfaces (#2374, PR #2375): the raw LOAD error
|
|
4
|
+
* embeds the absolute extension path (username, home dir) which must not leak to
|
|
5
|
+
* a network client. The error class words ("Failed to load library", "invalid
|
|
6
|
+
* ELF header", "has not been installed") have no leading path separator and
|
|
7
|
+
* survive. CLI/doctor/log surfaces keep the full path (they read the reason
|
|
8
|
+
* directly, not through this function).
|
|
9
|
+
*
|
|
10
|
+
* tri-review Residual-3: every real message shape observed from LadybugDB
|
|
11
|
+
* wraps the path in single quotes (`Failed to load library '<path>': ...`),
|
|
12
|
+
* so a QUOTED path is redacted first, consuming through its closing quote —
|
|
13
|
+
* spaces included (e.g. a Windows username like `alice smith`). The original
|
|
14
|
+
* unquoted-stop-at-first-whitespace pattern still runs afterward as a
|
|
15
|
+
* fallback for the rare case of a path appearing without quotes; that path's
|
|
16
|
+
* own known limitation (partial redaction if it itself contains a space) is
|
|
17
|
+
* unchanged, but is no longer the ONLY path this function knows how to redact.
|
|
18
|
+
*/
|
|
19
|
+
export declare const redactPaths: (reason: string) => string;
|
|
20
|
+
/**
|
|
21
|
+
* Resolved-repo/index identity a caller can attach to a degraded-FTS warning
|
|
22
|
+
* (#2767) so a reader can tell whether *this* session even resolved the index
|
|
23
|
+
* they expect, instead of guessing between a stale connection, a different
|
|
24
|
+
* repo/branch, or a genuine build failure. MCP-`query`-only today — never
|
|
25
|
+
* forwarded into the HTTP `/api/search` response (see that call site).
|
|
26
|
+
*/
|
|
27
|
+
export interface FtsWarningContext {
|
|
28
|
+
repoName: string;
|
|
29
|
+
branch?: string;
|
|
30
|
+
indexedAt?: string;
|
|
31
|
+
/** Already redacted by the caller (e.g. via {@link redactPaths} on a captured query error). */
|
|
32
|
+
lastErrorRedacted?: string;
|
|
33
|
+
}
|
|
1
34
|
/**
|
|
2
35
|
* Warning attached to search responses when BM25/FTS is degraded. Prefers the
|
|
3
36
|
* live extension-load failure (with LadybugDB's real reason, #2374) over the
|
|
4
37
|
* generic indexes-missing message, so "indexes exist but the extension broke"
|
|
5
38
|
* is not misreported as missing indexes.
|
|
39
|
+
*
|
|
40
|
+
* `context`, when supplied, appends the resolved repo/branch/indexed-at (and
|
|
41
|
+
* redacted query-error detail, if captured) so a CLI/MCP mismatch — or a real
|
|
42
|
+
* query error masquerading as "indexes missing" — is visible in the warning
|
|
43
|
+
* text itself (#2767). Optional and additive: omitting it reproduces today's
|
|
44
|
+
* exact message.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ftsDegradedWarning: (context?: FtsWarningContext) => string;
|
|
47
|
+
/**
|
|
48
|
+
* Warning for when the FTS extension is loaded and indexes exist, but every
|
|
49
|
+
* configured table's query failed for a real, non-benign reason (timeout,
|
|
50
|
+
* connection reset, native fault) — as opposed to `ftsDegradedWarning`'s
|
|
51
|
+
* missing-index case. `--repair-fts` will not fix a query/connection error,
|
|
52
|
+
* so this deliberately does NOT suggest it: reusing the missing-index
|
|
53
|
+
* message here would reproduce, for this cause, the exact misleading
|
|
54
|
+
* "run --repair-fts" guidance #2767 itself was about (tri-review NEW-1).
|
|
6
55
|
*/
|
|
7
|
-
export declare const
|
|
56
|
+
export declare const ftsQueryFailedWarning: (context: FtsWarningContext) => string;
|
|
8
57
|
export declare const SUPPORTED_FTS_STEMMERS: ReadonlySet<string>;
|
|
9
58
|
export interface CreateSearchFTSIndexesOptions {
|
|
10
59
|
onIndexStart?: (table: string, indexName: string) => void;
|
|
@@ -10,15 +10,43 @@ import { FTS_INDEXES } from './fts-schema.js';
|
|
|
10
10
|
* ELF header", "has not been installed") have no leading path separator and
|
|
11
11
|
* survive. CLI/doctor/log surfaces keep the full path (they read the reason
|
|
12
12
|
* directly, not through this function).
|
|
13
|
+
*
|
|
14
|
+
* tri-review Residual-3: every real message shape observed from LadybugDB
|
|
15
|
+
* wraps the path in single quotes (`Failed to load library '<path>': ...`),
|
|
16
|
+
* so a QUOTED path is redacted first, consuming through its closing quote —
|
|
17
|
+
* spaces included (e.g. a Windows username like `alice smith`). The original
|
|
18
|
+
* unquoted-stop-at-first-whitespace pattern still runs afterward as a
|
|
19
|
+
* fallback for the rare case of a path appearing without quotes; that path's
|
|
20
|
+
* own known limitation (partial redaction if it itself contains a space) is
|
|
21
|
+
* unchanged, but is no longer the ONLY path this function knows how to redact.
|
|
13
22
|
*/
|
|
14
|
-
const redactPaths = (reason) => reason
|
|
23
|
+
export const redactPaths = (reason) => reason
|
|
24
|
+
.replace(/'((?:[A-Za-z]:\\|\/)[^']*)'/g, "'<path>'")
|
|
25
|
+
.replace(/(?:[A-Za-z]:\\|\/)[^\s'"]+/g, '<path>');
|
|
26
|
+
/** The repo/branch/indexed-at portion shared by both warning-context formatters below. */
|
|
27
|
+
const formatResolvedSuffix = (context) => {
|
|
28
|
+
const branchSuffix = context.branch ? `/branch:${context.branch}` : '';
|
|
29
|
+
const indexedSuffix = context.indexedAt ? `, indexed ${context.indexedAt}` : '';
|
|
30
|
+
return `${context.repoName}${branchSuffix}${indexedSuffix}`;
|
|
31
|
+
};
|
|
32
|
+
const formatWarningContext = (context) => {
|
|
33
|
+
const errorSuffix = context.lastErrorRedacted ? `; last error: ${context.lastErrorRedacted}` : '';
|
|
34
|
+
return ` (resolved: ${formatResolvedSuffix(context)}${errorSuffix})`;
|
|
35
|
+
};
|
|
15
36
|
/**
|
|
16
37
|
* Warning attached to search responses when BM25/FTS is degraded. Prefers the
|
|
17
38
|
* live extension-load failure (with LadybugDB's real reason, #2374) over the
|
|
18
39
|
* generic indexes-missing message, so "indexes exist but the extension broke"
|
|
19
40
|
* is not misreported as missing indexes.
|
|
41
|
+
*
|
|
42
|
+
* `context`, when supplied, appends the resolved repo/branch/indexed-at (and
|
|
43
|
+
* redacted query-error detail, if captured) so a CLI/MCP mismatch — or a real
|
|
44
|
+
* query error masquerading as "indexes missing" — is visible in the warning
|
|
45
|
+
* text itself (#2767). Optional and additive: omitting it reproduces today's
|
|
46
|
+
* exact message.
|
|
20
47
|
*/
|
|
21
|
-
export const ftsDegradedWarning = () => {
|
|
48
|
+
export const ftsDegradedWarning = (context) => {
|
|
49
|
+
const suffix = context ? formatWarningContext(context) : '';
|
|
22
50
|
const fts = getExtensionCapabilities().find((c) => c.name === 'fts');
|
|
23
51
|
if (fts && !fts.loaded) {
|
|
24
52
|
const reason = fts.reason ? redactPaths(fts.reason).replace(/\.$/, '') : undefined;
|
|
@@ -33,10 +61,27 @@ export const ftsDegradedWarning = () => {
|
|
|
33
61
|
: '. Run `gitnexus doctor` for details, then `gitnexus analyze --repair-fts` with network access to reinstall.';
|
|
34
62
|
return ('FTS extension failed to load — keyword search degraded' +
|
|
35
63
|
(reason ? ` (${reason})` : '') +
|
|
36
|
-
tail
|
|
64
|
+
tail +
|
|
65
|
+
suffix);
|
|
37
66
|
}
|
|
38
|
-
return 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts
|
|
67
|
+
return ('FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts ' +
|
|
68
|
+
'(or gitnexus analyze --force) to rebuild indexes.' +
|
|
69
|
+
suffix);
|
|
39
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Warning for when the FTS extension is loaded and indexes exist, but every
|
|
73
|
+
* configured table's query failed for a real, non-benign reason (timeout,
|
|
74
|
+
* connection reset, native fault) — as opposed to `ftsDegradedWarning`'s
|
|
75
|
+
* missing-index case. `--repair-fts` will not fix a query/connection error,
|
|
76
|
+
* so this deliberately does NOT suggest it: reusing the missing-index
|
|
77
|
+
* message here would reproduce, for this cause, the exact misleading
|
|
78
|
+
* "run --repair-fts" guidance #2767 itself was about (tri-review NEW-1).
|
|
79
|
+
*/
|
|
80
|
+
export const ftsQueryFailedWarning = (context) => 'FTS keyword search failed — every configured index query returned an error' +
|
|
81
|
+
(context.lastErrorRedacted ? ` (${context.lastErrorRedacted})` : '') +
|
|
82
|
+
'; results do not include keyword matches. This is not a missing-index ' +
|
|
83
|
+
'condition — see server logs for details.' +
|
|
84
|
+
` (resolved: ${formatResolvedSuffix(context)})`;
|
|
40
85
|
// Stemmers shipped by the LadybugDB FTS extension. Mirrors the lowercase token
|
|
41
86
|
// set in the extension bundled with @ladybugdb/core 0.18.x (see package.json).
|
|
42
87
|
// Keep in sync on a LadybugDB minor bump — a value here that the installed
|
|
@@ -156,8 +156,9 @@ export declare class LocalBackend {
|
|
|
156
156
|
private reinitPromises;
|
|
157
157
|
private lastStalenessCheck;
|
|
158
158
|
private toolStalenessCache;
|
|
159
|
-
private
|
|
160
|
-
|
|
159
|
+
private lastObservedPoolState;
|
|
160
|
+
/** Merge-patch one poolKey's observed state, preserving fields not passed. */
|
|
161
|
+
private setObservedState;
|
|
161
162
|
private groupToolSvc;
|
|
162
163
|
/**
|
|
163
164
|
* One-shot stderr warnings for sibling-clone drift, keyed by
|
|
@@ -28,7 +28,7 @@ import { rankExactEmbeddingRows, } from '../../core/embeddings/exact-search.js';
|
|
|
28
28
|
import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME } from '../../core/lbug/schema.js';
|
|
29
29
|
import { getExactScanLimit } from '../../core/platform/capabilities.js';
|
|
30
30
|
import { PhaseTimer } from '../../core/search/phase-timer.js';
|
|
31
|
-
import { ftsDegradedWarning } from '../../core/search/fts-indexes.js';
|
|
31
|
+
import { ftsDegradedWarning, ftsQueryFailedWarning } from '../../core/search/fts-indexes.js';
|
|
32
32
|
import { cjkSegmentationModeMismatch, containsSegmentableCjkRun, getSearchFTSCjkSegmentation, isSupportedCjkSegmentationMode, MAX_CJK_SEGMENTATION_QUERY_LENGTH, } from '../../core/search/cjk-segmentation.js';
|
|
33
33
|
import { checkStalenessAsync, checkCwdMatch, } from '../../core/git-staleness.js';
|
|
34
34
|
import { logger } from '../../core/logger.js';
|
|
@@ -525,17 +525,30 @@ export class LocalBackend {
|
|
|
525
525
|
// lastCommit values, so a repoPath key would serve one handle's freshness for
|
|
526
526
|
// the other; lbugPath is unique per flat/branch index.
|
|
527
527
|
toolStalenessCache = new Map();
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
|
|
528
|
+
// tri-review Residual-2: consolidates what were three parallel per-poolKey
|
|
529
|
+
// Maps (lastObservedIndexedAt / lastObservedDbIdentity / lastObservedFtsStatus)
|
|
530
|
+
// touched in lockstep at every call site below — one Map, one delete, one
|
|
531
|
+
// shape. Keyed by lbugPath (not stored on the repo handle) because branch
|
|
532
|
+
// handles are produced fresh by applyBranchScope on every resolveRepo call,
|
|
533
|
+
// so mutating the handle would not persist across calls and the staleness
|
|
534
|
+
// check would reinit forever (#2106).
|
|
535
|
+
// - `indexedAt`: last meta.indexedAt observed for an open pool.
|
|
536
|
+
// - `dbIdentity`: file identity of the lbug the pool last opened (#2614 F1)
|
|
537
|
+
// — an atomic swap or in-place incremental changes the inode; reiniting
|
|
538
|
+
// on that covers the window where meta.indexedAt hasn't caught up (and
|
|
539
|
+
// the incremental case), so a rebuilt index is never served stale even
|
|
540
|
+
// when the stamp looks current.
|
|
541
|
+
// - `ftsStatus`: last meta.capabilities.fts.status observed (#2767).
|
|
542
|
+
// `--repair-fts` intentionally never restamps `indexedAt` (it doesn't
|
|
543
|
+
// regenerate the graph), so this is the dedicated signal a warm session
|
|
544
|
+
// uses to notice a repair — independent of the file-identity heuristic,
|
|
545
|
+
// which the repair path also triggers but only incidentally.
|
|
546
|
+
lastObservedPoolState = new Map();
|
|
547
|
+
/** Merge-patch one poolKey's observed state, preserving fields not passed. */
|
|
548
|
+
setObservedState(poolKey, patch) {
|
|
549
|
+
const current = this.lastObservedPoolState.get(poolKey) ?? { dbIdentity: null };
|
|
550
|
+
this.lastObservedPoolState.set(poolKey, { ...current, ...patch });
|
|
551
|
+
}
|
|
539
552
|
groupToolSvc = null;
|
|
540
553
|
/**
|
|
541
554
|
* One-shot stderr warnings for sibling-clone drift, keyed by
|
|
@@ -821,8 +834,7 @@ export class LocalBackend {
|
|
|
821
834
|
this.initializedRepos.delete(key);
|
|
822
835
|
this.lastStalenessCheck.delete(key);
|
|
823
836
|
this.toolStalenessCache.delete(key);
|
|
824
|
-
this.
|
|
825
|
-
this.lastObservedDbIdentity.delete(key);
|
|
837
|
+
this.lastObservedPoolState.delete(key);
|
|
826
838
|
this.reinitPromises.delete(key);
|
|
827
839
|
closeLbug(key).catch(() => { });
|
|
828
840
|
}
|
|
@@ -1182,18 +1194,28 @@ export class LocalBackend {
|
|
|
1182
1194
|
// Reading the flat meta for a branch handle would compare the branch
|
|
1183
1195
|
// index's indexedAt against the primary's and thrash the pool (#2106).
|
|
1184
1196
|
const meta = await loadMeta(path.dirname(repo.lbugPath));
|
|
1197
|
+
const observedState = this.lastObservedPoolState.get(poolKey);
|
|
1185
1198
|
// Compare against the last indexedAt OBSERVED for this pool (keyed by
|
|
1186
1199
|
// lbugPath), not the handle's — branch handles are fresh spreads so a
|
|
1187
1200
|
// handle mutation would not persist and would reinit on every check.
|
|
1188
|
-
const observed =
|
|
1201
|
+
const observed = observedState?.indexedAt ?? repo.indexedAt;
|
|
1189
1202
|
const stampChanged = !!meta?.indexedAt && meta.indexedAt !== observed;
|
|
1190
1203
|
// #2614 F1: also reinit on a file-identity change. An atomic swap (or an
|
|
1191
1204
|
// in-place incremental) changes the lbug inode; keying only on
|
|
1192
1205
|
// meta.indexedAt let a reader that reinited inside the pre-swap window
|
|
1193
1206
|
// latch on the old inode forever (its stamp already == meta.indexedAt).
|
|
1194
1207
|
const currentIdentity = await statDbIdentity(repo.lbugPath);
|
|
1195
|
-
const identityChanged = dbIdentityChanged(
|
|
1196
|
-
|
|
1208
|
+
const identityChanged = dbIdentityChanged(observedState?.dbIdentity ?? null, currentIdentity);
|
|
1209
|
+
// #2767: `--repair-fts` intentionally never restamps `indexedAt` (it
|
|
1210
|
+
// doesn't regenerate the graph), so `stampChanged` alone can't notice
|
|
1211
|
+
// a repair. `capabilities.fts.status` is the field repair-fts DOES
|
|
1212
|
+
// write, so a change there is a third, independent reinit trigger —
|
|
1213
|
+
// sibling to stampChanged/identityChanged, not a replacement for them
|
|
1214
|
+
// (identityChanged still catches an in-place mutation even if the
|
|
1215
|
+
// caps stamp were somehow missed).
|
|
1216
|
+
const ftsStatus = meta?.capabilities?.fts?.status;
|
|
1217
|
+
const ftsCapsChanged = observedState?.ftsStatus !== ftsStatus;
|
|
1218
|
+
if (stampChanged || identityChanged || ftsCapsChanged) {
|
|
1197
1219
|
// Index was rebuilt/swapped — DELEGATE the close/reopen to the pool's
|
|
1198
1220
|
// initLbug, which refuses to evict (and close the shared Database)
|
|
1199
1221
|
// while a query is in flight (its checkedOut>0 guard). Calling
|
|
@@ -1202,18 +1224,24 @@ export class LocalBackend {
|
|
|
1202
1224
|
// reinitPromises to serialize concurrent detectors.
|
|
1203
1225
|
const reinit = (async () => {
|
|
1204
1226
|
try {
|
|
1205
|
-
// Advance the observed stamp regardless: a stamp change with an
|
|
1206
|
-
// unchanged file must not re-trigger on every check.
|
|
1207
|
-
if (meta?.indexedAt)
|
|
1208
|
-
this.lastObservedIndexedAt.set(poolKey, meta.indexedAt);
|
|
1209
1227
|
const reopened = await initLbug(poolKey, repo.lbugPath);
|
|
1228
|
+
// tri-review NEW-7: advance the observed stamp/caps watermarks
|
|
1229
|
+
// only AFTER initLbug completes, not before calling it — still
|
|
1230
|
+
// regardless of `reopened` true/false (a stamp/caps change with
|
|
1231
|
+
// an unchanged file must not re-trigger on every check), but if
|
|
1232
|
+
// initLbug THROWS the watermark must stay at its old value so
|
|
1233
|
+
// the next staleness check retries, instead of a failed reinit
|
|
1234
|
+
// silently latching as "already applied" and never trying again.
|
|
1235
|
+
const patch = { ftsStatus };
|
|
1236
|
+
if (meta?.indexedAt)
|
|
1237
|
+
patch.indexedAt = meta.indexedAt;
|
|
1210
1238
|
// Advance the observed IDENTITY only when the pool actually rolled
|
|
1211
1239
|
// over. If a query was in flight, initLbug served the current
|
|
1212
1240
|
// handle and returned false; leaving the identity divergent
|
|
1213
1241
|
// re-triggers the reopen on a later idle check instead of latching.
|
|
1214
|
-
if (reopened)
|
|
1215
|
-
|
|
1216
|
-
|
|
1242
|
+
if (reopened)
|
|
1243
|
+
patch.dbIdentity = await statDbIdentity(repo.lbugPath);
|
|
1244
|
+
this.setObservedState(poolKey, patch);
|
|
1217
1245
|
}
|
|
1218
1246
|
finally {
|
|
1219
1247
|
this.reinitPromises.delete(poolKey);
|
|
@@ -1233,8 +1261,18 @@ export class LocalBackend {
|
|
|
1233
1261
|
try {
|
|
1234
1262
|
await initLbug(poolKey, repo.lbugPath);
|
|
1235
1263
|
this.initializedRepos.add(poolKey);
|
|
1236
|
-
|
|
1237
|
-
|
|
1264
|
+
// #2767: ftsStatus is deliberately left unset (undefined) here rather
|
|
1265
|
+
// than issuing an extra loadMeta read — every tool call already routes
|
|
1266
|
+
// through ensureInitialized, so an extra per-cold-init read adds up,
|
|
1267
|
+
// and the cost of skipping it is negligible: at most one redundant
|
|
1268
|
+
// initLbug call on the first warm check (initLbug itself no-ops
|
|
1269
|
+
// cheaply via a single fs.stat when the file identity is actually
|
|
1270
|
+
// unchanged, per pool-adapter.ts's own "unchanged → reuse" guard), not
|
|
1271
|
+
// a real reopen.
|
|
1272
|
+
this.setObservedState(poolKey, {
|
|
1273
|
+
indexedAt: repo.indexedAt,
|
|
1274
|
+
dbIdentity: await statDbIdentity(repo.lbugPath),
|
|
1275
|
+
});
|
|
1238
1276
|
}
|
|
1239
1277
|
catch (err) {
|
|
1240
1278
|
// If lock error, mark as not initialized so next call retries
|
|
@@ -1628,6 +1666,21 @@ export class LocalBackend {
|
|
|
1628
1666
|
// unavailable the search helper may return an unexpected shape.
|
|
1629
1667
|
const bm25Results = bm25SearchResult?.results ?? [];
|
|
1630
1668
|
const ftsUsed = bm25SearchResult?.ftsUsed ?? false;
|
|
1669
|
+
// #2767: log every non-benign per-table FTS query error server-side,
|
|
1670
|
+
// regardless of whether OTHER tables succeeded — previously a real error
|
|
1671
|
+
// on N-1 of N tables while one succeeded left zero diagnostic trail.
|
|
1672
|
+
const ftsQueryErrors = bm25SearchResult?.nonBenignErrors;
|
|
1673
|
+
if (ftsQueryErrors) {
|
|
1674
|
+
// tri-review NEW-5: these strings are already classified non-benign by
|
|
1675
|
+
// classifyFtsQueryError — do NOT route them through logQueryError,
|
|
1676
|
+
// whose own broader, unanchored isBenignMissingTableError regex (any
|
|
1677
|
+
// "does not exist" substring, anywhere) could disagree and silently
|
|
1678
|
+
// demote an already-flagged real error to debug, undercutting the
|
|
1679
|
+
// severity signal this classification exists to preserve.
|
|
1680
|
+
for (const err of ftsQueryErrors) {
|
|
1681
|
+
logger.warn({ context: 'query:fts-search', err }, 'GitNexus query failed (degraded)');
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1631
1684
|
// Merge via reciprocal rank fusion
|
|
1632
1685
|
timer.start('merge');
|
|
1633
1686
|
const scoreMap = new Map();
|
|
@@ -1868,7 +1921,34 @@ export class LocalBackend {
|
|
|
1868
1921
|
// path, leaving the success-path response shape byte-identical.
|
|
1869
1922
|
const warnings = [];
|
|
1870
1923
|
if (!ftsUsed) {
|
|
1871
|
-
|
|
1924
|
+
// #2767: attach what THIS session resolved (repo/branch/indexed-at) so a
|
|
1925
|
+
// CLI/MCP mismatch is visible in the warning itself rather than requiring
|
|
1926
|
+
// a separate debugging round-trip. tri-review NEW-3: `indexedAt` reads
|
|
1927
|
+
// from `lastObservedPoolState` (kept current by ensureInitialized's
|
|
1928
|
+
// staleness check, including a same-call reinit) rather than the `repo`
|
|
1929
|
+
// handle resolved before that check ran — a warm backend that just
|
|
1930
|
+
// reopened against a newer on-disk index must not warn with stale
|
|
1931
|
+
// metadata. No extra I/O: the map is already maintained per-request.
|
|
1932
|
+
const warningContext = {
|
|
1933
|
+
repoName: repo.name,
|
|
1934
|
+
branch: repo.branch,
|
|
1935
|
+
indexedAt: this.lastObservedPoolState.get(repo.lbugPath)?.indexedAt ?? repo.indexedAt,
|
|
1936
|
+
};
|
|
1937
|
+
// tri-review NEW-1: every table failing for a REAL error (timeout,
|
|
1938
|
+
// connection reset) is not a missing-index condition — `ftsDegradedWarning`'s
|
|
1939
|
+
// "run --repair-fts" headline won't fix it. Route to a dedicated message
|
|
1940
|
+
// instead of burying the real cause as a trailing suffix on bad advice.
|
|
1941
|
+
warnings.push(ftsQueryErrors
|
|
1942
|
+
? ftsQueryFailedWarning({ ...warningContext, lastErrorRedacted: ftsQueryErrors[0] })
|
|
1943
|
+
: ftsDegradedWarning(warningContext));
|
|
1944
|
+
}
|
|
1945
|
+
else if (ftsQueryErrors) {
|
|
1946
|
+
// #2767: at least one FTS table succeeded (ftsUsed=true) but another
|
|
1947
|
+
// hit a real, non-benign error — results may be silently missing
|
|
1948
|
+
// matches from that table with no signal, the same "partial success"
|
|
1949
|
+
// shape the enrichmentDegraded branch below already surfaces. Mirror
|
|
1950
|
+
// that convention instead of only logging server-side.
|
|
1951
|
+
warnings.push(`FTS keyword search partially failed — ${ftsQueryErrors.length} of the configured indexes hit a query error and were skipped; results may be missing matches from those node types (see server logs).`);
|
|
1872
1952
|
}
|
|
1873
1953
|
// #2331: a CJK query against a server process resolving
|
|
1874
1954
|
// GITNEXUS_FTS_CJK_SEGMENTATION to 'none' silently misses sub-phrase
|
|
@@ -1935,13 +2015,17 @@ export class LocalBackend {
|
|
|
1935
2015
|
if (enrichmentDegraded) {
|
|
1936
2016
|
warnings.push('Symbol enrichment partially failed — some process/cohesion/content data may be missing from these results (see server logs).');
|
|
1937
2017
|
}
|
|
2018
|
+
// #2767: a partial FTS failure (some tables ok, one or more real errors)
|
|
2019
|
+
// is as much a "results may be incomplete" signal as enrichmentDegraded —
|
|
2020
|
+
// flag it the same way rather than only via the warning string.
|
|
2021
|
+
const ftsPartial = ftsUsed && !!ftsQueryErrors;
|
|
1938
2022
|
return {
|
|
1939
2023
|
processes,
|
|
1940
2024
|
process_symbols: dedupedSymbols,
|
|
1941
2025
|
definitions: definitions.slice(0, 20), // cap standalone definitions
|
|
1942
2026
|
timing,
|
|
1943
2027
|
...(warnings.length > 0 && { warning: warnings.join(' ') }),
|
|
1944
|
-
...(enrichmentDegraded && { partial: true }),
|
|
2028
|
+
...((enrichmentDegraded || ftsPartial) && { partial: true }),
|
|
1945
2029
|
};
|
|
1946
2030
|
}
|
|
1947
2031
|
/**
|
|
@@ -1974,6 +2058,7 @@ export class LocalBackend {
|
|
|
1974
2058
|
// could be undefined when the FTS extension is unavailable in the MCP process.
|
|
1975
2059
|
const bm25Results = ftsResponse?.results ?? [];
|
|
1976
2060
|
const ftsUsed = ftsResponse?.ftsAvailable ?? false;
|
|
2061
|
+
const nonBenignErrors = ftsResponse?.nonBenignErrors;
|
|
1977
2062
|
const results = [];
|
|
1978
2063
|
for (const bm25Result of bm25Results) {
|
|
1979
2064
|
const fullPath = bm25Result.filePath;
|
|
@@ -2036,7 +2121,7 @@ export class LocalBackend {
|
|
|
2036
2121
|
});
|
|
2037
2122
|
}
|
|
2038
2123
|
}
|
|
2039
|
-
return { results, ftsUsed };
|
|
2124
|
+
return { results, ftsUsed, ...(nonBenignErrors && { nonBenignErrors }) };
|
|
2040
2125
|
}
|
|
2041
2126
|
/**
|
|
2042
2127
|
* Semantic vector search helper
|
package/dist/server/api.js
CHANGED
|
@@ -1558,8 +1558,16 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1558
1558
|
}
|
|
1559
1559
|
const forceReembedNodeIds = new Set(priorCheckpoint?.pendingNodeIds ?? []);
|
|
1560
1560
|
const saveEmbeddingCheckpoint = async (checkpoint, pendingNodeIds) => {
|
|
1561
|
+
// tri-review NEW-2: re-read immediately before writing (mirrors
|
|
1562
|
+
// the pattern in run-analyze.ts's --repair-fts stamp) instead of
|
|
1563
|
+
// spreading the stale `embeddingMeta` snapshot captured once at
|
|
1564
|
+
// job start. This job can run up to EMBED_TIMEOUT_MS (30 min);
|
|
1565
|
+
// without a fresh read, a concurrent writer's update (e.g. a
|
|
1566
|
+
// --repair-fts capability stamp) would be silently reverted on
|
|
1567
|
+
// every checkpoint save for the job's whole lifetime.
|
|
1568
|
+
const latestMeta = (await loadMeta(entry.storagePath)) ?? embeddingMeta;
|
|
1561
1569
|
embeddingMeta = {
|
|
1562
|
-
...
|
|
1570
|
+
...latestMeta,
|
|
1563
1571
|
embeddingCheckpoint: {
|
|
1564
1572
|
at: new Date().toISOString(),
|
|
1565
1573
|
...checkpoint,
|
|
@@ -1610,7 +1618,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1610
1618
|
// handles this during process exit, but the server keeps the
|
|
1611
1619
|
// connection open for other routes — a CHECKPOINT is enough.
|
|
1612
1620
|
await flushWAL();
|
|
1613
|
-
|
|
1621
|
+
// Same re-read-before-write reasoning as saveEmbeddingCheckpoint above.
|
|
1622
|
+
const finalMeta = (await loadMeta(entry.storagePath)) ?? embeddingMeta;
|
|
1623
|
+
embeddingMeta = { ...finalMeta, embeddingCheckpoint: undefined };
|
|
1614
1624
|
await saveMeta(entry.storagePath, embeddingMeta);
|
|
1615
1625
|
});
|
|
1616
1626
|
// Don't overwrite 'failed' if the job was cancelled while the pipeline was running
|
|
@@ -155,9 +155,12 @@ export interface RepoMeta {
|
|
|
155
155
|
* the meta literal in run-analyze.ts — typed here so the stamp site is
|
|
156
156
|
* compile-checked; tri-review 4669518496 P1/U3: `vectorSearch.status`
|
|
157
157
|
* must never claim 'vector-index' unless the run verified or recreated
|
|
158
|
-
* the HNSW index).
|
|
159
|
-
*
|
|
160
|
-
*
|
|
158
|
+
* the HNSW index). `fts.status` gained its first programmatic reader in
|
|
159
|
+
* #2767: `LocalBackend.ensureInitialized()` compares it against the
|
|
160
|
+
* warm connection pool's last-observed value as the dedicated signal
|
|
161
|
+
* that `--repair-fts` changed FTS availability (`doctor` still prints
|
|
162
|
+
* platform-derived capabilities separately; `graph`/`vectorSearch` remain
|
|
163
|
+
* forensic-only). The status unions mirror `CapabilityStatus` /
|
|
161
164
|
* `SemanticSearchMode` in core/platform/capabilities.ts; inlined to keep
|
|
162
165
|
* storage/ free of a core/ type dependency.
|
|
163
166
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.10-rc.
|
|
3
|
+
"version": "1.6.10-rc.136",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|