gitnexus 1.6.11-rc.2 → 1.6.11-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 (158) hide show
  1. package/README.md +43 -1
  2. package/dist/_shared/impact-risk.d.ts +37 -0
  3. package/dist/_shared/impact-risk.d.ts.map +1 -0
  4. package/dist/_shared/impact-risk.js +92 -0
  5. package/dist/_shared/impact-risk.js.map +1 -0
  6. package/dist/_shared/index.d.ts +2 -0
  7. package/dist/_shared/index.d.ts.map +1 -1
  8. package/dist/_shared/index.js +2 -0
  9. package/dist/_shared/index.js.map +1 -1
  10. package/dist/cli/ai-context.js +4 -4
  11. package/dist/cli/analyze-config.d.ts +2 -0
  12. package/dist/cli/analyze-config.js +16 -0
  13. package/dist/cli/analyze-options.d.ts +4 -0
  14. package/dist/cli/analyze.d.ts +17 -0
  15. package/dist/cli/analyze.js +43 -7
  16. package/dist/cli/eval-server.js +22 -5
  17. package/dist/cli/group.js +7 -1
  18. package/dist/cli/help-i18n.js +2 -0
  19. package/dist/cli/i18n/en.d.ts +12 -2
  20. package/dist/cli/i18n/en.js +12 -2
  21. package/dist/cli/i18n/resources.d.ts +22 -2
  22. package/dist/cli/i18n/zh-CN.d.ts +10 -0
  23. package/dist/cli/i18n/zh-CN.js +12 -2
  24. package/dist/cli/index.js +11 -4
  25. package/dist/cli/status.js +91 -6
  26. package/dist/cli/watch-queue.d.ts +41 -0
  27. package/dist/cli/watch-queue.js +184 -0
  28. package/dist/cli/watch.d.ts +20 -0
  29. package/dist/cli/watch.js +372 -0
  30. package/dist/cli/wiki.js +15 -2
  31. package/dist/config/ignore-service.d.ts +11 -0
  32. package/dist/config/ignore-service.js +41 -4
  33. package/dist/config/repo-control-file.d.ts +3 -0
  34. package/dist/config/repo-control-file.js +115 -0
  35. package/dist/core/group/config-parser.js +20 -2
  36. package/dist/core/group/cross-impact.d.ts +2 -1
  37. package/dist/core/group/cross-impact.js +33 -2
  38. package/dist/core/group/extractors/fs-utils.d.ts +2 -0
  39. package/dist/core/group/extractors/fs-utils.js +86 -0
  40. package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
  41. package/dist/core/group/extractors/graphql-extractor.js +652 -0
  42. package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
  43. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  44. package/dist/core/group/extractors/manifest-extractor.js +1 -1
  45. package/dist/core/group/matching.js +8 -1
  46. package/dist/core/group/service.js +5 -1
  47. package/dist/core/group/storage.js +1 -0
  48. package/dist/core/group/sync.d.ts +3 -1
  49. package/dist/core/group/sync.js +43 -5
  50. package/dist/core/group/types.d.ts +19 -5
  51. package/dist/core/incremental/derived-writeback.d.ts +36 -0
  52. package/dist/core/incremental/derived-writeback.js +68 -0
  53. package/dist/core/incremental/subgraph-extract.d.ts +6 -4
  54. package/dist/core/incremental/subgraph-extract.js +7 -5
  55. package/dist/core/index-content-drift.d.ts +54 -0
  56. package/dist/core/index-content-drift.js +127 -0
  57. package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
  58. package/dist/core/ingestion/filesystem-walker.js +20 -3
  59. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
  60. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
  61. package/dist/core/ingestion/language-provider.d.ts +43 -0
  62. package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
  63. package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
  64. package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
  65. package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
  66. package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
  67. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
  68. package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
  69. package/dist/core/ingestion/languages/java/captures.js +14 -1
  70. package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
  71. package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
  72. package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
  73. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
  74. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
  75. package/dist/core/ingestion/languages/java.js +2 -0
  76. package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
  77. package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
  78. package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
  79. package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
  80. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
  81. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
  82. package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
  83. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
  84. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
  85. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
  86. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
  87. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
  88. package/dist/core/ingestion/languages/kotlin.js +2 -0
  89. package/dist/core/ingestion/pipeline-phases/di.js +47 -14
  90. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
  91. package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
  92. package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
  93. package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
  94. package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
  95. package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
  96. package/dist/core/ingestion/pipeline.d.ts +6 -0
  97. package/dist/core/ingestion/pipeline.js +44 -12
  98. package/dist/core/ingestion/scope-extractor.js +1 -0
  99. package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
  100. package/dist/core/ingestion/utils/symbol-labels.js +2 -2
  101. package/dist/core/ingestion/workers/parse-worker.js +28 -2
  102. package/dist/core/lbug/lbug-adapter.d.ts +59 -0
  103. package/dist/core/lbug/lbug-adapter.js +154 -1
  104. package/dist/core/run-analyze.d.ts +17 -0
  105. package/dist/core/run-analyze.js +231 -23
  106. package/dist/core/search/fts-indexes.d.ts +19 -1
  107. package/dist/core/search/fts-indexes.js +28 -1
  108. package/dist/core/wiki/generator.js +8 -0
  109. package/dist/core/wiki/grok-client.d.ts +21 -0
  110. package/dist/core/wiki/grok-client.js +287 -0
  111. package/dist/core/wiki/llm-client.d.ts +1 -1
  112. package/dist/core/wiki/llm-client.js +5 -2
  113. package/dist/core/wiki/local-cli-client.d.ts +11 -0
  114. package/dist/core/wiki/local-cli-client.js +22 -9
  115. package/dist/mcp/local/local-backend.d.ts +24 -7
  116. package/dist/mcp/local/local-backend.js +190 -81
  117. package/dist/mcp/local/pdg-impact.d.ts +8 -4
  118. package/dist/mcp/local/pdg-impact.js +7 -2
  119. package/dist/mcp/repository-policy.d.ts +5 -1
  120. package/dist/mcp/repository-policy.js +48 -4
  121. package/dist/mcp/resources.js +2 -1
  122. package/dist/mcp/server.js +6 -5
  123. package/dist/mcp/tools.js +31 -19
  124. package/dist/server/api.js +23 -64
  125. package/dist/server/grep-params.d.ts +18 -0
  126. package/dist/server/grep-params.js +83 -0
  127. package/dist/server/grep-scan.d.ts +23 -0
  128. package/dist/server/grep-scan.js +107 -0
  129. package/dist/server/grep-worker.d.ts +1 -0
  130. package/dist/server/grep-worker.js +12 -0
  131. package/dist/server/mcp-http.d.ts +8 -0
  132. package/dist/server/mcp-http.js +16 -1
  133. package/dist/storage/file-hash.d.ts +5 -0
  134. package/dist/storage/file-hash.js +16 -6
  135. package/dist/storage/fs-atomic.d.ts +24 -0
  136. package/dist/storage/fs-atomic.js +86 -2
  137. package/dist/storage/git.d.ts +19 -8
  138. package/dist/storage/git.js +83 -24
  139. package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
  140. package/dist/storage/gitnexus-managed-paths.js +46 -0
  141. package/dist/storage/parse-cache.d.ts +22 -4
  142. package/dist/storage/parse-cache.js +106 -29
  143. package/dist/storage/parsedfile-store.d.ts +34 -60
  144. package/dist/storage/parsedfile-store.js +177 -171
  145. package/dist/storage/repo-manager.d.ts +11 -1
  146. package/dist/storage/repo-manager.js +23 -2
  147. package/dist/storage/repo-meta.d.ts +12 -0
  148. package/dist/storage/v8-sidecar.d.ts +48 -0
  149. package/dist/storage/v8-sidecar.js +347 -0
  150. package/dist/types/pipeline.d.ts +14 -0
  151. package/package.json +4 -1
  152. package/scripts/cross-platform-shard.ts +4 -2
  153. package/scripts/cross-platform-tests.ts +7 -1
  154. package/skills/gitnexus-cli.md +11 -3
  155. package/skills/gitnexus-impact-analysis.md +9 -0
  156. package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
  157. package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
  158. package/web/index.html +1 -1
@@ -6,10 +6,12 @@
6
6
  * does is skip the tree-sitter worker dispatch when a chunk's contents
7
7
  * haven't changed since the last run.
8
8
  *
9
- * Granularity: chunk-level. The parse phase chunks files into ~20MB byte
10
- * budgets. The cache key is `sha256(joined(filePath:contentHash for each
11
- * file in the chunk, sorted))`. A change to a single file invalidates only
12
- * that file's chunk — typically 1 of ~50 chunks on a 1000-file repo.
9
+ * Granularity: chunk-level. Files are assigned to a stable
10
+ * `(language, hash(path) mod 128)` bucket, then packed to a 2 MiB (or
11
+ * operator) byte budget *inside* that bucket. Membership does not depend
12
+ * on worker count. The cache key is `sha256(joined(filePath:contentHash
13
+ * for each file in the chunk, sorted))`. A content edit invalidates only
14
+ * that file's pack; add/delete/rename only the affected bucket.
13
15
  *
14
16
  * Why not per-file:
15
17
  * - Workers process sub-batches and emit aggregated `ParseWorkerResult`s.
@@ -26,6 +28,8 @@ import { createRequire } from 'module';
26
28
  import fs from 'fs/promises';
27
29
  import path from 'path';
28
30
  import { fileURLToPath } from 'url';
31
+ import { compareCodeUnits } from '../lib/utils.js';
32
+ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sidecar.js';
29
33
  /**
30
34
  * Cache version composed of:
31
35
  * - A schema bump knob (`SCHEMA_BUMP`) for hand-controlled invalidation
@@ -627,6 +631,16 @@ import { fileURLToPath } from 'url';
627
631
  // therefore takes 79, the next free value above origin/main and every open PR
628
632
  // found by the contents-API scan at their exact head SHAs.
629
633
  //
634
+ // 79 -> 80 for #3088: parse-cache membership is `(language, sha256(path) mod
635
+ // 128)` then the byte budget *inside* that bucket. Worker count is no longer a
636
+ // membership input, so a warm v79 cache keyed sequential scan-order packs (and
637
+ // on multi-worker hosts, pool×2 MiB mega-chunks) must miss. Sidecar-era
638
+ // ParsedFile stores (#3086/#3087) share PARSE_CACHE_VERSION, so both stores
639
+ // invalidate in lockstep. origin/main at allocation is 79; open PRs that still
640
+ // touch gitnexus/src/storage/parse-cache.ts claim 78 (#3060), 71 (#2840), and
641
+ // 2 (#1616) — none claim 80. RE-CHECK AGAINST origin/main AND OPEN PRs
642
+ // IMMEDIATELY BEFORE MERGING.
643
+ //
630
644
  // WHY THIS IS STILL A HAND-PICKED NUMBER, when `SCHEMA_FINGERPRINT` next door
631
645
  // is a derived sha256 that cannot collide. The derivation exists and already
632
646
  // runs: `resolveAnalyzerRunnerIdentity` computes `build.digest` over the
@@ -645,7 +659,28 @@ import { fileURLToPath } from 'url';
645
659
  // `route-extractors/` and `workers/` module content — would close the missing-
646
660
  // bump axis without invalidating on unrelated churn, and is the real follow-up.
647
661
  // RE-CHECK AGAINST origin/main AND OPEN PRs IMMEDIATELY BEFORE MERGING.
648
- const SCHEMA_BUMP = 79;
662
+ // 80 -> 81: ParsedFile and parse-cache shards are one immutable `.v8` envelope
663
+ // each (no JSON/path/generation siblings). A v80 index still names `.json`
664
+ // keys and would skip workers while scope-resolution found nothing — the
665
+ // #1983 main-thread reparse. origin/main at allocation is 80.
666
+ // 81 -> 82: Java and Kotlin ParsedFile capture side channels now carry
667
+ // programmatic Spring lookup facts. A warm v81 cache has no such facts, so it
668
+ // would skip workers and silently omit the new INJECTS edges. origin/main at
669
+ // allocation is 81.
670
+ // 82 -> 83: Java Lombok @Data/@Getter/@Setter accessor synthesis emits
671
+ // synthetic Method nodes, HAS_METHOD edges, and matching scope captures into
672
+ // ParseWorkerResult / ParsedFile. A warm v82 cache replays pre-Lombok worker
673
+ // output and silently omits those callables. origin/main at allocation is 82.
674
+ // 83 -> 84: Kotlin val/var properties synthesize JVM get/set Method nodes
675
+ // (same provider hook as Java Lombok). A warm v83 cache omits those callables.
676
+ // origin/main at allocation is 83 (Java Lombok on this branch).
677
+ // 84 -> 85: JVM synthetic accessor captures now use the declaration
678
+ // qualified_name key consumed by scope extraction, and Kotlin accessor planning
679
+ // follows JvmAbi naming plus conservative @JvmName suppression. A warm v84
680
+ // cache can replay stale names and declaration metadata.
681
+ // 85 -> 86: Kotlin interface property accessors now record isAbstract on the
682
+ // synthetic Method. A warm v85 cache replays them as concrete.
683
+ const SCHEMA_BUMP = 86;
649
684
  const GITNEXUS_PKG_VERSION = (() => {
650
685
  try {
651
686
  // package.json sits at gitnexus/package.json — two levels up from
@@ -673,16 +708,56 @@ const GITNEXUS_PKG_VERSION = (() => {
673
708
  return '0.0.0-unknown';
674
709
  })();
675
710
  export const PARSE_CACHE_VERSION = `${SCHEMA_BUMP}+${GITNEXUS_PKG_VERSION}`;
711
+ /** SHA-256 hex of a string or buffer (paths for bucket ids, contents for cache keys). */
712
+ const sha256Hex = (input) => createHash('sha256')
713
+ .update(typeof input === 'string' ? Buffer.from(input) : input)
714
+ .digest('hex');
715
+ /** Stable parse-cache bucket count (#3088). Changing this requires SCHEMA_BUMP. */
716
+ export const PARSE_CACHE_BUCKET_COUNT = 128;
717
+ /** Bucket id for cache membership: `sha256(path) mod N` without IEEE-754 truncation. */
718
+ export const parseCacheBucketId = (filePath) => Number(BigInt(`0x${sha256Hex(filePath)}`) % BigInt(PARSE_CACHE_BUCKET_COUNT));
719
+ /**
720
+ * Pack files into parse-cache chunks: group by (language, bucket id), sort
721
+ * paths inside the group, then cut at `byteBudget`. Bucket visit order is
722
+ * the lexicographic order of `${language}\\0${bucketId}` keys (deterministic,
723
+ * independent of scan order and worker count).
724
+ */
725
+ export const packParseCacheChunks = (files, byteBudget) => {
726
+ const buckets = new Map();
727
+ for (const file of files) {
728
+ const key = `${file.language}\0${parseCacheBucketId(file.path)}`;
729
+ const list = buckets.get(key);
730
+ if (list)
731
+ list.push(file);
732
+ else
733
+ buckets.set(key, [file]);
734
+ }
735
+ const chunks = [];
736
+ for (const key of [...buckets.keys()].sort()) {
737
+ const group = buckets.get(key);
738
+ group.sort((a, b) => compareCodeUnits(a.path, b.path));
739
+ let current = [];
740
+ let bytes = 0;
741
+ for (const file of group) {
742
+ if (current.length > 0 && bytes + file.size > byteBudget) {
743
+ chunks.push(current);
744
+ current = [];
745
+ bytes = 0;
746
+ }
747
+ current.push(file.path);
748
+ bytes += file.size;
749
+ }
750
+ if (current.length > 0)
751
+ chunks.push(current);
752
+ }
753
+ return chunks;
754
+ };
676
755
  const LEGACY_CACHE_FILENAME = 'parse-cache.json';
677
756
  const CACHE_DIRNAME = 'parse-cache';
678
757
  const CACHE_INDEX_FILENAME = 'index.json';
679
758
  /** Keys on disk always come from `computeChunkHash` — 64-char lowercase hex. */
680
759
  const CHUNK_CACHE_KEY_HEX_RE = /^[a-f0-9]{64}$/;
681
760
  const isValidChunkCacheKey = (chunkHash) => CHUNK_CACHE_KEY_HEX_RE.test(chunkHash);
682
- /** SHA-256 hex of a single string or buffer. */
683
- const sha256Hex = (input) => createHash('sha256')
684
- .update(typeof input === 'string' ? Buffer.from(input) : input)
685
- .digest('hex');
686
761
  /** Stable hash of a single file's contents — used by callers to compose a chunk hash. */
687
762
  export const fileContentHash = (content) => sha256Hex(content);
688
763
  export const computeChunkHash = (entries, pdg = false) => {
@@ -759,7 +834,7 @@ export const mapReviver = (_key, value) => {
759
834
  const getLegacyCachePath = (storagePath) => path.join(storagePath, LEGACY_CACHE_FILENAME);
760
835
  const getCacheDirPath = (storagePath) => path.join(storagePath, CACHE_DIRNAME);
761
836
  const getCacheIndexPath = (storagePath) => path.join(getCacheDirPath(storagePath), CACHE_INDEX_FILENAME);
762
- const getCacheChunkPath = (storagePath, chunkHash) => path.join(getCacheDirPath(storagePath), `${chunkHash}.json`);
837
+ const getCacheChunkPath = (storagePath, chunkHash) => path.join(getCacheDirPath(storagePath), `${chunkHash}.v8`);
763
838
  /**
764
839
  * Drop fields that are not replayed by `mergeChunkResults` / parse-impl after
765
840
  * RING4-1 (#942). Shrinks on-disk shards and peak RSS during cold runs.
@@ -784,9 +859,12 @@ const readParseCacheChunkFromDisk = async (storagePath, chunkHash) => {
784
859
  if (!isValidChunkCacheKey(chunkHash))
785
860
  return undefined;
786
861
  try {
787
- const chunkRaw = await fs.readFile(getCacheChunkPath(storagePath, chunkHash), 'utf-8');
788
- const chunkData = JSON.parse(chunkRaw, mapReviver);
789
- return Array.isArray(chunkData) ? chunkData : undefined;
862
+ const chunkPath = getCacheChunkPath(storagePath, chunkHash);
863
+ const v8Hit = await tryLoadV8Cache(chunkPath);
864
+ if (v8Hit?.kind === 'hit' && Array.isArray(v8Hit.value)) {
865
+ return v8Hit.value;
866
+ }
867
+ return undefined;
790
868
  }
791
869
  catch {
792
870
  return undefined;
@@ -821,8 +899,17 @@ export const persistParseCacheChunk = async (cache, chunkHash, chunkResults) =>
821
899
  await fs.mkdir(cacheDir, { recursive: true });
822
900
  createdCacheDirs.add(cacheDir);
823
901
  }
824
- const payload = JSON.stringify(slim, mapReplacer);
825
- await fs.writeFile(getCacheChunkPath(cache.storagePath, chunkHash), payload, 'utf-8');
902
+ const chunkPath = getCacheChunkPath(cache.storagePath, chunkHash);
903
+ let ok = await writeV8CacheFile(chunkPath, slim);
904
+ if (!ok) {
905
+ await fs.mkdir(cacheDir, { recursive: true });
906
+ createdCacheDirs.add(cacheDir);
907
+ ok = await writeV8CacheFile(chunkPath, slim);
908
+ }
909
+ if (!ok) {
910
+ cache.entries.set(chunkHash, slim);
911
+ return;
912
+ }
826
913
  cache.onDiskKeys ??= new Set();
827
914
  cache.onDiskKeys.add(chunkHash);
828
915
  cache.entries.delete(chunkHash);
@@ -918,28 +1005,18 @@ export const saveParseCache = async (storagePath, cache) => {
918
1005
  // index from what we persisted, not from the raw usedKeys snapshot.
919
1006
  const writtenKeys = [];
920
1007
  for (const chunkHash of keys) {
921
- const chunkPath = path.join(tmpDir, `${chunkHash}.json`);
1008
+ const chunkPath = path.join(tmpDir, `${chunkHash}.v8`);
922
1009
  const inMemory = cache.entries.get(chunkHash);
923
1010
  if (inMemory !== undefined) {
924
- let payload;
925
- try {
926
- payload = JSON.stringify(inMemory, mapReplacer);
927
- }
928
- catch {
929
- continue;
1011
+ if (await writeV8CacheFile(chunkPath, inMemory)) {
1012
+ writtenKeys.push(chunkHash);
930
1013
  }
931
- await fs.writeFile(chunkPath, payload, 'utf-8');
932
- writtenKeys.push(chunkHash);
933
1014
  continue;
934
1015
  }
935
1016
  const existingPath = getCacheChunkPath(storagePath, chunkHash);
936
- try {
937
- await fs.copyFile(existingPath, chunkPath);
1017
+ if (await copyV8CacheIfPresent(existingPath, chunkPath)) {
938
1018
  writtenKeys.push(chunkHash);
939
1019
  }
940
- catch {
941
- /* shard missing — skip; next run treats as cache miss */
942
- }
943
1020
  }
944
1021
  const index = {
945
1022
  version: cache.version,
@@ -24,11 +24,11 @@
24
24
  *
25
25
  * ## Shape
26
26
  *
27
- * `<storagePath>/parsedfile-store/<shardId>.json` — one shard per parse chunk,
28
- * a JSON array of `ParsedFile` serialized with the same `mapReplacer` the parse
29
- * cache uses (Scope.bindings / Scope.typeBindings are `Map`s). The store is
30
- * cleared at the start of each parse and after scope-resolution consumes it, so
31
- * it never lingers and never goes stale across runs.
27
+ * `<storagePath>/parsedfile-store/<shardId>.v8` — one shard per parse chunk,
28
+ * a V8 envelope of `ParsedFile[]` (Scope.bindings / Scope.typeBindings stay
29
+ * `Map`s). The store is cleared at the start of each parse and after
30
+ * scope-resolution consumes it, so it never lingers and never goes stale
31
+ * across runs.
32
32
  *
33
33
  * ## Durable sibling store (`parsedfile-cache/`, warm-cache coverage)
34
34
  *
@@ -41,11 +41,11 @@
41
41
  * that gap we ALSO write the worker's ParsedFiles to a second, CONTENT-ADDRESSED
42
42
  * store keyed by the parse chunk hash (`getDurableParsedFileDir`), which mirrors
43
43
  * the parse cache's lifecycle (persists across runs, pruned by `usedKeys`,
44
- * version-tied via `PARSE_CACHE_VERSION`). On a warm hit the chunk's durable
45
- * shards are byte-COPIED into the run-scoped store (no re-parse, no
46
- * re-serialize → byte-identical), so scope-resolution streams them exactly as
47
- * on a cold run. Content-addressing makes stale reuse impossible: a changed
48
- * file changes its chunk hash, which misses BOTH stores and re-dispatches.
44
+ * version-tied via `PARSE_CACHE_VERSION`). On a warm hit the chunk's immutable
45
+ * durable shards are hardlinked (or atomically copied) into the run store after
46
+ * their envelope metadata proves complete coverage. That pins a stable snapshot
47
+ * before workers are skipped, even when another branch refreshes the shared
48
+ * durable directory concurrently.
49
49
  */
50
50
  import type { ParsedFile, SymbolDefinition } from '../_shared/index.js';
51
51
  /**
@@ -73,30 +73,25 @@ export declare const getParsedFileStoreDir: (storagePath: string) => string;
73
73
  /** Remove any prior run's shards so a fresh parse starts clean. Idempotent. */
74
74
  export declare const clearParsedFileStore: (storagePath: string) => Promise<void>;
75
75
  /**
76
- * Write one parse chunk's `ParsedFile[]` to the store as a single shard (async).
77
- * No-op for an empty chunk. `shardId` must be unique within a run. Used by the
78
- * main-thread no-store-disabled fallback and any non-worker writer; the worker
79
- * store path uses {@link persistParsedFileShardSync}.
76
+ * Test seam for #3086. Production always calls {@link forceGc}; unit tests
77
+ * replace `run` to count cadence without requiring `--expose-gc`.
80
78
  */
81
- export declare const persistParsedFileChunk: (storagePath: string, shardId: string, parsedFiles: readonly ParsedFile[]) => Promise<void>;
79
+ export declare const parsedFileLoadGc: {
80
+ run: () => void;
81
+ /** V8 envelope bytes visited between GCs (#3086). Tests may lower this. */
82
+ byteBudget: number;
83
+ };
82
84
  /**
83
- * Synchronous shard writer for use INSIDE a parse worker (#1983 parallel
84
- * serialization). The worker is a dedicated thread, so a blocking write there
85
- * protects the main thread, and a sync write avoids threading `async`/`await`
86
- * through the synchronous per-file extract loop. Produces byte-identical shards
87
- * to {@link persistParsedFileChunk} via the shared {@link serializeParsedFileShard}.
88
- * No-op for an empty chunk. `shardId` must be globally unique for the run (the
89
- * worker uses `w<threadId>-<seq>`); a duplicate would silently overwrite.
85
+ * Write one parse chunk's `ParsedFile[]` to the store as a single `.v8` shard.
86
+ * No-op for an empty chunk. `shardId` must be unique within a run.
90
87
  */
91
- export declare const persistParsedFileShardSync: (storagePath: string, shardId: string, parsedFiles: readonly ParsedFile[]) => void;
88
+ export declare const persistParsedFileChunk: (storagePath: string, shardId: string, parsedFiles: readonly ParsedFile[]) => Promise<boolean>;
92
89
  /**
93
- * Stream the store and return the `ParsedFile`s whose `filePath` is in
94
- * `wantPaths`, keyed by path. Loads one shard at a time and retains only the
95
- * matching entries, so peak heap is bounded by (matched set) + (one shard)
96
- * rather than the whole store. Returns an empty map when the store is absent
97
- * (e.g. tests, or a run with no worker pool) — callers fall back to a fresh
98
- * extract for the missing files.
90
+ * Synchronous shard writer for use INSIDE a parse worker (#1983 parallel
91
+ * serialization). Returns false on write failure so the worker can keep
92
+ * ParsedFiles in the result instead of dropping them.
99
93
  */
94
+ export declare const persistParsedFileShardSync: (storagePath: string, shardId: string, parsedFiles: readonly ParsedFile[]) => boolean;
100
95
  export declare const loadParsedFilesForPaths: (storagePath: string, wantPaths: ReadonlySet<string>) => Promise<Map<string, ParsedFile>>;
101
96
  /** Durable store dir — a sibling of `parsedfile-store/`, NEVER cleared per run. */
102
97
  export declare const getDurableParsedFileDir: (storagePath: string) => string;
@@ -108,43 +103,22 @@ export declare const getDurableParsedFileDir: (storagePath: string) => string;
108
103
  * from accumulating across analyses.
109
104
  */
110
105
  export declare const prepareDurableParsedFileChunk: (durableDir: string, chunkHash: string) => Promise<void>;
106
+ export declare const persistDurableParsedFileShardSync: (durableDir: string, chunkHash: string, threadId: number, shardSeq: number, parsedFiles: readonly ParsedFile[]) => boolean;
111
107
  /**
112
- * Synchronous durable-shard writer for use INSIDE a parse worker, alongside
113
- * {@link persistParsedFileShardSync}. Writes the SAME bytes to a content-addressed
114
- * durable location keyed by the parse chunk hash so a future warm hit can reuse
115
- * them. `chunkHash`+`threadId`+`shardSeq` is collision-free across the
116
- * N-shards-per-chunk fan-out and across worker-death retries — the same
117
- * uniqueness that makes the run-scoped `w<tid>-<seq>` name safe, prefixed by
118
- * content. No-op for an empty chunk.
119
- */
120
- export declare const persistDurableParsedFileShardSync: (durableDir: string, chunkHash: string, threadId: number, shardSeq: number, parsedFiles: readonly ParsedFile[]) => void;
121
- /**
122
- * Restore a cached chunk's durable shards into the run-scoped store on a warm
123
- * hit. A verbatim byte copy (no parse, no re-serialize), so the restored
124
- * ParsedFiles are byte-identical to a cold run and `loadParsedFilesForPaths`
125
- * (which keys on `filePath`, not shard name) gives scope-resolution full
126
- * coverage. The durable shard names already carry the chunk hash, so they never
127
- * collide with the worker's run-scoped `w<tid>-<seq>` shards. Returns the number
128
- * of shards restored (0 ⇒ no durable coverage for this chunk; caller treats it
129
- * as a miss).
130
- */
131
- export declare const restoreDurableParsedFileShard: (durableDir: string, runStoragePath: string, chunkHash: string) => Promise<number>;
132
- /**
133
- * Read the durable index and return the set of chunk hashes it vouches for,
134
- * gated on `expectedVersion` (`PARSE_CACHE_VERSION`). A version mismatch or a
135
- * missing/corrupt index returns the empty set — the caller then treats every
136
- * chunk as a durable miss and re-dispatches workers (NEVER the main-thread
137
- * `extractParsedFile` fallback), which rewrites the durable store under the new
138
- * version. Mirrors `loadParseCache`'s version-invalidation contract.
108
+ * Validate and snapshot one durable chunk into the run store. Every envelope
109
+ * must be runtime-compatible and integrity-valid, and together they must match
110
+ * the path coverage recorded when the durable index was published. Linking
111
+ * before returning pins the inodes against concurrent branch-cache rotation.
139
112
  */
140
- export declare const loadDurableParsedFileIndex: (durableDir: string, expectedVersion: string) => Promise<Set<string>>;
113
+ export declare const durableChunkHasShards: (runStoragePath: string, chunkHash: string, expectedPaths: ReadonlySet<string>) => Promise<boolean>;
114
+ export declare const loadDurableParsedFileIndex: (durableDir: string, expectedVersion: string) => Promise<Map<string, ReadonlySet<string>>>;
141
115
  /**
142
116
  * Prune the durable store to `keepKeys` and rewrite its index. `keepKeys` must
143
117
  * be the parse cache's surviving on-disk keys (so the two stores stay coherent:
144
118
  * a chunk is "cached" iff BOTH its parse-cache shard and its durable shards
145
119
  * exist; a quarantined chunk — no parse-cache shard — drops its durable subdir
146
- * here and re-dispatches next run). Only subdirs with ≥1 shard are indexed
147
- * (mirrors `saveParseCache`'s written-keys discipline — never vouch for a chunk
148
- * hash with no backing shard). The index write is tmp+rename atomic.
120
+ * here and re-dispatches next run). Only chunks whose envelopes all validate
121
+ * are indexed, together with their exact persisted path coverage (never vouch
122
+ * for a missing/corrupt shard). The index write is tmp+rename atomic.
149
123
  */
150
124
  export declare const pruneAndSaveDurableParsedFileStore: (durableDir: string, version: string, keepKeys: ReadonlySet<string>) => Promise<void>;