gitnexus 1.6.10-rc.185 → 1.6.10-rc.186
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.
|
@@ -53,6 +53,29 @@ export declare const shouldFlushCSVBuffer: (byteCount: number) => boolean;
|
|
|
53
53
|
export declare const sanitizeUTF8: (str: string) => string;
|
|
54
54
|
export declare const escapeCSVField: (value: string | number | undefined | null) => string;
|
|
55
55
|
export declare const escapeCSVNumber: (value: number | undefined | null, defaultValue?: number) => string;
|
|
56
|
+
/**
|
|
57
|
+
* Did this text come from a binary payload? Density of non-printables over the
|
|
58
|
+
* first {@link BINARY_SAMPLE_CHARS} characters, above 10%.
|
|
59
|
+
*
|
|
60
|
+
* U+FFFD counts, and it is the character that matters most here (#2889). Every
|
|
61
|
+
* source file enters the pipeline through a `utf-8` decode — the content cache
|
|
62
|
+
* below reads with `fs.readFile(path, 'utf-8')`, and the parse worker decodes
|
|
63
|
+
* the same way. That decode is lossy and total: an invalid byte sequence never
|
|
64
|
+
* survives as invalid bytes, it is REPLACED with U+FFFD. So the embedded-binary
|
|
65
|
+
* payloads #2889 describes (webpack bundles, class-file constant pools,
|
|
66
|
+
* serialized objects inside .js/.vue sources) arrive here as long runs of
|
|
67
|
+
* U+FFFD, not as the control bytes this scan was originally written to count —
|
|
68
|
+
* charCode 0xFFFD is neither `< 9`, nor between 13 and 32, nor 127, so the
|
|
69
|
+
* detector scored a wholly corrupt payload as clean text and every caller waved
|
|
70
|
+
* it through.
|
|
71
|
+
*
|
|
72
|
+
* The threshold stays at 10%: a legitimate source file carries no replacement
|
|
73
|
+
* characters at all unless it was mis-decoded, and a handful (a stray latin-1
|
|
74
|
+
* comment, one bad byte in a license header) still scores far under the bar.
|
|
75
|
+
* Density rather than "contains any binary run" is deliberate — a description
|
|
76
|
+
* that is mostly real prose with one stray replacement character is worth more
|
|
77
|
+
* indexed than dropped.
|
|
78
|
+
*/
|
|
56
79
|
export declare const isBinaryContent: (content: string) => boolean;
|
|
57
80
|
/**
|
|
58
81
|
* Flatten newlines and tabs to single spaces for FTS-indexed text columns
|
|
@@ -113,17 +113,44 @@ const formatCSVStringArray = (value) => {
|
|
|
113
113
|
// ============================================================================
|
|
114
114
|
// CONTENT EXTRACTION (lazy — reads from disk on demand)
|
|
115
115
|
// ============================================================================
|
|
116
|
+
const BINARY_SAMPLE_CHARS = 1000;
|
|
117
|
+
const UNICODE_REPLACEMENT_CHAR = 0xfffd;
|
|
118
|
+
/**
|
|
119
|
+
* Did this text come from a binary payload? Density of non-printables over the
|
|
120
|
+
* first {@link BINARY_SAMPLE_CHARS} characters, above 10%.
|
|
121
|
+
*
|
|
122
|
+
* U+FFFD counts, and it is the character that matters most here (#2889). Every
|
|
123
|
+
* source file enters the pipeline through a `utf-8` decode — the content cache
|
|
124
|
+
* below reads with `fs.readFile(path, 'utf-8')`, and the parse worker decodes
|
|
125
|
+
* the same way. That decode is lossy and total: an invalid byte sequence never
|
|
126
|
+
* survives as invalid bytes, it is REPLACED with U+FFFD. So the embedded-binary
|
|
127
|
+
* payloads #2889 describes (webpack bundles, class-file constant pools,
|
|
128
|
+
* serialized objects inside .js/.vue sources) arrive here as long runs of
|
|
129
|
+
* U+FFFD, not as the control bytes this scan was originally written to count —
|
|
130
|
+
* charCode 0xFFFD is neither `< 9`, nor between 13 and 32, nor 127, so the
|
|
131
|
+
* detector scored a wholly corrupt payload as clean text and every caller waved
|
|
132
|
+
* it through.
|
|
133
|
+
*
|
|
134
|
+
* The threshold stays at 10%: a legitimate source file carries no replacement
|
|
135
|
+
* characters at all unless it was mis-decoded, and a handful (a stray latin-1
|
|
136
|
+
* comment, one bad byte in a license header) still scores far under the bar.
|
|
137
|
+
* Density rather than "contains any binary run" is deliberate — a description
|
|
138
|
+
* that is mostly real prose with one stray replacement character is worth more
|
|
139
|
+
* indexed than dropped.
|
|
140
|
+
*/
|
|
116
141
|
export const isBinaryContent = (content) => {
|
|
117
|
-
|
|
142
|
+
// `content &&` keeps the original tolerance for a null/undefined caller —
|
|
143
|
+
// `strict` is off in this package, so the type alone does not rule it out.
|
|
144
|
+
const end = content ? Math.min(content.length, BINARY_SAMPLE_CHARS) : 0;
|
|
145
|
+
if (end === 0)
|
|
118
146
|
return false;
|
|
119
|
-
const sample = content.slice(0, 1000);
|
|
120
147
|
let nonPrintable = 0;
|
|
121
|
-
for (let i = 0; i <
|
|
122
|
-
const code =
|
|
123
|
-
if (code < 9 || (code > 13 && code < 32) || code === 127)
|
|
148
|
+
for (let i = 0; i < end; i++) {
|
|
149
|
+
const code = content.charCodeAt(i);
|
|
150
|
+
if (code < 9 || (code > 13 && code < 32) || code === 127 || code === UNICODE_REPLACEMENT_CHAR)
|
|
124
151
|
nonPrintable++;
|
|
125
152
|
}
|
|
126
|
-
return nonPrintable /
|
|
153
|
+
return nonPrintable / end > 0.1;
|
|
127
154
|
};
|
|
128
155
|
/**
|
|
129
156
|
* LRU content cache — avoids re-reading the same source file for every
|
|
@@ -200,8 +227,20 @@ class FileContentCache {
|
|
|
200
227
|
* otherwise fail to match whitespace-normalized indexed content.
|
|
201
228
|
*/
|
|
202
229
|
export const normalizeFtsText = (text) => text.replace(/[\r\n\t]+/g, ' ');
|
|
203
|
-
/**
|
|
204
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Composes both FTS-text transforms for the `description` column — one place for
|
|
232
|
+
* the six emission sites below to call, instead of repeating the composition.
|
|
233
|
+
*
|
|
234
|
+
* Binary-looking descriptions are dropped rather than transformed (#2889). The
|
|
235
|
+
* `content` column has always been gated on {@link isBinaryContent} inside
|
|
236
|
+
* {@link extractContent}; `description` never was, so a symbol whose "doc
|
|
237
|
+
* comment" is really a slice of an embedded binary payload had that payload
|
|
238
|
+
* copied verbatim into an FTS-indexed column. Dropping it here rather than at
|
|
239
|
+
* each of the six call sites keeps the gate in the same place as the transforms
|
|
240
|
+
* it guards. Empty string, not a sentinel: unlike `content`, a description has
|
|
241
|
+
* no reader that needs to be told why it is missing.
|
|
242
|
+
*/
|
|
243
|
+
const formatFtsDescription = (description) => isBinaryContent(description) ? '' : normalizeFtsText(applyCjkSegmentationIfEnabled(description));
|
|
205
244
|
// Labels that get exact source-span content (no ±2 window). Single source of
|
|
206
245
|
// truth in `symbol-labels.ts` — see there for why the exactness depends on the
|
|
207
246
|
// 0-based line invariant. Kept as a named alias to read intent at the use site.
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -21,7 +21,7 @@ import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
|
21
21
|
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, ensureEmbeddingRowDmlSafe, ensureFtsRowDmlSafe, readIndexCatalogSnapshot, INDEX_CATALOG_UNREADABLE, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, deleteAllAdvisedBy, deleteSpringAopEvidenceNodes, deleteSpringAutoConfigurationDeclarations, deleteSpringAutoConfigurationSyntheticClasses, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
|
|
22
22
|
import { estimateBufferPool, setBufferPoolSizeHint, resolveNativeSafeStorageDir, } from './lbug/lbug-config.js';
|
|
23
23
|
import { escapeCypherString } from './lbug/cypher-escape.js';
|
|
24
|
-
import { buildSearchIndexesOrDegrade, ftsFailureIsFatal, createSearchFTSIndexes, dropSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
24
|
+
import { buildSearchIndexesOrDegrade, ftsFailureIsFatal, createSearchFTSIndexes, summarizeFtsIndexBuildFailures, dropSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
|
|
25
25
|
import { cjkSegmentationModeMismatch, getSearchFTSCjkSegmentation, initialiseSearchFTSCjkSegmentation, } from './search/cjk-segmentation.js';
|
|
26
26
|
import { getExtensionCapability, getExtensionCapabilities, getFtsCapability, resolveAnalyzeInstallPolicy, } from './lbug/extension-loader.js';
|
|
27
27
|
import { diagnoseExtensionLoad } from './lbug/extension-load-error.js';
|
|
@@ -716,7 +716,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
716
716
|
remedyTail);
|
|
717
717
|
}
|
|
718
718
|
progress('fts', 85, 'Repairing search indexes...');
|
|
719
|
-
await createSearchFTSIndexes({
|
|
719
|
+
const repairFailures = await createSearchFTSIndexes({
|
|
720
720
|
onIndexStart: options.verbose
|
|
721
721
|
? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
|
|
722
722
|
: undefined,
|
|
@@ -726,7 +726,14 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, r
|
|
|
726
726
|
});
|
|
727
727
|
const missing = await verifySearchFTSIndexes(executeQuery);
|
|
728
728
|
if (missing.length > 0) {
|
|
729
|
-
|
|
729
|
+
// #2889: name WHY each index is missing when the build itself said so.
|
|
730
|
+
// Repair now rebuilds every table it can before reporting, so the tables
|
|
731
|
+
// absent from this list were genuinely repaired even on a failed run —
|
|
732
|
+
// previously the first failure aborted the sweep and the message could
|
|
733
|
+
// only ever list "missing", never a reason. Same sentence the analyze
|
|
734
|
+
// degrade path prints, so one failure does not read two ways.
|
|
735
|
+
const reasons = repairFailures.length > 0 ? ` ${summarizeFtsIndexBuildFailures(repairFailures)}.` : '';
|
|
736
|
+
throw new Error(`FTS repair failed - missing indexes after rebuild: ${missing.join(', ')}.${reasons} ` +
|
|
730
737
|
'Run `gitnexus analyze --force` to perform a full graph+FTS rebuild; ' +
|
|
731
738
|
'if that also fails, verify FTS extension availability via `gitnexus doctor`.');
|
|
732
739
|
}
|
|
@@ -99,7 +99,46 @@ export declare function getSearchFTSStemmer(): string;
|
|
|
99
99
|
* `lbug-adapter.ts`. Omit it to have the sweep read the catalog itself.
|
|
100
100
|
*/
|
|
101
101
|
export declare function dropSearchFTSIndexes(indexRows?: IndexCatalogSnapshot): Promise<void>;
|
|
102
|
-
|
|
102
|
+
/** One configured index that could not be (re)built, and why. */
|
|
103
|
+
export interface FtsIndexBuildFailure {
|
|
104
|
+
table: string;
|
|
105
|
+
indexName: string;
|
|
106
|
+
/** The raw LadybugDB message, unmodified — it is the only row-level evidence there is. */
|
|
107
|
+
error: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build every configured FTS index, and keep going when one of them fails
|
|
111
|
+
* (#2889).
|
|
112
|
+
*
|
|
113
|
+
* The loop used to let the first failure propagate, which made a single
|
|
114
|
+
* untokenizable row far more expensive than it looks: `dropFTSIndex` has
|
|
115
|
+
* already run for the failing table, so that table ends with NO index, and
|
|
116
|
+
* every table after it in {@link FTS_INDEXES} order is never reached — on a
|
|
117
|
+
* fresh build, or on the incremental path where `dropSearchFTSIndexes` cleared
|
|
118
|
+
* them all up front, those tables end with no index either. One bad `Method`
|
|
119
|
+
* row therefore cost keyword search on Namespace, Property, Record, Union,
|
|
120
|
+
* Static and Variable as well, and `verifySearchFTSIndexes` never ran to say
|
|
121
|
+
* so. The blast radius was an artifact of loop control flow, not of the data.
|
|
122
|
+
*
|
|
123
|
+
* Isolating per index bounds the damage to the table that actually holds the
|
|
124
|
+
* bad row, and makes `--repair-fts` able to recover everything else. Failures
|
|
125
|
+
* are returned rather than thrown so the caller can decide — degrade or abort —
|
|
126
|
+
* with every failure in hand instead of only the first.
|
|
127
|
+
*/
|
|
128
|
+
export declare function createSearchFTSIndexes(options?: CreateSearchFTSIndexesOptions): Promise<FtsIndexBuildFailure[]>;
|
|
129
|
+
/**
|
|
130
|
+
* One sentence naming every table that failed and why, e.g. `FTS index build
|
|
131
|
+
* failed for 2 of 21 tables: Method.method_fts (Runtime exception: …), …`.
|
|
132
|
+
*
|
|
133
|
+
* Lives here rather than at the call sites because only this module knows the
|
|
134
|
+
* denominator. Both the analyze degrade path and `--repair-fts` render it, so
|
|
135
|
+
* one failure reads the same way whichever command produced it.
|
|
136
|
+
*
|
|
137
|
+
* Embeds the raw LadybugDB message UNREDACTED — CLI and log surfaces only.
|
|
138
|
+
* Anything heading for a network response has to pass it through
|
|
139
|
+
* {@link redactPaths} first, the same rule the query-side warnings follow.
|
|
140
|
+
*/
|
|
141
|
+
export declare const summarizeFtsIndexBuildFailures: (failures: readonly FtsIndexBuildFailure[]) => string;
|
|
103
142
|
export declare function verifySearchFTSIndexes(executeQuery: (cypher: string) => Promise<unknown[]>): Promise<string[]>;
|
|
104
143
|
/**
|
|
105
144
|
* Why an FTS build failed, so the caller can react correctly (#2658):
|
|
@@ -209,8 +209,28 @@ export async function dropSearchFTSIndexes(indexRows) {
|
|
|
209
209
|
await dropFTSIndex(table, indexName);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Build every configured FTS index, and keep going when one of them fails
|
|
214
|
+
* (#2889).
|
|
215
|
+
*
|
|
216
|
+
* The loop used to let the first failure propagate, which made a single
|
|
217
|
+
* untokenizable row far more expensive than it looks: `dropFTSIndex` has
|
|
218
|
+
* already run for the failing table, so that table ends with NO index, and
|
|
219
|
+
* every table after it in {@link FTS_INDEXES} order is never reached — on a
|
|
220
|
+
* fresh build, or on the incremental path where `dropSearchFTSIndexes` cleared
|
|
221
|
+
* them all up front, those tables end with no index either. One bad `Method`
|
|
222
|
+
* row therefore cost keyword search on Namespace, Property, Record, Union,
|
|
223
|
+
* Static and Variable as well, and `verifySearchFTSIndexes` never ran to say
|
|
224
|
+
* so. The blast radius was an artifact of loop control flow, not of the data.
|
|
225
|
+
*
|
|
226
|
+
* Isolating per index bounds the damage to the table that actually holds the
|
|
227
|
+
* bad row, and makes `--repair-fts` able to recover everything else. Failures
|
|
228
|
+
* are returned rather than thrown so the caller can decide — degrade or abort —
|
|
229
|
+
* with every failure in hand instead of only the first.
|
|
230
|
+
*/
|
|
212
231
|
export async function createSearchFTSIndexes(options) {
|
|
213
232
|
const stemmer = getSearchFTSStemmer();
|
|
233
|
+
const failures = [];
|
|
214
234
|
for (const { table, indexName, properties } of FTS_INDEXES) {
|
|
215
235
|
options?.onIndexStart?.(table, indexName);
|
|
216
236
|
// Drop first so the live `properties` always win. `createFTSIndex` is
|
|
@@ -224,11 +244,34 @@ export async function createSearchFTSIndexes(options) {
|
|
|
224
244
|
// skipping when present; FTS build is proportional to symbol-table size and
|
|
225
245
|
// runs inside the existing FTS phase. Gate on a stored schema fingerprint if
|
|
226
246
|
// this rebuild cost ever shows up in analyze profiles.
|
|
227
|
-
|
|
228
|
-
|
|
247
|
+
try {
|
|
248
|
+
await dropFTSIndex(table, indexName);
|
|
249
|
+
await createFTSIndex(table, indexName, [...properties], stemmer);
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
// The message, never the Error: holding the Error would pin its stack and
|
|
253
|
+
// whatever the native binding attached to it, for up to one per table.
|
|
254
|
+
failures.push({ table, indexName, error: e instanceof Error ? e.message : String(e) });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
229
257
|
options?.onIndexReady?.(table, indexName);
|
|
230
258
|
}
|
|
259
|
+
return failures;
|
|
231
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* One sentence naming every table that failed and why, e.g. `FTS index build
|
|
263
|
+
* failed for 2 of 21 tables: Method.method_fts (Runtime exception: …), …`.
|
|
264
|
+
*
|
|
265
|
+
* Lives here rather than at the call sites because only this module knows the
|
|
266
|
+
* denominator. Both the analyze degrade path and `--repair-fts` render it, so
|
|
267
|
+
* one failure reads the same way whichever command produced it.
|
|
268
|
+
*
|
|
269
|
+
* Embeds the raw LadybugDB message UNREDACTED — CLI and log surfaces only.
|
|
270
|
+
* Anything heading for a network response has to pass it through
|
|
271
|
+
* {@link redactPaths} first, the same rule the query-side warnings follow.
|
|
272
|
+
*/
|
|
273
|
+
export const summarizeFtsIndexBuildFailures = (failures) => `FTS index build failed for ${failures.length} of ${FTS_INDEXES.length} tables: ` +
|
|
274
|
+
failures.map((f) => `${f.table}.${f.indexName} (${f.error})`).join(', ');
|
|
232
275
|
export async function verifySearchFTSIndexes(executeQuery) {
|
|
233
276
|
// Read the catalog once and check each configured index both EXISTS and
|
|
234
277
|
// covers its expected columns. A queryability-only probe (CALL QUERY_FTS_INDEX
|
|
@@ -328,16 +371,35 @@ export const ftsFailureIsFatal = (failureClass, useAtomicSwap) => failureClass =
|
|
|
328
371
|
*/
|
|
329
372
|
export async function buildSearchIndexesOrDegrade(executeQuery, options) {
|
|
330
373
|
try {
|
|
331
|
-
|
|
374
|
+
// Verify ALWAYS runs, failures or not (#2889). Reporting must not jump the
|
|
375
|
+
// queue ahead of verification: a partial build is exactly when "the other
|
|
376
|
+
// tables are fine" needs proving rather than asserting, and a stale
|
|
377
|
+
// name+content-only index is invisible to the build (it succeeds) yet still
|
|
378
|
+
// means description search is broken (#2299).
|
|
379
|
+
const failures = await createSearchFTSIndexes(options);
|
|
332
380
|
const missing = await verifySearchFTSIndexes(executeQuery);
|
|
333
|
-
if (missing.length
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
381
|
+
if (failures.length === 0 && missing.length === 0)
|
|
382
|
+
return { ok: true };
|
|
383
|
+
// A table that failed to build is necessarily missing too — report it once,
|
|
384
|
+
// with its reason, and keep `missing` for indexes nothing explains.
|
|
385
|
+
const named = new Set(failures.map((f) => `${f.table}.${f.indexName}`));
|
|
386
|
+
const unexplained = missing.filter((name) => !named.has(name));
|
|
387
|
+
const error = [
|
|
388
|
+
failures.length > 0 ? summarizeFtsIndexBuildFailures(failures) : '',
|
|
389
|
+
// Structural incompleteness with no thrown error — classified capability
|
|
390
|
+
// (degrade) below, matching prior behavior; a broken *write* surfaces as
|
|
391
|
+
// a thrown IO/checkpoint error and is classified integrity there.
|
|
392
|
+
unexplained.length > 0 ? `missing indexes after build: ${unexplained.join(', ')}` : '',
|
|
393
|
+
]
|
|
394
|
+
.filter((part) => part.length > 0)
|
|
395
|
+
.join('; ');
|
|
396
|
+
// Classify per failure, not over the joined text: capability signatures are
|
|
397
|
+
// checked first, so folding the messages together would let an untokenizable
|
|
398
|
+
// row mask a genuinely broken write and downgrade an abort into a degrade.
|
|
399
|
+
const failureClass = failures.some((f) => classifyFtsBuildError(f.error) === 'integrity')
|
|
400
|
+
? 'integrity'
|
|
401
|
+
: classifyFtsBuildError(error);
|
|
402
|
+
return { ok: false, error, failureClass };
|
|
341
403
|
}
|
|
342
404
|
catch (e) {
|
|
343
405
|
const error = e instanceof Error ? e.message : String(e);
|
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.186",
|
|
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",
|