feat: add Google code retrieval across desktop CLI and MCP
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Canceled after 2m23s

This commit is contained in:
karl committed 2026-10-12 21:22:11 +02:00
1 parent 8b9b079469
commit 81846b3e98
32 files changed
+1303 -59

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
//! Shared code-file classification for extraction and pre-ranking scope filters.
use std::{path::Path, sync::OnceLock};
pub const CODE_EXTENSIONS: &[&str] = &[
"rs", "py", "pyi", "js", "jsx", "mjs", "cjs", "ts", "tsx", "mts", "cts", "c", "h", "cpp", "cc",
"cxx", "hpp", "hxx", "hh", "go", "java", "kt", "kts", "cs", "swift", "m", "mm", "rb", "php",
"phtml", "scala", "sc", "sql", "sh", "bash", "zsh", "fish", "ps1", "psm1", "psd1", "bat",
"cmd", "lua", "r", "pl", "pm", "ex", "exs", "erl", "hrl", "hs", "lhs", "clj", "cljs", "cljc",
"dart", "vue", "svelte", "graphql", "gql", "tf", "tfvars", "proto", "zig", "vb", "vbs", "f",
"for", "f90", "f95", "f03", "pas", "d", "cmake",
];
const CODE_FILENAMES: &[&str] = &[
"makefile",
"gnumakefile",
"dockerfile",
"cmakelists.txt",
"justfile",
];
pub fn is_code_path(path: &Path) -> bool {
let extension = path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
.to_ascii_lowercase();
let filename = path
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("")
.to_ascii_lowercase();
CODE_EXTENSIONS.contains(&extension.as_str()) || CODE_FILENAMES.contains(&filename.as_str())
}
/// Static trusted SQL predicate for the shared internal `files f` alias. No
/// caller-controlled query, path or extension is interpolated into this SQL.
pub fn code_file_predicate() -> &'static str {
static SQL: OnceLock<String> = OnceLock::new();
SQL.get_or_init(|| {
let literals = |values: &[&str]| {
values
.iter()
.map(|value| format!("'{value}'"))
.collect::<Vec<_>>()
.join(",")
};
format!(
"(f.extension IN ({}) OR lower(f.filename) IN ({}))",
literals(CODE_EXTENSIONS),
literals(CODE_FILENAMES)
)
})
}
+44 -21
View File
@@ -33,27 +33,30 @@ pub(crate) fn extract(path: &Path) -> Result<Option<Vec<Passage>>> {
docx(&bytes).map(Some)
};
}
if !matches!(
extension.as_str(),
"txt"
| "md"
| "rs"
| "py"
| "js"
| "ts"
| "tsx"
| "jsx"
| "json"
| "toml"
| "yaml"
| "yml"
| "csv"
| "html"
| "css"
| "xml"
| "log"
| "sh"
) {
let code = crate::is_code_path(path);
if !code
&& !matches!(
extension.as_str(),
"txt"
| "md"
| "rs"
| "py"
| "js"
| "ts"
| "tsx"
| "jsx"
| "json"
| "toml"
| "yaml"
| "yml"
| "csv"
| "html"
| "css"
| "xml"
| "log"
| "sh"
)
{
return Ok(None);
}
let file = File::open(path)?;
@@ -69,6 +72,26 @@ pub(crate) fn extract(path: &Path) -> Result<Option<Vec<Passage>>> {
return Ok(None);
};
let lines = text.split_inclusive('\n').collect::<Vec<_>>();
if code {
let mut passages = Vec::new();
for start in (0..lines.len()).step_by(96) {
if passages.len() >= MAX_PASSAGES {
return Ok(None);
}
let end = (start + 128).min(lines.len());
passages.push(Passage {
text: lines[start..end].concat(),
source: SourceLocation {
kind: "line".into(),
number: (start + 1) as u32,
},
});
if end == lines.len() {
break;
}
}
return Ok(Some(passages));
}
Ok(Some(
lines
.chunks(64)
+22 -3
View File
@@ -3,7 +3,9 @@
//! Reconciliation is atomic. Incomplete directory walks never delete records.
//! Text and document extraction have byte limits, but still run in this process.
//! Worker isolation remains a production milestone.
mod code;
mod extraction;
pub use code::{code_file_predicate, is_code_path, CODE_EXTENSIONS};
pub mod metrics;
use anyhow::{bail, Context, Result};
pub use metrics::{MetricsMode, SearchMetrics, SearchResponse, SearchSpan};
@@ -19,7 +21,7 @@ use std::{
};
use walkdir::WalkDir;
const SCHEMA_VERSION: i64 = 10;
const SCHEMA_VERSION: i64 = 11;
const MAX_RESULTS: usize = 1000;
#[derive(Debug, Default, Serialize)]
@@ -50,6 +52,8 @@ impl Default for IndexOptions {
/// The folder is canonicalized and includes descendants, with a path-boundary check.
#[derive(Debug, Default, Clone)]
pub struct SearchOptions {
/// Restrict every retrieval lane to source/build-script files before top-K.
pub code_only: bool,
pub folder: Option<PathBuf>,
pub extension: Option<String>,
pub min_size: Option<u64>,
@@ -242,6 +246,16 @@ impl SearchIndex {
CREATE TRIGGER IF NOT EXISTS files_media_delete AFTER DELETE ON files BEGIN DELETE FROM media_metadata WHERE path=old.path; END;
CREATE TRIGGER IF NOT EXISTS files_media_update AFTER UPDATE ON files WHEN new.path!=old.path OR new.size!=old.size OR new.mtime!=old.mtime BEGIN DELETE FROM media_metadata WHERE path=old.path; END;
PRAGMA user_version=10;")?;
if version < 11 {
initialization.execute(
&format!(
"UPDATE files AS f SET mtime=-1 WHERE {}",
code_file_predicate()
),
[],
)?;
}
initialization.execute_batch("PRAGMA user_version=11;")?;
initialization.commit()?;
}
Ok(Self {
@@ -620,12 +634,16 @@ impl SearchIndex {
trace.end(normalization, Default::default());
let planning = trace.begin("lexical.candidate_plan", parent);
let limit = limit.min(MAX_RESULTS);
let filters = " AND (?3 IS NULL OR substr(f.path,1,length(?3))=?3)
let mut filters = String::from(" AND (?3 IS NULL OR substr(f.path,1,length(?3))=?3)
AND (?4 IS NULL OR f.extension=?4)
AND (?5 IS NULL OR f.size>=?5) AND (?6 IS NULL OR f.size<=?6)
AND (?7 IS NULL OR f.mtime/1000000>=?7) AND (?8 IS NULL OR f.mtime/1000000<=?8)
AND (?10 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.file_id=f.id AND m.duration_ms>=?10))
AND (?11 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.file_id=f.id AND m.duration_ms<=?11))";
AND (?11 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.file_id=f.id AND m.duration_ms<=?11))");
if options.code_only {
filters.push_str(" AND ");
filters.push_str(code_file_predicate());
}
let filter_values = [
folder_prefix.map_or(Value::Null, Value::Text),
extension.map_or(Value::Null, Value::Text),
@@ -639,6 +657,7 @@ impl SearchIndex {
.map_or(Value::Null, Value::Integer),
];
let has_filters = filter_values.iter().any(|value| *value != Value::Null)
|| options.code_only
|| options.min_duration_ms.is_some()
|| options.max_duration_ms.is_some();
let mut results = Vec::new();