gitnexus 1.6.6-rc.22 → 1.6.6-rc.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -151,7 +151,8 @@ Your AI agent gets these tools automatically:
151
151
  ```bash
152
152
  gitnexus setup # Configure MCP for your editors (one-time)
153
153
  gitnexus analyze [path] # Index a repository (or update stale index)
154
- gitnexus analyze --force # Force full re-index
154
+ gitnexus analyze --repair-fts # Fast path: rebuild/verify only FTS indexes on existing index data
155
+ gitnexus analyze --force # Full rebuild: re-parse + graph rebuild + FTS rebuild
155
156
  gitnexus analyze --embeddings # Enable embedding generation (slower, better search)
156
157
  gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
157
158
  gitnexus analyze --verbose # Log skipped files when parsers are unavailable
@@ -9,6 +9,7 @@
9
9
  */
10
10
  export interface AnalyzeOptions {
11
11
  force?: boolean;
12
+ repairFts?: boolean;
12
13
  /**
13
14
  * Embedding generation toggle. Commander parses `--embeddings [limit]` as:
14
15
  * - `undefined` when the flag is omitted
@@ -216,6 +216,12 @@ export const analyzeCommand = async (inputPath, options) => {
216
216
  }
217
217
  process.env.GITNEXUS_EMBEDDING_DEVICE = options.embeddingDevice;
218
218
  }
219
+ if (options?.repairFts && options?.force) {
220
+ cliError(' Cannot combine `--repair-fts` with `--force`. ' +
221
+ 'Use `--repair-fts` for fast FTS-only repair, or `--force` for a full rebuild.\n');
222
+ process.exitCode = 1;
223
+ return;
224
+ }
219
225
  console.log('\n GitNexus Analyzer\n');
220
226
  // `--index-only` is the stronger contract — it suppresses every form of file
221
227
  // injection, including community skill writes that `--skills` would normally
@@ -368,9 +374,11 @@ export const analyzeCommand = async (inputPath, options) => {
368
374
  // needs a fresh pipelineResult. Has no bearing on the registry
369
375
  // collision guard (see allowDuplicateName below).
370
376
  force: options?.force || options?.skills,
377
+ repairFts: options?.repairFts,
371
378
  embeddings: embeddingsEnabled,
372
379
  embeddingsNodeLimit,
373
380
  dropEmbeddings: options?.dropEmbeddings,
381
+ verbose: options?.verbose,
374
382
  skipGit: options?.skipGit,
375
383
  skipAgentsMd,
376
384
  skipSkills,
@@ -411,6 +419,18 @@ export const analyzeCommand = async (inputPath, options) => {
411
419
  // runFullAnalysis never opens LadybugDB, so no native handles prevent exit.
412
420
  return;
413
421
  }
422
+ if (result.ftsRepairedOnly) {
423
+ clearInterval(elapsedTimer);
424
+ process.removeListener('SIGINT', sigintHandler);
425
+ console.log = origLog;
426
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
427
+ console.warn = origWarn;
428
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
429
+ console.error = origError;
430
+ bar.stop();
431
+ console.log(' FTS indexes repaired successfully\n');
432
+ return;
433
+ }
414
434
  // Post-finalize invariant (#1169): runFullAnalysis nominally writes
415
435
  // meta.json and registers the repo, but on Windows it has been
416
436
  // observed to return successfully with neither artifact present
package/dist/cli/index.js CHANGED
@@ -17,6 +17,7 @@ program
17
17
  .command('analyze [path]')
18
18
  .description('Index a repository (full analysis)')
19
19
  .option('-f, --force', 'Force full re-index even if up to date')
20
+ .option('--repair-fts', 'Repair/rebuild search FTS indexes without full re-analysis')
20
21
  .option('--embeddings [limit]', 'Enable embedding generation for semantic search (off by default). ' +
21
22
  'Optional [limit] overrides the 50,000-node safety cap; pass 0 to disable the cap entirely.')
22
23
  .option('--drop-embeddings', 'Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` ' +
@@ -48,12 +48,30 @@ export const walkRepositoryPaths = async (repoPath, onProgress) => {
48
48
  }
49
49
  if (skippedLarge > 0) {
50
50
  const isDefault = maxFileSizeBytes === DEFAULT_MAX_FILE_SIZE_BYTES;
51
+ const isOverrideUnset = !process.env.GITNEXUS_MAX_FILE_SIZE;
51
52
  const suffix = isDefault ? ', likely generated/vendored' : '';
52
53
  logger.warn(` Skipped ${skippedLarge} large files (>${maxFileSizeBytes / 1024}KB${suffix})`);
53
- if (isVerboseIngestionEnabled()) {
54
- for (const p of skippedLargePaths) {
55
- logger.warn(` - ${p}`);
56
- }
54
+ // Always show at least the first few paths so users can diagnose why
55
+ // edges are missing from a specific file (issue #1659). The full list is
56
+ // gated behind GITNEXUS_VERBOSE=1 to avoid flooding output on repos with
57
+ // many generated/vendored blobs. Sort before slicing so the preview is
58
+ // stable across runs (fs.stat callbacks race within each batch).
59
+ skippedLargePaths.sort();
60
+ const SKIPPED_PREVIEW_CAP = 5;
61
+ const showAll = isVerboseIngestionEnabled() || skippedLargePaths.length <= SKIPPED_PREVIEW_CAP;
62
+ const preview = showAll ? skippedLargePaths : skippedLargePaths.slice(0, SKIPPED_PREVIEW_CAP);
63
+ for (const p of preview) {
64
+ logger.warn(` - ${p}`);
65
+ }
66
+ if (!showAll) {
67
+ const remaining = skippedLargePaths.length - SKIPPED_PREVIEW_CAP;
68
+ logger.warn(` ...and ${remaining} more (set GITNEXUS_VERBOSE=1 to list them all)`);
69
+ }
70
+ // Only hint about the env var when the user has not set it at all. An
71
+ // explicit GITNEXUS_MAX_FILE_SIZE=512 happens to resolve to the same
72
+ // bytes as the default but the operator clearly already knows the knob.
73
+ if (isDefault && isOverrideUnset) {
74
+ logger.warn(` Set GITNEXUS_MAX_FILE_SIZE=<KB> to include files above the default cap.`);
57
75
  }
58
76
  }
59
77
  return entries;
@@ -1454,7 +1454,8 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
1454
1454
  if (ensuredFTSIndexes.has(key))
1455
1455
  return;
1456
1456
  if (!(await loadFTSExtension())) {
1457
- return;
1457
+ throw new Error(`FTS extension unavailable - cannot create FTS index ${tableName}.${indexName}. ` +
1458
+ 'Run `gitnexus doctor` and ensure the LadybugDB FTS extension is installed and loadable on this machine.');
1458
1459
  }
1459
1460
  const propList = properties.map((p) => `'${p}'`).join(', ');
1460
1461
  const query = `CALL CREATE_FTS_INDEX('${tableName}', '${indexName}', [${propList}], stemmer := '${stemmer}')`;
@@ -20,6 +20,10 @@ export interface AnalyzeOptions {
20
20
  * bypass. See `allowDuplicateName` below.
21
21
  */
22
22
  force?: boolean;
23
+ /** Repair only search indexes without re-running full parsing/indexing. */
24
+ repairFts?: boolean;
25
+ /** Emit per-index FTS create logs. */
26
+ verbose?: boolean;
23
27
  embeddings?: boolean;
24
28
  /**
25
29
  * Override the auto-skip node-count cap for embedding generation.
@@ -74,6 +78,8 @@ export interface AnalyzeResult {
74
78
  alreadyUpToDate?: boolean;
75
79
  /** The raw pipeline result — only populated when needed by callers (e.g. skill generation). */
76
80
  pipelineResult?: any;
81
+ /** True when analyze only repaired FTS indexes and skipped pipeline re-analysis. */
82
+ ftsRepairedOnly?: boolean;
77
83
  }
78
84
  export { deriveEmbeddingMode, DEFAULT_EMBEDDING_NODE_LIMIT } from './embedding-mode.js';
79
85
  export type { EmbeddingMode } from './embedding-mode.js';
@@ -13,7 +13,7 @@ import fs from 'fs/promises';
13
13
  import { execFileSync } from 'child_process';
14
14
  import { runPipelineFromRepo } from './ingestion/pipeline.js';
15
15
  import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, loadCachedEmbeddings, deleteNodesForFile, deleteAllCommunitiesAndProcesses, queryImporters, } from './lbug/lbug-adapter.js';
16
- import { createSearchFTSIndexes } from './search/fts-indexes.js';
16
+ import { createSearchFTSIndexes, verifySearchFTSIndexes } from './search/fts-indexes.js';
17
17
  import { getStoragePaths, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, cleanupOldKuzuFiles, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
18
18
  import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
19
19
  import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
@@ -68,6 +68,70 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
68
68
  const repoHasGit = hasGitDir(repoPath);
69
69
  const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
70
70
  const existingMeta = await loadMeta(storagePath);
71
+ // ── FTS-only repair path ────────────────────────────────────────────
72
+ if (options.repairFts) {
73
+ if (!existingMeta) {
74
+ throw new Error('Cannot repair FTS indexes because this repository has not been analyzed yet. ' +
75
+ 'Run `gitnexus analyze` first to create the initial index, then retry `--repair-fts`.');
76
+ }
77
+ let lbugStat;
78
+ try {
79
+ lbugStat = await fs.lstat(lbugPath);
80
+ }
81
+ catch {
82
+ throw new Error(`Cannot repair FTS indexes: graph store at ${lbugPath} is missing. ` +
83
+ 'Run `gitnexus analyze` (full) to rebuild from scratch.');
84
+ }
85
+ if (!lbugStat.isFile()) {
86
+ const foundType = lbugStat.isDirectory()
87
+ ? 'a directory'
88
+ : lbugStat.isSymbolicLink()
89
+ ? 'a symbolic link'
90
+ : lbugStat.isSocket()
91
+ ? 'a socket'
92
+ : lbugStat.isBlockDevice()
93
+ ? 'a block device'
94
+ : lbugStat.isCharacterDevice()
95
+ ? 'a character device'
96
+ : lbugStat.isFIFO()
97
+ ? 'a FIFO'
98
+ : 'not a regular file';
99
+ throw new Error(`Cannot repair FTS indexes: graph store at ${lbugPath} is ${foundType} (expected a file). ` +
100
+ 'Run `gitnexus analyze` (full) to rebuild from scratch.');
101
+ }
102
+ try {
103
+ await initLbug(lbugPath);
104
+ progress('fts', 85, 'Repairing search indexes...');
105
+ await createSearchFTSIndexes({
106
+ onIndexStart: options.verbose
107
+ ? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
108
+ : undefined,
109
+ onIndexReady: options.verbose
110
+ ? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
111
+ : undefined,
112
+ });
113
+ const missing = await verifySearchFTSIndexes(executeQuery);
114
+ if (missing.length > 0) {
115
+ throw new Error(`FTS repair failed - missing indexes after rebuild: ${missing.join(', ')}. ` +
116
+ 'Run `gitnexus analyze --force` to perform a full graph+FTS rebuild; ' +
117
+ 'if that also fails, verify FTS extension availability via `gitnexus doctor`.');
118
+ }
119
+ await ensureGitNexusIgnored(repoPath);
120
+ progress('fts', 90, 'Search indexes ready');
121
+ progress('done', 100, 'Done');
122
+ return {
123
+ repoName: options.registryName ??
124
+ getInferredRepoName(repoPath) ??
125
+ path.basename(resolveRepoIdentityRoot(repoPath)),
126
+ repoPath,
127
+ stats: existingMeta.stats ?? {},
128
+ ftsRepairedOnly: true,
129
+ };
130
+ }
131
+ finally {
132
+ await closeLbug().catch(() => { });
133
+ }
134
+ }
71
135
  // ── Crash recovery: dirty flag forces full rebuild ────────────────
72
136
  // If the previous incremental run set incrementalInProgress and didn't
73
137
  // clear it, the on-disk index may be in a half-state. Cheapest path
@@ -420,7 +484,19 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
420
484
  }
421
485
  // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
422
486
  progress('fts', 85, 'Creating search indexes...');
423
- await createSearchFTSIndexes();
487
+ await createSearchFTSIndexes({
488
+ onIndexStart: options.verbose
489
+ ? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
490
+ : undefined,
491
+ onIndexReady: options.verbose
492
+ ? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
493
+ : undefined,
494
+ });
495
+ const missingIndexNames = await verifySearchFTSIndexes(executeQuery);
496
+ if (missingIndexNames.length > 0) {
497
+ throw new Error(`FTS verification failed - missing indexes after analyze: ${missingIndexNames.join(', ')}. ` +
498
+ 'Check FTS extension availability, then retry `gitnexus analyze --force` for a full rebuild.');
499
+ }
424
500
  progress('fts', 90, 'Search indexes ready');
425
501
  // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
426
502
  // Runs on BOTH the full-rebuild path and the incremental path:
@@ -1 +1,6 @@
1
- export declare function createSearchFTSIndexes(): Promise<void>;
1
+ export interface CreateSearchFTSIndexesOptions {
2
+ onIndexStart?: (table: string, indexName: string) => void;
3
+ onIndexReady?: (table: string, indexName: string) => void;
4
+ }
5
+ export declare function createSearchFTSIndexes(options?: CreateSearchFTSIndexesOptions): Promise<void>;
6
+ export declare function verifySearchFTSIndexes(executeQuery: (cypher: string) => Promise<unknown[]>): Promise<string[]>;
@@ -1,7 +1,34 @@
1
1
  import { createFTSIndex } from '../lbug/lbug-adapter.js';
2
2
  import { FTS_INDEXES } from './fts-schema.js';
3
- export async function createSearchFTSIndexes() {
3
+ export async function createSearchFTSIndexes(options) {
4
4
  for (const { table, indexName, properties } of FTS_INDEXES) {
5
+ options?.onIndexStart?.(table, indexName);
5
6
  await createFTSIndex(table, indexName, [...properties]);
7
+ options?.onIndexReady?.(table, indexName);
6
8
  }
7
9
  }
10
+ export async function verifySearchFTSIndexes(executeQuery) {
11
+ const safeIdentifier = (value) => {
12
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
13
+ throw new Error(`Invalid FTS identifier: ${value}`);
14
+ }
15
+ return value;
16
+ };
17
+ const missing = [];
18
+ for (const { table, indexName } of FTS_INDEXES) {
19
+ const safeTable = safeIdentifier(table);
20
+ const safeIndex = safeIdentifier(indexName);
21
+ const probe = `
22
+ CALL QUERY_FTS_INDEX('${safeTable}', '${safeIndex}', '__gitnexus_fts_probe__', conjunctive := false)
23
+ RETURN score
24
+ LIMIT 1
25
+ `;
26
+ try {
27
+ await executeQuery(probe);
28
+ }
29
+ catch {
30
+ missing.push(`${table}.${indexName}`);
31
+ }
32
+ }
33
+ return missing;
34
+ }
@@ -894,7 +894,7 @@ export class LocalBackend {
894
894
  definitions: definitions.slice(0, 20), // cap standalone definitions
895
895
  timing,
896
896
  ...(!ftsUsed && {
897
- warning: 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.',
897
+ warning: 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.',
898
898
  }),
899
899
  };
900
900
  }
@@ -1071,7 +1071,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1071
1071
  const response = { results: results.searchResults ?? results };
1072
1072
  if (results.ftsAvailable === false) {
1073
1073
  response.warning =
1074
- 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.';
1074
+ 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.';
1075
1075
  }
1076
1076
  res.json(response);
1077
1077
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.22",
3
+ "version": "1.6.6-rc.24",
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",