@takara-ai/miru-code 1.1.3 → 1.2.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,206 @@
1
+ /**
2
+ * Benchmark mode history: JSONL of `{ r, m, g, s, p }` (repo + token deltas, no
3
+ * query text). Appends use `O_APPEND` (no lock). `read_benchmark` sums the file.
4
+ *
5
+ * Path: `<miru-state>/benchmark-history.json`, or `MIRU_BENCHMARK_HISTORY_PATH`.
6
+ * Cleared by `miru benchmark clear` / `miru uninstall`.
7
+ * Tests: `runWithBenchmarkHistoryPath` (AsyncLocalStorage).
8
+ */
9
+
10
+ import { AsyncLocalStorage } from "node:async_hooks";
11
+ import { appendFile, mkdir, rename } from "node:fs/promises";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { resolveMiruStateDir } from "../credentials.ts";
14
+ import { isGitUrl } from "../utils.ts";
15
+ import { agentBenchmarkFromTokens } from "./summary.ts";
16
+ import type { AgentBenchmarkRollup, AgentBenchmarkSummary, SearchBenchmarkBlock } from "./types.ts";
17
+
18
+ const HISTORY_FILENAME = "benchmark-history.json";
19
+ const historyPathStore = new AsyncLocalStorage<string>();
20
+
21
+ /** One JSONL line: r=repo, m=miru, g=grep, s=saved, p=save_pct. */
22
+ export interface BenchmarkContribution {
23
+ r: string;
24
+ m: number;
25
+ g: number;
26
+ s: number;
27
+ p: number;
28
+ }
29
+
30
+ /** Totals for one repo (or the global fold). pct_sum → mean = round(pct_sum / n). */
31
+ export interface TokenTotals {
32
+ n: number;
33
+ miru: number;
34
+ grep: number;
35
+ saved: number;
36
+ pct_sum: number;
37
+ }
38
+
39
+ /** In-memory fold of the JSONL file. */
40
+ export interface BenchmarkHistoryFile extends TokenTotals {
41
+ repos: Record<string, TokenTotals>;
42
+ }
43
+
44
+ export function runWithBenchmarkHistoryPath<T>(path: string, fn: () => Promise<T>): Promise<T> {
45
+ return historyPathStore.run(path, fn);
46
+ }
47
+
48
+ export function resolveBenchmarkHistoryPath(): string {
49
+ return (
50
+ historyPathStore.getStore() ??
51
+ process.env.MIRU_BENCHMARK_HISTORY_PATH?.trim() ??
52
+ join(resolveMiruStateDir(), HISTORY_FILENAME)
53
+ );
54
+ }
55
+
56
+ /** Absolute local path (no trailing slash), or git URL with trailing slashes stripped. */
57
+ export function normalizeBenchmarkRepoKey(repo: string): string {
58
+ const trimmed = repo.trim();
59
+ if (!trimmed) return trimmed;
60
+ if (isGitUrl(trimmed)) return trimmed.replace(/\/+$/, "") || trimmed;
61
+ return resolve(trimmed).replace(/[/\\]+$/, "") || resolve(trimmed);
62
+ }
63
+
64
+ export function recordFromBenchmark(
65
+ repo: string,
66
+ benchmark: SearchBenchmarkBlock,
67
+ ): BenchmarkContribution {
68
+ const summary = agentBenchmarkFromTokens(
69
+ benchmark.miru.workflow_tokens,
70
+ benchmark.grep_read.workflow_full_tokens,
71
+ benchmark.accuracy.rank1_match,
72
+ );
73
+ summary.save_pct = benchmark.efficiency.token_savings_pct;
74
+ return recordFromAgentSummary(repo, summary);
75
+ }
76
+
77
+ export function recordFromAgentSummary(
78
+ repo: string,
79
+ summary: Pick<AgentBenchmarkSummary, "miru_tok" | "grep_tok" | "save_pct" | "saved_tok">,
80
+ ): BenchmarkContribution {
81
+ return {
82
+ r: normalizeBenchmarkRepoKey(repo),
83
+ m: summary.miru_tok,
84
+ g: summary.grep_tok,
85
+ s: summary.saved_tok,
86
+ p: summary.save_pct,
87
+ };
88
+ }
89
+
90
+ function emptyTotals(): TokenTotals {
91
+ return { n: 0, miru: 0, grep: 0, saved: 0, pct_sum: 0 };
92
+ }
93
+
94
+ function isContribution(value: unknown): value is BenchmarkContribution {
95
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
96
+ const row = value as Record<string, unknown>;
97
+ return (
98
+ typeof row.r === "string" &&
99
+ [row.m, row.g, row.s, row.p].every((n) => typeof n === "number" && Number.isFinite(n))
100
+ );
101
+ }
102
+
103
+ function addContribution(totals: TokenTotals, c: BenchmarkContribution): void {
104
+ totals.n += 1;
105
+ totals.miru += c.m;
106
+ totals.grep += c.g;
107
+ totals.saved += c.s;
108
+ totals.pct_sum += c.p;
109
+ }
110
+
111
+ function foldContributions(rows: BenchmarkContribution[]): BenchmarkHistoryFile {
112
+ const history: BenchmarkHistoryFile = { ...emptyTotals(), repos: {} };
113
+ for (const row of rows) {
114
+ addContribution(history, row);
115
+ const repo = history.repos[row.r] ?? emptyTotals();
116
+ addContribution(repo, row);
117
+ history.repos[row.r] = repo;
118
+ }
119
+ return history;
120
+ }
121
+
122
+ function parseContributions(text: string): BenchmarkContribution[] {
123
+ const out: BenchmarkContribution[] = [];
124
+ for (const line of text.split("\n")) {
125
+ if (!line) continue;
126
+ const parsed: unknown = JSON.parse(line);
127
+ if (!isContribution(parsed)) throw new Error("invalid benchmark history line");
128
+ out.push(parsed);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ async function rotateCorruptHistory(path: string): Promise<void> {
134
+ await rename(path, `${path}.bak.${Date.now()}`).catch(() => {});
135
+ }
136
+
137
+ /** Missing/empty → zeros. Bad line → rotate file aside, return zeros. */
138
+ export async function loadBenchmarkHistory(
139
+ path: string = resolveBenchmarkHistoryPath(),
140
+ ): Promise<BenchmarkHistoryFile> {
141
+ if (!(await Bun.file(path).exists())) return { ...emptyTotals(), repos: {} };
142
+ const text = await Bun.file(path).text();
143
+ if (!text.trim()) return { ...emptyTotals(), repos: {} };
144
+ try {
145
+ return foldContributions(parseContributions(text));
146
+ } catch {
147
+ await rotateCorruptHistory(path);
148
+ return { ...emptyTotals(), repos: {} };
149
+ }
150
+ }
151
+
152
+ export async function clearBenchmarkHistory(
153
+ path: string = resolveBenchmarkHistoryPath(),
154
+ ): Promise<{ cleared: boolean; path: string }> {
155
+ if (!(await Bun.file(path).exists())) return { cleared: false, path };
156
+ await Bun.file(path).delete();
157
+ return { cleared: true, path };
158
+ }
159
+
160
+ /** Append one JSONL line (`O_APPEND`). */
161
+ export async function appendBenchmarkQuery(
162
+ contribution: BenchmarkContribution,
163
+ options?: { path?: string },
164
+ ): Promise<void> {
165
+ const path = options?.path ?? resolveBenchmarkHistoryPath();
166
+ await mkdir(dirname(path), { recursive: true });
167
+ await appendFile(path, `${JSON.stringify(contribution)}\n`, "utf8");
168
+ }
169
+
170
+ function totalsToRollup(t: TokenTotals): AgentBenchmarkRollup {
171
+ return {
172
+ n: t.n,
173
+ saved: t.saved,
174
+ save_pct: t.n > 0 ? Math.round(t.pct_sum / t.n) : 0,
175
+ miru: t.miru,
176
+ grep: t.grep,
177
+ };
178
+ }
179
+
180
+ /** Optional `repo` filter. With 2+ repos unfiltered, attach `repos` by saved desc. */
181
+ export function toAgentBenchmarkRollup(
182
+ history: BenchmarkHistoryFile,
183
+ options?: { repo?: string },
184
+ ): AgentBenchmarkRollup {
185
+ if (options?.repo) {
186
+ const key = normalizeBenchmarkRepoKey(options.repo);
187
+ return totalsToRollup(history.repos[key] ?? emptyTotals());
188
+ }
189
+ const out = totalsToRollup(history);
190
+ const entries = Object.entries(history.repos);
191
+ if (entries.length > 1) {
192
+ out.repos = entries
193
+ .map(([r, t]) => ({ r, n: t.n, saved: t.saved, save_pct: totalsToRollup(t).save_pct }))
194
+ .sort((a, b) => b.saved - a.saved);
195
+ }
196
+ return out;
197
+ }
198
+
199
+ export async function readBenchmarkRollup(options?: {
200
+ path?: string;
201
+ repo?: string;
202
+ }): Promise<AgentBenchmarkRollup> {
203
+ return toAgentBenchmarkRollup(await loadBenchmarkHistory(options?.path), {
204
+ repo: options?.repo,
205
+ });
206
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Compare Miru `locate` agent payload tokens vs the costlier native Grep path.
3
+ *
4
+ * Baseline mirrors Cursor/Claude Grep-style output: fixed-string matches with
5
+ * ±GREP_CONTEXT lines (not slim `rg -n`). That is the expensive path agents
6
+ * take without `locate`, so savings measure real workflow cost — not an
7
+ * apples-to-apples payload contest against optimally-minimal ripgrep.
8
+ */
9
+
10
+ import {
11
+ formatLiteralLocate,
12
+ type LiteralLocateOptions,
13
+ type LiteralLocateResult,
14
+ } from "../literal.ts";
15
+ import type { MiruIndex } from "../miru-index.ts";
16
+ import { countTokens } from "../token-count.ts";
17
+ import { GREP_CONTEXT } from "./grep.ts";
18
+ import { rgLiteralOutput } from "./rg-literal.ts";
19
+ import { agentBenchmarkFromTokens } from "./summary.ts";
20
+ import type { AgentBenchmarkSummary } from "./types.ts";
21
+
22
+ export { attachAgentBenchmark as attachLocateBenchmark } from "./summary.ts";
23
+
24
+ export interface LocateBenchmarkComparison {
25
+ result: LiteralLocateResult;
26
+ payload: Record<string, unknown>;
27
+ benchmark: AgentBenchmarkSummary;
28
+ latency_ms: {
29
+ miru: number;
30
+ grep: number;
31
+ };
32
+ }
33
+
34
+ export async function benchmarkLocateComparison(options: {
35
+ literal: string;
36
+ repoPath: string;
37
+ index: MiruIndex;
38
+ locate?: LiteralLocateOptions;
39
+ }): Promise<LocateBenchmarkComparison> {
40
+ const locateOpts: LiteralLocateOptions = {
41
+ mode: options.locate?.mode ?? "lines",
42
+ ignore_case: options.locate?.ignore_case,
43
+ ...(options.locate?.limit != null ? { limit: options.locate.limit } : {}),
44
+ };
45
+
46
+ const miruStart = performance.now();
47
+ const result = options.index.locateLiteral(options.literal, locateOpts);
48
+ const miruMs = performance.now() - miruStart;
49
+ const payload = formatLiteralLocate(result);
50
+ const miruTok = countTokens(JSON.stringify(payload));
51
+
52
+ // Unbounded agent-style Grep dump (±context) — the costlier path without locate.
53
+ const grep = await rgLiteralOutput(options.repoPath, options.literal, {
54
+ context: GREP_CONTEXT,
55
+ maxCount: 0,
56
+ ignoreCase: locateOpts.ignore_case,
57
+ });
58
+
59
+ return {
60
+ result,
61
+ payload,
62
+ benchmark: agentBenchmarkFromTokens(miruTok, grep.tokens, result.n > 0),
63
+ latency_ms: { miru: miruMs, grep: grep.latency_ms },
64
+ };
65
+ }
@@ -0,0 +1,143 @@
1
+ /** Exact-literal ripgrep baseline for locate benchmarking. */
2
+
3
+ import { relative } from "node:path";
4
+ import { countTokens } from "../token-count.ts";
5
+ import { RG_EXCLUDE_ARGS, selectBenchmarkSearchTool } from "./grep.ts";
6
+
7
+ export interface RgLiteralOutput {
8
+ text: string;
9
+ tokens: number;
10
+ latency_ms: number;
11
+ n: number;
12
+ files: number;
13
+ }
14
+
15
+ /** Count match lines (path:line:…) and unique files from `rg -n` output. */
16
+ export function parseRgLiteralStats(text: string, repoRoot: string): { n: number; files: number } {
17
+ const files = new Set<string>();
18
+ let n = 0;
19
+ for (const line of text.split("\n")) {
20
+ if (!line || line.startsWith("--")) {
21
+ continue;
22
+ }
23
+ // path:line:content or path-line-content for context, including Windows drive letters.
24
+ const parsed = parsePathLinePrefix(line);
25
+ if (!parsed) {
26
+ continue;
27
+ }
28
+ files.add(relative(repoRoot, parsed.path).replaceAll("\\", "/"));
29
+ if (line.includes(`${parsed.path}:${parsed.line}:`)) {
30
+ n += 1;
31
+ }
32
+ }
33
+ return { n, files: files.size };
34
+ }
35
+
36
+ function parsePathLinePrefix(line: string): { path: string; line: number } | null {
37
+ for (let i = 0; i < line.length; i++) {
38
+ const delim = line[i];
39
+ if (delim !== ":" && delim !== "-") {
40
+ continue;
41
+ }
42
+ let j = i + 1;
43
+ while (j < line.length && line[j] >= "0" && line[j] <= "9") {
44
+ j++;
45
+ }
46
+ if (j === i + 1 || j >= line.length) {
47
+ continue;
48
+ }
49
+ const trailing = line[j];
50
+ if (trailing !== ":" && trailing !== "-") {
51
+ continue;
52
+ }
53
+ const lineNo = Number(line.slice(i + 1, j));
54
+ if (!Number.isFinite(lineNo)) {
55
+ continue;
56
+ }
57
+ return { path: line.slice(0, i), line: lineNo };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ export async function rgLiteralOutput(
63
+ repoRoot: string,
64
+ literal: string,
65
+ options: {
66
+ context?: number;
67
+ maxCount?: number;
68
+ ignoreCase?: boolean;
69
+ } = {},
70
+ ): Promise<RgLiteralOutput> {
71
+ const context = options.context ?? 0;
72
+ const maxCount = options.maxCount ?? 20;
73
+ const tool = selectBenchmarkSearchTool();
74
+ if (!tool) {
75
+ throw new Error("No search tool found in PATH (tried rg, grep, and findstr on Windows)");
76
+ }
77
+ const start = performance.now();
78
+ const proc = Bun.spawn(
79
+ buildLiteralArgs(tool, repoRoot, literal, context, maxCount, !!options.ignoreCase),
80
+ {
81
+ stdout: "pipe",
82
+ stderr: "pipe",
83
+ ...(tool === "findstr" ? { cwd: repoRoot } : {}),
84
+ },
85
+ );
86
+ const text = await new Response(proc.stdout).text();
87
+ await proc.exited;
88
+ const latency_ms = performance.now() - start;
89
+ const stats = parseRgLiteralStats(text, repoRoot);
90
+ return { text, tokens: countTokens(text), latency_ms, ...stats };
91
+ }
92
+
93
+ function buildLiteralArgs(
94
+ tool: "rg" | "grep" | "findstr",
95
+ repoRoot: string,
96
+ literal: string,
97
+ context: number,
98
+ maxCount: number,
99
+ ignoreCase: boolean,
100
+ ): string[] {
101
+ if (tool === "rg") {
102
+ const args = ["rg", "-F", "-n", "--no-heading", ...RG_EXCLUDE_ARGS];
103
+ if (ignoreCase) {
104
+ args.push("-i");
105
+ }
106
+ if (context > 0) {
107
+ args.push("-C", String(context));
108
+ }
109
+ if (maxCount > 0) {
110
+ args.push("-m", String(maxCount));
111
+ }
112
+ args.push(literal, repoRoot);
113
+ return args;
114
+ }
115
+ if (tool === "grep") {
116
+ const args = [
117
+ "grep",
118
+ "-R",
119
+ "-I",
120
+ "-F",
121
+ "-n",
122
+ "--exclude-dir=node_modules",
123
+ "--exclude-dir=.git",
124
+ "--exclude-dir=tokenizer",
125
+ ];
126
+ if (ignoreCase) {
127
+ args.push("-i");
128
+ }
129
+ if (context > 0) {
130
+ args.push("-C", String(context));
131
+ }
132
+ if (maxCount > 0) {
133
+ args.push("-m", String(maxCount));
134
+ }
135
+ args.push(literal, repoRoot);
136
+ return args;
137
+ }
138
+ const args = ["findstr", "/S", "/N", "/P", `/C:${literal}`, "*"];
139
+ if (ignoreCase) {
140
+ args.splice(4, 0, "/I");
141
+ }
142
+ return args;
143
+ }
@@ -0,0 +1,29 @@
1
+ import type { AgentBenchmarkSummary } from "./types.ts";
2
+
3
+ /** Percent of baseline tokens avoided by Miru (0–100). */
4
+ export function tokenSavingsPct(miruTok: number, grepTok: number): number {
5
+ return grepTok > 0 ? Math.round((1 - miruTok / grepTok) * 100) : 0;
6
+ }
7
+
8
+ /** Compact agent-facing savings from Miru vs Grep token counts. */
9
+ export function agentBenchmarkFromTokens(
10
+ miruTok: number,
11
+ grepTok: number,
12
+ rank1: boolean,
13
+ ): AgentBenchmarkSummary {
14
+ return {
15
+ save_pct: tokenSavingsPct(miruTok, grepTok),
16
+ miru_tok: miruTok,
17
+ grep_tok: grepTok,
18
+ saved_tok: Math.max(0, grepTok - miruTok),
19
+ rank1,
20
+ };
21
+ }
22
+
23
+ /** Attach compact `benchmark` to an MCP tool payload. */
24
+ export function attachAgentBenchmark<T extends Record<string, unknown>>(
25
+ payload: T,
26
+ benchmark: AgentBenchmarkSummary,
27
+ ): T & { benchmark: AgentBenchmarkSummary } {
28
+ return { ...payload, benchmark };
29
+ }
@@ -0,0 +1,65 @@
1
+ export interface SearchBenchmarkBlock {
2
+ mode: true;
3
+ token_count_method: "wordpiece";
4
+ tokenizer_json: string | null;
5
+ miru: {
6
+ search_tokens: number;
7
+ workflow_tokens: number;
8
+ latency_ms: number;
9
+ top_file: string | null;
10
+ top_files: string[];
11
+ };
12
+ grep_read: {
13
+ search_tokens: number;
14
+ read_full_tokens: number;
15
+ read_window_tokens: number;
16
+ workflow_full_tokens: number;
17
+ workflow_window_tokens: number;
18
+ latency_ms: number;
19
+ top_file: string | null;
20
+ top_files: string[];
21
+ pattern: string | null;
22
+ keywords: string[];
23
+ };
24
+ efficiency: {
25
+ token_savings_pct: number;
26
+ baseline: "grep_search_plus_read_full";
27
+ };
28
+ accuracy: {
29
+ rank1_match: boolean;
30
+ top_k_overlap_pct: number;
31
+ miru_only: string[];
32
+ grep_only: string[];
33
+ labeled_recall?: {
34
+ miru: boolean;
35
+ grep: boolean;
36
+ };
37
+ };
38
+ overhead: {
39
+ parallel_total_ms: number;
40
+ miru_share_ms: number;
41
+ grep_share_ms: number;
42
+ };
43
+ }
44
+
45
+ /** Compact per-search stats attached to MCP `search` responses (agent-facing). */
46
+ export interface AgentBenchmarkSummary {
47
+ save_pct: number;
48
+ miru_tok: number;
49
+ grep_tok: number;
50
+ saved_tok: number;
51
+ rank1: boolean;
52
+ /** Present only when non-empty; capped at 3 paths. */
53
+ miru_only?: string[];
54
+ }
55
+
56
+ /** Compact cumulative rollup for MCP `read_benchmark` (agent-facing). */
57
+ export interface AgentBenchmarkRollup {
58
+ n: number;
59
+ saved: number;
60
+ save_pct: number;
61
+ miru: number;
62
+ grep: number;
63
+ /** Only when more than one repo is present. */
64
+ repos?: Array<{ r: string; n: number; saved: number; save_pct: number }>;
65
+ }