Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Compact-mode defaults**: `files_search` now defaults to `compact: true`, matching `context_bundle` so previews are omitted unless explicitly requested.
- **Why tag control**: `context_bundle` compact responses omit `why[]` arrays by default to save ~170 bytes per item; callers can pass `includeWhy: true` to retain them.
- **Preview clamp**: `files_search` fallback previews are truncated to 100 characters (was 240) for additional token savings.

## [0.25.8] - 2025-12-30

### Fixed
Expand Down
6 changes: 4 additions & 2 deletions docs/api-and-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@ for (const item of result.context.slice(0, 3)) {

| Mode | Tokens | Information Included |
| ---------------------------------- | ------- | ------------------------------------ |
| `compact: true` (default, v0.8.0+) | ~2,500 | path, range, why, score |
| `compact: true` (default, v0.8.0+) | ~2,500 | path, range, why\*, score |
| `compact: false` | ~55,000 | path, range, why, score, **preview** |

`*` Pass `includeWhy: true` if you still need `why[]` explanations in compact mode.

**Reduction: 95%**

### Usage Guidelines
Expand All @@ -121,7 +123,7 @@ for (const item of result.context.slice(0, 3)) {

### Lightweight inspection options

- `files_search(..., compact: true)` removes previews from every result for 60–70% fewer tokens during keyword scans. Use `compact: false` only when the preview text is required inline.
- `files_search(..., compact: true)` (now the default) removes previews from every result for 60–70% fewer tokens during keyword scans. Use `compact: false` only when the preview text is required inline.
- `snippets_get(..., compact: true)` returns only metadata (`path`, `startLine`, `endLine`, totals, symbol info) so that you can confirm the symbol boundaries without streaming full text.
- `snippets_get(..., includeLineNumbers: true)` prefixes each returned line with an aligned counter such as ` 1375→export async function...`, making it easier to quote exact locations when copying into bug reports or chats.

Expand Down
21 changes: 11 additions & 10 deletions docs/tools-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The most powerful tool for getting started with unfamiliar code. Provide a task
| `goal` | string | Yes | - | Task description or question about the code |
| `limit` | number | No | 7 | Max snippets to return (max: 20) |
| `compact` | boolean | No | true | Return only metadata without preview |
| `includeWhy` | boolean | No | false | Keep `why[]` even when compact mode is used |
| `boost_profile` | string | No | "default" | File type boosting mode |
| `path_prefix` | string | No | - | Filter by path prefix |
| `category` | string | No | - | Query category for adaptive K |
Expand Down Expand Up @@ -100,16 +101,16 @@ Fast search across all indexed files with BM25 ranking.

### Parameters

| Parameter | Type | Required | Default | Description |
| ------------------ | ------- | -------- | --------- | --------------------------- |
| `query` | string | Yes | - | Search keywords or phrase |
| `limit` | number | No | 50 | Max results (max: 200) |
| `lang` | string | No | - | Filter by language |
| `ext` | string | No | - | Filter by extension |
| `path_prefix` | string | No | - | Filter by path prefix |
| `boost_profile` | string | No | "default" | File type boosting mode |
| `compact` | boolean | No | false | Omit previews |
| `metadata_filters` | object | No | - | Filter by document metadata |
| Parameter | Type | Required | Default | Description |
| ------------------ | ------- | -------- | --------- | -------------------------------- |
| `query` | string | Yes | - | Search keywords or phrase |
| `limit` | number | No | 50 | Max results (max: 200) |
| `lang` | string | No | - | Filter by language |
| `ext` | string | No | - | Filter by extension |
| `path_prefix` | string | No | - | Filter by path prefix |
| `boost_profile` | string | No | "default" | File type boosting mode |
| `compact` | boolean | No | true | Omit previews unless you opt out |
| `metadata_filters` | object | No | - | Filter by document metadata |

### Query Syntax

Expand Down
5 changes: 5 additions & 0 deletions src/server/compact-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const DEFAULT_COMPACT_MODE = true;

export function resolveCompactFlag(value: boolean | undefined): boolean {
return value ?? DEFAULT_COMPACT_MODE;
}
30 changes: 19 additions & 11 deletions src/server/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ export interface ContextBundleParams {
profile?: string;
boost_profile?: BoostProfileName;
compact?: boolean; // If true, omit preview field to reduce token usage
includeWhy?: boolean; // When true, keep why tags even in compact mode
includeTokensEstimate?: boolean; // If true, compute tokens_estimate (slower)
metadata_filters?: Record<string, string | string[]>;
requestId?: string; // Optional request ID for tracing/debugging
Expand All @@ -461,7 +462,7 @@ export interface ContextBundleItem {
path: string;
range: [number, number];
preview?: string; // Omitted when compact: true
why: string[];
why?: string[]; // Omitted when compact mode suppresses reasons
score: number;
}

Expand Down Expand Up @@ -1122,21 +1123,28 @@ function normalizeLimit(limit?: number): number {
return Math.min(Math.max(1, Math.floor(limit)), 100);
}

const PREVIEW_MATCH_WINDOW = 120;
const PREVIEW_MAX_LENGTH = 100;

function buildPreview(content: string, query: string): { preview: string; line: number } {
const lowerContent = content.toLowerCase();
const lowerQuery = query.toLowerCase();
const index = lowerContent.indexOf(lowerQuery);
if (index === -1) {
return { preview: content.slice(0, 240), line: 1 };
return { preview: content.slice(0, PREVIEW_MAX_LENGTH), line: 1 };
}

const prefix = content.slice(0, index);
const prefixLines = prefix.split(/\r?\n/);
const matchLine = prefix.length === 0 ? 1 : prefixLines.length;

const snippetStart = Math.max(0, index - 120);
const snippetEnd = Math.min(content.length, index + query.length + 120);
const preview = content.slice(snippetStart, snippetEnd);
const snippetStart = Math.max(0, index - PREVIEW_MATCH_WINDOW);
const snippetEnd = Math.min(content.length, index + query.length + PREVIEW_MATCH_WINDOW);
const previewSlice = content.slice(snippetStart, snippetEnd);
const preview =
previewSlice.length > PREVIEW_MAX_LENGTH
? `${previewSlice.slice(0, PREVIEW_MAX_LENGTH - 1)}…`
: previewSlice;

return { preview, line: matchLine };
}
Expand Down Expand Up @@ -2332,15 +2340,17 @@ function selectSnippet(snippets: SnippetRow[], matchLine: number | null): Snippe
return lastSnippet ?? firstSnippet;
}

const SNIPPET_PREVIEW_MAX_LENGTH = 240;

function buildSnippetPreview(content: string, startLine: number, endLine: number): string {
const lines = content.split(/\r?\n/);
const startIndex = Math.max(0, Math.min(startLine - 1, lines.length));
const endIndex = Math.max(startIndex, Math.min(endLine, lines.length));
const snippet = lines.slice(startIndex, endIndex).join("\n");
if (snippet.length <= 240) {
if (snippet.length <= SNIPPET_PREVIEW_MAX_LENGTH) {
return snippet;
}
return `${snippet.slice(0, 239)}…`;
return `${snippet.slice(0, SNIPPET_PREVIEW_MAX_LENGTH - 1)}…`;
}

/**
Expand Down Expand Up @@ -5080,14 +5090,12 @@ async function contextBundleImpl(

const roundedScore = Number.isFinite(normalizedScore) ? Number(normalizedScore.toFixed(3)) : 0;

// Select why tags with diversity guarantee (reserves slots for dep/symbol/near)
const why = selectWhyTags(reasons);

const shouldIncludeWhy = params.includeWhy ?? !isCompact;
const item: ContextBundleItem = {
path: candidate.path,
range: [startLine, endLine],
why,
score: roundedScore,
...(shouldIncludeWhy ? { why: selectWhyTags(reasons) } : {}),
};

// Add preview only if not in compact mode
Expand Down
5 changes: 4 additions & 1 deletion src/server/output-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export const ContextBundleItemSchema = z.object({
path: z.string().describe("ファイルパス"),
range: z.tuple([z.number(), z.number()]).describe("行範囲 [start, end]"),
preview: z.string().optional().describe("コードプレビュー(compact=falseの場合)"),
why: z.array(z.string()).describe("スコアリング理由"),
why: z
.array(z.string())
.optional()
.describe("スコアリング理由(compact=false または includeWhy=true の場合)"),
score: z.number().describe("関連度スコア"),
});

Expand Down
33 changes: 19 additions & 14 deletions src/server/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
} from "../shared/adaptive-k-categories.js";
import { maskValue } from "../shared/security/masker.js";

import { resolveCompactFlag } from "./compact-mode.js";
import { isValidBoostProfile, BOOST_PROFILES } from "./boost-profiles.js";

Check warning on line 12 in src/server/rpc.ts

View workflow job for this annotation

GitHub Actions / build

`./boost-profiles.js` import should occur before import of `./compact-mode.js`
import { ServerContext } from "./context.js";
import { DegradeController } from "./fallbacks/degradeController.js";
import {
Expand Down Expand Up @@ -452,9 +453,8 @@
params.boost_profile = selectProfileFromQuery(params.query, "default");
}

if (typeof record.compact === "boolean") {
params.compact = record.compact;
}
const compactValue = typeof record.compact === "boolean" ? record.compact : undefined;
params.compact = resolveCompactFlag(compactValue);

if (record.metadata_filters && typeof record.metadata_filters === "object") {
params.metadata_filters = record.metadata_filters as Record<string, string | string[]>;
Expand Down Expand Up @@ -624,23 +624,25 @@
params.path_prefix = normalizedPrefix;
}

// Parse compact parameter (default: true for token efficiency)
if (typeof record.compact === "boolean") {
params.compact = record.compact;
} else {
params.compact = true; // Default to compact mode (v0.8.0+: breaking change)

// Show one-time warning about breaking change using WarningManager
// forResponse: true adds this warning to the API response
const compactValue = typeof record.compact === "boolean" ? record.compact : undefined;
params.compact = resolveCompactFlag(compactValue);
if (compactValue === undefined) {
context.warningManager.warnOnce(
"compact-default-v0.8.0",
"BREAKING CHANGE (v0.8.0): compact mode is now default. " +
"Set compact: false to restore previous behavior. " +
"See CHANGELOG.md for details.",
true // Add to API response
true
);
}

const includeWhyValue = record.includeWhy ?? record.include_why;
if (typeof includeWhyValue === "boolean") {
params.includeWhy = includeWhyValue;
} else {
params.includeWhy = false;
}

const includeTokensEstimate = record.includeTokensEstimate ?? record.include_tokens_estimate;
if (typeof includeTokensEstimate === "boolean") {
params.includeTokensEstimate = includeTokensEstimate;
Expand Down Expand Up @@ -771,8 +773,11 @@
case "files_search": {
const params = parseFilesSearchParams(toolParams);
if (degrade.current.active && allowDegrade) {
// Use same output option logic as normal mode for consistency
const includePreview = params.compact !== true;
// When DuckDB is unavailable, keep previews to preserve readability regardless of compact
const includePreview =
toolParams && typeof (toolParams as Record<string, unknown>).compact === "boolean"
? params.compact !== true
: true;
const results = degrade.search(params.query, params.limit ?? 20).map((hit) => {
const result = {
path: hit.path,
Expand Down
19 changes: 11 additions & 8 deletions tests/indexer/fts-incremental.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/

import { existsSync, unlinkSync } from "node:fs";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

Expand Down Expand Up @@ -234,7 +234,6 @@ describe("FTS incremental rebuild via IndexWatcher (Issue #158)", () => {
"src/initial.ts": ["export const initial = 'value';"].join("\n"),
});
cleanupTargets.push({ dispose: repo.cleanup });

const testId = Math.random().toString(36).substring(7);
const dbDir = await mkdtemp(join(tmpdir(), `kiri-fts-watch-${testId}-`));
const dbPath = join(dbDir, "index.duckdb");
Expand Down Expand Up @@ -331,6 +330,13 @@ describe("FTS incremental rebuild via IndexWatcher (Issue #158)", () => {
});
cleanupTargets.push({ dispose: repo.cleanup });

const { execa } = await import("execa");
const fixturesDir = join(repo.path, "tests/fixtures");
await mkdir(fixturesDir, { recursive: true });
await writeFile(join(fixturesDir, ".gitkeep"), "");
await execa("git", ["add", "tests/fixtures/.gitkeep"], { cwd: repo.path });
await execa("git", ["commit", "-m", "Prepare fixtures directory"], { cwd: repo.path });

const testId = Math.random().toString(36).substring(7);
const dbDir = await mkdtemp(join(tmpdir(), `kiri-fts-gherkin-${testId}-`));
const dbPath = join(dbDir, "index.duckdb");
Expand Down Expand Up @@ -375,17 +381,14 @@ describe("FTS incremental rebuild via IndexWatcher (Issue #158)", () => {
When I perform an action
Then I should see a result
`;
const { mkdir } = await import("node:fs/promises");
await mkdir(join(repo.path, "tests/fixtures"), { recursive: true });
await writeFile(join(repo.path, "tests/fixtures/test.feature"), gherkinContent);

// 5. ファイルをgitにステージしてコミット
const { execa } = await import("execa");
await execa("git", ["add", "tests/fixtures/test.feature"], { cwd: repo.path });
await execa("git", ["commit", "-m", "Add gherkin feature file"], { cwd: repo.path });

// 6. Watcherがリインデックスを完了するまで待機
await waitForCondition(() => watcher.getStatistics().reindexCount >= 1);
await waitForCondition(() => watcher.getStatistics().reindexCount >= 1, 30_000);

// 7. DB接続してFTS検索
const db = await DuckDBClient.connect({ databasePath: dbPath });
Expand All @@ -403,7 +406,7 @@ describe("FTS incremental rebuild via IndexWatcher (Issue #158)", () => {
db.all<{ hash: string; content: string }>(
"SELECT hash, content FROM blob WHERE content LIKE '%GHERKINUNIQUE158%'"
);
await waitForCondition(async () => (await fetchBlobResults()).length > 0);
await waitForCondition(async () => (await fetchBlobResults()).length > 0, 30_000);
const blobResults = await fetchBlobResults();
expect(blobResults.length).toBeGreaterThan(0);

Expand All @@ -419,5 +422,5 @@ describe("FTS incremental rebuild via IndexWatcher (Issue #158)", () => {

// Issue #158: FTS検索で新規追加されたblobが見つかることを確認
expect(ftsResults.length).toBeGreaterThan(0);
}, 30000);
}, 45000);
});
Loading
Loading