gitnexus 1.6.13-rc.80 → 1.6.13-rc.81

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.
@@ -1035,18 +1035,33 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1035
1035
  ? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
1036
1036
  : undefined,
1037
1037
  });
1038
- const missing = await verifySearchFTSIndexes(executeQuery, ftsIndexes);
1039
- if (missing.length > 0) {
1040
- // #2889: name WHY each index is missing when the build itself said so.
1041
- // Repair now rebuilds every table it can before reporting, so the tables
1042
- // absent from this list were genuinely repaired even on a failed run —
1043
- // previously the first failure aborted the sweep and the message could
1044
- // only ever list "missing", never a reason. Same sentence the analyze
1045
- // degrade path prints, so one failure does not read two ways.
1046
- const reasons = repairFailures.length > 0
1047
- ? ` ${summarizeFtsIndexBuildFailures(repairFailures, ftsIndexes)}.`
1048
- : '';
1049
- throw new Error(`FTS repair failed - missing indexes after rebuild: ${missing.join(', ')}.${reasons} ` +
1038
+ const catalogDiagnostics = [];
1039
+ let missing = [];
1040
+ let catalogError;
1041
+ try {
1042
+ missing = await verifySearchFTSIndexes(executeQuery, ftsIndexes, (diagnostic) => {
1043
+ catalogDiagnostics.push(diagnostic.message);
1044
+ });
1045
+ }
1046
+ catch (error) {
1047
+ // An unreadable catalog is unknown, not proof of missing indexes.
1048
+ // Keep earlier native build errors when this read also fails.
1049
+ catalogError = error instanceof Error ? error.message : String(error);
1050
+ }
1051
+ if (repairFailures.length > 0 || missing.length > 0 || catalogError !== undefined) {
1052
+ // A failed DROP may leave a valid old catalog entry. Its presence must
1053
+ // not certify that this repair succeeded or clear the recovery marker.
1054
+ const reasons = [
1055
+ missing.length > 0 ? `missing indexes after rebuild: ${missing.join(', ')}` : '',
1056
+ repairFailures.length > 0
1057
+ ? summarizeFtsIndexBuildFailures(repairFailures, ftsIndexes)
1058
+ : missing.length > 0
1059
+ ? 'no build error was returned'
1060
+ : '',
1061
+ ...catalogDiagnostics,
1062
+ catalogError !== undefined ? `catalog verification failed: ${catalogError}` : '',
1063
+ ].filter(Boolean);
1064
+ throw new Error(`FTS repair failed - ${reasons.join('. ')}. ` +
1050
1065
  'Run `gitnexus analyze --force` to perform a full graph+FTS rebuild; ' +
1051
1066
  'if that also fails, verify FTS extension availability via `gitnexus doctor`.');
1052
1067
  }
@@ -159,7 +159,18 @@ export declare function createSearchFTSIndexes(options?: CreateSearchFTSIndexesO
159
159
  * {@link redactPaths} first, the same rule the query-side warnings follow.
160
160
  */
161
161
  export declare const summarizeFtsIndexBuildFailures: (failures: readonly FtsIndexBuildFailure[], indexes?: readonly FTSIndexDefinition[]) => string;
162
- export declare function verifySearchFTSIndexes(executeQuery: (cypher: string) => Promise<unknown[]>, indexes?: readonly FTSIndexDefinition[]): Promise<string[]>;
162
+ /** Bounded catalog evidence for CLI/log diagnostics, never source or index definitions. */
163
+ export interface FtsIndexVerificationDiagnostic {
164
+ table: string;
165
+ indexName: string;
166
+ message: string;
167
+ }
168
+ /**
169
+ * Return missing or invalid configured indexes. An optional callback receives
170
+ * bounded evidence from the same catalog read; a failed read still throws and
171
+ * proves neither presence nor absence.
172
+ */
173
+ export declare function verifySearchFTSIndexes(executeQuery: (cypher: string) => Promise<unknown[]>, indexes?: readonly FTSIndexDefinition[], onMismatch?: (diagnostic: FtsIndexVerificationDiagnostic) => void): Promise<string[]>;
163
174
  /**
164
175
  * Why an FTS build failed, so the caller can react correctly (#2658):
165
176
  *
@@ -1,4 +1,4 @@
1
- import { createFTSIndex, dropFTSIndex, indexRowName, indexRowTable, resolveGateRows, DEFAULT_FTS_STEMMER, } from '../lbug/lbug-adapter.js';
1
+ import { createFTSIndex, dropFTSIndex, indexRowName, indexRowTable, indexRowType, resolveGateRows, DEFAULT_FTS_STEMMER, } from '../lbug/lbug-adapter.js';
2
2
  import { getFtsCapability } from '../lbug/extension-loader.js';
3
3
  import { FTS_DISABLED_MESSAGE } from './fts-policy.js';
4
4
  import { classifyExtensionLoadError, usesClassifiedLoadRemedy, } from '../lbug/extension-load-error.js';
@@ -312,38 +312,65 @@ export async function createSearchFTSIndexes(options) {
312
312
  */
313
313
  export const summarizeFtsIndexBuildFailures = (failures, indexes = FTS_INDEXES) => `FTS index build failed for ${failures.length} of ${indexes.length} tables: ` +
314
314
  failures.map((f) => `${f.table}.${f.indexName} (${f.error})`).join(', ');
315
- export async function verifySearchFTSIndexes(executeQuery, indexes = FTS_INDEXES) {
315
+ const boundCatalogText = (text, limit) => text.length > limit ? `${text.slice(0, limit - 14)}… [truncated]` : text;
316
+ const catalogFieldEvidence = (value) => typeof value === 'string'
317
+ ? JSON.stringify(boundCatalogText(value, 80))
318
+ : value === undefined
319
+ ? '<missing>'
320
+ : `<invalid ${value === null ? 'null' : typeof value}>`;
321
+ const catalogPropertiesEvidence = (value) => {
322
+ if (!Array.isArray(value))
323
+ return `<invalid properties: ${catalogFieldEvidence(value)}>`;
324
+ return (`[${value.slice(0, 8).map(catalogFieldEvidence).join(', ')}]` +
325
+ (value.length > 8 ? ` (${value.length - 8} properties truncated)` : ''));
326
+ };
327
+ /**
328
+ * Return missing or invalid configured indexes. An optional callback receives
329
+ * bounded evidence from the same catalog read; a failed read still throws and
330
+ * proves neither presence nor absence.
331
+ */
332
+ export async function verifySearchFTSIndexes(executeQuery, indexes = FTS_INDEXES, onMismatch) {
316
333
  // Read the catalog once and check each configured index both EXISTS and
317
334
  // covers its expected columns. A queryability-only probe (CALL QUERY_FTS_INDEX
318
335
  // ... catch) is not enough: a stale `name+content`-only index left on a
319
336
  // pre-#2299 DB stays queryable yet silently misses `description`, so the probe
320
337
  // would pass while doc-comment search is still broken (#2299). SHOW_INDEXES
321
338
  // exposes `property_names` (STRING[]) per index, so we assert coverage directly.
322
- const rows = await executeQuery('CALL SHOW_INDEXES() RETURN *');
323
- const propsByIndex = new Map();
324
- for (const row of rows) {
325
- if (typeof row !== 'object' || row === null)
326
- continue;
327
- const record = row;
328
- const indexName = indexRowName(record);
329
- // LADYBUGDB-CONTRACT: `property_names` is the one SHOW_INDEXES column with a
330
- // single reader, so it has no shared accessor — see {@link IndexCatalogRow}
331
- // in lbug-adapter.ts for the full column list and the re-validation rule.
332
- // Unlike the gates, an unreadable shape here is safe: it reports the index as
333
- // not covering its columns, i.e. "missing", which degrades keyword search
334
- // loudly rather than passing a broken index off as verified.
335
- const propertyNames = record.property_names;
336
- if (typeof indexName !== 'string' || !Array.isArray(propertyNames))
337
- continue;
338
- propsByIndex.set(indexName, propertyNames.filter((p) => typeof p === 'string'));
339
- }
339
+ const rows = (await executeQuery('CALL SHOW_INDEXES() RETURN *')).filter((row) => typeof row === 'object' && row !== null);
340
340
  const missing = [];
341
341
  for (const { table, indexName, properties } of indexes) {
342
- const actual = propsByIndex.get(indexName);
343
- // Absent from the catalog, or present but not covering every expected column.
344
- if (!actual || !properties.every((p) => actual.includes(p))) {
345
- missing.push(`${table}.${indexName}`);
346
- }
342
+ const valid = rows.some((row) => {
343
+ // LADYBUGDB-CONTRACT: `property_names` is a named STRING[] column;
344
+ // identity/type use the shared catalog readers in lbug-adapter.ts.
345
+ const actual = row.property_names;
346
+ return (indexRowTable(row) === table &&
347
+ indexRowName(row) === indexName &&
348
+ indexRowType(row) === 'FTS' &&
349
+ Array.isArray(actual) &&
350
+ actual.every((property) => typeof property === 'string') &&
351
+ properties.every((property) => actual.includes(property)));
352
+ });
353
+ if (valid)
354
+ continue;
355
+ missing.push(`${table}.${indexName}`);
356
+ if (!onMismatch)
357
+ continue;
358
+ // Show only rows that could explain this identity mismatch, and only
359
+ // catalog identity/type/property fields. Malformed objects are represented
360
+ // by their type rather than serialized (they can carry arbitrary content).
361
+ const relevant = rows.filter((row) => indexRowTable(row) === table || indexRowName(row) === indexName);
362
+ const observed = relevant
363
+ .slice(0, 3)
364
+ .map((row) => `{table=${catalogFieldEvidence(indexRowTable(row))}, ` +
365
+ `index=${catalogFieldEvidence(indexRowName(row))}, ` +
366
+ `type=${catalogFieldEvidence(indexRowType(row))}, ` +
367
+ `properties=${catalogPropertiesEvidence(row.property_names)}}`);
368
+ const message = `${table}.${indexName}: expected table=${catalogFieldEvidence(table)}, ` +
369
+ `index=${catalogFieldEvidence(indexName)}, type=FTS, ` +
370
+ `properties=${catalogPropertiesEvidence(properties)}; observed ` +
371
+ (observed.length > 0 ? observed.join(', ') : 'no matching table or index name') +
372
+ (relevant.length > 3 ? ` (${relevant.length - 3} catalog rows truncated)` : '');
373
+ onMismatch({ table, indexName, message: boundCatalogText(message, 2048) });
347
374
  }
348
375
  return missing;
349
376
  }
@@ -418,28 +445,47 @@ export async function buildSearchIndexesOrDegrade(executeQuery, options) {
418
445
  // means description search is broken (#2299).
419
446
  const failures = await createSearchFTSIndexes(options);
420
447
  const indexes = options?.indexes ?? FTS_INDEXES;
421
- const missing = await verifySearchFTSIndexes(executeQuery, indexes);
422
- if (failures.length === 0 && missing.length === 0)
448
+ const diagnostics = [];
449
+ let missing = [];
450
+ let verificationError;
451
+ try {
452
+ missing = await verifySearchFTSIndexes(executeQuery, indexes, (diagnostic) => diagnostics.push(diagnostic));
453
+ }
454
+ catch (e) {
455
+ // A failed catalog read is unknown, not absence. Keep the native build
456
+ // failures already collected rather than replacing them with this error.
457
+ verificationError = e instanceof Error ? e.message : String(e);
458
+ }
459
+ if (failures.length === 0 && missing.length === 0 && verificationError === undefined) {
423
460
  return { ok: true };
424
- // A table that failed to build is necessarily missing too — report it once,
425
- // with its reason, and keep `missing` for indexes nothing explains.
461
+ }
462
+ // A failed DROP may leave an old catalog row present. Build failures stand
463
+ // on their own; avoid naming the same table again as an unexplained failure.
426
464
  const named = new Set(failures.map((f) => `${f.table}.${f.indexName}`));
427
- const unexplained = missing.filter((name) => !named.has(name));
465
+ const unexplained = diagnostics.filter((d) => !named.has(`${d.table}.${d.indexName}`));
428
466
  const error = [
429
467
  failures.length > 0 ? summarizeFtsIndexBuildFailures(failures, indexes) : '',
430
468
  // Structural incompleteness with no thrown error — classified capability
431
469
  // (degrade) below, matching prior behavior; a broken *write* surfaces as
432
470
  // a thrown IO/checkpoint error and is classified integrity there.
433
- unexplained.length > 0 ? `missing indexes after build: ${unexplained.join(', ')}` : '',
471
+ unexplained.length > 0
472
+ ? 'missing indexes or mismatched catalog rows after build ' +
473
+ '(no build error was returned for these indexes): ' +
474
+ unexplained.map((d) => d.message).join('; ')
475
+ : '',
476
+ verificationError !== undefined
477
+ ? `FTS catalog verification failed: ${verificationError}`
478
+ : '',
434
479
  ]
435
480
  .filter((part) => part.length > 0)
436
481
  .join('; ');
437
482
  // Classify per failure, not over the joined text: capability signatures are
438
483
  // checked first, so folding the messages together would let an untokenizable
439
484
  // row mask a genuinely broken write and downgrade an abort into a degrade.
440
- const failureClass = failures.some((f) => classifyFtsBuildError(f.error) === 'integrity')
485
+ const failureClass = failures.some((f) => classifyFtsBuildError(f.error) === 'integrity') ||
486
+ (verificationError !== undefined && classifyFtsBuildError(verificationError) === 'integrity')
441
487
  ? 'integrity'
442
- : classifyFtsBuildError(error);
488
+ : 'capability';
443
489
  return { ok: false, error, failureClass };
444
490
  }
445
491
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.80",
3
+ "version": "1.6.13-rc.81",
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",