gitnexus 1.6.7 → 1.6.8-rc.10

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.
Files changed (82) hide show
  1. package/README.md +20 -0
  2. package/dist/cli/analyze-config.js +39 -0
  3. package/dist/cli/analyze.d.ts +16 -0
  4. package/dist/cli/analyze.js +34 -6
  5. package/dist/cli/clean.d.ts +1 -0
  6. package/dist/cli/clean.js +43 -1
  7. package/dist/cli/eval-server.js +43 -0
  8. package/dist/cli/help-i18n.js +6 -0
  9. package/dist/cli/i18n/en.d.ts +11 -0
  10. package/dist/cli/i18n/en.js +11 -0
  11. package/dist/cli/i18n/resources.d.ts +22 -0
  12. package/dist/cli/i18n/zh-CN.d.ts +11 -0
  13. package/dist/cli/i18n/zh-CN.js +11 -0
  14. package/dist/cli/index.js +9 -0
  15. package/dist/cli/list.js +12 -0
  16. package/dist/cli/optional-grammars.d.ts +6 -8
  17. package/dist/cli/optional-grammars.js +8 -11
  18. package/dist/cli/status.js +26 -5
  19. package/dist/cli/tool.d.ts +5 -0
  20. package/dist/cli/tool.js +5 -0
  21. package/dist/core/embeddings/embedder.js +4 -0
  22. package/dist/core/embeddings/embedding-pipeline.js +27 -16
  23. package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
  24. package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
  25. package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
  26. package/dist/core/group/extractors/http-patterns/java.js +3 -50
  27. package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
  28. package/dist/core/group/extractors/include-extractor.js +7 -7
  29. package/dist/core/ingestion/language-provider.d.ts +25 -2
  30. package/dist/core/ingestion/languages/c-cpp.js +11 -4
  31. package/dist/core/ingestion/languages/java.js +3 -0
  32. package/dist/core/ingestion/languages/kotlin/query.js +3 -2
  33. package/dist/core/ingestion/languages/kotlin.js +5 -1
  34. package/dist/core/ingestion/parsing-processor.js +21 -0
  35. package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
  36. package/dist/core/ingestion/pipeline.d.ts +9 -0
  37. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
  38. package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
  39. package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
  40. package/dist/core/ingestion/route-extractors/spring.js +136 -0
  41. package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
  42. package/dist/core/ingestion/workers/clone-safety.js +465 -0
  43. package/dist/core/ingestion/workers/parse-worker.d.ts +10 -0
  44. package/dist/core/ingestion/workers/parse-worker.js +27 -52
  45. package/dist/core/ingestion/workers/post-result.d.ts +22 -0
  46. package/dist/core/ingestion/workers/post-result.js +87 -0
  47. package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
  48. package/dist/core/ingestion/workers/result-merge.js +43 -0
  49. package/dist/core/ingestion/workers/worker-pool.d.ts +6 -0
  50. package/dist/core/ingestion/workers/worker-pool.js +28 -14
  51. package/dist/core/lbug/lbug-adapter.d.ts +19 -0
  52. package/dist/core/lbug/lbug-adapter.js +56 -1
  53. package/dist/core/run-analyze.d.ts +45 -0
  54. package/dist/core/run-analyze.js +175 -24
  55. package/dist/core/tree-sitter/parser-loader.js +5 -4
  56. package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
  57. package/dist/core/tree-sitter/vendored-grammars.js +57 -0
  58. package/dist/mcp/core/embedder.js +4 -0
  59. package/dist/mcp/local/local-backend.d.ts +55 -2
  60. package/dist/mcp/local/local-backend.js +363 -25
  61. package/dist/mcp/tools.js +32 -0
  62. package/dist/server/analyze-worker-ipc.d.ts +58 -0
  63. package/dist/server/analyze-worker-ipc.js +16 -0
  64. package/dist/server/analyze-worker.js +7 -1
  65. package/dist/storage/branch-index.d.ts +52 -0
  66. package/dist/storage/branch-index.js +65 -0
  67. package/dist/storage/git.d.ts +11 -0
  68. package/dist/storage/git.js +28 -0
  69. package/dist/storage/parse-cache.js +3 -0
  70. package/dist/storage/repo-manager.d.ts +57 -2
  71. package/dist/storage/repo-manager.js +132 -18
  72. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +24 -4
  73. package/hooks/claude/gitnexus-hook.cjs +18 -3
  74. package/package.json +2 -2
  75. package/scripts/assert-publish-grammar-coverage.cjs +31 -0
  76. package/scripts/build-tree-sitter-grammars.cjs +16 -10
  77. package/vendor/tree-sitter-c/package.json +1 -1
  78. package/vendor/tree-sitter-dart/package.json +1 -1
  79. package/vendor/tree-sitter-kotlin/package.json +1 -1
  80. package/vendor/tree-sitter-proto/package.json +1 -1
  81. package/vendor/tree-sitter-swift/package.json +1 -1
  82. package/scripts/materialize-vendor-grammars.cjs +0 -97
@@ -17,14 +17,15 @@ import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReuse
17
17
  import { createSearchFTSIndexes, verifySearchFTSIndexes } from './search/fts-indexes.js';
18
18
  import { resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
19
19
  import { startWalCheckpointDriver, } from './lbug/wal-checkpoint-driver.js';
20
- import { getStoragePaths, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, cleanupOldKuzuFiles, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
20
+ import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, cleanupOldKuzuFiles, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
21
21
  import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
22
22
  import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
23
23
  import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
24
24
  import { loadParseCache, saveParseCache, pruneCache, PARSE_CACHE_VERSION, } from '../storage/parse-cache.js';
25
25
  import { getDurableParsedFileDir, pruneAndSaveDurableParsedFileStore, } from '../storage/parsedfile-store.js';
26
- import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
26
+ import { getCurrentCommit, getCurrentBranch, getDefaultBranch, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
27
27
  import { generateAIContextFiles } from '../cli/ai-context.js';
28
+ import { sanitizeDetectedBranch } from '../cli/analyze-config.js';
28
29
  import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
29
30
  import { STALE_HASH_SENTINEL } from './lbug/schema.js';
30
31
  /**
@@ -70,6 +71,63 @@ export const PHASE_LABELS = {
70
71
  * the {@link AnalyzeCallbacks} interface — it never writes to stdout/stderr
71
72
  * directly and never calls `process.exit()`.
72
73
  */
74
+ /**
75
+ * Build the primary-inversion warning (#2106 R8), or `undefined` when there is
76
+ * nothing to warn about. Pure + exported for testing. Both inputs are trimmed
77
+ * (a diagnostic — a missed warning is low-harm; a false warning is the thing to
78
+ * avoid). `defaultBranch` is the repo's `origin/HEAD` branch (null when unset,
79
+ * e.g. fresh clones / CI), `flatOwner` is the branch that owns the flat slot.
80
+ */
81
+ export const primaryInversionWarning = (defaultBranch, flatOwner) => {
82
+ const norm = (s) => s?.trim() || undefined;
83
+ const d = norm(defaultBranch);
84
+ const o = norm(flatOwner);
85
+ if (!d || !o || d === o)
86
+ return undefined;
87
+ return (`Warning: the default branch "${d}" is not the primary index — "${o}" owns the flat slot. ` +
88
+ `Run \`gitnexus clean --branch ${o}\` then re-index on "${d}", or query it explicitly with \`--branch ${d}\`.`);
89
+ };
90
+ /**
91
+ * Collect the recorded parse-cache chunk keys across the flat + every branch
92
+ * meta under a flat `.gitnexus` storage, EXCLUDING `excludeDir` (the current
93
+ * run's own meta dir) so a single-branch repo collects nothing and its prune
94
+ * stays byte-identical to today (#2106 R6). `complete` is false when a sibling
95
+ * meta.json exists but fails to parse — callers then retain the whole shared
96
+ * cache rather than over-evict another branch's still-live shards. Exported for
97
+ * testing.
98
+ */
99
+ export const collectBranchCacheKeys = async (storagePath, excludeDir) => {
100
+ const keys = new Set();
101
+ let complete = true;
102
+ const metaDirs = [storagePath];
103
+ const branchesDir = path.join(storagePath, 'branches');
104
+ const slugs = await fs.readdir(branchesDir).catch(() => []);
105
+ for (const slug of slugs)
106
+ metaDirs.push(path.join(branchesDir, slug));
107
+ for (const dir of metaDirs) {
108
+ if (excludeDir && path.resolve(dir) === path.resolve(excludeDir))
109
+ continue;
110
+ let raw;
111
+ try {
112
+ raw = await fs.readFile(path.join(dir, 'meta.json'), 'utf-8');
113
+ }
114
+ catch {
115
+ continue; // no meta here — not a branch index, not a failure
116
+ }
117
+ try {
118
+ const parsed = JSON.parse(raw);
119
+ if (Array.isArray(parsed.cacheKeys)) {
120
+ for (const k of parsed.cacheKeys)
121
+ if (typeof k === 'string')
122
+ keys.add(k);
123
+ }
124
+ }
125
+ catch {
126
+ complete = false; // present but corrupt → fail-safe toward retention
127
+ }
128
+ }
129
+ return { keys, complete };
130
+ };
73
131
  export async function runFullAnalysis(repoPath, options, callbacks) {
74
132
  const log = (msg) => callbacks.onLog?.(msg);
75
133
  const progress = (phase, percent, message) => callbacks.onProgress(phase, percent, message);
@@ -80,7 +138,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
80
138
  // counter in its own module instance and is process-scoped, so no separate
81
139
  // worker-side reset is needed (see safe-parse.ts ParseTimeoutError contract).
82
140
  resetDegradedParseCounter();
83
- const { storagePath, lbugPath } = getStoragePaths(repoPath);
141
+ // `storagePath` is ALWAYS the flat `.gitnexus` — content-addressed caches
142
+ // (parse-cache, parsedfile-store) and the kuzu-migration cleanup live there
143
+ // and are shared across branches (#2106 KTD7).
144
+ const { storagePath } = getStoragePaths(repoPath);
84
145
  // Clean up stale KuzuDB files from before the LadybugDB migration.
85
146
  const kuzuResult = await cleanupOldKuzuFiles(storagePath);
86
147
  if (kuzuResult.found && kuzuResult.needsReindex) {
@@ -88,7 +149,53 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
88
149
  }
89
150
  const repoHasGit = hasGitDir(repoPath);
90
151
  const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
91
- const existingMeta = await loadMeta(storagePath);
152
+ // ── #2106: resolve which branch slot this run writes to ───────────────
153
+ // `branchLabel` is the branch identity recorded in meta.json (incl. the
154
+ // primary). `placement.branch` is undefined for the flat/primary slot (the
155
+ // lbug/meta paths stay byte-identical to single-branch behavior) and set for
156
+ // a `branches/<slug>/` sub-directory. Explicit `--branch` is always honored;
157
+ // otherwise auto-detect the checked-out branch (null for detached HEAD /
158
+ // non-git → flat slot).
159
+ // Normalize the auto-detected branch the same way an explicit `--branch` is
160
+ // validated (#2106 R1): a git ref the branch-name rules forbid (backtick,
161
+ // `~ ^ : ? *`, leading `-`, `..`) becomes `null` → the flat slot, matching
162
+ // that a later `--branch <that-ref>` query would also be rejected. A normal
163
+ // ref passes through unchanged so index-time and query-time labels round-trip.
164
+ const checkedOutBranch = repoHasGit
165
+ ? (sanitizeDetectedBranch(getCurrentBranch(repoPath)) ?? null)
166
+ : null;
167
+ // Analyze indexes the working tree, not an arbitrary ref. An explicit
168
+ // `--branch X` while a DIFFERENT branch Y is checked out would write Y's
169
+ // content (and Y's commit) into X's index slot, corrupting X (#2106). Refuse
170
+ // the mismatch. Detached HEAD / non-git (checkedOutBranch === null) still
171
+ // allow an explicit label so CI checkouts can name their snapshot.
172
+ if (options.branch && checkedOutBranch && options.branch !== checkedOutBranch) {
173
+ throw new Error(`--branch "${options.branch}" does not match the checked-out branch "${checkedOutBranch}". ` +
174
+ `Check out "${options.branch}" before indexing it, or omit --branch to index the current branch.`);
175
+ }
176
+ const branchLabel = options.branch ?? checkedOutBranch;
177
+ const placement = await resolveBranchPlacement(repoPath, branchLabel);
178
+ const { lbugPath, metaPath } = getStoragePaths(repoPath, placement.branch);
179
+ // Directory that owns this run's meta.json (flat `.gitnexus` for the primary
180
+ // slot, `branches/<slug>/` otherwise). loadMeta/saveMeta operate on it so
181
+ // each branch keeps its own lastCommit / fileHashes / incremental dirty flag.
182
+ const metaDir = path.dirname(metaPath);
183
+ const existingMeta = await loadMeta(metaDir);
184
+ // ── #2106 (R8): warn when the repo's default branch is not the primary ──
185
+ // A non-default branch can own the flat slot (it was indexed first). That
186
+ // index is still fully queryable via `--branch`, so this is an ergonomics
187
+ // wart, not data loss — we only warn (no risky relocation of a live DB).
188
+ if (repoHasGit) {
189
+ // Who owns the flat slot after this run? For a flat/primary run it is this
190
+ // run's resolved label (carrying an existing stamp forward); for a branch
191
+ // run the flat owner is unchanged, so read the flat meta.
192
+ const flatOwner = placement.branch
193
+ ? (await loadMeta(storagePath))?.branch
194
+ : (branchLabel ?? existingMeta?.branch);
195
+ const warning = primaryInversionWarning(getDefaultBranch(repoPath), flatOwner);
196
+ if (warning)
197
+ log(warning);
198
+ }
92
199
  // ── FTS-only repair path ────────────────────────────────────────────
93
200
  if (options.repairFts) {
94
201
  if (!existingMeta) {
@@ -220,6 +327,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
220
327
  repoPath,
221
328
  stats: existingMeta.stats ?? {},
222
329
  alreadyUpToDate: true,
330
+ isPrimaryBranch: !placement.branch,
223
331
  };
224
332
  }
225
333
  }
@@ -308,6 +416,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
308
416
  }, {
309
417
  parseCache,
310
418
  workerPoolSize: options.workerPoolSize,
419
+ fetchWrappers: options.fetchWrappers,
311
420
  });
312
421
  // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
313
422
  progress('lbug', 60, 'Loading into LadybugDB...');
@@ -345,8 +454,8 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
345
454
  `deleted=${hashDiff.deleted.length} ` +
346
455
  `(skipping wipe + ${allFilePaths.length - hashDiff.toWrite.length} unchanged file rows preserved)`);
347
456
  // Set the dirty flag BEFORE any destructive DB mutation. Cleared on
348
- // success at the meta-save step.
349
- await saveMeta(storagePath, {
457
+ // success at the meta-save step. Scoped to this branch's meta.json.
458
+ await saveMeta(metaDir, {
350
459
  ...existingMeta,
351
460
  incrementalInProgress: {
352
461
  startedAt: Date.now(),
@@ -685,6 +794,14 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
685
794
  repoPath,
686
795
  lastCommit: currentCommit,
687
796
  indexedAt: new Date().toISOString(),
797
+ // Branch identity this index represents (#2106). Recorded for the flat
798
+ // slot too (so resolveBranchPlacement knows which branch owns it). When
799
+ // the label is null (detached HEAD / non-git re-analyze) we PRESERVE an
800
+ // existing stamp rather than stripping it — otherwise a detached re-index
801
+ // of the primary (e.g. CI's `actions/checkout` default) would un-claim the
802
+ // flat slot and let the next branch analyze overwrite the primary index.
803
+ // Stays absent only when never stamped (fresh detached/non-git repo).
804
+ branch: branchLabel ?? existingMeta?.branch,
688
805
  // Captured here (not at registration) so it travels with the
689
806
  // on-disk meta.json — sibling-clone fingerprinting works for
690
807
  // out-of-tree consumers (group-status, future tooling) without
@@ -723,9 +840,14 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
723
840
  // dirty flag (full and incremental success paths converge here).
724
841
  schemaVersion: hasGitDir(repoPath) ? INCREMENTAL_SCHEMA_VERSION : undefined,
725
842
  fileHashes: hasGitDir(repoPath) ? newFileHashesRecord : undefined,
843
+ // This branch's full live chunk-key set (#2106 R6). `usedKeys` is every
844
+ // chunk hash touched in this scan — cache HITS included (see parse-impl
845
+ // usedKeys.add) — so it's complete even on an incremental run. Persisted
846
+ // so a sibling branch's prune can union it and not evict our shards.
847
+ cacheKeys: [...parseCache.usedKeys],
726
848
  incrementalInProgress: undefined,
727
849
  };
728
- await saveMeta(storagePath, meta);
850
+ await saveMeta(metaDir, meta);
729
851
  // Persist the incremental parse cache for the next run. Wraps in
730
852
  // try/catch so a cache-write failure never breaks an otherwise
731
853
  // successful indexing run. Prune stale chunk-hash entries first so
@@ -734,6 +856,25 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
734
856
  // dead weight; the parse phase populates `usedKeys` as it processes
735
857
  // chunks).
736
858
  try {
859
+ // #2106 R6: the parse cache + durable store are shared across branches.
860
+ // Before pruning to this run's keys, fold in the OTHER branches' recorded
861
+ // chunk keys so a branch switch doesn't evict their still-live shards.
862
+ // Adding to usedKeys makes them survive pruneCache AND land in the saved
863
+ // index (saveParseCache builds the index from usedKeys). Excludes this
864
+ // run's own meta dir, so a single-branch repo folds in nothing → prune
865
+ // set byte-identical to today.
866
+ const { keys: siblingKeys, complete } = await collectBranchCacheKeys(storagePath, metaDir);
867
+ if (complete) {
868
+ for (const k of siblingKeys)
869
+ parseCache.usedKeys.add(k);
870
+ }
871
+ else {
872
+ // Fail-safe toward retention: a sibling meta was unreadable, so keep
873
+ // everything currently loaded rather than evict on incomplete info.
874
+ log('Parse cache: a branch meta was unreadable — retaining all cached chunks (#2106).');
875
+ for (const k of parseCache.entries.keys())
876
+ parseCache.usedKeys.add(k);
877
+ }
737
878
  const pruned = pruneCache(parseCache, parseCache.usedKeys);
738
879
  if (pruned > 0) {
739
880
  log(`Parse cache: pruned ${pruned} stale chunk entries`);
@@ -763,6 +904,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
763
904
  const projectName = await registerRepo(repoPath, meta, {
764
905
  name: options.registryName,
765
906
  allowDuplicateName: options.allowDuplicateName,
907
+ // Non-primary branch runs upsert into the entry's branches[]; the
908
+ // primary/flat run (placement.branch === undefined) refreshes the
909
+ // top-level fields (#2106).
910
+ branch: placement.branch,
766
911
  });
767
912
  // Keep generated .gitnexus contents ignored without editing the user's root .gitignore.
768
913
  await ensureGitNexusIgnored(repoPath);
@@ -776,23 +921,28 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
776
921
  }
777
922
  aggregatedClusterCount = Array.from(groups.values()).filter((count) => count >= 5).length;
778
923
  }
779
- try {
780
- await generateAIContextFiles(repoPath, storagePath, projectName, {
781
- files: pipelineResult.totalFileCount,
782
- nodes: stats.nodes,
783
- edges: stats.edges,
784
- communities: pipelineResult.communityResult?.stats.totalCommunities,
785
- clusters: aggregatedClusterCount,
786
- processes: pipelineResult.processResult?.stats.totalProcesses,
787
- }, undefined, {
788
- skipAgentsMd: options.skipAgentsMd,
789
- skipSkills: options.skipSkills,
790
- noStats: options.noStats,
791
- defaultBranch: options.defaultBranch,
792
- });
793
- }
794
- catch {
795
- // Best-effort — don't fail the entire analysis for context file issues
924
+ // Only (re)generate the repo-root AI context files (AGENTS.md / CLAUDE.md /
925
+ // skills) for the primary/flat index (#2106). A non-primary branch analyze
926
+ // must not churn the repo's committed AGENTS.md with branch-specific stats.
927
+ if (!placement.branch) {
928
+ try {
929
+ await generateAIContextFiles(repoPath, storagePath, projectName, {
930
+ files: pipelineResult.totalFileCount,
931
+ nodes: stats.nodes,
932
+ edges: stats.edges,
933
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
934
+ clusters: aggregatedClusterCount,
935
+ processes: pipelineResult.processResult?.stats.totalProcesses,
936
+ }, undefined, {
937
+ skipAgentsMd: options.skipAgentsMd,
938
+ skipSkills: options.skipSkills,
939
+ noStats: options.noStats,
940
+ defaultBranch: options.defaultBranch,
941
+ });
942
+ }
943
+ catch {
944
+ // Best-effort — don't fail the entire analysis for context file issues
945
+ }
796
946
  }
797
947
  // ── Close LadybugDB ──────────────────────────────────────────────
798
948
  // Stop the manual checkpoint driver before closeLbug so its
@@ -806,6 +956,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
806
956
  stats: meta.stats,
807
957
  pipelineResult,
808
958
  ftsSkipped: !ftsAvailable,
959
+ isPrimaryBranch: !placement.branch,
809
960
  };
810
961
  }
811
962
  catch (err) {
@@ -2,6 +2,7 @@ import Parser from 'tree-sitter';
2
2
  import { createRequire } from 'node:module';
3
3
  import { SupportedLanguages } from '../../_shared/index.js';
4
4
  import { logger } from '../logger.js';
5
+ import { requireVendoredGrammar } from './vendored-grammars.js';
5
6
  const _require = createRequire(import.meta.url);
6
7
  const ISSUES_URL = 'https://github.com/abhigyanpatwari/GitNexus/issues';
7
8
  const SOURCES = {
@@ -71,7 +72,7 @@ const SOURCES = {
71
72
  // user-opt-out grammar like Swift/Dart/Kotlin: a failure here is always an
72
73
  // install/platform problem the user needs to see.
73
74
  [SupportedLanguages.C]: {
74
- load: () => _require('tree-sitter-c'),
75
+ load: () => requireVendoredGrammar('tree-sitter-c'),
75
76
  optional: true,
76
77
  severity: 'error',
77
78
  unavailableNote: 'C parsing disabled: vendored `tree-sitter-c` (under ' +
@@ -85,7 +86,7 @@ const SOURCES = {
85
86
  // optionalDependencies — may be absent on platforms without prebuilds
86
87
  // or when users skip optional installs.
87
88
  [SupportedLanguages.Swift]: {
88
- load: () => _require('tree-sitter-swift'),
89
+ load: () => requireVendoredGrammar('tree-sitter-swift'),
89
90
  optional: true,
90
91
  userSkippable: true,
91
92
  unavailableNote: 'Swift parsing disabled: vendored `tree-sitter-swift` (under ' +
@@ -94,7 +95,7 @@ const SOURCES = {
94
95
  `See ${ISSUES_URL}/1130.`,
95
96
  },
96
97
  [SupportedLanguages.Dart]: {
97
- load: () => _require('tree-sitter-dart'),
98
+ load: () => requireVendoredGrammar('tree-sitter-dart'),
98
99
  optional: true,
99
100
  userSkippable: true,
100
101
  unavailableNote: 'Dart parsing disabled: vendored `tree-sitter-dart` (under ' +
@@ -103,7 +104,7 @@ const SOURCES = {
103
104
  `See ${ISSUES_URL}/1125.`,
104
105
  },
105
106
  [SupportedLanguages.Kotlin]: {
106
- load: () => _require('tree-sitter-kotlin'),
107
+ load: () => requireVendoredGrammar('tree-sitter-kotlin'),
107
108
  optional: true,
108
109
  userSkippable: true,
109
110
  unavailableNote: 'Kotlin parsing disabled: vendored `tree-sitter-kotlin` (under ' +
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Absolute path to the vendored grammar tree (`<pkg>/vendor`).
3
+ *
4
+ * This module compiles to `<pkg>/dist/core/tree-sitter/vendored-grammars.js`
5
+ * and runs from `<pkg>/src/core/tree-sitter/...` under tsx in dev — both sit
6
+ * three directories below the package root, and the build (`tsc`) never bundles,
7
+ * so `import.meta.url` resolves the same way in both. `vendor/` ships in the
8
+ * published package via package.json `files`.
9
+ */
10
+ export declare const VENDOR_ROOT: string;
11
+ /**
12
+ * The tree-sitter grammars GitNexus vendors inside its own package (NOT npm
13
+ * dependencies). Kept in one place so consumers (runtime loaders, the CLI
14
+ * availability probe, and the test grammar-introspection helper) agree on which
15
+ * grammars resolve from `vendor/` rather than `node_modules`.
16
+ */
17
+ export declare const VENDORED_GRAMMAR_PACKAGES: ReadonlySet<string>;
18
+ /** Absolute directory of a vendored grammar package under `vendor/`. */
19
+ export declare const vendoredGrammarDir: (packageName: string) => string;
20
+ /**
21
+ * Load a vendored tree-sitter grammar by its absolute path under `vendor/`.
22
+ *
23
+ * GitNexus vendors five grammars (c/dart/proto/swift/kotlin) inside its own
24
+ * package under `vendor/`, each shipping committed per-platform prebuilds. They
25
+ * are deliberately NOT npm dependencies and must NEVER be copied into
26
+ * `node_modules`: an undeclared package under `node_modules` is "extraneous" to
27
+ * every subsequent `npm`/`npx` arborist reify, which prunes or relocates it.
28
+ * That is the root cause of #2111 / #1728 — on Windows the relocation throws
29
+ * `EPERM: operation not permitted, symlink` (errno -4048) during the npx-cache
30
+ * reify Antigravity triggers when it launches the MCP server, and on every OS
31
+ * the second run silently deletes the materialized grammars.
32
+ *
33
+ * Resolving the grammar by absolute path runs its own `bindings/node` entry,
34
+ * which calls `node-gyp-build(<grammarDir>)` and loads
35
+ * `vendor/<name>/prebuilds/<platform>-<arch>/…` directly — no build, no write,
36
+ * no `node_modules` copy. (`node-gyp-build` itself IS an npm dependency and
37
+ * resolves normally from the grammar directory.)
38
+ */
39
+ export declare const requireVendoredGrammar: (packageName: string) => unknown;
@@ -0,0 +1,57 @@
1
+ import { createRequire } from 'node:module';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+ const _require = createRequire(import.meta.url);
5
+ /**
6
+ * Absolute path to the vendored grammar tree (`<pkg>/vendor`).
7
+ *
8
+ * This module compiles to `<pkg>/dist/core/tree-sitter/vendored-grammars.js`
9
+ * and runs from `<pkg>/src/core/tree-sitter/...` under tsx in dev — both sit
10
+ * three directories below the package root, and the build (`tsc`) never bundles,
11
+ * so `import.meta.url` resolves the same way in both. `vendor/` ships in the
12
+ * published package via package.json `files`.
13
+ */
14
+ export const VENDOR_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'vendor');
15
+ /**
16
+ * The tree-sitter grammars GitNexus vendors inside its own package (NOT npm
17
+ * dependencies). Kept in one place so consumers (runtime loaders, the CLI
18
+ * availability probe, and the test grammar-introspection helper) agree on which
19
+ * grammars resolve from `vendor/` rather than `node_modules`.
20
+ */
21
+ export const VENDORED_GRAMMAR_PACKAGES = new Set([
22
+ 'tree-sitter-c',
23
+ 'tree-sitter-dart',
24
+ 'tree-sitter-proto',
25
+ 'tree-sitter-swift',
26
+ 'tree-sitter-kotlin',
27
+ ]);
28
+ /** Absolute directory of a vendored grammar package under `vendor/`. */
29
+ export const vendoredGrammarDir = (packageName) => path.join(VENDOR_ROOT, packageName);
30
+ /**
31
+ * Load a vendored tree-sitter grammar by its absolute path under `vendor/`.
32
+ *
33
+ * GitNexus vendors five grammars (c/dart/proto/swift/kotlin) inside its own
34
+ * package under `vendor/`, each shipping committed per-platform prebuilds. They
35
+ * are deliberately NOT npm dependencies and must NEVER be copied into
36
+ * `node_modules`: an undeclared package under `node_modules` is "extraneous" to
37
+ * every subsequent `npm`/`npx` arborist reify, which prunes or relocates it.
38
+ * That is the root cause of #2111 / #1728 — on Windows the relocation throws
39
+ * `EPERM: operation not permitted, symlink` (errno -4048) during the npx-cache
40
+ * reify Antigravity triggers when it launches the MCP server, and on every OS
41
+ * the second run silently deletes the materialized grammars.
42
+ *
43
+ * Resolving the grammar by absolute path runs its own `bindings/node` entry,
44
+ * which calls `node-gyp-build(<grammarDir>)` and loads
45
+ * `vendor/<name>/prebuilds/<platform>-<arch>/…` directly — no build, no write,
46
+ * no `node_modules` copy. (`node-gyp-build` itself IS an npm dependency and
47
+ * resolves normally from the grammar directory.)
48
+ */
49
+ export const requireVendoredGrammar = (packageName) => {
50
+ // Fail loudly on a name that isn't actually vendored — a typo or a list that
51
+ // drifted out of sync (VENDORED_GRAMMAR_PACKAGES vs the CLI probe vs the build
52
+ // registry) would otherwise surface as a confusing absolute-path require miss.
53
+ if (!VENDORED_GRAMMAR_PACKAGES.has(packageName)) {
54
+ throw new Error(`'${packageName}' is not a vendored grammar (expected one of: ${[...VENDORED_GRAMMAR_PACKAGES].join(', ')}).`);
55
+ }
56
+ return _require(vendoredGrammarDir(packageName));
57
+ };
@@ -8,6 +8,7 @@ import { isHttpMode, getHttpDimensions, httpEmbedQuery, } from '../../core/embed
8
8
  import { resolveEmbeddingConfig } from '../../core/embeddings/config.js';
9
9
  import { applyHfEnvOverrides, isHfDownloadFailure, withHfDownloadRetry, } from '../../core/embeddings/hf-env.js';
10
10
  import { getLocalEmbeddingRuntimeBlocker } from '../../core/embeddings/runtime-support.js';
11
+ import { ensureOnnxRuntimeCommonResolvable } from '../../core/embeddings/onnxruntime-common-resolver.js';
11
12
  import { silenceStdout, restoreStdout, realStderrWrite } from '../../core/lbug/pool-adapter.js';
12
13
  import { logger } from '../../core/logger.js';
13
14
  // Model config
@@ -43,6 +44,9 @@ export const initEmbedder = async () => {
43
44
  try {
44
45
  // Lazy-load transformers.js only after the runtime guard has passed, so
45
46
  // unsupported platforms never reach the native ONNX import (#1515).
47
+ // Under pnpm-strict / `pnpm dlx`, transformers' phantom `onnxruntime-common`
48
+ // import is unresolvable; register the fallback resolver first (#307).
49
+ ensureOnnxRuntimeCommonResolvable();
46
50
  const { pipeline, env } = await import('@huggingface/transformers');
47
51
  env.allowLocalModels = false;
48
52
  // Bridge user-controlled env vars to transformers.js: HF_HOME →
@@ -5,7 +5,7 @@
5
5
  * Supports multiple indexed repositories via a global registry.
6
6
  * LadybugDB connections are opened lazily per repo on first query.
7
7
  */
8
- import { type RegistryEntry } from '../../storage/repo-manager.js';
8
+ import { type RegistryEntry, type BranchSummary } from '../../storage/repo-manager.js';
9
9
  import { GroupService } from '../../core/group/service.js';
10
10
  /**
11
11
  * Quick test-file detection for filtering impact results.
@@ -16,6 +16,15 @@ export declare function isTestFilePath(filePath: string): boolean;
16
16
  export declare const VALID_NODE_LABELS: Set<string>;
17
17
  /** Valid relation types for impact analysis filtering */
18
18
  export declare const VALID_RELATION_TYPES: Set<string>;
19
+ /**
20
+ * Relation types the #1858 epistemic-boundary probe keys on. Kept as
21
+ * module-level `readonly` arrays (not Sets) because computeEpistemicBoundary
22
+ * binds them as Cypher query params (`r.type IN $heritage` / `IN $types`).
23
+ * The heritage set is exactly the IMPACT_RELATION_CONFIDENCE 0.85 tier —
24
+ * "statically verifiable, but the concrete binding past it is not".
25
+ */
26
+ export declare const EPISTEMIC_HERITAGE_RELATION_TYPES: readonly string[];
27
+ export declare const EPISTEMIC_CONSUMER_RELATION_TYPES: readonly string[];
19
28
  /**
20
29
  * Per-relation-type confidence floor for impact analysis.
21
30
  *
@@ -56,6 +65,10 @@ interface RepoHandle {
56
65
  lastCommit: string;
57
66
  remoteUrl?: string;
58
67
  stats?: RegistryEntry['stats'];
68
+ /** Primary/flat branch name, when known (#2106). */
69
+ branch?: string;
70
+ /** Non-primary branch indexes available for this repo (#2106). */
71
+ branches?: BranchSummary[];
59
72
  }
60
73
  /**
61
74
  * Resolve the git diff cwd for detect_changes, auto-detecting linked worktrees.
@@ -107,6 +120,10 @@ export interface RepoListing {
107
120
  path: string;
108
121
  lastCommit: string;
109
122
  }>;
123
+ /** Primary/flat branch name, when known (#2106). */
124
+ branch?: string;
125
+ /** Non-primary branch indexes available for this repo (#2106). */
126
+ branches?: Array<Omit<BranchSummary, 'stats'>>;
110
127
  }
111
128
  /** Continuation metadata for the paginated `list_repos` MCP tool (#2119). */
112
129
  export interface ListReposPagination {
@@ -153,6 +170,7 @@ export declare class LocalBackend {
153
170
  private initializedRepos;
154
171
  private reinitPromises;
155
172
  private lastStalenessCheck;
173
+ private lastObservedIndexedAt;
156
174
  private groupToolSvc;
157
175
  /**
158
176
  * One-shot stderr warnings for sibling-clone drift, keyed by
@@ -214,7 +232,20 @@ export declare class LocalBackend {
214
232
  * On a miss, re-reads the registry once in case a new repo was indexed
215
233
  * while the MCP server was running.
216
234
  */
217
- resolveRepo(repoParam?: string): Promise<RepoHandle>;
235
+ resolveRepo(repoParam?: string, branch?: string): Promise<RepoHandle>;
236
+ /**
237
+ * Re-point a resolved repo handle at a specific branch index (#2106).
238
+ *
239
+ * - No `branch` (default) → the primary/flat handle, unchanged (backward
240
+ * compatible: every existing caller passes no branch).
241
+ * - `branch` equal to the known primary → the flat handle.
242
+ * - `branch` matching an indexed non-primary branch → a handle whose
243
+ * `lbugPath` points at `branches/<slug>/lbug`; the connection pool keys by
244
+ * `lbugPath`, so this is the only change needed to scope every tool.
245
+ * - `branch` that was never indexed → a clear error (never a silently-empty
246
+ * result against the wrong DB).
247
+ */
248
+ private applyBranchScope;
218
249
  /**
219
250
  * Try to resolve a repo from the in-memory cache. Returns null on miss.
220
251
  * Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches
@@ -409,6 +440,28 @@ export declare class LocalBackend {
409
440
  private rename;
410
441
  private impact;
411
442
  private _impactImpl;
443
+ /**
444
+ * #1858 — epistemic lower-bound detection.
445
+ *
446
+ * impact()/context() traverse only edges materialized in the graph. When the
447
+ * queried symbol sits on an interface / abstract boundary, callers that bind
448
+ * to the interface via DI, a container, or dynamic dispatch — rather than
449
+ * naming the concrete symbol — are not traced. The reported count is then a
450
+ * lower bound, not an exact figure. Instead of returning a confident count
451
+ * that silently omits those callers, annotate the result with
452
+ * `epistemic: 'lower-bound'` plus a human-readable boundary note. A fully
453
+ * resolved leaf with no indirection stays `epistemic: 'exact'`.
454
+ *
455
+ * Aligns with the numeric confidence model rather than the long-deleted
456
+ * TIER_CONFIDENCE enum: the heritage/indirection edges this keys on
457
+ * (IMPLEMENTS / METHOD_IMPLEMENTS / EXTENDS) carry the 0.85
458
+ * `IMPACT_RELATION_CONFIDENCE` floor — "statically verifiable, but the
459
+ * concrete binding past it is not".
460
+ *
461
+ * Never throws: on query error it returns 'exact', so it can only add signal,
462
+ * never suppress a result.
463
+ */
464
+ private computeEpistemicBoundary;
412
465
  /**
413
466
  * Shared BFS traversal for impact analysis (name-resolved or UID-resolved symbol).
414
467
  */