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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added inclusive `blameUntil` retrieval filters across search, peek, and find-similar tools. Temporal bounds now constrain native vector and keyword candidate retrieval using SQLite blame timestamps instead of filtering only after candidate generation.
- Added an Ollama-only representative retrieval evaluation command with expanded coverage for embedding failure recovery, index-lock recovery, and duplicate-definition path disambiguation.
- Added `--embedding-model` to the cross-repository benchmark runner, preserving `nomic-embed-text` as the default while recording the selected local Ollama model in its artifacts.
- Expanded the frozen mixed-intent cross-repository cohort to 100 reviewed queries across JavaScript, Python, Go, Rust, Java, C#, PHP, and Ruby, with additional hard Ruby and C# cases.
Expand Down
3 changes: 2 additions & 1 deletion commands/peek.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ The first part is the search query. Look for optional parameters:
- `author=X` or `blameAuthor=X` → set blameAuthor filter
- `sha=X` or `blameSha=X` → set blameSha filter
- `since=YYYY-MM-DD` or `blameSince=YYYY-MM-DD` → set blameSince filter
- `until=YYYY-MM-DD` or `blameUntil=YYYY-MM-DD` → set blameUntil filter

Call `codebase_peek` with the parsed arguments.

Examples:
- `/peek authentication logic` → query="authentication logic"
- `/peek error handling limit=5` → query="error handling", limit=5
- `/peek validation functions` → query="validation", chunkType="function"
- `/peek auth logic author=jane@example.com since=2025-01-01` → query="auth logic", blameAuthor="jane@example.com", blameSince="2025-01-01"
- `/peek auth logic author=jane@example.com since=2025-01-01 until=2025-01-31` → query="auth logic", blameAuthor="jane@example.com", blameSince="2025-01-01", blameUntil="2025-01-31"

If the index doesn't exist, run `index_codebase` first.

Expand Down
3 changes: 2 additions & 1 deletion commands/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ The first part is the search query. Look for optional parameters:
- `author=X` or `blameAuthor=X` → set blameAuthor filter
- `sha=X` or `blameSha=X` → set blameSha filter
- `since=YYYY-MM-DD` or `blameSince=YYYY-MM-DD` → set blameSince filter
- `until=YYYY-MM-DD` or `blameUntil=YYYY-MM-DD` → set blameUntil filter

Call `codebase_search` with the parsed arguments.

Examples:
- `/search authentication logic` → query="authentication logic"
- `/search error handling limit=5` → query="error handling", limit=5
- `/search validation functions` → query="validation", chunkType="function"
- `/search auth logic author=jane@example.com since=2025-01-01` → query="auth logic", blameAuthor="jane@example.com", blameSince="2025-01-01"
- `/search auth logic author=jane@example.com since=2025-01-01 until=2025-01-31` → query="auth logic", blameAuthor="jane@example.com", blameSince="2025-01-01", blameUntil="2025-01-31"

If the index doesn't exist, run `index_codebase` first.

Expand Down
12 changes: 12 additions & 0 deletions native/src/bindings/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,18 @@ impl Database {
})
}

#[napi]
pub fn get_chunk_ids_by_blame_date(
&self,
since: Option<i64>,
until: Option<i64>,
) -> Result<Vec<String>> {
self.with_conn(|conn| {
db::get_chunk_ids_by_blame_date(conn, since, until)
.map_err(|e| Error::from_reason(e.to_string()))
})
}

#[napi]
pub fn get_branch_delta(&self, branch: String, base_branch: String) -> Result<BranchDelta> {
self.with_conn(|conn| {
Expand Down
25 changes: 25 additions & 0 deletions native/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,31 @@ pub fn get_branch_chunk_ids(conn: &Connection, branch: &str) -> DbResult<Vec<Str
Ok(results)
}

/// Get chunk IDs whose blame commit timestamp is within the inclusive bounds.
/// A temporal filter excludes chunks without blame metadata.
pub fn get_chunk_ids_by_blame_date(
conn: &Connection,
since: Option<i64>,
until: Option<i64>,
) -> DbResult<Vec<String>> {
let mut stmt = conn.prepare(
r#"
SELECT chunk_id FROM chunks
WHERE blame_committed_at IS NOT NULL
AND (?1 IS NULL OR blame_committed_at >= ?1)
AND (?2 IS NULL OR blame_committed_at <= ?2)
ORDER BY chunk_id
"#,
)?;
let rows = stmt.query_map(params![since, until], |row| row.get::<_, String>(0))?;

let mut results = Vec::new();
for row in rows {
results.push(row?);
}
Ok(results)
}

/// Get chunks that exist on branch A but not on branch B (delta)
pub fn get_branch_delta(
conn: &Connection,
Expand Down
13 changes: 13 additions & 0 deletions native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,19 @@ impl VectorStore {
.map_err(|e| Error::from_reason(e.to_string()))
}

#[napi]
pub fn search_filtered(
&self,
query_vector: Vec<f64>,
limit: u32,
allowed_ids: Vec<String>,
) -> Result<Vec<SearchResult>> {
let query_f32: Vec<f32> = query_vector.iter().map(|&x| x as f32).collect();
self.inner
.search_filtered(&query_f32, limit as usize, &allowed_ids)
.map_err(|e| Error::from_reason(e.to_string()))
}

#[napi]
pub fn remove(&mut self, id: String) -> Result<bool> {
self.inner
Expand Down
55 changes: 53 additions & 2 deletions native/src/store.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{hasher::xxhash_file, SearchResult};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -240,6 +240,32 @@ impl VectorStoreInner {
}

pub fn search(&self, query_vector: &[f32], limit: usize) -> Result<Vec<SearchResult>> {
self.search_with_allowed_keys(query_vector, limit, None)
}

pub fn search_filtered(
&self,
query_vector: &[f32],
limit: usize,
allowed_keys: &[String],
) -> Result<Vec<SearchResult>> {
let allowed_ids = allowed_keys
.iter()
.filter_map(|key| self.stored.key_to_id.get(key).copied())
.collect::<HashSet<_>>();
if allowed_ids.is_empty() || limit == 0 {
return Ok(Vec::new());
}

self.search_with_allowed_keys(query_vector, limit, Some(&allowed_ids))
}

fn search_with_allowed_keys(
&self,
query_vector: &[f32],
limit: usize,
allowed_ids: Option<&HashSet<u64>>,
) -> Result<Vec<SearchResult>> {
if query_vector.len() != self.dimensions {
return Err(anyhow!(
"Query vector dimension mismatch: expected {}, got {}",
Expand All @@ -248,7 +274,12 @@ impl VectorStoreInner {
));
}

let results = self.index.search(query_vector, limit)?;
let results = match allowed_ids {
Some(ids) => self
.index
.filtered_search(query_vector, limit, |id| ids.contains(&id))?,
None => self.index.search(query_vector, limit)?,
};

let mut search_results = Vec::with_capacity(results.keys.len());

Expand Down Expand Up @@ -525,6 +556,26 @@ mod tests {
assert_eq!(results[0].id, "vec1");
}

#[test]
fn test_vector_store_filtered_search() {
let dir = tempdir().unwrap();
let index_path = dir.path().join("test.usearch");
let mut store = VectorStoreInner::new(index_path, 3).unwrap();

store.add("closest", &[1.0, 0.0, 0.0], "meta1").unwrap();
store.add("allowed", &[0.0, 1.0, 0.0], "meta2").unwrap();

let results = store
.search_filtered(&[1.0, 0.0, 0.0], 1, &["allowed".to_string()])
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "allowed");
assert!(store
.search_filtered(&[1.0, 0.0, 0.0], 1, &[])
.unwrap()
.is_empty());
}

#[test]
fn test_vector_store_persistence() {
let dir = tempdir().unwrap();
Expand Down
5 changes: 3 additions & 2 deletions skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ Find code with full content. Use when you need to see implementation.

```
codebase_search(query="error handling middleware", fileType="ts", contextLines=2)
codebase_search(query="rate limiter", blameSince="2025-01-01")
codebase_search(query="rate limiter", blameSince="2025-01-01", blameUntil="2025-01-31")
```

### `find_similar`
Find code similar to a given snippet. Use for duplicate detection, pattern discovery, refactoring.

```
find_similar(code="function validate(input) { return input.length > 0; }", excludeFile="src/current.ts")
find_similar(code="function validate(input) { return input.length > 0; }", excludeFile="src/current.ts", blameSince="2025-01-01")
```

### `call_graph`
Expand Down Expand Up @@ -137,3 +137,4 @@ remove_knowledge_base(path="/path/to/docs")
| `blameAuthor` | `"jane@example.com"` or `"Jane Doe"` |
| `blameSha` | `"abc1234"` |
| `blameSince` | `"2025-01-01"` |
| `blameUntil` | `"2025-01-31"` |
8 changes: 8 additions & 0 deletions src/adapters/mcp/register-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export function registerMcpTools(server: McpServer, runtime: McpServerRuntime):
blameAuthor: allowNullAsUndefined(z.string().optional()).describe("Filter by git blame author name or email"),
blameSha: allowNullAsUndefined(z.string().optional()).describe("Filter by git blame commit SHA or prefix"),
blameSince: allowNullAsUndefined(z.string().optional()).describe("Filter to chunks last changed on or after this date"),
blameUntil: allowNullAsUndefined(z.string().optional()).describe("Filter to chunks last changed on or before this date"),
},
async (args) => {
return searchCodebaseWithEffectiveness(runtime.projectRoot, runtime.host, "search", args.query, {
Expand All @@ -136,6 +137,7 @@ export function registerMcpTools(server: McpServer, runtime: McpServerRuntime):
blameAuthor: args.blameAuthor,
blameSha: args.blameSha,
blameSince: args.blameSince,
blameUntil: args.blameUntil,
}, (results) => {
const text = results.length === 0
? "No matching code found. Try a different query or run index_codebase first."
Expand All @@ -157,6 +159,7 @@ export function registerMcpTools(server: McpServer, runtime: McpServerRuntime):
blameAuthor: allowNullAsUndefined(z.string().optional()).describe("Filter by git blame author name or email"),
blameSha: allowNullAsUndefined(z.string().optional()).describe("Filter by git blame commit SHA or prefix"),
blameSince: allowNullAsUndefined(z.string().optional()).describe("Filter to chunks last changed on or after this date"),
blameUntil: allowNullAsUndefined(z.string().optional()).describe("Filter to chunks last changed on or before this date"),
},
async (args) => {
return searchCodebaseWithEffectiveness(runtime.projectRoot, runtime.host, "peek", args.query, {
Expand All @@ -168,6 +171,7 @@ export function registerMcpTools(server: McpServer, runtime: McpServerRuntime):
blameAuthor: args.blameAuthor,
blameSha: args.blameSha,
blameSince: args.blameSince,
blameUntil: args.blameUntil,
}, (results) => {
const text = results.length === 0
? "No matching code found. Try a different query or run index_codebase first."
Expand Down Expand Up @@ -251,6 +255,8 @@ export function registerMcpTools(server: McpServer, runtime: McpServerRuntime):
directory: allowNullAsUndefined(z.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
chunkType: allowNullAsUndefined(z.enum(CHUNK_TYPES).optional()).describe("Filter by code chunk type"),
excludeFile: allowNullAsUndefined(z.string().optional()).describe("Exclude results from this file path"),
blameSince: allowNullAsUndefined(z.string().optional()).describe("Filter to chunks last changed on or after this date"),
blameUntil: allowNullAsUndefined(z.string().optional()).describe("Filter to chunks last changed on or before this date"),
},
async (args) => {
const results = await findSimilarCode(runtime.projectRoot, runtime.host, args.code, {
Expand All @@ -259,6 +265,8 @@ export function registerMcpTools(server: McpServer, runtime: McpServerRuntime):
directory: args.directory,
chunkType: args.chunkType,
excludeFile: args.excludeFile,
blameSince: args.blameSince,
blameUntil: args.blameUntil,
});

if (results.length === 0) {
Expand Down
8 changes: 8 additions & 0 deletions src/adapters/opencode/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export const codebase_peek: ToolDefinition = tool({
blameAuthor: z.string().optional().describe("Filter by git blame author name or email"),
blameSha: z.string().optional().describe("Filter by git blame commit SHA or prefix"),
blameSince: z.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)"),
blameUntil: z.string().optional().describe("Filter to chunks last changed on or before this date (e.g., 2025-01-31)"),
},
async execute(args, context) {
return searchCodebaseWithEffectiveness(context?.worktree, DEFAULT_HOST, "peek", args.query, {
Expand All @@ -177,6 +178,7 @@ export const codebase_peek: ToolDefinition = tool({
blameAuthor: args.blameAuthor,
blameSha: args.blameSha,
blameSince: args.blameSince,
blameUntil: args.blameUntil,
}, (results) => {
const text = formatCodebasePeek(results);
return { output: text, text };
Expand Down Expand Up @@ -251,6 +253,8 @@ export const find_similar: ToolDefinition = tool({
directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
chunkType: z.enum(CHUNK_TYPE_VALUES).optional().describe("Filter by code chunk type"),
excludeFile: z.string().optional().describe("Exclude results from this file path (useful when searching for duplicates of code from a specific file)"),
blameSince: z.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)"),
blameUntil: z.string().optional().describe("Filter to chunks last changed on or before this date (e.g., 2025-01-31)"),
},
async execute(args, context) {
const results = await findSimilarCode(context?.worktree, DEFAULT_HOST, args.code, {
Expand All @@ -259,6 +263,8 @@ export const find_similar: ToolDefinition = tool({
directory: args.directory,
chunkType: args.chunkType,
excludeFile: args.excludeFile,
blameSince: args.blameSince,
blameUntil: args.blameUntil,
});

if (results.length === 0) {
Expand All @@ -282,6 +288,7 @@ export const codebase_search: ToolDefinition = tool({
blameAuthor: z.string().optional().describe("Filter by git blame author name or email"),
blameSha: z.string().optional().describe("Filter by git blame commit SHA or prefix"),
blameSince: z.string().optional().describe("Filter to chunks last changed on or after this date (e.g., 2025-01-01)"),
blameUntil: z.string().optional().describe("Filter to chunks last changed on or before this date (e.g., 2025-01-31)"),
},
async execute(args, context) {
return searchCodebaseWithEffectiveness(context?.worktree, DEFAULT_HOST, "search", args.query, {
Expand All @@ -293,6 +300,7 @@ export const codebase_search: ToolDefinition = tool({
blameAuthor: args.blameAuthor,
blameSha: args.blameSha,
blameSince: args.blameSince,
blameUntil: args.blameUntil,
}, (results) => {
const text = results.length === 0
? "No matching code found. Try a different query or run index_codebase first."
Expand Down
4 changes: 4 additions & 0 deletions src/adapters/pi/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export default function codebaseIndexPiExtension(pi: ExtensionAPI): void {
blameAuthor: Type.Optional(Type.String({ description: "Filter by git blame author name or email" })),
blameSha: Type.Optional(Type.String({ description: "Filter by git blame commit SHA or prefix" })),
blameSince: Type.Optional(Type.String({ description: "Filter to chunks last changed on or after this date" })),
blameUntil: Type.Optional(Type.String({ description: "Filter to chunks last changed on or before this date" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
return searchCodebaseWithEffectiveness(projectRoot(ctx), HOST, "search", params.query, {
Expand Down Expand Up @@ -183,6 +184,7 @@ export default function codebaseIndexPiExtension(pi: ExtensionAPI): void {
blameAuthor: Type.Optional(Type.String()),
blameSha: Type.Optional(Type.String()),
blameSince: Type.Optional(Type.String()),
blameUntil: Type.Optional(Type.String()),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
return searchCodebaseWithEffectiveness(projectRoot(ctx), HOST, "peek", params.query, {
Expand All @@ -206,6 +208,8 @@ export default function codebaseIndexPiExtension(pi: ExtensionAPI): void {
directory: Type.Optional(Type.String()),
chunkType: Type.Optional(ChunkType),
excludeFile: Type.Optional(Type.String()),
blameSince: Type.Optional(Type.String({ description: "Filter to chunks last changed on or after this date" })),
blameUntil: Type.Optional(Type.String({ description: "Filter to chunks last changed on or before this date" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const results = await findSimilarCode(projectRoot(ctx), HOST, params.code, params);
Expand Down
Loading