doc2vec 2.0.0 → 2.2.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.
- package/README.md +64 -5
- package/dist/code-chunker.js +179 -0
- package/dist/content-processor.js +337 -12
- package/dist/database.js +197 -7
- package/dist/doc2vec.js +472 -2
- package/dist/vitest.config.js +16 -0
- package/package.json +10 -2
|
@@ -47,6 +47,7 @@ const turndown_1 = __importDefault(require("turndown"));
|
|
|
47
47
|
const fs = __importStar(require("fs"));
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
49
|
const utils_1 = require("./utils");
|
|
50
|
+
const code_chunker_1 = require("./code-chunker");
|
|
50
51
|
class ContentProcessor {
|
|
51
52
|
constructor(logger) {
|
|
52
53
|
this.logger = logger;
|
|
@@ -54,6 +55,9 @@ class ContentProcessor {
|
|
|
54
55
|
codeBlockStyle: 'fenced',
|
|
55
56
|
headingStyle: 'atx'
|
|
56
57
|
});
|
|
58
|
+
this.tokenChunkerCache = new Map();
|
|
59
|
+
this.codeChunkerCache = new Map();
|
|
60
|
+
this.tokenizerCache = null;
|
|
57
61
|
this.setupTurndownRules();
|
|
58
62
|
}
|
|
59
63
|
setupTurndownRules() {
|
|
@@ -200,15 +204,38 @@ class ContentProcessor {
|
|
|
200
204
|
async crawlWebsite(baseUrl, sourceConfig, processPageContent, parentLogger, visitedUrls) {
|
|
201
205
|
const logger = parentLogger.child('crawler');
|
|
202
206
|
const queue = [baseUrl];
|
|
207
|
+
const brokenLinks = [];
|
|
208
|
+
const brokenLinkKeys = new Set();
|
|
209
|
+
const referrers = new Map();
|
|
210
|
+
const addReferrer = (targetUrl, sourceUrl) => {
|
|
211
|
+
const existing = referrers.get(targetUrl);
|
|
212
|
+
if (existing) {
|
|
213
|
+
existing.add(sourceUrl);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
referrers.set(targetUrl, new Set([sourceUrl]));
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const addBrokenLink = (sourceUrl, targetUrl) => {
|
|
220
|
+
const key = `${sourceUrl} -> ${targetUrl}`;
|
|
221
|
+
if (brokenLinkKeys.has(key))
|
|
222
|
+
return;
|
|
223
|
+
brokenLinkKeys.add(key);
|
|
224
|
+
brokenLinks.push({ source: sourceUrl, target: targetUrl });
|
|
225
|
+
};
|
|
226
|
+
addReferrer(baseUrl, baseUrl);
|
|
203
227
|
// Process sitemap if provided
|
|
204
228
|
if (sourceConfig.sitemap_url) {
|
|
205
229
|
logger.section('SITEMAP PROCESSING');
|
|
206
230
|
const sitemapUrls = await this.parseSitemap(sourceConfig.sitemap_url, logger);
|
|
207
231
|
// Add sitemap URLs to the queue if they're within the website scope
|
|
208
232
|
for (const url of sitemapUrls) {
|
|
209
|
-
if (url.startsWith(sourceConfig.url)
|
|
210
|
-
|
|
211
|
-
queue.
|
|
233
|
+
if (url.startsWith(sourceConfig.url)) {
|
|
234
|
+
addReferrer(url, sourceConfig.sitemap_url);
|
|
235
|
+
if (!queue.includes(url)) {
|
|
236
|
+
logger.debug(`Adding URL from sitemap to queue: ${url}`);
|
|
237
|
+
queue.push(url);
|
|
238
|
+
}
|
|
212
239
|
}
|
|
213
240
|
}
|
|
214
241
|
logger.info(`Added ${queue.length - 1} URLs from sitemap to the crawl queue`);
|
|
@@ -235,7 +262,14 @@ class ContentProcessor {
|
|
|
235
262
|
}
|
|
236
263
|
try {
|
|
237
264
|
logger.info(`Crawling: ${url}`);
|
|
238
|
-
const
|
|
265
|
+
const sources = referrers.get(url) ?? new Set([baseUrl]);
|
|
266
|
+
const content = await this.processPage(url, sourceConfig, (reportedUrl, status) => {
|
|
267
|
+
if (status === 404) {
|
|
268
|
+
for (const source of sources) {
|
|
269
|
+
addBrokenLink(source, reportedUrl);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
239
273
|
if (content !== null) {
|
|
240
274
|
await processPageContent(url, content);
|
|
241
275
|
if (utils_1.Utils.isPdfUrl(url)) {
|
|
@@ -252,17 +286,21 @@ class ContentProcessor {
|
|
|
252
286
|
if (!utils_1.Utils.isPdfUrl(url)) {
|
|
253
287
|
const response = await axios_1.default.get(url);
|
|
254
288
|
const $ = (0, cheerio_1.load)(response.data);
|
|
289
|
+
const pageUrlForLinks = response?.request?.res?.responseUrl || url;
|
|
255
290
|
logger.debug(`Finding links on page ${url}`);
|
|
256
291
|
let newLinksFound = 0;
|
|
257
292
|
$('a[href]').each((_, element) => {
|
|
258
293
|
const href = $(element).attr('href');
|
|
259
294
|
if (!href || href.startsWith('#') || href.startsWith('mailto:'))
|
|
260
295
|
return;
|
|
261
|
-
const fullUrl = utils_1.Utils.buildUrl(href,
|
|
262
|
-
if (fullUrl.startsWith(sourceConfig.url)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
296
|
+
const fullUrl = utils_1.Utils.buildUrl(href, pageUrlForLinks);
|
|
297
|
+
if (fullUrl.startsWith(sourceConfig.url)) {
|
|
298
|
+
addReferrer(fullUrl, pageUrlForLinks);
|
|
299
|
+
if (!visitedUrls.has(utils_1.Utils.normalizeUrl(fullUrl))) {
|
|
300
|
+
if (!queue.includes(fullUrl)) {
|
|
301
|
+
queue.push(fullUrl);
|
|
302
|
+
newLinksFound++;
|
|
303
|
+
}
|
|
266
304
|
}
|
|
267
305
|
}
|
|
268
306
|
});
|
|
@@ -272,6 +310,13 @@ class ContentProcessor {
|
|
|
272
310
|
catch (error) {
|
|
273
311
|
logger.error(`Failed during processing or link discovery for ${url}:`, error);
|
|
274
312
|
errorCount++;
|
|
313
|
+
const status = this.getHttpStatus(error);
|
|
314
|
+
if (status === 404) {
|
|
315
|
+
const sources = referrers.get(url) ?? new Set([baseUrl]);
|
|
316
|
+
for (const source of sources) {
|
|
317
|
+
addBrokenLink(source, url);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
275
320
|
// Check if this is a network error (DNS resolution, connection issues, etc.)
|
|
276
321
|
if (this.isNetworkError(error)) {
|
|
277
322
|
hasNetworkErrors = true;
|
|
@@ -283,7 +328,7 @@ class ContentProcessor {
|
|
|
283
328
|
if (hasNetworkErrors) {
|
|
284
329
|
logger.warn('Network errors were encountered during crawling. Cleanup may be skipped to avoid removing valid chunks.');
|
|
285
330
|
}
|
|
286
|
-
return { hasNetworkErrors };
|
|
331
|
+
return { hasNetworkErrors, brokenLinks };
|
|
287
332
|
}
|
|
288
333
|
isNetworkError(error) {
|
|
289
334
|
// Check for common network error patterns
|
|
@@ -324,7 +369,7 @@ class ContentProcessor {
|
|
|
324
369
|
}
|
|
325
370
|
return false;
|
|
326
371
|
}
|
|
327
|
-
async processPage(url, sourceConfig) {
|
|
372
|
+
async processPage(url, sourceConfig, onHttpStatus) {
|
|
328
373
|
const logger = this.logger.child('page-processor');
|
|
329
374
|
logger.debug(`Processing content from ${url}`);
|
|
330
375
|
// Check if this is a PDF URL
|
|
@@ -340,6 +385,13 @@ class ContentProcessor {
|
|
|
340
385
|
return markdown;
|
|
341
386
|
}
|
|
342
387
|
catch (error) {
|
|
388
|
+
const status = this.getHttpStatus(error);
|
|
389
|
+
if (status !== undefined && status >= 400) {
|
|
390
|
+
if (onHttpStatus) {
|
|
391
|
+
onHttpStatus(url, status);
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
343
395
|
logger.error(`Failed to process PDF ${url}:`, error);
|
|
344
396
|
return null;
|
|
345
397
|
}
|
|
@@ -363,7 +415,13 @@ class ContentProcessor {
|
|
|
363
415
|
});
|
|
364
416
|
const page = await browser.newPage();
|
|
365
417
|
logger.debug(`Navigating to ${url}`);
|
|
366
|
-
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
|
418
|
+
const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
|
419
|
+
const status = response?.status();
|
|
420
|
+
if (status !== undefined && status >= 400) {
|
|
421
|
+
const error = new Error(`Failed to load page: HTTP ${status}`);
|
|
422
|
+
error.status = status;
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
367
425
|
const htmlContent = await page.evaluate(() => {
|
|
368
426
|
// 💡 Try specific content selectors first, then fall back to broader ones
|
|
369
427
|
const mainContentElement = document.querySelector('.docs-content') || // Common docs pattern
|
|
@@ -480,6 +538,13 @@ class ContentProcessor {
|
|
|
480
538
|
return markdown;
|
|
481
539
|
}
|
|
482
540
|
catch (error) {
|
|
541
|
+
const status = this.getHttpStatus(error);
|
|
542
|
+
if (status !== undefined && status >= 400) {
|
|
543
|
+
if (onHttpStatus) {
|
|
544
|
+
onHttpStatus(url, status);
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
483
548
|
logger.error(`Error processing page ${url}:`, error);
|
|
484
549
|
return null;
|
|
485
550
|
}
|
|
@@ -490,6 +555,15 @@ class ContentProcessor {
|
|
|
490
555
|
}
|
|
491
556
|
}
|
|
492
557
|
}
|
|
558
|
+
getHttpStatus(error) {
|
|
559
|
+
if (typeof error?.status === 'number') {
|
|
560
|
+
return error.status;
|
|
561
|
+
}
|
|
562
|
+
if (typeof error?.response?.status === 'number') {
|
|
563
|
+
return error.response.status;
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
493
567
|
markCodeParents(node) {
|
|
494
568
|
if (!node)
|
|
495
569
|
return;
|
|
@@ -812,6 +886,257 @@ class ContentProcessor {
|
|
|
812
886
|
logger.error(`Error reading directory ${dirPath}:`, error);
|
|
813
887
|
}
|
|
814
888
|
}
|
|
889
|
+
async processCodeDirectory(dirPath, config, processFileContent, parentLogger, visitedPaths = new Set(), options) {
|
|
890
|
+
const logger = parentLogger.child('code-directory-processor');
|
|
891
|
+
logger.info(`Processing code directory: ${dirPath}`);
|
|
892
|
+
const recursive = config.recursive !== undefined ? config.recursive : true;
|
|
893
|
+
const includeExtensions = config.include_extensions || [
|
|
894
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
895
|
+
'.py', '.go', '.rs', '.java', '.kt', '.kts', '.swift',
|
|
896
|
+
'.c', '.cc', '.cpp', '.h', '.hpp', '.cs',
|
|
897
|
+
'.rb', '.php', '.scala', '.sql', '.sh', '.bash', '.zsh',
|
|
898
|
+
'.html', '.css', '.scss', '.sass', '.less',
|
|
899
|
+
'.json', '.yaml', '.yml', '.md'
|
|
900
|
+
];
|
|
901
|
+
const excludeExtensions = config.exclude_extensions || [];
|
|
902
|
+
const encoding = config.encoding || 'utf8';
|
|
903
|
+
let maxMtime = 0;
|
|
904
|
+
try {
|
|
905
|
+
const files = fs.readdirSync(dirPath);
|
|
906
|
+
let processedFiles = 0;
|
|
907
|
+
let skippedFiles = 0;
|
|
908
|
+
const allowedFiles = options?.allowedFiles;
|
|
909
|
+
const mtimeCutoff = options?.mtimeCutoff;
|
|
910
|
+
const trackFiles = options?.trackFiles;
|
|
911
|
+
for (const file of files) {
|
|
912
|
+
const filePath = path.join(dirPath, file);
|
|
913
|
+
const stat = fs.statSync(filePath);
|
|
914
|
+
if (visitedPaths.has(filePath)) {
|
|
915
|
+
logger.debug(`Skipping already visited path: ${filePath}`);
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
visitedPaths.add(filePath);
|
|
919
|
+
if (stat.isDirectory()) {
|
|
920
|
+
if (recursive) {
|
|
921
|
+
const childResult = await this.processCodeDirectory(filePath, config, processFileContent, logger, visitedPaths, options);
|
|
922
|
+
processedFiles += childResult.processedFiles;
|
|
923
|
+
skippedFiles += childResult.skippedFiles;
|
|
924
|
+
maxMtime = Math.max(maxMtime, childResult.maxMtime);
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
logger.debug(`Skipping directory ${filePath} (recursive=false)`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
else if (stat.isFile()) {
|
|
931
|
+
const extension = path.extname(file).toLowerCase();
|
|
932
|
+
if (excludeExtensions.includes(extension)) {
|
|
933
|
+
logger.debug(`Skipping file with excluded extension: ${filePath}`);
|
|
934
|
+
skippedFiles++;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (includeExtensions.length > 0 && !includeExtensions.includes(extension)) {
|
|
938
|
+
logger.debug(`Skipping file with non-included extension: ${filePath}`);
|
|
939
|
+
skippedFiles++;
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
trackFiles?.add(filePath);
|
|
943
|
+
maxMtime = Math.max(maxMtime, stat.mtimeMs);
|
|
944
|
+
if (allowedFiles && !allowedFiles.has(filePath)) {
|
|
945
|
+
skippedFiles++;
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (mtimeCutoff !== undefined && stat.mtimeMs <= mtimeCutoff) {
|
|
949
|
+
skippedFiles++;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
logger.info(`Reading file: ${filePath}`);
|
|
954
|
+
const content = fs.readFileSync(filePath, { encoding: encoding });
|
|
955
|
+
if (content.length > config.max_size) {
|
|
956
|
+
logger.warn(`File content (${content.length} chars) exceeds max size (${config.max_size}). Skipping ${filePath}.`);
|
|
957
|
+
skippedFiles++;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
await processFileContent(filePath, content);
|
|
961
|
+
processedFiles++;
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
logger.error(`Error processing file ${filePath}:`, error);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
logger.info(`Code directory processed. Processed: ${processedFiles}, Skipped: ${skippedFiles}`);
|
|
969
|
+
return { processedFiles, skippedFiles, maxMtime };
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
logger.error(`Error reading code directory ${dirPath}:`, error);
|
|
973
|
+
return { processedFiles: 0, skippedFiles: 0, maxMtime };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async getTokenChunker(chunkSize) {
|
|
977
|
+
const cacheKey = `${chunkSize || 'default'}`;
|
|
978
|
+
const cached = this.tokenChunkerCache.get(cacheKey);
|
|
979
|
+
if (cached) {
|
|
980
|
+
return cached;
|
|
981
|
+
}
|
|
982
|
+
const chunkerPromise = (async () => {
|
|
983
|
+
const { TokenChunker } = await this.importChonkieModule('@chonkiejs/core');
|
|
984
|
+
return await TokenChunker.create({ chunkSize, tokenizer: 'character' });
|
|
985
|
+
})();
|
|
986
|
+
this.tokenChunkerCache.set(cacheKey, chunkerPromise);
|
|
987
|
+
return chunkerPromise;
|
|
988
|
+
}
|
|
989
|
+
async getCodeChunker(lang, chunkSize) {
|
|
990
|
+
const cacheKey = `${lang}:${chunkSize || 'default'}`;
|
|
991
|
+
const cached = this.codeChunkerCache.get(cacheKey);
|
|
992
|
+
if (cached) {
|
|
993
|
+
return cached;
|
|
994
|
+
}
|
|
995
|
+
const chunkerPromise = (async () => {
|
|
996
|
+
const tokenizer = await this.getTokenizer();
|
|
997
|
+
return await code_chunker_1.CodeChunker.create({
|
|
998
|
+
lang,
|
|
999
|
+
chunkSize,
|
|
1000
|
+
tokenCounter: async (text) => tokenizer.countTokens(text)
|
|
1001
|
+
});
|
|
1002
|
+
})();
|
|
1003
|
+
this.codeChunkerCache.set(cacheKey, chunkerPromise);
|
|
1004
|
+
return chunkerPromise;
|
|
1005
|
+
}
|
|
1006
|
+
async getTokenizer() {
|
|
1007
|
+
if (!this.tokenizerCache) {
|
|
1008
|
+
this.tokenizerCache = (async () => {
|
|
1009
|
+
const { Tokenizer } = await this.importChonkieModule('@chonkiejs/core');
|
|
1010
|
+
return await Tokenizer.create('character');
|
|
1011
|
+
})();
|
|
1012
|
+
}
|
|
1013
|
+
return this.tokenizerCache;
|
|
1014
|
+
}
|
|
1015
|
+
detectCodeLanguage(filePath) {
|
|
1016
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
1017
|
+
const languageMap = {
|
|
1018
|
+
'.ts': 'typescript',
|
|
1019
|
+
'.tsx': 'typescript',
|
|
1020
|
+
'.js': 'javascript',
|
|
1021
|
+
'.jsx': 'javascript',
|
|
1022
|
+
'.mjs': 'javascript',
|
|
1023
|
+
'.cjs': 'javascript',
|
|
1024
|
+
'.py': 'python',
|
|
1025
|
+
'.go': 'go',
|
|
1026
|
+
'.rs': 'rust',
|
|
1027
|
+
'.java': 'java',
|
|
1028
|
+
'.kt': 'kotlin',
|
|
1029
|
+
'.kts': 'kotlin',
|
|
1030
|
+
'.swift': 'swift',
|
|
1031
|
+
'.c': 'c',
|
|
1032
|
+
'.cc': 'cpp',
|
|
1033
|
+
'.cpp': 'cpp',
|
|
1034
|
+
'.h': 'cpp',
|
|
1035
|
+
'.hpp': 'cpp',
|
|
1036
|
+
'.cs': 'csharp',
|
|
1037
|
+
'.rb': 'ruby',
|
|
1038
|
+
'.php': 'php',
|
|
1039
|
+
'.scala': 'scala',
|
|
1040
|
+
'.sql': 'sql',
|
|
1041
|
+
'.sh': 'bash',
|
|
1042
|
+
'.bash': 'bash',
|
|
1043
|
+
'.zsh': 'bash',
|
|
1044
|
+
'.html': 'html',
|
|
1045
|
+
'.css': 'css',
|
|
1046
|
+
'.scss': 'scss',
|
|
1047
|
+
'.sass': 'scss',
|
|
1048
|
+
'.less': 'css',
|
|
1049
|
+
'.json': 'json',
|
|
1050
|
+
'.yaml': 'yaml',
|
|
1051
|
+
'.yml': 'yaml',
|
|
1052
|
+
'.md': 'markdown'
|
|
1053
|
+
};
|
|
1054
|
+
return languageMap[extension];
|
|
1055
|
+
}
|
|
1056
|
+
async importChonkieModule(specifier) {
|
|
1057
|
+
// Use dynamic import() directly - preserved by TypeScript with target ES2020+
|
|
1058
|
+
// even in CommonJS mode, as Node.js supports import() in CJS contexts
|
|
1059
|
+
return Promise.resolve(`${specifier}`).then(s => __importStar(require(s)));
|
|
1060
|
+
}
|
|
1061
|
+
async chunkCode(code, sourceConfig, url, filePath, branch, repo) {
|
|
1062
|
+
const logger = this.logger.child('code-chunker');
|
|
1063
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
1064
|
+
const lang = this.detectCodeLanguage(filePath);
|
|
1065
|
+
let chunks;
|
|
1066
|
+
if (lang === 'markdown') {
|
|
1067
|
+
const markdownChunks = await this.chunkMarkdown(code, sourceConfig, url);
|
|
1068
|
+
for (const chunk of markdownChunks) {
|
|
1069
|
+
if (normalizedPath) {
|
|
1070
|
+
const filePrefix = `[File: ${normalizedPath}]\n`;
|
|
1071
|
+
const searchableText = filePrefix + chunk.content;
|
|
1072
|
+
const chunkId = utils_1.Utils.generateHash(`${url}::${searchableText}`);
|
|
1073
|
+
chunk.content = searchableText;
|
|
1074
|
+
chunk.metadata.heading_hierarchy = [normalizedPath, ...chunk.metadata.heading_hierarchy.filter(Boolean)];
|
|
1075
|
+
chunk.metadata.section = normalizedPath;
|
|
1076
|
+
chunk.metadata.chunk_id = chunkId;
|
|
1077
|
+
chunk.metadata.hash = chunkId;
|
|
1078
|
+
}
|
|
1079
|
+
if (branch) {
|
|
1080
|
+
chunk.metadata.branch = branch;
|
|
1081
|
+
}
|
|
1082
|
+
if (repo) {
|
|
1083
|
+
chunk.metadata.repo = repo;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
logger.debug(`Chunked ${normalizedPath || url}: ${markdownChunks.length} chunks created.`);
|
|
1087
|
+
return markdownChunks;
|
|
1088
|
+
}
|
|
1089
|
+
if (lang) {
|
|
1090
|
+
try {
|
|
1091
|
+
const codeChunker = await this.getCodeChunker(lang, sourceConfig.chunk_size);
|
|
1092
|
+
chunks = await codeChunker.chunk(code);
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
logger.warn(`CodeChunker failed for ${normalizedPath || url}, falling back to token chunking:`, error);
|
|
1096
|
+
const chunker = await this.getTokenChunker(sourceConfig.chunk_size);
|
|
1097
|
+
chunks = await chunker.chunk(code);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
const chunker = await this.getTokenChunker(sourceConfig.chunk_size);
|
|
1102
|
+
chunks = await chunker.chunk(code);
|
|
1103
|
+
}
|
|
1104
|
+
const documentChunks = [];
|
|
1105
|
+
let chunkCounter = 0;
|
|
1106
|
+
const headingHierarchy = normalizedPath ? [normalizedPath] : [];
|
|
1107
|
+
const contextPrefix = normalizedPath ? `[File: ${normalizedPath}]\n` : '';
|
|
1108
|
+
for (const chunk of chunks) {
|
|
1109
|
+
const content = chunk.text?.trim();
|
|
1110
|
+
if (!content) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const searchableText = contextPrefix + content;
|
|
1114
|
+
const chunkId = utils_1.Utils.generateHash(`${url}::${searchableText}`);
|
|
1115
|
+
documentChunks.push({
|
|
1116
|
+
content: searchableText,
|
|
1117
|
+
metadata: {
|
|
1118
|
+
product_name: sourceConfig.product_name,
|
|
1119
|
+
version: sourceConfig.version,
|
|
1120
|
+
...(branch ? { branch } : {}),
|
|
1121
|
+
...(repo ? { repo } : {}),
|
|
1122
|
+
heading_hierarchy: headingHierarchy,
|
|
1123
|
+
section: normalizedPath || 'Code',
|
|
1124
|
+
chunk_id: chunkId,
|
|
1125
|
+
url: url,
|
|
1126
|
+
hash: chunkId,
|
|
1127
|
+
chunk_index: chunkCounter,
|
|
1128
|
+
total_chunks: 0
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
chunkCounter++;
|
|
1132
|
+
}
|
|
1133
|
+
const totalChunks = documentChunks.length;
|
|
1134
|
+
for (const chunk of documentChunks) {
|
|
1135
|
+
chunk.metadata.total_chunks = totalChunks;
|
|
1136
|
+
}
|
|
1137
|
+
logger.debug(`Chunked ${normalizedPath || url}: ${documentChunks.length} chunks created.`);
|
|
1138
|
+
return documentChunks;
|
|
1139
|
+
}
|
|
815
1140
|
async chunkMarkdown(markdown, sourceConfig, url) {
|
|
816
1141
|
const logger = this.logger.child('chunker');
|
|
817
1142
|
// --- Configuration ---
|