@vizalice/codegraph 1.5.0 → 1.6.1

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 (135) hide show
  1. package/README.md +29 -18
  2. package/dist/bin/codegraph.js +127 -25
  3. package/dist/bin/codegraph.js.map +1 -1
  4. package/dist/context/index.d.ts +12 -0
  5. package/dist/context/index.d.ts.map +1 -1
  6. package/dist/context/index.js +47 -8
  7. package/dist/context/index.js.map +1 -1
  8. package/dist/db/index.d.ts +2 -0
  9. package/dist/db/index.d.ts.map +1 -1
  10. package/dist/db/index.js +23 -0
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/db/queries.d.ts +39 -2
  13. package/dist/db/queries.d.ts.map +1 -1
  14. package/dist/db/queries.js +104 -19
  15. package/dist/db/queries.js.map +1 -1
  16. package/dist/directory.d.ts +29 -0
  17. package/dist/directory.d.ts.map +1 -1
  18. package/dist/directory.js +52 -0
  19. package/dist/directory.js.map +1 -1
  20. package/dist/extraction/grammars.d.ts.map +1 -1
  21. package/dist/extraction/grammars.js +38 -2
  22. package/dist/extraction/grammars.js.map +1 -1
  23. package/dist/extraction/index.d.ts +59 -0
  24. package/dist/extraction/index.d.ts.map +1 -1
  25. package/dist/extraction/index.js +175 -53
  26. package/dist/extraction/index.js.map +1 -1
  27. package/dist/extraction/languages/erlang.d.ts.map +1 -1
  28. package/dist/extraction/languages/erlang.js +58 -22
  29. package/dist/extraction/languages/erlang.js.map +1 -1
  30. package/dist/extraction/languages/rust.d.ts +20 -0
  31. package/dist/extraction/languages/rust.d.ts.map +1 -1
  32. package/dist/extraction/languages/rust.js +44 -20
  33. package/dist/extraction/languages/rust.js.map +1 -1
  34. package/dist/extraction/parse-pool.d.ts +7 -0
  35. package/dist/extraction/parse-pool.d.ts.map +1 -1
  36. package/dist/extraction/parse-pool.js +20 -1
  37. package/dist/extraction/parse-pool.js.map +1 -1
  38. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  39. package/dist/extraction/tree-sitter.js +66 -35
  40. package/dist/extraction/tree-sitter.js.map +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +68 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/installer/index.d.ts +2 -2
  45. package/dist/installer/index.d.ts.map +1 -1
  46. package/dist/installer/index.js +8 -7
  47. package/dist/installer/index.js.map +1 -1
  48. package/dist/installer/targets/codex.d.ts +22 -8
  49. package/dist/installer/targets/codex.d.ts.map +1 -1
  50. package/dist/installer/targets/codex.js +65 -48
  51. package/dist/installer/targets/codex.js.map +1 -1
  52. package/dist/installer/targets/types.d.ts +4 -4
  53. package/dist/mcp/daemon-manager.d.ts +1 -1
  54. package/dist/mcp/daemon-manager.d.ts.map +1 -1
  55. package/dist/mcp/daemon-manager.js +1 -1
  56. package/dist/mcp/daemon-manager.js.map +1 -1
  57. package/dist/mcp/daemon-paths.d.ts +6 -0
  58. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  59. package/dist/mcp/daemon-paths.js +54 -0
  60. package/dist/mcp/daemon-paths.js.map +1 -1
  61. package/dist/mcp/daemon-registry.d.ts +10 -0
  62. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  63. package/dist/mcp/daemon-registry.js +51 -3
  64. package/dist/mcp/daemon-registry.js.map +1 -1
  65. package/dist/mcp/daemon.d.ts +9 -6
  66. package/dist/mcp/daemon.d.ts.map +1 -1
  67. package/dist/mcp/daemon.js +10 -8
  68. package/dist/mcp/daemon.js.map +1 -1
  69. package/dist/mcp/engine.d.ts +3 -0
  70. package/dist/mcp/engine.d.ts.map +1 -1
  71. package/dist/mcp/engine.js +45 -3
  72. package/dist/mcp/engine.js.map +1 -1
  73. package/dist/mcp/explore-diagnostics.d.ts +2 -0
  74. package/dist/mcp/explore-diagnostics.d.ts.map +1 -1
  75. package/dist/mcp/explore-diagnostics.js +3 -0
  76. package/dist/mcp/explore-diagnostics.js.map +1 -1
  77. package/dist/mcp/index.d.ts.map +1 -1
  78. package/dist/mcp/index.js +17 -4
  79. package/dist/mcp/index.js.map +1 -1
  80. package/dist/mcp/session.d.ts.map +1 -1
  81. package/dist/mcp/session.js +17 -12
  82. package/dist/mcp/session.js.map +1 -1
  83. package/dist/mcp/tools.d.ts +17 -0
  84. package/dist/mcp/tools.d.ts.map +1 -1
  85. package/dist/mcp/tools.js +164 -25
  86. package/dist/mcp/tools.js.map +1 -1
  87. package/dist/project-config.d.ts +23 -0
  88. package/dist/project-config.d.ts.map +1 -1
  89. package/dist/project-config.js +39 -2
  90. package/dist/project-config.js.map +1 -1
  91. package/dist/resolution/c-fnptr-synthesizer.js +2 -2
  92. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  93. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  94. package/dist/resolution/callback-synthesizer.js +38 -10
  95. package/dist/resolution/callback-synthesizer.js.map +1 -1
  96. package/dist/resolution/frameworks/swift.d.ts.map +1 -1
  97. package/dist/resolution/frameworks/swift.js +11 -1
  98. package/dist/resolution/frameworks/swift.js.map +1 -1
  99. package/dist/resolution/import-resolver.d.ts.map +1 -1
  100. package/dist/resolution/import-resolver.js +16 -1
  101. package/dist/resolution/import-resolver.js.map +1 -1
  102. package/dist/resolution/index.d.ts.map +1 -1
  103. package/dist/resolution/index.js +5 -1
  104. package/dist/resolution/index.js.map +1 -1
  105. package/dist/resolution/name-matcher.d.ts +30 -0
  106. package/dist/resolution/name-matcher.d.ts.map +1 -1
  107. package/dist/resolution/name-matcher.js +278 -0
  108. package/dist/resolution/name-matcher.js.map +1 -1
  109. package/dist/resolution/path-aliases.d.ts +6 -4
  110. package/dist/resolution/path-aliases.d.ts.map +1 -1
  111. package/dist/resolution/path-aliases.js +125 -17
  112. package/dist/resolution/path-aliases.js.map +1 -1
  113. package/dist/search/identifier-segments.d.ts +10 -0
  114. package/dist/search/identifier-segments.d.ts.map +1 -1
  115. package/dist/search/identifier-segments.js +29 -0
  116. package/dist/search/identifier-segments.js.map +1 -1
  117. package/dist/search/query-paths.d.ts +57 -0
  118. package/dist/search/query-paths.d.ts.map +1 -0
  119. package/dist/search/query-paths.js +298 -0
  120. package/dist/search/query-paths.js.map +1 -0
  121. package/dist/search/query-utils.d.ts +1 -1
  122. package/dist/search/query-utils.d.ts.map +1 -1
  123. package/dist/search/query-utils.js +12 -3
  124. package/dist/search/query-utils.js.map +1 -1
  125. package/dist/sync/watcher.d.ts +22 -0
  126. package/dist/sync/watcher.d.ts.map +1 -1
  127. package/dist/sync/watcher.js +51 -5
  128. package/dist/sync/watcher.js.map +1 -1
  129. package/dist/types.d.ts +8 -0
  130. package/dist/types.d.ts.map +1 -1
  131. package/dist/upgrade/index.d.ts +1 -1
  132. package/dist/upgrade/index.d.ts.map +1 -1
  133. package/dist/upgrade/index.js +6 -1
  134. package/dist/upgrade/index.js.map +1 -1
  135. package/package.json +2 -2
@@ -47,6 +47,7 @@ exports.buildDefaultIgnore = buildDefaultIgnore;
47
47
  exports.buildScopeIgnore = buildScopeIgnore;
48
48
  exports.discoverEmbeddedRepoRoots = discoverEmbeddedRepoRoots;
49
49
  exports.findUnindexedIgnoredRepos = findUnindexedIgnoredRepos;
50
+ exports.getGitChangedFiles = getGitChangedFiles;
50
51
  exports.scanDirectory = scanDirectory;
51
52
  exports.scanDirectoryAsync = scanDirectoryAsync;
52
53
  const fs = __importStar(require("fs"));
@@ -1057,7 +1058,14 @@ function getGitChangedFiles(rootDir) {
1057
1058
  }
1058
1059
  }
1059
1060
  function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null, exclude = null) {
1060
- const output = (0, child_process_1.execFileSync)('git', ['status', '--porcelain', '--no-renames'], { cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
1061
+ const output = (0, child_process_1.execFileSync)('git',
1062
+ // `-uall` lists individual untracked files instead of collapsing an
1063
+ // entirely-untracked directory into one `?? dir/` entry, which would
1064
+ // otherwise be dropped here (only embedded git repos are recursed into
1065
+ // below). Nested untracked git repos still collapse to `?? repo/` even
1066
+ // with `-uall` — git never crosses a repo boundary — so the recursion
1067
+ // still handles them. (#1213)
1068
+ ['status', '--porcelain', '--no-renames', '-uall'], { cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
1061
1069
  // This repo's own ignore rules — built-in defaults (#407) plus its .gitignore.
1062
1070
  // Change detection must exclude the SAME files the full index does, but git
1063
1071
  // status hides neither: it ignores nothing for *tracked* paths, and the
@@ -1344,10 +1352,47 @@ class ExtractionOrchestrator {
1344
1352
  * hasn't run yet so single-file re-index paths can detect on the spot.
1345
1353
  */
1346
1354
  detectedFrameworkNames = null;
1355
+ /**
1356
+ * Scope matcher for SCOPED syncs, memoized on the mtimes of the two root
1357
+ * files it is derived from (`codegraph.json`, `.gitignore`). See
1358
+ * {@link scopedSyncMatcher}.
1359
+ */
1360
+ scopedMatcher = null;
1347
1361
  constructor(rootDir, queries) {
1348
1362
  this.rootDir = rootDir;
1349
1363
  this.queries = queries;
1350
1364
  }
1365
+ /**
1366
+ * The scope matcher a scoped sync applies to the paths it was handed — the
1367
+ * same `buildScopeIgnore` the full scan uses, so an explicitly-passed path
1368
+ * that is OUT of scope (a user `exclude` in `codegraph.json`, a `.gitignore`
1369
+ * rule, a built-in default) is treated exactly as the full walk would treat
1370
+ * it: absent, hence removed if tracked, never parsed (#1590).
1371
+ *
1372
+ * Memoized on the root config + root `.gitignore` mtimes: building the
1373
+ * matcher runs embedded-repo discovery (`git ls-files`), which would defeat
1374
+ * the scoped path's whole point (skipping O(repo) work) if paid per sync.
1375
+ * Two `stat`s per sync while nothing changed. An embedded repo created
1376
+ * between config edits joins the scoped matcher on the next full sync, the
1377
+ * same lifecycle the watcher's own matcher already has.
1378
+ */
1379
+ scopedSyncMatcher() {
1380
+ const key = [project_config_1.PROJECT_CONFIG_FILENAME, '.gitignore']
1381
+ .map((name) => {
1382
+ try {
1383
+ return String(fs.statSync(path.join(this.rootDir, name)).mtimeMs);
1384
+ }
1385
+ catch {
1386
+ return '-';
1387
+ }
1388
+ })
1389
+ .join('|');
1390
+ if (this.scopedMatcher && this.scopedMatcher.key === key)
1391
+ return this.scopedMatcher.matcher;
1392
+ const matcher = buildScopeIgnore(this.rootDir);
1393
+ this.scopedMatcher = { key, matcher };
1394
+ return matcher;
1395
+ }
1351
1396
  /**
1352
1397
  * Build a filesystem-backed ResolutionContext sufficient for framework
1353
1398
  * detection. Graph-query methods (getNodesByName etc.) return empty because
@@ -1473,6 +1518,10 @@ class ExtractionOrchestrator {
1473
1518
  });
1474
1519
  if (process.env.CODEGRAPH_SYNTH_TIMINGS)
1475
1520
  console.error(`[phase-timing] scan: ${Date.now() - tScan}ms (${files.length} files)`);
1521
+ // A re-index over an existing DB skips unchanged-hash files at the store,
1522
+ // which would preserve wiped zero-node rows (#1541) — drop them first so
1523
+ // this run stores their files fresh. No-op on a fresh DB.
1524
+ this.healZeroNodeRows();
1476
1525
  // Detect frameworks once per indexAll run using the scanned file list.
1477
1526
  // Names are passed to each parse call so framework-specific extractors
1478
1527
  // (route nodes, middleware, etc.) run after the tree-sitter pass.
@@ -1619,29 +1668,27 @@ class ExtractionOrchestrator {
1619
1668
  // Store: on the writer thread when active (fresh DB — bundles applied
1620
1669
  // in the same file order this chain dispatches them), else on the main
1621
1670
  // thread (SQLite connections are per-thread).
1622
- if (nodeCount > 0 || result.errors.length === 0) {
1623
- const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1624
- if (storeWriter) {
1625
- if (result.kernelBuffers) {
1626
- // Buffers go to the writer as-is; the worker decodes + finalizes.
1627
- // The main thread's only per-file work stays O(1) + the content hash.
1628
- storeWriter.send({
1629
- kernel: true,
1630
- filePath,
1631
- language,
1632
- buffers: result.kernelBuffers,
1633
- file: this.buildFileRecord(filePath, content, language, stats, nodeCount, result.errors),
1634
- });
1635
- }
1636
- else {
1637
- storeWriter.send(this.buildFreshStoreBundle(filePath, content, language, stats, result));
1638
- }
1639
- await storeWriter.waitBelow(STORE_WRITER_WINDOW);
1671
+ const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1672
+ if (storeWriter) {
1673
+ if (result.kernelBuffers) {
1674
+ // Buffers go to the writer as-is; the worker decodes + finalizes.
1675
+ // The main thread's only per-file work stays O(1) + the content hash.
1676
+ storeWriter.send({
1677
+ kernel: true,
1678
+ filePath,
1679
+ language,
1680
+ buffers: result.kernelBuffers,
1681
+ file: this.buildFileRecord(filePath, content, language, stats, nodeCount, result.errors),
1682
+ });
1640
1683
  }
1641
1684
  else {
1642
- const materialized = (0, kernel_1.materializeKernelResult)(result, filePath, language);
1643
- await this.storeExtractionResult(filePath, content, language, stats, materialized, commitYield);
1685
+ storeWriter.send(this.buildFreshStoreBundle(filePath, content, language, stats, result));
1644
1686
  }
1687
+ await storeWriter.waitBelow(STORE_WRITER_WINDOW);
1688
+ }
1689
+ else {
1690
+ const materialized = (0, kernel_1.materializeKernelResult)(result, filePath, language);
1691
+ await this.storeExtractionResult(filePath, content, language, stats, materialized, commitYield);
1645
1692
  }
1646
1693
  if (result.errors.length > 0) {
1647
1694
  for (const err of result.errors) {
@@ -1672,16 +1719,19 @@ class ExtractionOrchestrator {
1672
1719
  }
1673
1720
  onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
1674
1721
  };
1675
- const recordParseFailure = (filePath, err) => {
1676
- processed++;
1677
- filesErrored++;
1678
- errors.push({
1679
- message: err instanceof Error ? err.message : String(err),
1680
- filePath,
1681
- severity: 'error',
1682
- code: 'parse_error',
1722
+ const recordParseFailure = async (filePath, content, stats, err) => {
1723
+ await storeResult(filePath, content, stats, {
1724
+ nodes: [],
1725
+ edges: [],
1726
+ unresolvedReferences: [],
1727
+ errors: [{
1728
+ message: err instanceof Error ? err.message : String(err),
1729
+ filePath,
1730
+ severity: 'error',
1731
+ code: 'parse_error',
1732
+ }],
1733
+ durationMs: 0,
1683
1734
  });
1684
- onProgress?.({ phase: 'parsing', current: processed, total });
1685
1735
  };
1686
1736
  // Commit buffered parses to the DB in file order, advancing the cursor over
1687
1737
  // contiguous completed results. Runs after each parse settles (and once more
@@ -1706,7 +1756,7 @@ class ExtractionOrchestrator {
1706
1756
  if (item.ok)
1707
1757
  await storeResult(item.filePath, item.content, item.stats, item.result);
1708
1758
  else
1709
- recordParseFailure(item.filePath, item.err);
1759
+ await recordParseFailure(item.filePath, item.content, item.stats, item.err);
1710
1760
  }
1711
1761
  }
1712
1762
  catch (err) {
@@ -1726,7 +1776,7 @@ class ExtractionOrchestrator {
1726
1776
  completed.set(seq, { ok: true, filePath, content, stats, result });
1727
1777
  }
1728
1778
  catch (parseErr) {
1729
- completed.set(seq, { ok: false, filePath, err: parseErr });
1779
+ completed.set(seq, { ok: false, filePath, content, stats, err: parseErr });
1730
1780
  }
1731
1781
  flushOrdered();
1732
1782
  })();
@@ -1798,15 +1848,18 @@ class ExtractionOrchestrator {
1798
1848
  // useful symbols. The single-file extractFile path already enforces
1799
1849
  // this; the bulk path used to silently skip the check.
1800
1850
  if (stats.size > MAX_FILE_SIZE) {
1801
- processed++;
1802
- filesSkipped++;
1803
- errors.push({
1804
- message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
1805
- filePath,
1806
- severity: 'warning',
1807
- code: 'size_exceeded',
1851
+ await storeResult(filePath, content, stats, {
1852
+ nodes: [],
1853
+ edges: [],
1854
+ unresolvedReferences: [],
1855
+ errors: [{
1856
+ message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
1857
+ filePath,
1858
+ severity: 'warning',
1859
+ code: 'size_exceeded',
1860
+ }],
1861
+ durationMs: 0,
1808
1862
  });
1809
- onProgress?.({ phase: 'parsing', current: processed, total });
1810
1863
  continue;
1811
1864
  }
1812
1865
  // Parse on the pool (main thread stays unblocked). Errors/timeouts are
@@ -1907,8 +1960,15 @@ class ExtractionOrchestrator {
1907
1960
  stillFailing.push(errEntry);
1908
1961
  continue;
1909
1962
  }
1963
+ // The pool hands kernel results back as an undecoded buffer transport
1964
+ // (`nodes`/`edges` EMPTY, tables in kernelBuffers). The main loop
1965
+ // decodes or forwards to the store worker; this path stores directly,
1966
+ // so decode here — otherwise a kernel-language retry passes the gate
1967
+ // below via `errors.length === 0`, stores nothing, and the file is
1968
+ // permanently recorded as "(0 symbols)" with the error erased (#1541).
1969
+ const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1970
+ result = (0, kernel_1.materializeKernelResult)(result, filePath, language);
1910
1971
  if (result.nodes.length > 0 || result.errors.length === 0) {
1911
- const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1912
1972
  const stats = await fsp.stat(path.join(this.rootDir, filePath));
1913
1973
  await this.storeExtractionResult(filePath, content, language, stats, result, commitYield);
1914
1974
  const idx = errors.indexOf(errEntry);
@@ -1955,13 +2015,20 @@ class ExtractionOrchestrator {
1955
2015
  catch {
1956
2016
  continue;
1957
2017
  }
2018
+ // Same undecoded-transport hazard as the first retry pass (#1541).
2019
+ const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
2020
+ result = (0, kernel_1.materializeKernelResult)(result, filePath, language);
1958
2021
  if (result.nodes.length > 0 || result.errors.length === 0) {
1959
- const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
1960
2022
  const stats = await fsp.stat(path.join(this.rootDir, filePath));
1961
2023
  await this.storeExtractionResult(filePath, fullContent, language, stats, result, commitYield);
1962
- const idx = errors.indexOf(errEntry);
1963
- if (idx >= 0)
1964
- errors.splice(idx, 1);
2024
+ // Salvaged from comment-stripped source: keep a visible trace in
2025
+ // the summary instead of erasing the failure outright — the
2026
+ // stored result may be missing whatever the failing parse choked
2027
+ // on, and a silently "clean" file here is how an index quietly
2028
+ // disagrees with a later per-file sync of the same bytes (#1565).
2029
+ errEntry.severity = 'warning';
2030
+ errEntry.code = 'salvaged_stripped';
2031
+ errEntry.message = `Indexed from comment-stripped source after repeated parse failures (symbols may be incomplete until the file is re-indexed): ${errEntry.message}`;
1965
2032
  filesErrored--;
1966
2033
  filesIndexed++;
1967
2034
  totalNodes += result.nodes.length;
@@ -2088,9 +2155,10 @@ class ExtractionOrchestrator {
2088
2155
  durationMs: 0,
2089
2156
  };
2090
2157
  }
2158
+ const language = (0, grammars_1.detectLanguage)(relativePath, content, (0, project_config_1.loadExtensionOverrides)(this.rootDir));
2091
2159
  // Check file size
2092
2160
  if (stats.size > MAX_FILE_SIZE) {
2093
- return {
2161
+ const result = {
2094
2162
  nodes: [],
2095
2163
  edges: [],
2096
2164
  unresolvedReferences: [],
@@ -2104,9 +2172,10 @@ class ExtractionOrchestrator {
2104
2172
  ],
2105
2173
  durationMs: 0,
2106
2174
  };
2175
+ await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
2176
+ return result;
2107
2177
  }
2108
2178
  // Detect language (honoring the project's codegraph.json extension overrides)
2109
- const language = (0, grammars_1.detectLanguage)(relativePath, content, (0, project_config_1.loadExtensionOverrides)(this.rootDir));
2110
2179
  if (!(0, grammars_1.isLanguageSupported)(language)) {
2111
2180
  return {
2112
2181
  nodes: [],
@@ -2122,15 +2191,43 @@ class ExtractionOrchestrator {
2122
2191
  const frameworkNames = this.ensureDetectedFrameworks();
2123
2192
  const result = (0, tree_sitter_1.extractFromSource)(relativePath, content, language, frameworkNames);
2124
2193
  // Store in database
2125
- if (result.nodes.length > 0 || result.errors.length === 0) {
2126
- await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
2127
- }
2194
+ await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
2128
2195
  return result;
2129
2196
  }
2130
2197
  /**
2131
2198
  * Store extraction result in database
2132
2199
  */
2200
+ /**
2201
+ * Delete file rows recorded with ZERO nodes so their files re-index.
2202
+ *
2203
+ * No extraction path stores an empty, error-free result for a
2204
+ * symbol-bearing language — even an empty file keeps its file node — so a
2205
+ * zero-node row is a wiped one (#1541: an interrupted parse's retry stored
2206
+ * an undecoded kernel transport). The wiped row's content hash matches the
2207
+ * on-disk bytes, so every hash-based reconcile skips the file forever;
2208
+ * deleting the row lets the normal add path repair it. File-level-only
2209
+ * languages (yaml, twig, properties) are left alone. Deleting a zero-node
2210
+ * row cascades nothing: it has no nodes, so no edges or refs either.
2211
+ */
2212
+ healZeroNodeRows() {
2213
+ for (const f of this.queries.getAllFiles()) {
2214
+ // A zero-node row WITH recorded errors is a deliberate skip marker
2215
+ // (#1557: oversized / repeatedly-unparseable files are persisted with
2216
+ // their reason so syncs stop retrying them) — leave those alone. The
2217
+ // #1541 wipe rows are the error-FREE zero-node rows.
2218
+ if (f.nodeCount === 0 &&
2219
+ !(0, grammars_1.isFileLevelOnlyLanguage)(f.language) &&
2220
+ (f.errors === undefined || f.errors.length === 0)) {
2221
+ this.queries.deleteFile(f.path);
2222
+ }
2223
+ }
2224
+ }
2133
2225
  async storeExtractionResult(filePath, content, language, stats, result, onYield) {
2226
+ // A kernel result can arrive as an undecoded buffer transport (empty
2227
+ // node/edge arrays, tables riding in kernelBuffers). Decode it before
2228
+ // storing — persisting the transport as-is records the file as having no
2229
+ // symbols at all (#1541). No-op for already-decoded results.
2230
+ result = (0, kernel_1.materializeKernelResult)(result, filePath, language);
2134
2231
  // Bulk inserts run in bounded sub-transactions with a yield between, so a
2135
2232
  // giant generated file (tens of thousands of symbols) can't block the
2136
2233
  // event loop — and the #850 watchdog heartbeat — for the whole store.
@@ -2139,10 +2236,19 @@ class ExtractionOrchestrator {
2139
2236
  // is unchanged: a partially-stored file has no record and re-indexes.
2140
2237
  const STORE_CHUNK = 2000;
2141
2238
  const contentHash = hashContent(content);
2142
- // Check if file already exists and hasn't changed
2239
+ // Check if file already exists and hasn't changed. A skip/failure MARKER
2240
+ // row (zero nodes + recorded errors, #1557) never blocks a store carrying
2241
+ // real content: markers are written BEFORE the retry pass under the same
2242
+ // content hash, so treating them as "no changes" would silently discard a
2243
+ // successful retry's symbols — a permanent empty file presented as
2244
+ // recovered (the #1541 wipe, reintroduced through the marker path).
2143
2245
  const existingFile = this.queries.getFileByPath(filePath);
2144
2246
  if (existingFile && existingFile.contentHash === contentHash) {
2145
- return; // No changes
2247
+ const existingIsMarker = existingFile.nodeCount === 0 && (existingFile.errors?.length ?? 0) > 0;
2248
+ const incomingHasContent = result.nodes.length > 0;
2249
+ if (!existingIsMarker || !incomingHasContent) {
2250
+ return; // No changes
2251
+ }
2146
2252
  }
2147
2253
  // Re-decided on every re-index of a changed file, so a banner added (or
2148
2254
  // removed) by an edit is reflected on the next sync (#1500). Computed after
@@ -2439,7 +2545,20 @@ class ExtractionOrchestrator {
2439
2545
  // reads `filesChecked === 0 && durationMs === 0` as the
2440
2546
  // lock-unavailable signature (#449).
2441
2547
  const unique = [...new Set(scopedPaths)];
2442
- currentFiles = unique.filter((p) => fs.existsSync(path.join(this.rootDir, p)));
2548
+ // A scoped path is "present" only if it exists AND is in scope — the
2549
+ // same two gates the full walk applies (source extension, scope
2550
+ // matcher). Without the scope gate a caller's stale view of scope
2551
+ // leaked straight into the index: the watcher re-parsed a file the
2552
+ // user had just excluded in `codegraph.json` while `codegraph sync`
2553
+ // removed it (#1590). Out-of-scope paths fall out of `currentFiles`,
2554
+ // so a tracked one takes the removal branch below, exactly as a full
2555
+ // sync would treat it. (`include`-forced paths pass: ScopeIgnore
2556
+ // applies the include precedence itself.)
2557
+ const scope = this.scopedSyncMatcher();
2558
+ const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
2559
+ currentFiles = unique.filter((p) => (0, grammars_1.isSourceFile)(p, overrides) &&
2560
+ !scope.ignores(p) &&
2561
+ fs.existsSync(path.join(this.rootDir, p)));
2443
2562
  trackedFiles = [];
2444
2563
  for (const p of unique) {
2445
2564
  const rec = this.queries.getFileByPath(p);
@@ -2455,6 +2574,9 @@ class ExtractionOrchestrator {
2455
2574
  if (process.env.CODEGRAPH_SYNTH_TIMINGS)
2456
2575
  console.error(`[phase-timing] sync-scan: ${Date.now() - tSyncScan}ms (${currentFiles.length} files)`);
2457
2576
  filesChecked = currentFiles.length;
2577
+ // Full reconcile only (scoped syncs must not touch rows outside their
2578
+ // scope): drop zero-node rows so the wiped files re-index as adds below.
2579
+ this.healZeroNodeRows();
2458
2580
  const tTracked = Date.now();
2459
2581
  trackedFiles = this.queries.getAllFiles();
2460
2582
  if (process.env.CODEGRAPH_SYNTH_TIMINGS)