@takara-ai/miru-code 1.7.7 → 1.8.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/src/snippet.ts CHANGED
@@ -24,6 +24,15 @@ export function resolveSnippetLines(): number {
24
24
  return envOptionalInt(["MIRU_SNIPPET_LINES"], 3) ?? 15;
25
25
  }
26
26
 
27
+ export function resolveSupportingSnippetLines(): number {
28
+ return envOptionalInt(["MIRU_SUPPORTING_SNIPPET_LINES"], 3) ?? 12;
29
+ }
30
+
31
+ /** Keep normal best hits complete; outline only unusually large implementations. */
32
+ export function resolvePrimarySnippetMaxLines(): number {
33
+ return envOptionalInt(["MIRU_PRIMARY_SNIPPET_MAX_LINES"], 3) ?? 48;
34
+ }
35
+
27
36
  export function searchSnippetsEnabled(): boolean {
28
37
  const value = process.env.MIRU_SEARCH_SNIPPETS;
29
38
  if (value === "0" || value === "false") {
@@ -62,6 +71,306 @@ function scoreLine(line: string, terms: Set<string>): number {
62
71
  return score;
63
72
  }
64
73
 
74
+ const DECLARATION_RE =
75
+ /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|enum|type|struct|trait|impl|record|namespace|contract|library|object)\b|^\s*(?:export\s+)?(?:const|let|var)\s+[$\w]+\s*=\s*[{[(]|^\s*(?:async\s+)?def\s+\w+|^\s*def\s+\w+|^\s*fn\s+\w+|^\s*func\s+\w+|^\s*defmodule\b|^\s*def\w*\b|^\s*(?:(?:public|private|protected|internal|static|final|abstract|virtual|override|sealed|partial|unsafe|extern)\s+)+(?:[A-Za-z_$][\w:<>[\]?.,\s]*\s+)+[A-Za-z_$]\w*\s*\(|^\s*(?:void|bool|boolean|char|short|int|long|float|double|string|[A-Z][\w:<>[\]?.,]*)\s+[A-Za-z_$]\w*\s*\(|^\s*(?:function\s+)?[A-Za-z_]\w*\s*\(\)\s*\{/;
76
+ const BRACE_BODY_RE =
77
+ /\b(?:function|class|interface|enum|struct|trait|impl|record|namespace|contract|library|object|fn|func)\b|=\s*[{[(]|[A-Za-z_$]\w*\s*\([^)]*\)\s*\{/;
78
+ const RUBY_OR_ELIXIR_BODY_RE = /^\s*def\w*\b|^\s*defmodule\b/;
79
+ const MAX_DECLARATION_LOOKBACK = 40;
80
+ const INDENTED_BLOCK_LANGUAGES = new Set(["python", "haskell", "ocaml"]);
81
+ const END_BLOCK_LANGUAGES = new Set(["ruby", "elixir"]);
82
+ const TAG_LANGUAGES = new Set(["html", "embeddedtemplate", "xml", "vue", "svelte"]);
83
+ const BRACE_BLOCK_LANGUAGES = new Set([
84
+ "bash",
85
+ "c",
86
+ "cpp",
87
+ "csharp",
88
+ "css",
89
+ "dart",
90
+ "go",
91
+ "java",
92
+ "javascript",
93
+ "json",
94
+ "kotlin",
95
+ "php",
96
+ "rust",
97
+ "scala",
98
+ "solidity",
99
+ "swift",
100
+ "typescript",
101
+ ]);
102
+ const HTML_TAG_RE = /<\/?([A-Za-z][\w:-]*)\b[^>]*>/g;
103
+ const HTML_VOID_TAGS = new Set([
104
+ "area",
105
+ "base",
106
+ "br",
107
+ "col",
108
+ "embed",
109
+ "hr",
110
+ "img",
111
+ "input",
112
+ "link",
113
+ "meta",
114
+ "param",
115
+ "source",
116
+ "track",
117
+ "wbr",
118
+ ]);
119
+
120
+ function withoutStrings(line: string): string {
121
+ return line.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, "");
122
+ }
123
+
124
+ function indentation(line: string): number {
125
+ return line.length - line.trimStart().length;
126
+ }
127
+
128
+ function completeIndentedBlock(lines: string[], start: number, end: number): [number, number] {
129
+ const baseIndent = indentation(lines[start] ?? "");
130
+ for (let i = Math.max(start + 1, end); i < lines.length; i++) {
131
+ const line = lines[i] ?? "";
132
+ if (line.trim() && indentation(line) <= baseIndent) {
133
+ return [start, i];
134
+ }
135
+ }
136
+ return [start, lines.length];
137
+ }
138
+
139
+ function isIndentedDeclaration(line: string, language: string | null): boolean {
140
+ if (language === "python") {
141
+ return /^\s*(?:async\s+)?(?:def|class)\b/.test(line);
142
+ }
143
+ if (language === "haskell") {
144
+ return /^\s*[A-Za-z_][\w']*\b.*=/.test(line);
145
+ }
146
+ return /^\s*(?:let|type|module)\b/.test(line);
147
+ }
148
+
149
+ function isEndBlockOpen(line: string, language: string | null): boolean {
150
+ const trimmed = line.trimStart();
151
+ if (language === "ruby") {
152
+ return (
153
+ /^(?:def|class|module|if|unless|case|begin|while|until|for)\b/.test(trimmed) ||
154
+ /\bdo\b/.test(trimmed)
155
+ );
156
+ }
157
+ return (
158
+ /^(?:def\w*|defmodule|if|unless|case|cond|receive|try|for|with)\b/.test(trimmed) ||
159
+ /\b(?:do|fn)\b/.test(trimmed)
160
+ );
161
+ }
162
+
163
+ function completeEndBlock(
164
+ lines: string[],
165
+ start: number,
166
+ end: number,
167
+ language: string | null,
168
+ ): [number, number] {
169
+ let depth = 0;
170
+ for (let i = start; i < lines.length; i++) {
171
+ const line = withoutStrings(lines[i] ?? "");
172
+ if (isEndBlockOpen(line, language)) {
173
+ depth++;
174
+ }
175
+ if (/^\s*end\b/.test(line)) {
176
+ depth--;
177
+ }
178
+ if (i + 1 >= end && depth <= 0) {
179
+ return [start, i + 1];
180
+ }
181
+ }
182
+ return [start, lines.length];
183
+ }
184
+
185
+ function completeHtmlTag(
186
+ lines: string[],
187
+ start: number,
188
+ end: number,
189
+ anchor: number,
190
+ ): [number, number] {
191
+ const openTags: Array<{ name: string; start: number }> = [];
192
+ for (let i = Math.max(0, start - MAX_DECLARATION_LOOKBACK); i <= anchor; i++) {
193
+ const line = lines[i] ?? "";
194
+ for (const match of line.matchAll(HTML_TAG_RE)) {
195
+ const name = match[1]?.toLowerCase();
196
+ if (!name) {
197
+ continue;
198
+ }
199
+ if (match[0].startsWith("</")) {
200
+ for (let j = openTags.length - 1; j >= 0; j--) {
201
+ if (openTags[j]?.name === name) {
202
+ openTags.splice(j, 1);
203
+ break;
204
+ }
205
+ }
206
+ } else if (!match[0].endsWith("/>") && !HTML_VOID_TAGS.has(name)) {
207
+ openTags.push({ name, start: i });
208
+ }
209
+ }
210
+ }
211
+ const container = openTags.at(-1);
212
+ if (!container) {
213
+ return [start, end];
214
+ }
215
+ const closeTag = new RegExp(`</${container.name}\\s*>`, "i");
216
+ for (let i = Math.max(end - 1, container.start); i < lines.length; i++) {
217
+ if (closeTag.test(lines[i] ?? "")) {
218
+ return [container.start, i + 1];
219
+ }
220
+ }
221
+ return [container.start, lines.length];
222
+ }
223
+
224
+ /**
225
+ * Keep a query-centred snippet readable without relying on a parser at response time.
226
+ *
227
+ * Chunks are already AST-derived when a grammar is available. This final guard avoids
228
+ * cutting through the declaration/object that contains the selected line: it moves the
229
+ * start to a nearby declaration and extends the end through its closing delimiter.
230
+ * Unknown syntaxes retain the original fixed window.
231
+ */
232
+ function completeStructure(
233
+ lines: string[],
234
+ start: number,
235
+ end: number,
236
+ anchor: number,
237
+ language: string | null,
238
+ ): [number, number] {
239
+ if (TAG_LANGUAGES.has(language ?? "")) {
240
+ return completeHtmlTag(lines, start, end, anchor);
241
+ }
242
+ let structuralStart = start;
243
+ const lookback = Math.max(0, anchor - MAX_DECLARATION_LOOKBACK);
244
+ let foundDeclaration = false;
245
+ for (let i = anchor; i >= lookback; i--) {
246
+ const line = lines[i] ?? "";
247
+ if (DECLARATION_RE.test(line)) {
248
+ structuralStart = i;
249
+ foundDeclaration = true;
250
+ break;
251
+ }
252
+ // Include a doc block only when the candidate already begins inside it.
253
+ if (line.trimStart().startsWith("/**")) {
254
+ structuralStart = i;
255
+ break;
256
+ }
257
+ }
258
+
259
+ if (!foundDeclaration && BRACE_BLOCK_LANGUAGES.has(language ?? "")) {
260
+ for (let i = anchor; i >= lookback; i--) {
261
+ if ((lines[i] ?? "").includes("{")) {
262
+ structuralStart = i;
263
+ break;
264
+ }
265
+ }
266
+ }
267
+
268
+ const declaration = lines[structuralStart] ?? "";
269
+ if (
270
+ INDENTED_BLOCK_LANGUAGES.has(language ?? "") &&
271
+ isIndentedDeclaration(declaration, language)
272
+ ) {
273
+ return completeIndentedBlock(lines, structuralStart, end);
274
+ }
275
+ if (END_BLOCK_LANGUAGES.has(language ?? "") && RUBY_OR_ELIXIR_BODY_RE.test(declaration)) {
276
+ return completeEndBlock(lines, structuralStart, end, language);
277
+ }
278
+ const braceBody = BRACE_BODY_RE.test(declaration);
279
+ const rubyOrElixirBody = RUBY_OR_ELIXIR_BODY_RE.test(declaration);
280
+ let braces = 0;
281
+ let parens = 0;
282
+ let blockComment = false;
283
+ let sawBraceBody = false;
284
+ let rubyEnds = 0;
285
+
286
+ for (let i = structuralStart; i < lines.length; i++) {
287
+ const raw = lines[i] ?? "";
288
+ const line = withoutStrings(raw);
289
+ if (line.includes("/*")) {
290
+ blockComment = true;
291
+ }
292
+ if (line.includes("*/")) {
293
+ blockComment = false;
294
+ }
295
+ braces += (line.match(/{/g) ?? []).length - (line.match(/}/g) ?? []).length;
296
+ parens += (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
297
+ if (line.includes("{")) {
298
+ sawBraceBody = true;
299
+ }
300
+ if (rubyOrElixirBody && /^\s*end\b/.test(line)) {
301
+ rubyEnds++;
302
+ }
303
+
304
+ if (i + 1 < end) {
305
+ continue;
306
+ }
307
+ if (blockComment || parens > 0) {
308
+ continue;
309
+ }
310
+ if (rubyOrElixirBody) {
311
+ if (rubyEnds > 0) {
312
+ return [structuralStart, i + 1];
313
+ }
314
+ continue;
315
+ }
316
+ if (braceBody && (!sawBraceBody || braces > 0)) {
317
+ continue;
318
+ }
319
+ // A non-declaration snippet is still made safe if its window opened a block.
320
+ if (!braceBody && braces > 0) {
321
+ continue;
322
+ }
323
+ return [structuralStart, i + 1];
324
+ }
325
+
326
+ return [structuralStart, lines.length];
327
+ }
328
+
329
+ /**
330
+ * Turn a long supporting hit into an explicit outline instead of emitting a
331
+ * syntactically broken fragment. The best hit remains complete; this is only
332
+ * for lower-ranked context that helps navigation but rarely needs a full body.
333
+ */
334
+ function compactSupportingSnippet(snippet: SnippetResult, maxLines: number): SnippetResult {
335
+ const lines = snippet.chunk.content.split("\n");
336
+ if (lines.length <= maxLines) {
337
+ return snippet;
338
+ }
339
+
340
+ const declarationIndex = lines.findIndex((line) => DECLARATION_RE.test(line));
341
+ const anchorOffset = Math.max(
342
+ 0,
343
+ Math.min(lines.length - 1, snippet.meta.anchor_line - snippet.chunk.start_line),
344
+ );
345
+ const declaration =
346
+ declarationIndex >= 0
347
+ ? lines
348
+ .slice(declarationIndex, Math.min(lines.length, declarationIndex + 4))
349
+ .join("\n")
350
+ .trim()
351
+ : null;
352
+ const anchor = (lines[anchorOffset] ?? "").trim();
353
+ const outline = [
354
+ "[Supporting context — complete outline]",
355
+ declaration ? `Symbol:\n${declaration}` : null,
356
+ anchor ? `Matched line ${snippet.meta.anchor_line}: ${anchor}` : null,
357
+ "Implementation omitted; call expand only if you need its full body.",
358
+ ]
359
+ .filter((line): line is string => line !== null)
360
+ .join("\n");
361
+
362
+ return {
363
+ chunk: {
364
+ ...snippet.chunk,
365
+ content: outline,
366
+ },
367
+ meta: {
368
+ ...snippet.meta,
369
+ truncated: true,
370
+ },
371
+ };
372
+ }
373
+
65
374
  /** Pick the 0-based line index inside `content` that best matches the query. */
66
375
  export function anchorLineOffset(content: string, query: string): number {
67
376
  const lines = content.split("\n");
@@ -89,6 +398,7 @@ export function trimChunkToSnippet(
89
398
  chunk: Chunk,
90
399
  query: string,
91
400
  linesEachSide = resolveSnippetLines(),
401
+ anchorLine?: number,
92
402
  ): SnippetResult {
93
403
  const lines = chunk.content.split("\n");
94
404
  if (lines.length === 0) {
@@ -103,9 +413,20 @@ export function trimChunkToSnippet(
103
413
  };
104
414
  }
105
415
 
106
- const anchorOffset = anchorLineOffset(chunk.content, query);
107
- const startOffset = Math.max(0, anchorOffset - linesEachSide);
108
- const endOffset = Math.min(lines.length, anchorOffset + linesEachSide + 1);
416
+ const queryAnchorOffset = anchorLineOffset(chunk.content, query);
417
+ const anchorOffset =
418
+ anchorLine == null
419
+ ? queryAnchorOffset
420
+ : Math.max(0, Math.min(lines.length - 1, anchorLine - chunk.start_line));
421
+ const requestedStart = Math.max(0, anchorOffset - linesEachSide);
422
+ const requestedEnd = Math.min(lines.length, anchorOffset + linesEachSide + 1);
423
+ const [startOffset, endOffset] = completeStructure(
424
+ lines,
425
+ requestedStart,
426
+ requestedEnd,
427
+ anchorOffset,
428
+ chunk.language,
429
+ );
109
430
  const truncated = startOffset > 0 || endOffset < lines.length;
110
431
 
111
432
  if (!truncated) {
@@ -141,11 +462,27 @@ export function applySnippetsToResults(
141
462
  results: SearchResult[],
142
463
  query: string,
143
464
  linesEachSide?: number,
465
+ sourceChunks?: ReadonlyMap<string, Chunk>,
144
466
  ): Array<{ result: SearchResult; meta: SnippetMeta }> {
145
467
  const radius = linesEachSide ?? resolveSnippetLines();
146
- return results.map((result) => {
147
- const { chunk, meta } = trimChunkToSnippet(result.chunk, query, radius);
148
- return { result: { chunk, score: result.score }, meta };
468
+ const supportingMaxLines = resolveSupportingSnippetLines();
469
+ const primaryMaxLines = resolvePrimarySnippetMaxLines();
470
+ const contextualResults = results.map((result) => {
471
+ const source = sourceChunks?.get(result.chunk.file_path);
472
+ return {
473
+ result: source ? { ...result, chunk: source } : result,
474
+ // Select the anchor from the ranked chunk, not from the full source file.
475
+ // The same query may occur elsewhere in that file.
476
+ anchorLine: result.chunk.start_line + anchorLineOffset(result.chunk.content, query),
477
+ };
478
+ });
479
+ return contextualResults.map(({ result, anchorLine }, rank) => {
480
+ const { chunk, meta } = trimChunkToSnippet(result.chunk, query, radius, anchorLine);
481
+ const snippet = compactSupportingSnippet(
482
+ { chunk, meta },
483
+ rank === 0 ? primaryMaxLines : supportingMaxLines,
484
+ );
485
+ return { result: { chunk: snippet.chunk, score: result.score }, meta: snippet.meta };
149
486
  });
150
487
  }
151
488
 
package/src/utils.ts CHANGED
@@ -196,11 +196,17 @@ export function formatRelevanceScore(score: number, maxScore: number): string {
196
196
  export function formatResults(
197
197
  query: string,
198
198
  results: SearchResult[],
199
- options?: { repoRoot?: string | null; snippet?: boolean },
199
+ options?: {
200
+ repoRoot?: string | null;
201
+ snippet?: boolean;
202
+ snippetSourceChunks?: ReadonlyMap<string, Chunk>;
203
+ },
200
204
  ): { query: string; results: Record<string, unknown>[] } {
201
205
  const repoRoot = options?.repoRoot ?? null;
202
206
  const useSnippet = options?.snippet ?? searchSnippetsEnabled();
203
- const payload = useSnippet ? applySnippetsToResults(results, query) : null;
207
+ const payload = useSnippet
208
+ ? applySnippetsToResults(results, query, undefined, options?.snippetSourceChunks)
209
+ : null;
204
210
  const maxScore = results.reduce((max, result) => Math.max(max, result.score), 0);
205
211
 
206
212
  return {
package/src/version.ts CHANGED
@@ -9,6 +9,8 @@ const UPDATE_CHECK_FILENAME = "update-check.json";
9
9
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10
10
  const FETCH_TIMEOUT_MS = 2_000;
11
11
 
12
+ type Fetcher = (input: string, init?: RequestInit) => Promise<Response>;
13
+
12
14
  interface UpdateCheckCache {
13
15
  checkedAt: number;
14
16
  latest: string;
@@ -93,8 +95,8 @@ async function writeUpdateCheckCache(latest: string): Promise<void> {
93
95
  await Bun.write(updateCheckPath(), `${JSON.stringify(payload)}\n`);
94
96
  }
95
97
 
96
- export async function fetchLatestPublishedVersion(): Promise<string> {
97
- const response = await fetch(REGISTRY_URL, {
98
+ export async function fetchLatestPublishedVersion(fetchImpl: Fetcher = fetch): Promise<string> {
99
+ const response = await fetchImpl(REGISTRY_URL, {
98
100
  headers: { accept: "application/json" },
99
101
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
100
102
  });