doc2vec 2.11.0 → 2.12.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.
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.lookupChunks = lookupChunks;
40
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
41
+ const path = __importStar(require("path"));
42
+ const sqliteVec = __importStar(require("sqlite-vec"));
43
+ const yaml = __importStar(require("js-yaml"));
44
+ const js_client_rest_1 = require("@qdrant/js-client-rest");
45
+ const types_1 = require("./types");
46
+ const MAX_CHUNKS = 500;
47
+ /**
48
+ * Read-only lookup of the chunks stored for a URL in a source's vector store.
49
+ * Resolves the store the same way a sync job would (same defaults as
50
+ * DatabaseManager.initDatabase, same ${ENV_VAR} substitution as Doc2Vec's
51
+ * config loader), but never creates tables or collections.
52
+ */
53
+ async function lookupChunks(configContent, selector, url) {
54
+ const source = findSource(configContent, selector);
55
+ const dbConfig = source.database_config ?? {};
56
+ const params = dbConfig.params ?? {};
57
+ if (dbConfig.type === 'sqlite') {
58
+ const dbPath = params.db_path
59
+ || path.join(process.cwd(), `${String(source.product_name).replace(/\s+/g, '_')}-${source.version}.db`);
60
+ return {
61
+ source: { product_name: source.product_name, type: source.type, version: source.version },
62
+ database: { type: 'sqlite', path: dbPath },
63
+ chunks: lookupSqlite(dbPath, url),
64
+ };
65
+ }
66
+ if (dbConfig.type === 'qdrant') {
67
+ const qdrantUrl = params.qdrant_url || 'http://localhost:6333';
68
+ const qdrantPort = params.qdrant_port || 443;
69
+ const collectionName = params.collection_name
70
+ || `${String(source.product_name).toLowerCase().replace(/\s+/g, '_')}_${source.version}`;
71
+ const client = new js_client_rest_1.QdrantClient({ url: qdrantUrl, apiKey: process.env.QDRANT_API_KEY, port: qdrantPort });
72
+ return {
73
+ source: { product_name: source.product_name, type: source.type, version: source.version },
74
+ database: { type: 'qdrant', url: qdrantUrl, collection: collectionName },
75
+ chunks: await lookupQdrant(client, collectionName, url),
76
+ };
77
+ }
78
+ throw new types_1.ValidationError(`source has unsupported database type: ${dbConfig.type}`);
79
+ }
80
+ function findSource(configContent, selector) {
81
+ // Same ${VAR} substitution the sync job applies when it loads the config
82
+ const substituted = configContent.replace(/\$\{([^}]+)\}/g, (match, varName) => process.env[varName] !== undefined ? process.env[varName] : match);
83
+ let parsed;
84
+ try {
85
+ parsed = yaml.load(substituted);
86
+ }
87
+ catch (err) {
88
+ throw new types_1.ValidationError(`config does not parse: ${err instanceof Error ? err.message : String(err)}`);
89
+ }
90
+ const sources = Array.isArray(parsed?.sources) ? parsed.sources : [];
91
+ const source = sources.find(s => s?.product_name === selector.product_name
92
+ && (selector.type === undefined || s?.type === selector.type)
93
+ && (selector.version === undefined || String(effectiveVersion(s)) === selector.version));
94
+ if (!source) {
95
+ throw new types_1.NotFoundError(`source '${selector.product_name}' not found in config`);
96
+ }
97
+ return { ...source, version: effectiveVersion(source) };
98
+ }
99
+ // Mirrors the version defaulting in Doc2Vec.loadConfig for code sources
100
+ function effectiveVersion(source) {
101
+ if (source?.version && String(source.version).trim().length > 0)
102
+ return String(source.version);
103
+ if (source?.type === 'code') {
104
+ if (source.branch && String(source.branch).trim().length > 0)
105
+ return String(source.branch);
106
+ return 'local';
107
+ }
108
+ return String(source?.version ?? '');
109
+ }
110
+ function lookupSqlite(dbPath, url) {
111
+ let db;
112
+ try {
113
+ db = new better_sqlite3_1.default(dbPath, { readonly: true, fileMustExist: true, allowExtension: true });
114
+ }
115
+ catch {
116
+ throw new types_1.NotFoundError(`SQLite database not found at ${dbPath} (has this source ever been synced?)`);
117
+ }
118
+ try {
119
+ sqliteVec.load(db);
120
+ const hasDates = db
121
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'vec_chunk_dates'`)
122
+ .get() !== undefined;
123
+ const rows = db.prepare(`
124
+ SELECT v.chunk_id, v.url, v.product_name, v.version, v.section, v.heading_hierarchy,
125
+ v.chunk_index, v.total_chunks, v.hash, v.content
126
+ ${hasDates ? ', d.created_at AS created_at' : ', NULL AS created_at'}
127
+ FROM vec_items v
128
+ ${hasDates ? 'LEFT JOIN vec_chunk_dates d ON d.chunk_id = v.chunk_id' : ''}
129
+ WHERE v.url = ?
130
+ LIMIT ${MAX_CHUNKS}
131
+ `).all(url);
132
+ return sortChunks(rows.map(row => ({
133
+ chunk_id: String(row.chunk_id),
134
+ url: String(row.url),
135
+ product_name: row.product_name ?? null,
136
+ version: row.version ?? null,
137
+ section: row.section ?? null,
138
+ heading_hierarchy: parseHierarchy(row.heading_hierarchy),
139
+ chunk_index: row.chunk_index === null || row.chunk_index === undefined ? null : Number(row.chunk_index),
140
+ total_chunks: row.total_chunks === null || row.total_chunks === undefined ? null : Number(row.total_chunks),
141
+ hash: row.hash ?? null,
142
+ created_at: row.created_at ?? null,
143
+ content: String(row.content ?? ''),
144
+ })));
145
+ }
146
+ finally {
147
+ db.close();
148
+ }
149
+ }
150
+ async function lookupQdrant(client, collectionName, url) {
151
+ const chunks = [];
152
+ let offset = undefined;
153
+ do {
154
+ const response = await client.scroll(collectionName, {
155
+ limit: 200,
156
+ offset,
157
+ with_payload: true,
158
+ with_vector: false,
159
+ filter: {
160
+ must: [{ key: 'url', match: { value: url } }],
161
+ must_not: [{ key: 'is_metadata', match: { value: true } }],
162
+ },
163
+ });
164
+ for (const point of response.points) {
165
+ const p = point.payload ?? {};
166
+ chunks.push({
167
+ chunk_id: String(p.original_chunk_id ?? point.id),
168
+ url: String(p.url ?? url),
169
+ product_name: p.product_name ?? null,
170
+ version: p.version ?? null,
171
+ section: p.section ?? null,
172
+ heading_hierarchy: parseHierarchy(p.heading_hierarchy),
173
+ chunk_index: p.chunk_index === null || p.chunk_index === undefined ? null : Number(p.chunk_index),
174
+ total_chunks: p.total_chunks === null || p.total_chunks === undefined ? null : Number(p.total_chunks),
175
+ hash: p.hash ?? null,
176
+ created_at: p.created_at ?? null,
177
+ content: String(p.content ?? ''),
178
+ });
179
+ if (chunks.length >= MAX_CHUNKS)
180
+ return sortChunks(chunks);
181
+ }
182
+ offset = response.next_page_offset;
183
+ } while (offset !== null && offset !== undefined);
184
+ return sortChunks(chunks);
185
+ }
186
+ function parseHierarchy(value) {
187
+ if (Array.isArray(value))
188
+ return value.map(String);
189
+ if (typeof value === 'string' && value.trim().startsWith('[')) {
190
+ try {
191
+ const parsed = JSON.parse(value);
192
+ if (Array.isArray(parsed))
193
+ return parsed.map(String);
194
+ }
195
+ catch { /* fall through */ }
196
+ }
197
+ return value ? [String(value)] : [];
198
+ }
199
+ // Default order: position within the page. The UI offers per-column sorting
200
+ // (including by creation date) on top of this.
201
+ function sortChunks(chunks) {
202
+ return chunks.sort((a, b) => {
203
+ const ai = a.chunk_index ?? Number.MAX_SAFE_INTEGER;
204
+ const bi = b.chunk_index ?? Number.MAX_SAFE_INTEGER;
205
+ if (ai !== bi)
206
+ return ai - bi;
207
+ return a.chunk_id.localeCompare(b.chunk_id);
208
+ });
209
+ }
@@ -40,6 +40,7 @@ exports.createServer = createServer;
40
40
  const express_1 = __importDefault(require("express"));
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
+ const chunk_inspector_1 = require("./chunk-inspector");
43
44
  const config_registry_1 = require("./config-registry");
44
45
  const types_1 = require("./types");
45
46
  const SSE_HEARTBEAT_MS = 15000;
@@ -151,6 +152,34 @@ function createServer(deps) {
151
152
  const days = Number(req.query.days) || 30;
152
153
  res.json(await store.getConfigStats(id, days));
153
154
  });
155
+ // Inspect the chunks currently stored for a URL in one source's vector store.
156
+ // This reads the live store, so results reflect the present state, not a
157
+ // historical run.
158
+ app.get('/api/configs/:id/chunks', async (req, res) => {
159
+ const config = registry.get(parseId(String(req.params.id)));
160
+ if (!config)
161
+ throw new types_1.NotFoundError('config not found');
162
+ const url = typeof req.query.url === 'string' ? req.query.url.trim() : '';
163
+ const productName = typeof req.query.product_name === 'string' ? req.query.product_name : '';
164
+ if (!url)
165
+ throw new types_1.ValidationError('url query parameter is required');
166
+ if (!productName)
167
+ throw new types_1.ValidationError('product_name query parameter is required');
168
+ try {
169
+ res.json(await (0, chunk_inspector_1.lookupChunks)(config.content, {
170
+ product_name: productName,
171
+ type: typeof req.query.type === 'string' && req.query.type ? req.query.type : undefined,
172
+ version: typeof req.query.version === 'string' && req.query.version ? req.query.version : undefined,
173
+ }, url));
174
+ }
175
+ catch (err) {
176
+ if (err instanceof types_1.NotFoundError || err instanceof types_1.ValidationError)
177
+ throw err;
178
+ // Vector store unreachable (e.g. Qdrant down) — surface the reason
179
+ logger.error('Chunk lookup failed:', err);
180
+ res.status(502).json({ error: `chunk lookup failed: ${err instanceof Error ? err.message : String(err)}` });
181
+ }
182
+ });
154
183
  // ------------------------------------------------------------------ runs
155
184
  app.get('/api/runs', async (req, res) => {
156
185
  res.json(await store.listRuns({
package/dist/database.js CHANGED
@@ -71,6 +71,7 @@ class DatabaseManager {
71
71
  total_chunks INTEGER
72
72
  );
73
73
  `);
74
+ this.ensureChunkDatesTable(db);
74
75
  logger.info(`SQLite database initialized successfully`);
75
76
  return { db, type: 'sqlite' };
76
77
  }
@@ -307,9 +308,24 @@ class DatabaseManager {
307
308
  logger.error(`Failed to update last run date for ${repo}:`, error);
308
309
  }
309
310
  }
311
+ /**
312
+ * Chunk creation timestamps live in a companion table because vec_items is
313
+ * a vec0 virtual table that cannot be ALTERed to add a column. Rows are
314
+ * written on every chunk insert/update; stale rows for deleted chunks are
315
+ * harmless (lookups always join against vec_items).
316
+ */
317
+ static ensureChunkDatesTable(db) {
318
+ db.exec(`
319
+ CREATE TABLE IF NOT EXISTS vec_chunk_dates (
320
+ chunk_id TEXT PRIMARY KEY,
321
+ created_at TEXT NOT NULL
322
+ );
323
+ `);
324
+ }
310
325
  static prepareSQLiteStatements(db) {
311
326
  let cached = this.columnCache.get(db);
312
327
  if (!cached) {
328
+ this.ensureChunkDatesTable(db);
313
329
  cached = {
314
330
  hasBranch: this.hasColumn(db, 'branch'),
315
331
  hasRepo: this.hasColumn(db, 'repo')
@@ -360,12 +376,16 @@ class DatabaseManager {
360
376
  `),
361
377
  getAllChunkIdsStmt: db.prepare(`SELECT chunk_id FROM vec_items`),
362
378
  deleteChunkStmt: db.prepare(`DELETE FROM vec_items WHERE chunk_id = ?`),
379
+ touchDateStmt: db.prepare(`
380
+ INSERT INTO vec_chunk_dates (chunk_id, created_at) VALUES (?, ?)
381
+ ON CONFLICT(chunk_id) DO UPDATE SET created_at = excluded.created_at
382
+ `),
363
383
  hasBranchColumn,
364
384
  hasRepoColumn
365
385
  };
366
386
  }
367
387
  static insertVectorsSQLite(db, chunk, embedding, logger, chunkHash) {
368
- const { insertStmt, updateStmt, hasBranchColumn, hasRepoColumn } = this.prepareSQLiteStatements(db);
388
+ const { insertStmt, updateStmt, touchDateStmt, hasBranchColumn, hasRepoColumn } = this.prepareSQLiteStatements(db);
369
389
  const hash = chunkHash || utils_1.Utils.generateHash(chunk.content);
370
390
  const transaction = db.transaction(() => {
371
391
  // Use BigInt for true integer representation in SQLite vec0
@@ -401,6 +421,7 @@ class DatabaseManager {
401
421
  updateValues.push(JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks, chunk.metadata.chunk_id);
402
422
  updateStmt.run(...updateValues);
403
423
  }
424
+ touchDateStmt.run(chunk.metadata.chunk_id, new Date().toISOString());
404
425
  });
405
426
  transaction();
406
427
  }
@@ -434,6 +455,7 @@ class DatabaseManager {
434
455
  original_chunk_id: chunk.metadata.chunk_id,
435
456
  chunk_index: chunk.metadata.chunk_index,
436
457
  total_chunks: chunk.metadata.total_chunks,
458
+ created_at: new Date().toISOString(),
437
459
  },
438
460
  };
439
461
  await client.upsert(collectionName, {
@@ -457,17 +479,20 @@ class DatabaseManager {
457
479
  const deleteChunkStmt = db.prepare(`DELETE FROM vec_items WHERE chunk_id = ?`);
458
480
  const existingChunks = getChunksForUrlStmt.all(urlPrefix);
459
481
  let deletedCount = 0;
482
+ const deletedUrls = new Set();
460
483
  const transaction = db.transaction(() => {
461
484
  for (const { chunk_id, url } of existingChunks) {
462
485
  if (!visitedUrls.has(url)) {
463
486
  logger.debug(`Deleting obsolete chunk from SQLite: ${chunk_id.substring(0, 8)}... (URL not visited)`);
464
487
  deleteChunkStmt.run(chunk_id);
465
488
  deletedCount++;
489
+ deletedUrls.add(url);
466
490
  }
467
491
  }
468
492
  });
469
493
  transaction();
470
494
  logger.info(`Deleted ${deletedCount} obsolete chunks from SQLite for URL ${urlPrefix}`);
495
+ return { items: deletedUrls.size, chunks: deletedCount };
471
496
  }
472
497
  static async removeObsoleteChunksQdrant(db, visitedUrls, urlPrefix, logger) {
473
498
  const { client, collectionName } = db;
@@ -496,6 +521,7 @@ class DatabaseManager {
496
521
  ]
497
522
  };
498
523
  const obsoletePointIds = [];
524
+ const obsoleteUrls = new Set();
499
525
  let offset = undefined;
500
526
  do {
501
527
  const response = await client.scroll(collectionName, {
@@ -513,6 +539,7 @@ class DatabaseManager {
513
539
  }
514
540
  if (url && !visitedUrls.has(url)) {
515
541
  obsoletePointIds.push(point.id);
542
+ obsoleteUrls.add(url);
516
543
  }
517
544
  }
518
545
  offset = response.next_page_offset;
@@ -526,6 +553,7 @@ class DatabaseManager {
526
553
  else {
527
554
  logger.info(`No obsolete chunks to delete from Qdrant for URL ${urlPrefix}`);
528
555
  }
556
+ return { items: obsoleteUrls.size, chunks: obsoletePointIds.length };
529
557
  }
530
558
  catch (error) {
531
559
  // Rethrow so a failed cleanup fails the job instead of silently
@@ -548,31 +576,42 @@ class DatabaseManager {
548
576
  const deleteStmt = db.prepare(`DELETE FROM vec_items WHERE url = ?`);
549
577
  const result = deleteStmt.run(url);
550
578
  logger.info(`Deleted ${result.changes} chunks from SQLite for URL ${url}`);
579
+ return result.changes;
551
580
  }
552
581
  static async removeChunksByUrlQdrant(db, url, logger) {
553
582
  const { client, collectionName } = db;
554
583
  try {
555
- await client.delete(collectionName, {
556
- filter: {
557
- must: [
558
- {
559
- key: 'url',
560
- match: {
561
- value: url // Exact match — must match getChunkHashesByUrlQdrant's filter
562
- }
584
+ const filter = {
585
+ must: [
586
+ {
587
+ key: 'url',
588
+ match: {
589
+ value: url // Exact match — must match getChunkHashesByUrlQdrant's filter
563
590
  }
564
- ],
565
- must_not: [
566
- {
567
- key: 'is_metadata',
568
- match: {
569
- value: true
570
- }
591
+ }
592
+ ],
593
+ must_not: [
594
+ {
595
+ key: 'is_metadata',
596
+ match: {
597
+ value: true
571
598
  }
572
- ]
573
- }
574
- });
599
+ }
600
+ ]
601
+ };
602
+ // Count first so callers can report how many chunks the delete removed
603
+ // (Qdrant's delete-by-filter response doesn't include a count).
604
+ let deletedCount = 0;
605
+ try {
606
+ const countResult = await client.count(collectionName, { filter, exact: true });
607
+ deletedCount = countResult?.count ?? 0;
608
+ }
609
+ catch {
610
+ // Count is informational only — never fail the delete over it
611
+ }
612
+ await client.delete(collectionName, { filter });
575
613
  logger.info(`Deleted chunks from Qdrant for URL ${url}`);
614
+ return deletedCount;
576
615
  }
577
616
  catch (error) {
578
617
  // Rethrow so the failure propagates and the job exits non-zero.
@@ -697,6 +736,7 @@ class DatabaseManager {
697
736
  logger.debug(`Searching for chunks with URL prefix: ${urlPrefix}`);
698
737
  const existingChunks = getChunksForPathStmt.all(urlPrefix);
699
738
  let deletedCount = 0;
739
+ const deletedUrls = new Set();
700
740
  const transaction = db.transaction(() => {
701
741
  for (const { chunk_id, url } of existingChunks) {
702
742
  // Skip if it's not from our URL prefix (safety check)
@@ -720,11 +760,13 @@ class DatabaseManager {
720
760
  logger.debug(`Deleting obsolete chunk from SQLite: ${chunk_id.substring(0, 8)}... (File not processed: ${filePath})`);
721
761
  deleteChunkStmt.run(chunk_id);
722
762
  deletedCount++;
763
+ deletedUrls.add(url);
723
764
  }
724
765
  }
725
766
  });
726
767
  transaction();
727
768
  logger.info(`Deleted ${deletedCount} obsolete chunks from SQLite for URL prefix ${urlPrefix}`);
769
+ return { items: deletedUrls.size, chunks: deletedCount };
728
770
  }
729
771
  static async removeObsoleteFilesQdrant(db, processedFiles, pathConfig, logger) {
730
772
  const { client, collectionName } = db;
@@ -768,6 +810,7 @@ class DatabaseManager {
768
810
  ]
769
811
  };
770
812
  const obsoletePointIds = [];
813
+ const obsoleteUrls = new Set();
771
814
  let offset = undefined;
772
815
  do {
773
816
  const response = await client.scroll(collectionName, {
@@ -799,6 +842,7 @@ class DatabaseManager {
799
842
  }
800
843
  if (filePath && !processedFiles.has(filePath)) {
801
844
  obsoletePointIds.push(point.id);
845
+ obsoleteUrls.add(url);
802
846
  }
803
847
  }
804
848
  offset = response.next_page_offset;
@@ -812,6 +856,7 @@ class DatabaseManager {
812
856
  else {
813
857
  logger.info(`No obsolete chunks to delete from Qdrant for URL prefix ${urlPrefix}`);
814
858
  }
859
+ return { items: obsoleteUrls.size, chunks: obsoletePointIds.length };
815
860
  }
816
861
  catch (error) {
817
862
  // Rethrow so a failed cleanup fails the job instead of silently