pub mod mcf;
pub mod query;
use crate::config::Config;
use crate::errors::{err, ErrorCode};
use crate::gitx::Repo;
use crate::index::Index;
use crate::team::Revisions;
use anyhow::Result;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
const W_EXACT_KEY: i32 = 100;
const W_EXACT_QUALIFIED: i32 = 95;
const W_EXACT_SYMBOL: i32 = 85;
const W_EXACT_PATH: i32 = 80;
const W_DIFF: i32 = 70;
const W_BRANCH_DELTA: i32 = 55;
const W_SCOPE: i32 = 45;
const W_GRAPH_1: i32 = 35;
const W_FTS_MAX: i32 = 30;
const W_VERIFIED: i32 = 8;
const W_INFERRED: i32 = 2;
const W_STALE: i32 = -50;
#[derive(Debug, Clone, serde::Serialize)]
pub struct MemoryItem {
pub ref_id: String,
pub key: String,
pub kind: String,
pub summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub rationale: Option<String>,
pub confidence: String,
pub layer: String,
pub score: i32,
pub reason: String,
pub stale: bool,
pub conflicted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub introduced_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
pub created_at: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CodeItem {
pub ref_id: String,
pub symbol: String,
pub kind: String,
pub path: String,
pub line: u32,
pub signature: String,
pub score: i32,
pub reason: String,
pub is_test: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FileItem {
pub ref_id: String,
pub path: String,
pub language: String,
pub score: i32,
pub reason: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ChangeItem {
pub ref_id: String,
pub summary: String,
pub date: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub human: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
pub layer: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ConflictItem {
pub key: String,
pub heads: Vec<(String, String)>, pub alias_induced: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct EdgeItem {
pub from: String,
pub to: String,
}
#[derive(Debug, serde::Serialize)]
pub struct ContextResult {
pub revisions: Revisions,
pub branch: Option<String>,
pub dirty: bool,
pub modules: Vec<(String, usize)>, pub memories: Vec<MemoryItem>,
pub code_targets: Vec<CodeItem>,
pub files: Vec<FileItem>,
pub tests: Vec<CodeItem>,
pub edges: Vec<EdgeItem>,
pub changes: Vec<ChangeItem>,
pub conflicts: Vec<ConflictItem>,
pub warnings: Vec<String>,
pub estimated_tokens: u32,
pub token_budget: u32,
}
pub struct ContextRequest {
pub task: String,
pub token_budget: u32,
pub scopes: Vec<String>,
pub explain: bool,
}
fn fts_escape(term: &str) -> String {
format!("\"{}\"", term.replace('"', ""))
}
fn fts_prefix(term: &str) -> String {
format!("\"{}\"*", term.replace('"', ""))
}
pub fn update_stale(index: &mut Index) -> Result<()> {
let evidence: Vec<(String, String, String)> = {
let mut stmt = index.conn.prepare(
"SELECT e.record_id, e.etype, e.value FROM record_evidence e
JOIN records r ON r.id = e.record_id
WHERE r.head = 1 AND r.valid = 1 AND e.etype IN ('path', 'symbol', 'test')",
)?;
let rows: Vec<(String, String, String)> = stmt
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))?
.filter_map(|r| r.ok())
.collect();
rows
};
let mut stale: BTreeMap<String, String> = BTreeMap::new();
for (record_id, etype, value) in evidence {
let (target, hash) = match value.split_once("@blake3:") {
Some((t, h)) => (t.to_string(), Some(h.to_string())),
None => (value.clone(), None),
};
let (path_part, symbol_part) = match target.split_once('#') {
Some((p, s)) => (p.to_string(), Some(s.to_string())),
None => (target.clone(), None),
};
let missing = match etype.as_str() {
"path" | "test" => {
let exists: bool = index
.conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM files WHERE path = ?1 OR path LIKE ?1 || '/%')",
[&path_part],
|r| r.get(0),
)
.unwrap_or(false);
if let (true, Some(h)) = (exists, &hash) {
let current: Option<String> = index
.conn
.query_row(
"SELECT content_hash FROM files WHERE path = ?1",
[&path_part],
|r| r.get(0),
)
.ok();
current.map(|c| !c.starts_with(h.as_str())).unwrap_or(false)
} else {
!exists
}
}
"symbol" => {
let name = symbol_part.unwrap_or_else(|| path_part.clone());
let exists: bool = index
.conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM symbols WHERE qualified_name = ?1 OR name = ?1)",
[&name],
|r| r.get(0),
)
.unwrap_or(false);
!exists
}
_ => false,
};
if missing {
stale
.entry(record_id)
.or_insert_with(|| format!("evidence '{value}' no longer resolves"));
}
}
let tx = index.conn.transaction()?;
tx.execute("UPDATE records SET stale = 0, stale_reason = NULL", [])?;
tx.execute("DELETE FROM stale_records", [])?;
{
let mut up = tx.prepare("UPDATE records SET stale = 1, stale_reason = ?2 WHERE id = ?1")?;
let mut ins = tx.prepare("INSERT INTO stale_records(record_id, reason) VALUES (?1, ?2)")?;
for (id, reason) in &stale {
up.execute(rusqlite::params![id, reason])?;
ins.execute(rusqlite::params![id, reason])?;
}
}
tx.commit()?;
Ok(())
}
pub fn run_context(
repo: &Repo,
cfg: &Config,
index: &Index,
revisions: Revisions,
req: &ContextRequest,
) -> Result<ContextResult> {
let analysis = query::analyze(&req.task);
let budget = req
.token_budget
.clamp(200, cfg.retrieval.max_token_budget.max(200));
let per_type = cfg.retrieval.max_results_per_type as usize;
if cfg.team.require_current_for_context
&& !matches!(
revisions.sync_state,
crate::team::SyncState::Current | crate::team::SyncState::Ahead
)
{
return Err(err(
ErrorCode::TeamMemoryBehind,
format!(
"strict mode: team memory is '{}' (baseline {}); run 'memlay sync' before requesting context",
revisions.sync_state.as_str(),
revisions.team_memory_revision
),
));
}
let diff_paths: HashSet<String> = repo.uncommitted_paths()?.into_iter().collect();
let branch_paths: HashSet<String> = repo
.changed_paths_since(&cfg.team.baseline_ref)
.unwrap_or_default()
.into_iter()
.collect();
let conn = &index.conn;
let mut warnings: Vec<String> = Vec::new();
if revisions.sync_state != crate::team::SyncState::Current {
warnings.push(format!(
"team baseline freshness is '{}'; run 'memlay sync' to confirm shared memory",
revisions.sync_state.as_str()
));
}
#[derive(Default)]
struct MemCand {
score: i32,
reasons: Vec<String>,
}
let mut mem_scores: HashMap<String, MemCand> = HashMap::new(); let bump = |map: &mut HashMap<String, MemCand>, id: &str, w: i32, reason: &str| {
let c = map.entry(id.to_string()).or_default();
c.score += w;
if !c.reasons.iter().any(|r| r == reason) {
c.reasons.push(reason.to_string());
}
};
let mut key_terms: Vec<String> = analysis.keys.clone();
for id in &analysis.identifiers {
let lower = id.to_ascii_lowercase();
if crate::records::validate_key(&lower).is_ok() && lower.contains('.') {
key_terms.push(lower);
}
}
for term in &key_terms {
let mut stmt = conn.prepare(
"SELECT id FROM records WHERE head = 1 AND valid = 1 AND (canonical_key = ?1 OR key = ?1)",
)?;
for row in stmt.query_map([term], |r| r.get::<_, String>(0))? {
bump(
&mut mem_scores,
&row?,
W_EXACT_KEY,
"exact memory key match",
);
}
}
let mut scope_terms: BTreeSet<String> = BTreeSet::new();
scope_terms.extend(analysis.paths.iter().cloned());
scope_terms.extend(analysis.identifiers.iter().cloned());
scope_terms.extend(req.scopes.iter().cloned());
scope_terms.extend(analysis.keywords.iter().cloned());
for term in &scope_terms {
let mut stmt = conn.prepare(
"SELECT DISTINCT s.record_id FROM record_scopes s
JOIN records r ON r.id = s.record_id
WHERE r.head = 1 AND r.valid = 1
AND (s.value = ?1 OR ?1 LIKE s.value || '/%' OR s.value LIKE ?1 || '/%')",
)?;
for row in stmt.query_map([term], |r| r.get::<_, String>(0))? {
bump(&mut mem_scores, &row?, W_SCOPE, "scope match");
}
}
let mut fts_terms: Vec<String> = analysis.quoted.iter().map(|q| fts_escape(q)).collect();
fts_terms.extend(analysis.keywords.iter().take(12).map(|k| fts_prefix(k)));
if !fts_terms.is_empty() {
let match_expr = fts_terms.join(" OR ");
let mut stmt = conn.prepare(
"SELECT record_id, bm25(records_fts) FROM records_fts
WHERE records_fts MATCH ?1 ORDER BY bm25(records_fts) LIMIT 40",
)?;
let fetched: Vec<(String, f64)> = stmt
.query_map([&match_expr], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, f64>(1)?))
})
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default();
{
for (id, rank) in fetched {
let w = ((-rank).clamp(0.0, 10.0) / 10.0 * W_FTS_MAX as f64) as i32;
let head: bool = conn
.query_row(
"SELECT head FROM records WHERE id = ?1 AND valid = 1",
[&id],
|r| r.get::<_, i64>(0).map(|v| v == 1),
)
.unwrap_or(false);
if head {
bump(&mut mem_scores, &id, w.max(1), "text relevance");
}
}
}
}
let mut memories: Vec<MemoryItem> = Vec::new();
let mut seen_keys: HashSet<String> = HashSet::new();
let mut conflicted_keys: BTreeSet<String> = BTreeSet::new();
{
let mut ordered: Vec<(String, i32, Vec<String>)> = mem_scores
.into_iter()
.map(|(id, c)| (id, c.score, c.reasons))
.collect();
ordered.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
for (id, base_score, reasons) in ordered {
let row = conn.query_row(
"SELECT canonical_key, kind, summary, rationale, confidence, layer, stale,
conflicted, human, agent, created_at
FROM records WHERE id = ?1 AND valid = 1",
[&id],
|r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, Option<String>>(3)?,
r.get::<_, String>(4)?,
r.get::<_, String>(5)?,
r.get::<_, i64>(6)? == 1,
r.get::<_, i64>(7)? == 1,
r.get::<_, Option<String>>(8)?,
r.get::<_, Option<String>>(9)?,
r.get::<_, String>(10)?,
))
},
);
let Ok((
key,
kind,
summary,
rationale,
confidence,
layer,
stale,
conflicted,
human,
agent,
created_at,
)) = row
else {
continue;
};
if kind == "change" || kind == "incident" {
continue; }
let mut score = base_score;
score += match confidence.as_str() {
"verified" => W_VERIFIED,
"inferred" => W_INFERRED,
_ => 0,
};
if stale {
score += W_STALE;
}
if conflicted {
conflicted_keys.insert(key.clone());
continue; }
if !seen_keys.insert(key.clone()) {
continue; }
memories.push(MemoryItem {
ref_id: format!("memory:{id}"),
key,
kind,
summary,
rationale,
confidence,
layer,
score,
reason: reasons.join("; "),
stale,
conflicted: false,
introduced_by: human,
agent,
created_at,
});
if memories.len() >= per_type {
break;
}
}
memories.sort_by(|a, b| b.score.cmp(&a.score).then(a.key.cmp(&b.key)));
}
#[derive(Default)]
struct SymCand {
score: i32,
reasons: Vec<String>,
}
let mut sym_scores: HashMap<i64, SymCand> = HashMap::new();
let bump_sym = |map: &mut HashMap<i64, SymCand>, id: i64, w: i32, reason: &str| {
let c = map.entry(id).or_default();
c.score += w;
if !c.reasons.iter().any(|r| r == reason) {
c.reasons.push(reason.to_string());
}
};
for ident in &analysis.identifiers {
let mut stmt = conn.prepare("SELECT id FROM symbols WHERE qualified_name = ?1 LIMIT 8")?;
for row in stmt.query_map([ident], |r| r.get::<_, i64>(0))? {
bump_sym(
&mut sym_scores,
row?,
W_EXACT_QUALIFIED,
"exact qualified symbol",
);
}
let mut stmt = conn.prepare("SELECT id FROM symbols WHERE name = ?1 LIMIT 8")?;
for row in stmt.query_map([ident], |r| r.get::<_, i64>(0))? {
bump_sym(&mut sym_scores, row?, W_EXACT_SYMBOL, "exact symbol name");
}
}
if !fts_terms.is_empty() {
let match_expr = fts_terms.join(" OR ");
let mut stmt = conn.prepare(
"SELECT symbol_id, bm25(symbols_fts) FROM symbols_fts
WHERE symbols_fts MATCH ?1 ORDER BY bm25(symbols_fts) LIMIT 40",
)?;
let fetched: Vec<(i64, f64)> = stmt
.query_map([&match_expr], |r| {
Ok((r.get::<_, i64>(0)?, r.get::<_, f64>(1)?))
})
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default();
{
for (id, rank) in fetched {
let w = ((-rank).clamp(0.0, 10.0) / 10.0 * W_FTS_MAX as f64) as i32;
bump_sym(&mut sym_scores, id, w.max(1), "text relevance");
}
}
}
let mut code_targets: Vec<CodeItem> = Vec::new();
let mut tests: Vec<CodeItem> = Vec::new();
let mut matched_paths: BTreeSet<String> = BTreeSet::new();
{
let mut ordered: Vec<(i64, i32, Vec<String>)> = sym_scores
.into_iter()
.map(|(id, c)| (id, c.score, c.reasons))
.collect();
ordered.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
let mut seen_qualified: HashSet<String> = HashSet::new();
for (id, mut score, mut reasons) in ordered {
let row = conn.query_row(
"SELECT qualified_name, kind, path, start_line, signature, is_test
FROM symbols WHERE id = ?1",
[id],
|r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, i64>(3)? as u32,
r.get::<_, Option<String>>(4)?.unwrap_or_default(),
r.get::<_, i64>(5)? == 1,
))
},
);
let Ok((qualified, kind, path, line, signature, is_test)) = row else {
continue;
};
if !seen_qualified.insert(format!("{path}#{qualified}")) {
continue;
}
if diff_paths.contains(&path) {
score += W_DIFF;
reasons.push("in current diff".into());
} else if branch_paths.contains(&path) {
score += W_BRANCH_DELTA;
reasons.push("changed on branch".into());
}
let item = CodeItem {
ref_id: format!("symbol:{id}"),
symbol: qualified,
kind,
path: path.clone(),
line,
signature,
score,
reason: reasons.join("; "),
is_test,
};
if is_test {
if tests.len() < per_type {
tests.push(item);
}
} else if code_targets.len() < per_type {
matched_paths.insert(path);
code_targets.push(item);
}
if code_targets.len() >= per_type && tests.len() >= per_type {
break;
}
}
code_targets.sort_by(|a, b| b.score.cmp(&a.score).then(a.symbol.cmp(&b.symbol)));
tests.sort_by(|a, b| b.score.cmp(&a.score).then(a.symbol.cmp(&b.symbol)));
}
let mut file_scores: HashMap<String, (i32, Vec<String>)> = HashMap::new();
let bump_file =
|map: &mut HashMap<String, (i32, Vec<String>)>, p: &str, w: i32, reason: &str| {
let e = map.entry(p.to_string()).or_default();
e.0 += w;
if !e.1.iter().any(|r| r == reason) {
e.1.push(reason.to_string());
}
};
for p in analysis.paths.iter().chain(req.scopes.iter()) {
let mut stmt = conn
.prepare("SELECT path FROM files WHERE path = ?1 OR path LIKE ?1 || '/%' LIMIT 12")?;
for row in stmt.query_map([p], |r| r.get::<_, String>(0))? {
bump_file(&mut file_scores, &row?, W_EXACT_PATH, "exact path match");
}
}
for p in diff_paths.iter() {
if analysis
.keywords
.iter()
.any(|k| p.to_ascii_lowercase().contains(k.as_str()))
|| !analysis.paths.is_empty() && analysis.paths.iter().any(|ap| p.starts_with(ap))
{
bump_file(&mut file_scores, p, W_DIFF, "in current diff");
}
}
for p in branch_paths.iter() {
if analysis
.keywords
.iter()
.any(|k| p.to_ascii_lowercase().contains(k.as_str()))
{
bump_file(&mut file_scores, p, W_BRANCH_DELTA, "changed on branch");
}
}
if !fts_terms.is_empty() {
let match_expr = fts_terms.join(" OR ");
let mut stmt =
conn.prepare("SELECT path FROM files_fts WHERE files_fts MATCH ?1 LIMIT 20")?;
let fetched: Vec<String> = stmt
.query_map([&match_expr], |r| r.get::<_, String>(0))
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default();
for p in fetched {
bump_file(&mut file_scores, &p, 10, "path text match");
}
}
let seed_paths: Vec<String> = matched_paths.iter().cloned().collect();
let mut edges: Vec<EdgeItem> = Vec::new();
for seed in seed_paths.iter().take(4) {
let mut stmt = conn.prepare(
"SELECT dst FROM symbol_edges WHERE src_path = ?1 AND edge_type = 'imports' LIMIT 6",
)?;
for row in stmt.query_map([seed], |r| r.get::<_, String>(0))? {
let dst = row?;
edges.push(EdgeItem {
from: seed.clone(),
to: dst.clone(),
});
let like = format!("%{}%", dst.trim_start_matches("./").replace("./", ""));
let mut fstmt = conn.prepare("SELECT path FROM files WHERE path LIKE ?1 LIMIT 2")?;
let fetched: Vec<String> = fstmt
.query_map([&like], |r| r.get::<_, String>(0))
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default();
for p in fetched {
bump_file(&mut file_scores, &p, W_GRAPH_1, "imported by matched file");
}
}
}
edges.truncate(8);
let mut files: Vec<FileItem> = Vec::new();
{
let mut ordered: Vec<(String, i32, Vec<String>)> = file_scores
.into_iter()
.map(|(p, (s, r))| (p, s, r))
.collect();
ordered.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
for (path, score, reasons) in ordered.into_iter().take(per_type) {
let language: String = conn
.query_row("SELECT language FROM files WHERE path = ?1", [&path], |r| {
r.get(0)
})
.unwrap_or_else(|_| "text".into());
matched_paths.insert(path.clone());
files.push(FileItem {
ref_id: format!("file:{path}"),
path,
language,
score,
reason: reasons.join("; "),
});
}
}
let changes: Vec<ChangeItem>;
{
let mut candidates: BTreeMap<String, ChangeItem> = BTreeMap::new();
for term in matched_paths
.iter()
.cloned()
.chain(analysis.identifiers.iter().cloned())
.chain(analysis.keywords.iter().take(6).cloned())
{
let mut stmt = conn.prepare(
"SELECT r.id, r.summary, r.created_at, r.human, r.agent, r.layer
FROM records r JOIN record_scopes s ON s.record_id = r.id
WHERE r.kind IN ('change','incident') AND r.head = 1 AND r.valid = 1
AND (s.value = ?1 OR ?1 LIKE s.value || '/%')
LIMIT 10",
)?;
let fetched: Vec<ChangeItem> = stmt
.query_map([&term], |r| {
Ok(ChangeItem {
ref_id: format!("memory:{}", r.get::<_, String>(0)?),
summary: r.get(1)?,
date: r.get::<_, String>(2)?.chars().take(10).collect(),
human: r.get(3)?,
agent: r.get(4)?,
layer: r.get(5)?,
})
})
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default();
for c in fetched {
candidates.insert(format!("{}-{}", c.date, c.ref_id), c);
}
}
let mut collected: Vec<ChangeItem> = candidates.into_values().collect();
collected.sort_by(|a, b| b.date.cmp(&a.date).then(a.ref_id.cmp(&b.ref_id)));
collected.truncate(5);
changes = collected;
}
let mut conflicts: Vec<ConflictItem> = Vec::new();
{
let mut stmt = conn.prepare(
"SELECT canonical_key, head_ids, alias_induced FROM semantic_conflicts ORDER BY canonical_key",
)?;
let rows: Vec<(String, String, bool)> = stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, i64>(2)? == 1,
))
})?
.filter_map(|r| r.ok())
.collect();
for (key, head_ids, alias_induced) in rows {
let relevant = conflicted_keys.contains(&key)
|| analysis.keys.contains(&key)
|| scope_terms.iter().any(|t| key.contains(t.as_str()));
if !relevant {
continue;
}
let mut heads = Vec::new();
for id in head_ids.split(',') {
let summary: String = conn
.query_row("SELECT summary FROM records WHERE id = ?1", [id], |r| {
r.get(0)
})
.unwrap_or_default();
heads.push((format!("memory:{id}"), summary));
}
warnings.push(format!(
"key '{key}' has {} competing heads; no value was selected",
heads.len()
));
conflicts.push(ConflictItem {
key,
heads,
alias_induced,
});
}
}
let mut module_counts: BTreeMap<String, usize> = BTreeMap::new();
for p in &matched_paths {
let module = p
.rsplit_once('/')
.map(|(d, _)| d.to_string())
.unwrap_or_else(|| ".".into());
*module_counts.entry(module).or_default() += 1;
}
let mut modules: Vec<(String, usize)> = module_counts.into_iter().collect();
modules.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
modules.truncate(6);
if memories.is_empty() && code_targets.is_empty() && files.is_empty() {
warnings.push("no exact matches; consider refining the task description or running 'memlay index update'".into());
}
let mut result = ContextResult {
revisions,
branch: repo.branch(),
dirty: repo.dirty()?,
modules,
memories,
code_targets,
files,
tests,
edges,
changes,
conflicts,
warnings,
estimated_tokens: 0,
token_budget: budget,
};
let rendered = mcf::render(&result, budget, req.explain);
result.estimated_tokens = query::estimate_tokens(&rendered);
Ok(result)
}
#[derive(Debug, serde::Serialize)]
pub struct Expansion {
pub r#ref: String,
pub kind: String,
pub body: serde_json::Value,
}
pub fn run_expand(
repo: &Repo,
index: &Index,
refs: &[String],
token_budget: u32,
) -> Result<Vec<Expansion>> {
let mut out = Vec::new();
let mut used: u32 = 0;
let budget = token_budget.max(200);
for r in refs {
if used >= budget {
break;
}
let (kind, value) = r.split_once(':').ok_or_else(|| {
err(
ErrorCode::InvalidRecord,
format!("ref '{r}' must be memory:<id>, key:<key>, symbol:<id>, or file:<path>"),
)
})?;
let body = match kind {
"memory" => expand_memory(repo, index, value)?,
"key" => expand_key(index, value)?,
"symbol" => expand_symbol(repo, index, value)?,
"file" => expand_file(repo, index, value)?,
"audit" => expand_audit(repo, value)?,
"module" => crate::mapview::expand_module(repo, index, value)?,
other => {
return Err(err(
ErrorCode::InvalidRecord,
format!("unknown ref type '{other}' in '{r}'"),
))
}
};
used += query::estimate_tokens(&body.to_string());
out.push(Expansion {
r#ref: r.clone(),
kind: kind.to_string(),
body,
});
}
Ok(out)
}
fn expand_memory(repo: &Repo, index: &Index, id: &str) -> Result<serde_json::Value> {
let file: String = index
.conn
.query_row("SELECT file FROM records WHERE id = ?1", [id], |r| r.get(0))
.map_err(|_| {
err(
ErrorCode::InvalidRecord,
format!("unknown record id '{id}'"),
)
})?;
#[allow(clippy::type_complexity)]
let (stale, stale_reason, conflicted, layer, intro_commit, intro_author, intro_at): (
bool,
Option<String>,
bool,
String,
Option<String>,
Option<String>,
Option<String>,
) = index.conn.query_row(
"SELECT stale, stale_reason, conflicted, layer, intro_commit, intro_author, intro_at
FROM records WHERE id = ?1",
[id],
|r| {
Ok((
r.get::<_, i64>(0)? == 1,
r.get(1)?,
r.get::<_, i64>(2)? == 1,
r.get(3)?,
r.get(4)?,
r.get(5)?,
r.get(6)?,
))
},
)?;
let abs = repo
.root
.join(file.replace('/', std::path::MAIN_SEPARATOR_STR));
let record = std::fs::read(&abs)
.ok()
.and_then(|b| crate::records::mrf::parse(&b).ok());
let mut body = match record {
Some(rec) => serde_json::to_value(&rec)?,
None => serde_json::json!({ "file": file, "error": "record file unavailable" }),
};
if let Some(obj) = body.as_object_mut() {
obj.insert("layer".into(), layer.into());
obj.insert("stale".into(), stale.into());
if let Some(sr) = stale_reason {
obj.insert("stale-reason".into(), sr.into());
}
obj.insert("conflicted".into(), conflicted.into());
obj.insert("file".into(), file.into());
if let Some(c) = intro_commit {
obj.insert("introduced-commit".into(), c.into());
}
if let Some(a) = intro_author {
obj.insert("introduced-by".into(), a.into());
}
if let Some(t) = intro_at {
obj.insert("introduced-at".into(), t.into());
}
}
Ok(body)
}
fn expand_key(index: &Index, key: &str) -> Result<serde_json::Value> {
let mut stmt = index.conn.prepare(
"SELECT id, op, summary, created_at, head, layer FROM records
WHERE (canonical_key = ?1 OR key = ?1) AND valid = 1
ORDER BY id",
)?;
let versions: Vec<serde_json::Value> = stmt
.query_map([key], |r| {
Ok(serde_json::json!({
"ref": format!("memory:{}", r.get::<_, String>(0)?),
"op": r.get::<_, String>(1)?,
"summary": r.get::<_, String>(2)?,
"created_at": r.get::<_, String>(3)?,
"head": r.get::<_, i64>(4)? == 1,
"layer": r.get::<_, String>(5)?,
}))
})?
.filter_map(|r| r.ok())
.collect();
if versions.is_empty() {
return Err(err(
ErrorCode::InvalidRecord,
format!("unknown key '{key}'"),
));
}
Ok(serde_json::json!({ "key": key, "versions": versions }))
}
fn safe_repo_path(repo: &Repo, rel: &str) -> Result<std::path::PathBuf> {
crate::records::validate_repo_relative_path(rel)
.map_err(|m| err(ErrorCode::PathOutsideRepository, m))?;
let abs = repo
.root
.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR));
let canonical = abs.canonicalize().map_err(|_| {
err(
ErrorCode::PathOutsideRepository,
format!("'{rel}' not found"),
)
})?;
let root = repo
.root
.canonicalize()
.unwrap_or_else(|_| repo.root.clone());
if !canonical.starts_with(&root) {
return Err(err(
ErrorCode::PathOutsideRepository,
format!("'{rel}' resolves outside the repository"),
));
}
Ok(canonical)
}
const SNIPPET_MAX_LINES: usize = 120;
fn expand_symbol(repo: &Repo, index: &Index, id: &str) -> Result<serde_json::Value> {
let sym_id: i64 = id.parse().map_err(|_| {
err(
ErrorCode::InvalidRecord,
format!("symbol ref '{id}' must be numeric"),
)
})?;
let (qualified, kind, path, start, end, signature): (String, String, String, i64, i64, String) =
index
.conn
.query_row(
"SELECT qualified_name, kind, path, start_line, end_line, COALESCE(signature,'')
FROM symbols WHERE id = ?1",
[sym_id],
|r| {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get(3)?,
r.get(4)?,
r.get(5)?,
))
},
)
.map_err(|_| {
err(
ErrorCode::InvalidRecord,
format!("unknown symbol id '{id}'"),
)
})?;
let abs = safe_repo_path(repo, &path)?;
let content = std::fs::read_to_string(&abs).unwrap_or_default();
let hash = blake3::hash(content.as_bytes()).to_hex().to_string();
let lines: Vec<&str> = content.lines().collect();
let from = (start as usize).saturating_sub(1);
let to = (end as usize)
.min(from + SNIPPET_MAX_LINES)
.min(lines.len());
let snippet = lines
.get(from..to)
.map(|l| l.join("\n"))
.unwrap_or_default();
Ok(serde_json::json!({
"symbol": qualified,
"kind": kind,
"path": path,
"lines": [start, to as i64],
"truncated": (to as i64) < end,
"signature": signature,
"content_blake3": &hash[..16],
"snippet": snippet,
}))
}
fn expand_audit(repo: &Repo, session: &str) -> Result<serde_json::Value> {
let safe: String = session
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' {
c
} else {
'_'
}
})
.collect();
let spool = repo
.state_dir()
.join("spool")
.join(format!("{safe}.ndjson"));
if spool.exists() {
let text = std::fs::read_to_string(&spool)?;
let events: Vec<serde_json::Value> = text
.lines()
.filter_map(|l| serde_json::from_str(l).ok())
.take(50)
.collect();
return Ok(serde_json::json!({
"session": session, "state": "open", "events": events.len(), "sample": events,
}));
}
let staging = repo.shared_dir().join("audit-staging").join(&safe);
let summary_path = staging.join("summary.json");
if summary_path.exists() {
let summary: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&summary_path)?)?;
let events = std::fs::read(staging.join("events.jsonl.zst"))
.ok()
.and_then(|z| zstd::decode_all(z.as_slice()).ok())
.map(|bytes| {
String::from_utf8_lossy(&bytes)
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.take(50)
.collect::<Vec<_>>()
})
.unwrap_or_default();
return Ok(serde_json::json!({
"session": session, "state": "finalized", "summary": summary, "sample": events,
}));
}
Err(err(
ErrorCode::InvalidRecord,
format!("unknown audit session '{session}'"),
))
}
fn expand_file(repo: &Repo, index: &Index, path: &str) -> Result<serde_json::Value> {
let abs = safe_repo_path(repo, path)?;
let content = std::fs::read_to_string(&abs).unwrap_or_default();
let hash = blake3::hash(content.as_bytes()).to_hex().to_string();
let head: String = content.lines().take(80).collect::<Vec<_>>().join("\n");
let mut stmt = index.conn.prepare(
"SELECT id, qualified_name, kind, start_line FROM symbols WHERE path = ?1 ORDER BY start_line LIMIT 40",
)?;
let symbols: Vec<serde_json::Value> = stmt
.query_map([path], |r| {
Ok(serde_json::json!({
"ref": format!("symbol:{}", r.get::<_, i64>(0)?),
"symbol": r.get::<_, String>(1)?,
"kind": r.get::<_, String>(2)?,
"line": r.get::<_, i64>(3)?,
}))
})?
.filter_map(|r| r.ok())
.collect();
let total_lines = content.lines().count();
Ok(serde_json::json!({
"path": path,
"content_blake3": &hash[..16],
"total_lines": total_lines,
"head": head,
"truncated": total_lines > 80,
"symbols": symbols,
}))
}