@takara-ai/miru-code 1.0.4 → 1.1.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.
@@ -1,15 +1,25 @@
1
- import { availableParallelism, cpus } from "node:os";
1
+ /**
2
+ * Concurrency helpers for indexing and embedding pipelines.
3
+ *
4
+ * Caps parallel file parsing, BM25 scoring, and embed batching so Miru stays
5
+ * responsive on laptops and shared CI runners without starving the OS.
6
+ */
7
+ import { availableParallelism } from "node:os";
2
8
 
3
- const DEFAULT_RESERVE_CORES = 2;
9
+ /** Cores left for the event loop, MCP stdio, and other host processes during indexing. */
10
+ export const DEFAULT_RESERVE_CORES = 2;
4
11
 
5
- export function resolveCpuCount(): number {
6
- if (typeof availableParallelism === "function") {
7
- return availableParallelism();
8
- }
9
- return cpus().length;
12
+ function workerCount(concurrency: number, itemCount: number): number {
13
+ return Math.max(1, Math.min(concurrency, itemCount));
10
14
  }
11
15
 
12
- /** Worker pool size: logical CPUs minus 2, minimum 1. Override with MIRU_CONCURRENCY. */
16
+ /**
17
+ * Worker pool size for file parsing and other CPU-bound index work.
18
+ *
19
+ * Default: `availableParallelism() - DEFAULT_RESERVE_CORES`, floored at 1.
20
+ * Override with `MIRU_CONCURRENCY` or legacy alias `MIRU_WORKERS`.
21
+ * Invalid env values are ignored so a typo does not crash indexing.
22
+ */
13
23
  export function resolveWorkerConcurrency(): number {
14
24
  const raw = process.env.MIRU_CONCURRENCY ?? process.env.MIRU_WORKERS;
15
25
  if (raw) {
@@ -18,37 +28,34 @@ export function resolveWorkerConcurrency(): number {
18
28
  return Math.floor(n);
19
29
  }
20
30
  }
21
- return Math.max(1, resolveCpuCount() - DEFAULT_RESERVE_CORES);
31
+ return Math.max(1, availableParallelism() - DEFAULT_RESERVE_CORES);
22
32
  }
23
33
 
24
- /** Run async work over items with at most `concurrency` tasks in flight. Results keep input order. */
34
+ /**
35
+ * Bounded parallel map: at most `concurrency` workers run `worker` at once.
36
+ *
37
+ * Unlike `Promise.all(items.map(worker))`, this limits in-flight tasks when
38
+ * `items` is large (e.g. thousands of source files). Output order matches
39
+ * input order because each result is stored at its original index.
40
+ *
41
+ * Workers share a single `items.entries()` iterator; each claims the next
42
+ * `[index, item]` pair until the input is exhausted.
43
+ */
25
44
  export async function mapPool<T, R>(
26
45
  items: readonly T[],
27
46
  concurrency: number,
28
47
  worker: (item: T, index: number) => Promise<R>,
29
48
  ): Promise<R[]> {
30
- if (items.length === 0) {
31
- return [];
32
- }
33
-
34
- const limit = Math.max(1, Math.min(concurrency, items.length));
35
49
  const results = new Array<R>(items.length);
36
- let nextIndex = 0;
50
+ const pending = items.entries();
37
51
 
38
- async function drain(): Promise<void> {
39
- while (true) {
40
- const i = nextIndex++;
41
- if (i >= items.length) {
42
- return;
52
+ await Promise.all(
53
+ Array.from({ length: workerCount(concurrency, items.length) }, async () => {
54
+ for (const [i, item] of pending) {
55
+ results[i] = await worker(item, i);
43
56
  }
44
- const item = items[i];
45
- if (item === undefined) {
46
- continue;
47
- }
48
- results[i] = await worker(item, i);
49
- }
50
- }
57
+ }),
58
+ );
51
59
 
52
- await Promise.all(Array.from({ length: limit }, () => drain()));
53
60
  return results;
54
61
  }
@@ -1,4 +1,4 @@
1
- import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { chmod, mkdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { hasTakaraApiKeyInEnv, normalizeTakaraApiKeyEnv } from "./env.ts";
4
4
 
@@ -37,7 +37,7 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
37
37
  return null;
38
38
  }
39
39
  try {
40
- const parsed = JSON.parse(await readFile(path, "utf-8")) as StoredCredentials;
40
+ const parsed = JSON.parse(await Bun.file(path).text()) as StoredCredentials;
41
41
  if (parsed.version !== CREDENTIALS_VERSION || !parsed.takara_api_key) {
42
42
  return null;
43
43
  }
@@ -69,10 +69,7 @@ export async function saveStoredCredentials(apiKey: string): Promise<string> {
69
69
  version: CREDENTIALS_VERSION,
70
70
  takara_api_key: apiKey,
71
71
  };
72
- await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, {
73
- encoding: "utf-8",
74
- mode: 0o600,
75
- });
72
+ await Bun.write(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
76
73
  try {
77
74
  await chmod(path, 0o600);
78
75
  } catch {
@@ -88,7 +85,7 @@ export async function clearStoredCredentials(): Promise<{ cleared: boolean; path
88
85
  }
89
86
 
90
87
  const stored = await readStoredCredentials();
91
- await rm(path, { force: true });
88
+ await Bun.file(path).delete();
92
89
 
93
90
  if (stored && process.env.TAKARA_API_KEY === stored.takara_api_key) {
94
91
  delete process.env.TAKARA_API_KEY;
@@ -1,8 +1,8 @@
1
1
  import { mapPool, resolveWorkerConcurrency } from "../concurrency.ts";
2
2
  import { envFirstString, envOptionalInt, resolveEmbeddingApiKey } from "../env.ts";
3
3
 
4
- const DEFAULT_MODEL = "ds1-miru-int8";
5
- const DEFAULT_BASE_URL = "https://infer.takara.ai/v1";
4
+ export const DEFAULT_EMBEDDING_MODEL = "ds1-miru-int8";
5
+ export const DEFAULT_EMBEDDING_BASE_URL = "https://infer.takara.ai/v1";
6
6
  const DEFAULT_BATCH_SIZE = 32;
7
7
  const DEFAULT_MAX_EMBED_CHARS = 1300;
8
8
  const WINDOW_OVERLAP_CHARS = 120;
@@ -32,7 +32,10 @@ export interface EmbeddingBackend {
32
32
  }
33
33
 
34
34
  export function resolveEmbeddingModel(): string {
35
- return envFirstString(["MIRU_OPENAI_EMBEDDING_MODEL", "OPENAI_EMBEDDING_MODEL"], DEFAULT_MODEL);
35
+ return envFirstString(
36
+ ["MIRU_OPENAI_EMBEDDING_MODEL", "OPENAI_EMBEDDING_MODEL"],
37
+ DEFAULT_EMBEDDING_MODEL,
38
+ );
36
39
  }
37
40
 
38
41
  export function resolveMaxEmbedChars(): number {
@@ -118,10 +121,10 @@ export function resolveEmbeddingBatchSize(): number {
118
121
  }
119
122
 
120
123
  export function resolveEmbeddingBaseUrl(): string {
121
- return envFirstString(["MIRU_OPENAI_BASE_URL", "OPENAI_BASE_URL"], DEFAULT_BASE_URL).replace(
122
- /\/$/,
123
- "",
124
- );
124
+ return envFirstString(
125
+ ["MIRU_OPENAI_BASE_URL", "OPENAI_BASE_URL"],
126
+ DEFAULT_EMBEDDING_BASE_URL,
127
+ ).replace(/\/$/, "");
125
128
  }
126
129
 
127
130
  interface Int8Embedding {
package/src/env-files.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
1
  import { join } from "node:path";
3
2
 
4
3
  function applyEnvLine(line: string): void {
@@ -24,21 +23,25 @@ function applyEnvLine(line: string): void {
24
23
  process.env[key] = value;
25
24
  }
26
25
 
27
- function loadEnvFile(path: string): void {
28
- if (!existsSync(path)) {
26
+ async function loadEnvFile(path: string): Promise<void> {
27
+ const file = Bun.file(path);
28
+ if (!(await file.exists())) {
29
29
  return;
30
30
  }
31
- for (const line of readFileSync(path, "utf-8").split("\n")) {
31
+ for (const line of (await file.text()).split("\n")) {
32
32
  applyEnvLine(line);
33
33
  }
34
34
  }
35
35
 
36
36
  /** Load .env.local / .env without overriding vars already set (e.g. MCP config env). */
37
- export function loadEnvFiles(options?: { packageRoot?: string; cwd?: string }): void {
37
+ export async function loadEnvFiles(options?: {
38
+ packageRoot?: string;
39
+ cwd?: string;
40
+ }): Promise<void> {
38
41
  const cwd = options?.cwd ?? process.cwd();
39
42
  const packageRoot = options?.packageRoot ?? join(import.meta.dir, "..");
40
43
  for (const dir of [packageRoot, cwd]) {
41
- loadEnvFile(join(dir, ".env.local"));
42
- loadEnvFile(join(dir, ".env"));
44
+ await loadEnvFile(join(dir, ".env.local"));
45
+ await loadEnvFile(join(dir, ".env"));
43
46
  }
44
47
  }
package/src/help.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AgentId } from "./agents.ts";
2
2
  import {
3
- brandTitle,
3
+ commandHeader,
4
4
  commandRow,
5
5
  divider,
6
6
  fail,
@@ -9,6 +9,9 @@ import {
9
9
  section,
10
10
  writeStdout,
11
11
  } from "./cli-ui.ts";
12
+ import { DEFAULT_RESERVE_CORES } from "./concurrency.ts";
13
+ import { DEFAULT_EMBEDDING_BASE_URL, DEFAULT_EMBEDDING_MODEL } from "./embeddings/openai.ts";
14
+ import { TAKARA_API_KEY_ENV } from "./env.ts";
12
15
 
13
16
  export const AGENT_IDS: readonly AgentId[] = [
14
17
  "claude",
@@ -22,7 +25,7 @@ export const AGENT_IDS: readonly AgentId[] = [
22
25
  const AGENT_LIST = AGENT_IDS.join(", ");
23
26
 
24
27
  export function printMainHelp(): void {
25
- header("hybrid code search for agents");
28
+ header();
26
29
 
27
30
  section("Usage");
28
31
  writeStdout(" miru Start MCP server (stdio)");
@@ -51,14 +54,14 @@ export function printMainHelp(): void {
51
54
 
52
55
  export function printEnvHelp(): void {
53
56
  section("Environment");
54
- writeStdout(" TAKARA_API_KEY");
57
+ writeStdout(` ${TAKARA_API_KEY_ENV}`);
55
58
  writeStdout(" Takara bearer token for embeddings");
56
59
  writeStdout(" MIRU_OPENAI_BASE_URL");
57
- writeStdout(" Default: https://infer.takara.ai/v1");
60
+ writeStdout(` Default: ${DEFAULT_EMBEDDING_BASE_URL}`);
58
61
  writeStdout(" MIRU_OPENAI_EMBEDDING_MODEL");
59
- writeStdout(" Default: ds1-miru-int8");
62
+ writeStdout(` Default: ${DEFAULT_EMBEDDING_MODEL}`);
60
63
  writeStdout(" MIRU_CONCURRENCY");
61
- writeStdout(" Parallel workers (default: CPUs − 2)");
64
+ writeStdout(` Parallel workers (default: CPUs − ${DEFAULT_RESERVE_CORES})`);
62
65
  writeStdout("");
63
66
  }
64
67
 
@@ -67,17 +70,10 @@ export function printFullHelp(): void {
67
70
  printEnvHelp();
68
71
  }
69
72
 
70
- function printCommandHeader(name: string, summary: string): void {
71
- writeStdout("");
72
- writeStdout(`${brandTitle()} ${name}`);
73
- writeStdout(summary);
74
- divider();
75
- }
76
-
77
73
  export function printCommandHelp(command: string): void {
78
74
  switch (command) {
79
75
  case "search":
80
- printCommandHeader("search", "Hybrid semantic + keyword search.");
76
+ commandHeader("search", "Hybrid semantic + keyword search.");
81
77
  section("Usage");
82
78
  writeStdout(" miru search <query> [path] [options]");
83
79
  section("Options");
@@ -89,7 +85,7 @@ export function printCommandHelp(command: string): void {
89
85
  writeStdout("");
90
86
  return;
91
87
  case "expand":
92
- printCommandHeader("expand", "More context in the same file as a search hit.");
88
+ commandHeader("expand", "More context in the same file as a search hit.");
93
89
  section("Usage");
94
90
  writeStdout(" miru expand <file> <line> [path] [--before N] [--after N]");
95
91
  section("Example");
@@ -97,7 +93,7 @@ export function printCommandHelp(command: string): void {
97
93
  writeStdout("");
98
94
  return;
99
95
  case "find-related":
100
- printCommandHeader("find-related", "Semantic neighbors of a file location.");
96
+ commandHeader("find-related", "Semantic neighbors of a file location.");
101
97
  section("Usage");
102
98
  writeStdout(" miru find-related <file> <line> [path] [options]");
103
99
  section("Example");
@@ -105,7 +101,7 @@ export function printCommandHelp(command: string): void {
105
101
  writeStdout("");
106
102
  return;
107
103
  case "setup":
108
- printCommandHeader("setup", "Store and validate your Takara API key.");
104
+ commandHeader("setup", "Store and validate your Takara API key.");
109
105
  section("Usage");
110
106
  writeStdout(" miru setup [--key TOKEN] [--force] [--clear]");
111
107
  section("Options");
@@ -115,7 +111,7 @@ export function printCommandHelp(command: string): void {
115
111
  writeStdout("");
116
112
  return;
117
113
  case "install":
118
- printCommandHeader("install", "Interactive global agent setup.");
114
+ commandHeader("install", "Interactive global agent setup.");
119
115
  writeStdout("Configures MCP server, instructions, and sub-agent files under");
120
116
  writeStdout("your user config (~/.claude, ~/.cursor, etc.).");
121
117
  writeStdout("");
@@ -123,12 +119,12 @@ export function printCommandHelp(command: string): void {
123
119
  writeStdout("");
124
120
  return;
125
121
  case "uninstall":
126
- printCommandHeader("uninstall", "Remove miru configuration from agents.");
122
+ commandHeader("uninstall", "Remove miru configuration from agents.");
127
123
  writeStdout("Removes MCP entries, marked instruction blocks, and global sub-agents.");
128
124
  writeStdout("");
129
125
  return;
130
126
  case "init":
131
- printCommandHeader("init", "Project-local sub-agent file.");
127
+ commandHeader("init", "Project-local sub-agent file.");
132
128
  writeStdout("Prefer miru install for global setup. Use init to commit into a repo.");
133
129
  section("Usage");
134
130
  writeStdout(" miru init --agent AGENT [--force]");
@@ -139,7 +135,7 @@ export function printCommandHelp(command: string): void {
139
135
  writeStdout("");
140
136
  return;
141
137
  case "clear":
142
- printCommandHeader("clear", "Drop the on-disk index cache.");
138
+ commandHeader("clear", "Drop the on-disk index cache.");
143
139
  section("Usage");
144
140
  writeStdout(" miru clear [path]");
145
141
  section("Example");
@@ -147,7 +143,7 @@ export function printCommandHelp(command: string): void {
147
143
  writeStdout("");
148
144
  return;
149
145
  case "mcp":
150
- printCommandHeader("mcp", "Stdio MCP server (default with no subcommand).");
146
+ commandHeader("mcp", "Stdio MCP server (default with no subcommand).");
151
147
  section("Usage");
152
148
  writeStdout(" miru [--ref BRANCH] [--content TYPE ...]");
153
149
  writeStdout("");
@@ -0,0 +1,57 @@
1
+ export type ChunkIndexMappings = {
2
+ fileMapping: ReadonlyMap<string, readonly number[]>;
3
+ languageMapping: ReadonlyMap<string, readonly number[]>;
4
+ };
5
+
6
+ /**
7
+ * Union of chunk indices matching language and/or path filters.
8
+ *
9
+ * Filters combine with OR semantics: a chunk is included if it matches any
10
+ * requested language or path. Returns `undefined` when no filters are given
11
+ * (search all chunks) or when no chunks match.
12
+ *
13
+ * Single-filter queries return the prebuilt map array directly (read-only for
14
+ * callers). Multi-filter queries allocate a deduped index list.
15
+ */
16
+ export function buildChunkSelector(
17
+ mappings: ChunkIndexMappings,
18
+ filterLanguages?: readonly string[],
19
+ filterPaths?: readonly string[],
20
+ ): readonly number[] | undefined {
21
+ const langCount = filterLanguages?.length ?? 0;
22
+ const pathCount = filterPaths?.length ?? 0;
23
+
24
+ if (langCount === 0 && pathCount === 0) {
25
+ return undefined;
26
+ }
27
+
28
+ if (langCount === 1 && pathCount === 0) {
29
+ const lang = filterLanguages?.[0];
30
+ if (!lang) {
31
+ return undefined;
32
+ }
33
+ const indices = mappings.languageMapping.get(lang);
34
+ return indices && indices.length > 0 ? indices : undefined;
35
+ }
36
+
37
+ if (pathCount === 1 && langCount === 0) {
38
+ const fp = filterPaths?.[0];
39
+ if (!fp) {
40
+ return undefined;
41
+ }
42
+ const indices = mappings.fileMapping.get(fp);
43
+ return indices && indices.length > 0 ? indices : undefined;
44
+ }
45
+
46
+ const selector: number[] = [];
47
+ for (const lang of filterLanguages ?? []) {
48
+ selector.push(...(mappings.languageMapping.get(lang) ?? []));
49
+ }
50
+ for (const fp of filterPaths ?? []) {
51
+ selector.push(...(mappings.fileMapping.get(fp) ?? []));
52
+ }
53
+ if (selector.length === 0) {
54
+ return undefined;
55
+ }
56
+ return [...new Set(selector)];
57
+ }
@@ -1,70 +1,114 @@
1
- /** Cosine-distance vector index with optional chunk selector. */
2
- import { mkdir } from "node:fs/promises";
1
+ /** Uncompressed float32 semantic index. Debug/precision baseline only; production uses int8. */
3
2
  import type { SemanticIndex } from "./semantic-index.ts";
4
- import { selectTopKByDistance } from "./topk.ts";
3
+ import { TopKDistanceCollector } from "./topk.ts";
4
+
5
+ function cosineDistanceFlat(
6
+ data: Float32Array,
7
+ dim: number,
8
+ docIndex: number,
9
+ query: Float32Array,
10
+ ): number {
11
+ const offset = docIndex * dim;
12
+ let dot = 0;
13
+ for (let i = 0; i < dim; i++) {
14
+ dot += (query[i] ?? 0) * (data[offset + i] ?? 0);
15
+ }
16
+ return 1 - dot;
17
+ }
5
18
 
6
19
  export class VectorIndex implements SemanticIndex {
7
- private vectors: Float32Array[];
20
+ private readonly data: Float32Array;
21
+ private readonly count: number;
22
+ private readonly dim: number;
23
+ private vectorsCache: Float32Array[] | null = null;
8
24
 
9
25
  constructor(vectors: Float32Array[]) {
10
- this.vectors = vectors;
26
+ if (vectors.length === 0) {
27
+ this.count = 0;
28
+ this.dim = 0;
29
+ this.data = new Float32Array(0);
30
+ return;
31
+ }
32
+ this.count = vectors.length;
33
+ this.dim = vectors[0]?.length ?? 0;
34
+ this.data = new Float32Array(this.count * this.dim);
35
+ for (let i = 0; i < this.count; i++) {
36
+ const vec = vectors[i];
37
+ if (vec) {
38
+ this.data.set(vec, i * this.dim);
39
+ }
40
+ }
41
+ }
42
+
43
+ static fromFlatBuffer(data: Float32Array, count: number, dim: number): VectorIndex {
44
+ const index = Object.create(VectorIndex.prototype) as VectorIndex;
45
+ Object.assign(index, { data, count, dim });
46
+ return index;
11
47
  }
12
48
 
13
49
  get size(): number {
14
- return this.vectors.length;
50
+ return this.count;
15
51
  }
16
52
 
17
53
  get dimensions(): number {
18
- return this.vectors[0]?.length ?? 0;
54
+ return this.dim;
19
55
  }
20
56
 
21
57
  getVectors(): readonly Float32Array[] {
22
- return this.vectors;
58
+ if (this.vectorsCache) {
59
+ return this.vectorsCache;
60
+ }
61
+ const vectors: Float32Array[] = [];
62
+ for (let i = 0; i < this.count; i++) {
63
+ vectors.push(this.data.subarray(i * this.dim, (i + 1) * this.dim));
64
+ }
65
+ this.vectorsCache = vectors;
66
+ return vectors;
23
67
  }
24
68
 
25
69
  vectorAt(docIndex: number): Float32Array {
26
- const vec = this.vectors[docIndex];
27
- if (!vec) {
70
+ if (docIndex < 0 || docIndex >= this.count) {
28
71
  throw new Error(`Missing vector at index ${docIndex}`);
29
72
  }
30
- return vec;
73
+ return this.data.subarray(docIndex * this.dim, (docIndex + 1) * this.dim);
31
74
  }
32
75
 
33
76
  memoryBytes(): number {
34
- return this.vectors.length * this.dimensions * 4;
77
+ return this.count * this.dim * 4;
35
78
  }
36
79
 
37
80
  query(
38
81
  queryVector: Float32Array,
39
82
  k: number,
40
- selector?: number[],
83
+ selector?: readonly number[],
41
84
  ): { indices: number[]; distances: number[] } {
42
85
  if (k < 1) {
43
86
  throw new Error(`k should be >= 1, is now ${k}`);
44
87
  }
45
-
46
- const numVectors = this.vectors.length;
47
- if (numVectors === 0) {
88
+ if (this.count === 0) {
48
89
  return { indices: [], distances: [] };
49
90
  }
50
91
 
51
- const indices = selector ?? Array.from({ length: numVectors }, (_, i) => i);
52
- const effectiveK = Math.min(k, indices.length);
92
+ const effectiveK = Math.min(k, selector?.length ?? this.count);
53
93
  if (effectiveK === 0) {
54
94
  return { indices: [], distances: [] };
55
95
  }
56
96
 
57
- const top = selectTopKByDistance(
58
- indices.flatMap((idx) => {
59
- const vec = this.vectors[idx];
60
- if (!vec) {
61
- return [];
97
+ const collector = new TopKDistanceCollector(effectiveK);
98
+ if (selector) {
99
+ for (const idx of selector) {
100
+ if (idx < 0 || idx >= this.count) {
101
+ continue;
62
102
  }
63
- return [{ index: idx, distance: cosineDistance(queryVector, vec) }];
64
- }),
65
- effectiveK,
66
- );
103
+ collector.offer(idx, cosineDistanceFlat(this.data, this.dim, idx, queryVector));
104
+ }
105
+ } else {
106
+ for (let i = 0; i < this.count; i++) {
107
+ collector.offer(i, cosineDistanceFlat(this.data, this.dim, i, queryVector));
108
+ }
109
+ }
67
110
 
111
+ const top = collector.finish();
68
112
  return {
69
113
  indices: top.map((t) => t.index),
70
114
  distances: top.map((t) => t.distance),
@@ -72,21 +116,10 @@ export class VectorIndex implements SemanticIndex {
72
116
  }
73
117
 
74
118
  async save(dir: string): Promise<void> {
75
- await mkdir(dir, { recursive: true });
76
-
77
- const dim = this.dimensions;
78
- const count = this.size;
79
- const buffer = new Float32Array(count * dim);
80
- for (let i = 0; i < count; i++) {
81
- const vec = this.vectors[i];
82
- if (vec) {
83
- buffer.set(vec, i * dim);
84
- }
85
- }
86
- await Bun.write(`${dir}/vectors.bin`, buffer);
119
+ await Bun.write(`${dir}/vectors.bin`, this.data);
87
120
  await Bun.write(
88
121
  `${dir}/meta.json`,
89
- JSON.stringify({ count, dimensions: dim, storage: "float32" }),
122
+ JSON.stringify({ count: this.count, dimensions: this.dim, storage: "float32" }),
90
123
  );
91
124
  }
92
125
 
@@ -95,19 +128,7 @@ export class VectorIndex implements SemanticIndex {
95
128
  count: number;
96
129
  dimensions: number;
97
130
  };
98
- const raw = new Float32Array(await Bun.file(`${dir}/vectors.bin`).arrayBuffer());
99
- const vectors: Float32Array[] = [];
100
- for (let i = 0; i < meta.count; i++) {
101
- vectors.push(raw.subarray(i * meta.dimensions, (i + 1) * meta.dimensions));
102
- }
103
- return new VectorIndex(vectors);
131
+ const data = new Float32Array(await Bun.file(`${dir}/vectors.bin`).arrayBuffer());
132
+ return VectorIndex.fromFlatBuffer(data, meta.count, meta.dimensions);
104
133
  }
105
134
  }
106
-
107
- function cosineDistance(a: Float32Array, b: Float32Array): number {
108
- let dot = 0;
109
- for (let i = 0; i < a.length; i++) {
110
- dot += (a[i] ?? 0) * (b[i] ?? 0);
111
- }
112
- return 1 - dot;
113
- }
@@ -1,7 +1,7 @@
1
1
  import { join, relative } from "node:path";
2
2
  import type { Chunk } from "../types.ts";
3
3
 
4
- function indexedPath(absPath: string, repoRoot: string, displayRoot?: string): string {
4
+ function indexedPath(absPath: string, _repoRoot: string, displayRoot?: string): string {
5
5
  if (displayRoot) {
6
6
  return relative(displayRoot, absPath).replace(/\\/g, "/");
7
7
  }
@@ -0,0 +1,55 @@
1
+ type QuantizedVector = {
2
+ codes: Int8Array;
3
+ scale: number;
4
+ };
5
+
6
+ /** Reference scalar int8 dot product (A/B regression baseline). */
7
+ export function quantizedDotFlatScalar(
8
+ query: QuantizedVector,
9
+ codes: Int8Array,
10
+ offset: number,
11
+ dim: number,
12
+ docScale: number,
13
+ ): number {
14
+ let sum = 0;
15
+ const q = query.codes;
16
+ for (let i = 0; i < dim; i++) {
17
+ sum += q[i] * codes[offset + i];
18
+ }
19
+ return sum * query.scale * docScale;
20
+ }
21
+
22
+ /** Production int8 dot product with 8-wide unrolling for JSC auto-vectorization. */
23
+ export function quantizedDotFlat(
24
+ query: QuantizedVector,
25
+ codes: Int8Array,
26
+ offset: number,
27
+ dim: number,
28
+ docScale: number,
29
+ ): number {
30
+ const q = query.codes;
31
+ let s0 = 0;
32
+ let s1 = 0;
33
+ let s2 = 0;
34
+ let s3 = 0;
35
+ let s4 = 0;
36
+ let s5 = 0;
37
+ let s6 = 0;
38
+ let s7 = 0;
39
+ let i = 0;
40
+ for (; i + 7 < dim; i += 8) {
41
+ s0 += q[i] * codes[offset + i];
42
+ s1 += q[i + 1] * codes[offset + i + 1];
43
+ s2 += q[i + 2] * codes[offset + i + 2];
44
+ s3 += q[i + 3] * codes[offset + i + 3];
45
+ s4 += q[i + 4] * codes[offset + i + 4];
46
+ s5 += q[i + 5] * codes[offset + i + 5];
47
+ s6 += q[i + 6] * codes[offset + i + 6];
48
+ s7 += q[i + 7] * codes[offset + i + 7];
49
+ }
50
+ let sum = s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7;
51
+ for (; i < dim; i++) {
52
+ sum += q[i] * codes[offset + i];
53
+ }
54
+ return sum * query.scale * docScale;
55
+ }
@@ -1,4 +1,3 @@
1
- import { mkdir } from "node:fs/promises";
2
1
  import { join } from "node:path";
3
2
  import { BM25Index } from "./bm25.ts";
4
3
  import { VectorIndex } from "./dense.ts";
@@ -78,7 +77,6 @@ export async function saveIndexBundle(options: {
78
77
  metadata: Record<string, unknown>;
79
78
  }): Promise<void> {
80
79
  const { paths, bm25, semantic, chunks, metadata } = options;
81
- await mkdir(paths.root, { recursive: true });
82
80
  await saveBm25(bm25, paths.bm25Index);
83
81
  await saveSemantic(semantic, paths.semanticIndex);
84
82
  await Bun.write(