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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Upload working-directory paths.
3
+ *
4
+ * Browser folder uploads are written into ~/.gitnexus/uploads/{name}/ — a
5
+ * sibling of the clone root (git-clone.ts CLONE_ROOT) — so an uploaded repo
6
+ * persists and behaves like a cloned one (the graph UI's /api/file reads its
7
+ * files after analysis, and DELETE /api/repo removes it). Staging happens in
8
+ * an mkdtemp dir *under* UPLOAD_ROOT so the promote rename stays on one
9
+ * filesystem and remains atomic (a rename from os.tmpdir() could trip EXDEV —
10
+ * the exact Docker case this feature targets; see bridge-db.ts for the same
11
+ * anchored-staging pattern).
12
+ */
13
+ import path from 'path';
14
+ import os from 'os';
15
+ import { sanitizeRepoName } from '../storage/git.js';
16
+ import { REPO_NAME_PATTERN } from './git-clone.js';
17
+ /** Root directory for all uploaded repositories. Targets must resolve inside this. */
18
+ export const UPLOAD_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'uploads'));
19
+ /** Prefix for per-upload staging directories created under UPLOAD_ROOT. */
20
+ export const STAGING_PREFIX = '.staging-';
21
+ /**
22
+ * Get the upload target directory for a repo name.
23
+ *
24
+ * Re-validates at the boundary (callers may derive the name from an untrusted
25
+ * manifest). Rejects `.`, `..`, the `'unknown'` sentinel that sanitizeRepoName
26
+ * emits for un-nameable inputs, names beginning with `.` (which would collide
27
+ * with the `.staging-` prefix), and anything outside the safe charset.
28
+ */
29
+ export function getUploadDir(repoName) {
30
+ if (!repoName ||
31
+ repoName === '.' ||
32
+ repoName === '..' ||
33
+ repoName === 'unknown' ||
34
+ repoName.startsWith('.') ||
35
+ !REPO_NAME_PATTERN.test(repoName)) {
36
+ throw new Error('Invalid repository name');
37
+ }
38
+ return path.join(UPLOAD_ROOT, repoName);
39
+ }
40
+ /**
41
+ * Derive a filesystem-safe upload directory name from the manifest's
42
+ * top-level folder. Returns null when the name is un-nameable (so the caller
43
+ * rejects with 400 rather than colliding everyone on `UPLOAD_ROOT/unknown`).
44
+ */
45
+ export function deriveUploadName(topLevelName) {
46
+ const safe = sanitizeRepoName(topLevelName);
47
+ if (safe === 'unknown' || safe === '.' || safe === '..' || safe.startsWith('.')) {
48
+ return null;
49
+ }
50
+ return safe;
51
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Backstop cleanup for abandoned upload staging directories.
3
+ *
4
+ * A crashed/killed process can leave a `.staging-*` directory under
5
+ * UPLOAD_ROOT (the normal path removes it on success/failure/abort). This
6
+ * sweep, run once at server startup, removes staging dirs older than a
7
+ * threshold. Promoted upload dirs are persistent registered repos (like
8
+ * clones) and are NOT touched here — they are removed via DELETE /api/repo.
9
+ */
10
+ export interface SweepOptions {
11
+ /** Remove staging dirs older than this (default 6h). */
12
+ maxAgeMs?: number;
13
+ /** Override the root to sweep (defaults to UPLOAD_ROOT; for tests). */
14
+ root?: string;
15
+ /** Clock injection for tests. */
16
+ now?: number;
17
+ }
18
+ export declare function sweepStaleUploads(opts?: SweepOptions): Promise<{
19
+ removed: string[];
20
+ }>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Backstop cleanup for abandoned upload staging directories.
3
+ *
4
+ * A crashed/killed process can leave a `.staging-*` directory under
5
+ * UPLOAD_ROOT (the normal path removes it on success/failure/abort). This
6
+ * sweep, run once at server startup, removes staging dirs older than a
7
+ * threshold. Promoted upload dirs are persistent registered repos (like
8
+ * clones) and are NOT touched here — they are removed via DELETE /api/repo.
9
+ */
10
+ import path from 'path';
11
+ import fsp from 'fs/promises';
12
+ import { UPLOAD_ROOT, STAGING_PREFIX } from './upload-paths.js';
13
+ export async function sweepStaleUploads(opts = {}) {
14
+ const maxAgeMs = opts.maxAgeMs ?? 6 * 60 * 60 * 1000;
15
+ const root = opts.root ?? UPLOAD_ROOT;
16
+ const now = opts.now ?? Date.now();
17
+ const removed = [];
18
+ let entries;
19
+ try {
20
+ entries = await fsp.readdir(root, { withFileTypes: true });
21
+ }
22
+ catch {
23
+ return { removed }; // root does not exist yet — nothing to sweep
24
+ }
25
+ for (const entry of entries) {
26
+ if (!entry.isDirectory())
27
+ continue;
28
+ const full = path.join(root, entry.name);
29
+ try {
30
+ const st = await fsp.stat(full);
31
+ if (now - st.mtimeMs <= maxAgeMs)
32
+ continue; // recent — keep
33
+ if (entry.name.startsWith(STAGING_PREFIX)) {
34
+ // Transient staging dir orphaned by a crash — always removable.
35
+ await fsp.rm(full, { recursive: true, force: true }).catch(() => { });
36
+ removed.push(full);
37
+ }
38
+ else {
39
+ // Promoted upload dir. A successfully-analyzed (registered) repo always
40
+ // has a `.gitnexus` index inside it; a stale promoted dir WITHOUT one is
41
+ // an orphan from an analysis that failed before registering — remove it.
42
+ const hasIndex = await fsp
43
+ .access(path.join(full, '.gitnexus'))
44
+ .then(() => true)
45
+ .catch(() => false);
46
+ if (!hasIndex) {
47
+ await fsp.rm(full, { recursive: true, force: true }).catch(() => { });
48
+ removed.push(full);
49
+ }
50
+ }
51
+ }
52
+ catch {
53
+ /* stat race — skip */
54
+ }
55
+ }
56
+ return { removed };
57
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Branch-index primitives (#2106).
3
+ *
4
+ * Extracted from `repo-manager.ts` to keep the multi-branch slug/placement
5
+ * logic in one focused module. `getStoragePaths`, `loadMeta`, and the registry
6
+ * I/O stay in `repo-manager.ts`; this module imports the two it needs at
7
+ * call-time only (no module-load cross-calls), so the repo-manager ⇄
8
+ * branch-index import cycle is ESM-safe. `repo-manager.ts` re-exports these so
9
+ * existing import sites keep working unchanged.
10
+ */
11
+ import { type RepoMeta } from './repo-manager.js';
12
+ /**
13
+ * Per-branch index summary nested under a registry entry (#2106). Records
14
+ * non-primary branches indexed for the same repo path so `list`, `status`, and
15
+ * `list_repos` can surface them without a second registry entry.
16
+ */
17
+ export interface BranchSummary {
18
+ /** Git branch name this sub-index represents. */
19
+ branch: string;
20
+ indexedAt: string;
21
+ lastCommit: string;
22
+ stats?: RepoMeta['stats'];
23
+ }
24
+ /** Branch-index sub-directory name, relative to the flat `.gitnexus` storage. */
25
+ export declare const BRANCHES_DIR = "branches";
26
+ /**
27
+ * Filesystem-safe slug for a git branch ref (#2106).
28
+ *
29
+ * `sanitizeRepoName` alone is lossy — it maps `/`→`_`, so `feature/x` and
30
+ * `feature_x` would collide into the same directory. We append a short sha256
31
+ * of the RAW ref (mirroring `assignRepoId`'s digest fallback) so two distinct
32
+ * refs can never share a branch directory, while keeping the human prefix
33
+ * readable.
34
+ */
35
+ export declare const branchSlug: (rawRef: string) => string;
36
+ /**
37
+ * Decide where a freshly-analyzed branch's index lives: the flat (primary) slot
38
+ * or a per-branch sub-directory (#2106 KTD2).
39
+ *
40
+ * Returns `{}` for the flat/primary placement (byte-identical layout) or
41
+ * `{ branch }` for a `branches/<slug>/` sub-directory. The flat slot is owned by
42
+ * the FIRST branch indexed, recorded as `branch` in the flat `meta.json`; a
43
+ * different checked-out branch then auto-routes to its own sub-directory so it
44
+ * never overwrites the primary index.
45
+ *
46
+ * `label` is the resolved index-branch (explicit `--branch`, else the
47
+ * checked-out branch, else `null`). A `null` label — detached HEAD, non-git
48
+ * folder, or CI checkout — always maps to the flat slot.
49
+ */
50
+ export declare const resolveBranchPlacement: (repoPath: string, label: string | null) => Promise<{
51
+ branch?: string;
52
+ }>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Branch-index primitives (#2106).
3
+ *
4
+ * Extracted from `repo-manager.ts` to keep the multi-branch slug/placement
5
+ * logic in one focused module. `getStoragePaths`, `loadMeta`, and the registry
6
+ * I/O stay in `repo-manager.ts`; this module imports the two it needs at
7
+ * call-time only (no module-load cross-calls), so the repo-manager ⇄
8
+ * branch-index import cycle is ESM-safe. `repo-manager.ts` re-exports these so
9
+ * existing import sites keep working unchanged.
10
+ */
11
+ import { createHash } from 'crypto';
12
+ import { sanitizeRepoName } from './git.js';
13
+ import { getStoragePaths, loadMeta } from './repo-manager.js';
14
+ /** Branch-index sub-directory name, relative to the flat `.gitnexus` storage. */
15
+ export const BRANCHES_DIR = 'branches';
16
+ /**
17
+ * Filesystem-safe slug for a git branch ref (#2106).
18
+ *
19
+ * `sanitizeRepoName` alone is lossy — it maps `/`→`_`, so `feature/x` and
20
+ * `feature_x` would collide into the same directory. We append a short sha256
21
+ * of the RAW ref (mirroring `assignRepoId`'s digest fallback) so two distinct
22
+ * refs can never share a branch directory, while keeping the human prefix
23
+ * readable.
24
+ */
25
+ export const branchSlug = (rawRef) => {
26
+ const safe = sanitizeRepoName(rawRef);
27
+ const hash = createHash('sha256').update(rawRef).digest('hex').slice(0, 8);
28
+ return `${safe}-${hash}`;
29
+ };
30
+ /**
31
+ * Decide where a freshly-analyzed branch's index lives: the flat (primary) slot
32
+ * or a per-branch sub-directory (#2106 KTD2).
33
+ *
34
+ * Returns `{}` for the flat/primary placement (byte-identical layout) or
35
+ * `{ branch }` for a `branches/<slug>/` sub-directory. The flat slot is owned by
36
+ * the FIRST branch indexed, recorded as `branch` in the flat `meta.json`; a
37
+ * different checked-out branch then auto-routes to its own sub-directory so it
38
+ * never overwrites the primary index.
39
+ *
40
+ * `label` is the resolved index-branch (explicit `--branch`, else the
41
+ * checked-out branch, else `null`). A `null` label — detached HEAD, non-git
42
+ * folder, or CI checkout — always maps to the flat slot.
43
+ */
44
+ export const resolveBranchPlacement = async (repoPath, label) => {
45
+ // Detached HEAD / non-git / no label → flat (CI-safe, byte-identical).
46
+ if (!label)
47
+ return {};
48
+ const { storagePath } = getStoragePaths(repoPath);
49
+ const flatMeta = await loadMeta(storagePath);
50
+ // The flat slot's owner is authoritative ONLY when it is a non-empty string.
51
+ // A corrupt/hand-edited meta (empty string, or a non-string value that slips
52
+ // past JSON typing) must not be trusted to route the real primary into a
53
+ // sub-directory (#2106 review R5).
54
+ const owner = flatMeta && typeof flatMeta.branch === 'string' && flatMeta.branch.length > 0
55
+ ? flatMeta.branch
56
+ : undefined;
57
+ // Fresh repo (no flat index) or legacy/unstamped flat index (no recorded
58
+ // owner): the current label claims/adopts the flat slot. The legacy case
59
+ // preserves today's overwrite-in-place behavior until the slot is stamped.
60
+ if (!owner)
61
+ return {};
62
+ // Flat slot is owned. Same branch → flat; otherwise this branch gets its own
63
+ // sub-directory.
64
+ return owner === label ? {} : { branch: label };
65
+ };
@@ -118,6 +118,17 @@ export declare const getRemoteOriginUrl: (repoPath: string) => string | null;
118
118
  * git is unavailable, so callers can fall back to a configured/default branch.
119
119
  */
120
120
  export declare const getDefaultBranch: (repoPath: string) => string | null;
121
+ /**
122
+ * Name of the currently checked-out branch, or `null` when HEAD is detached
123
+ * (CI checkouts, `git checkout <sha>`), the directory is not a git worktree, or
124
+ * git is unavailable.
125
+ *
126
+ * `git rev-parse --abbrev-ref HEAD` prints the literal `HEAD` for a detached
127
+ * checkout. We map that (and empty output) to `null` so callers fall back to the
128
+ * flat/default index rather than ever creating a branch literally named
129
+ * "HEAD" (#2106).
130
+ */
131
+ export declare const getCurrentBranch: (repoPath: string) => string | null;
121
132
  /**
122
133
  * Sanitize a repository name to prevent argument injection and ensure
123
134
  * cross-platform filesystem compatibility.
@@ -295,6 +295,34 @@ export const getDefaultBranch = (repoPath) => {
295
295
  return null;
296
296
  }
297
297
  };
298
+ /**
299
+ * Name of the currently checked-out branch, or `null` when HEAD is detached
300
+ * (CI checkouts, `git checkout <sha>`), the directory is not a git worktree, or
301
+ * git is unavailable.
302
+ *
303
+ * `git rev-parse --abbrev-ref HEAD` prints the literal `HEAD` for a detached
304
+ * checkout. We map that (and empty output) to `null` so callers fall back to the
305
+ * flat/default index rather than ever creating a branch literally named
306
+ * "HEAD" (#2106).
307
+ */
308
+ export const getCurrentBranch = (repoPath) => {
309
+ try {
310
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
311
+ cwd: repoPath,
312
+ // Suppress stderr -- see getCurrentCommit comment and #1172.
313
+ stdio: ['ignore', 'pipe', 'ignore'],
314
+ windowsHide: true,
315
+ })
316
+ .toString()
317
+ .trim();
318
+ if (!branch || branch === 'HEAD')
319
+ return null;
320
+ return branch;
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ };
298
326
  /**
299
327
  * Sanitize a repository name to prevent argument injection and ensure
300
328
  * cross-platform filesystem compatibility.
@@ -53,10 +53,31 @@ export declare const fileContentHash: (content: Buffer | string) => string;
53
53
  * in the chunk. We sort by filePath before hashing so chunks composed of
54
54
  * the same files in different order produce the same key.
55
55
  */
56
+ /** PDG/CFG cache namespace (#2081 M1) — every input that changes the
57
+ * WORKER-EMITTED `cfgSideChannel` must be folded into the chunk key, and
58
+ * ONLY those. The classification test for a future option: does the worker
59
+ * see it (workerData) and does it change the bytes the worker writes to the
60
+ * shard? `pdgMaxEdgesPerFunction` famously fails that test — it is applied
61
+ * at EMIT time on the main thread (scope-resolution run.ts), the worker
62
+ * never receives it, and the cached output is byte-identical across cap
63
+ * values; folding it in (as a prior review round did) only forced a
64
+ * spurious full re-parse on every cap change (#2099 F3). Options that
65
+ * change the PERSISTED GRAPH but not the shard belong in the RepoMeta pdg
66
+ * stamp (incremental-eligibility), not here. */
67
+ export interface PdgCacheKey {
68
+ readonly pdg?: boolean;
69
+ /** Per-function source-line cap (changes WHICH functions get a CFG —
70
+ * applied in the worker, so it shapes the cached shard). Callers must
71
+ * pass the RESOLVED value (the production call site in parse-impl.ts
72
+ * applies the worker's default before folding) so an explicit-default
73
+ * run shares the default run's keys — this function folds whatever it
74
+ * is given verbatim. */
75
+ readonly maxFunctionLines?: number;
76
+ }
56
77
  export declare const computeChunkHash: (entries: Array<{
57
78
  filePath: string;
58
79
  contentHash: string;
59
- }>) => string;
80
+ }>, pdg?: boolean | PdgCacheKey) => string;
60
81
  export declare const mapReplacer: (_key: string, value: unknown) => unknown;
61
82
  export declare const mapReviver: (_key: string, value: unknown) => unknown;
62
83
  /**
@@ -52,7 +52,7 @@ import { fileURLToPath } from 'url';
52
52
  // the main thread (the #1983 OOM). Because the two stores share this version,
53
53
  // any future change to the `ParsedFile` serialization shape MUST bump
54
54
  // SCHEMA_BUMP so both invalidate in lockstep.
55
- const SCHEMA_BUMP = 4;
55
+ const SCHEMA_BUMP = 6; // #2082 M2: cfgSideChannel gained bindings + per-block statement facts
56
56
  const GITNEXUS_PKG_VERSION = (() => {
57
57
  try {
58
58
  // package.json sits at gitnexus/package.json — two levels up from
@@ -92,17 +92,36 @@ const sha256Hex = (input) => createHash('sha256')
92
92
  .digest('hex');
93
93
  /** Stable hash of a single file's contents — used by callers to compose a chunk hash. */
94
94
  export const fileContentHash = (content) => sha256Hex(content);
95
- /**
96
- * Compute the canonical cache key for a chunk's contents.
97
- *
98
- * `entries` is the list of (filePath, file content hash) for every file
99
- * in the chunk. We sort by filePath before hashing so chunks composed of
100
- * the same files in different order produce the same key.
101
- */
102
- export const computeChunkHash = (entries) => {
95
+ export const computeChunkHash = (entries, pdg = false) => {
103
96
  const sorted = [...entries].sort((a, b) => (a.filePath < b.filePath ? -1 : 1));
104
97
  const joined = sorted.map((e) => `${e.filePath}:${e.contentHash}`).join('\n');
105
- return sha256Hex(joined);
98
+ const opts = typeof pdg === 'boolean' ? { pdg } : pdg;
99
+ // pdg-off path keeps its pre-#2081 chunk-KEY format verbatim. Note this does
100
+ // NOT mean caches survive the M1 upgrade: SCHEMA_BUMP 4→5 changed
101
+ // PARSE_CACHE_VERSION, and both loadParseCache (below) and the durable
102
+ // parsedfile-store index hard-invalidate on it — every user pays one full
103
+ // cold re-parse on upgrade regardless of --pdg. Keeping the key format
104
+ // stable only means no SECOND invalidation class is introduced here.
105
+ if (!opts.pdg)
106
+ return sha256Hex(joined);
107
+ // Fold the worker-visible --pdg configuration into the key: the boolean
108
+ // plus `maxFunctionLines` (decides which functions get a CFG at all, in the
109
+ // worker). Without it a warm chunk built under one cap is served to a run
110
+ // with a different cap → a stale/under-built CFG: the #2038-class
111
+ // option-blind-key trap. `def` marks an unset (default) value so two
112
+ // default-cap runs share a key. The emit-time edge cap is deliberately
113
+ // absent — see the PdgCacheKey doc comment.
114
+ //
115
+ // NAMESPACE VERSION (`pdg:2`): bumped when the worker-emitted
116
+ // `cfgSideChannel` SHAPE changes for pdg-mode runs only — pdg:1→2 in #2083
117
+ // M3 U1 (TsHarvester emits taint `sites` on StatementFacts). Invalidates
118
+ // pdg-mode chunks and their durable parsedfile-cache entries; flag-off
119
+ // chunk keys never reach this line and stay byte-identical, so non-pdg
120
+ // users pay nothing. Deliberately NOT a SCHEMA_BUMP — that gates the whole
121
+ // cache version and would force a full cold re-parse on EVERY user (the M1
122
+ // bump comment above records that cost).
123
+ const ns = `pdg:2;maxFn=${opts.maxFunctionLines ?? 'def'}`;
124
+ return sha256Hex(`${ns}\n${joined}`);
106
125
  };
107
126
  /**
108
127
  * JSON replacer that round-trips Map/Set instances through plain JSON.
@@ -149,6 +168,9 @@ export const slimParseWorkerResultsForCache = (chunkResults) => {
149
168
  assignments: [],
150
169
  constructorBindings: [],
151
170
  parsedFiles: [],
171
+ // #2112: a clone-safety skip list is per-run telemetry, not graph data —
172
+ // replay ignores it. Drop it so it doesn't bloat the cached shard.
173
+ skippedPaths: [],
152
174
  });
153
175
  }
154
176
  return slim;
@@ -5,6 +5,9 @@
5
5
  * Also maintains a global registry at ~/.gitnexus/registry.json
6
6
  * so the MCP server can discover indexed repos from any cwd.
7
7
  */
8
+ import { branchSlug, resolveBranchPlacement, type BranchSummary } from './branch-index.js';
9
+ export { branchSlug, resolveBranchPlacement };
10
+ export type { BranchSummary };
8
11
  /**
9
12
  * Normalise a repo path for registry comparison across platforms
10
13
  * (#664 review feedback from @evander-wang).
@@ -71,18 +74,85 @@ export interface RepoMeta {
71
74
  */
72
75
  fileHashes?: Record<string, string>;
73
76
  /**
74
- * Crash-recovery dirty flag. Written to meta.json BEFORE any
75
- * destructive DB mutation in an incremental run; cleared on success
76
- * by overwriting meta.json. If a run crashes between, the next run
77
- * sees the flag and forces a full rebuild the cheapest path back
78
- * to a known-good index.
77
+ * Crash-recovery dirty flag a generic marker written to meta.json
78
+ * BEFORE any destructive DB mutation by BOTH writeback branches
79
+ * (incremental since its introduction; full rebuilds over an existing
80
+ * meta since #2099 F1); cleared on success by overwriting meta.json.
81
+ * If a run crashes between, the next run sees the flag and forces a
82
+ * full rebuild — the cheapest path back to a known-good index.
79
83
  */
80
84
  incrementalInProgress?: {
81
- /** When the incremental run started (epoch ms). */
85
+ /** When the run started (epoch ms). */
82
86
  startedAt: number;
83
- /** Number of files in the writable set, for diagnostic logs. */
87
+ /** Number of files in the writable set, for diagnostic logs.
88
+ * `0` on the full-rebuild path (no incremental write set exists). */
84
89
  toWriteCount: number;
85
90
  };
91
+ /**
92
+ * Name of the git branch this index represents (#2106). Absent for the
93
+ * default/legacy single-branch case so the flat `meta.json` stays
94
+ * byte-identical to pre-multi-branch output. When present in the FLAT
95
+ * `meta.json`, it records which branch "owns" the flat slot (the first
96
+ * branch indexed); per-branch indexes under `branches/<slug>/` always carry
97
+ * their own `branch`.
98
+ */
99
+ branch?: string;
100
+ /**
101
+ * The parse-cache chunk keys this branch's index needs (#2106 R6). The
102
+ * parse-cache and durable parsedfile store live ONCE at the repo root and are
103
+ * shared across branches; recording each branch's live chunk keys lets the
104
+ * prune step union them so re-analyzing one branch doesn't evict another
105
+ * branch's still-live shards. Additive/optional; absent in legacy metas.
106
+ */
107
+ cacheKeys?: string[];
108
+ /**
109
+ * The effective `--pdg` configuration this index's DB rows were built
110
+ * under (#2099 F1). Presence ≡ the BasicBlock/CFG layer exists in the DB;
111
+ * ABSENT ≡ pdg-off — which covers every legacy meta, since `--pdg`
112
+ * shipped opt-in. Caps are recorded RESOLVED (defaults applied) so an
113
+ * explicit-default run compares equal to a default run. run-analyze
114
+ * compares this against the requested options and forces a full
115
+ * writeback on any mismatch — the incremental path only persists
116
+ * changed-file nodes and would otherwise silently drop (or strand) the
117
+ * CFG layer on a mode flip. Additive/optional, no
118
+ * INCREMENTAL_SCHEMA_VERSION bump (a bump would force a one-time full
119
+ * rebuild for every user). NOTE the removal mechanism is load-bearing:
120
+ * the end-of-run meta is a fresh object literal, NOT a spread of the
121
+ * prior meta, so omitting this field on a pdg-off run is what clears
122
+ * the stamp after an on→off flip.
123
+ */
124
+ pdg?: {
125
+ /** Worker-side per-function source-line cap, resolved (0 = unlimited). */
126
+ maxFunctionLines: number;
127
+ /** Emit-side per-function CFG edge cap, resolved (0 = unlimited). */
128
+ maxEdgesPerFunction: number;
129
+ /**
130
+ * Emit-side per-function REACHING_DEF edge cap, resolved (0 = unlimited;
131
+ * #2082 M2). ABSENT on an M1-era stamp — which is exactly what makes
132
+ * `pdgModeMismatch` trip on the first M2 run over an M1 index and force
133
+ * the full writeback that populates REACHING_DEF rows. Optional in the
134
+ * type for that reason; resolved (always present) on every M2+ write.
135
+ */
136
+ maxReachingDefEdgesPerFunction?: number;
137
+ /**
138
+ * Per-function taint findings cap, resolved (0 = unlimited; #2083 M3).
139
+ * ABSENT on an M1/M2-era stamp — like `maxReachingDefEdgesPerFunction`,
140
+ * that absence is what trips `pdgModeMismatch` on the first M3 run and
141
+ * forces the full writeback that populates TAINTED/SANITIZES rows.
142
+ */
143
+ maxTaintFindingsPerFunction?: number;
144
+ /** Per-finding taint hop cap, resolved (0 = unlimited; #2083 M3 KTD6 —
145
+ * bounds the persisted hop-encoded `reason`). Optional for the same
146
+ * M2-era-stamp upgrade reason as the findings cap. */
147
+ maxTaintHops?: number;
148
+ /**
149
+ * Digest of the built-in taint model the persisted findings were
150
+ * produced under (#2083 M3 KTD7/R7). Any model-content change ships a
151
+ * new digest → mismatch → full writeback repopulates taint edges
152
+ * without `--force`. Optional: absent on pre-M3 stamps.
153
+ */
154
+ taintModelVersion?: string;
155
+ };
86
156
  }
87
157
  /**
88
158
  * Bumped whenever incremental-indexing invariants change incompatibly.
@@ -107,15 +177,33 @@ export interface RegistryEntry {
107
177
  /** See {@link RepoMeta.remoteUrl}. Mirrored from meta at register time. */
108
178
  remoteUrl?: string;
109
179
  stats?: RepoMeta['stats'];
180
+ /**
181
+ * Branch name owning the flat/primary index (#2106). Mirrors the flat
182
+ * `meta.branch`. Absent for legacy single-branch entries and non-git repos —
183
+ * additive and backward compatible.
184
+ */
185
+ branch?: string;
186
+ /**
187
+ * Non-primary branch indexes for this same path (#2106). Absent when only the
188
+ * primary branch is indexed, preserving the one-entry-per-path model and the
189
+ * legacy registry shape.
190
+ */
191
+ branches?: BranchSummary[];
110
192
  }
111
193
  /**
112
194
  * Get the .gitnexus storage path for a repository
113
195
  */
114
196
  export declare const getStoragePath: (repoPath: string) => string;
115
197
  /**
116
- * Get paths to key storage files
198
+ * Get paths to key storage files.
199
+ *
200
+ * `storagePath` is ALWAYS the flat `<repo>/.gitnexus` — content-addressed
201
+ * caches (`parse-cache/`, `parsedfile-store/`) live there and are shared
202
+ * across branches (#2106 KTD7). When `branch` is provided, only `lbugPath` and
203
+ * `metaPath` are scoped under `branches/<slug>/`; the flat call (no `branch`)
204
+ * returns byte-identical paths to the pre-multi-branch behavior.
117
205
  */
118
- export declare const getStoragePaths: (repoPath: string) => {
206
+ export declare const getStoragePaths: (repoPath: string, branch?: string) => {
119
207
  storagePath: string;
120
208
  lbugPath: string;
121
209
  metaPath: string;
@@ -213,6 +301,14 @@ export interface RegisterRepoOptions {
213
301
  * re-run the full pipeline.
214
302
  */
215
303
  allowDuplicateName?: boolean;
304
+ /**
305
+ * Non-primary branch this run indexed (#2106). When set, the branch's
306
+ * summary is upserted into the entry's `branches[]` and the primary
307
+ * top-level fields are left untouched. When `undefined`, this is a
308
+ * primary/flat run that refreshes the top-level fields (and preserves any
309
+ * existing branch summaries).
310
+ */
311
+ branch?: string;
216
312
  }
217
313
  /**
218
314
  * Thrown by {@link registerRepo} when a requested name is already in
@@ -262,6 +358,15 @@ export declare const registerRepo: (repoPath: string, meta: RepoMeta, opts?: Reg
262
358
  * Called after `gitnexus clean`.
263
359
  */
264
360
  export declare const unregisterRepo: (repoPath: string) => Promise<void>;
361
+ /**
362
+ * Remove a single non-primary branch's summary from a repo's registry entry
363
+ * (#2106 R7). Called by `gitnexus clean --branch`. Returns `true` when a
364
+ * matching `branches[]` summary was found and removed; `false` otherwise (so
365
+ * the CLI can report "no such indexed branch" without crashing). The top-level
366
+ * primary entry is left intact; an empty `branches[]` is dropped to keep the
367
+ * registry shape legacy-clean.
368
+ */
369
+ export declare const removeBranchIndex: (repoPath: string, branch: string) => Promise<boolean>;
265
370
  /**
266
371
  * Thrown by {@link resolveRegistryEntry} when no registered repo matches
267
372
  * the caller's target string (by alias, basename, remote-inferred name,
@@ -404,7 +509,14 @@ export declare const assertSafeStoragePath: (entry: RegistryEntry) => void;
404
509
  export declare const resolveRegistryEntry: (entries: RegistryEntry[], target: string) => RegistryEntry;
405
510
  /**
406
511
  * List all registered repos from the global registry.
407
- * Optionally validates that each entry's .gitnexus/ still exists.
512
+ *
513
+ * With `validate: true`, prunes only entries whose index is *provably* gone
514
+ * (fs.access on .gitnexus/meta.json fails with ENOENT or ENOTDIR) and persists
515
+ * the result. Entries that are merely "not provably absent" — any other
516
+ * fs.access failure (EIO/EAGAIN/EBUSY/EACCES, etc.) — are KEPT, so a transient
517
+ * I/O storm cannot wipe the registry. A kept entry is therefore "not confirmed
518
+ * present," not "confirmed present"; downstream DB opens are independently and
519
+ * lazily guarded.
408
520
  */
409
521
  export declare const listRegisteredRepos: (opts?: {
410
522
  validate?: boolean;