doc2vec 2.13.0 → 2.13.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.
package/README.md CHANGED
@@ -767,6 +767,12 @@ If you don't specify a config path, it will look for config.yaml in the current
767
767
 
768
768
  ## Recent Changes
769
769
 
770
+ ### Code Scans: Dangling Symlinks and Incomplete Walks
771
+ - **Dangling symlinks no longer abort a directory:** `statSync` follows symlinks, so a link whose target isn't in the clone (common in vendored forks, e.g. `crates/cel-fork/cel/README.md`) threw `ENOENT` and abandoned every remaining entry in that directory. Broken entries are now skipped with a warning and the walk continues
772
+ - **Incomplete scans never delete chunks:** if a directory genuinely can't be listed, the scan is reported as incomplete — obsolete-file cleanup, the tracked-file list, the last-mtime marker, and the last-synced git SHA are all skipped, so unscanned files keep their chunks and the next run rescans them
773
+ - **Incomplete scans fail the source:** a partial ingest is reported as a failed source instead of a silent success
774
+ - **Quieter crawls:** planned browser restarts (initial launch, the every-50-pages recycle) log at `info`; only unplanned restarts (disconnected or degraded browser) warn
775
+
770
776
  ### Searchable Controller Run Logs
771
777
  - **Server-side log filtering:** Level chips and the keyword filter now query Postgres instead of the browser's in-memory buffer, so errors and warnings from the start of a long run are reachable (`GET /api/runs/:id/logs?levels=error,warn&q=...`, paged with **Load 2,000 more**)
772
778
  - **Whole-run level counts:** New `GET /api/runs/:id/logs/counts` endpoint backs the level chips, which previously only counted the lines still held in the browser
@@ -462,8 +462,14 @@ class ContentProcessor {
462
462
  let pagesSinceRestart = 0;
463
463
  // Restart the browser every N pages to prevent memory accumulation
464
464
  const PAGES_BETWEEN_RESTARTS = 50;
465
- const restartBrowser = async (reason) => {
466
- logger.warn(`Restarting browser: ${reason}`);
465
+ // Planned restarts (first launch, the every-N-pages recycle) are routine
466
+ // and would otherwise dominate a crawl's warning count; only unplanned
467
+ // ones — a disconnected or degraded browser — deserve a warning.
468
+ const restartBrowser = async (reason, planned = false) => {
469
+ if (planned)
470
+ logger.info(`Restarting browser: ${reason}`);
471
+ else
472
+ logger.warn(`Restarting browser: ${reason}`);
467
473
  if (browser) {
468
474
  try {
469
475
  await browser.close();
@@ -485,11 +491,11 @@ class ContentProcessor {
485
491
  const reason = needsBrowserRestart
486
492
  ? `protocol errors persisting after page recreation (${consecutiveProtocolErrors} consecutive)`
487
493
  : browser ? 'browser disconnected' : 'initial launch';
488
- await restartBrowser(reason);
494
+ await restartBrowser(reason, !browser && !needsBrowserRestart);
489
495
  }
490
496
  else if (pagesSinceRestart >= PAGES_BETWEEN_RESTARTS) {
491
497
  // Periodic restart to prevent memory accumulation from long crawls
492
- await restartBrowser(`periodic restart after ${pagesSinceRestart} pages`);
498
+ await restartBrowser(`periodic restart after ${pagesSinceRestart} pages`, true);
493
499
  }
494
500
  else if (pageNeedsRecreation) {
495
501
  // The previous page.evaluate or navigation timed out / errored.
@@ -1539,7 +1545,17 @@ class ContentProcessor {
1539
1545
  let skippedFiles = 0;
1540
1546
  for (const file of files) {
1541
1547
  const filePath = path.join(dirPath, file);
1542
- const stat = fs.statSync(filePath);
1548
+ // A dangling symlink makes statSync throw ENOENT; skip the
1549
+ // entry rather than abandoning the rest of the directory
1550
+ let stat;
1551
+ try {
1552
+ stat = fs.statSync(filePath);
1553
+ }
1554
+ catch (error) {
1555
+ logger.warn(`Skipping unreadable entry ${filePath}:`, error);
1556
+ skippedFiles++;
1557
+ continue;
1558
+ }
1543
1559
  // Skip already visited paths
1544
1560
  if (visitedPaths.has(filePath)) {
1545
1561
  logger.debug(`Skipping already visited path: ${filePath}`);
@@ -1629,6 +1645,7 @@ class ContentProcessor {
1629
1645
  const excludeExtensions = config.exclude_extensions || [];
1630
1646
  const encoding = config.encoding || 'utf8';
1631
1647
  let maxMtime = 0;
1648
+ let incomplete = false;
1632
1649
  try {
1633
1650
  const files = fs.readdirSync(dirPath);
1634
1651
  let processedFiles = 0;
@@ -1638,7 +1655,19 @@ class ContentProcessor {
1638
1655
  const trackFiles = options?.trackFiles;
1639
1656
  for (const file of files) {
1640
1657
  const filePath = path.join(dirPath, file);
1641
- const stat = fs.statSync(filePath);
1658
+ // statSync follows symlinks, so a dangling one (common in
1659
+ // vendored forks) throws ENOENT. Skipping the entry keeps the
1660
+ // rest of the directory — letting it throw used to abandon
1661
+ // every sibling after it, and cleanup then deleted their chunks.
1662
+ let stat;
1663
+ try {
1664
+ stat = fs.statSync(filePath);
1665
+ }
1666
+ catch (error) {
1667
+ logger.warn(`Skipping unreadable entry ${filePath}:`, error);
1668
+ skippedFiles++;
1669
+ continue;
1670
+ }
1642
1671
  if (visitedPaths.has(filePath)) {
1643
1672
  logger.debug(`Skipping already visited path: ${filePath}`);
1644
1673
  continue;
@@ -1650,6 +1679,7 @@ class ContentProcessor {
1650
1679
  processedFiles += childResult.processedFiles;
1651
1680
  skippedFiles += childResult.skippedFiles;
1652
1681
  maxMtime = Math.max(maxMtime, childResult.maxMtime);
1682
+ incomplete = incomplete || childResult.incomplete;
1653
1683
  }
1654
1684
  else {
1655
1685
  logger.debug(`Skipping directory ${filePath} (recursive=false)`);
@@ -1694,11 +1724,14 @@ class ContentProcessor {
1694
1724
  }
1695
1725
  }
1696
1726
  logger.info(`Code directory processed. Processed: ${processedFiles}, Skipped: ${skippedFiles}`);
1697
- return { processedFiles, skippedFiles, maxMtime };
1727
+ return { processedFiles, skippedFiles, maxMtime, incomplete };
1698
1728
  }
1699
1729
  catch (error) {
1730
+ // The directory could not be listed at all — report the scan as
1731
+ // incomplete so the caller skips cleanup instead of treating every
1732
+ // file under this path as deleted
1700
1733
  logger.error(`Error reading code directory ${dirPath}:`, error);
1701
- return { processedFiles: 0, skippedFiles: 0, maxMtime };
1734
+ return { processedFiles: 0, skippedFiles: 0, maxMtime, incomplete: true };
1702
1735
  }
1703
1736
  }
1704
1737
  async getTokenChunker(chunkSize) {
package/dist/doc2vec.js CHANGED
@@ -1051,6 +1051,10 @@ class Doc2Vec {
1051
1051
  let lastMtimeKey;
1052
1052
  let trackedFiles;
1053
1053
  let maxObservedMtime = 0;
1054
+ // Set when a directory could not be listed: the files we saw are only a
1055
+ // subset of what is on disk, so nothing may be treated as deleted and no
1056
+ // "last scanned" marker may advance past this run
1057
+ let scanIncomplete = false;
1054
1058
  if (config.source === 'local_directory') {
1055
1059
  if (!config.path) {
1056
1060
  logger.error('Code source type local_directory requires a path.');
@@ -1132,13 +1136,20 @@ class Doc2Vec {
1132
1136
  mtimeCutoff,
1133
1137
  trackFiles: trackedFiles
1134
1138
  });
1139
+ scanIncomplete = scanResult.incomplete;
1140
+ if (scanIncomplete) {
1141
+ logger.error('Directory scan was incomplete — skipping cleanup so unscanned files keep their chunks');
1142
+ }
1135
1143
  if (trackedFiles) {
1136
1144
  maxObservedMtime = scanResult.maxMtime;
1137
1145
  }
1138
1146
  }
1139
1147
  finally {
1140
1148
  logger.section('CLEANUP');
1141
- if (incrementalMode) {
1149
+ if (scanIncomplete) {
1150
+ logger.warn('Skipping cleanup and last-scanned markers: the scan did not cover the whole tree');
1151
+ }
1152
+ else if (incrementalMode) {
1142
1153
  if (deleteUrls.length > 0) {
1143
1154
  logger.info(`Cleaning up ${deleteUrls.length} deleted/renamed files`);
1144
1155
  for (const url of deleteUrls) {
@@ -1197,7 +1208,9 @@ class Doc2Vec {
1197
1208
  this.counters.items_deleted += removed.items;
1198
1209
  this.counters.chunks_deleted += removed.chunks;
1199
1210
  }
1200
- if (config.source === 'github' && basePath && repoBranch) {
1211
+ // Storing the SHA after an incomplete scan would make the next run
1212
+ // diff from it and never revisit the files this run missed
1213
+ if (config.source === 'github' && basePath && repoBranch && !scanIncomplete) {
1201
1214
  const headSha = await this.getRepoHeadSha(basePath, logger);
1202
1215
  if (headSha) {
1203
1216
  const shaKey = this.buildCodeShaMetadataKey(config.repo, repoBranch);
@@ -1214,6 +1227,11 @@ class Doc2Vec {
1214
1227
  }
1215
1228
  }
1216
1229
  }
1230
+ // Fail the source rather than reporting a partial ingest as a success —
1231
+ // the collection is still serving whatever it had for the unscanned files
1232
+ if (scanIncomplete) {
1233
+ throw new Error(`Code scan of ${basePath} was incomplete: at least one directory could not be read`);
1234
+ }
1217
1235
  logger.info(`Finished processing code source (${config.source})`);
1218
1236
  }
1219
1237
  buildCodeShaMetadataKey(repo, branch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "type": "commonjs",
5
5
  "description": "",
6
6
  "main": "dist/doc2vec.js",