gitnexus 1.6.8-rc.2 → 1.6.8-rc.21

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 (193) hide show
  1. package/README.md +20 -0
  2. package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
  3. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  4. package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
  5. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  6. package/dist/cli/ai-context.js +1 -0
  7. package/dist/cli/analyze-config.js +40 -0
  8. package/dist/cli/analyze.d.ts +22 -0
  9. package/dist/cli/analyze.js +36 -6
  10. package/dist/cli/clean.d.ts +1 -0
  11. package/dist/cli/clean.js +43 -1
  12. package/dist/cli/eval-server.js +43 -0
  13. package/dist/cli/help-i18n.js +11 -0
  14. package/dist/cli/i18n/en.d.ts +13 -0
  15. package/dist/cli/i18n/en.js +13 -0
  16. package/dist/cli/i18n/resources.d.ts +26 -0
  17. package/dist/cli/i18n/zh-CN.d.ts +13 -0
  18. package/dist/cli/i18n/zh-CN.js +13 -0
  19. package/dist/cli/index.js +19 -0
  20. package/dist/cli/list.js +12 -0
  21. package/dist/cli/optional-grammars.d.ts +6 -8
  22. package/dist/cli/optional-grammars.js +8 -11
  23. package/dist/cli/skill-gen.js +1 -0
  24. package/dist/cli/status.js +26 -5
  25. package/dist/cli/tool.d.ts +12 -1
  26. package/dist/cli/tool.js +41 -1
  27. package/dist/core/embeddings/embedder.js +4 -0
  28. package/dist/core/embeddings/embedding-pipeline.js +27 -16
  29. package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
  30. package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
  31. package/dist/core/graph/import-cycles.d.ts +10 -0
  32. package/dist/core/graph/import-cycles.js +103 -0
  33. package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
  34. package/dist/core/group/extractors/http-patterns/java.js +3 -50
  35. package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
  36. package/dist/core/group/extractors/include-extractor.js +7 -7
  37. package/dist/core/ingestion/cfg/cfg-builder.d.ts +64 -0
  38. package/dist/core/ingestion/cfg/cfg-builder.js +129 -0
  39. package/dist/core/ingestion/cfg/collect.d.ts +30 -0
  40. package/dist/core/ingestion/cfg/collect.js +34 -0
  41. package/dist/core/ingestion/cfg/control-flow-context.d.ts +97 -0
  42. package/dist/core/ingestion/cfg/control-flow-context.js +113 -0
  43. package/dist/core/ingestion/cfg/emit.d.ts +144 -0
  44. package/dist/core/ingestion/cfg/emit.js +315 -0
  45. package/dist/core/ingestion/cfg/reaching-defs.d.ts +90 -0
  46. package/dist/core/ingestion/cfg/reaching-defs.js +364 -0
  47. package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
  48. package/dist/core/ingestion/cfg/traversal-result.js +2 -0
  49. package/dist/core/ingestion/cfg/types.d.ts +231 -0
  50. package/dist/core/ingestion/cfg/types.js +13 -0
  51. package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +144 -0
  52. package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1032 -0
  53. package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
  54. package/dist/core/ingestion/cfg/visitors/typescript.js +584 -0
  55. package/dist/core/ingestion/language-provider.d.ts +36 -2
  56. package/dist/core/ingestion/languages/c-cpp.js +11 -4
  57. package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
  58. package/dist/core/ingestion/languages/cpp/captures.js +24 -1
  59. package/dist/core/ingestion/languages/cpp/query.js +23 -0
  60. package/dist/core/ingestion/languages/java.js +3 -0
  61. package/dist/core/ingestion/languages/kotlin/query.js +3 -2
  62. package/dist/core/ingestion/languages/kotlin.js +5 -1
  63. package/dist/core/ingestion/languages/typescript.js +5 -0
  64. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
  65. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  66. package/dist/core/ingestion/method-types.d.ts +2 -0
  67. package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
  68. package/dist/core/ingestion/model/symbol-table.js +1 -0
  69. package/dist/core/ingestion/parsing-processor.js +22 -0
  70. package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
  71. package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
  72. package/dist/core/ingestion/pipeline.d.ts +57 -0
  73. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
  74. package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
  75. package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
  76. package/dist/core/ingestion/route-extractors/spring.js +136 -0
  77. package/dist/core/ingestion/scope-extractor.js +3 -0
  78. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
  79. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
  80. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
  81. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
  82. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +21 -0
  83. package/dist/core/ingestion/scope-resolution/pipeline/run.js +207 -0
  84. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  85. package/dist/core/ingestion/taint/emit.d.ts +124 -0
  86. package/dist/core/ingestion/taint/emit.js +204 -0
  87. package/dist/core/ingestion/taint/match.d.ts +153 -0
  88. package/dist/core/ingestion/taint/match.js +278 -0
  89. package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
  90. package/dist/core/ingestion/taint/path-codec.js +190 -0
  91. package/dist/core/ingestion/taint/propagate.d.ts +216 -0
  92. package/dist/core/ingestion/taint/propagate.js +664 -0
  93. package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
  94. package/dist/core/ingestion/taint/site-safety.js +98 -0
  95. package/dist/core/ingestion/taint/source-sink-config.d.ts +94 -23
  96. package/dist/core/ingestion/taint/source-sink-config.js +11 -11
  97. package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
  98. package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
  99. package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
  100. package/dist/core/ingestion/taint/typescript-model.js +102 -0
  101. package/dist/core/ingestion/utils/method-props.js +1 -0
  102. package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
  103. package/dist/core/ingestion/workers/clone-safety.js +465 -0
  104. package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
  105. package/dist/core/ingestion/workers/parse-worker.js +70 -53
  106. package/dist/core/ingestion/workers/post-result.d.ts +22 -0
  107. package/dist/core/ingestion/workers/post-result.js +87 -0
  108. package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
  109. package/dist/core/ingestion/workers/result-merge.js +43 -0
  110. package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
  111. package/dist/core/ingestion/workers/worker-pool.js +33 -16
  112. package/dist/core/lbug/lbug-adapter.d.ts +19 -0
  113. package/dist/core/lbug/lbug-adapter.js +56 -1
  114. package/dist/core/run-analyze.d.ts +87 -0
  115. package/dist/core/run-analyze.js +280 -25
  116. package/dist/core/tree-sitter/parser-loader.js +5 -4
  117. package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
  118. package/dist/core/tree-sitter/vendored-grammars.js +57 -0
  119. package/dist/mcp/core/embedder.js +4 -0
  120. package/dist/mcp/local/local-backend.d.ts +86 -2
  121. package/dist/mcp/local/local-backend.js +657 -27
  122. package/dist/mcp/resources.js +1 -0
  123. package/dist/mcp/tools.d.ts +9 -0
  124. package/dist/mcp/tools.js +109 -0
  125. package/dist/server/analyze-launch.d.ts +29 -0
  126. package/dist/server/analyze-launch.js +137 -0
  127. package/dist/server/analyze-upload.d.ts +33 -0
  128. package/dist/server/analyze-upload.js +123 -0
  129. package/dist/server/analyze-worker-ipc.d.ts +58 -0
  130. package/dist/server/analyze-worker-ipc.js +16 -0
  131. package/dist/server/analyze-worker.d.ts +17 -1
  132. package/dist/server/analyze-worker.js +7 -1
  133. package/dist/server/api.js +46 -140
  134. package/dist/server/git-clone.d.ts +1 -0
  135. package/dist/server/git-clone.js +1 -1
  136. package/dist/server/middleware.d.ts +11 -0
  137. package/dist/server/middleware.js +27 -0
  138. package/dist/server/upload-ingest.d.ts +56 -0
  139. package/dist/server/upload-ingest.js +276 -0
  140. package/dist/server/upload-paths.d.ts +31 -0
  141. package/dist/server/upload-paths.js +51 -0
  142. package/dist/server/upload-sweep.d.ts +20 -0
  143. package/dist/server/upload-sweep.js +57 -0
  144. package/dist/storage/branch-index.d.ts +52 -0
  145. package/dist/storage/branch-index.js +65 -0
  146. package/dist/storage/git.d.ts +11 -0
  147. package/dist/storage/git.js +28 -0
  148. package/dist/storage/parse-cache.d.ts +22 -1
  149. package/dist/storage/parse-cache.js +32 -10
  150. package/dist/storage/repo-manager.d.ts +122 -10
  151. package/dist/storage/repo-manager.js +162 -21
  152. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +42 -8
  153. package/hooks/claude/gitnexus-hook.cjs +36 -8
  154. package/hooks/claude/hook-db-lock-probe.cjs +123 -2
  155. package/package.json +4 -2
  156. package/scripts/assert-publish-grammar-coverage.cjs +31 -0
  157. package/scripts/build-tree-sitter-grammars.cjs +16 -10
  158. package/skills/gitnexus-guide.md +11 -0
  159. package/vendor/tree-sitter-c/package.json +1 -1
  160. package/vendor/tree-sitter-dart/package.json +1 -1
  161. package/vendor/tree-sitter-kotlin/package.json +1 -1
  162. package/vendor/tree-sitter-proto/package.json +1 -1
  163. package/vendor/tree-sitter-swift/package.json +1 -1
  164. package/web/assets/{agent-CKPMqImC.js → agent-ay4LD70X.js} +1 -1
  165. package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-Dc-viYhd.js} +1 -1
  166. package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-4rpojOyj.js} +1 -1
  167. package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-BnOy944n.js} +1 -1
  168. package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-Bsgpy98Q.js} +1 -1
  169. package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-D5atDGjc.js} +1 -1
  170. package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-Cz1OEMVi.js} +1 -1
  171. package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CWYwzfP8.js} +1 -1
  172. package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-DtBObr8L.js} +1 -1
  173. package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-BUV44Ov_.js} +1 -1
  174. package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-Ds9s-sRF.js} +1 -1
  175. package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CupStHQb.js} +1 -1
  176. package/web/assets/index-COMMmbxW.css +2 -0
  177. package/web/assets/{index-CG6q8eTs.js → index-Do8AE5yF.js} +85 -85
  178. package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-DPTXSbgu.js} +1 -1
  179. package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CeK-Qhet.js} +1 -1
  180. package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-D4tx6QQ-.js} +1 -1
  181. package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-Bcto_kGa.js} +1 -1
  182. package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-DPIeBTG6.js} +1 -1
  183. package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-DDGXjVKO.js} +1 -1
  184. package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BzDqpcVW.js} +1 -1
  185. package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-Bx__als3.js} +1 -1
  186. package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-QqOh2yO-.js} +1 -1
  187. package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-C1eogTOG.js} +1 -1
  188. package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-D20F4rSW.js} +1 -1
  189. package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-BeBHg7ST.js} +1 -1
  190. package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CdOWeoNI.js} +1 -1
  191. package/web/index.html +2 -2
  192. package/scripts/materialize-vendor-grammars.cjs +0 -97
  193. package/web/assets/index-BKWA-m7o.css +0 -2
@@ -8,6 +8,7 @@
8
8
  * IMPORTANT: This module must NEVER call process.exit(). The caller (CLI
9
9
  * wrapper or server worker) is responsible for process lifecycle.
10
10
  */
11
+ import { type RepoMeta } from '../storage/repo-manager.js';
11
12
  export interface AnalyzeCallbacks {
12
13
  onProgress: (phase: string, percent: number, message: string) => void;
13
14
  onLog?: (message: string) => void;
@@ -48,6 +49,29 @@ export interface AnalyzeOptions {
48
49
  noStats?: boolean;
49
50
  /** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */
50
51
  skipSkills?: boolean;
52
+ /**
53
+ * Build the CFG/PDG substrate (#2081 M1). Forwarded to `PipelineOptions.pdg`,
54
+ * which threads to BOTH the worker (CFG build, via workerData) AND
55
+ * scope-resolution (BasicBlock/CFG emit gate). Off by default.
56
+ */
57
+ pdg?: boolean;
58
+ /** Per-function source-line cap for worker-side CFG construction (#2081 M1).
59
+ * Forwarded to `PipelineOptions.pdgMaxFunctionLines`. No CLI flag in M1 —
60
+ * programmatic / server analyze-worker path only; the worker applies
61
+ * `DEFAULT_PDG_MAX_FUNCTION_LINES` when unset. */
62
+ pdgMaxFunctionLines?: number;
63
+ /** Per-function CFG edge cap. Forwarded to `PipelineOptions.pdgMaxEdgesPerFunction`. */
64
+ pdgMaxEdgesPerFunction?: number;
65
+ /** Per-function REACHING_DEF edge cap (#2082 M2). Forwarded to
66
+ * `PipelineOptions.pdgMaxReachingDefEdgesPerFunction`. */
67
+ pdgMaxReachingDefEdgesPerFunction?: number;
68
+ /** Per-function taint findings cap (#2083 M3). Forwarded to
69
+ * `PipelineOptions.pdgMaxTaintFindingsPerFunction`. No CLI flag or rc key
70
+ * (KTD8) — programmatic / server path only, like the other pdg caps. */
71
+ pdgMaxTaintFindingsPerFunction?: number;
72
+ /** Per-finding taint hop cap (#2083 M3, KTD6). Forwarded to
73
+ * `PipelineOptions.pdgMaxTaintHops`. No CLI flag or rc key (KTD8). */
74
+ pdgMaxTaintHops?: number;
51
75
  /**
52
76
  * Default branch threaded into generated AGENTS.md / CLAUDE.md so the
53
77
  * regression-compare example uses the configured branch instead of a
@@ -55,6 +79,16 @@ export interface AnalyzeOptions {
55
79
  * "main" fallback for non-CLI callers (e.g. the server analyze worker).
56
80
  */
57
81
  defaultBranch?: string;
82
+ /**
83
+ * Index-branch selector (#2106). Distinct from `defaultBranch` (which only
84
+ * affects generated AGENTS.md/CLAUDE.md base_ref text). When set, this run is
85
+ * labelled as that branch and routed to a per-branch index slot unless it is
86
+ * the primary branch. When `undefined`, the branch is auto-detected from the
87
+ * checked-out HEAD (the flat/primary slot for the first-indexed branch, a
88
+ * `branches/<slug>/` sub-directory otherwise). Detached HEAD / non-git always
89
+ * maps to the flat slot.
90
+ */
91
+ branch?: string;
58
92
  /**
59
93
  * User-provided alias for the registry `name` (#829). When set,
60
94
  * forwarded to `registerRepo` so the indexed repo is stored under
@@ -78,6 +112,13 @@ export interface AnalyzeOptions {
78
112
  * removed); `undefined` defers to the env / auto-formula fallback.
79
113
  */
80
114
  workerPoolSize?: number;
115
+ /**
116
+ * Extra fetch-wrapper function names to treat as HTTP consumers, forwarded to
117
+ * `PipelineOptions.fetchWrappers` (#1589/#1852 residual). Sourced from the CLI
118
+ * `.gitnexusrc` `fetchWrappers` list. `undefined`/empty leaves the route
119
+ * consumer scan unchanged.
120
+ */
121
+ fetchWrappers?: string[];
81
122
  }
82
123
  export interface AnalyzeResult {
83
124
  repoName: string;
@@ -102,6 +143,13 @@ export interface AnalyzeResult {
102
143
  * the persisted meta surface the degraded state instead of reporting healthy.
103
144
  */
104
145
  ftsSkipped?: boolean;
146
+ /**
147
+ * True when the index this run produced/validated is the primary/flat slot
148
+ * (#2106 R2). `false` for a non-primary branch index. Lets the CLI skip
149
+ * repo-root AGENTS.md/CLAUDE.md refreshes (e.g. the base_ref fast-path) for a
150
+ * branch analyze, mirroring the in-pipeline `if (!placement.branch)` gate.
151
+ */
152
+ isPrimaryBranch?: boolean;
105
153
  }
106
154
  export { deriveEmbeddingMode, DEFAULT_EMBEDDING_NODE_LIMIT } from './embedding-mode.js';
107
155
  export type { EmbeddingMode } from './embedding-mode.js';
@@ -117,4 +165,43 @@ export declare const PHASE_LABELS: Record<string, string>;
117
165
  * the {@link AnalyzeCallbacks} interface — it never writes to stdout/stderr
118
166
  * directly and never calls `process.exit()`.
119
167
  */
168
+ /**
169
+ * Build the primary-inversion warning (#2106 R8), or `undefined` when there is
170
+ * nothing to warn about. Pure + exported for testing. Both inputs are trimmed
171
+ * (a diagnostic — a missed warning is low-harm; a false warning is the thing to
172
+ * avoid). `defaultBranch` is the repo's `origin/HEAD` branch (null when unset,
173
+ * e.g. fresh clones / CI), `flatOwner` is the branch that owns the flat slot.
174
+ */
175
+ export declare const primaryInversionWarning: (defaultBranch: string | null | undefined, flatOwner: string | null | undefined) => string | undefined;
176
+ /**
177
+ * Collect the recorded parse-cache chunk keys across the flat + every branch
178
+ * meta under a flat `.gitnexus` storage, EXCLUDING `excludeDir` (the current
179
+ * run's own meta dir) so a single-branch repo collects nothing and its prune
180
+ * stays byte-identical to today (#2106 R6). `complete` is false when a sibling
181
+ * meta.json exists but fails to parse — callers then retain the whole shared
182
+ * cache rather than over-evict another branch's still-live shards. Exported for
183
+ * testing.
184
+ */
185
+ export declare const collectBranchCacheKeys: (storagePath: string, excludeDir?: string) => Promise<{
186
+ keys: Set<string>;
187
+ complete: boolean;
188
+ }>;
189
+ /**
190
+ * Resolve the requested `--pdg` configuration to the shape recorded in
191
+ * `RepoMeta.pdg`, or `undefined` for a pdg-off run. Caps resolve to their
192
+ * defaults so an explicit-default run compares equal to a default run
193
+ * (`0` = unlimited is preserved as `0`). Pure + exported for testing.
194
+ */
195
+ type PdgOptions = Pick<AnalyzeOptions, 'pdg' | 'pdgMaxFunctionLines' | 'pdgMaxEdgesPerFunction' | 'pdgMaxReachingDefEdgesPerFunction' | 'pdgMaxTaintFindingsPerFunction' | 'pdgMaxTaintHops'>;
196
+ export declare const resolvePdgConfig: (options: PdgOptions) => RepoMeta["pdg"];
197
+ /**
198
+ * Whether the requested `--pdg` configuration differs from the one the
199
+ * existing index's DB rows were built under (#2099 F1). An absent recorded
200
+ * stamp means pdg-off (every legacy meta — `--pdg` shipped opt-in). Any
201
+ * mismatch means the incremental writeback (which only persists changed-file
202
+ * nodes) cannot produce a coherent index: off→on would silently drop the
203
+ * freshly built CFG layer, on→off would strand zombie BasicBlocks — so the
204
+ * caller forces a full writeback. Pure + exported for testing.
205
+ */
206
+ export declare const pdgModeMismatch: (recorded: RepoMeta["pdg"], options: PdgOptions) => boolean;
120
207
  export declare function runFullAnalysis(repoPath: string, options: AnalyzeOptions, callbacks: AnalyzeCallbacks): Promise<AnalyzeResult>;
@@ -17,14 +17,19 @@ 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
+ import { DEFAULT_PDG_MAX_FUNCTION_LINES } from './ingestion/cfg/collect.js';
22
+ import { DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION, } from './ingestion/cfg/emit.js';
23
+ import { DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION, DEFAULT_PDG_MAX_TAINT_HOPS, } from './ingestion/taint/propagate.js';
24
+ import { taintModelVersion } from './ingestion/taint/typescript-model.js';
21
25
  import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
22
26
  import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
23
27
  import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
24
28
  import { loadParseCache, saveParseCache, pruneCache, PARSE_CACHE_VERSION, } from '../storage/parse-cache.js';
25
29
  import { getDurableParsedFileDir, pruneAndSaveDurableParsedFileStore, } from '../storage/parsedfile-store.js';
26
- import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
30
+ import { getCurrentCommit, getCurrentBranch, getDefaultBranch, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
27
31
  import { generateAIContextFiles } from '../cli/ai-context.js';
32
+ import { sanitizeDetectedBranch } from '../cli/analyze-config.js';
28
33
  import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
29
34
  import { STALE_HASH_SENTINEL } from './lbug/schema.js';
30
35
  /**
@@ -70,6 +75,112 @@ export const PHASE_LABELS = {
70
75
  * the {@link AnalyzeCallbacks} interface — it never writes to stdout/stderr
71
76
  * directly and never calls `process.exit()`.
72
77
  */
78
+ /**
79
+ * Build the primary-inversion warning (#2106 R8), or `undefined` when there is
80
+ * nothing to warn about. Pure + exported for testing. Both inputs are trimmed
81
+ * (a diagnostic — a missed warning is low-harm; a false warning is the thing to
82
+ * avoid). `defaultBranch` is the repo's `origin/HEAD` branch (null when unset,
83
+ * e.g. fresh clones / CI), `flatOwner` is the branch that owns the flat slot.
84
+ */
85
+ export const primaryInversionWarning = (defaultBranch, flatOwner) => {
86
+ const norm = (s) => s?.trim() || undefined;
87
+ const d = norm(defaultBranch);
88
+ const o = norm(flatOwner);
89
+ if (!d || !o || d === o)
90
+ return undefined;
91
+ return (`Warning: the default branch "${d}" is not the primary index — "${o}" owns the flat slot. ` +
92
+ `Run \`gitnexus clean --branch ${o}\` then re-index on "${d}", or query it explicitly with \`--branch ${d}\`.`);
93
+ };
94
+ /**
95
+ * Collect the recorded parse-cache chunk keys across the flat + every branch
96
+ * meta under a flat `.gitnexus` storage, EXCLUDING `excludeDir` (the current
97
+ * run's own meta dir) so a single-branch repo collects nothing and its prune
98
+ * stays byte-identical to today (#2106 R6). `complete` is false when a sibling
99
+ * meta.json exists but fails to parse — callers then retain the whole shared
100
+ * cache rather than over-evict another branch's still-live shards. Exported for
101
+ * testing.
102
+ */
103
+ export const collectBranchCacheKeys = async (storagePath, excludeDir) => {
104
+ const keys = new Set();
105
+ let complete = true;
106
+ const metaDirs = [storagePath];
107
+ const branchesDir = path.join(storagePath, 'branches');
108
+ const slugs = await fs.readdir(branchesDir).catch(() => []);
109
+ for (const slug of slugs)
110
+ metaDirs.push(path.join(branchesDir, slug));
111
+ for (const dir of metaDirs) {
112
+ if (excludeDir && path.resolve(dir) === path.resolve(excludeDir))
113
+ continue;
114
+ let raw;
115
+ try {
116
+ raw = await fs.readFile(path.join(dir, 'meta.json'), 'utf-8');
117
+ }
118
+ catch {
119
+ continue; // no meta here — not a branch index, not a failure
120
+ }
121
+ try {
122
+ const parsed = JSON.parse(raw);
123
+ if (Array.isArray(parsed.cacheKeys)) {
124
+ for (const k of parsed.cacheKeys)
125
+ if (typeof k === 'string')
126
+ keys.add(k);
127
+ }
128
+ }
129
+ catch {
130
+ complete = false; // present but corrupt → fail-safe toward retention
131
+ }
132
+ }
133
+ return { keys, complete };
134
+ };
135
+ export const resolvePdgConfig = (options) => options.pdg === true
136
+ ? {
137
+ maxFunctionLines: options.pdgMaxFunctionLines ?? DEFAULT_PDG_MAX_FUNCTION_LINES,
138
+ maxEdgesPerFunction: options.pdgMaxEdgesPerFunction ?? DEFAULT_MAX_CFG_EDGES_PER_FUNCTION,
139
+ maxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction ??
140
+ DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION,
141
+ // #2083 M3: taint caps + model identity. The key-union comparator in
142
+ // pdgModeMismatch picks these up structurally — an M2-era stamp lacks
143
+ // all three, so the first M3 run over an M2 `--pdg` index trips a full
144
+ // writeback that populates TAINTED/SANITIZES rows without `--force`.
145
+ maxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction ?? DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION,
146
+ maxTaintHops: options.pdgMaxTaintHops ?? DEFAULT_PDG_MAX_TAINT_HOPS,
147
+ // Built-in model digest (KTD7/R7): persisted findings must never
148
+ // outlive the model that produced them — ANY model-content change
149
+ // ships as a new digest and repopulates the taint edges.
150
+ taintModelVersion,
151
+ }
152
+ : undefined;
153
+ /**
154
+ * Whether the requested `--pdg` configuration differs from the one the
155
+ * existing index's DB rows were built under (#2099 F1). An absent recorded
156
+ * stamp means pdg-off (every legacy meta — `--pdg` shipped opt-in). Any
157
+ * mismatch means the incremental writeback (which only persists changed-file
158
+ * nodes) cannot produce a coherent index: off→on would silently drop the
159
+ * freshly built CFG layer, on→off would strand zombie BasicBlocks — so the
160
+ * caller forces a full writeback. Pure + exported for testing.
161
+ */
162
+ export const pdgModeMismatch = (recorded, options) => {
163
+ const requested = resolvePdgConfig(options);
164
+ if (!requested && !recorded)
165
+ return false;
166
+ if (!requested || !recorded)
167
+ return true;
168
+ // Structural comparison over the KEY UNION of both resolved records — not a
169
+ // hand-maintained field list. Both sides come fully resolved from
170
+ // resolvePdgConfig, so any new emit-affecting knob added there joins the
171
+ // comparison automatically (M1's hand-extended comparator was the trap this
172
+ // closes: a knob it missed would silently strand a stale projection). It is
173
+ // also what makes the M1→M2 upgrade work with zero extra code: an M1-era
174
+ // stamp lacks maxReachingDefEdgesPerFunction, so `4000 !== undefined` trips
175
+ // a full writeback that populates REACHING_DEF rows without `--force`.
176
+ const reqRecord = requested;
177
+ const recRecord = recorded;
178
+ for (const key of new Set([...Object.keys(reqRecord), ...Object.keys(recRecord)])) {
179
+ if (reqRecord[key] !== recRecord[key])
180
+ return true;
181
+ }
182
+ return false;
183
+ };
73
184
  export async function runFullAnalysis(repoPath, options, callbacks) {
74
185
  const log = (msg) => callbacks.onLog?.(msg);
75
186
  const progress = (phase, percent, message) => callbacks.onProgress(phase, percent, message);
@@ -80,7 +191,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
80
191
  // counter in its own module instance and is process-scoped, so no separate
81
192
  // worker-side reset is needed (see safe-parse.ts ParseTimeoutError contract).
82
193
  resetDegradedParseCounter();
83
- const { storagePath, lbugPath } = getStoragePaths(repoPath);
194
+ // `storagePath` is ALWAYS the flat `.gitnexus` — content-addressed caches
195
+ // (parse-cache, parsedfile-store) and the kuzu-migration cleanup live there
196
+ // and are shared across branches (#2106 KTD7).
197
+ const { storagePath } = getStoragePaths(repoPath);
84
198
  // Clean up stale KuzuDB files from before the LadybugDB migration.
85
199
  const kuzuResult = await cleanupOldKuzuFiles(storagePath);
86
200
  if (kuzuResult.found && kuzuResult.needsReindex) {
@@ -88,7 +202,53 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
88
202
  }
89
203
  const repoHasGit = hasGitDir(repoPath);
90
204
  const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
91
- const existingMeta = await loadMeta(storagePath);
205
+ // ── #2106: resolve which branch slot this run writes to ───────────────
206
+ // `branchLabel` is the branch identity recorded in meta.json (incl. the
207
+ // primary). `placement.branch` is undefined for the flat/primary slot (the
208
+ // lbug/meta paths stay byte-identical to single-branch behavior) and set for
209
+ // a `branches/<slug>/` sub-directory. Explicit `--branch` is always honored;
210
+ // otherwise auto-detect the checked-out branch (null for detached HEAD /
211
+ // non-git → flat slot).
212
+ // Normalize the auto-detected branch the same way an explicit `--branch` is
213
+ // validated (#2106 R1): a git ref the branch-name rules forbid (backtick,
214
+ // `~ ^ : ? *`, leading `-`, `..`) becomes `null` → the flat slot, matching
215
+ // that a later `--branch <that-ref>` query would also be rejected. A normal
216
+ // ref passes through unchanged so index-time and query-time labels round-trip.
217
+ const checkedOutBranch = repoHasGit
218
+ ? (sanitizeDetectedBranch(getCurrentBranch(repoPath)) ?? null)
219
+ : null;
220
+ // Analyze indexes the working tree, not an arbitrary ref. An explicit
221
+ // `--branch X` while a DIFFERENT branch Y is checked out would write Y's
222
+ // content (and Y's commit) into X's index slot, corrupting X (#2106). Refuse
223
+ // the mismatch. Detached HEAD / non-git (checkedOutBranch === null) still
224
+ // allow an explicit label so CI checkouts can name their snapshot.
225
+ if (options.branch && checkedOutBranch && options.branch !== checkedOutBranch) {
226
+ throw new Error(`--branch "${options.branch}" does not match the checked-out branch "${checkedOutBranch}". ` +
227
+ `Check out "${options.branch}" before indexing it, or omit --branch to index the current branch.`);
228
+ }
229
+ const branchLabel = options.branch ?? checkedOutBranch;
230
+ const placement = await resolveBranchPlacement(repoPath, branchLabel);
231
+ const { lbugPath, metaPath } = getStoragePaths(repoPath, placement.branch);
232
+ // Directory that owns this run's meta.json (flat `.gitnexus` for the primary
233
+ // slot, `branches/<slug>/` otherwise). loadMeta/saveMeta operate on it so
234
+ // each branch keeps its own lastCommit / fileHashes / incremental dirty flag.
235
+ const metaDir = path.dirname(metaPath);
236
+ const existingMeta = await loadMeta(metaDir);
237
+ // ── #2106 (R8): warn when the repo's default branch is not the primary ──
238
+ // A non-default branch can own the flat slot (it was indexed first). That
239
+ // index is still fully queryable via `--branch`, so this is an ergonomics
240
+ // wart, not data loss — we only warn (no risky relocation of a live DB).
241
+ if (repoHasGit) {
242
+ // Who owns the flat slot after this run? For a flat/primary run it is this
243
+ // run's resolved label (carrying an existing stamp forward); for a branch
244
+ // run the flat owner is unchanged, so read the flat meta.
245
+ const flatOwner = placement.branch
246
+ ? (await loadMeta(storagePath))?.branch
247
+ : (branchLabel ?? existingMeta?.branch);
248
+ const warning = primaryInversionWarning(getDefaultBranch(repoPath), flatOwner);
249
+ if (warning)
250
+ log(warning);
251
+ }
92
252
  // ── FTS-only repair path ────────────────────────────────────────────
93
253
  if (options.repairFts) {
94
254
  if (!existingMeta) {
@@ -158,13 +318,37 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
158
318
  // clear it, the on-disk index may be in a half-state. Cheapest path
159
319
  // back to a known-good index is to wipe + rebuild from scratch.
160
320
  if (existingMeta?.incrementalInProgress) {
161
- log('Previous incremental run did not complete cleanly (incrementalInProgress flag set); ' +
321
+ log(
322
+ // "analyze run", not "incremental run" — since #2099 F1 the flag is a
323
+ // generic dirty marker written by BOTH writeback branches.
324
+ 'Previous analyze run did not complete cleanly (incrementalInProgress flag set); ' +
162
325
  'forcing full rebuild to restore a known-good index.');
163
326
  options = { ...options, force: true };
164
327
  // Reload meta after clearing the flag in-memory; we still want fileHashes
165
328
  // for the post-rebuild meta carry-over, but force=true ensures the
166
329
  // rebuild path executes.
167
330
  }
331
+ // ── pdg-mode flip forces full writeback (#2099 F1) ─────────────────
332
+ // The incremental writeback persists only changed-file nodes, so a pdg
333
+ // config differing from the one the DB rows were built under cannot be
334
+ // reconciled incrementally: off→on silently drops the freshly built CFG
335
+ // layer ("Incremental: changed=0", zero BasicBlock rows), on→off strands
336
+ // zombie blocks for unchanged files. MUST sit before the alreadyUpToDate
337
+ // fast path below — a clean-tree flip would otherwise early-return without
338
+ // running the pipeline at all. The notice is deliberately NOT gated on
339
+ // options.force: --skills implies force with no message of its own, and a
340
+ // mode change deserves a diagnostic regardless of why a rebuild happens.
341
+ if (existingMeta && pdgModeMismatch(existingMeta.pdg, options)) {
342
+ const pdgOn = options.pdg === true;
343
+ const capsOnly = !!existingMeta.pdg && pdgOn; // both-on can only mismatch via caps
344
+ const was = existingMeta.pdg ? 'with --pdg' : 'without --pdg';
345
+ const now = pdgOn ? 'with --pdg' : 'without --pdg';
346
+ log(`pdg mode changed (index built ${was}, this run is ${now}` +
347
+ `${capsOnly ? ', but with different caps' : ''}); forcing a full ` +
348
+ `rebuild so the CFG layer is ${pdgOn ? 'fully persisted' : 'fully removed'}. ` +
349
+ `Tip: set \`pdg: ${pdgOn}\` in .gitnexusrc to pin the mode across runs.`);
350
+ options = { ...options, force: true };
351
+ }
168
352
  // ── Early-return: already up to date ──────────────────────────────
169
353
  if (existingMeta && !options.force && existingMeta.lastCommit === currentCommit) {
170
354
  // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
@@ -220,6 +404,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
220
404
  repoPath,
221
405
  stats: existingMeta.stats ?? {},
222
406
  alreadyUpToDate: true,
407
+ isPrimaryBranch: !placement.branch,
223
408
  };
224
409
  }
225
410
  }
@@ -308,6 +493,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
308
493
  }, {
309
494
  parseCache,
310
495
  workerPoolSize: options.workerPoolSize,
496
+ // CFG/PDG opt-in (#2081 M1). PipelineOptions.pdg fans out to the worker
497
+ // build gate (workerData.pdg) and the scope-resolution emit gate.
498
+ pdg: options.pdg === true,
499
+ pdgMaxFunctionLines: options.pdgMaxFunctionLines,
500
+ pdgMaxEdgesPerFunction: options.pdgMaxEdgesPerFunction,
501
+ pdgMaxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction,
502
+ pdgMaxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction,
503
+ pdgMaxTaintHops: options.pdgMaxTaintHops,
504
+ fetchWrappers: options.fetchWrappers,
311
505
  });
312
506
  // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
313
507
  progress('lbug', 60, 'Loading into LadybugDB...');
@@ -345,8 +539,8 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
345
539
  `deleted=${hashDiff.deleted.length} ` +
346
540
  `(skipping wipe + ${allFilePaths.length - hashDiff.toWrite.length} unchanged file rows preserved)`);
347
541
  // Set the dirty flag BEFORE any destructive DB mutation. Cleared on
348
- // success at the meta-save step.
349
- await saveMeta(storagePath, {
542
+ // success at the meta-save step. Scoped to this branch's meta.json.
543
+ await saveMeta(metaDir, {
350
544
  ...existingMeta,
351
545
  incrementalInProgress: {
352
546
  startedAt: Date.now(),
@@ -356,6 +550,19 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
356
550
  }
357
551
  else {
358
552
  // Full rebuild path: wipe DB files first.
553
+ // Set the dirty flag BEFORE the wipe whenever a prior meta exists,
554
+ // mirroring the incremental branch above (#2099 F1, KTD2b). Without it a
555
+ // full rebuild crashing between the wipe and the end-of-run saveMeta
556
+ // leaves a meta that vouches for a DB it no longer matches — the next
557
+ // clean-tree run's fast path would certify a destroyed DB (or, after a
558
+ // pdg flip, certify zombie/missing BasicBlock rows indefinitely).
559
+ // toWriteCount: 0 is the full-path sentinel (no incremental write set).
560
+ if (existingMeta) {
561
+ await saveMeta(metaDir, {
562
+ ...existingMeta,
563
+ incrementalInProgress: { startedAt: Date.now(), toWriteCount: 0 },
564
+ });
565
+ }
359
566
  await closeLbug();
360
567
  const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
361
568
  for (const f of lbugFiles) {
@@ -685,6 +892,14 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
685
892
  repoPath,
686
893
  lastCommit: currentCommit,
687
894
  indexedAt: new Date().toISOString(),
895
+ // Branch identity this index represents (#2106). Recorded for the flat
896
+ // slot too (so resolveBranchPlacement knows which branch owns it). When
897
+ // the label is null (detached HEAD / non-git re-analyze) we PRESERVE an
898
+ // existing stamp rather than stripping it — otherwise a detached re-index
899
+ // of the primary (e.g. CI's `actions/checkout` default) would un-claim the
900
+ // flat slot and let the next branch analyze overwrite the primary index.
901
+ // Stays absent only when never stamped (fresh detached/non-git repo).
902
+ branch: branchLabel ?? existingMeta?.branch,
688
903
  // Captured here (not at registration) so it travels with the
689
904
  // on-disk meta.json — sibling-clone fingerprinting works for
690
905
  // out-of-tree consumers (group-status, future tooling) without
@@ -723,9 +938,20 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
723
938
  // dirty flag (full and incremental success paths converge here).
724
939
  schemaVersion: hasGitDir(repoPath) ? INCREMENTAL_SCHEMA_VERSION : undefined,
725
940
  fileHashes: hasGitDir(repoPath) ? newFileHashesRecord : undefined,
941
+ // This branch's full live chunk-key set (#2106 R6). `usedKeys` is every
942
+ // chunk hash touched in this scan — cache HITS included (see parse-impl
943
+ // usedKeys.add) — so it's complete even on an incremental run. Persisted
944
+ // so a sibling branch's prune can union it and not evict our shards.
945
+ cacheKeys: [...parseCache.usedKeys],
726
946
  incrementalInProgress: undefined,
947
+ // The effective pdg config this run's DB rows were built under
948
+ // (#2099 F1). `undefined` on pdg-off runs — this meta is a fresh
949
+ // literal (no spread of existingMeta), so omission is what CLEARS the
950
+ // stamp after an on→off flip; the next pdgModeMismatch then compares
951
+ // off==off and incremental eligibility is restored.
952
+ pdg: resolvePdgConfig(options),
727
953
  };
728
- await saveMeta(storagePath, meta);
954
+ await saveMeta(metaDir, meta);
729
955
  // Persist the incremental parse cache for the next run. Wraps in
730
956
  // try/catch so a cache-write failure never breaks an otherwise
731
957
  // successful indexing run. Prune stale chunk-hash entries first so
@@ -734,6 +960,25 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
734
960
  // dead weight; the parse phase populates `usedKeys` as it processes
735
961
  // chunks).
736
962
  try {
963
+ // #2106 R6: the parse cache + durable store are shared across branches.
964
+ // Before pruning to this run's keys, fold in the OTHER branches' recorded
965
+ // chunk keys so a branch switch doesn't evict their still-live shards.
966
+ // Adding to usedKeys makes them survive pruneCache AND land in the saved
967
+ // index (saveParseCache builds the index from usedKeys). Excludes this
968
+ // run's own meta dir, so a single-branch repo folds in nothing → prune
969
+ // set byte-identical to today.
970
+ const { keys: siblingKeys, complete } = await collectBranchCacheKeys(storagePath, metaDir);
971
+ if (complete) {
972
+ for (const k of siblingKeys)
973
+ parseCache.usedKeys.add(k);
974
+ }
975
+ else {
976
+ // Fail-safe toward retention: a sibling meta was unreadable, so keep
977
+ // everything currently loaded rather than evict on incomplete info.
978
+ log('Parse cache: a branch meta was unreadable — retaining all cached chunks (#2106).');
979
+ for (const k of parseCache.entries.keys())
980
+ parseCache.usedKeys.add(k);
981
+ }
737
982
  const pruned = pruneCache(parseCache, parseCache.usedKeys);
738
983
  if (pruned > 0) {
739
984
  log(`Parse cache: pruned ${pruned} stale chunk entries`);
@@ -763,6 +1008,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
763
1008
  const projectName = await registerRepo(repoPath, meta, {
764
1009
  name: options.registryName,
765
1010
  allowDuplicateName: options.allowDuplicateName,
1011
+ // Non-primary branch runs upsert into the entry's branches[]; the
1012
+ // primary/flat run (placement.branch === undefined) refreshes the
1013
+ // top-level fields (#2106).
1014
+ branch: placement.branch,
766
1015
  });
767
1016
  // Keep generated .gitnexus contents ignored without editing the user's root .gitignore.
768
1017
  await ensureGitNexusIgnored(repoPath);
@@ -776,23 +1025,28 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
776
1025
  }
777
1026
  aggregatedClusterCount = Array.from(groups.values()).filter((count) => count >= 5).length;
778
1027
  }
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
1028
+ // Only (re)generate the repo-root AI context files (AGENTS.md / CLAUDE.md /
1029
+ // skills) for the primary/flat index (#2106). A non-primary branch analyze
1030
+ // must not churn the repo's committed AGENTS.md with branch-specific stats.
1031
+ if (!placement.branch) {
1032
+ try {
1033
+ await generateAIContextFiles(repoPath, storagePath, projectName, {
1034
+ files: pipelineResult.totalFileCount,
1035
+ nodes: stats.nodes,
1036
+ edges: stats.edges,
1037
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
1038
+ clusters: aggregatedClusterCount,
1039
+ processes: pipelineResult.processResult?.stats.totalProcesses,
1040
+ }, undefined, {
1041
+ skipAgentsMd: options.skipAgentsMd,
1042
+ skipSkills: options.skipSkills,
1043
+ noStats: options.noStats,
1044
+ defaultBranch: options.defaultBranch,
1045
+ });
1046
+ }
1047
+ catch {
1048
+ // Best-effort — don't fail the entire analysis for context file issues
1049
+ }
796
1050
  }
797
1051
  // ── Close LadybugDB ──────────────────────────────────────────────
798
1052
  // Stop the manual checkpoint driver before closeLbug so its
@@ -806,6 +1060,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
806
1060
  stats: meta.stats,
807
1061
  pipelineResult,
808
1062
  ftsSkipped: !ftsAvailable,
1063
+ isPrimaryBranch: !placement.branch,
809
1064
  };
810
1065
  }
811
1066
  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;