@takara-ai/miru-code 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -48
- package/docs/coverage-badge.json +6 -0
- package/grammars/manifest.json +10 -2
- package/grammars/tree-sitter-astro.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-php_only.wasm +0 -0
- package/grammars/tree-sitter-sql.wasm +0 -0
- package/grammars/tree-sitter-svelte.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/package.json +10 -2
- package/src/agents.ts +4 -4
- package/src/auth/device.ts +26 -21
- package/src/auth/providers.ts +26 -11
- package/src/benchmark/compare.ts +3 -10
- package/src/benchmark/grep.ts +64 -37
- package/src/benchmark/locate-compare.ts +8 -1
- package/src/benchmark/rg-literal.ts +9 -4
- package/src/cache.ts +3 -3
- package/src/chunking/ast.ts +21 -3
- package/src/chunking/grammars.ts +8 -3
- package/src/chunking/structural.ts +13 -24
- package/src/cli-mode.ts +18 -0
- package/src/cli.ts +10 -45
- package/src/credentials.ts +8 -13
- package/src/embeddings/openai.ts +6 -3
- package/src/embeddings/sagemaker.ts +18 -3
- package/src/embeddings/validate.ts +10 -7
- package/src/git.ts +6 -2
- package/src/help.ts +5 -4
- package/src/index/bm25.ts +66 -6
- package/src/index/create.ts +5 -9
- package/src/index/files.ts +5 -1
- package/src/index/incremental.ts +48 -8
- package/src/index/sparse.ts +14 -2
- package/src/installer/agents.ts +85 -65
- package/src/installer/benchmark-mode.ts +40 -11
- package/src/installer/config.ts +105 -42
- package/src/installer/installer.ts +59 -34
- package/src/installer/legacy-hooks.ts +131 -0
- package/src/installer/prompt.ts +20 -22
- package/src/installer/search-policy.ts +8 -8
- package/src/mcp/index-cache.ts +16 -3
- package/src/mcp/server.ts +42 -22
- package/src/mcp/stdio.ts +17 -2
- package/src/miru-index.ts +54 -18
- package/src/prompt.ts +51 -14
- package/src/ranking/boosting.ts +5 -6
- package/src/ranking/location.ts +6 -10
- package/src/ranking/penalties.ts +14 -22
- package/src/search.ts +4 -12
- package/src/setup.ts +25 -13
- package/src/snippet.ts +8 -26
- package/src/spinner.ts +26 -16
- package/src/tokenizer/bert-wordpiece.ts +0 -6
- package/src/utils.ts +0 -4
- package/src/version.ts +4 -4
- package/src/installer/hooks/install.ts +0 -549
- package/src/installer/hooks/opencode-plugin.ts +0 -54
- package/src/installer/hooks/search-guard.ts +0 -272
|
@@ -26,6 +26,11 @@ function loadAwsCredentialProvider(): Promise<AwsCredentialProviderModule> {
|
|
|
26
26
|
return awsCredentialProviderPromise;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Warm the lazily loaded AWS SDK modules before the first SageMaker request. */
|
|
30
|
+
export async function preloadSageMakerSdk(): Promise<void> {
|
|
31
|
+
await Promise.all([loadSageMakerRuntime(), loadAwsCredentialProvider()]);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
/** arn:aws:sagemaker:<region>:<account-id>:endpoint/<endpoint-name> (also covers aws-cn/aws-us-gov). */
|
|
30
35
|
const ENDPOINT_ARN_PATTERN = /^arn:aws[a-z0-9-]*:sagemaker:([a-z0-9-]+):(\d{12}):endpoint\/(.+)$/;
|
|
31
36
|
|
|
@@ -344,7 +349,13 @@ function buildRequestBody(
|
|
|
344
349
|
return body;
|
|
345
350
|
}
|
|
346
351
|
|
|
347
|
-
export function createSageMakerClient(
|
|
352
|
+
export function createSageMakerClient(
|
|
353
|
+
config: SageMakerEmbeddingConfig,
|
|
354
|
+
loaders: {
|
|
355
|
+
loadRuntime?: () => Promise<SageMakerRuntimeModule>;
|
|
356
|
+
loadCredentials?: () => Promise<AwsCredentialProviderModule>;
|
|
357
|
+
} = {},
|
|
358
|
+
): EmbeddingClient {
|
|
348
359
|
let client: InstanceType<SageMakerRuntimeModule["SageMakerRuntimeClient"]> | null = null;
|
|
349
360
|
|
|
350
361
|
return {
|
|
@@ -354,7 +365,10 @@ export function createSageMakerClient(config: SageMakerEmbeddingConfig): Embeddi
|
|
|
354
365
|
dimensions?: number,
|
|
355
366
|
): Promise<EmbeddingResponse> {
|
|
356
367
|
const [{ InvokeEndpointCommand, SageMakerRuntimeClient }, { defaultProvider }] =
|
|
357
|
-
await Promise.all([
|
|
368
|
+
await Promise.all([
|
|
369
|
+
(loaders.loadRuntime ?? loadSageMakerRuntime)(),
|
|
370
|
+
(loaders.loadCredentials ?? loadAwsCredentialProvider)(),
|
|
371
|
+
]);
|
|
358
372
|
if (!client) {
|
|
359
373
|
const profile = config.profile?.trim() || process.env.AWS_PROFILE?.trim();
|
|
360
374
|
const sdkCredentials = defaultProvider({ profile });
|
|
@@ -414,9 +428,10 @@ export interface ValidateSageMakerResult {
|
|
|
414
428
|
/** Real InvokeEndpoint round trip, used by `miru setup --sagemaker` to confirm auth + shape. */
|
|
415
429
|
export async function validateSageMakerConnection(
|
|
416
430
|
config: SageMakerEmbeddingConfig,
|
|
431
|
+
createClient: typeof createSageMakerClient = createSageMakerClient,
|
|
417
432
|
): Promise<ValidateSageMakerResult> {
|
|
418
433
|
try {
|
|
419
|
-
const client =
|
|
434
|
+
const client = createClient(config);
|
|
420
435
|
const response = await client.createEmbeddings("miru setup validation", "sagemaker-setup");
|
|
421
436
|
const embedding = response.data[0]?.embedding;
|
|
422
437
|
if (!embedding) {
|
|
@@ -12,12 +12,15 @@ export interface ValidateApiKeyResult {
|
|
|
12
12
|
message: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export async function validateEmbeddingApiKey(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export async function validateEmbeddingApiKey(
|
|
16
|
+
options: {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
dimensions?: number;
|
|
21
|
+
},
|
|
22
|
+
fetchImpl: (input: string | URL | Request, init?: RequestInit) => Promise<Response> = fetch,
|
|
23
|
+
): Promise<ValidateApiKeyResult> {
|
|
21
24
|
const baseUrl = (options.baseUrl ?? resolveEmbeddingBaseUrl()).replace(/\/$/, "");
|
|
22
25
|
const model = options.model ?? resolveTakaraEmbeddingModel();
|
|
23
26
|
const dimensions = options.dimensions ?? resolveEmbeddingDimensions(model);
|
|
@@ -33,7 +36,7 @@ export async function validateEmbeddingApiKey(options: {
|
|
|
33
36
|
|
|
34
37
|
let response: Response;
|
|
35
38
|
try {
|
|
36
|
-
response = await
|
|
39
|
+
response = await fetchImpl(endpoint, {
|
|
37
40
|
method: "POST",
|
|
38
41
|
headers: {
|
|
39
42
|
Authorization: `Bearer ${options.apiKey}`,
|
package/src/git.ts
CHANGED
|
@@ -5,7 +5,11 @@ import { envOptionalInt } from "./env.ts";
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_CLONE_TIMEOUT_SEC = 60;
|
|
7
7
|
|
|
8
|
-
export async function cloneGitRepository(
|
|
8
|
+
export async function cloneGitRepository(
|
|
9
|
+
url: string,
|
|
10
|
+
ref?: string | null,
|
|
11
|
+
spawn: typeof Bun.spawn = Bun.spawn,
|
|
12
|
+
): Promise<string> {
|
|
9
13
|
const timeoutSec = envOptionalInt(["MIRU_CLONE_TIMEOUT"], 1) ?? DEFAULT_CLONE_TIMEOUT_SEC;
|
|
10
14
|
const dir = await mkdtemp(join(tmpdir(), "miru-git-"));
|
|
11
15
|
const args = ["clone", "--depth", "1"];
|
|
@@ -16,7 +20,7 @@ export async function cloneGitRepository(url: string, ref?: string | null): Prom
|
|
|
16
20
|
|
|
17
21
|
let proc: ReturnType<typeof Bun.spawn>;
|
|
18
22
|
try {
|
|
19
|
-
proc =
|
|
23
|
+
proc = spawn(["git", ...args], {
|
|
20
24
|
stdout: "pipe",
|
|
21
25
|
stderr: "pipe",
|
|
22
26
|
stdin: "ignore",
|
package/src/help.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function printMainHelp(): void {
|
|
|
28
28
|
header();
|
|
29
29
|
|
|
30
30
|
section("Usage");
|
|
31
|
-
writeStdout(" miru
|
|
31
|
+
writeStdout(" miru Show help in a terminal");
|
|
32
32
|
writeStdout(" miru <command> [options]");
|
|
33
33
|
divider();
|
|
34
34
|
|
|
@@ -37,6 +37,7 @@ export function printMainHelp(): void {
|
|
|
37
37
|
commandRow("locate", "Exact substring location in the index");
|
|
38
38
|
commandRow("expand", "Adjacent chunks in the same file as a hit");
|
|
39
39
|
commandRow("find-related", "Find chunks related to a file:line");
|
|
40
|
+
commandRow("mcp", "Start MCP server (stdio)");
|
|
40
41
|
commandRow("setup", "Authenticate with Takara");
|
|
41
42
|
commandRow("install", "Configure miru across coding agents");
|
|
42
43
|
commandRow("uninstall", "Remove miru agent configuration");
|
|
@@ -177,7 +178,7 @@ export function printCommandHelp(command: string): void {
|
|
|
177
178
|
case "install":
|
|
178
179
|
commandHeader("install", "Interactive global agent setup.");
|
|
179
180
|
writeStdout("Configures MCP server, instructions, sub-agent files, and optional");
|
|
180
|
-
writeStdout("experimental integrations (
|
|
181
|
+
writeStdout("experimental integrations (Caveman, STE writing) under");
|
|
181
182
|
writeStdout("your user config (~/.claude, ~/.cursor, etc.).");
|
|
182
183
|
writeStdout("");
|
|
183
184
|
writeStdout(
|
|
@@ -243,9 +244,9 @@ export function printCommandHelp(command: string): void {
|
|
|
243
244
|
writeStdout("");
|
|
244
245
|
return;
|
|
245
246
|
case "mcp":
|
|
246
|
-
commandHeader("mcp", "Stdio MCP server
|
|
247
|
+
commandHeader("mcp", "Stdio MCP server.");
|
|
247
248
|
section("Usage");
|
|
248
|
-
writeStdout(" miru [--ref BRANCH] [--content TYPE ...] [--benchmark]");
|
|
249
|
+
writeStdout(" miru mcp [--ref BRANCH] [--content TYPE ...] [--benchmark]");
|
|
249
250
|
writeStdout(" Default content: code config");
|
|
250
251
|
writeStdout("");
|
|
251
252
|
writeStdout("Indexes on the first search/expand/find_related tool call (repo argument).");
|
package/src/index/bm25.ts
CHANGED
|
@@ -42,12 +42,21 @@ function runScoreWorker(job: Bm25ScoreJob): Promise<Bm25ScoreResult> {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export class BM25Index {
|
|
45
|
-
private docFreq
|
|
46
|
-
private docLengths: number[]
|
|
47
|
-
private postings
|
|
48
|
-
private avgDocLength
|
|
49
|
-
private numDocs
|
|
50
|
-
private totalLen
|
|
45
|
+
private docFreq: Map<string, number>;
|
|
46
|
+
private docLengths: number[];
|
|
47
|
+
private postings: Map<string, PostingsList>;
|
|
48
|
+
private avgDocLength: number;
|
|
49
|
+
private numDocs: number;
|
|
50
|
+
private totalLen: number;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
this.docFreq = new Map();
|
|
54
|
+
this.docLengths = [];
|
|
55
|
+
this.postings = new Map();
|
|
56
|
+
this.avgDocLength = 0;
|
|
57
|
+
this.numDocs = 0;
|
|
58
|
+
this.totalLen = 0;
|
|
59
|
+
}
|
|
51
60
|
|
|
52
61
|
/** Append one tokenized document. Safe to call incrementally while indexing. */
|
|
53
62
|
addDocument(doc: string[]): number {
|
|
@@ -73,6 +82,57 @@ export class BM25Index {
|
|
|
73
82
|
return docIndex;
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
get size(): number {
|
|
86
|
+
return this.numDocs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A new index with the given documents removed and later doc indices shifted down,
|
|
91
|
+
* equal to re-indexing the surviving docs in order. `this` is left untouched, so
|
|
92
|
+
* searches already holding it never see a half-updated index.
|
|
93
|
+
*/
|
|
94
|
+
withoutDocuments(removed: ReadonlySet<number>): BM25Index {
|
|
95
|
+
const next = new BM25Index();
|
|
96
|
+
const remap = new Int32Array(this.numDocs);
|
|
97
|
+
let kept = 0;
|
|
98
|
+
let firstRemoved = this.numDocs;
|
|
99
|
+
for (let doc = 0; doc < this.numDocs; doc++) {
|
|
100
|
+
if (removed.has(doc)) {
|
|
101
|
+
remap[doc] = -1;
|
|
102
|
+
if (firstRemoved === this.numDocs) {
|
|
103
|
+
firstRemoved = doc;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
remap[doc] = kept++;
|
|
107
|
+
const length = this.docLengths[doc] ?? 0;
|
|
108
|
+
next.docLengths.push(length);
|
|
109
|
+
next.totalLen += length;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
next.numDocs = kept;
|
|
113
|
+
next.avgDocLength = kept === 0 ? 0 : next.totalLen / kept;
|
|
114
|
+
|
|
115
|
+
for (const [term, list] of this.postings) {
|
|
116
|
+
// Lists ascend by doc index, so one ending before the first removal is untouched.
|
|
117
|
+
// Copy the array (addDocument appends to it) but share the immutable tuples.
|
|
118
|
+
const untouched = (list.at(-1)?.[0] ?? 0) < firstRemoved;
|
|
119
|
+
const nextList: PostingsList = untouched ? list.slice() : [];
|
|
120
|
+
if (!untouched) {
|
|
121
|
+
for (const entry of list) {
|
|
122
|
+
const mapped = remap[entry[0]] ?? -1;
|
|
123
|
+
if (mapped !== -1) {
|
|
124
|
+
nextList.push(mapped === entry[0] ? entry : [mapped, entry[1]]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (nextList.length > 0) {
|
|
129
|
+
next.postings.set(term, nextList);
|
|
130
|
+
next.docFreq.set(term, nextList.length);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
}
|
|
135
|
+
|
|
76
136
|
index(tokenizedDocs: string[][]): void {
|
|
77
137
|
this.numDocs = 0;
|
|
78
138
|
this.docFreq.clear();
|
package/src/index/create.ts
CHANGED
|
@@ -136,9 +136,6 @@ export async function createIndexFromPath(
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
const take = Math.min(embedBatchSize, pendingWindows.length);
|
|
139
|
-
if (!force && take < embedBatchSize) {
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
139
|
|
|
143
140
|
const batch = pendingWindows.splice(0, take);
|
|
144
141
|
emittedApiBatches++;
|
|
@@ -158,9 +155,10 @@ export async function createIndexFromPath(
|
|
|
158
155
|
})();
|
|
159
156
|
embedPromises.push(promise);
|
|
160
157
|
embedInFlight.add(promise);
|
|
161
|
-
promise.
|
|
162
|
-
embedInFlight.delete(promise)
|
|
163
|
-
|
|
158
|
+
promise.then(
|
|
159
|
+
() => embedInFlight.delete(promise),
|
|
160
|
+
() => embedInFlight.delete(promise),
|
|
161
|
+
);
|
|
164
162
|
}
|
|
165
163
|
};
|
|
166
164
|
|
|
@@ -209,9 +207,7 @@ export async function createIndexFromPath(
|
|
|
209
207
|
await maybeScheduleEmbed(true);
|
|
210
208
|
await Promise.all(embedPromises);
|
|
211
209
|
// Finish any BM25 docs not yet processed during idle gaps.
|
|
212
|
-
|
|
213
|
-
progressBm25(50);
|
|
214
|
-
}
|
|
210
|
+
progressBm25(Number.POSITIVE_INFINITY);
|
|
215
211
|
const embedsDoneAt = performance.now();
|
|
216
212
|
|
|
217
213
|
const chunks = emittedChunks;
|
package/src/index/files.ts
CHANGED
|
@@ -52,6 +52,9 @@ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
|
52
52
|
".zig": "zig",
|
|
53
53
|
".vue": "vue",
|
|
54
54
|
".svelte": "svelte",
|
|
55
|
+
".astro": "astro",
|
|
56
|
+
".erb": "embeddedtemplate",
|
|
57
|
+
".ejs": "embeddedtemplate",
|
|
55
58
|
".md": "markdown",
|
|
56
59
|
".mdx": "markdown",
|
|
57
60
|
".rst": "rst",
|
|
@@ -64,6 +67,7 @@ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
|
64
67
|
".cfg": "ini",
|
|
65
68
|
".xml": "xml",
|
|
66
69
|
".html": "html",
|
|
70
|
+
".htm": "html",
|
|
67
71
|
".css": "css",
|
|
68
72
|
".scss": "scss",
|
|
69
73
|
".less": "less",
|
|
@@ -74,7 +78,7 @@ const CODE_EXTENSIONS = new Set(
|
|
|
74
78
|
Object.entries(EXTENSION_TO_LANGUAGE)
|
|
75
79
|
.filter(
|
|
76
80
|
([, lang]) =>
|
|
77
|
-
!["markdown", "rst", "text", "json", "yaml", "toml", "ini", "xml"
|
|
81
|
+
!["markdown", "rst", "text", "json", "yaml", "toml", "ini", "xml"].includes(lang),
|
|
78
82
|
)
|
|
79
83
|
.map(([ext]) => ext),
|
|
80
84
|
);
|
package/src/index/incremental.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Chunk, ContentType } from "../types.ts";
|
|
|
5
5
|
import type { BM25Index } from "./bm25.ts";
|
|
6
6
|
import { detectLanguage, getExtensions, getFileStatus, readFileText } from "./files.ts";
|
|
7
7
|
import type { SemanticIndex } from "./semantic-index.ts";
|
|
8
|
-
import { buildBm25FromChunks } from "./sparse.ts";
|
|
8
|
+
import { buildBm25FromChunks, patchBm25 } from "./sparse.ts";
|
|
9
9
|
import { buildSemanticIndex } from "./vector-storage.ts";
|
|
10
10
|
import { vectorAt } from "./vectors.ts";
|
|
11
11
|
|
|
@@ -40,9 +40,34 @@ function isIndexableRelativePath(relativePath: string, extensions: Set<string>):
|
|
|
40
40
|
return extensions.has(ext);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Vector for every text: reused from `known`, otherwise embedded once (duplicates share a call). */
|
|
44
|
+
async function vectorsByText(
|
|
45
|
+
embeddings: EmbeddingBackend,
|
|
46
|
+
texts: readonly string[],
|
|
47
|
+
known: ReadonlyMap<string, Float32Array>,
|
|
48
|
+
): Promise<Map<string, Float32Array>> {
|
|
49
|
+
const result = new Map(known);
|
|
50
|
+
const missing = [...new Set(texts)].filter((text) => !result.has(text));
|
|
51
|
+
if (missing.length === 0) {
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
const embedded = await embeddings.embedDocuments(missing);
|
|
55
|
+
if (embedded.length !== missing.length) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Vector count ${embedded.length} does not match chunk count ${missing.length}.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
for (const [i, text] of missing.entries()) {
|
|
61
|
+
result.set(text, embedded[i] as Float32Array);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
/**
|
|
44
67
|
* Replace chunks for the given repo-relative paths only: remove old chunks,
|
|
45
|
-
* embed new ones, rebuild
|
|
68
|
+
* embed new ones, rebuild the semantic index from the merged vector set, and update
|
|
69
|
+
* BM25 by dropping the old docs and indexing only the new chunks. Pass `bm25` (the
|
|
70
|
+
* index matching `chunks`) to enable that; without it BM25 is rebuilt from scratch.
|
|
46
71
|
*/
|
|
47
72
|
export async function applyIncrementalFileChanges(options: {
|
|
48
73
|
root: string;
|
|
@@ -50,6 +75,7 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
50
75
|
embeddings: EmbeddingBackend;
|
|
51
76
|
chunks: Chunk[];
|
|
52
77
|
semanticIndex: SemanticIndex;
|
|
78
|
+
bm25?: BM25Index;
|
|
53
79
|
relativePaths: readonly string[];
|
|
54
80
|
}): Promise<{ chunks: Chunk[]; bm25: BM25Index; semantic: SemanticIndex }> {
|
|
55
81
|
const root = resolve(options.root);
|
|
@@ -60,16 +86,24 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
60
86
|
.filter((p) => p.length > 0 && isIndexableRelativePath(p, extensions)),
|
|
61
87
|
);
|
|
62
88
|
|
|
89
|
+
// BM25 doc i is chunk i; an index of another size can't be patched safely.
|
|
90
|
+
const baseBm25 = options.bm25?.size === options.chunks.length ? options.bm25 : undefined;
|
|
91
|
+
|
|
63
92
|
if (targets.size === 0) {
|
|
64
93
|
return {
|
|
65
94
|
chunks: options.chunks,
|
|
66
|
-
bm25: buildBm25FromChunks(options.chunks),
|
|
95
|
+
bm25: baseBm25 ?? buildBm25FromChunks(options.chunks),
|
|
67
96
|
semantic: options.semanticIndex,
|
|
68
97
|
};
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
const keptChunks: Chunk[] = [];
|
|
72
101
|
const keptVectors: Float32Array[] = [];
|
|
102
|
+
const removedDocs = new Set<number>();
|
|
103
|
+
// An embedding is a pure function of chunk text, so a re-chunked file can reuse
|
|
104
|
+
// the vector of any old chunk with byte-identical content (keyed on the exact
|
|
105
|
+
// string, never a hash, so a collision can't leave a stale vector).
|
|
106
|
+
const reusableVectors = new Map<string, Float32Array>();
|
|
73
107
|
|
|
74
108
|
for (let i = 0; i < options.chunks.length; i++) {
|
|
75
109
|
const chunk = options.chunks[i];
|
|
@@ -78,6 +112,10 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
78
112
|
}
|
|
79
113
|
const rel = normalizeRelativePath(chunk.file_path);
|
|
80
114
|
if (targets.has(rel)) {
|
|
115
|
+
removedDocs.add(i);
|
|
116
|
+
if (!reusableVectors.has(chunk.content)) {
|
|
117
|
+
reusableVectors.set(chunk.content, vectorAt(options.semanticIndex, i));
|
|
118
|
+
}
|
|
81
119
|
continue;
|
|
82
120
|
}
|
|
83
121
|
keptChunks.push(chunk);
|
|
@@ -89,10 +127,12 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
89
127
|
addedChunks.push(...(await chunksForFile(root, rel)));
|
|
90
128
|
}
|
|
91
129
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
const vectorFor = await vectorsByText(
|
|
131
|
+
options.embeddings,
|
|
132
|
+
addedChunks.map((c) => c.content),
|
|
133
|
+
reusableVectors,
|
|
134
|
+
);
|
|
135
|
+
const addedVectors = addedChunks.map((c) => vectorFor.get(c.content) as Float32Array);
|
|
96
136
|
|
|
97
137
|
const chunks = [...keptChunks, ...addedChunks];
|
|
98
138
|
const vectors = [...keptVectors, ...addedVectors];
|
|
@@ -107,7 +147,7 @@ export async function applyIncrementalFileChanges(options: {
|
|
|
107
147
|
|
|
108
148
|
return {
|
|
109
149
|
chunks,
|
|
110
|
-
bm25: buildBm25FromChunks(chunks),
|
|
150
|
+
bm25: baseBm25 ? patchBm25(baseBm25, removedDocs, addedChunks) : buildBm25FromChunks(chunks),
|
|
111
151
|
semantic: buildSemanticIndex(vectors),
|
|
112
152
|
};
|
|
113
153
|
}
|
package/src/index/sparse.ts
CHANGED
|
@@ -30,10 +30,22 @@ export function addChunkToBm25(bm25: BM25Index, chunk: Chunk): void {
|
|
|
30
30
|
bm25.addDocument(tokenize(enrichForBm25(chunk)));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
const bm25 = new BM25Index();
|
|
33
|
+
function appendChunksToBm25(bm25: BM25Index, chunks: readonly Chunk[]): BM25Index {
|
|
35
34
|
for (const chunk of chunks) {
|
|
36
35
|
addChunkToBm25(bm25, chunk);
|
|
37
36
|
}
|
|
38
37
|
return bm25;
|
|
39
38
|
}
|
|
39
|
+
|
|
40
|
+
export function buildBm25FromChunks(chunks: readonly Chunk[]): BM25Index {
|
|
41
|
+
return appendChunksToBm25(new BM25Index(), chunks);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Drop docs `removedDocs` from `base` and append `added`; `base` is left untouched. */
|
|
45
|
+
export function patchBm25(
|
|
46
|
+
base: BM25Index,
|
|
47
|
+
removedDocs: ReadonlySet<number>,
|
|
48
|
+
added: readonly Chunk[],
|
|
49
|
+
): BM25Index {
|
|
50
|
+
return appendChunksToBm25(base.withoutDocuments(removedDocs), added);
|
|
51
|
+
}
|