@veewo/claw-core 0.1.21 → 0.1.23
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/dist/src/context.js +56 -0
- package/dist/src/context.js.map +1 -1
- package/dist/src/embedding-local.d.ts +32 -0
- package/dist/src/embedding-local.js +75 -0
- package/dist/src/embedding-local.js.map +1 -0
- package/dist/src/embedding-worker.d.ts +1 -0
- package/dist/src/embedding-worker.js +160 -0
- package/dist/src/embedding-worker.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init.js +17 -1
- package/dist/src/init.js.map +1 -1
- package/dist/src/memory-query.d.ts +14 -0
- package/dist/src/memory-query.js +361 -0
- package/dist/src/memory-query.js.map +1 -0
- package/dist/src/memory.js +762 -29
- package/dist/src/memory.js.map +1 -1
- package/dist/src/plan.js +61 -11
- package/dist/src/plan.js.map +1 -1
- package/dist/src/project-check.js +175 -1
- package/dist/src/project-check.js.map +1 -1
- package/dist/src/text-encoding.d.ts +3 -0
- package/dist/src/text-encoding.js +14 -0
- package/dist/src/text-encoding.js.map +1 -0
- package/dist/src/truth.js +2 -1
- package/dist/src/truth.js.map +1 -1
- package/dist/src/types.d.ts +32 -0
- package/package.json +4 -1
package/dist/src/memory.js
CHANGED
|
@@ -1,36 +1,201 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
3
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
4
8
|
import { resolveProjectContext, resolveTaskContext } from "./context.js";
|
|
5
9
|
import { ClawError } from "./errors.js";
|
|
6
10
|
import { readJsonFile, readTextFile } from "./io.js";
|
|
11
|
+
import { buildProjectKeywordSearchPlan, buildProjectQueryIntent } from "./memory-query.js";
|
|
12
|
+
const DEFAULT_PROJECT_REFRESH_FILE_LIMIT = 100;
|
|
13
|
+
const PROJECT_SEARCH_CANDIDATE_MULTIPLIER = 8;
|
|
7
14
|
export function buildMemoryIndex(input) {
|
|
8
15
|
const { scope, project, task } = resolveMemoryScope(input);
|
|
9
16
|
const storePath = getMemoryStorePath(project, scope, task);
|
|
10
17
|
const sources = collectMemorySources(project, scope, task);
|
|
18
|
+
const embedding = scope === "project" ? resolveProjectMemoryEmbeddingConfig(project) : undefined;
|
|
11
19
|
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
12
20
|
const db = new DatabaseSync(storePath);
|
|
13
21
|
try {
|
|
14
22
|
prepareSchema(db);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const syncResult = scope === "project"
|
|
24
|
+
? syncProjectMemoryIndex(db, sources, embedding ?? null, input.maxFiles ?? DEFAULT_PROJECT_REFRESH_FILE_LIMIT)
|
|
25
|
+
: {
|
|
26
|
+
vectorIndex: rebuildTaskMemoryIndex(db, sources),
|
|
27
|
+
processedFileCount: sources.length,
|
|
28
|
+
pendingFileCount: 0,
|
|
29
|
+
};
|
|
30
|
+
upsertMetadata(db, "scope", scope);
|
|
31
|
+
upsertMetadata(db, "indexed_at", new Date().toISOString());
|
|
32
|
+
if (embedding) {
|
|
33
|
+
upsertMetadata(db, "embedding_config", JSON.stringify(embedding));
|
|
22
34
|
}
|
|
35
|
+
else {
|
|
36
|
+
deleteMetadata(db, "embedding_config");
|
|
37
|
+
}
|
|
38
|
+
if (syncResult.vectorIndex) {
|
|
39
|
+
upsertMetadata(db, "vector_index", JSON.stringify(syncResult.vectorIndex));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
deleteMetadata(db, "vector_index");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
scope,
|
|
46
|
+
storePath,
|
|
47
|
+
indexedCount: sources.length,
|
|
48
|
+
processedFileCount: syncResult.processedFileCount,
|
|
49
|
+
pendingFileCount: syncResult.pendingFileCount,
|
|
50
|
+
sources: sources.map((entry) => entry.sourcePath),
|
|
51
|
+
...(scope === "project" ? { embedding, vectorIndex: syncResult.vectorIndex } : {}),
|
|
52
|
+
};
|
|
23
53
|
}
|
|
24
54
|
finally {
|
|
25
55
|
db.close();
|
|
26
56
|
}
|
|
57
|
+
}
|
|
58
|
+
function rebuildTaskMemoryIndex(db, sources) {
|
|
59
|
+
db.exec("DELETE FROM docs;");
|
|
60
|
+
db.exec("DELETE FROM docs_fts;");
|
|
61
|
+
db.exec("DELETE FROM doc_embeddings;");
|
|
62
|
+
const insertDoc = db.prepare("INSERT INTO docs (source_path, kind, content, content_hash) VALUES (?, ?, ?, ?)");
|
|
63
|
+
const insertFts = db.prepare("INSERT INTO docs_fts (rowid, source_path, kind, content) VALUES (?, ?, ?, ?)");
|
|
64
|
+
for (const source of sources) {
|
|
65
|
+
const result = insertDoc.run(source.sourcePath, source.kind, source.content, hashMemoryContent(source.content));
|
|
66
|
+
insertFts.run(Number(result.lastInsertRowid), source.sourcePath, source.kind, source.content);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function syncProjectMemoryIndex(db, sources, embedding, maxFiles) {
|
|
71
|
+
const currentEmbeddingConfig = embedding ? JSON.stringify(embedding) : null;
|
|
72
|
+
const storedEmbeddingConfig = getMetadata(db, "embedding_config");
|
|
73
|
+
const shouldIndexVectors = canBuildProjectVectors(embedding);
|
|
74
|
+
const requiresVectorReset = storedEmbeddingConfig !== currentEmbeddingConfig || !shouldIndexVectors;
|
|
75
|
+
const nextSources = sources.map((source) => ({
|
|
76
|
+
...source,
|
|
77
|
+
contentHash: hashMemoryContent(source.content),
|
|
78
|
+
}));
|
|
79
|
+
const nextByPath = new Map(nextSources.map((source) => [source.sourcePath, source]));
|
|
80
|
+
const existingDocs = db
|
|
81
|
+
.prepare("SELECT id, source_path, kind, content_hash FROM docs")
|
|
82
|
+
.all();
|
|
83
|
+
const existingByPath = new Map(existingDocs.map((doc) => [doc.source_path, doc]));
|
|
84
|
+
const docsToDelete = existingDocs.filter((doc) => {
|
|
85
|
+
const next = nextByPath.get(doc.source_path);
|
|
86
|
+
if (!next) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (requiresVectorReset) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return doc.kind !== next.kind || doc.content_hash !== next.contentHash;
|
|
93
|
+
});
|
|
94
|
+
const docsToInsert = nextSources.filter((source) => {
|
|
95
|
+
const existing = existingByPath.get(source.sourcePath);
|
|
96
|
+
if (!existing) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (requiresVectorReset) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return existing.kind !== source.kind || existing.content_hash !== source.contentHash;
|
|
103
|
+
});
|
|
104
|
+
const canLimitFiles = !maxFiles
|
|
105
|
+
? false
|
|
106
|
+
: maxFiles > 0 && (!requiresVectorReset || existingDocs.length === 0);
|
|
107
|
+
const sortedDocsToInsert = [...docsToInsert].sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
|
108
|
+
const limitedDocsToInsert = canLimitFiles ? sortedDocsToInsert.slice(0, maxFiles) : sortedDocsToInsert;
|
|
109
|
+
const pendingFileCount = sortedDocsToInsert.length - limitedDocsToInsert.length;
|
|
110
|
+
db.exec("BEGIN");
|
|
111
|
+
try {
|
|
112
|
+
deleteDocsById(db, docsToDelete.map((doc) => doc.id));
|
|
113
|
+
if (requiresVectorReset) {
|
|
114
|
+
db.exec("DELETE FROM doc_embeddings;");
|
|
115
|
+
}
|
|
116
|
+
const indexedDocs = insertDocs(db, limitedDocsToInsert);
|
|
117
|
+
let vectorIndexingFailed = false;
|
|
118
|
+
if (shouldIndexVectors && embedding) {
|
|
119
|
+
try {
|
|
120
|
+
indexDocEmbeddings(db, indexedDocs, embedding);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (!isEmbeddingGenerationFailure(error)) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
vectorIndexingFailed = true;
|
|
127
|
+
db.exec("DELETE FROM doc_embeddings;");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
db.exec("COMMIT");
|
|
131
|
+
if (vectorIndexingFailed) {
|
|
132
|
+
return {
|
|
133
|
+
vectorIndex: null,
|
|
134
|
+
processedFileCount: limitedDocsToInsert.length,
|
|
135
|
+
pendingFileCount,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
db.exec("ROLLBACK");
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (!shouldIndexVectors || !embedding) {
|
|
144
|
+
return {
|
|
145
|
+
vectorIndex: null,
|
|
146
|
+
processedFileCount: limitedDocsToInsert.length,
|
|
147
|
+
pendingFileCount,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
27
150
|
return {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
sources: sources.map((entry) => entry.sourcePath),
|
|
151
|
+
vectorIndex: summarizeVectorIndex(db, embedding),
|
|
152
|
+
processedFileCount: limitedDocsToInsert.length,
|
|
153
|
+
pendingFileCount,
|
|
32
154
|
};
|
|
33
155
|
}
|
|
156
|
+
function insertDocs(db, sources) {
|
|
157
|
+
const insertDoc = db.prepare("INSERT INTO docs (source_path, kind, content, content_hash) VALUES (?, ?, ?, ?)");
|
|
158
|
+
const insertFts = db.prepare("INSERT INTO docs_fts (rowid, source_path, kind, content) VALUES (?, ?, ?, ?)");
|
|
159
|
+
const indexedDocs = [];
|
|
160
|
+
for (const source of sources) {
|
|
161
|
+
const result = insertDoc.run(source.sourcePath, source.kind, source.content, source.contentHash);
|
|
162
|
+
const docId = Number(result.lastInsertRowid);
|
|
163
|
+
insertFts.run(docId, source.sourcePath, source.kind, source.content);
|
|
164
|
+
indexedDocs.push({
|
|
165
|
+
docId,
|
|
166
|
+
sourcePath: source.sourcePath,
|
|
167
|
+
kind: source.kind,
|
|
168
|
+
content: source.content,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return indexedDocs;
|
|
172
|
+
}
|
|
173
|
+
function deleteDocsById(db, docIds) {
|
|
174
|
+
if (docIds.length === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const deleteFts = db.prepare("DELETE FROM docs_fts WHERE rowid = ?");
|
|
178
|
+
const deleteEmbeddings = db.prepare("DELETE FROM doc_embeddings WHERE doc_id = ?");
|
|
179
|
+
const deleteDoc = db.prepare("DELETE FROM docs WHERE id = ?");
|
|
180
|
+
for (const docId of docIds) {
|
|
181
|
+
deleteFts.run(docId);
|
|
182
|
+
deleteEmbeddings.run(docId);
|
|
183
|
+
deleteDoc.run(docId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function upsertMetadata(db, key, value) {
|
|
187
|
+
db.prepare([
|
|
188
|
+
"INSERT INTO index_metadata (key, value) VALUES (?, ?)",
|
|
189
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
190
|
+
].join(" ")).run(key, value);
|
|
191
|
+
}
|
|
192
|
+
function deleteMetadata(db, key) {
|
|
193
|
+
db.prepare("DELETE FROM index_metadata WHERE key = ?").run(key);
|
|
194
|
+
}
|
|
195
|
+
function getMetadata(db, key) {
|
|
196
|
+
const row = db.prepare("SELECT value FROM index_metadata WHERE key = ?").get(key);
|
|
197
|
+
return row?.value ?? null;
|
|
198
|
+
}
|
|
34
199
|
export function searchMemory(input) {
|
|
35
200
|
if (!input.query.trim()) {
|
|
36
201
|
throw new ClawError("MEMORY_QUERY_REQUIRED", "memory search requires a non-empty query.");
|
|
@@ -47,22 +212,9 @@ export function searchMemory(input) {
|
|
|
47
212
|
const db = new DatabaseSync(storePath);
|
|
48
213
|
try {
|
|
49
214
|
prepareSchema(db);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
"SELECT source_path, kind, snippet(docs_fts, 2, '[', ']', ' ... ', 18) AS snippet, bm25(docs_fts) AS score",
|
|
54
|
-
"FROM docs_fts",
|
|
55
|
-
"WHERE docs_fts MATCH ?",
|
|
56
|
-
"ORDER BY score ASC",
|
|
57
|
-
"LIMIT ?",
|
|
58
|
-
].join(" "))
|
|
59
|
-
.all(input.query, limit);
|
|
60
|
-
const results = rows.map((row) => ({
|
|
61
|
-
sourcePath: row.source_path,
|
|
62
|
-
kind: row.kind,
|
|
63
|
-
snippet: row.snippet,
|
|
64
|
-
score: row.score,
|
|
65
|
-
}));
|
|
215
|
+
const results = scope === "project"
|
|
216
|
+
? searchProjectMemoryHybrid(db, input.query, input.limit ?? 10, project)
|
|
217
|
+
: searchTaskMemoryFts(db, input.query, input.limit ?? 10);
|
|
66
218
|
return {
|
|
67
219
|
scope,
|
|
68
220
|
storePath,
|
|
@@ -204,14 +356,53 @@ function prepareSchema(db) {
|
|
|
204
356
|
" id INTEGER PRIMARY KEY,",
|
|
205
357
|
" source_path TEXT NOT NULL,",
|
|
206
358
|
" kind TEXT NOT NULL,",
|
|
207
|
-
" content TEXT NOT NULL",
|
|
359
|
+
" content TEXT NOT NULL,",
|
|
360
|
+
" content_hash TEXT",
|
|
208
361
|
");",
|
|
209
362
|
"CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(",
|
|
210
363
|
" source_path,",
|
|
211
364
|
" kind,",
|
|
212
365
|
" content",
|
|
213
366
|
");",
|
|
367
|
+
"CREATE TABLE IF NOT EXISTS index_metadata (",
|
|
368
|
+
" key TEXT PRIMARY KEY,",
|
|
369
|
+
" value TEXT NOT NULL",
|
|
370
|
+
");",
|
|
371
|
+
"CREATE TABLE IF NOT EXISTS doc_embeddings (",
|
|
372
|
+
" doc_id INTEGER NOT NULL,",
|
|
373
|
+
" chunk_index INTEGER NOT NULL,",
|
|
374
|
+
" source_path TEXT NOT NULL,",
|
|
375
|
+
" kind TEXT NOT NULL,",
|
|
376
|
+
" chunk_text TEXT NOT NULL,",
|
|
377
|
+
" embedding_json TEXT NOT NULL,",
|
|
378
|
+
" PRIMARY KEY (doc_id, chunk_index)",
|
|
379
|
+
");",
|
|
214
380
|
].join("\n"));
|
|
381
|
+
const docsColumns = db.prepare("PRAGMA table_info(docs)").all();
|
|
382
|
+
if (!docsColumns.some((column) => column.name === "content_hash")) {
|
|
383
|
+
db.exec("ALTER TABLE docs ADD COLUMN content_hash TEXT;");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function resolveProjectMemoryEmbeddingConfig(project) {
|
|
387
|
+
const configured = project.projectConfig?.memory?.embedding;
|
|
388
|
+
if (!configured) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
provider: configured.provider,
|
|
393
|
+
model: configured.model,
|
|
394
|
+
...(configured.remote ? { remote: configured.remote } : {}),
|
|
395
|
+
...(configured.local ? { local: configured.local } : {}),
|
|
396
|
+
...(configured.outputDimensionality ? { outputDimensionality: configured.outputDimensionality } : {}),
|
|
397
|
+
store: {
|
|
398
|
+
vector: {
|
|
399
|
+
enabled: configured.store?.vector?.enabled ?? true,
|
|
400
|
+
...(configured.store?.vector?.extensionPath
|
|
401
|
+
? { extensionPath: configured.store.vector.extensionPath }
|
|
402
|
+
: {}),
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
};
|
|
215
406
|
}
|
|
216
407
|
function listFiles(rootDir, matcher) {
|
|
217
408
|
if (!fs.existsSync(rootDir)) {
|
|
@@ -235,6 +426,548 @@ function listFiles(rootDir, matcher) {
|
|
|
235
426
|
return entries;
|
|
236
427
|
}
|
|
237
428
|
function isExternalDocFile(filePath) {
|
|
238
|
-
return /\.
|
|
429
|
+
return /\.md$/i.test(filePath);
|
|
430
|
+
}
|
|
431
|
+
function indexDocEmbeddings(db, docs, embedding) {
|
|
432
|
+
if (!embedding || embedding.store?.vector?.enabled === false) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!canBuildProjectVectors(embedding)) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const chunks = docs.flatMap((doc) => chunkMarkdownContent(doc.content).map((chunkText, chunkIndex) => ({
|
|
439
|
+
docId: doc.docId,
|
|
440
|
+
chunkIndex,
|
|
441
|
+
sourcePath: doc.sourcePath,
|
|
442
|
+
kind: doc.kind,
|
|
443
|
+
chunkText,
|
|
444
|
+
})));
|
|
445
|
+
if (chunks.length === 0) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const output = runEmbeddingWorker({
|
|
449
|
+
embedding,
|
|
450
|
+
texts: chunks.map((chunk) => chunk.chunkText),
|
|
451
|
+
});
|
|
452
|
+
const insertEmbedding = db.prepare([
|
|
453
|
+
"INSERT INTO doc_embeddings (doc_id, chunk_index, source_path, kind, chunk_text, embedding_json)",
|
|
454
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
455
|
+
].join(" "));
|
|
456
|
+
chunks.forEach((chunk, index) => {
|
|
457
|
+
insertEmbedding.run(chunk.docId, chunk.chunkIndex, chunk.sourcePath, chunk.kind, chunk.chunkText, JSON.stringify(output.vectors[index] ?? []));
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function summarizeVectorIndex(db, embedding) {
|
|
461
|
+
const vectorCount = db
|
|
462
|
+
.prepare("SELECT COUNT(*) AS count FROM doc_embeddings")
|
|
463
|
+
.get();
|
|
464
|
+
const firstVector = db
|
|
465
|
+
.prepare("SELECT embedding_json FROM doc_embeddings ORDER BY doc_id ASC, chunk_index ASC LIMIT 1")
|
|
466
|
+
.get();
|
|
467
|
+
const dimensions = firstVector
|
|
468
|
+
? parseEmbeddingJson(firstVector.embedding_json).length
|
|
469
|
+
: resolveEmbeddingDimensions(embedding, 0);
|
|
470
|
+
return {
|
|
471
|
+
enabled: true,
|
|
472
|
+
provider: embedding.provider,
|
|
473
|
+
model: embedding.model,
|
|
474
|
+
dimensions,
|
|
475
|
+
chunkCount: vectorCount.count,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function canBuildProjectVectors(embedding) {
|
|
479
|
+
if (!embedding || embedding.store?.vector?.enabled === false) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (embedding.provider === "openai" && !resolveEmbeddingApiKey(embedding)) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
function hashMemoryContent(content) {
|
|
488
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
489
|
+
}
|
|
490
|
+
function chunkMarkdownContent(content) {
|
|
491
|
+
return content
|
|
492
|
+
.split(/\r?\n\s*\r?\n/g)
|
|
493
|
+
.map((chunk) => chunk.trim())
|
|
494
|
+
.filter((chunk) => chunk.length > 0);
|
|
495
|
+
}
|
|
496
|
+
function runEmbeddingWorker(input) {
|
|
497
|
+
const workerPath = fileURLToPath(new URL("./embedding-worker.js", import.meta.url));
|
|
498
|
+
const outputPath = path.join(os.tmpdir(), `claw-embedding-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`);
|
|
499
|
+
const result = spawnSync(process.execPath, [workerPath], {
|
|
500
|
+
input: JSON.stringify({
|
|
501
|
+
...input,
|
|
502
|
+
outputPath,
|
|
503
|
+
}),
|
|
504
|
+
encoding: "utf-8",
|
|
505
|
+
env: process.env,
|
|
506
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
507
|
+
});
|
|
508
|
+
if (result.status !== 0) {
|
|
509
|
+
cleanupTemporaryEmbeddingOutput(outputPath);
|
|
510
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "Memory embedding generation failed.", {
|
|
511
|
+
stdout: result.stdout,
|
|
512
|
+
stderr: result.stderr,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const payload = JSON.parse(stripBom(fs.readFileSync(outputPath, "utf-8")));
|
|
517
|
+
return payload;
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
cleanupTemporaryEmbeddingOutput(outputPath);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function stripBom(content) {
|
|
524
|
+
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
525
|
+
}
|
|
526
|
+
function cleanupTemporaryEmbeddingOutput(outputPath) {
|
|
527
|
+
if (fs.existsSync(outputPath)) {
|
|
528
|
+
fs.unlinkSync(outputPath);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function isEmbeddingGenerationFailure(error) {
|
|
532
|
+
return error instanceof ClawError &&
|
|
533
|
+
error.code === "PROJECT_CONFIG_INVALID" &&
|
|
534
|
+
error.message === "Memory embedding generation failed.";
|
|
535
|
+
}
|
|
536
|
+
function resolveEmbeddingApiKey(embedding) {
|
|
537
|
+
const envVar = embedding.remote?.apiKeyEnvVar?.trim();
|
|
538
|
+
if (!envVar) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const value = process.env[envVar];
|
|
542
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
543
|
+
}
|
|
544
|
+
function resolveEmbeddingDimensions(embedding, fallback) {
|
|
545
|
+
if (typeof embedding.outputDimensionality === "number" && embedding.outputDimensionality > 0) {
|
|
546
|
+
return embedding.outputDimensionality;
|
|
547
|
+
}
|
|
548
|
+
if (embedding.provider === "local") {
|
|
549
|
+
return 384;
|
|
550
|
+
}
|
|
551
|
+
return fallback > 0 ? fallback : 1536;
|
|
552
|
+
}
|
|
553
|
+
function searchTaskMemoryFts(db, query, limit) {
|
|
554
|
+
const rows = db
|
|
555
|
+
.prepare([
|
|
556
|
+
"SELECT source_path, kind, snippet(docs_fts, 2, '[', ']', ' ... ', 18) AS snippet, bm25(docs_fts) AS score",
|
|
557
|
+
"FROM docs_fts",
|
|
558
|
+
"WHERE docs_fts MATCH ?",
|
|
559
|
+
"ORDER BY score ASC",
|
|
560
|
+
"LIMIT ?",
|
|
561
|
+
].join(" "))
|
|
562
|
+
.all(query, limit);
|
|
563
|
+
return rows.map((row) => ({
|
|
564
|
+
sourcePath: row.source_path,
|
|
565
|
+
kind: row.kind,
|
|
566
|
+
snippet: row.snippet,
|
|
567
|
+
score: row.score,
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
function searchProjectMemoryHybrid(db, query, limit, project) {
|
|
571
|
+
const embedding = resolveProjectMemoryEmbeddingConfig(project);
|
|
572
|
+
if (!embedding || embedding.store?.vector?.enabled === false) {
|
|
573
|
+
throw new ClawError("MEMORY_VECTOR_INDEX_REQUIRED", "Project search requires memory.embedding with vector indexing enabled. Configure .claw/project.json and run `claw search index --refresh` first.");
|
|
574
|
+
}
|
|
575
|
+
const vectorIndexMetadata = db
|
|
576
|
+
.prepare("SELECT value FROM index_metadata WHERE key = ?")
|
|
577
|
+
.get("vector_index");
|
|
578
|
+
if (!vectorIndexMetadata) {
|
|
579
|
+
throw new ClawError("MEMORY_VECTOR_INDEX_REQUIRED", "Project search requires a refreshed vector index. Run `claw search index --refresh` first.");
|
|
580
|
+
}
|
|
581
|
+
const queryIntent = buildProjectQueryIntent(query);
|
|
582
|
+
const queryEmbedding = runEmbeddingWorker({
|
|
583
|
+
embedding,
|
|
584
|
+
texts: [queryIntent.embeddingText || query],
|
|
585
|
+
}).vectors[0];
|
|
586
|
+
if (!queryEmbedding?.length) {
|
|
587
|
+
throw new ClawError("MEMORY_VECTOR_INDEX_REQUIRED", "Unable to generate a query embedding for project search.");
|
|
588
|
+
}
|
|
589
|
+
const candidateLimit = Math.max(limit * PROJECT_SEARCH_CANDIDATE_MULTIPLIER, 40);
|
|
590
|
+
const projectDocs = db
|
|
591
|
+
.prepare("SELECT source_path, kind, content FROM docs")
|
|
592
|
+
.all();
|
|
593
|
+
const docSignals = new Map(projectDocs.map((row) => [
|
|
594
|
+
row.source_path,
|
|
595
|
+
buildProjectSearchSignals({
|
|
596
|
+
sourcePath: row.source_path,
|
|
597
|
+
content: row.content,
|
|
598
|
+
query,
|
|
599
|
+
queryIntent,
|
|
600
|
+
}),
|
|
601
|
+
]));
|
|
602
|
+
const ftsRows = searchProjectMemoryKeywords(db, query, candidateLimit);
|
|
603
|
+
const signalRows = searchProjectMemorySignals(projectDocs, docSignals, candidateLimit);
|
|
604
|
+
const vectorRows = db
|
|
605
|
+
.prepare([
|
|
606
|
+
"SELECT source_path, kind, chunk_text, embedding_json",
|
|
607
|
+
"FROM doc_embeddings",
|
|
608
|
+
].join(" "))
|
|
609
|
+
.all();
|
|
610
|
+
if (vectorRows.length === 0) {
|
|
611
|
+
throw new ClawError("MEMORY_VECTOR_INDEX_REQUIRED", "Project search requires stored vectors. Run `claw search index --refresh` first.");
|
|
612
|
+
}
|
|
613
|
+
const rankedVectors = vectorRows
|
|
614
|
+
.map((row) => {
|
|
615
|
+
const signals = docSignals.get(row.source_path);
|
|
616
|
+
return {
|
|
617
|
+
sourcePath: row.source_path,
|
|
618
|
+
kind: row.kind,
|
|
619
|
+
snippet: buildSnippet(row.chunk_text),
|
|
620
|
+
similarity: cosineSimilarity(queryEmbedding, parseEmbeddingJson(row.embedding_json)),
|
|
621
|
+
exactBoost: signals?.exactBoost ?? 0,
|
|
622
|
+
};
|
|
623
|
+
})
|
|
624
|
+
.filter((row) => Number.isFinite(row.similarity))
|
|
625
|
+
.sort((left, right) => {
|
|
626
|
+
const leftScore = left.similarity + left.exactBoost;
|
|
627
|
+
const rightScore = right.similarity + right.exactBoost;
|
|
628
|
+
return rightScore - leftScore;
|
|
629
|
+
});
|
|
630
|
+
const bestVectorBySource = new Map();
|
|
631
|
+
for (const row of rankedVectors) {
|
|
632
|
+
const existing = bestVectorBySource.get(row.sourcePath);
|
|
633
|
+
if (!existing || row.similarity > existing.similarity) {
|
|
634
|
+
bestVectorBySource.set(row.sourcePath, {
|
|
635
|
+
kind: row.kind,
|
|
636
|
+
snippet: row.snippet,
|
|
637
|
+
similarity: row.similarity,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const fused = new Map();
|
|
642
|
+
Array.from(bestVectorBySource.entries())
|
|
643
|
+
.sort((left, right) => {
|
|
644
|
+
const leftScore = left[1].similarity + (docSignals.get(left[0])?.exactBoost ?? 0);
|
|
645
|
+
const rightScore = right[1].similarity + (docSignals.get(right[0])?.exactBoost ?? 0);
|
|
646
|
+
return rightScore - leftScore;
|
|
647
|
+
})
|
|
648
|
+
.slice(0, candidateLimit)
|
|
649
|
+
.forEach(([sourcePath, row], index) => {
|
|
650
|
+
const signals = docSignals.get(sourcePath);
|
|
651
|
+
fused.set(sourcePath, {
|
|
652
|
+
sourcePath,
|
|
653
|
+
kind: row.kind,
|
|
654
|
+
snippet: row.snippet,
|
|
655
|
+
score: reciprocalRankScore(index + 1, 0.6) + (signals?.exactBoost ?? 0),
|
|
656
|
+
vectorRank: index + 1,
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
ftsRows.forEach((row, index) => {
|
|
660
|
+
const existing = fused.get(row.source_path);
|
|
661
|
+
const signals = docSignals.get(row.source_path);
|
|
662
|
+
const nextScore = (existing?.score ?? 0) + reciprocalRankScore(index + 1, 0.25);
|
|
663
|
+
fused.set(row.source_path, {
|
|
664
|
+
sourcePath: row.source_path,
|
|
665
|
+
kind: existing?.kind ?? row.kind,
|
|
666
|
+
snippet: row.snippet || existing?.snippet || "",
|
|
667
|
+
score: nextScore + (existing ? 0 : (signals?.exactBoost ?? 0)),
|
|
668
|
+
vectorRank: existing?.vectorRank,
|
|
669
|
+
textRank: index + 1,
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
signalRows.forEach((row, index) => {
|
|
673
|
+
const existing = fused.get(row.source_path);
|
|
674
|
+
const signals = docSignals.get(row.source_path);
|
|
675
|
+
const nextScore = (existing?.score ?? 0) + reciprocalRankScore(index + 1, 0.15);
|
|
676
|
+
fused.set(row.source_path, {
|
|
677
|
+
sourcePath: row.source_path,
|
|
678
|
+
kind: existing?.kind ?? row.kind,
|
|
679
|
+
snippet: existing?.snippet || row.snippet || "",
|
|
680
|
+
score: nextScore + (existing ? 0 : (signals?.exactBoost ?? 0)),
|
|
681
|
+
vectorRank: existing?.vectorRank,
|
|
682
|
+
textRank: existing?.textRank,
|
|
683
|
+
signalRank: index + 1,
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
return rerankProjectSearchCandidates(Array.from(fused.values()), docSignals, limit);
|
|
687
|
+
}
|
|
688
|
+
function searchProjectMemoryKeywords(db, query, limit) {
|
|
689
|
+
const plan = buildProjectKeywordSearchPlan(query);
|
|
690
|
+
if (plan.length === 0) {
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
const searchFts = db.prepare([
|
|
694
|
+
"SELECT docs.source_path, docs.kind, snippet(docs_fts, 2, '[', ']', ' ... ', 18) AS snippet, bm25(docs_fts) AS score",
|
|
695
|
+
"FROM docs_fts",
|
|
696
|
+
"JOIN docs ON docs.id = docs_fts.rowid",
|
|
697
|
+
"WHERE docs_fts MATCH ?",
|
|
698
|
+
"ORDER BY score ASC",
|
|
699
|
+
"LIMIT ?",
|
|
700
|
+
].join(" "));
|
|
701
|
+
const bySource = new Map();
|
|
702
|
+
for (const step of plan) {
|
|
703
|
+
const rows = collectProjectKeywordRows(db, searchFts, step, limit);
|
|
704
|
+
for (const row of rows) {
|
|
705
|
+
const existing = bySource.get(row.source_path);
|
|
706
|
+
if (!existing) {
|
|
707
|
+
bySource.set(row.source_path, {
|
|
708
|
+
...row,
|
|
709
|
+
matchedTerms: new Set(step.matchedTerms),
|
|
710
|
+
exactMatches: step.matchedTerms.length > 1 ? 1 : 0,
|
|
711
|
+
});
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (row.score < existing.score) {
|
|
715
|
+
existing.score = row.score;
|
|
716
|
+
}
|
|
717
|
+
if (row.snippet && row.snippet.length > existing.snippet.length) {
|
|
718
|
+
existing.snippet = row.snippet;
|
|
719
|
+
}
|
|
720
|
+
if (step.matchedTerms.length > 1) {
|
|
721
|
+
existing.exactMatches += 1;
|
|
722
|
+
}
|
|
723
|
+
for (const term of step.matchedTerms) {
|
|
724
|
+
existing.matchedTerms.add(term);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return Array.from(bySource.values())
|
|
729
|
+
.sort((left, right) => {
|
|
730
|
+
if (right.matchedTerms.size !== left.matchedTerms.size) {
|
|
731
|
+
return right.matchedTerms.size - left.matchedTerms.size;
|
|
732
|
+
}
|
|
733
|
+
if (right.exactMatches !== left.exactMatches) {
|
|
734
|
+
return right.exactMatches - left.exactMatches;
|
|
735
|
+
}
|
|
736
|
+
return left.score - right.score;
|
|
737
|
+
})
|
|
738
|
+
.slice(0, limit)
|
|
739
|
+
.map(({ source_path, kind, snippet, score }) => ({
|
|
740
|
+
source_path,
|
|
741
|
+
kind,
|
|
742
|
+
snippet,
|
|
743
|
+
score,
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
function collectProjectKeywordRows(db, searchFts, step, limit) {
|
|
747
|
+
const rows = [];
|
|
748
|
+
const seen = new Set();
|
|
749
|
+
if (step.query) {
|
|
750
|
+
const ftsRows = searchFts.all(step.query, limit);
|
|
751
|
+
for (const row of ftsRows) {
|
|
752
|
+
if (matchesAllSubstrings(db, row.source_path, step.substringTerms)) {
|
|
753
|
+
rows.push(row);
|
|
754
|
+
seen.add(row.source_path);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (step.substringTerms.length > 0) {
|
|
759
|
+
const substringRows = searchDocsBySubstring(db, step.substringTerms, limit);
|
|
760
|
+
for (const row of substringRows) {
|
|
761
|
+
if (!seen.has(row.source_path)) {
|
|
762
|
+
rows.push(row);
|
|
763
|
+
seen.add(row.source_path);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return rows;
|
|
768
|
+
}
|
|
769
|
+
function searchDocsBySubstring(db, substringTerms, limit) {
|
|
770
|
+
const clauses = substringTerms.map(() => "content LIKE ? ESCAPE '\\'").join(" AND ");
|
|
771
|
+
const query = [
|
|
772
|
+
"SELECT source_path, kind, content",
|
|
773
|
+
"FROM docs",
|
|
774
|
+
`WHERE ${clauses}`,
|
|
775
|
+
"LIMIT ?",
|
|
776
|
+
].join(" ");
|
|
777
|
+
const dynamicRows = db
|
|
778
|
+
.prepare(query)
|
|
779
|
+
.all(...substringTerms.map((term) => `%${escapeLikePattern(term)}%`), limit);
|
|
780
|
+
return dynamicRows.map((row) => ({
|
|
781
|
+
source_path: row.source_path,
|
|
782
|
+
kind: row.kind,
|
|
783
|
+
snippet: buildSnippet(row.content),
|
|
784
|
+
score: 0,
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
787
|
+
function searchProjectMemorySignals(projectDocs, docSignals, limit) {
|
|
788
|
+
return projectDocs
|
|
789
|
+
.filter((row) => (docSignals.get(row.source_path)?.matchedTermCount ?? 0) > 0)
|
|
790
|
+
.sort((left, right) => {
|
|
791
|
+
const leftSignals = docSignals.get(left.source_path);
|
|
792
|
+
const rightSignals = docSignals.get(right.source_path);
|
|
793
|
+
if ((rightSignals?.strongMatchedTermCount ?? 0) !== (leftSignals?.strongMatchedTermCount ?? 0)) {
|
|
794
|
+
return (rightSignals?.strongMatchedTermCount ?? 0) - (leftSignals?.strongMatchedTermCount ?? 0);
|
|
795
|
+
}
|
|
796
|
+
if ((rightSignals?.matchedTermCount ?? 0) !== (leftSignals?.matchedTermCount ?? 0)) {
|
|
797
|
+
return (rightSignals?.matchedTermCount ?? 0) - (leftSignals?.matchedTermCount ?? 0);
|
|
798
|
+
}
|
|
799
|
+
if ((rightSignals?.exactBoost ?? 0) !== (leftSignals?.exactBoost ?? 0)) {
|
|
800
|
+
return (rightSignals?.exactBoost ?? 0) - (leftSignals?.exactBoost ?? 0);
|
|
801
|
+
}
|
|
802
|
+
if ((rightSignals?.fileNameHits ?? 0) !== (leftSignals?.fileNameHits ?? 0)) {
|
|
803
|
+
return (rightSignals?.fileNameHits ?? 0) - (leftSignals?.fileNameHits ?? 0);
|
|
804
|
+
}
|
|
805
|
+
return (rightSignals?.pathHits ?? 0) - (leftSignals?.pathHits ?? 0);
|
|
806
|
+
})
|
|
807
|
+
.slice(0, limit)
|
|
808
|
+
.map((row) => ({
|
|
809
|
+
source_path: row.source_path,
|
|
810
|
+
kind: row.kind,
|
|
811
|
+
snippet: buildSnippet(row.content),
|
|
812
|
+
score: 0,
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
function matchesAllSubstrings(db, sourcePath, substringTerms) {
|
|
816
|
+
if (substringTerms.length === 0) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
const row = db
|
|
820
|
+
.prepare("SELECT content FROM docs WHERE source_path = ?")
|
|
821
|
+
.get(sourcePath);
|
|
822
|
+
if (!row) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
return substringTerms.every((term) => row.content.includes(term));
|
|
826
|
+
}
|
|
827
|
+
function escapeLikePattern(term) {
|
|
828
|
+
return term.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
829
|
+
}
|
|
830
|
+
function rerankProjectSearchCandidates(candidates, docSignals, limit) {
|
|
831
|
+
const remaining = [...candidates];
|
|
832
|
+
const selected = [];
|
|
833
|
+
const coveredStrongTerms = new Set();
|
|
834
|
+
const coveredTerms = new Set();
|
|
835
|
+
while (remaining.length > 0 && selected.length < limit) {
|
|
836
|
+
let bestIndex = 0;
|
|
837
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
838
|
+
for (let index = 0; index < remaining.length; index += 1) {
|
|
839
|
+
const candidate = remaining[index];
|
|
840
|
+
const signals = docSignals.get(candidate.sourcePath);
|
|
841
|
+
const routeCount = (candidate.vectorRank ? 1 : 0) + (candidate.textRank ? 1 : 0) + (candidate.signalRank ? 1 : 0);
|
|
842
|
+
const uncoveredStrongTerms = (signals?.strongMatchedTerms ?? []).filter((term) => !coveredStrongTerms.has(term));
|
|
843
|
+
const uncoveredTerms = (signals?.matchedTerms ?? []).filter((term) => !coveredTerms.has(term));
|
|
844
|
+
const adjustedScore = candidate.score
|
|
845
|
+
+ uncoveredStrongTerms.length * 0.045
|
|
846
|
+
+ uncoveredTerms.length * 0.01
|
|
847
|
+
+ Math.max(routeCount - 1, 0) * 0.003;
|
|
848
|
+
if (adjustedScore > bestScore) {
|
|
849
|
+
bestScore = adjustedScore;
|
|
850
|
+
bestIndex = index;
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (adjustedScore === bestScore) {
|
|
854
|
+
const currentBest = remaining[bestIndex];
|
|
855
|
+
const currentSignals = docSignals.get(currentBest.sourcePath);
|
|
856
|
+
if ((signals?.strongMatchedTermCount ?? 0) !== (currentSignals?.strongMatchedTermCount ?? 0)) {
|
|
857
|
+
if ((signals?.strongMatchedTermCount ?? 0) > (currentSignals?.strongMatchedTermCount ?? 0)) {
|
|
858
|
+
bestIndex = index;
|
|
859
|
+
}
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if ((signals?.matchedTermCount ?? 0) !== (currentSignals?.matchedTermCount ?? 0)) {
|
|
863
|
+
if ((signals?.matchedTermCount ?? 0) > (currentSignals?.matchedTermCount ?? 0)) {
|
|
864
|
+
bestIndex = index;
|
|
865
|
+
}
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if ((signals?.exactBoost ?? 0) > (currentSignals?.exactBoost ?? 0)) {
|
|
869
|
+
bestIndex = index;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const [next] = remaining.splice(bestIndex, 1);
|
|
874
|
+
const nextSignals = docSignals.get(next.sourcePath);
|
|
875
|
+
nextSignals?.strongMatchedTerms.forEach((term) => coveredStrongTerms.add(term));
|
|
876
|
+
nextSignals?.matchedTerms.forEach((term) => coveredTerms.add(term));
|
|
877
|
+
selected.push({
|
|
878
|
+
sourcePath: next.sourcePath,
|
|
879
|
+
kind: next.kind,
|
|
880
|
+
snippet: next.snippet,
|
|
881
|
+
score: next.score,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return selected;
|
|
885
|
+
}
|
|
886
|
+
function buildProjectSearchSignals(input) {
|
|
887
|
+
const normalizedQuery = input.query.trim();
|
|
888
|
+
const normalizedContent = input.content.toLowerCase();
|
|
889
|
+
const normalizedPath = input.sourcePath.toLowerCase();
|
|
890
|
+
const fileName = path.basename(input.sourcePath).toLowerCase();
|
|
891
|
+
const lowerTerms = input.queryIntent.terms.map((term) => term.toLowerCase());
|
|
892
|
+
const lowerStrongTerms = input.queryIntent.strongTerms.map((term) => term.toLowerCase());
|
|
893
|
+
const lowerWeakTerms = input.queryIntent.weakTerms.map((term) => term.toLowerCase());
|
|
894
|
+
const matchedContentTerms = lowerTerms.filter((term) => normalizedContent.includes(term));
|
|
895
|
+
const matchedPathTerms = lowerTerms.filter((term) => normalizedPath.includes(term));
|
|
896
|
+
const matchedTerms = new Set([...matchedContentTerms, ...matchedPathTerms]);
|
|
897
|
+
const strongMatchedTerms = new Set(lowerStrongTerms.filter((term) => normalizedContent.includes(term) || normalizedPath.includes(term)));
|
|
898
|
+
const weakMatchedTerms = new Set(lowerWeakTerms.filter((term) => normalizedContent.includes(term) || normalizedPath.includes(term)));
|
|
899
|
+
const matchedCharacters = Array.from(matchedTerms).reduce((sum, term) => sum + term.length, 0);
|
|
900
|
+
const normalizedLength = Math.max(Array.from(input.content.trim()).length, 1);
|
|
901
|
+
const coverageRatio = lowerTerms.length > 0 ? matchedTerms.size / lowerTerms.length : 0;
|
|
902
|
+
const strongCoverageRatio = lowerStrongTerms.length > 0 ? strongMatchedTerms.size / lowerStrongTerms.length : 0;
|
|
903
|
+
const densityRatio = matchedCharacters / normalizedLength;
|
|
904
|
+
const fileNameHits = lowerTerms.filter((term) => fileName.includes(term)).length;
|
|
905
|
+
const pathHits = matchedPathTerms.length;
|
|
906
|
+
const phraseMatch = normalizedQuery.length > 0
|
|
907
|
+
&& (normalizedContent.includes(normalizedQuery.toLowerCase()) || normalizedPath.includes(normalizedQuery.toLowerCase()));
|
|
908
|
+
const weakOnlyPenalty = strongMatchedTerms.size === 0 && weakMatchedTerms.size > 0 ? 0.012 : 0;
|
|
909
|
+
const missingStrongPenalty = lowerStrongTerms.length > 0 && strongMatchedTerms.size === 0
|
|
910
|
+
? 0.035
|
|
911
|
+
: lowerStrongTerms.length > 1 && strongMatchedTerms.size === 1
|
|
912
|
+
? 0.01
|
|
913
|
+
: 0;
|
|
914
|
+
const indexFilePenalty = isIndexLikeDocName(fileName) ? 0.06 : 0;
|
|
915
|
+
return {
|
|
916
|
+
matchedTerms: Array.from(matchedTerms),
|
|
917
|
+
strongMatchedTerms: Array.from(strongMatchedTerms),
|
|
918
|
+
weakMatchedTerms: Array.from(weakMatchedTerms),
|
|
919
|
+
matchedTermCount: matchedTerms.size,
|
|
920
|
+
strongMatchedTermCount: strongMatchedTerms.size,
|
|
921
|
+
fileNameHits,
|
|
922
|
+
pathHits,
|
|
923
|
+
exactBoost: strongMatchedTerms.size * 0.016
|
|
924
|
+
+ weakMatchedTerms.size * 0.004
|
|
925
|
+
+ coverageRatio * 0.008
|
|
926
|
+
+ strongCoverageRatio * 0.014
|
|
927
|
+
+ densityRatio * 0.02
|
|
928
|
+
+ fileNameHits * 0.025
|
|
929
|
+
+ pathHits * 0.01
|
|
930
|
+
+ (phraseMatch ? 0.018 : 0)
|
|
931
|
+
- weakOnlyPenalty
|
|
932
|
+
- missingStrongPenalty
|
|
933
|
+
- indexFilePenalty,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function isIndexLikeDocName(fileName) {
|
|
937
|
+
return fileName === "contents.md" || fileName === "summary.md" || fileName === "index.md" || fileName === "readme.md";
|
|
938
|
+
}
|
|
939
|
+
function reciprocalRankScore(rank, weight) {
|
|
940
|
+
return weight * (1 / (40 + rank));
|
|
941
|
+
}
|
|
942
|
+
function parseEmbeddingJson(value) {
|
|
943
|
+
const parsed = JSON.parse(value);
|
|
944
|
+
return Array.isArray(parsed) ? parsed.map((entry) => Number(entry)) : [];
|
|
945
|
+
}
|
|
946
|
+
function cosineSimilarity(left, right) {
|
|
947
|
+
const dimensions = Math.min(left.length, right.length);
|
|
948
|
+
if (dimensions === 0) {
|
|
949
|
+
return Number.NEGATIVE_INFINITY;
|
|
950
|
+
}
|
|
951
|
+
let dot = 0;
|
|
952
|
+
let leftNorm = 0;
|
|
953
|
+
let rightNorm = 0;
|
|
954
|
+
for (let index = 0; index < dimensions; index += 1) {
|
|
955
|
+
const leftValue = left[index] ?? 0;
|
|
956
|
+
const rightValue = right[index] ?? 0;
|
|
957
|
+
dot += leftValue * rightValue;
|
|
958
|
+
leftNorm += leftValue * leftValue;
|
|
959
|
+
rightNorm += rightValue * rightValue;
|
|
960
|
+
}
|
|
961
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
962
|
+
return Number.NEGATIVE_INFINITY;
|
|
963
|
+
}
|
|
964
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
965
|
+
}
|
|
966
|
+
function buildSnippet(text) {
|
|
967
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
968
|
+
if (normalized.length <= 180) {
|
|
969
|
+
return normalized;
|
|
970
|
+
}
|
|
971
|
+
return `${normalized.slice(0, 177)}...`;
|
|
239
972
|
}
|
|
240
973
|
//# sourceMappingURL=memory.js.map
|