doc2vec 2.14.2 → 2.15.0

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.
@@ -72,12 +72,12 @@ exports.BrowserLaunchError = BrowserLaunchError;
72
72
  * Chrome for Testing is not published)
73
73
  * 4. undefined — let puppeteer decide
74
74
  */
75
- function resolveBrowserExecutablePath() {
75
+ async function resolveBrowserExecutablePath() {
76
76
  const override = process.env.PUPPETEER_EXECUTABLE_PATH;
77
77
  if (override)
78
78
  return override;
79
79
  try {
80
- const bundled = puppeteer_1.default.executablePath();
80
+ const bundled = await puppeteer_1.default.executablePath();
81
81
  if (bundled && fs.existsSync(bundled))
82
82
  return bundled;
83
83
  }
@@ -421,7 +421,7 @@ class ContentProcessor {
421
421
  let browser = null;
422
422
  let page = null;
423
423
  const launchBrowser = async () => {
424
- const executablePath = resolveBrowserExecutablePath();
424
+ const executablePath = await resolveBrowserExecutablePath();
425
425
  const launchOptions = {
426
426
  executablePath,
427
427
  args: [
@@ -487,7 +487,7 @@ class ContentProcessor {
487
487
  // If protocol errors are persisting after page recreation, the browser
488
488
  // process itself is degraded — a full restart is needed.
489
489
  const needsBrowserRestart = pageNeedsRecreation && consecutiveProtocolErrors >= 1;
490
- if (!browser || !browser.isConnected() || needsBrowserRestart) {
490
+ if (!browser || !browser.connected || needsBrowserRestart) {
491
491
  const reason = needsBrowserRestart
492
492
  ? `protocol errors persisting after page recreation (${consecutiveProtocolErrors} consecutive)`
493
493
  : browser ? 'browser disconnected' : 'initial launch';
@@ -793,7 +793,9 @@ class ContentProcessor {
793
793
  // A browser that cannot launch is fatal for the whole crawl —
794
794
  // rethrow so the source (and the run) is reported as failed
795
795
  // rather than logging an error per URL and finishing "green".
796
- if (error instanceof BrowserLaunchError) {
796
+ // Same for a permanent embedding failure (bad API key):
797
+ // every remaining page would fail identically.
798
+ if (error instanceof BrowserLaunchError || error?.name === 'FatalEmbeddingError') {
797
799
  logger.error(`Aborting crawl of ${baseUrl}: ${error.message}`);
798
800
  throw error;
799
801
  }
@@ -858,7 +860,7 @@ class ContentProcessor {
858
860
  finally {
859
861
  // Close the shared browser instance when the crawl is done
860
862
  const browserToClose = browser;
861
- if (browserToClose && browserToClose.isConnected()) {
863
+ if (browserToClose && browserToClose.connected) {
862
864
  await browserToClose.close();
863
865
  logger.debug('Shared browser closed after crawl completed');
864
866
  }
@@ -962,7 +964,7 @@ class ContentProcessor {
962
964
  else {
963
965
  // Standalone mode: launch a browser for this single page
964
966
  browser = await puppeteer_1.default.launch({
965
- executablePath: resolveBrowserExecutablePath(),
967
+ executablePath: await resolveBrowserExecutablePath(),
966
968
  args: [
967
969
  '--no-sandbox',
968
970
  '--disable-setuid-sandbox',
@@ -1143,7 +1145,7 @@ class ContentProcessor {
1143
1145
  }
1144
1146
  finally {
1145
1147
  // Only close the browser if we launched it ourselves (standalone mode)
1146
- if (ownsTheBrowser && browser && browser.isConnected()) {
1148
+ if (ownsTheBrowser && browser && browser.connected) {
1147
1149
  await browser.close();
1148
1150
  logger.debug(`Browser closed for ${url}`);
1149
1151
  }
package/dist/doc2vec.js CHANGED
@@ -39,7 +39,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.Doc2Vec = void 0;
41
41
  const axios_1 = __importDefault(require("axios"));
42
- const crypto_1 = __importDefault(require("crypto"));
43
42
  const yaml = __importStar(require("js-yaml"));
44
43
  const fs = __importStar(require("fs"));
45
44
  const path = __importStar(require("path"));
@@ -481,6 +480,30 @@ class Doc2Vec {
481
480
  };
482
481
  const chunks = await this.contentProcessor.chunkMarkdown(markdown, issueConfig, url);
483
482
  logger.info(`Issue #${issueNumber}: Created ${chunks.length} chunks`);
483
+ // Embed every chunk BEFORE touching stored data, so an embedding
484
+ // failure can't purge the issue's existing chunks and store nothing
485
+ // back. A FatalEmbeddingError (bad API key) propagates and aborts
486
+ // the source; transient failures skip this issue but count toward
487
+ // failedChunks so the last-run date is not advanced.
488
+ const embedded = [];
489
+ let failedEmbeddings = 0;
490
+ for (const chunk of chunks) {
491
+ const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
492
+ const embeddings = await this.createEmbeddings([chunk.content]);
493
+ if (embeddings.length) {
494
+ embedded.push(embeddings[0]);
495
+ }
496
+ else {
497
+ failedEmbeddings++;
498
+ logger.error(`Embedding failed for chunk: ${chunkId}`);
499
+ }
500
+ }
501
+ if (failedEmbeddings > 0) {
502
+ failedChunks += failedEmbeddings;
503
+ logger.error(`Issue #${issueNumber}: ${failedEmbeddings}/${chunks.length} chunks failed to embed; ` +
504
+ `keeping its existing chunks so the next run retries`);
505
+ return;
506
+ }
484
507
  // Purge the issue's existing chunks before inserting the fresh set.
485
508
  // chunk_id is a content hash, so when an issue changes (closed/reopened,
486
509
  // edited, new comments) the regenerated chunks get new ids and the old
@@ -501,57 +524,21 @@ class Doc2Vec {
501
524
  else {
502
525
  this.counters.items_new++;
503
526
  }
504
- // Process and store each chunk immediately
505
- for (const chunk of chunks) {
527
+ // Store each chunk
528
+ for (let i = 0; i < chunks.length; i++) {
529
+ const chunk = chunks[i];
506
530
  const chunkHash = utils_1.Utils.generateHash(chunk.content);
507
531
  const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
508
532
  if (dbConnection.type === 'sqlite') {
509
- const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
510
- const existing = checkHashStmt.get(chunk.metadata.chunk_id);
511
- if (existing && existing.hash === chunkHash) {
512
- logger.info(`Skipping unchanged chunk: ${chunkId}`);
513
- continue;
514
- }
515
- const embeddings = await this.createEmbeddings([chunk.content]);
516
- if (embeddings.length) {
517
- database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embeddings[0], logger, chunkHash);
518
- this.counters.chunks_added++;
519
- logger.debug(`Stored chunk ${chunkId} in SQLite`);
520
- }
521
- else {
522
- logger.error(`Embedding failed for chunk: ${chunkId}`);
523
- }
533
+ database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedded[i], logger, chunkHash);
534
+ this.counters.chunks_added++;
535
+ logger.debug(`Stored chunk ${chunkId} in SQLite`);
524
536
  }
525
537
  else if (dbConnection.type === 'qdrant') {
526
538
  try {
527
- let pointId;
528
- try {
529
- pointId = chunk.metadata.chunk_id;
530
- if (!utils_1.Utils.isValidUuid(pointId)) {
531
- pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
532
- }
533
- }
534
- catch (e) {
535
- pointId = crypto_1.default.randomUUID();
536
- }
537
- const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
538
- ids: [pointId],
539
- with_payload: true,
540
- with_vector: false,
541
- });
542
- if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
543
- logger.info(`Skipping unchanged chunk: ${chunkId}`);
544
- continue;
545
- }
546
- const embeddings = await this.createEmbeddings([chunk.content]);
547
- if (embeddings.length) {
548
- await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embeddings[0], chunkHash);
549
- this.counters.chunks_added++;
550
- logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
551
- }
552
- else {
553
- logger.error(`Embedding failed for chunk: ${chunkId}`);
554
- }
539
+ await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedded[i], chunkHash);
540
+ this.counters.chunks_added++;
541
+ logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
555
542
  }
556
543
  catch (error) {
557
544
  // Keep going so one bad chunk can't cost the whole repo,
@@ -680,6 +667,8 @@ class Doc2Vec {
680
667
  return true;
681
668
  }
682
669
  catch (error) {
670
+ if (error instanceof types_1.FatalEmbeddingError)
671
+ throw error;
683
672
  logger.error(`Error during chunking or embedding for ${url}:`, error);
684
673
  return false;
685
674
  }
@@ -829,6 +818,8 @@ class Doc2Vec {
829
818
  await this.processChunksForUrl(chunks, fileUrl, dbConnection, logger);
830
819
  }
831
820
  catch (error) {
821
+ if (error instanceof types_1.FatalEmbeddingError)
822
+ throw error;
832
823
  logger.error(`Error during chunking or embedding for ${filePath}:`, error);
833
824
  }
834
825
  }, logger);
@@ -999,6 +990,8 @@ class Doc2Vec {
999
990
  processedCount++;
1000
991
  }
1001
992
  catch (error) {
993
+ if (error instanceof types_1.FatalEmbeddingError)
994
+ throw error;
1002
995
  logger.error(`Error processing S3 object ${obj.key}:`, error);
1003
996
  }
1004
997
  }
@@ -1155,6 +1148,8 @@ class Doc2Vec {
1155
1148
  await this.processChunksForUrl(chunks, fileUrl, dbConnection, logger);
1156
1149
  }
1157
1150
  catch (error) {
1151
+ if (error instanceof types_1.FatalEmbeddingError)
1152
+ throw error;
1158
1153
  logger.error(`Error during code chunking or embedding for ${filePath}:`, error);
1159
1154
  }
1160
1155
  }, logger, undefined, {
@@ -1594,6 +1589,8 @@ class Doc2Vec {
1594
1589
  totalTickets++;
1595
1590
  }
1596
1591
  catch (error) {
1592
+ if (error instanceof types_1.FatalEmbeddingError)
1593
+ throw error;
1597
1594
  failedTickets++;
1598
1595
  logger.error(`Failed to process ticket #${ticket.id}, will retry next run: ${error.message}`);
1599
1596
  }
@@ -1781,9 +1778,36 @@ class Doc2Vec {
1781
1778
  this.counters.items_unchanged++;
1782
1779
  return 0;
1783
1780
  }
1784
- // 4. Content changed — delete all existing chunks for this URL
1781
+ // 4. Content changed — embed all new chunks BEFORE touching stored
1782
+ // data, so an embedding failure (e.g. a bad API key) can't delete the
1783
+ // existing chunks and then store nothing back. Throwing here also
1784
+ // signals the caller that the item failed: for websites that means the
1785
+ // ETag/lastmod is not stored and the page is retried next run.
1786
+ const chunkProgress = logger.progress(`Embedding chunks for ${url}`, chunks.length);
1787
+ const embedded = [];
1788
+ let failedEmbeddings = 0;
1789
+ for (let i = 0; i < chunks.length; i++) {
1790
+ const chunk = chunks[i];
1791
+ const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
1792
+ const embeddings = await this.createEmbeddings([chunk.content]);
1793
+ if (embeddings.length > 0) {
1794
+ embedded[i] = embeddings[0];
1795
+ chunkProgress.update(1, `Embedded chunk ${chunkId}`);
1796
+ }
1797
+ else {
1798
+ failedEmbeddings++;
1799
+ logger.error(`Embedding failed for chunk: ${chunkId}`);
1800
+ chunkProgress.update(1, `Failed to embed chunk ${chunkId}`);
1801
+ }
1802
+ }
1803
+ if (failedEmbeddings > 0) {
1804
+ chunkProgress.complete();
1805
+ throw new Error(`embedding failed for ${failedEmbeddings}/${chunks.length} chunks of ${url}; ` +
1806
+ `keeping the ${existingHashesSorted.length} existing chunks so the next run retries`);
1807
+ }
1808
+ // 5. All embeddings succeeded — delete the old chunks and insert the new set
1785
1809
  if (existingHashesSorted.length > 0) {
1786
- logger.info(`Content changed for ${url} (${existingHashesSorted.length} old chunks → ${chunks.length} new chunks), re-embedding all`);
1810
+ logger.info(`Content changed for ${url} (${existingHashesSorted.length} old chunks → ${chunks.length} new chunks), replacing all`);
1787
1811
  if (dbConnection.type === 'sqlite') {
1788
1812
  database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
1789
1813
  }
@@ -1796,34 +1820,22 @@ class Doc2Vec {
1796
1820
  else {
1797
1821
  this.counters.items_new++;
1798
1822
  }
1799
- // 5. Embed and insert all new chunks
1800
- let embeddedCount = 0;
1801
- const chunkProgress = logger.progress(`Embedding chunks for ${url}`, chunks.length);
1802
1823
  for (let i = 0; i < chunks.length; i++) {
1803
1824
  const chunk = chunks[i];
1804
1825
  const chunkHash = newHashes[i];
1805
1826
  const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
1806
- const embeddings = await this.createEmbeddings([chunk.content]);
1807
- if (embeddings.length > 0) {
1808
- const embedding = embeddings[0];
1809
- if (dbConnection.type === 'sqlite') {
1810
- database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedding, logger, chunkHash);
1811
- chunkProgress.update(1, `Stored chunk ${chunkId} in SQLite`);
1812
- }
1813
- else if (dbConnection.type === 'qdrant') {
1814
- await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedding, chunkHash);
1815
- chunkProgress.update(1, `Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
1816
- }
1817
- embeddedCount++;
1827
+ if (dbConnection.type === 'sqlite') {
1828
+ database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedded[i], logger, chunkHash);
1829
+ logger.debug(`Stored chunk ${chunkId} in SQLite`);
1818
1830
  }
1819
- else {
1820
- logger.error(`Embedding failed for chunk: ${chunkId}`);
1821
- chunkProgress.update(1, `Failed to embed chunk ${chunkId}`);
1831
+ else if (dbConnection.type === 'qdrant') {
1832
+ await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedded[i], chunkHash);
1833
+ logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
1822
1834
  }
1823
1835
  }
1824
1836
  chunkProgress.complete();
1825
- this.counters.chunks_added += embeddedCount;
1826
- return embeddedCount;
1837
+ this.counters.chunks_added += chunks.length;
1838
+ return chunks.length;
1827
1839
  }
1828
1840
  async createEmbeddings(texts) {
1829
1841
  const logger = this.logger.child('embeddings');
@@ -1852,6 +1864,12 @@ class Doc2Vec {
1852
1864
  }
1853
1865
  catch (error) {
1854
1866
  logger.error('Failed to create embeddings:', error);
1867
+ // An auth rejection is permanent: every later request in this run
1868
+ // fails the same way, so abort the source instead of returning []
1869
+ // and letting each chunk degrade into a logged-and-skipped failure.
1870
+ if (error?.status === 401 || error?.status === 403) {
1871
+ throw new types_1.FatalEmbeddingError(`embedding request rejected with HTTP ${error.status}: ${error?.message ?? error}`);
1872
+ }
1855
1873
  return [];
1856
1874
  }
1857
1875
  }
package/dist/types.js CHANGED
@@ -1,6 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FatalEmbeddingError = void 0;
3
4
  exports.newSourceRunCounters = newSourceRunCounters;
5
+ // A permanent embedding failure (e.g. an invalid API key): every subsequent
6
+ // request in the run would fail identically, so callers must abort the source
7
+ // instead of degrading each chunk into a logged-and-skipped failure.
8
+ class FatalEmbeddingError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'FatalEmbeddingError';
12
+ Object.setPrototypeOf(this, FatalEmbeddingError.prototype);
13
+ }
14
+ }
15
+ exports.FatalEmbeddingError = FatalEmbeddingError;
4
16
  function newSourceRunCounters(itemsKind) {
5
17
  return {
6
18
  items_kind: itemsKind,
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.14.2",
3
+ "version": "2.15.0",
4
4
  "type": "commonjs",
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
5
8
  "description": "",
6
9
  "main": "dist/doc2vec.js",
7
10
  "private": false,
@@ -33,23 +36,23 @@
33
36
  "@aws-sdk/client-s3": "^3.1004.0",
34
37
  "@chonkiejs/core": "^0.0.7",
35
38
  "@mozilla/readability": "^0.4.4",
36
- "@qdrant/js-client-rest": "^1.13.0",
37
- "@qdrant/qdrant-js": "^1.13.0",
38
- "axios": "^1.6.2",
39
- "better-sqlite3": "^11.9.1",
39
+ "@qdrant/js-client-rest": "^1.19.0",
40
+ "@qdrant/qdrant-js": "^1.19.0",
41
+ "axios": "^1.19.0",
42
+ "better-sqlite3": "^13.0.3",
40
43
  "chalk": "^5.4.1",
41
44
  "cheerio": "^1.0.0-rc.12",
42
45
  "commander": "^15.0.0",
43
46
  "croner": "^10.0.1",
44
47
  "dotenv": "^16.3.1",
45
48
  "express": "^5.2.1",
46
- "js-yaml": "^4.1.0",
49
+ "js-yaml": "^4.3.1",
47
50
  "jsdom": "^26.0.0",
48
51
  "mammoth": "^1.11.0",
49
52
  "openai": "^4.20.1",
50
53
  "pdfjs-dist": "^5.3.31",
51
54
  "pg": "^8.18.0",
52
- "puppeteer": "^24.1.1",
55
+ "puppeteer": "^25.8.0",
53
56
  "sanitize-html": "^2.11.0",
54
57
  "sqlite-vec": "0.1.7-alpha.2",
55
58
  "tree-sitter-wasms": "^0.1.0",