scream-code 0.8.0 → 0.8.2

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,1674 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
+ import { dirname as __cjsShimDirname } from 'node:path';
4
+ const __filename = __cjsShimFileURLToPath(import.meta.url);
5
+ const __dirname = __cjsShimDirname(__filename);
6
+ import { mkdir, readFile, readdir } from "node:fs/promises";
7
+ import { DatabaseSync } from "node:sqlite";
8
+ //#region ../../node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
9
+ const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
10
+ function normalizeWindowsPath(input = "") {
11
+ if (!input) return input;
12
+ return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());
13
+ }
14
+ const _UNC_REGEX = /^[/\\]{2}/;
15
+ const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
16
+ const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
17
+ const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
18
+ const _EXTNAME_RE = /.(\.[^./]+|\.)$/;
19
+ const _PATH_ROOT_RE = /^[/\\]|^[a-zA-Z]:[/\\]/;
20
+ const normalize = function(path) {
21
+ if (path.length === 0) return ".";
22
+ path = normalizeWindowsPath(path);
23
+ const isUNCPath = path.match(_UNC_REGEX);
24
+ const isPathAbsolute = isAbsolute(path);
25
+ const trailingSeparator = path[path.length - 1] === "/";
26
+ path = normalizeString(path, !isPathAbsolute);
27
+ if (path.length === 0) {
28
+ if (isPathAbsolute) return "/";
29
+ return trailingSeparator ? "./" : ".";
30
+ }
31
+ if (trailingSeparator) path += "/";
32
+ if (_DRIVE_LETTER_RE.test(path)) path += "/";
33
+ if (isUNCPath) {
34
+ if (!isPathAbsolute) return `//./${path}`;
35
+ return `//${path}`;
36
+ }
37
+ return isPathAbsolute && !isAbsolute(path) ? `/${path}` : path;
38
+ };
39
+ const join = function(...segments) {
40
+ let path = "";
41
+ for (const seg of segments) {
42
+ if (!seg) continue;
43
+ if (path.length > 0) {
44
+ const pathTrailing = path[path.length - 1] === "/";
45
+ const segLeading = seg[0] === "/";
46
+ if (pathTrailing && segLeading) path += seg.slice(1);
47
+ else path += pathTrailing || segLeading ? seg : `/${seg}`;
48
+ } else path += seg;
49
+ }
50
+ return normalize(path);
51
+ };
52
+ function cwd() {
53
+ if (typeof process !== "undefined" && typeof process.cwd === "function") return process.cwd().replace(/\\/g, "/");
54
+ return "/";
55
+ }
56
+ const resolve = function(...arguments_) {
57
+ arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
58
+ let resolvedPath = "";
59
+ let resolvedAbsolute = false;
60
+ for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
61
+ const path = index >= 0 ? arguments_[index] : cwd();
62
+ if (!path || path.length === 0) continue;
63
+ resolvedPath = `${path}/${resolvedPath}`;
64
+ resolvedAbsolute = isAbsolute(path);
65
+ }
66
+ resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
67
+ if (resolvedAbsolute && !isAbsolute(resolvedPath)) return `/${resolvedPath}`;
68
+ return resolvedPath.length > 0 ? resolvedPath : ".";
69
+ };
70
+ function normalizeString(path, allowAboveRoot) {
71
+ let res = "";
72
+ let lastSegmentLength = 0;
73
+ let lastSlash = -1;
74
+ let dots = 0;
75
+ let char = null;
76
+ for (let index = 0; index <= path.length; ++index) {
77
+ if (index < path.length) char = path[index];
78
+ else if (char === "/") break;
79
+ else char = "/";
80
+ if (char === "/") {
81
+ if (lastSlash === index - 1 || dots === 1);
82
+ else if (dots === 2) {
83
+ if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
84
+ if (res.length > 2) {
85
+ const lastSlashIndex = res.lastIndexOf("/");
86
+ if (lastSlashIndex === -1) {
87
+ res = "";
88
+ lastSegmentLength = 0;
89
+ } else {
90
+ res = res.slice(0, lastSlashIndex);
91
+ lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
92
+ }
93
+ lastSlash = index;
94
+ dots = 0;
95
+ continue;
96
+ } else if (res.length > 0) {
97
+ res = "";
98
+ lastSegmentLength = 0;
99
+ lastSlash = index;
100
+ dots = 0;
101
+ continue;
102
+ }
103
+ }
104
+ if (allowAboveRoot) {
105
+ res += res.length > 0 ? "/.." : "..";
106
+ lastSegmentLength = 2;
107
+ }
108
+ } else {
109
+ if (res.length > 0) res += `/${path.slice(lastSlash + 1, index)}`;
110
+ else res = path.slice(lastSlash + 1, index);
111
+ lastSegmentLength = index - lastSlash - 1;
112
+ }
113
+ lastSlash = index;
114
+ dots = 0;
115
+ } else if (char === "." && dots !== -1) ++dots;
116
+ else dots = -1;
117
+ }
118
+ return res;
119
+ }
120
+ const isAbsolute = function(p) {
121
+ return _IS_ABSOLUTE_RE.test(p);
122
+ };
123
+ const extname = function(p) {
124
+ if (p === "..") return "";
125
+ const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));
126
+ return match && match[1] || "";
127
+ };
128
+ const relative = function(from, to) {
129
+ const _from = resolve(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
130
+ const _to = resolve(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
131
+ if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) return _to.join("/");
132
+ const _fromCopy = [..._from];
133
+ for (const segment of _fromCopy) {
134
+ if (_to[0] !== segment) break;
135
+ _from.shift();
136
+ _to.shift();
137
+ }
138
+ return [..._from.map(() => ".."), ..._to].join("/");
139
+ };
140
+ const dirname = function(p) {
141
+ const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
142
+ if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) segments[0] += "/";
143
+ return segments.join("/") || (isAbsolute(p) ? "/" : ".");
144
+ };
145
+ const basename = function(p, extension) {
146
+ const segments = normalizeWindowsPath(p).split("/");
147
+ let lastSegment = "";
148
+ for (let i = segments.length - 1; i >= 0; i--) {
149
+ const val = segments[i];
150
+ if (val) {
151
+ lastSegment = val;
152
+ break;
153
+ }
154
+ }
155
+ return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
156
+ };
157
+ const parse = function(p) {
158
+ const root = _PATH_ROOT_RE.exec(p)?.[0]?.replace(/\\/g, "/") || "";
159
+ const base = basename(p);
160
+ const extension = extname(base);
161
+ return {
162
+ root,
163
+ dir: dirname(p),
164
+ base,
165
+ ext: extension,
166
+ name: base.slice(0, base.length - extension.length)
167
+ };
168
+ };
169
+ //#endregion
170
+ //#region ../../packages/knowledge/src/store.ts
171
+ const ENTITY_TYPES$1 = new Set([
172
+ "person",
173
+ "organization",
174
+ "location",
175
+ "time",
176
+ "product",
177
+ "metric",
178
+ "action",
179
+ "work",
180
+ "group",
181
+ "subject",
182
+ "tags"
183
+ ]);
184
+ function generateId(prefix) {
185
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
186
+ }
187
+ function normalizeName(name) {
188
+ return name.trim().toLowerCase().replaceAll(/\s+/g, " ");
189
+ }
190
+ function vectorToJson(vec) {
191
+ if (vec === null) return null;
192
+ return JSON.stringify(Array.from(vec));
193
+ }
194
+ function jsonToVector(json) {
195
+ if (json === null) return null;
196
+ try {
197
+ const arr = JSON.parse(json);
198
+ return new Float32Array(arr);
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+ function isEntityType(value) {
204
+ return ENTITY_TYPES$1.has(value);
205
+ }
206
+ var KnowledgeStore = class {
207
+ dbPath;
208
+ db;
209
+ initialized = false;
210
+ initPromise = null;
211
+ embeddingEngine;
212
+ constructor(projectDir) {
213
+ this.dbPath = join(projectDir, "knowledge", "knowledge.db");
214
+ }
215
+ async init() {
216
+ if (this.initPromise) return this.initPromise;
217
+ this.initPromise = this._doInit();
218
+ return this.initPromise;
219
+ }
220
+ async _doInit() {
221
+ if (this.initialized) return;
222
+ await mkdir(dirname(this.dbPath), { recursive: true });
223
+ this.db = new DatabaseSync(this.dbPath);
224
+ this.db.exec("PRAGMA journal_mode = WAL;");
225
+ this.db.exec("PRAGMA foreign_keys = ON;");
226
+ this.db.exec("PRAGMA busy_timeout = 5000;");
227
+ this.createSchema();
228
+ this.initialized = true;
229
+ }
230
+ close() {
231
+ if (this.db !== void 0) {
232
+ this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
233
+ this.db.close();
234
+ this.db = void 0;
235
+ }
236
+ this.initialized = false;
237
+ this.initPromise = null;
238
+ }
239
+ setEmbeddingEngine(engine) {
240
+ this.embeddingEngine = engine;
241
+ }
242
+ getEmbeddingEngine() {
243
+ return this.embeddingEngine;
244
+ }
245
+ /**
246
+ * Begin a write transaction. Use commitTransaction / rollbackTransaction
247
+ * to close it. Used by ingest to keep chunks/events/entities/edges atomic —
248
+ * a mid-ingest failure leaves no partial rows.
249
+ */
250
+ beginTransaction() {
251
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
252
+ this.db.exec("BEGIN");
253
+ }
254
+ commitTransaction() {
255
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
256
+ this.db.exec("COMMIT");
257
+ }
258
+ rollbackTransaction() {
259
+ if (this.db === void 0) return;
260
+ try {
261
+ this.db.exec("ROLLBACK");
262
+ } catch {}
263
+ }
264
+ createSchema() {
265
+ if (this.db === void 0) return;
266
+ this.db.exec(`
267
+ CREATE TABLE IF NOT EXISTS knowledge_sources (
268
+ id TEXT PRIMARY KEY,
269
+ name TEXT NOT NULL,
270
+ file_path TEXT,
271
+ description TEXT,
272
+ created_at INTEGER NOT NULL
273
+ );
274
+
275
+ CREATE TABLE IF NOT EXISTS knowledge_documents (
276
+ id TEXT PRIMARY KEY,
277
+ source_id TEXT NOT NULL REFERENCES knowledge_sources(id) ON DELETE CASCADE,
278
+ title TEXT NOT NULL,
279
+ content TEXT,
280
+ status TEXT NOT NULL DEFAULT 'pending',
281
+ chunk_count INTEGER DEFAULT 0,
282
+ created_at INTEGER NOT NULL
283
+ );
284
+
285
+ CREATE INDEX IF NOT EXISTS idx_knowledge_documents_source ON knowledge_documents(source_id);
286
+
287
+ CREATE TABLE IF NOT EXISTS knowledge_chunks (
288
+ id TEXT PRIMARY KEY,
289
+ source_id TEXT NOT NULL REFERENCES knowledge_sources(id) ON DELETE CASCADE,
290
+ document_id TEXT NOT NULL REFERENCES knowledge_documents(id) ON DELETE CASCADE,
291
+ rank INTEGER NOT NULL,
292
+ heading TEXT,
293
+ content TEXT NOT NULL,
294
+ raw_content TEXT,
295
+ embedding_json TEXT,
296
+ created_at INTEGER NOT NULL
297
+ );
298
+
299
+ CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_doc ON knowledge_chunks(document_id);
300
+ CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_source ON knowledge_chunks(source_id);
301
+
302
+ CREATE TABLE IF NOT EXISTS knowledge_events (
303
+ id TEXT PRIMARY KEY,
304
+ source_id TEXT NOT NULL REFERENCES knowledge_sources(id) ON DELETE CASCADE,
305
+ document_id TEXT NOT NULL REFERENCES knowledge_documents(id) ON DELETE CASCADE,
306
+ chunk_id TEXT NOT NULL REFERENCES knowledge_chunks(id) ON DELETE CASCADE,
307
+ rank INTEGER NOT NULL,
308
+ title TEXT NOT NULL,
309
+ summary TEXT,
310
+ content TEXT NOT NULL,
311
+ category TEXT,
312
+ keywords TEXT,
313
+ title_embedding_json TEXT,
314
+ content_embedding_json TEXT,
315
+ created_at INTEGER NOT NULL
316
+ );
317
+
318
+ CREATE INDEX IF NOT EXISTS idx_knowledge_events_chunk ON knowledge_events(chunk_id);
319
+ CREATE INDEX IF NOT EXISTS idx_knowledge_events_doc ON knowledge_events(document_id);
320
+ CREATE INDEX IF NOT EXISTS idx_knowledge_events_source ON knowledge_events(source_id);
321
+
322
+ CREATE TABLE IF NOT EXISTS knowledge_entities (
323
+ id TEXT PRIMARY KEY,
324
+ source_id TEXT NOT NULL REFERENCES knowledge_sources(id) ON DELETE CASCADE,
325
+ type TEXT NOT NULL,
326
+ name TEXT NOT NULL,
327
+ normalized_name TEXT NOT NULL,
328
+ description TEXT,
329
+ embedding_json TEXT,
330
+ created_at INTEGER NOT NULL,
331
+ UNIQUE(source_id, type, normalized_name)
332
+ );
333
+
334
+ CREATE INDEX IF NOT EXISTS idx_knowledge_entities_source ON knowledge_entities(source_id);
335
+ CREATE INDEX IF NOT EXISTS idx_knowledge_entities_name ON knowledge_entities(normalized_name);
336
+
337
+ CREATE TABLE IF NOT EXISTS knowledge_event_entities (
338
+ id TEXT PRIMARY KEY,
339
+ event_id TEXT NOT NULL REFERENCES knowledge_events(id) ON DELETE CASCADE,
340
+ entity_id TEXT NOT NULL REFERENCES knowledge_entities(id) ON DELETE CASCADE,
341
+ weight REAL DEFAULT 1.0,
342
+ description TEXT,
343
+ embedding_json TEXT,
344
+ UNIQUE(event_id, entity_id)
345
+ );
346
+
347
+ CREATE INDEX IF NOT EXISTS idx_knowledge_event_entities_event ON knowledge_event_entities(event_id);
348
+ CREATE INDEX IF NOT EXISTS idx_knowledge_event_entities_entity ON knowledge_event_entities(entity_id);
349
+
350
+ CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_chunks_fts USING fts5(
351
+ heading, content, content='knowledge_chunks', content_rowid='rowid'
352
+ );
353
+ CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_events_fts USING fts5(
354
+ title, summary, content, content='knowledge_events', content_rowid='rowid'
355
+ );
356
+ CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_entities_fts USING fts5(
357
+ name, description, content='knowledge_entities', content_rowid='rowid'
358
+ );
359
+ `);
360
+ }
361
+ async createSource(params) {
362
+ await this.init();
363
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
364
+ const id = generateId("src");
365
+ const createdAt = Date.now();
366
+ this.db.prepare("INSERT INTO knowledge_sources (id, name, file_path, description, created_at) VALUES (?, ?, ?, ?, ?)").run(id, params.name, params.filePath ?? null, params.description ?? null, createdAt);
367
+ return {
368
+ id,
369
+ name: params.name,
370
+ filePath: params.filePath ?? null,
371
+ description: params.description ?? null,
372
+ createdAt
373
+ };
374
+ }
375
+ async findSourceByFilePath(filePath) {
376
+ await this.init();
377
+ if (this.db === void 0) return void 0;
378
+ const row = this.db.prepare("SELECT * FROM knowledge_sources WHERE file_path = ?").get(filePath);
379
+ return row === void 0 ? void 0 : rowToSource(row);
380
+ }
381
+ async listSources() {
382
+ await this.init();
383
+ if (this.db === void 0) return [];
384
+ return this.db.prepare("SELECT * FROM knowledge_sources ORDER BY created_at DESC").all().map(rowToSource);
385
+ }
386
+ async getSource(id) {
387
+ await this.init();
388
+ if (this.db === void 0) return void 0;
389
+ const row = this.db.prepare("SELECT * FROM knowledge_sources WHERE id = ?").get(id);
390
+ return row === void 0 ? void 0 : rowToSource(row);
391
+ }
392
+ async deleteSource(id) {
393
+ await this.init();
394
+ if (this.db === void 0) return false;
395
+ return this.db.prepare("DELETE FROM knowledge_sources WHERE id = ?").run(id).changes > 0;
396
+ }
397
+ async createDocument(params) {
398
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
399
+ const id = generateId("doc");
400
+ const createdAt = Date.now();
401
+ this.db.prepare("INSERT INTO knowledge_documents (id, source_id, title, content, status, chunk_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, params.sourceId, params.title, params.content ?? null, "pending", 0, createdAt);
402
+ return {
403
+ id,
404
+ sourceId: params.sourceId,
405
+ title: params.title,
406
+ content: params.content ?? null,
407
+ status: "pending",
408
+ chunkCount: 0,
409
+ createdAt
410
+ };
411
+ }
412
+ async updateDocumentStatus(documentId, status, chunkCount) {
413
+ if (this.db === void 0) return;
414
+ if (chunkCount !== void 0) this.db.prepare("UPDATE knowledge_documents SET status = ?, chunk_count = ? WHERE id = ?").run(status, chunkCount, documentId);
415
+ else this.db.prepare("UPDATE knowledge_documents SET status = ? WHERE id = ?").run(status, documentId);
416
+ }
417
+ async listDocuments() {
418
+ await this.init();
419
+ if (this.db === void 0) return [];
420
+ return this.db.prepare(`SELECT d.*, s.name AS source_name
421
+ FROM knowledge_documents d
422
+ JOIN knowledge_sources s ON s.id = d.source_id
423
+ ORDER BY d.created_at DESC`).all().map((row) => ({
424
+ ...rowToDocument(row),
425
+ sourceName: String(row["source_name"])
426
+ }));
427
+ }
428
+ async getDocument(id) {
429
+ await this.init();
430
+ if (this.db === void 0) return void 0;
431
+ const row = this.db.prepare("SELECT * FROM knowledge_documents WHERE id = ?").get(id);
432
+ return row === void 0 ? void 0 : rowToDocument(row);
433
+ }
434
+ async insertChunk(params) {
435
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
436
+ const id = generateId("chk");
437
+ const createdAt = Date.now();
438
+ const embeddingJson = vectorToJson(params.embedding);
439
+ this.db.prepare(`INSERT INTO knowledge_chunks
440
+ (id, source_id, document_id, rank, heading, content, raw_content, embedding_json, created_at)
441
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.sourceId, params.documentId, params.rank, params.heading, params.content, params.rawContent, embeddingJson, createdAt);
442
+ this.db.prepare(`INSERT INTO knowledge_chunks_fts (rowid, heading, content)
443
+ VALUES ((SELECT rowid FROM knowledge_chunks WHERE id = ?), ?, ?)`).run(id, params.heading ?? "", params.content);
444
+ return {
445
+ id,
446
+ sourceId: params.sourceId,
447
+ documentId: params.documentId,
448
+ rank: params.rank,
449
+ heading: params.heading,
450
+ content: params.content,
451
+ rawContent: params.rawContent,
452
+ embedding: params.embedding,
453
+ createdAt
454
+ };
455
+ }
456
+ async getChunk(id) {
457
+ await this.init();
458
+ if (this.db === void 0) return void 0;
459
+ const row = this.db.prepare("SELECT * FROM knowledge_chunks WHERE id = ?").get(id);
460
+ return row === void 0 ? void 0 : rowToChunk(row);
461
+ }
462
+ async listChunksByDocument(documentId) {
463
+ await this.init();
464
+ if (this.db === void 0) return [];
465
+ return this.db.prepare("SELECT * FROM knowledge_chunks WHERE document_id = ? ORDER BY rank ASC").all(documentId).map(rowToChunk);
466
+ }
467
+ /** Load all chunk embeddings + ids for vector search. */
468
+ async loadAllChunkEmbeddings() {
469
+ await this.init();
470
+ if (this.db === void 0) return [];
471
+ const rows = this.db.prepare("SELECT id, embedding_json FROM knowledge_chunks WHERE embedding_json IS NOT NULL").all();
472
+ const out = [];
473
+ for (const row of rows) {
474
+ const vec = jsonToVector(row.embedding_json);
475
+ if (vec !== null) out.push({
476
+ id: row.id,
477
+ embedding: vec
478
+ });
479
+ }
480
+ return out;
481
+ }
482
+ async insertEvent(params) {
483
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
484
+ const id = generateId("evt");
485
+ const createdAt = Date.now();
486
+ this.db.prepare(`INSERT INTO knowledge_events
487
+ (id, source_id, document_id, chunk_id, rank, title, summary, content, category, keywords,
488
+ title_embedding_json, content_embedding_json, created_at)
489
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.sourceId, params.documentId, params.chunkId, params.rank, params.title, params.summary, params.content, params.category, JSON.stringify(params.keywords), vectorToJson(params.titleEmbedding), vectorToJson(params.contentEmbedding), createdAt);
490
+ this.db.prepare(`INSERT INTO knowledge_events_fts (rowid, title, summary, content)
491
+ VALUES ((SELECT rowid FROM knowledge_events WHERE id = ?), ?, ?, ?)`).run(id, params.title, params.summary ?? "", params.content);
492
+ return {
493
+ id,
494
+ sourceId: params.sourceId,
495
+ documentId: params.documentId,
496
+ chunkId: params.chunkId,
497
+ rank: params.rank,
498
+ title: params.title,
499
+ summary: params.summary,
500
+ content: params.content,
501
+ category: params.category,
502
+ keywords: params.keywords,
503
+ titleEmbedding: params.titleEmbedding,
504
+ contentEmbedding: params.contentEmbedding,
505
+ createdAt
506
+ };
507
+ }
508
+ async getEvent(id) {
509
+ await this.init();
510
+ if (this.db === void 0) return void 0;
511
+ const row = this.db.prepare("SELECT * FROM knowledge_events WHERE id = ?").get(id);
512
+ return row === void 0 ? void 0 : rowToEvent(row);
513
+ }
514
+ async listEventsByDocument(documentId) {
515
+ await this.init();
516
+ if (this.db === void 0) return [];
517
+ return this.db.prepare("SELECT * FROM knowledge_events WHERE document_id = ? ORDER BY rank ASC").all(documentId).map(rowToEvent);
518
+ }
519
+ /** Find events by title vector similarity. */
520
+ async findEventsByTitleVector(queryVec, options = {}) {
521
+ await this.init();
522
+ if (this.db === void 0) return [];
523
+ const limit = options.limit ?? 20;
524
+ const threshold = options.threshold ?? 0;
525
+ const rows = this.db.prepare("SELECT id, title_embedding_json FROM knowledge_events WHERE title_embedding_json IS NOT NULL").all();
526
+ const scored = [];
527
+ for (const row of rows) {
528
+ const vec = jsonToVector(row.title_embedding_json);
529
+ if (vec === null) continue;
530
+ const score = this.embeddingEngine?.cosineSimilarity(queryVec, vec) ?? 0;
531
+ if (score >= threshold) scored.push({
532
+ id: row.id,
533
+ score
534
+ });
535
+ }
536
+ scored.sort((a, b) => b.score - a.score);
537
+ const top = scored.slice(0, limit);
538
+ const out = [];
539
+ for (const item of top) {
540
+ const event = await this.getEvent(item.id);
541
+ if (event !== void 0) out.push({
542
+ event,
543
+ score: item.score
544
+ });
545
+ }
546
+ return out;
547
+ }
548
+ async upsertEntity(params) {
549
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
550
+ if (!isEntityType(params.type)) throw new Error(`invalid entity type: ${params.type}`);
551
+ const normalizedName = normalizeName(params.name);
552
+ const existing = this.db.prepare("SELECT * FROM knowledge_entities WHERE source_id = ? AND type = ? AND normalized_name = ?").get(params.sourceId, params.type, normalizedName);
553
+ if (existing !== void 0) {
554
+ if (params.description !== null && existing["description"] === null) this.db.prepare("UPDATE knowledge_entities SET description = ? WHERE id = ?").run(params.description, String(existing["id"]));
555
+ if (params.embedding !== null && existing["embedding_json"] === null) this.db.prepare("UPDATE knowledge_entities SET embedding_json = ? WHERE id = ?").run(vectorToJson(params.embedding), String(existing["id"]));
556
+ return rowToEntity(existing);
557
+ }
558
+ const id = generateId("ent");
559
+ const createdAt = Date.now();
560
+ this.db.prepare(`INSERT INTO knowledge_entities
561
+ (id, source_id, type, name, normalized_name, description, embedding_json, created_at)
562
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.sourceId, params.type, params.name, normalizedName, params.description, vectorToJson(params.embedding), createdAt);
563
+ this.db.prepare(`INSERT INTO knowledge_entities_fts (rowid, name, description)
564
+ VALUES ((SELECT rowid FROM knowledge_entities WHERE id = ?), ?, ?)`).run(id, params.name, params.description ?? "");
565
+ return {
566
+ id,
567
+ sourceId: params.sourceId,
568
+ type: params.type,
569
+ name: params.name,
570
+ normalizedName,
571
+ description: params.description,
572
+ embedding: params.embedding,
573
+ createdAt
574
+ };
575
+ }
576
+ async findEntitiesByName(name, sourceId) {
577
+ await this.init();
578
+ if (this.db === void 0) return [];
579
+ const normalized = normalizeName(name);
580
+ return (sourceId === void 0 ? this.db.prepare("SELECT * FROM knowledge_entities WHERE normalized_name = ?").all(normalized) : this.db.prepare("SELECT * FROM knowledge_entities WHERE normalized_name = ? AND source_id = ?").all(normalized, sourceId)).map(rowToEntity);
581
+ }
582
+ /** Find entities by name vector similarity. */
583
+ async findEntitiesByVector(queryVec, options = {}) {
584
+ await this.init();
585
+ if (this.db === void 0) return [];
586
+ const limit = options.limit ?? 20;
587
+ const threshold = options.threshold ?? 0;
588
+ const rows = this.db.prepare("SELECT id, embedding_json FROM knowledge_entities WHERE embedding_json IS NOT NULL").all();
589
+ const scored = [];
590
+ for (const row of rows) {
591
+ const vec = jsonToVector(row.embedding_json);
592
+ if (vec === null) continue;
593
+ const score = this.embeddingEngine?.cosineSimilarity(queryVec, vec) ?? 0;
594
+ if (score >= threshold) scored.push({
595
+ id: row.id,
596
+ score
597
+ });
598
+ }
599
+ scored.sort((a, b) => b.score - a.score);
600
+ const top = scored.slice(0, limit);
601
+ const out = [];
602
+ for (const item of top) {
603
+ const row = this.db.prepare("SELECT * FROM knowledge_entities WHERE id = ?").get(item.id);
604
+ if (row !== void 0) out.push({
605
+ entity: rowToEntity(row),
606
+ score: item.score
607
+ });
608
+ }
609
+ return out;
610
+ }
611
+ async insertEventEntity(params) {
612
+ if (this.db === void 0) throw new Error("knowledge store not initialized");
613
+ const id = generateId("ee");
614
+ this.db.prepare(`INSERT OR IGNORE INTO knowledge_event_entities
615
+ (id, event_id, entity_id, weight, description, embedding_json)
616
+ VALUES (?, ?, ?, ?, ?, ?)`).run(id, params.eventId, params.entityId, params.weight ?? 1, params.description, vectorToJson(params.embedding));
617
+ return {
618
+ id,
619
+ eventId: params.eventId,
620
+ entityId: params.entityId,
621
+ weight: params.weight ?? 1,
622
+ description: params.description,
623
+ embedding: params.embedding
624
+ };
625
+ }
626
+ async findEventsByEntity(entityId) {
627
+ await this.init();
628
+ if (this.db === void 0) return [];
629
+ return this.db.prepare(`SELECT e.* FROM knowledge_events e
630
+ JOIN knowledge_event_entities ee ON ee.event_id = e.id
631
+ WHERE ee.entity_id = ?`).all(entityId).map(rowToEvent);
632
+ }
633
+ async findEntitiesByEvent(eventId) {
634
+ await this.init();
635
+ if (this.db === void 0) return [];
636
+ return this.db.prepare(`SELECT en.* FROM knowledge_entities en
637
+ JOIN knowledge_event_entities ee ON ee.entity_id = en.id
638
+ WHERE ee.event_id = ?`).all(eventId).map(rowToEntity);
639
+ }
640
+ async ftsSearchChunks(query, limit = 50) {
641
+ await this.init();
642
+ if (this.db === void 0) return [];
643
+ const ftsQuery = buildFtsQuery(query);
644
+ if (ftsQuery === void 0) return [];
645
+ return this.db.prepare(`SELECT c.* FROM knowledge_chunks c
646
+ JOIN knowledge_chunks_fts f ON c.rowid = f.rowid
647
+ WHERE f.knowledge_chunks_fts MATCH ?
648
+ LIMIT ?`).all(ftsQuery, limit).map(rowToChunk);
649
+ }
650
+ async searchChunksByVector(queryVec, options = {}) {
651
+ await this.init();
652
+ if (this.db === void 0) return [];
653
+ const limit = options.limit ?? 50;
654
+ const threshold = options.threshold ?? 0;
655
+ const rows = this.db.prepare("SELECT id, embedding_json FROM knowledge_chunks WHERE embedding_json IS NOT NULL").all();
656
+ const scored = [];
657
+ for (const row of rows) {
658
+ const vec = jsonToVector(row.embedding_json);
659
+ if (vec === null) continue;
660
+ const score = this.embeddingEngine?.cosineSimilarity(queryVec, vec) ?? 0;
661
+ if (score >= threshold) scored.push({
662
+ id: row.id,
663
+ score
664
+ });
665
+ }
666
+ scored.sort((a, b) => b.score - a.score);
667
+ const top = scored.slice(0, limit);
668
+ const out = [];
669
+ for (const item of top) {
670
+ const chunk = await this.getChunk(item.id);
671
+ if (chunk !== void 0) out.push({
672
+ chunk,
673
+ score: item.score
674
+ });
675
+ }
676
+ return out;
677
+ }
678
+ async stats() {
679
+ await this.init();
680
+ if (this.db === void 0) return {
681
+ sources: 0,
682
+ documents: 0,
683
+ chunks: 0,
684
+ events: 0,
685
+ entities: 0
686
+ };
687
+ const count = (table) => {
688
+ return this.db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get()?.c ?? 0;
689
+ };
690
+ return {
691
+ sources: count("knowledge_sources"),
692
+ documents: count("knowledge_documents"),
693
+ chunks: count("knowledge_chunks"),
694
+ events: count("knowledge_events"),
695
+ entities: count("knowledge_entities")
696
+ };
697
+ }
698
+ async buildSearchResult(chunkId, score, eventId = null) {
699
+ if (this.db === void 0) return void 0;
700
+ const chunk = await this.getChunk(chunkId);
701
+ if (chunk === void 0) return void 0;
702
+ if (this.db.prepare("SELECT * FROM knowledge_documents WHERE id = ?").get(chunk.documentId) === void 0) return void 0;
703
+ const sourceRow = this.db.prepare("SELECT * FROM knowledge_sources WHERE id = ?").get(chunk.sourceId);
704
+ if (sourceRow === void 0) return void 0;
705
+ let eventTitle = null;
706
+ if (eventId !== null) eventTitle = (await this.getEvent(eventId))?.title ?? null;
707
+ return {
708
+ chunkId: chunk.id,
709
+ documentId: chunk.documentId,
710
+ sourceId: chunk.sourceId,
711
+ sourceName: String(sourceRow["name"]),
712
+ heading: chunk.heading,
713
+ content: chunk.content,
714
+ score,
715
+ eventId,
716
+ eventTitle
717
+ };
718
+ }
719
+ };
720
+ function asString(value) {
721
+ if (typeof value === "string") return value;
722
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
723
+ return "";
724
+ }
725
+ function asNullableString(value) {
726
+ if (value === null || value === void 0) return null;
727
+ if (typeof value === "string") return value;
728
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
729
+ return null;
730
+ }
731
+ function rowToSource(row) {
732
+ return {
733
+ id: asString(row["id"]),
734
+ name: asString(row["name"]),
735
+ filePath: asNullableString(row["file_path"]),
736
+ description: asNullableString(row["description"]),
737
+ createdAt: Number(row["created_at"])
738
+ };
739
+ }
740
+ function rowToDocument(row) {
741
+ return {
742
+ id: asString(row["id"]),
743
+ sourceId: asString(row["source_id"]),
744
+ title: asString(row["title"]),
745
+ content: asNullableString(row["content"]),
746
+ status: asString(row["status"]),
747
+ chunkCount: Number(row["chunk_count"] ?? 0),
748
+ createdAt: Number(row["created_at"])
749
+ };
750
+ }
751
+ function rowToChunk(row) {
752
+ return {
753
+ id: asString(row["id"]),
754
+ sourceId: asString(row["source_id"]),
755
+ documentId: asString(row["document_id"]),
756
+ rank: Number(row["rank"]),
757
+ heading: asNullableString(row["heading"]),
758
+ content: asString(row["content"]),
759
+ rawContent: asNullableString(row["raw_content"]),
760
+ embedding: jsonToVector(asNullableString(row["embedding_json"])),
761
+ createdAt: Number(row["created_at"])
762
+ };
763
+ }
764
+ function rowToEvent(row) {
765
+ let keywords = [];
766
+ try {
767
+ const parsed = JSON.parse(asString(row["keywords"] ?? "[]"));
768
+ if (Array.isArray(parsed)) keywords = parsed.filter((k) => typeof k === "string");
769
+ } catch {}
770
+ return {
771
+ id: asString(row["id"]),
772
+ sourceId: asString(row["source_id"]),
773
+ documentId: asString(row["document_id"]),
774
+ chunkId: asString(row["chunk_id"]),
775
+ rank: Number(row["rank"]),
776
+ title: asString(row["title"]),
777
+ summary: asNullableString(row["summary"]),
778
+ content: asString(row["content"]),
779
+ category: asNullableString(row["category"]),
780
+ keywords,
781
+ titleEmbedding: jsonToVector(asNullableString(row["title_embedding_json"])),
782
+ contentEmbedding: jsonToVector(asNullableString(row["content_embedding_json"])),
783
+ createdAt: Number(row["created_at"])
784
+ };
785
+ }
786
+ function rowToEntity(row) {
787
+ return {
788
+ id: asString(row["id"]),
789
+ sourceId: asString(row["source_id"]),
790
+ type: asString(row["type"]),
791
+ name: asString(row["name"]),
792
+ normalizedName: asString(row["normalized_name"]),
793
+ description: asNullableString(row["description"]),
794
+ embedding: jsonToVector(asNullableString(row["embedding_json"])),
795
+ createdAt: Number(row["created_at"])
796
+ };
797
+ }
798
+ /**
799
+ * Tokenize text so FTS5's unicode61 tokenizer can index mixed CJK/ASCII text.
800
+ * Mirrors the memory package's `toFtsText`.
801
+ */
802
+ function toFtsText(text) {
803
+ const parts = text.toLowerCase().replaceAll(/([一-鿿㐀-䶿])([a-z0-9])/g, "$1 $2").replaceAll(/([a-z0-9])([一-鿿㐀-䶿])/g, "$1 $2").split(/[^a-z0-9一-鿿㐀-䶿]+/);
804
+ const tokens = [];
805
+ for (const part of parts) {
806
+ if (part.length === 0) continue;
807
+ if (/^[a-z0-9]+$/.test(part)) tokens.push(part);
808
+ else for (const ch of part) if (ch.length > 0) tokens.push(ch);
809
+ }
810
+ return tokens.join(" ");
811
+ }
812
+ function buildFtsQuery(search) {
813
+ const tokens = toFtsText(search).split(/\s+/).filter((t) => t.length > 0);
814
+ if (tokens.length === 0) return void 0;
815
+ return tokens.map((t) => `"${t.replaceAll("\"", "\"\"")}"`).join(" AND ");
816
+ }
817
+ //#endregion
818
+ //#region ../../packages/knowledge/src/chunking.ts
819
+ const HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
820
+ const CODE_FENCE_RE = /^(\s*)(`{3,}|~{3,})/;
821
+ /**
822
+ * Split a markdown document into sections using heading_strict strategy:
823
+ * each section begins at a heading and contains all body content until the
824
+ * next heading. Consecutive headings collapse to the lowest-level one as the
825
+ * main heading. Content before the first heading is grouped as "Introduction".
826
+ *
827
+ * Mirrors SAG's `buildHeadingStrictSections`.
828
+ */
829
+ function chunkMarkdown(content) {
830
+ const lines = content.split("\n");
831
+ const sections = [];
832
+ let buffer = [];
833
+ let currentHeading = null;
834
+ let currentLevel = null;
835
+ let rank = 0;
836
+ let inFence = false;
837
+ const flush = () => {
838
+ const raw = buffer.join("\n").trim();
839
+ const heading = currentHeading;
840
+ buffer = [];
841
+ if (raw.length === 0) return;
842
+ const body = stripMarkdown(raw);
843
+ if (body.length === 0 && heading === null) return;
844
+ sections.push({
845
+ heading,
846
+ headingLevel: currentLevel,
847
+ content: body,
848
+ rawContent: raw,
849
+ rank
850
+ });
851
+ rank += 1;
852
+ };
853
+ for (const line of lines) {
854
+ if (CODE_FENCE_RE.exec(line)) {
855
+ inFence = !inFence;
856
+ buffer.push(line);
857
+ continue;
858
+ }
859
+ if (inFence) {
860
+ buffer.push(line);
861
+ continue;
862
+ }
863
+ const headingMatch = HEADING_RE.exec(line);
864
+ if (headingMatch) {
865
+ flush();
866
+ currentHeading = headingMatch[2].trim();
867
+ currentLevel = headingMatch[1].length;
868
+ } else buffer.push(line);
869
+ }
870
+ flush();
871
+ return sections;
872
+ }
873
+ /**
874
+ * Strip markdown decorations that hurt embedding quality:
875
+ * code fences, inline code, images, links, emphasis markers.
876
+ *
877
+ * Mirrors SAG's `stripMarkdown`.
878
+ */
879
+ function stripMarkdown(text) {
880
+ return text.replaceAll(/^(\s*)```[^\n]*$\n?/gm, "$1").replaceAll(/^(\s*)```[^\n]*$/gm, "").replaceAll(/`([^`]+)`/g, "$1").replaceAll(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replaceAll(/\[([^\]]+)\]\([^)]+\)/g, "$1").replaceAll(/\*\*([^*]+)\*\*/g, "$1").replaceAll(/__([^_]+)__/g, "$1").replaceAll(/(?<!\w)\*([^*\n]+)\*(?!\w)/g, "$1").replaceAll(/(?<!\w)_([^_\n]+)_(?!\w)/g, "$1").replaceAll(/^#{1,6}\s+/gm, "").replaceAll(/\n{3,}/g, "\n\n").trim();
881
+ }
882
+ /**
883
+ * Split a plain text file into chunks by blank-line paragraphs.
884
+ * Each non-empty paragraph becomes one chunk with no heading.
885
+ * Long paragraphs are split at sentence boundaries to avoid huge chunks.
886
+ */
887
+ function chunkText(content) {
888
+ const MAX_PARA_TOKENS = 800;
889
+ const MAX_PARA_CHARS = MAX_PARA_TOKENS * 4;
890
+ const paragraphs = content.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0);
891
+ const sections = [];
892
+ let rank = 0;
893
+ for (const para of paragraphs) {
894
+ if (estimateTokens(para) <= MAX_PARA_TOKENS) {
895
+ sections.push({
896
+ heading: null,
897
+ headingLevel: null,
898
+ content: para,
899
+ rawContent: para,
900
+ rank: rank++
901
+ });
902
+ continue;
903
+ }
904
+ const sentences = para.match(/[^.!?。!?\n]+[.!?。!?\n]+|[^.!?。!?\n]+$/g) ?? [para];
905
+ let buffer = "";
906
+ const flushBuffer = () => {
907
+ const text = buffer.trim();
908
+ if (text.length === 0) return;
909
+ sections.push({
910
+ heading: null,
911
+ headingLevel: null,
912
+ content: text,
913
+ rawContent: text,
914
+ rank: rank++
915
+ });
916
+ buffer = "";
917
+ };
918
+ for (const sentence of sentences) {
919
+ const candidate = (buffer + " " + sentence).trim();
920
+ if (buffer.length > 0 && candidate.length > MAX_PARA_CHARS) flushBuffer();
921
+ buffer = buffer.length === 0 ? sentence : `${buffer} ${sentence}`;
922
+ }
923
+ flushBuffer();
924
+ }
925
+ return sections;
926
+ }
927
+ /** Rough token estimate — chars / 4 (English-leaning; CJK underestimates). */
928
+ function estimateTokens(text) {
929
+ return Math.ceil(text.length / 4);
930
+ }
931
+ //#endregion
932
+ //#region ../../packages/knowledge/src/extractor.ts
933
+ const ENTITY_TYPES = [
934
+ "person",
935
+ "organization",
936
+ "location",
937
+ "time",
938
+ "product",
939
+ "metric",
940
+ "action",
941
+ "work",
942
+ "group",
943
+ "subject",
944
+ "tags"
945
+ ];
946
+ const EXTRACTION_SYSTEM_PROMPT = `You are a professional knowledge content extractor for a SAG-style event-centric knowledge graph.
947
+
948
+ Your task: given a markdown section (one chunk), extract exactly ONE fused event that captures the section's core informational content, plus the entities mentioned.
949
+
950
+ # Output Format
951
+
952
+ Return a JSON object with this exact shape:
953
+
954
+ {
955
+ "items": [
956
+ {
957
+ "title": "short event title (≤80 chars, noun phrase or short sentence)",
958
+ "summary": "one-sentence summary of what this section is about",
959
+ "content": "the full informational content of this section, rephrased as a self-contained event description (1-4 sentences)",
960
+ "category": "one of: definition, process, comparison, decision, history, reference, other",
961
+ "keywords": ["3-5 short keywords or phrases"],
962
+ "entities": [
963
+ { "type": "one of ENTITY_TYPES below", "name": "entity name", "description": "one-line context for the entity" }
964
+ ]
965
+ }
966
+ ]
967
+ }
968
+
969
+ # Entity Types
970
+
971
+ Use only these types:
972
+ - person — named individuals
973
+ - organization — companies, teams, institutions
974
+ - location — places, geographic regions
975
+ - time — dates, time periods, eras
976
+ - product — products, services, software, tools
977
+ - metric — numbers, statistics, measurements
978
+ - action — processes, operations, activities
979
+ - work — books, papers, projects, artworks
980
+ - group — categories, classes, groups of things
981
+ - subject — abstract topics, concepts, fields
982
+ - tags — use sparingly for cross-cutting labels
983
+
984
+ # Rules
985
+
986
+ 1. Produce EXACTLY ONE item in the "items" array.
987
+ 2. The event title should be specific enough to disambiguate from sibling sections.
988
+ 3. The content must be self-contained — a reader should understand it without the surrounding context.
989
+ 4. Extract 2-6 entities per chunk. Only include entities that are clearly referenced in the text.
990
+ 5. Use the entity's most specific name (e.g. "Anthropic" rather than "the company").
991
+ 6. Output language must follow the main input language. Chinese input requires Chinese title, summary, content, category, keywords, and entity descriptions. English input requires English fields.
992
+ 7. Output ONLY the JSON object — no prose, no markdown fences, no commentary.
993
+
994
+ # Example
995
+
996
+ Input section:
997
+ """
998
+ ## Model Context Protocol
999
+
1000
+ The Model Context Protocol (MCP) is an open standard introduced by Anthropic in 2024. It defines a JSON-RPC interface between AI assistants and external tools. MCP servers expose resources, prompts, and tools that clients like Claude Desktop can consume.
1001
+ """
1002
+
1003
+ Output:
1004
+ {
1005
+ "items": [
1006
+ {
1007
+ "title": "Model Context Protocol introduction",
1008
+ "summary": "MCP is an open standard for AI assistant / tool integration introduced by Anthropic in 2024.",
1009
+ "content": "The Model Context Protocol (MCP) is an open standard introduced by Anthropic in 2024 that defines a JSON-RPC interface between AI assistants and external tools. MCP servers expose resources, prompts, and tools that clients like Claude Desktop can consume.",
1010
+ "category": "definition",
1011
+ "keywords": ["MCP", "JSON-RPC", "Anthropic", "AI tools"],
1012
+ "entities": [
1013
+ { "type": "product", "name": "Model Context Protocol", "description": "Open standard for AI assistant / tool integration" },
1014
+ { "type": "organization", "name": "Anthropic", "description": "Company that introduced MCP in 2024" },
1015
+ { "type": "time", "name": "2024", "description": "Year MCP was introduced" },
1016
+ { "type": "product", "name": "Claude Desktop", "description": "An MCP client application" }
1017
+ ]
1018
+ }
1019
+ ]
1020
+ }`;
1021
+ /**
1022
+ * Build the user prompt for extracting events/entities from a chunk.
1023
+ * Includes a one-shot example to anchor the expected output shape.
1024
+ */
1025
+ function buildExtractionUserPrompt(chunk) {
1026
+ return [
1027
+ "Extract the fused event and entities from the following markdown section.",
1028
+ "",
1029
+ `Section heading: ${chunk.heading ?? "(no heading)"}`,
1030
+ "",
1031
+ "Section content:",
1032
+ "\"\"\"",
1033
+ chunk.content,
1034
+ "\"\"\"",
1035
+ "",
1036
+ "Return only the JSON object per the schema."
1037
+ ].join("\n");
1038
+ }
1039
+ /** Extract the first {...} JSON block from a text response. */
1040
+ function extractJsonFromText(text) {
1041
+ const match = text.match(/\{[\s\S]*\}/);
1042
+ return match === null ? null : match[0];
1043
+ }
1044
+ /** Parse LLM extraction output into structured events. Returns at most 1 event. */
1045
+ function parseExtractionResponse(text, fallbackChunk) {
1046
+ const jsonText = extractJsonFromText(text);
1047
+ if (jsonText === null) return fallbackEvent(fallbackChunk);
1048
+ try {
1049
+ const items = JSON.parse(jsonText)["items"];
1050
+ if (!Array.isArray(items) || items.length === 0) return fallbackEvent(fallbackChunk);
1051
+ const first = items[0];
1052
+ if (first === null || typeof first !== "object") return fallbackEvent(fallbackChunk);
1053
+ const obj = first;
1054
+ return {
1055
+ title: typeof obj["title"] === "string" && obj["title"].trim().length > 0 ? obj["title"].trim() : fallbackChunk.heading ?? "Untitled",
1056
+ summary: typeof obj["summary"] === "string" ? obj["summary"].trim() : "",
1057
+ content: typeof obj["content"] === "string" && obj["content"].trim().length > 0 ? obj["content"].trim() : fallbackChunk.content,
1058
+ category: typeof obj["category"] === "string" ? obj["category"].trim() : "other",
1059
+ keywords: parseKeywords(obj["keywords"]),
1060
+ entities: parseEntities(obj["entities"])
1061
+ };
1062
+ } catch {
1063
+ return fallbackEvent(fallbackChunk);
1064
+ }
1065
+ }
1066
+ function parseKeywords(value) {
1067
+ if (!Array.isArray(value)) return [];
1068
+ const out = [];
1069
+ for (const item of value) if (typeof item === "string" && item.trim().length > 0) out.push(item.trim());
1070
+ return out.slice(0, 10);
1071
+ }
1072
+ function parseEntities(value) {
1073
+ if (!Array.isArray(value)) return [];
1074
+ const out = [];
1075
+ const seen = /* @__PURE__ */ new Set();
1076
+ for (const item of value) {
1077
+ if (item === null || typeof item !== "object") continue;
1078
+ const obj = item;
1079
+ const type = typeof obj["type"] === "string" ? obj["type"] : "";
1080
+ const name = typeof obj["name"] === "string" ? obj["name"].trim() : "";
1081
+ const description = typeof obj["description"] === "string" ? obj["description"].trim() : "";
1082
+ if (name.length === 0) continue;
1083
+ if (!ENTITY_TYPES.includes(type)) continue;
1084
+ const key = `${type}|${name.toLowerCase()}`;
1085
+ if (seen.has(key)) continue;
1086
+ seen.add(key);
1087
+ out.push({
1088
+ type,
1089
+ name,
1090
+ description
1091
+ });
1092
+ }
1093
+ return out.slice(0, 20);
1094
+ }
1095
+ function fallbackEvent(chunk) {
1096
+ return {
1097
+ title: chunk.heading ?? "Untitled section",
1098
+ summary: "",
1099
+ content: chunk.content,
1100
+ category: "other",
1101
+ keywords: [],
1102
+ entities: []
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Run extraction on a single chunk via the LLM caller.
1107
+ * Returns the parsed event (with fallback on failure).
1108
+ */
1109
+ async function extractEventFromChunk(llm, chunk) {
1110
+ try {
1111
+ return parseExtractionResponse(await llm.generate(EXTRACTION_SYSTEM_PROMPT, buildExtractionUserPrompt(chunk)), chunk);
1112
+ } catch {
1113
+ return fallbackEvent(chunk);
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Ask the LLM to rerank candidate events by relevance to a query.
1118
+ * Returns the candidate ids in reranked order (most relevant first).
1119
+ *
1120
+ * If the LLM fails or returns unparseable output, returns the input order.
1121
+ */
1122
+ async function rerankEventsWithLlm(llm, query, candidates, topK) {
1123
+ if (candidates.length === 0) return [];
1124
+ if (candidates.length <= topK) return candidates.map((c) => c.id);
1125
+ const candidateList = candidates.map((c, i) => `[${i}] id=${c.id}\n title: ${c.title}\n summary: ${c.summary}`).join("\n");
1126
+ const system = `You are a precise relevance judge for a knowledge base retrieval system.
1127
+
1128
+ Given a user query and a list of candidate events, select the ${topK} event ids most useful for answering the query.
1129
+
1130
+ Return ONLY a JSON object: {"ids": ["id1", "id2", ...]}
1131
+
1132
+ The ids must come from the candidate list. Output at most ${topK} ids, most relevant first. No prose, no markdown.`;
1133
+ const user = `Query: ${query}
1134
+
1135
+ Candidates:
1136
+ ${candidateList}
1137
+
1138
+ Return the JSON object now.`;
1139
+ try {
1140
+ const jsonText = extractJsonFromText(await llm.generate(system, user));
1141
+ if (jsonText === null) return candidates.slice(0, topK).map((c) => c.id);
1142
+ const parsed = JSON.parse(jsonText);
1143
+ if (!Array.isArray(parsed["ids"])) return candidates.slice(0, topK).map((c) => c.id);
1144
+ const validIds = new Set(candidates.map((c) => c.id));
1145
+ const result = [];
1146
+ for (const id of parsed["ids"]) {
1147
+ if (typeof id === "string" && validIds.has(id) && !result.includes(id)) result.push(id);
1148
+ if (result.length >= topK) break;
1149
+ }
1150
+ if (result.length === 0) return candidates.slice(0, topK).map((c) => c.id);
1151
+ return result;
1152
+ } catch {
1153
+ return candidates.slice(0, topK).map((c) => c.id);
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Extract named entities from a query for entity-recall retrieval.
1158
+ * Returns a list of {type, name} pairs. Best-effort — falls back to empty.
1159
+ */
1160
+ async function extractQueryEntities(llm, query) {
1161
+ const system = `Extract named entities from the user query. Return ONLY a JSON object: {"entities": [{"type": "...", "name": "..."}]}
1162
+
1163
+ Use these entity types: ${ENTITY_TYPES.join(", ")}.
1164
+
1165
+ Only include entities that explicitly appear in the query. If no clear entities, return an empty array.`;
1166
+ try {
1167
+ const jsonText = extractJsonFromText(await llm.generate(system, `Query: ${query}`));
1168
+ if (jsonText === null) return [];
1169
+ const parsed = JSON.parse(jsonText);
1170
+ if (!Array.isArray(parsed["entities"])) return [];
1171
+ const out = [];
1172
+ for (const item of parsed["entities"]) {
1173
+ if (item === null || typeof item !== "object") continue;
1174
+ const obj = item;
1175
+ const type = typeof obj["type"] === "string" ? obj["type"] : "";
1176
+ const name = typeof obj["name"] === "string" ? obj["name"].trim() : "";
1177
+ if (name.length === 0) continue;
1178
+ if (!ENTITY_TYPES.includes(type)) continue;
1179
+ out.push({
1180
+ type,
1181
+ name
1182
+ });
1183
+ }
1184
+ return out;
1185
+ } catch {
1186
+ return [];
1187
+ }
1188
+ }
1189
+ //#endregion
1190
+ //#region ../../packages/knowledge/src/ingest.ts
1191
+ const LLM_CONCURRENCY = 5;
1192
+ const SUPPORTED_EXTENSIONS = new Set([
1193
+ ".md",
1194
+ ".markdown",
1195
+ ".txt"
1196
+ ]);
1197
+ /** Read a file and return its content. */
1198
+ async function readFileContent(filePath) {
1199
+ return readFile(filePath, "utf-8");
1200
+ }
1201
+ function getFileExtension(filePath) {
1202
+ const lastDot = filePath.lastIndexOf(".");
1203
+ return lastDot > 0 ? filePath.slice(lastDot).toLowerCase() : "";
1204
+ }
1205
+ function chunkContent(content, ext) {
1206
+ return ext === ".txt" ? chunkText(content) : chunkMarkdown(content);
1207
+ }
1208
+ function isSupportedFile(filePath) {
1209
+ return SUPPORTED_EXTENSIONS.has(getFileExtension(filePath));
1210
+ }
1211
+ /** Map async iterator values with a concurrency limit. */
1212
+ async function mapWithConcurrency(items, limit, fn, onProgress) {
1213
+ const results = Array.from({ length: items.length });
1214
+ let cursor = 0;
1215
+ let completed = 0;
1216
+ const total = items.length;
1217
+ async function worker() {
1218
+ while (cursor < items.length) {
1219
+ const idx = cursor;
1220
+ cursor += 1;
1221
+ const item = items[idx];
1222
+ const result = await fn(item, idx);
1223
+ results[idx] = result;
1224
+ completed += 1;
1225
+ onProgress?.(completed, total, result);
1226
+ }
1227
+ }
1228
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
1229
+ await Promise.all(workers);
1230
+ return results;
1231
+ }
1232
+ /**
1233
+ * Ingest a markdown file into the knowledge base.
1234
+ *
1235
+ * Steps:
1236
+ * 1. Dedupe by file_path — if a source already exists for this file, error.
1237
+ * 2. Read file → chunkMarkdown (heading_strict).
1238
+ * 3. Embed chunks ("{heading}\n{content}") → store knowledge_chunks.
1239
+ * 4. For each chunk (concurrency 3): LLM extract 1 event + N entities.
1240
+ * 5. Embed event title + content ("{title}\n\n{content}") → store knowledge_events.
1241
+ * 6. Upsert entities (dedupe by source_id+type+normalizedName) + embed entity name → store.
1242
+ * 7. For each (event, entity) pair: embed relation (entity.description || "{eventTitle} {entityName}") → store knowledge_event_entities.
1243
+ * 8. Update document status = 'completed'.
1244
+ *
1245
+ * All writes run inside a single transaction — a mid-ingest failure rolls back
1246
+ * every partial row (source/document/chunks/events/entities/edges).
1247
+ *
1248
+ * Reports progress via callback.
1249
+ */
1250
+ async function ingestFile(store, llm, filePath, onProgress) {
1251
+ const engine = store.getEmbeddingEngine();
1252
+ if (engine === void 0 || !engine.available) {
1253
+ onProgress?.({
1254
+ stage: "error",
1255
+ message: "embedding engine unavailable"
1256
+ });
1257
+ throw new Error("embedding engine unavailable — knowledge base requires fastembed");
1258
+ }
1259
+ const existing = await store.findSourceByFilePath(filePath);
1260
+ if (existing !== void 0) {
1261
+ onProgress?.({
1262
+ stage: "error",
1263
+ message: `文件已摄入:${existing.name}(source id=${existing.id})`
1264
+ });
1265
+ throw new Error(`file already ingested: ${filePath} (source ${existing.id})`);
1266
+ }
1267
+ onProgress?.({
1268
+ stage: "chunking",
1269
+ message: `读取并切分文件: ${basename(filePath)}`
1270
+ });
1271
+ const content = await readFileContent(filePath);
1272
+ const sections = chunkContent(content, getFileExtension(filePath));
1273
+ if (sections.length === 0) {
1274
+ onProgress?.({
1275
+ stage: "error",
1276
+ message: "文件无有效内容"
1277
+ });
1278
+ throw new Error("file has no content to ingest");
1279
+ }
1280
+ store.beginTransaction();
1281
+ try {
1282
+ const fileName = basename(filePath);
1283
+ const source = await store.createSource({
1284
+ name: fileName,
1285
+ filePath,
1286
+ description: null
1287
+ });
1288
+ const document = await store.createDocument({
1289
+ sourceId: source.id,
1290
+ title: fileName,
1291
+ content
1292
+ });
1293
+ onProgress?.({
1294
+ stage: "embedding-chunks",
1295
+ chunkIndex: 0,
1296
+ totalChunks: sections.length,
1297
+ message: `嵌入 chunks: 0/${sections.length}`
1298
+ });
1299
+ const chunkEmbeddings = await engine.embedBatch(sections.map((s) => s.heading !== null ? `${s.heading}\n${s.content}` : s.content));
1300
+ if (chunkEmbeddings === null) {
1301
+ onProgress?.({
1302
+ stage: "error",
1303
+ message: "chunk embedding failed"
1304
+ });
1305
+ throw new Error("chunk embedding failed");
1306
+ }
1307
+ const chunks = [];
1308
+ for (let i = 0; i < sections.length; i++) {
1309
+ const section = sections[i];
1310
+ const embedding = chunkEmbeddings[i] ?? null;
1311
+ const chunk = await store.insertChunk({
1312
+ sourceId: source.id,
1313
+ documentId: document.id,
1314
+ rank: section.rank,
1315
+ heading: section.heading,
1316
+ content: section.content,
1317
+ rawContent: section.rawContent,
1318
+ embedding
1319
+ });
1320
+ chunks.push(chunk);
1321
+ onProgress?.({
1322
+ stage: "embedding-chunks",
1323
+ chunkIndex: i + 1,
1324
+ totalChunks: sections.length,
1325
+ message: `嵌入 chunks: ${i + 1}/${sections.length}`
1326
+ });
1327
+ }
1328
+ onProgress?.({
1329
+ stage: "extracting",
1330
+ chunkIndex: 0,
1331
+ totalChunks: sections.length,
1332
+ message: `抽取事件: 0/${sections.length}`
1333
+ });
1334
+ const extractedEvents = await mapWithConcurrency(sections, LLM_CONCURRENCY, (section) => extractEventFromChunk(llm, section), (completed, total) => {
1335
+ onProgress?.({
1336
+ stage: "extracting",
1337
+ chunkIndex: completed,
1338
+ totalChunks: total,
1339
+ message: `抽取事件: ${completed}/${total}`
1340
+ });
1341
+ });
1342
+ onProgress?.({
1343
+ stage: "embedding-events",
1344
+ chunkIndex: 0,
1345
+ totalChunks: sections.length,
1346
+ message: `嵌入 events: 0/${sections.length}`
1347
+ });
1348
+ const titleTexts = extractedEvents.map((e) => e.title);
1349
+ const contentTexts = extractedEvents.map((e) => `${e.title}\n\n${e.content}`);
1350
+ const [titleEmbeddings, contentEmbeddings] = await Promise.all([engine.embedBatch(titleTexts), engine.embedBatch(contentTexts)]);
1351
+ if (titleEmbeddings === null || contentEmbeddings === null) {
1352
+ onProgress?.({
1353
+ stage: "error",
1354
+ message: "event embedding failed"
1355
+ });
1356
+ throw new Error("event embedding failed");
1357
+ }
1358
+ const events = [];
1359
+ for (let i = 0; i < extractedEvents.length; i++) {
1360
+ const extracted = extractedEvents[i];
1361
+ const chunk = chunks[i];
1362
+ const event = await store.insertEvent({
1363
+ sourceId: source.id,
1364
+ documentId: document.id,
1365
+ chunkId: chunk.id,
1366
+ rank: i,
1367
+ title: extracted.title,
1368
+ summary: extracted.summary.length > 0 ? extracted.summary : null,
1369
+ content: extracted.content,
1370
+ category: extracted.category.length > 0 ? extracted.category : null,
1371
+ keywords: extracted.keywords,
1372
+ titleEmbedding: titleEmbeddings[i] ?? null,
1373
+ contentEmbedding: contentEmbeddings[i] ?? null
1374
+ });
1375
+ events.push(event);
1376
+ onProgress?.({
1377
+ stage: "embedding-events",
1378
+ chunkIndex: i + 1,
1379
+ totalChunks: sections.length,
1380
+ message: `嵌入 events: ${i + 1}/${sections.length}`
1381
+ });
1382
+ }
1383
+ onProgress?.({
1384
+ stage: "embedding-entities",
1385
+ message: "嵌入 entities..."
1386
+ });
1387
+ const entityMap = /* @__PURE__ */ new Map();
1388
+ for (const extracted of extractedEvents) for (const entity of extracted.entities) {
1389
+ const key = `${entity.type}|${entity.name.toLowerCase()}`;
1390
+ if (!entityMap.has(key)) entityMap.set(key, entity);
1391
+ }
1392
+ const uniqueEntities = Array.from(entityMap.values());
1393
+ const entityEmbeddings = uniqueEntities.length > 0 ? await engine.embedBatch(uniqueEntities.map((e) => e.name)) : [];
1394
+ if (uniqueEntities.length > 0 && entityEmbeddings === null) {
1395
+ onProgress?.({
1396
+ stage: "error",
1397
+ message: "entity embedding failed"
1398
+ });
1399
+ throw new Error("entity embedding failed");
1400
+ }
1401
+ const entityIdByEntityKey = /* @__PURE__ */ new Map();
1402
+ for (let i = 0; i < uniqueEntities.length; i++) {
1403
+ const entity = uniqueEntities[i];
1404
+ const embedding = entityEmbeddings?.[i] ?? null;
1405
+ const stored = await store.upsertEntity({
1406
+ sourceId: source.id,
1407
+ type: entity.type,
1408
+ name: entity.name,
1409
+ description: entity.description.length > 0 ? entity.description : null,
1410
+ embedding
1411
+ });
1412
+ entityIdByEntityKey.set(`${entity.type}|${entity.name.toLowerCase()}`, stored.id);
1413
+ }
1414
+ onProgress?.({
1415
+ stage: "embedding-relations",
1416
+ message: "嵌入关系..."
1417
+ });
1418
+ const relationPairs = [];
1419
+ for (let i = 0; i < extractedEvents.length; i++) {
1420
+ const extracted = extractedEvents[i];
1421
+ for (const entity of extracted.entities) relationPairs.push({
1422
+ eventIndex: i,
1423
+ entity
1424
+ });
1425
+ }
1426
+ const relationTexts = relationPairs.map(({ eventIndex, entity }) => {
1427
+ const event = events[eventIndex];
1428
+ return entity.description.length > 0 ? entity.description : `${event.title} ${entity.name}`;
1429
+ });
1430
+ const relationEmbeddings = relationPairs.length > 0 ? await engine.embedBatch(relationTexts) : [];
1431
+ if (relationPairs.length > 0 && relationEmbeddings === null) {
1432
+ onProgress?.({
1433
+ stage: "error",
1434
+ message: "relation embedding failed"
1435
+ });
1436
+ throw new Error("relation embedding failed");
1437
+ }
1438
+ for (let i = 0; i < relationPairs.length; i++) {
1439
+ const pair = relationPairs[i];
1440
+ const event = events[pair.eventIndex];
1441
+ const entityKey = `${pair.entity.type}|${pair.entity.name.toLowerCase()}`;
1442
+ const entityId = entityIdByEntityKey.get(entityKey);
1443
+ if (entityId === void 0) continue;
1444
+ const embedding = relationEmbeddings?.[i] ?? null;
1445
+ await store.insertEventEntity({
1446
+ eventId: event.id,
1447
+ entityId,
1448
+ weight: 1,
1449
+ description: pair.entity.description.length > 0 ? pair.entity.description : null,
1450
+ embedding
1451
+ });
1452
+ }
1453
+ await store.updateDocumentStatus(document.id, "completed", chunks.length);
1454
+ store.commitTransaction();
1455
+ onProgress?.({
1456
+ stage: "completed",
1457
+ message: `摄入完成:${chunks.length} chunks, ${events.length} events, ${uniqueEntities.length} entities`
1458
+ });
1459
+ return {
1460
+ documentId: document.id,
1461
+ chunkCount: chunks.length,
1462
+ eventCount: events.length,
1463
+ entityCount: uniqueEntities.length
1464
+ };
1465
+ } catch (error) {
1466
+ store.rollbackTransaction();
1467
+ throw error;
1468
+ }
1469
+ }
1470
+ async function collectSupportedFiles(dirPath) {
1471
+ const files = [];
1472
+ const entries = await readdir(dirPath, { withFileTypes: true });
1473
+ for (const entry of entries) {
1474
+ const fullPath = join(dirPath, entry.name);
1475
+ if (entry.isDirectory()) files.push(...await collectSupportedFiles(fullPath));
1476
+ else if (entry.isFile() && isSupportedFile(fullPath)) files.push(fullPath);
1477
+ }
1478
+ return files.sort();
1479
+ }
1480
+ async function ingestDirectory(store, llm, dirPath, onProgress) {
1481
+ const files = await collectSupportedFiles(dirPath);
1482
+ if (files.length === 0) throw new Error(`no supported files (.md, .markdown, .txt) found in ${dirPath}`);
1483
+ const errors = [];
1484
+ let succeeded = 0;
1485
+ let totalChunks = 0;
1486
+ let totalEvents = 0;
1487
+ let totalEntities = 0;
1488
+ for (let i = 0; i < files.length; i++) {
1489
+ const filePath = files[i];
1490
+ const fileProgress = (progress) => {
1491
+ const prefix = `批量摄入 ${i + 1}/${files.length}: ${basename(filePath)}`;
1492
+ onProgress?.({
1493
+ ...progress,
1494
+ message: `${prefix} · ${progress.message}`
1495
+ });
1496
+ };
1497
+ try {
1498
+ const result = await ingestFile(store, llm, filePath, fileProgress);
1499
+ succeeded += 1;
1500
+ totalChunks += result.chunkCount;
1501
+ totalEvents += result.eventCount;
1502
+ totalEntities += result.entityCount;
1503
+ } catch (error) {
1504
+ const message = error instanceof Error ? error.message : String(error);
1505
+ errors.push({
1506
+ filePath,
1507
+ message
1508
+ });
1509
+ onProgress?.({
1510
+ stage: "error",
1511
+ message: `批量摄入 ${i + 1}/${files.length}: ${basename(filePath)} · 失败: ${message}`
1512
+ });
1513
+ }
1514
+ }
1515
+ onProgress?.({
1516
+ stage: "completed",
1517
+ message: `批量摄入完成:${succeeded}/${files.length} 个文件成功,${totalChunks} chunks, ${totalEvents} events, ${totalEntities} entities`
1518
+ });
1519
+ return {
1520
+ succeeded,
1521
+ failed: errors.length,
1522
+ totalChunks,
1523
+ totalEvents,
1524
+ totalEntities,
1525
+ errors
1526
+ };
1527
+ }
1528
+ //#endregion
1529
+ //#region ../../packages/knowledge/src/search.ts
1530
+ const DEFAULT_TOP_K = 5;
1531
+ const MAX_TOP_K = 20;
1532
+ const SEED_EVENT_LIMIT = 30;
1533
+ const EXPANDED_EVENT_LIMIT = 100;
1534
+ const COARSE_RANK_LIMIT = 50;
1535
+ const ENTITY_VECTOR_THRESHOLD = .7;
1536
+ const TITLE_VECTOR_THRESHOLD = .4;
1537
+ /**
1538
+ * Multi-hop retrieval:
1539
+ * 1. Vectorize query.
1540
+ * 2. Entity recall — LLM extracts entities from query → match by name + vector.
1541
+ * 3. Seed events — events linked to recalled entities + events by title vector similarity.
1542
+ * 4. BFS expand (1 hop) — from seed events, walk to entities, then to other events.
1543
+ * 5. Coarse rank — score graph-reachable events by content embedding similarity.
1544
+ * Graph associativity is the gate; content vector orders within it.
1545
+ * 6. LLM rerank — pick top-K most relevant.
1546
+ * 7. Return corresponding chunks (deduped by chunk_id) with scores and provenance.
1547
+ * If rerank yields fewer than topK chunks, backfill by direct chunk vector search.
1548
+ */
1549
+ async function multiSearch(store, llm, query, options = {}) {
1550
+ const topK = Math.min(options.topK ?? DEFAULT_TOP_K, MAX_TOP_K);
1551
+ const engine = store.getEmbeddingEngine();
1552
+ if (engine === void 0 || !engine.available) return ftsFallback(store, query, topK);
1553
+ const queryEmbeddings = await engine.embedBatch([query]);
1554
+ if (queryEmbeddings === null || queryEmbeddings.length === 0) return ftsFallback(store, query, topK);
1555
+ const queryVec = queryEmbeddings[0];
1556
+ const recalledEntities = await recallEntities(store, llm, query, queryVec);
1557
+ const seedEventIds = /* @__PURE__ */ new Set();
1558
+ for (const entity of recalledEntities) {
1559
+ const events = await store.findEventsByEntity(entity.id);
1560
+ for (const event of events) seedEventIds.add(event.id);
1561
+ }
1562
+ const titleMatches = await store.findEventsByTitleVector(queryVec, {
1563
+ limit: SEED_EVENT_LIMIT,
1564
+ threshold: TITLE_VECTOR_THRESHOLD
1565
+ });
1566
+ for (const { event } of titleMatches) seedEventIds.add(event.id);
1567
+ const expandedEventIds = new Set(seedEventIds);
1568
+ for (const eventId of seedEventIds) {
1569
+ const entities = await store.findEntitiesByEvent(eventId);
1570
+ for (const entity of entities) {
1571
+ const neighborEvents = await store.findEventsByEntity(entity.id);
1572
+ for (const neighbor of neighborEvents) {
1573
+ expandedEventIds.add(neighbor.id);
1574
+ if (expandedEventIds.size >= EXPANDED_EVENT_LIMIT) break;
1575
+ }
1576
+ if (expandedEventIds.size >= EXPANDED_EVENT_LIMIT) break;
1577
+ }
1578
+ if (expandedEventIds.size >= EXPANDED_EVENT_LIMIT) break;
1579
+ }
1580
+ const candidates = [];
1581
+ for (const eventId of expandedEventIds) {
1582
+ const event = await store.getEvent(eventId);
1583
+ if (event === void 0) continue;
1584
+ const vec = event.contentEmbedding;
1585
+ const score = vec === null ? 0 : engine.cosineSimilarity(queryVec, vec);
1586
+ candidates.push({
1587
+ id: event.id,
1588
+ title: event.title,
1589
+ summary: event.summary ?? "",
1590
+ score,
1591
+ chunkId: event.chunkId
1592
+ });
1593
+ }
1594
+ if (candidates.length === 0) return chunkVectorFallback(store, queryVec, topK);
1595
+ candidates.sort((a, b) => b.score - a.score);
1596
+ const topCandidates = candidates.slice(0, COARSE_RANK_LIMIT);
1597
+ let rankedIds;
1598
+ if (options.skipRerank === true || topCandidates.length <= topK) rankedIds = topCandidates.map((c) => c.id);
1599
+ else rankedIds = await rerankEventsWithLlm(llm, query, topCandidates.map((c) => ({
1600
+ id: c.id,
1601
+ title: c.title,
1602
+ summary: c.summary
1603
+ })), topK);
1604
+ const seen = /* @__PURE__ */ new Set();
1605
+ const results = [];
1606
+ for (const eventId of rankedIds) {
1607
+ const candidate = topCandidates.find((c) => c.id === eventId);
1608
+ if (candidate === void 0) continue;
1609
+ if (seen.has(candidate.chunkId)) continue;
1610
+ seen.add(candidate.chunkId);
1611
+ const result = await store.buildSearchResult(candidate.chunkId, candidate.score, eventId);
1612
+ if (result !== void 0) results.push(result);
1613
+ if (results.length >= topK) break;
1614
+ }
1615
+ if (results.length < topK) for (const candidate of topCandidates) {
1616
+ if (results.length >= topK) break;
1617
+ if (rankedIds.includes(candidate.id)) continue;
1618
+ if (seen.has(candidate.chunkId)) continue;
1619
+ seen.add(candidate.chunkId);
1620
+ const result = await store.buildSearchResult(candidate.chunkId, candidate.score, candidate.id);
1621
+ if (result !== void 0) results.push(result);
1622
+ }
1623
+ if (results.length < topK) {
1624
+ const remaining = topK - results.length;
1625
+ const backfill = await store.searchChunksByVector(queryVec, { limit: remaining * 2 });
1626
+ for (const { chunk, score } of backfill) {
1627
+ if (results.length >= topK) break;
1628
+ if (seen.has(chunk.id)) continue;
1629
+ seen.add(chunk.id);
1630
+ const result = await store.buildSearchResult(chunk.id, score);
1631
+ if (result !== void 0) results.push(result);
1632
+ }
1633
+ }
1634
+ return results;
1635
+ }
1636
+ /** Recall entities by name (LLM-extracted) and by vector similarity. */
1637
+ async function recallEntities(store, llm, query, queryVec) {
1638
+ const out = /* @__PURE__ */ new Map();
1639
+ const queryEntities = await extractQueryEntities(llm, query);
1640
+ for (const { name } of queryEntities) {
1641
+ const matches = await store.findEntitiesByName(name);
1642
+ for (const entity of matches) out.set(entity.id, { id: entity.id });
1643
+ }
1644
+ const vectorMatches = await store.findEntitiesByVector(queryVec, {
1645
+ limit: 20,
1646
+ threshold: ENTITY_VECTOR_THRESHOLD
1647
+ });
1648
+ for (const { entity } of vectorMatches) out.set(entity.id, { id: entity.id });
1649
+ return Array.from(out.values());
1650
+ }
1651
+ /** FTS5 keyword fallback when embeddings are unavailable. */
1652
+ async function ftsFallback(store, query, topK) {
1653
+ const chunks = await store.ftsSearchChunks(query, topK * 2);
1654
+ const results = [];
1655
+ for (const chunk of chunks) {
1656
+ const result = await store.buildSearchResult(chunk.id, 0);
1657
+ if (result !== void 0) results.push(result);
1658
+ if (results.length >= topK) break;
1659
+ }
1660
+ return results;
1661
+ }
1662
+ /** Direct chunk vector search fallback. */
1663
+ async function chunkVectorFallback(store, queryVec, topK) {
1664
+ const matches = await store.searchChunksByVector(queryVec, { limit: topK * 2 });
1665
+ const results = [];
1666
+ for (const { chunk, score } of matches) {
1667
+ const result = await store.buildSearchResult(chunk.id, score);
1668
+ if (result !== void 0) results.push(result);
1669
+ if (results.length >= topK) break;
1670
+ }
1671
+ return results;
1672
+ }
1673
+ //#endregion
1674
+ export { parse as C, normalize as S, resolve as T, KnowledgeStore as _, ENTITY_TYPES as a, isAbsolute as b, extractEventFromChunk as c, parseExtractionResponse as d, rerankEventsWithLlm as f, stripMarkdown as g, estimateTokens as h, isSupportedFile as i, extractJsonFromText as l, chunkText as m, ingestDirectory as n, EXTRACTION_SYSTEM_PROMPT as o, chunkMarkdown as p, ingestFile as r, buildExtractionUserPrompt as s, multiSearch as t, extractQueryEntities as u, basename as v, relative as w, join as x, dirname as y };