gitnexus 1.6.13-rc.67 → 1.6.13-rc.69
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/group-status-format.d.ts +4 -2
- package/dist/cli/group-status-format.js +4 -8
- package/dist/core/git-staleness.js +43 -10
- package/dist/core/lbug/lbug-adapter.d.ts +2 -1
- package/dist/core/lbug/lbug-adapter.js +3 -2
- package/dist/core/search/bm25-index.d.ts +2 -0
- package/dist/core/search/bm25-index.js +15 -7
- package/dist/core/search/fts-indexes.d.ts +5 -8
- package/dist/core/search/fts-indexes.js +9 -10
- package/dist/core/staleness-status.d.ts +28 -14
- package/dist/mcp/local/local-backend.js +17 -7
- package/dist/mcp/resources.js +1 -1
- package/package.json +1 -1
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* against a real group, which is how `STALE (-1 commits behind)` went unnoticed
|
|
5
5
|
* (#3256).
|
|
6
6
|
*/
|
|
7
|
+
import type { StalenessStatus } from '../core/staleness-status.js';
|
|
7
8
|
/** The fields of a `groupStatus` repo row the index column reads. */
|
|
8
9
|
export interface GroupRepoIndexRow {
|
|
9
10
|
indexStale: boolean;
|
|
10
11
|
commitsBehind?: number;
|
|
12
|
+
status?: StalenessStatus;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
|
-
* The index column of a `group status` row.
|
|
14
|
-
*
|
|
15
|
+
* The index column of a `group status` row. Diverged indexes differ from HEAD
|
|
16
|
+
* without a forward commit count; an unavailable count renders as `?`.
|
|
15
17
|
*
|
|
16
18
|
* `group/service.ts` has always reported a repo with no recorded commit as
|
|
17
19
|
* `{ indexStale: true, commitsBehind: -1 }`. The previous `?? '?'` fallback
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* against a real group, which is how `STALE (-1 commits behind)` went unnoticed
|
|
5
|
-
* (#3256).
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* The index column of a `group status` row. The output is unchanged except
|
|
9
|
-
* for one case: a count that is not a real count renders as `?`.
|
|
2
|
+
* The index column of a `group status` row. Diverged indexes differ from HEAD
|
|
3
|
+
* without a forward commit count; an unavailable count renders as `?`.
|
|
10
4
|
*
|
|
11
5
|
* `group/service.ts` has always reported a repo with no recorded commit as
|
|
12
6
|
* `{ indexStale: true, commitsBehind: -1 }`. The previous `?? '?'` fallback
|
|
@@ -15,6 +9,8 @@
|
|
|
15
9
|
export const formatIndexStatusCell = (row) => {
|
|
16
10
|
if (!row.indexStale)
|
|
17
11
|
return 'OK ';
|
|
12
|
+
if (row.status === 'diverged')
|
|
13
|
+
return 'STALE (index differs from HEAD)';
|
|
18
14
|
const n = row.commitsBehind;
|
|
19
15
|
const count = typeof n === 'number' && n >= 0 ? String(n) : '?';
|
|
20
16
|
return `STALE (${count} commits behind)`;
|
|
@@ -9,8 +9,8 @@ import { readRegistry } from '../storage/repo-manager.js';
|
|
|
9
9
|
import { findGitRootByDotGit, getCurrentCommit, getRemoteUrl } from '../storage/git.js';
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Per-command ceiling for async `git rev-list` and both HEAD probes.
|
|
13
|
+
* Generous for local Git queries, and short enough that an unresponsive working tree
|
|
14
14
|
* degrades to "not stale" quickly rather than holding a request open.
|
|
15
15
|
*/
|
|
16
16
|
const STALENESS_TIMEOUT_MS = 5_000;
|
|
@@ -20,10 +20,20 @@ const behindHint = (n) => `⚠️ Index is ${n} commit${n > 1 ? 's' : ''} behind
|
|
|
20
20
|
// commit left history — that is the usual cause (a pruned `fetch --depth 1`),
|
|
21
21
|
// but any other `rev-list` failure lands here too, so the cause is hedged.
|
|
22
22
|
const DIVERGED_HINT = "⚠️ Index is not at HEAD and the commit gap could not be counted — the recorded commit may no longer be in this clone's history. Run analyze tool to update.";
|
|
23
|
+
// `rev-list --count lastCommit..HEAD` answering 0 does NOT mean "HEAD is the
|
|
24
|
+
// indexed commit" — it means "HEAD has no commits lastCommit lacks", which is
|
|
25
|
+
// also true when HEAD is an *ancestor* of lastCommit (the working tree checked
|
|
26
|
+
// out an older commit than the one indexed, or switched to a line of history
|
|
27
|
+
// behind it). That read a rollback as `current` until this hint existed.
|
|
28
|
+
const REGRESSED_HINT = '⚠️ Index is not at HEAD — the indexed commit is not reachable from the checked-out commit (the working tree may have checked out an older commit, or a different line of history). Run analyze tool to update.';
|
|
23
29
|
const unknown = () => ({ isStale: false, commitsBehind: 0, status: 'unknown' });
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
// Called only once a positive HEAD-only count is in hand.
|
|
31
|
+
const behind = (commitsBehind) => ({
|
|
32
|
+
isStale: true,
|
|
33
|
+
commitsBehind,
|
|
34
|
+
hint: behindHint(commitsBehind),
|
|
35
|
+
status: 'behind',
|
|
36
|
+
});
|
|
27
37
|
/**
|
|
28
38
|
* `rev-list` could not answer. Asking for HEAD alone needs no history walk and
|
|
29
39
|
* still separates all three answers: HEAD unreadable is `unknown`, HEAD past the
|
|
@@ -38,6 +48,28 @@ const fromHead = (head, lastCommit) => {
|
|
|
38
48
|
return { isStale: false, commitsBehind: 0, status: 'current' };
|
|
39
49
|
return { isStale: false, commitsBehind: 0, hint: DIVERGED_HINT, status: 'diverged' };
|
|
40
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* `rev-list --left-right --count lastCommit...HEAD` measures both sides in
|
|
53
|
+
* one process, so a later HEAD change cannot mix two snapshots. The left
|
|
54
|
+
* count identifies a rollback even when the HEAD-only (right) count is 0.
|
|
55
|
+
* Positive right counts keep the existing `behind` behavior, including when
|
|
56
|
+
* both sides have commits (divergent branches or a re-shallowed clone).
|
|
57
|
+
*/
|
|
58
|
+
const fromCounts = (output) => {
|
|
59
|
+
const counts = /^(\d+)\s+(\d+)$/.exec(output.trim());
|
|
60
|
+
if (!counts)
|
|
61
|
+
return unknown();
|
|
62
|
+
const indexedOnly = Number(counts[1]);
|
|
63
|
+
const headOnly = Number(counts[2]);
|
|
64
|
+
if (!Number.isSafeInteger(indexedOnly) || !Number.isSafeInteger(headOnly))
|
|
65
|
+
return unknown();
|
|
66
|
+
if (headOnly > 0)
|
|
67
|
+
return behind(headOnly);
|
|
68
|
+
if (indexedOnly > 0) {
|
|
69
|
+
return { isStale: true, commitsBehind: 0, hint: REGRESSED_HINT, status: 'diverged' };
|
|
70
|
+
}
|
|
71
|
+
return { isStale: false, commitsBehind: 0, status: 'current' };
|
|
72
|
+
};
|
|
41
73
|
const readHeadSync = (repoPath) => {
|
|
42
74
|
try {
|
|
43
75
|
return (execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
@@ -45,6 +77,7 @@ const readHeadSync = (repoPath) => {
|
|
|
45
77
|
encoding: 'utf-8',
|
|
46
78
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
79
|
windowsHide: true,
|
|
80
|
+
timeout: STALENESS_TIMEOUT_MS,
|
|
48
81
|
}).trim() || null);
|
|
49
82
|
}
|
|
50
83
|
catch {
|
|
@@ -73,13 +106,13 @@ export function checkStaleness(repoPath, lastCommit) {
|
|
|
73
106
|
if (!lastCommit)
|
|
74
107
|
return unknown();
|
|
75
108
|
try {
|
|
76
|
-
const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}
|
|
109
|
+
const result = execFileSync('git', ['rev-list', '--left-right', '--count', `${lastCommit}...HEAD`], {
|
|
77
110
|
cwd: repoPath,
|
|
78
111
|
encoding: 'utf-8',
|
|
79
112
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
80
113
|
windowsHide: true,
|
|
81
|
-
})
|
|
82
|
-
return
|
|
114
|
+
});
|
|
115
|
+
return fromCounts(result);
|
|
83
116
|
}
|
|
84
117
|
catch {
|
|
85
118
|
return fromHead(readHeadSync(repoPath), lastCommit);
|
|
@@ -96,7 +129,7 @@ export async function checkStalenessAsync(repoPath, lastCommit) {
|
|
|
96
129
|
try {
|
|
97
130
|
// Note: promisified execFile captures stdout/stderr by default (no stdio option needed,
|
|
98
131
|
// unlike the sync variant which requires explicit stdio: ['pipe','pipe','pipe']).
|
|
99
|
-
const { stdout } = await execFileAsync('git', ['rev-list', '--count', `${lastCommit}
|
|
132
|
+
const { stdout } = await execFileAsync('git', ['rev-list', '--left-right', '--count', `${lastCommit}...HEAD`], {
|
|
100
133
|
cwd: repoPath,
|
|
101
134
|
encoding: 'utf-8',
|
|
102
135
|
windowsHide: true,
|
|
@@ -109,7 +142,7 @@ export async function checkStalenessAsync(repoPath, lastCommit) {
|
|
|
109
142
|
// below reports it as `unknown` — still the fail-closed `isStale: false`.
|
|
110
143
|
timeout: STALENESS_TIMEOUT_MS,
|
|
111
144
|
});
|
|
112
|
-
return
|
|
145
|
+
return fromCounts(stdout);
|
|
113
146
|
}
|
|
114
147
|
catch (err) {
|
|
115
148
|
// A rev-list that timed out means the working tree is not answering. Asking
|
|
@@ -893,9 +893,10 @@ export declare const buildFtsQueryCypher: (tableName: string, indexName: string,
|
|
|
893
893
|
* @param query - Search query string
|
|
894
894
|
* @param limit - Maximum results
|
|
895
895
|
* @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR)
|
|
896
|
+
* @param missingIndex - Preserve the empty-result default, or propagate missing indexes for diagnostics
|
|
896
897
|
* @returns Array of { node properties, score }
|
|
897
898
|
*/
|
|
898
|
-
export declare const queryFTS: (tableName: string, indexName: string, query: string, limit?: number, conjunctive?: boolean) => Promise<Array<{
|
|
899
|
+
export declare const queryFTS: (tableName: string, indexName: string, query: string, limit?: number, conjunctive?: boolean, missingIndex?: 'empty' | 'throw') => Promise<Array<{
|
|
899
900
|
nodeId: string;
|
|
900
901
|
name: string;
|
|
901
902
|
filePath: string;
|
|
@@ -3504,9 +3504,10 @@ export const buildFtsQueryCypher = (tableName, indexName, limit, conjunctive = f
|
|
|
3504
3504
|
* @param query - Search query string
|
|
3505
3505
|
* @param limit - Maximum results
|
|
3506
3506
|
* @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR)
|
|
3507
|
+
* @param missingIndex - Preserve the empty-result default, or propagate missing indexes for diagnostics
|
|
3507
3508
|
* @returns Array of { node properties, score }
|
|
3508
3509
|
*/
|
|
3509
|
-
export const queryFTS = async (tableName, indexName, query, limit = 20, conjunctive = false) => {
|
|
3510
|
+
export const queryFTS = async (tableName, indexName, query, limit = 20, conjunctive = false, missingIndex = 'empty') => {
|
|
3510
3511
|
if (!conn) {
|
|
3511
3512
|
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
3512
3513
|
}
|
|
@@ -3532,7 +3533,7 @@ export const queryFTS = async (tableName, indexName, query, limit = 20, conjunct
|
|
|
3532
3533
|
// NEW-6 — this used to be a bare `.includes('does not exist')` check
|
|
3533
3534
|
// that could not tell the two apart).
|
|
3534
3535
|
const message = e instanceof Error ? e.message : String(e);
|
|
3535
|
-
if (classifyFtsQueryError(message) === 'missing-index') {
|
|
3536
|
+
if (missingIndex === 'empty' && classifyFtsQueryError(message) === 'missing-index') {
|
|
3536
3537
|
return [];
|
|
3537
3538
|
}
|
|
3538
3539
|
throw e;
|
|
@@ -29,6 +29,8 @@ export interface FTSSearchResponse {
|
|
|
29
29
|
* which only does so when every table failed).
|
|
30
30
|
*/
|
|
31
31
|
nonBenignErrors?: string[];
|
|
32
|
+
/** Configured table.index names that could not be queried because their index is missing. */
|
|
33
|
+
missingIndexes?: string[];
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Search using LadybugDB's built-in FTS (fresh, reads from disk)
|
|
@@ -86,6 +86,7 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId, disabledReaso
|
|
|
86
86
|
const resultsByIndex = [];
|
|
87
87
|
let queriesSucceeded = 0;
|
|
88
88
|
const nonBenignErrors = [];
|
|
89
|
+
const missingIndexes = [];
|
|
89
90
|
const ftsExtension = getExtensionCapabilities().find((c) => c.name === 'fts');
|
|
90
91
|
if (ftsExtension && !ftsExtension.loaded) {
|
|
91
92
|
// tri-review NEW-4 (applies to BOTH the MCP pool path and the CLI/pipeline
|
|
@@ -114,26 +115,32 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId, disabledReaso
|
|
|
114
115
|
queriesSucceeded++;
|
|
115
116
|
resultsByIndex.push(outcome.rows);
|
|
116
117
|
}
|
|
117
|
-
else if (
|
|
118
|
+
else if (outcome.benign) {
|
|
119
|
+
missingIndexes.push(`${table}.${indexName}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
118
122
|
nonBenignErrors.push(redactPaths(outcome.message ?? 'Unknown FTS query error'));
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
else {
|
|
123
127
|
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// missing-table or real query error rethrows here — track it the same way
|
|
127
|
-
// the MCP pool path does instead of a bare `catch {}` that dropped it.
|
|
128
|
+
// Opt into missing-index propagation so absent indexes cannot masquerade
|
|
129
|
+
// as successful zero-match queries. Keep core/pool classification identical.
|
|
128
130
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
129
131
|
try {
|
|
130
|
-
const result = await queryFTS(table, indexName, searchQuery, limit, false);
|
|
132
|
+
const result = await queryFTS(table, indexName, searchQuery, limit, false, 'throw');
|
|
131
133
|
queriesSucceeded++;
|
|
132
134
|
resultsByIndex.push(result);
|
|
133
135
|
}
|
|
134
136
|
catch (e) {
|
|
135
137
|
const message = e instanceof Error ? e.message : String(e);
|
|
136
|
-
|
|
138
|
+
if (classifyFtsQueryError(message) === 'missing-index') {
|
|
139
|
+
missingIndexes.push(`${table}.${indexName}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
nonBenignErrors.push(redactPaths(message));
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
}
|
|
139
146
|
}
|
|
@@ -174,5 +181,6 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId, disabledReaso
|
|
|
174
181
|
})),
|
|
175
182
|
ftsAvailable,
|
|
176
183
|
...(nonBenignErrors.length > 0 && { nonBenignErrors }),
|
|
184
|
+
...(missingIndexes.length > 0 && { missingIndexes }),
|
|
177
185
|
};
|
|
178
186
|
};
|
|
@@ -48,15 +48,12 @@ export interface FtsWarningContext {
|
|
|
48
48
|
*/
|
|
49
49
|
export declare const ftsDegradedWarning: (context?: FtsWarningContext, disabledReason?: FtsDisabledReason) => string;
|
|
50
50
|
/**
|
|
51
|
-
* Warning
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* so this deliberately does NOT suggest it: reusing the missing-index
|
|
56
|
-
* message here would reproduce, for this cause, the exact misleading
|
|
57
|
-
* "run --repair-fts" guidance #2767 itself was about (tri-review NEW-1).
|
|
51
|
+
* Warning when no FTS query succeeded and at least one failed for a real,
|
|
52
|
+
* non-benign reason (timeout, connection reset, native fault). `--repair-fts`
|
|
53
|
+
* will not fix those errors. If indexes are also missing, the caller composes
|
|
54
|
+
* their repair guidance separately; do not deny that additional failure cause.
|
|
58
55
|
*/
|
|
59
|
-
export declare const ftsQueryFailedWarning: (context: FtsWarningContext) => string;
|
|
56
|
+
export declare const ftsQueryFailedWarning: (context: FtsWarningContext, hasMissingIndexes?: boolean) => string;
|
|
60
57
|
export declare const SUPPORTED_FTS_STEMMERS: ReadonlySet<string>;
|
|
61
58
|
export interface CreateSearchFTSIndexesOptions {
|
|
62
59
|
indexes?: readonly FTSIndexDefinition[];
|
|
@@ -72,18 +72,17 @@ export const ftsDegradedWarning = (context, disabledReason) => {
|
|
|
72
72
|
suffix);
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
|
-
* Warning
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* so this deliberately does NOT suggest it: reusing the missing-index
|
|
80
|
-
* message here would reproduce, for this cause, the exact misleading
|
|
81
|
-
* "run --repair-fts" guidance #2767 itself was about (tri-review NEW-1).
|
|
75
|
+
* Warning when no FTS query succeeded and at least one failed for a real,
|
|
76
|
+
* non-benign reason (timeout, connection reset, native fault). `--repair-fts`
|
|
77
|
+
* will not fix those errors. If indexes are also missing, the caller composes
|
|
78
|
+
* their repair guidance separately; do not deny that additional failure cause.
|
|
82
79
|
*/
|
|
83
|
-
export const ftsQueryFailedWarning = (context) => 'FTS keyword search failed — every configured index query returned an error' +
|
|
80
|
+
export const ftsQueryFailedWarning = (context, hasMissingIndexes = false) => 'FTS keyword search failed — every configured index query returned an error' +
|
|
84
81
|
(context.lastErrorRedacted ? ` (${context.lastErrorRedacted})` : '') +
|
|
85
|
-
'; results do not include keyword matches.
|
|
86
|
-
|
|
82
|
+
'; results do not include keyword matches. ' +
|
|
83
|
+
(hasMissingIndexes
|
|
84
|
+
? 'See server logs for query error details.'
|
|
85
|
+
: 'This is not a missing-index condition — see server logs for details.') +
|
|
87
86
|
` (resolved: ${formatResolvedSuffix(context)})`;
|
|
88
87
|
// Stemmers shipped by the LadybugDB FTS extension. Mirrors the lowercase token
|
|
89
88
|
// set in the extension bundled with @ladybugdb/core 0.18.x (see package.json).
|
|
@@ -17,21 +17,35 @@
|
|
|
17
17
|
* provably stale index indistinguishable from a fresh one. `status` is the
|
|
18
18
|
* additive channel that separates them for a caller that wants to act on it:
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* - `diverged` — `rev-list` could not answer, but HEAD resolved and is not the
|
|
24
|
-
* indexed commit. The index is provably not at HEAD; only the count is
|
|
25
|
-
* unknown. A branch-pinned `serve` clone reaches this once git prunes the
|
|
26
|
-
* commit a failed re-index left behind — the pinned update is a
|
|
27
|
-
* `fetch --depth 1`, which orphans it — and a rewritten history reaches it
|
|
28
|
-
* directly. It is the rule the Claude hook already applies:
|
|
29
|
-
* HEAD !== lastCommit.
|
|
30
|
-
* - `unknown` — HEAD could not be resolved at all: not a git repository, git
|
|
31
|
-
* timed out, or no commit was recorded.
|
|
20
|
+
* The successful probe is `rev-list --left-right --count lastCommit...HEAD`:
|
|
21
|
+
* the left count is indexed-only commits, and the right is HEAD-only commits.
|
|
22
|
+
* Both counts come from one HEAD snapshot, without a follow-up process.
|
|
32
23
|
*
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
24
|
+
* - `current` — both counts are 0, or `rev-list` could not answer but a
|
|
25
|
+
* fallback `rev-parse HEAD` resolved to the indexed commit.
|
|
26
|
+
* - `behind` — the right count is N > 0; `commitsBehind` is N, including
|
|
27
|
+
* when the left count is positive too (divergent or shallow history).
|
|
28
|
+
* - `diverged` — the index is provably not at HEAD, reached two different ways:
|
|
29
|
+
* - The left count is positive and the right is 0: HEAD is an ancestor
|
|
30
|
+
* of the indexed commit (#3127). The working tree checked out an older
|
|
31
|
+
* commit than the one indexed, or a release branch behind the indexed
|
|
32
|
+
* tip. The mismatch is established: `isStale` is `true` and
|
|
33
|
+
* `commitsBehind` stays 0 (there is no forward count to report).
|
|
34
|
+
* - `rev-list` could not answer at all, but HEAD resolved and is not the
|
|
35
|
+
* indexed commit: only the count is unknown. A branch-pinned `serve`
|
|
36
|
+
* clone reaches this once git prunes the commit a failed re-index left
|
|
37
|
+
* behind — the pinned update is a `fetch --depth 1`, which orphans it —
|
|
38
|
+
* and a rewritten history reaches it directly. This arm keeps the
|
|
39
|
+
* historical fail-open `isStale: false` (see below).
|
|
40
|
+
* - `unknown` — the probe could not establish the relationship: no readable
|
|
41
|
+
* HEAD, a timeout, no recorded commit, or malformed count output.
|
|
42
|
+
*
|
|
43
|
+
* `isStale` and `commitsBehind` keep their historical fail-open values
|
|
44
|
+
* (`false` / `0`) whenever the check could not fully answer — every `unknown`,
|
|
45
|
+
* and the `rev-list`-failure arm of `diverged` — so no existing consumer
|
|
46
|
+
* changes behaviour there unless it reads `status`. The other arm of
|
|
47
|
+
* `diverged` (the confirmed rollback) is a successful, computed
|
|
48
|
+
* answer rather than a failure, so `isStale` reflects it (`true`) instead.
|
|
35
49
|
*/
|
|
36
50
|
export type StalenessStatus = 'current' | 'behind' | 'diverged' | 'unknown';
|
|
37
51
|
export interface StalenessInfo {
|
|
@@ -2413,6 +2413,7 @@ export class LocalBackend {
|
|
|
2413
2413
|
// regardless of whether OTHER tables succeeded — previously a real error
|
|
2414
2414
|
// on N-1 of N tables while one succeeded left zero diagnostic trail.
|
|
2415
2415
|
const ftsQueryErrors = bm25SearchResult?.nonBenignErrors;
|
|
2416
|
+
const ftsMissingIndexes = bm25SearchResult?.missingIndexes;
|
|
2416
2417
|
if (ftsQueryErrors) {
|
|
2417
2418
|
// tri-review NEW-5: these strings are already classified non-benign by
|
|
2418
2419
|
// classifyFtsQueryError — do NOT route them through logQueryError,
|
|
@@ -2847,12 +2848,11 @@ export class LocalBackend {
|
|
|
2847
2848
|
branch: repo.branch,
|
|
2848
2849
|
indexedAt: this.lastObservedPoolState.get(repo.lbugPath)?.indexedAt ?? repo.indexedAt,
|
|
2849
2850
|
};
|
|
2850
|
-
//
|
|
2851
|
-
//
|
|
2852
|
-
//
|
|
2853
|
-
// instead of burying the real cause as a trailing suffix on bad advice.
|
|
2851
|
+
// Real errors (timeout, connection reset) need their own diagnosis.
|
|
2852
|
+
// When some indexes are also missing, preserve both causes and append
|
|
2853
|
+
// their repair guidance below even though no FTS query succeeded.
|
|
2854
2854
|
warnings.push(ftsQueryErrors
|
|
2855
|
-
? ftsQueryFailedWarning({ ...warningContext, lastErrorRedacted: ftsQueryErrors[0] })
|
|
2855
|
+
? ftsQueryFailedWarning({ ...warningContext, lastErrorRedacted: ftsQueryErrors[0] }, !!ftsMissingIndexes?.length)
|
|
2856
2856
|
: ftsDegradedWarning(warningContext, ftsDisabledReason));
|
|
2857
2857
|
}
|
|
2858
2858
|
else if (ftsQueryErrors) {
|
|
@@ -2863,6 +2863,10 @@ export class LocalBackend {
|
|
|
2863
2863
|
// that convention instead of only logging server-side.
|
|
2864
2864
|
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).`);
|
|
2865
2865
|
}
|
|
2866
|
+
if (ftsMissingIndexes?.length && (ftsUsed || ftsQueryErrors)) {
|
|
2867
|
+
warnings.push(`FTS keyword search is incomplete: missing configured indexes (${ftsMissingIndexes.join(', ')}). ` +
|
|
2868
|
+
'Results may be missing matches from those node types. Run `gitnexus analyze --repair-fts`.');
|
|
2869
|
+
}
|
|
2866
2870
|
// #2331: a CJK query against a server process resolving
|
|
2867
2871
|
// GITNEXUS_FTS_CJK_SEGMENTATION to 'none' silently misses sub-phrase
|
|
2868
2872
|
// matches with no other signal — this is the only place an agent driving
|
|
@@ -2979,7 +2983,7 @@ export class LocalBackend {
|
|
|
2979
2983
|
// #2767: a partial FTS failure (some tables ok, one or more real errors)
|
|
2980
2984
|
// is as much a "results may be incomplete" signal as enrichmentDegraded —
|
|
2981
2985
|
// flag it the same way rather than only via the warning string.
|
|
2982
|
-
const ftsPartial = ftsUsed && !!ftsQueryErrors;
|
|
2986
|
+
const ftsPartial = ftsUsed && (!!ftsQueryErrors || !!ftsMissingIndexes?.length);
|
|
2983
2987
|
return {
|
|
2984
2988
|
processes,
|
|
2985
2989
|
process_symbols,
|
|
@@ -3023,6 +3027,7 @@ export class LocalBackend {
|
|
|
3023
3027
|
const bm25Results = ftsResponse?.results ?? [];
|
|
3024
3028
|
const ftsUsed = ftsResponse?.ftsAvailable ?? false;
|
|
3025
3029
|
const nonBenignErrors = ftsResponse?.nonBenignErrors;
|
|
3030
|
+
const missingIndexes = ftsResponse?.missingIndexes;
|
|
3026
3031
|
const results = [];
|
|
3027
3032
|
for (const bm25Result of bm25Results) {
|
|
3028
3033
|
const fullPath = bm25Result.filePath;
|
|
@@ -3087,7 +3092,12 @@ export class LocalBackend {
|
|
|
3087
3092
|
});
|
|
3088
3093
|
}
|
|
3089
3094
|
}
|
|
3090
|
-
return {
|
|
3095
|
+
return {
|
|
3096
|
+
results,
|
|
3097
|
+
ftsUsed,
|
|
3098
|
+
...(nonBenignErrors && { nonBenignErrors }),
|
|
3099
|
+
...(missingIndexes && { missingIndexes }),
|
|
3100
|
+
};
|
|
3091
3101
|
}
|
|
3092
3102
|
/**
|
|
3093
3103
|
* Keyword-only indexes are the default. Do not put this on `query.warning`
|
package/dist/mcp/resources.js
CHANGED
|
@@ -281,7 +281,7 @@ async function getContextResource(backend, repoName) {
|
|
|
281
281
|
const incompleteReasons = getIndexIncompleteReasons(freshMeta);
|
|
282
282
|
// Check staleness using the current on-disk lastCommit (not the cached handle)
|
|
283
283
|
const repoPath = repo.repoPath;
|
|
284
|
-
const lastCommit = freshMeta?.lastCommit ?? repo.lastCommit ?? '
|
|
284
|
+
const lastCommit = freshMeta?.lastCommit ?? repo.lastCommit ?? '';
|
|
285
285
|
const staleness = repoPath
|
|
286
286
|
? checkStaleness(repoPath, lastCommit)
|
|
287
287
|
: { isStale: false, commitsBehind: 0 };
|
package/package.json
CHANGED