gitnexus 1.6.12-rc.2 → 1.6.12-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 (188) hide show
  1. package/dist/_shared/graph/types.d.ts +1 -1
  2. package/dist/_shared/graph/types.d.ts.map +1 -1
  3. package/dist/_shared/language-detection.d.ts.map +1 -1
  4. package/dist/_shared/language-detection.js +2 -0
  5. package/dist/_shared/language-detection.js.map +1 -1
  6. package/dist/_shared/languages.d.ts +1 -0
  7. package/dist/_shared/languages.d.ts.map +1 -1
  8. package/dist/_shared/languages.js +1 -0
  9. package/dist/_shared/languages.js.map +1 -1
  10. package/dist/_shared/lbug/schema-constants.d.ts +1 -1
  11. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  12. package/dist/_shared/lbug/schema-constants.js +2 -0
  13. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  14. package/dist/_shared/scope-resolution/language-classification.d.ts +2 -1
  15. package/dist/_shared/scope-resolution/language-classification.d.ts.map +1 -1
  16. package/dist/_shared/scope-resolution/language-classification.js +3 -1
  17. package/dist/_shared/scope-resolution/language-classification.js.map +1 -1
  18. package/dist/_shared/scope-resolution/registries/class-registry.d.ts +3 -3
  19. package/dist/_shared/scope-resolution/registries/class-registry.js +3 -3
  20. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
  21. package/dist/_shared/scope-resolution/registries/context.js +2 -0
  22. package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
  23. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
  24. package/dist/_shared/scope-resolution/resolve-type-ref.js +2 -0
  25. package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
  26. package/dist/cli/analyze-config.d.ts +2 -2
  27. package/dist/cli/analyze-config.js +10 -33
  28. package/dist/cli/doctor.js +6 -4
  29. package/dist/cli/eval-server.js +13 -1
  30. package/dist/cli/i18n/en.d.ts +4 -2
  31. package/dist/cli/i18n/en.js +4 -2
  32. package/dist/cli/i18n/resources.d.ts +6 -2
  33. package/dist/cli/i18n/zh-CN.d.ts +2 -0
  34. package/dist/cli/i18n/zh-CN.js +4 -2
  35. package/dist/core/analysis-feature-registry.d.ts +1 -1
  36. package/dist/core/analysis-feature-registry.js +2 -0
  37. package/dist/core/auto-sync/starter.js +2 -2
  38. package/dist/core/embeddings/ast-utils.js +8 -3
  39. package/dist/core/embeddings/chunker.js +67 -10
  40. package/dist/core/embeddings/embedding-pipeline.d.ts +1 -1
  41. package/dist/core/embeddings/embedding-pipeline.js +1 -1
  42. package/dist/core/embeddings/structural-extractor.js +2 -2
  43. package/dist/core/embeddings/types.d.ts +4 -2
  44. package/dist/core/embeddings/types.js +18 -0
  45. package/dist/core/git-ref.d.ts +22 -0
  46. package/dist/core/git-ref.js +114 -0
  47. package/dist/core/git-staleness.js +14 -0
  48. package/dist/core/group/extractors/graphql-extractor.d.ts +1 -1
  49. package/dist/core/group/extractors/graphql-extractor.js +149 -13
  50. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  51. package/dist/core/group/extractors/manifest-extractor.js +2 -2
  52. package/dist/core/ingestion/call-extractors/zig-static-gating.d.ts +5 -8
  53. package/dist/core/ingestion/content-language-classification.d.ts +10 -0
  54. package/dist/core/ingestion/content-language-classification.js +30 -0
  55. package/dist/core/ingestion/filesystem-walker.d.ts +1 -0
  56. package/dist/core/ingestion/filesystem-walker.js +1 -1
  57. package/dist/core/ingestion/import-resolvers/node-workspace-packages.js +3 -2
  58. package/dist/core/ingestion/language-config.d.ts +82 -13
  59. package/dist/core/ingestion/language-config.js +217 -13
  60. package/dist/core/ingestion/language-provider.d.ts +88 -1
  61. package/dist/core/ingestion/languages/index.d.ts +7 -0
  62. package/dist/core/ingestion/languages/index.js +30 -1
  63. package/dist/core/ingestion/languages/objective-c/analysis-features.d.ts +10 -0
  64. package/dist/core/ingestion/languages/objective-c/analysis-features.js +19 -0
  65. package/dist/core/ingestion/languages/objective-c/compilation-unit-siblings.d.ts +13 -0
  66. package/dist/core/ingestion/languages/objective-c/compilation-unit-siblings.js +124 -0
  67. package/dist/core/ingestion/languages/objective-c/facts.d.ts +148 -0
  68. package/dist/core/ingestion/languages/objective-c/facts.js +1150 -0
  69. package/dist/core/ingestion/languages/objective-c/import-target.d.ts +11 -0
  70. package/dist/core/ingestion/languages/objective-c/import-target.js +122 -0
  71. package/dist/core/ingestion/languages/objective-c/macro-marker-preprocess.d.ts +24 -0
  72. package/dist/core/ingestion/languages/objective-c/macro-marker-preprocess.js +261 -0
  73. package/dist/core/ingestion/languages/objective-c/resolution-config.d.ts +41 -0
  74. package/dist/core/ingestion/languages/objective-c/resolution-config.js +282 -0
  75. package/dist/core/ingestion/languages/objective-c/scope-resolver.d.ts +2 -0
  76. package/dist/core/ingestion/languages/objective-c/scope-resolver.js +503 -0
  77. package/dist/core/ingestion/languages/objective-c.d.ts +2 -0
  78. package/dist/core/ingestion/languages/objective-c.js +300 -0
  79. package/dist/core/ingestion/languages/typescript/query.d.ts +31 -0
  80. package/dist/core/ingestion/languages/typescript/query.js +7 -1
  81. package/dist/core/ingestion/languages/typescript/tsconfig.js +3 -2
  82. package/dist/core/ingestion/languages/typescript.js +1 -1
  83. package/dist/core/ingestion/languages/zig/query.d.ts +26 -0
  84. package/dist/core/ingestion/languages/zig/query.js +61 -1
  85. package/dist/core/ingestion/languages/zig/range-binding.d.ts +8 -0
  86. package/dist/core/ingestion/languages/zig/range-binding.js +24 -0
  87. package/dist/core/ingestion/languages/zig/scope-resolver.d.ts +3 -2
  88. package/dist/core/ingestion/languages/zig/scope-resolver.js +18 -5
  89. package/dist/core/ingestion/languages/zig/this-alias-bindings.d.ts +58 -0
  90. package/dist/core/ingestion/languages/zig/this-alias-bindings.js +176 -0
  91. package/dist/core/ingestion/languages/zig/workspace-static-gating.d.ts +8 -0
  92. package/dist/core/ingestion/languages/zig/workspace-static-gating.js +82 -0
  93. package/dist/core/ingestion/model/registration-table.d.ts +3 -3
  94. package/dist/core/ingestion/model/registration-table.js +9 -5
  95. package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
  96. package/dist/core/ingestion/model/symbol-table.js +2 -0
  97. package/dist/core/ingestion/model/type-registry.d.ts +1 -1
  98. package/dist/core/ingestion/parsing-processor.d.ts +22 -6
  99. package/dist/core/ingestion/parsing-processor.js +39 -20
  100. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +9 -1
  101. package/dist/core/ingestion/pipeline-phases/parse-impl.js +307 -96
  102. package/dist/core/ingestion/pipeline-phases/parse-round-budget.d.ts +42 -0
  103. package/dist/core/ingestion/pipeline-phases/parse-round-budget.js +50 -0
  104. package/dist/core/ingestion/pipeline-phases/parse.d.ts +7 -1
  105. package/dist/core/ingestion/scope-extractor.js +4 -0
  106. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +13 -0
  107. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +2 -0
  108. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +2 -0
  109. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +78 -5
  110. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +262 -7
  111. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +5 -2
  112. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  113. package/dist/core/ingestion/scope-resolution/pipeline/run.js +10 -1
  114. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +39 -3
  115. package/dist/core/ingestion/scope-resolution/scope/walkers.js +82 -3
  116. package/dist/core/ingestion/scope-resolution/value-ref-edges.d.ts +14 -0
  117. package/dist/core/ingestion/scope-resolution/value-ref-edges.js +14 -0
  118. package/dist/core/ingestion/tree-sitter-queries.js +2 -0
  119. package/dist/core/ingestion/utils/symbol-labels.js +2 -0
  120. package/dist/core/ingestion/workers/parse-worker.d.ts +1 -1
  121. package/dist/core/ingestion/workers/parse-worker.js +63 -3
  122. package/dist/core/ingestion/workers/worker-pool.d.ts +56 -2
  123. package/dist/core/ingestion/workers/worker-pool.js +117 -26
  124. package/dist/core/lbug/csv-generator.js +2 -0
  125. package/dist/core/lbug/schema.d.ts +3 -1
  126. package/dist/core/lbug/schema.js +27 -0
  127. package/dist/core/search/fts-indexes.js +13 -4
  128. package/dist/core/search/fts-schema.js +2 -0
  129. package/dist/core/tree-sitter/parser-loader.js +10 -0
  130. package/dist/core/tree-sitter/vendored-grammars.d.ts +1 -1
  131. package/dist/core/tree-sitter/vendored-grammars.js +2 -1
  132. package/dist/mcp/local/local-backend.d.ts +33 -0
  133. package/dist/mcp/local/local-backend.js +166 -5
  134. package/dist/mcp/tools.js +6 -4
  135. package/dist/server/analyze-job.d.ts +19 -1
  136. package/dist/server/analyze-job.js +14 -3
  137. package/dist/server/analyze-launch.d.ts +14 -0
  138. package/dist/server/analyze-launch.js +59 -14
  139. package/dist/server/analyze-worker-ipc.d.ts +6 -5
  140. package/dist/server/analyze-worker-ipc.js +4 -0
  141. package/dist/server/api.js +68 -21
  142. package/dist/server/git-clone.d.ts +38 -5
  143. package/dist/server/git-clone.js +216 -38
  144. package/dist/server/repo-projection.d.ts +88 -0
  145. package/dist/server/repo-projection.js +53 -0
  146. package/dist/storage/file-lock.js +5 -3
  147. package/dist/storage/parse-cache.js +32 -1
  148. package/dist/storage/parsedfile-store.js +53 -1
  149. package/dist/storage/v8-sidecar.d.ts +11 -0
  150. package/dist/storage/v8-sidecar.js +15 -7
  151. package/dist/utils/process-identity.d.ts +13 -0
  152. package/dist/utils/process-identity.js +18 -0
  153. package/package.json +1 -1
  154. package/scripts/build-tree-sitter-grammars.cjs +4 -3
  155. package/vendor/tree-sitter-objc/LICENSE +21 -0
  156. package/vendor/tree-sitter-objc/README.md +18 -0
  157. package/vendor/tree-sitter-objc/binding.gyp +35 -0
  158. package/vendor/tree-sitter-objc/bindings/node/binding.cc +19 -0
  159. package/vendor/tree-sitter-objc/bindings/node/binding_test.js +9 -0
  160. package/vendor/tree-sitter-objc/bindings/node/index.d.ts +27 -0
  161. package/vendor/tree-sitter-objc/bindings/node/index.js +11 -0
  162. package/vendor/tree-sitter-objc/grammar.js +1286 -0
  163. package/vendor/tree-sitter-objc/package.json +56 -0
  164. package/vendor/tree-sitter-objc/prebuilds/SHA256SUMS +6 -0
  165. package/vendor/tree-sitter-objc/prebuilds/darwin-arm64/tree-sitter-objc.node +0 -0
  166. package/vendor/tree-sitter-objc/prebuilds/darwin-x64/tree-sitter-objc.node +0 -0
  167. package/vendor/tree-sitter-objc/prebuilds/linux-arm64/tree-sitter-objc.node +0 -0
  168. package/vendor/tree-sitter-objc/prebuilds/linux-x64/tree-sitter-objc.node +0 -0
  169. package/vendor/tree-sitter-objc/prebuilds/win32-arm64/tree-sitter-objc.node +0 -0
  170. package/vendor/tree-sitter-objc/prebuilds/win32-x64/tree-sitter-objc.node +0 -0
  171. package/vendor/tree-sitter-objc/queries/folds.scm +20 -0
  172. package/vendor/tree-sitter-objc/queries/highlights.scm +216 -0
  173. package/vendor/tree-sitter-objc/queries/indents.scm +1 -0
  174. package/vendor/tree-sitter-objc/queries/injections.scm +10 -0
  175. package/vendor/tree-sitter-objc/queries/locals.scm +1 -0
  176. package/vendor/tree-sitter-objc/src/grammar.json +16292 -0
  177. package/vendor/tree-sitter-objc/src/node-types.json +7524 -0
  178. package/vendor/tree-sitter-objc/src/parser.c +684023 -0
  179. package/vendor/tree-sitter-objc/src/tree_sitter/alloc.h +54 -0
  180. package/vendor/tree-sitter-objc/src/tree_sitter/array.h +291 -0
  181. package/vendor/tree-sitter-objc/src/tree_sitter/parser.h +266 -0
  182. package/vendor/tree-sitter-objc/tree-sitter-objc.wasm +0 -0
  183. package/vendor/tree-sitter-objc/tree-sitter.json +38 -0
  184. package/web/assets/{agent-BMRqXorA.js → agent-C2n33ANd.js} +1 -1
  185. package/web/assets/{index-Dj3vQOHK.js → index-CHJoW_P1.js} +18 -18
  186. package/web/assets/src-jc8Ffy3T.js +1 -0
  187. package/web/index.html +2 -2
  188. package/web/assets/src-Df5C1Nz4.js +0 -1
@@ -10,6 +10,7 @@ import fs from 'fs/promises';
10
10
  import os from 'node:os';
11
11
  import { logger } from '../core/logger.js';
12
12
  import { getGlobalDir } from '../storage/repo-manager.js';
13
+ import { branchSlug } from '../storage/branch-index.js';
13
14
  import { sanitizeRepoName, stripUrlCredentials } from '../storage/git.js';
14
15
  import { validateGitUrl } from '../core/net/url-guard.js';
15
16
  import { assertDirectoryOwnerAndPermissions, quarantineAutoSyncPartial, } from '../core/auto-sync/path-security.js';
@@ -74,14 +75,83 @@ export function extractWebRepoName(url) {
74
75
  }
75
76
  return safeName;
76
77
  }
77
- /** Get the clone target directory for a repo name. */
78
- export function getCloneDir(repoName) {
78
+ /**
79
+ * Longest single path component the supported filesystems accept (ext4, APFS,
80
+ * NTFS all cap at 255). Compared against `.length`, which equals the byte
81
+ * count here because every name this guards is ASCII by construction
82
+ * (REPO_NAME_PATTERN and sanitizeRepoName both restrict to `[a-zA-Z0-9._-]`).
83
+ */
84
+ const MAX_PATH_COMPONENT_BYTES = 255;
85
+ /**
86
+ * `branchSlug` for a clone-directory name, trimmed to fit one path component.
87
+ *
88
+ * `validateBranchName` allows a ref up to 255 characters and `branchSlug`
89
+ * appends `-` plus 8 hash characters, so `<repo>__<slug>` can reach 267 — past
90
+ * the filesystem limit, and the clone would then fail to create its target
91
+ * directory (#3199 review).
92
+ *
93
+ * Only the READABLE half is trimmed; the 8-character hash is always kept, and
94
+ * it is a digest of the full ref, so two long branches that share a prefix
95
+ * still get different directories. The slug is not trimmed inside
96
+ * `branchSlug` itself because the per-branch *index* slots already use those
97
+ * names on disk — shortening them there would orphan existing indexes.
98
+ */
99
+ const boundedBranchSegment = (repoName, branch) => {
100
+ const slug = branchSlug(branch);
101
+ if (`${repoName}__${slug}`.length <= MAX_PATH_COMPONENT_BYTES)
102
+ return slug;
103
+ const hash = slug.slice(slug.lastIndexOf('-')); // "-" + 8 hex
104
+ const budget = MAX_PATH_COMPONENT_BYTES - repoName.length - '__'.length - hash.length;
105
+ // A repo name long enough to leave no budget falls through to the caller's
106
+ // length check, which rejects it rather than building an unusable path.
107
+ return `${slug.slice(0, Math.max(0, budget))}${hash}`;
108
+ };
109
+ /** Get the clone target directory for a repo name, optionally pinned to a branch. */
110
+ export function getCloneDir(repoName, branch) {
79
111
  // Re-validate at the boundary even though extractRepoName already checked —
80
112
  // callers may pass a repoName from another source (test fixtures, scripts).
81
113
  if (!repoName || repoName === '.' || repoName === '..' || !REPO_NAME_PATTERN.test(repoName)) {
82
114
  throw new Error('Invalid repository name');
83
115
  }
84
- return path.join(CLONE_ROOT, repoName);
116
+ // A branch-pinned analyze gets its OWN working tree.
117
+ //
118
+ // Sharing one checkout per repo made `branch` unusable in practice: the tree
119
+ // is dirty after any analyze (generated AGENTS.md / CLAUDE.md / .claude/), so
120
+ // a pinned request hit `cloneOrPull`'s porcelain refusal; and a later request
121
+ // that OMITTED `branch` would pull whatever branch the last pin left checked
122
+ // out and index it as the default (#3199 review). Separate directories remove
123
+ // both, because the two requests no longer share a tree.
124
+ //
125
+ // `branchSlug` is the same helper the per-branch index slots use, so the two
126
+ // layouts agree on how a ref becomes a path segment. It emits only
127
+ // `[a-zA-Z0-9._-]`, so the composed name still satisfies REPO_NAME_PATTERN and
128
+ // round-trips through this function — which is how DELETE /api/repo re-derives
129
+ // the directory from the registry name.
130
+ const dirName = branch ? `${repoName}__${boundedBranchSegment(repoName, branch)}` : repoName;
131
+ if (!REPO_NAME_PATTERN.test(dirName) || dirName.length > MAX_PATH_COMPONENT_BYTES) {
132
+ throw new Error('Invalid repository name');
133
+ }
134
+ return path.join(CLONE_ROOT, dirName);
135
+ }
136
+ /**
137
+ * Build the `cloneOrPull` options for an `/api/analyze` request.
138
+ *
139
+ * Extracted from the route so the token/branch combination is unit-testable.
140
+ * Inline, the branch-only case was the one nothing asserted: every existing
141
+ * test still passed if `branch` were dropped whenever no token was supplied —
142
+ * i.e. silently cloning the default branch for every public URL, which is the
143
+ * exact behavior #3198 is about (#3199 review).
144
+ *
145
+ * Returns `undefined` rather than `{}` when neither is set, because that is
146
+ * what `cloneOrPull` treats as "no options" at its own call sites.
147
+ */
148
+ export function analyzeCloneOptions(token, branch) {
149
+ if (!token && !branch)
150
+ return undefined;
151
+ return {
152
+ ...(token ? { token } : {}),
153
+ ...(branch ? { branch } : {}),
154
+ };
85
155
  }
86
156
  /**
87
157
  * Build the `git clone` argument list for a given URL and target directory.
@@ -248,11 +318,110 @@ export async function assertRemoteMatchesRequestedUrl(targetDir, requestedUrl, t
248
318
  `not the requested URL ${stripUrlCredentials(requestedUrl)}`);
249
319
  }
250
320
  }
321
+ /**
322
+ * Fetch refspec that updates `origin/<branch>` from `refs/heads/<branch>`.
323
+ *
324
+ * The leading `+` is git's dest-update prefix (`+refs/heads/*:refs/remotes/origin/*`
325
+ * is what `git clone` writes into `.git/config`). Without it, `fetch --depth 1`
326
+ * refuses to move `origin/<branch>` when the shallow history cannot prove a
327
+ * fast-forward — so a same-branch re-index stays stuck on the old tip.
328
+ *
329
+ * The user string is interpolated inside `refs/heads/…`, never as a raw pull
330
+ * dest. A branch named `+develop` becomes `+refs/heads/+develop:…`, not a
331
+ * force-update of `develop`.
332
+ */
333
+ function branchFetchRefspec(branch) {
334
+ return `+refs/heads/${branch}:refs/remotes/origin/${branch}`;
335
+ }
336
+ /** Overlays `analyze` writes into a clone; they must not block a same-ref update. */
337
+ const GITNEXUS_GENERATED_OVERLAYS = ['./AGENTS.md', './CLAUDE.md', './.claude'];
338
+ /**
339
+ * Restore only GitNexus-generated overlays so a same-ref update is not
340
+ * blocked by analyze dirt. Path-limited and root-anchored (`./`): tracked
341
+ * files are checked out from HEAD; untracked overlays (including gitignored
342
+ * ones — `AGENTS.md` / `.claude/` are commonly ignored) are `git clean -fdx`'d.
343
+ * A slash-free `AGENTS.md` would also hit `docs/AGENTS.md`. Never a
344
+ * whole-clone `git clean --force -d`.
345
+ */
346
+ async function restoreGitNexusGeneratedOverlays(runGitImpl, cwd, gitOpts) {
347
+ for (const overlay of GITNEXUS_GENERATED_OVERLAYS) {
348
+ const listed = (await runGitImpl(['ls-files', '--', overlay], cwd, gitOpts)).trim();
349
+ if (!listed)
350
+ continue;
351
+ await runGitImpl(['checkout', 'HEAD', '--', overlay], cwd, gitOpts);
352
+ }
353
+ // Path-limited: untracked analyze output still blocks checkout when the
354
+ // incoming tree has the same path, and otherwise leaves a dirty tree to
355
+ // index. `-x` is required because these overlays are often gitignored.
356
+ // Never a whole-clone `git clean --force -d`.
357
+ await runGitImpl(['clean', '-fdx', '--', ...GITNEXUS_GENERATED_OVERLAYS], cwd, gitOpts);
358
+ }
359
+ /**
360
+ * True when the working tree is already at the requested pin: either HEAD is
361
+ * that named branch, or HEAD is detached at the same SHA as `branch` /
362
+ * `origin/<branch>`. A missing ref falls through to the switch path.
363
+ */
364
+ async function matchRequestedRef(runGitImpl, cwd, branch, gitOpts) {
365
+ const abbrev = (await runGitImpl(['rev-parse', '--abbrev-ref', 'HEAD'], cwd, gitOpts)).trim();
366
+ if (abbrev === branch)
367
+ return 'branch';
368
+ // Detached HEAD reports `HEAD`; compare SHAs so a tag/SHA pin is not a switch.
369
+ if (abbrev !== 'HEAD')
370
+ return undefined;
371
+ let headSha;
372
+ try {
373
+ headSha = (await runGitImpl(['rev-parse', 'HEAD'], cwd, gitOpts)).trim();
374
+ }
375
+ catch {
376
+ return undefined;
377
+ }
378
+ for (const candidate of [branch, `origin/${branch}`]) {
379
+ try {
380
+ // Peel annotated tags (`v1.0` is a tag object; HEAD is the commit).
381
+ const requestedSha = (await runGitImpl(['rev-parse', `${candidate}^{commit}`], cwd, gitOpts)).trim();
382
+ if (requestedSha && requestedSha === headSha)
383
+ return 'sha';
384
+ }
385
+ catch {
386
+ // Ref missing — try origin/<branch>, then the switch path.
387
+ }
388
+ }
389
+ return undefined;
390
+ }
391
+ async function fetchAndCheckoutRequestedBranch(runGitImpl, cwd, branch, gitOpts) {
392
+ // Analyze clones are `--depth 1`. `merge --ff-only` cannot walk O→N when
393
+ // the remote moved 2+ commits (the merge-base is not in the shallow
394
+ // history). `checkout -B` points the local branch at the fetched tip —
395
+ // same as the switch path, no ancestry walk. No `--force`: leftover
396
+ // non-overlay dirt still refuses.
397
+ await runGitImpl(['fetch', '--depth', '1', 'origin', branchFetchRefspec(branch)], cwd, gitOpts);
398
+ await runGitImpl(['checkout', '-B', branch, `origin/${branch}`], cwd, gitOpts);
399
+ }
251
400
  /**
252
401
  * Clone or pull a git repository.
253
- * If targetDir doesn't exist: git clone --depth 1
254
- * If targetDir exists with .git: git pull --ff-only (after verifying the
255
- * existing clone's remote.origin matches the requested URL).
402
+ *
403
+ * If targetDir doesn't exist: git clone --depth 1, adding `--branch <branch>`
404
+ * when one is requested.
405
+ *
406
+ * If targetDir exists with .git, its remote.origin is verified against the
407
+ * requested URL first, and then the branch decides the update:
408
+ * - no `options.branch`: git pull --ff-only, which updates the current
409
+ * branch in place via its configured upstream. Nothing moves, so no
410
+ * dirty-tree check applies.
411
+ * - a `options.branch` that is ALREADY the current named branch: restore
412
+ * GitNexus overlays, then fetch via
413
+ * `+refs/heads/<branch>:refs/remotes/origin/<branch>` and
414
+ * `checkout -B <branch> origin/<branch>` (shallow clones cannot
415
+ * `merge --ff-only` across a 2+ commit move). Never a raw
416
+ * `origin <branch>` pull refspec. No porcelain refuse.
417
+ * - a detached HEAD whose SHA already matches the requested ref (tag /
418
+ * SHA pin): restore overlays only. Do not fetch/merge — a same-named
419
+ * branch could otherwise fast-forward the pin past the tag.
420
+ * - a `options.branch` that DIFFERS from the current pin: fetch that ref,
421
+ * then `checkout -B <branch> origin/<branch>` — so the requested branch,
422
+ * not the one already checked out, is what ends up in the working tree.
423
+ * This is the switching case, and it refuses a dirty tree unless
424
+ * `overwriteLocalChanges` is set.
256
425
  *
257
426
  * Security:
258
427
  * - targetDir must resolve inside CLONE_ROOT (~/.gitnexus/repos/). The
@@ -321,39 +490,47 @@ export async function cloneOrPull(url, targetDir, onProgress, options) {
321
490
  await assertRemoteMatchesRequestedUrl(safeTarget, url, options?.timeoutMs);
322
491
  onProgress?.({ phase: 'pulling', message: 'Pulling latest changes...' });
323
492
  const runGitImpl = options?.runGitForTest ?? runGit;
324
- if (options?.branch) {
493
+ const gitOpts = {
494
+ token: options?.token,
495
+ url,
496
+ timeoutMs: options?.timeoutMs,
497
+ };
498
+ // Already at the requested pin? Then there is no switch to make, so do
499
+ // not take the checkout path below — that would run the porcelain check
500
+ // against a tree ANALYZE ITSELF dirtied (it writes AGENTS.md / CLAUDE.md /
501
+ // .claude/ into the clone), which made a pinned RE-index impossible: the
502
+ // first pin succeeded and every later one failed asking for
503
+ // `overwrite_local_changes`, a flag this route deliberately does not pass
504
+ // because it would `git clean --force -d` the directory (#3199 review).
505
+ //
506
+ // "Already there" is a named-branch match OR a detached HEAD whose SHA
507
+ // equals `branch` / `origin/<branch>` (tag / SHA pin). A missing ref
508
+ // falls through to the switch path, which still refuses a dirty tree.
509
+ //
510
+ // Same-named-branch update uses the heads/ → remotes/ fetch refspec plus
511
+ // `checkout -B <branch> origin/<branch>`, never `pull origin <user-string>`
512
+ // (a leading `+` would otherwise be a force-fetch). Only
513
+ // `remote.origin.url` is verified above; `branch.<name>.remote` /
514
+ // `.merge` are not, so an implicit-upstream pull can update a different
515
+ // ref while the job still carries this branch (#3199 review).
516
+ const requestedRefMatch = options?.branch
517
+ ? await matchRequestedRef(runGitImpl, safeTarget, options.branch, gitOpts)
518
+ : undefined;
519
+ if (options?.branch && !requestedRefMatch) {
325
520
  if (!options.overwriteLocalChanges) {
326
- const status = await runGitImpl(['status', '--porcelain'], safeTarget, {
327
- token: options?.token,
328
- url,
329
- timeoutMs: options?.timeoutMs,
330
- });
521
+ const status = await runGitImpl(['status', '--porcelain'], safeTarget, gitOpts);
331
522
  if (status.trim()) {
332
523
  throw new Error(`Refusing to update ${safeTarget}: local changes detected. Set overwrite_local_changes: true to overwrite them.`);
333
524
  }
334
525
  }
335
- await runGitImpl([
336
- 'fetch',
337
- '--depth',
338
- '1',
339
- 'origin',
340
- `refs/heads/${options.branch}:refs/remotes/origin/${options.branch}`,
341
- ], safeTarget, {
342
- token: options?.token,
343
- url,
344
- timeoutMs: options?.timeoutMs,
345
- });
526
+ await runGitImpl(['fetch', '--depth', '1', 'origin', branchFetchRefspec(options.branch)], safeTarget, gitOpts);
346
527
  await runGitImpl([
347
528
  'checkout',
348
529
  ...(options.overwriteLocalChanges ? ['--force'] : []),
349
530
  '-B',
350
531
  options.branch,
351
532
  `origin/${options.branch}`,
352
- ], safeTarget, {
353
- token: options?.token,
354
- url,
355
- timeoutMs: options?.timeoutMs,
356
- });
533
+ ], safeTarget, gitOpts);
357
534
  if (options.overwriteLocalChanges) {
358
535
  // `checkout --force` rewrites tracked files only, so untracked sources
359
536
  // left by an operator or an earlier branch survive and then get indexed
@@ -361,19 +538,20 @@ export async function cloneOrPull(url, targetDir, onProgress, options) {
361
538
  // ignored paths must survive, and `-e /.gitnexus` is belt-and-braces
362
539
  // because `.git/info/exclude` is skipped on a read-only storage mount
363
540
  // and a freshly cloned repo may not have been analyzed yet at all.
364
- await runGitImpl(['clean', '--force', '-d', '-e', '/.gitnexus'], safeTarget, {
365
- token: options?.token,
366
- url,
367
- timeoutMs: options?.timeoutMs,
368
- });
541
+ await runGitImpl(['clean', '--force', '-d', '-e', '/.gitnexus'], safeTarget, gitOpts);
369
542
  }
370
543
  }
544
+ else if (options?.branch && requestedRefMatch === 'branch') {
545
+ await restoreGitNexusGeneratedOverlays(runGitImpl, safeTarget, gitOpts);
546
+ await fetchAndCheckoutRequestedBranch(runGitImpl, safeTarget, options.branch, gitOpts);
547
+ }
548
+ else if (options?.branch && requestedRefMatch === 'sha') {
549
+ // Tag / SHA pin: already at the requested commit. Fetching
550
+ // `refs/heads/<name>` would follow a same-named branch past the pin.
551
+ await restoreGitNexusGeneratedOverlays(runGitImpl, safeTarget, gitOpts);
552
+ }
371
553
  else {
372
- await runGitImpl(['pull', '--ff-only'], safeTarget, {
373
- token: options?.token,
374
- url,
375
- timeoutMs: options?.timeoutMs,
376
- });
554
+ await runGitImpl(['pull', '--ff-only'], safeTarget, gitOpts);
377
555
  }
378
556
  }
379
557
  else {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Response projections for the `serve` repo routes.
3
+ *
4
+ * Extracted from the route bodies so the field list is assertable. Inline, the
5
+ * projections were only reachable by booting a server and indexing a real
6
+ * repository, which is how `branch` came to sit on `RegistryEntry` unexposed
7
+ * over HTTP while `gitnexus list` printed it, with no test to notice (#3226).
8
+ *
9
+ * These are pure: the caller resolves the registry entry, the on-disk metadata
10
+ * and the staleness check, and passes the results in.
11
+ */
12
+ import type { StalenessInfo } from '../core/git-staleness.js';
13
+ import type { RegistryEntry } from '../storage/repo-manager.js';
14
+ import type { RepoMeta } from '../storage/repo-meta.js';
15
+ /**
16
+ * Staleness in the shape MCP `list_repos` already returns
17
+ * (`mcp/local/local-backend.ts`): the key is present only when the index is
18
+ * actually behind, so "fresh" stays the absence of a field rather than a second
19
+ * thing for a client to interpret. Deliberately identical across the two
20
+ * surfaces — the same fact should not have two shapes.
21
+ *
22
+ * `checkStalenessAsync` self-catches and reports 0 commits behind when the
23
+ * commit cannot be resolved, so an unanswerable check degrades to "not stale"
24
+ * rather than failing the request that carries it.
25
+ *
26
+ * That makes the field meaningful mainly for `path`-registered repositories,
27
+ * where an operator commits into the working tree the index was built from.
28
+ * A `url` repository is cloned `--depth 1` and is re-analyzed by the same run
29
+ * that pulls it, so its recorded commit is HEAD; and were the two ever to
30
+ * diverge, `git rev-list <old>..HEAD` cannot walk a shallow history and fails
31
+ * closed to "not stale". Reporting fresh there is not a claim that the remote
32
+ * has not moved — this measures the index against the local working tree, the
33
+ * same thing `gitnexus status` and MCP `list_repos` measure.
34
+ */
35
+ export declare const stalenessField: (info: StalenessInfo) => {
36
+ staleness?: {
37
+ commitsBehind: number;
38
+ hint?: string;
39
+ };
40
+ };
41
+ /** One entry of `GET /api/repos`. */
42
+ export declare const projectRepoListEntry: (entry: RegistryEntry, staleness: StalenessInfo) => {
43
+ staleness?: {
44
+ commitsBehind: number;
45
+ hint?: string;
46
+ };
47
+ name: string;
48
+ path: string;
49
+ repoPath: string;
50
+ indexedAt: string;
51
+ lastCommit: string;
52
+ stats: {
53
+ files?: number;
54
+ nodes?: number;
55
+ edges?: number;
56
+ communities?: number;
57
+ processes?: number;
58
+ embeddings?: number;
59
+ };
60
+ branch: string;
61
+ branches: import("../storage/branch-index.js").BranchSummary[];
62
+ };
63
+ /**
64
+ * `GET /api/repo`. `meta ?? entry` throughout, for the reason `indexedAt`
65
+ * already did it: the on-disk metadata is the fresher record when the two
66
+ * disagree, and the entry is the fallback for a repo whose meta cannot be read.
67
+ */
68
+ export declare const projectRepoDetail: (entry: RegistryEntry, meta: RepoMeta | null | undefined, staleness: StalenessInfo) => {
69
+ staleness?: {
70
+ commitsBehind: number;
71
+ hint?: string;
72
+ };
73
+ name: string;
74
+ repoPath: string;
75
+ indexedAt: string;
76
+ stats: {
77
+ files?: number;
78
+ nodes?: number;
79
+ edges?: number;
80
+ communities?: number;
81
+ processes?: number;
82
+ embeddings?: number;
83
+ };
84
+ lastCommit: string;
85
+ branch: string;
86
+ };
87
+ /** The commit a `/api/repo` staleness check should be measured against. */
88
+ export declare const resolveLastCommit: (entry: RegistryEntry, meta: RepoMeta | null | undefined) => string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Staleness in the shape MCP `list_repos` already returns
3
+ * (`mcp/local/local-backend.ts`): the key is present only when the index is
4
+ * actually behind, so "fresh" stays the absence of a field rather than a second
5
+ * thing for a client to interpret. Deliberately identical across the two
6
+ * surfaces — the same fact should not have two shapes.
7
+ *
8
+ * `checkStalenessAsync` self-catches and reports 0 commits behind when the
9
+ * commit cannot be resolved, so an unanswerable check degrades to "not stale"
10
+ * rather than failing the request that carries it.
11
+ *
12
+ * That makes the field meaningful mainly for `path`-registered repositories,
13
+ * where an operator commits into the working tree the index was built from.
14
+ * A `url` repository is cloned `--depth 1` and is re-analyzed by the same run
15
+ * that pulls it, so its recorded commit is HEAD; and were the two ever to
16
+ * diverge, `git rev-list <old>..HEAD` cannot walk a shallow history and fails
17
+ * closed to "not stale". Reporting fresh there is not a claim that the remote
18
+ * has not moved — this measures the index against the local working tree, the
19
+ * same thing `gitnexus status` and MCP `list_repos` measure.
20
+ */
21
+ export const stalenessField = (info) => info.isStale ? { staleness: { commitsBehind: info.commitsBehind, hint: info.hint } } : {};
22
+ /** One entry of `GET /api/repos`. */
23
+ export const projectRepoListEntry = (entry, staleness) => ({
24
+ name: entry.name,
25
+ path: entry.path,
26
+ repoPath: entry.path,
27
+ indexedAt: entry.indexedAt,
28
+ lastCommit: entry.lastCommit,
29
+ stats: entry.stats,
30
+ // The registry has carried these since #2106; #3199 made them load-bearing
31
+ // over HTTP, because a branch-pinned analyze now gets its own entry and the
32
+ // only other way to tell two entries apart is to parse the clone-directory
33
+ // slug — a layout detail, not an API contract.
34
+ branch: entry.branch,
35
+ branches: entry.branches,
36
+ ...stalenessField(staleness),
37
+ });
38
+ /**
39
+ * `GET /api/repo`. `meta ?? entry` throughout, for the reason `indexedAt`
40
+ * already did it: the on-disk metadata is the fresher record when the two
41
+ * disagree, and the entry is the fallback for a repo whose meta cannot be read.
42
+ */
43
+ export const projectRepoDetail = (entry, meta, staleness) => ({
44
+ name: entry.name,
45
+ repoPath: entry.path,
46
+ indexedAt: meta?.indexedAt ?? entry.indexedAt,
47
+ stats: meta?.stats ?? entry.stats ?? {},
48
+ lastCommit: meta?.lastCommit ?? entry.lastCommit,
49
+ branch: meta?.branch ?? entry.branch,
50
+ ...stalenessField(staleness),
51
+ });
52
+ /** The commit a `/api/repo` staleness check should be measured against. */
53
+ export const resolveLastCommit = (entry, meta) => meta?.lastCommit ?? entry.lastCommit;
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { setTimeout as sleep } from 'node:timers/promises';
6
- import { isProcessAlive, readProcessStartTime } from '../utils/process-identity.js';
6
+ import { isProcessAlive, readProcessStartTimeCached } from '../utils/process-identity.js';
7
7
  const HOSTNAME = os.hostname();
8
8
  export class FileLockBusyError extends Error {
9
9
  lockPath;
@@ -22,7 +22,9 @@ export async function acquireFileLock(lockPath, options = {}) {
22
22
  const owner = {
23
23
  pid,
24
24
  ownerId: crypto.randomUUID(),
25
- processStartTime: options.processStartTime ?? (options.readProcessStartTime ?? readProcessStartTime)(pid) ?? '',
25
+ processStartTime: options.processStartTime ??
26
+ (options.readProcessStartTime ?? readProcessStartTimeCached)(pid) ??
27
+ '',
26
28
  hostname: options.hostname ?? HOSTNAME,
27
29
  };
28
30
  if (!owner.processStartTime) {
@@ -40,7 +42,7 @@ export async function acquireFileLock(lockPath, options = {}) {
40
42
  catch (error) {
41
43
  if (!(await isLockConflict(error, resolvedPath)))
42
44
  throw error;
43
- if (await reclaimStaleLock(resolvedPath, owner, options.isProcessAlive ?? isProcessAlive, options.readProcessStartTime ?? readProcessStartTime)) {
45
+ if (await reclaimStaleLock(resolvedPath, owner, options.isProcessAlive ?? isProcessAlive, options.readProcessStartTime ?? readProcessStartTimeCached)) {
44
46
  continue;
45
47
  }
46
48
  if (attempt >= retries)
@@ -730,7 +730,38 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
730
730
  // v93: Zig call captures inside a comptime-false branch carry
731
731
  // `@reference.static-gated` (feat/zig-static-gated-edges); the site gains
732
732
  // `staticGated` and the CALLS edge a BOOLEAN column.
733
- const SCHEMA_BUMP = 93;
733
+ // v94: Objective-C now elides bare, file-scope macro markers before parsing.
734
+ // A warm v93 cache can retain error-recovered trees and provider facts that
735
+ // omit Objective-C declarations following markers such as RCT_EXTERN_C_END.
736
+ // v95: Objective-C header classification no longer treats framework `#import`
737
+ // alone as Objective-C syntax. A warm v94 cache can replay Objective-C worker
738
+ // output for a C++ header during `--force`, even though the current classifier
739
+ // routes that same header through the C++ provider.
740
+ // v96: Objective-C macro-marker preprocessing recognizes all C preprocessing
741
+ // whitespace before a directive or bare marker. A warm v95 cache can retain
742
+ // error-recovered facts for sources that begin those lines with form feed or
743
+ // vertical tab.
744
+ // v97: Objective-C macro-marker preprocessing recognizes comment-prefixed
745
+ // directives and rejects invalid numeric marker prefixes. A warm v96 cache can
746
+ // replay error-recovered facts from the previous normalization behavior.
747
+ // v98 (#3219): `ZIG_SCOPE_QUERY` gained three `@reference.value-ref` rules —
748
+ // bare call argument, qualified call argument (with `@reference.receiver`), and
749
+ // const-binding initialiser — so a Zig callable named in VALUE position now
750
+ // produces a `value-ref` entry in `ParsedFile.referenceSites` where it produced
751
+ // none before. These captures are PARSE-TIME facts, so a warm pre-v98 cache
752
+ // replays unchanged `.zig` files with zero value-ref sites, `--force` included
753
+ // (shards are content-addressed): `emitPropertyDispatchCalls` then emits no
754
+ // USES edge, `callableValueReferenceBoundaries` measures a real zero, and
755
+ // `impact` on a registered accessor republishes `epistemic: "exact"` — the exact
756
+ // #3399 defect this change exists to close, silently un-fixed.
757
+ //
758
+ // 94-97 went to #3179 (Objective-C), which has since merged; 98 is the next
759
+ // value above it. Re-checked against origin/main and every open PR touching
760
+ // this file at merge time, which is the rule the paragraphs above were written
761
+ // by PRs that each checked only once: main is at 97, and the remaining open
762
+ // claims (#3190 at 94, #2840 at 71, #1616 at 2) all sit below it and must
763
+ // re-bump themselves.
764
+ const SCHEMA_BUMP = 98;
734
765
  const GITNEXUS_PKG_VERSION = (() => {
735
766
  try {
736
767
  // package.json sits at gitnexus/package.json — two levels up from
@@ -149,6 +149,7 @@ export const getParsedFileStoreDir = (storagePath) => path.join(storagePath, STO
149
149
  /** Remove any prior run's shards so a fresh parse starts clean. Idempotent. */
150
150
  export const clearParsedFileStore = async (storagePath) => {
151
151
  await fs.rm(getParsedFileStoreDir(storagePath), { recursive: true, force: true });
152
+ forgetShardListings();
152
153
  };
153
154
  const isV8ShardName = (name) => name.endsWith('.v8') && !name.includes('.v8.');
154
155
  const shardPath = (storagePath, shardId) => path.join(getParsedFileStoreDir(storagePath), `${shardId}.v8`);
@@ -200,11 +201,46 @@ const listV8Shards = async (dir) => {
200
201
  return [];
201
202
  }
202
203
  };
204
+ /**
205
+ * Per-run memo of each shard's authenticated path listing, so the SECOND and
206
+ * later passes over the store can decide "this shard holds nothing I want"
207
+ * without reopening it.
208
+ *
209
+ * Scope resolution calls `loadParsedFilesForPaths` once per language, and each
210
+ * call walks every shard in the store. The skip decision needs the envelope's
211
+ * path listing, and that listing is only trustworthy once the payload digest
212
+ * has been checked — so today a pass that wants 50 Python files still reads and
213
+ * SHA-256s all ~300MB of a TypeScript-dominated store to prove it can skip it.
214
+ * Measured on a 2234-file repo: a pass wanting a single file costs 335ms, and
215
+ * the three language passes together spend 4426ms in here. The cost scales with
216
+ * LANGUAGE COUNT, so a polyglot repo pays it worst.
217
+ *
218
+ * Keyed by size+mtime as well as name. Shard names are content-addressed
219
+ * (parse-chunk hash + worker id), so a name collision across different content
220
+ * should be impossible — but that invariant lives in the parse-cache keying,
221
+ * not here, and one `stat` per shard is a few ms against the hundreds this
222
+ * saves. The memo holds one store directory at a time: a different `dir` (a new
223
+ * repo in a long-lived MCP process, or a wiped store) drops the previous set
224
+ * rather than accumulating.
225
+ */
226
+ let shardListingMemo;
227
+ const shardIdentity = (file, size, mtimeMs) => `${file}\0${size}\0${mtimeMs}`;
228
+ const memoFor = (dir) => {
229
+ if (shardListingMemo?.dir !== dir)
230
+ shardListingMemo = { dir, byIdentity: new Map() };
231
+ return shardListingMemo.byIdentity;
232
+ };
233
+ /** Drop the memo when the store it describes is removed. */
234
+ const forgetShardListings = () => {
235
+ shardListingMemo = undefined;
236
+ };
203
237
  export const loadParsedFilesForPaths = async (storagePath, wantPaths) => {
204
238
  const out = new Map();
205
239
  if (wantPaths.size === 0)
206
240
  return out;
207
- const shardPaths = await listV8Shards(getParsedFileStoreDir(storagePath));
241
+ const storeDir = getParsedFileStoreDir(storagePath);
242
+ const shardPaths = await listV8Shards(storeDir);
243
+ const listings = memoFor(storeDir);
208
244
  const pool = new Map();
209
245
  let droppedSites = 0;
210
246
  let filesWithDroppedSites = 0;
@@ -227,11 +263,27 @@ export const loadParsedFilesForPaths = async (storagePath, wantPaths) => {
227
263
  }
228
264
  };
229
265
  for (const shardFull of shardPaths) {
266
+ // Re-stat rather than trusting the name alone, then reuse this run's
267
+ // authenticated listing to skip without opening the file. A shard with no
268
+ // memoized listing — first pass, or an envelope whose listing did not
269
+ // validate — falls through to the full read, so this only ever removes
270
+ // work that a later pass had already proved unnecessary.
271
+ const st = await fs.stat(shardFull).catch(() => undefined);
272
+ const identity = st ? shardIdentity(shardFull, st.size, st.mtimeMs) : undefined;
273
+ const memoized = identity === undefined ? undefined : listings.get(identity);
274
+ if (memoized !== undefined && !memoized.some((p) => wantPaths.has(p))) {
275
+ bytesSinceGc += st?.size ?? 0;
276
+ await maybeYieldAndGc(bytesSinceGc >= parsedFileLoadGc.byteBudget);
277
+ continue;
278
+ }
230
279
  const loaded = await tryLoadV8Cache(shardFull, pool, wantPaths);
231
280
  if (loaded === undefined) {
232
281
  await maybeYieldAndGc(false);
233
282
  continue;
234
283
  }
284
+ if (identity !== undefined && loaded.paths !== undefined) {
285
+ listings.set(identity, loaded.paths);
286
+ }
235
287
  if (loaded.kind === 'skip') {
236
288
  bytesSinceGc += loaded.bytes;
237
289
  await maybeYieldAndGc(bytesSinceGc >= parsedFileLoadGc.byteBudget);
@@ -16,14 +16,25 @@ export declare const encodeCachePathListing: (paths: readonly string[]) => Buffe
16
16
  * that does not match the remaining lines.
17
17
  */
18
18
  export declare const parseCachePathListing: (raw: Buffer) => string[] | null;
19
+ /**
20
+ * `paths` is the envelope's digest-authenticated path listing, present only
21
+ * when it parsed and its entry count matched the recorded `pathCount` — i.e.
22
+ * exactly when the skip decision below is willing to trust it. A caller that
23
+ * loads the same immutable shard more than once per run can memoize it and
24
+ * make its own skip decision without reopening the file (see
25
+ * `parsedfile-store.ts`). Absent means "this envelope has no listing worth
26
+ * trusting", which is fail-closed: load, never skip.
27
+ */
19
28
  export type V8CacheHit = {
20
29
  kind: 'hit';
21
30
  value: unknown;
22
31
  bytes: number;
32
+ paths?: readonly string[];
23
33
  };
24
34
  export type V8CacheSkip = {
25
35
  kind: 'skip';
26
36
  bytes: number;
37
+ paths: readonly string[];
27
38
  };
28
39
  export type V8CacheLoad = V8CacheHit | V8CacheSkip;
29
40
  export type V8CacheInspection = {