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 +6 -0
- package/dist/content-processor.js +41 -8
- package/dist/doc2vec.js +20 -2
- package/package.json +1 -1
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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) {
|