use super::{GRAPH_SOURCE_EXTS, open_graph};
use crate::tools::graph_meta::{graph_summary, project_meta};
use crate::tools::output_format::OutputFormat;
use lean_ctx::core::git_util::{git_dirty, git_out};
use lean_ctx::core::property_graph::{CodeGraph, Edge, EdgeKind, Node};
use lean_ctx::core::tokens::count_tokens;
use lean_ctx::core::type_ref_edges::{DefIndex, ExtMethodIndex};
use serde_json::json;
use std::collections::BTreeSet;
use std::path::Path;
use std::process::Stdio;
fn walk_supported_sources(root_path: &Path) -> (Vec<String>, Vec<(String, String, String)>) {
let walker = ignore::WalkBuilder::new(root_path)
.hidden(true)
.git_ignore(true)
.require_git(false)
.filter_entry(lean_ctx::core::walk_filter::keep_entry)
.build();
let mut file_paths: Vec<String> = Vec::new();
let mut file_contents: Vec<(String, String, String)> = Vec::new();
let root = root_path.to_string_lossy().into_owned();
let mut remaining = usize::MAX;
for entry in walker.flatten() {
if !entry.file_type().is_some_and(|ft| ft.is_file()) {
continue;
}
let path = entry.path();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !GRAPH_SOURCE_EXTS.contains(&ext) {
continue;
}
let rel_path = path
.strip_prefix(root_path)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/");
if lean_ctx::core::io_boundary::is_secret_like(Path::new(&rel_path)).is_some() {
continue;
}
file_paths.push(rel_path.clone());
let read = crate::tools::admitted_sources::read(
&path.to_string_lossy(),
&root,
"ctx_impact",
&mut remaining,
);
if let Some(content) = read.storable() {
file_contents.push((rel_path, content.to_owned(), ext.to_string()));
}
}
file_paths.sort();
file_paths.dedup();
file_contents.sort_by(|a, b| a.0.cmp(&b.0));
(file_paths, file_contents)
}
type AnalyzedFile<'a> = (
&'a str,
&'a str,
&'a str,
lean_ctx::core::deep_queries::DeepAnalysis,
);
fn analyze_all(
file_contents: &[(String, String, String)],
) -> (Vec<AnalyzedFile<'_>>, DefIndex, ExtMethodIndex) {
use rayon::prelude::*;
let per_file: Vec<AnalyzedFile<'_>> = file_contents
.par_iter()
.map(|(p, c, e)| {
(
p.as_str(),
c.as_str(),
e.as_str(),
lean_ctx::core::deep_queries::analyze(c.as_str(), e.as_str()),
)
})
.collect();
let def_index = lean_ctx::core::type_ref_edges::build_def_index(
per_file.iter().map(|(p, _, _, a)| (*p, a)),
);
let ext_method_index = lean_ctx::core::type_ref_edges::build_ext_method_index(
per_file.iter().map(|(p, _, _, a)| (*p, a)),
);
(per_file, def_index, ext_method_index)
}
fn insert_type_ref_edges(
graph: &CodeGraph,
file_node_id: i64,
rel_path: &str,
type_uses: &[lean_ctx::core::deep_queries::TypeUse],
def_index: &DefIndex,
scope: &lean_ctx::core::type_ref_edges::ResolveScope,
) -> usize {
let mut added = 0usize;
for (target_file, type_name, line_start, line_end) in
lean_ctx::core::type_ref_edges::type_ref_targets(
def_index,
type_uses,
rel_path,
&scope.visible_ns,
scope.allow_global_fallback,
)
{
let Ok(target_id) = graph.upsert_node(&Node::file(&target_file)) else {
continue;
};
let _ = graph.upsert_edge(&Edge::new(file_node_id, target_id, EdgeKind::TypeRef));
added += 1;
let sym_node = Node::symbol(
&type_name,
&target_file,
lean_ctx::core::property_graph::NodeKind::Symbol,
)
.with_lines(line_start, line_end);
if let Ok(sym_id) = graph.upsert_node(&sym_node) {
let _ = graph.upsert_edge(&Edge::new(file_node_id, sym_id, EdgeKind::TypeRef));
added += 1;
}
}
added
}
fn insert_ext_method_edges(
graph: &CodeGraph,
file_node_id: i64,
rel_path: &str,
calls: &[lean_ctx::core::deep_queries::CallSite],
ext_method_index: &ExtMethodIndex,
) -> usize {
let mut added = 0usize;
for (target_file, method_name, line_start, line_end) in
lean_ctx::core::type_ref_edges::ext_method_targets(ext_method_index, calls, rel_path)
{
let Ok(target_id) = graph.upsert_node(&Node::file(&target_file)) else {
continue;
};
let _ = graph.upsert_edge(&Edge::new(file_node_id, target_id, EdgeKind::TypeRef));
added += 1;
let sym_node = Node::symbol(
&method_name,
&target_file,
lean_ctx::core::property_graph::NodeKind::Symbol,
)
.with_lines(line_start, line_end);
if let Ok(sym_id) = graph.upsert_node(&sym_node) {
let _ = graph.upsert_edge(&Edge::new(file_node_id, sym_id, EdgeKind::TypeRef));
added += 1;
}
}
added
}
fn normalize_git_path(line: &str) -> String {
line.trim().replace('\\', "/")
}
fn git_diff_name_only_lines(project_root: &Path, args: &[&str]) -> Option<Vec<String>> {
let out = std::process::Command::new("git")
.args(args)
.current_dir(project_root)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8(out.stdout).ok()?;
Some(
s.lines()
.map(normalize_git_path)
.filter(|l| !l.is_empty())
.collect(),
)
}
fn collect_git_changed_paths(project_root: &Path, last_git_head: &str) -> Option<BTreeSet<String>> {
let range = format!("{last_git_head}..HEAD");
let mut set: BTreeSet<String> = BTreeSet::new();
for line in git_diff_name_only_lines(project_root, &["diff", "--name-only", &range])? {
set.insert(line);
}
for line in git_diff_name_only_lines(project_root, &["diff", "--name-only"])? {
set.insert(line);
}
for line in git_diff_name_only_lines(project_root, &["diff", "--name-only", "--cached"])? {
set.insert(line);
}
Some(set)
}
#[cfg(feature = "embeddings")]
fn enclosing_symbol_name_for_line(
types: &[lean_ctx::core::deep_queries::TypeDef],
line: usize,
) -> String {
let mut best: Option<(&lean_ctx::core::deep_queries::TypeDef, usize)> = None;
for t in types {
if line >= t.line && line <= t.end_line {
let span = t.end_line.saturating_sub(t.line);
match best {
None => best = Some((t, span)),
Some((_, prev_span)) => {
if span < prev_span {
best = Some((t, span));
}
}
}
}
}
best.map_or_else(|| "<module>".to_string(), |(t, _)| t.name.clone())
}
#[cfg(feature = "embeddings")]
#[cfg_attr(not(feature = "embeddings"), allow(dead_code))]
fn resolve_call_callee_site(
def_index: &DefIndex,
callee: &str,
caller_file: &str,
imported: &[String],
) -> Option<(
String,
usize,
usize,
lean_ctx::core::semantic::EvidenceGrade,
)> {
use lean_ctx::core::semantic::EvidenceGrade;
let sites = def_index.get(callee)?;
let first_in = |file: &str| {
let mut in_file = sites.iter().filter(|(f, ..)| f == file);
let (f, _, ls, le) = in_file.next()?;
in_file.next().is_none().then(|| (f.clone(), *ls, *le))
};
let mut files: Vec<&str> = sites.iter().map(|(f, ..)| f.as_str()).collect();
files.sort_unstable();
files.dedup();
if files.contains(&caller_file) {
let (f, ls, le) = first_in(caller_file)?;
return Some((f, ls, le, EvidenceGrade::ResolvedStructural));
}
let in_scope: Vec<&str> = files
.iter()
.copied()
.filter(|f| imported.iter().any(|i| i == f))
.collect();
if let [only] = in_scope.as_slice() {
let (f, ls, le) = first_in(only)?;
return Some((f, ls, le, EvidenceGrade::ResolvedStructural));
}
if let [only] = files.as_slice() {
let (f, ls, le) = first_in(only)?;
return Some((f, ls, le, EvidenceGrade::HeuristicStructural));
}
None
}
#[cfg(feature = "embeddings")]
fn index_graph_file_embeddings(
graph: &CodeGraph,
rel_path: &str,
ext: &str,
analysis: &lean_ctx::core::deep_queries::DeepAnalysis,
resolver_ctx: &lean_ctx::core::import_resolver::ResolverContext,
def_index: &DefIndex,
ext_method_index: &ExtMethodIndex,
) -> (usize, usize) {
let mut total_nodes = 0usize;
let mut total_edges = 0usize;
let Ok(file_node_id) = graph.upsert_node(&Node::file(rel_path)) else {
return (0, 0);
};
total_nodes += 1;
for type_def in &analysis.types {
let sym_node = Node::symbol(
&type_def.name,
rel_path,
lean_ctx::core::property_graph::NodeKind::Symbol,
)
.with_lines(type_def.line, type_def.end_line);
if let Ok(sym_id) = graph.upsert_node(&sym_node) {
total_nodes += 1;
let _ = graph.upsert_edge(&Edge::new(file_node_id, sym_id, EdgeKind::Defines));
total_edges += 1;
if type_def.is_exported {
let _ = graph.upsert_edge(&Edge::new(sym_id, file_node_id, EdgeKind::Exports));
total_edges += 1;
}
}
}
let resolved = lean_ctx::core::import_resolver::resolve_imports(
&analysis.imports,
rel_path,
ext,
resolver_ctx,
);
let mut targets: Vec<String> = resolved
.into_iter()
.filter(|imp| !imp.is_external)
.filter_map(|imp| imp.resolved_path)
.collect();
targets.sort();
targets.dedup();
for target_path in &targets {
let Ok(target_id) = graph.upsert_node(&Node::file(target_path)) else {
continue;
};
let _ = graph.upsert_edge(&Edge::new(file_node_id, target_id, EdgeKind::Imports));
total_edges += 1;
}
for call in &analysis.calls {
let caller_name = enclosing_symbol_name_for_line(&analysis.types, call.line);
let mut caller_node = Node::symbol(
&caller_name,
rel_path,
lean_ctx::core::property_graph::NodeKind::Symbol,
);
if let Some(t) = analysis.types.iter().find(|t| t.name == caller_name) {
caller_node = caller_node.with_lines(t.line, t.end_line);
}
let Ok(caller_id) = graph.upsert_node(&caller_node) else {
continue;
};
total_nodes += 1;
let Some((callee_file, c_line, c_end, grade)) =
resolve_call_callee_site(def_index, &call.callee, rel_path, &targets)
else {
continue;
};
let evidence = lean_ctx::core::semantic::EdgeEvidence::new(
grade,
lean_ctx::core::semantic::EvidenceOrigin::ImpactIndex,
None,
1,
);
let callee_node = Node::symbol(
&call.callee,
&callee_file,
lean_ctx::core::property_graph::NodeKind::Symbol,
)
.with_lines(c_line, c_end);
let Ok(callee_id) = graph.upsert_node(&callee_node) else {
continue;
};
total_nodes += 1;
let _ = graph.upsert_edge_with_evidence(caller_id, callee_id, &EdgeKind::Calls, &evidence);
total_edges += 1;
if callee_file != rel_path {
let Ok(callee_file_id) = graph.upsert_node(&Node::file(&callee_file)) else {
continue;
};
let _ = graph.upsert_edge_with_evidence(
file_node_id,
callee_file_id,
&EdgeKind::Calls,
&evidence,
);
total_edges += 1;
}
}
let scope = lean_ctx::core::type_ref_edges::resolve_scope(rel_path, ext, analysis);
total_edges += insert_type_ref_edges(
graph,
file_node_id,
rel_path,
&analysis.type_uses,
def_index,
&scope,
);
total_edges += insert_ext_method_edges(
graph,
file_node_id,
rel_path,
&analysis.calls,
ext_method_index,
);
(total_nodes, total_edges)
}
#[cfg(not(feature = "embeddings"))]
fn index_graph_file_minimal(
graph: &CodeGraph,
rel_path: &str,
content: &str,
ext: &str,
analysis: &lean_ctx::core::deep_queries::DeepAnalysis,
resolver_ctx: &lean_ctx::core::import_resolver::ResolverContext,
def_index: &DefIndex,
ext_method_index: &ExtMethodIndex,
) -> (usize, usize) {
let Ok(file_node_id) = graph.upsert_node(&Node::file(rel_path)) else {
return (0, 0);
};
let mut total_nodes = 1usize;
let mut total_edges = 0usize;
let resolved = lean_ctx::core::import_resolver::resolve_imports(
&analysis.imports,
rel_path,
ext,
resolver_ctx,
);
let mut targets: Vec<String> = resolved
.into_iter()
.filter(|imp| !imp.is_external)
.filter_map(|imp| imp.resolved_path)
.filter(|p| p != rel_path)
.collect();
targets.sort();
targets.dedup();
for target_path in targets {
let Ok(target_id) = graph.upsert_node(&Node::file(&target_path)) else {
continue;
};
total_nodes += 1;
let _ = graph.upsert_edge(&Edge::new(file_node_id, target_id, EdgeKind::Imports));
total_edges += 1;
}
for type_def in &analysis.types {
if type_def.is_exported {
let sym_node = Node::symbol(
&type_def.name,
rel_path,
lean_ctx::core::property_graph::NodeKind::Symbol,
)
.with_lines(type_def.line, type_def.end_line);
if let Ok(sym_id) = graph.upsert_node(&sym_node) {
total_nodes += 1;
let _ = graph.upsert_edge(&Edge::new(file_node_id, sym_id, EdgeKind::Defines));
let _ = graph.upsert_edge(&Edge::new(sym_id, file_node_id, EdgeKind::Exports));
total_edges += 2;
}
}
}
let scope = lean_ctx::core::type_ref_edges::resolve_scope(rel_path, ext, analysis);
total_edges += insert_type_ref_edges(
graph,
file_node_id,
rel_path,
&analysis.type_uses,
def_index,
&scope,
);
total_edges += insert_ext_method_edges(
graph,
file_node_id,
rel_path,
&analysis.calls,
ext_method_index,
);
let exports: Vec<String> = analysis
.types
.iter()
.filter(|t| t.is_exported)
.map(|t| t.name.clone())
.collect();
let line_count = content.lines().count();
let token_count = lean_ctx::core::tokens::count_tokens(content);
let hash = {
use md5::{Digest, Md5};
let mut h = Md5::new();
h.update(content.as_bytes());
lean_ctx::core::agent_identity::hex_encode(&h.finalize())
};
let _ = graph.upsert_file_catalog(&lean_ctx::core::property_graph::FileCatalogEntry {
path: rel_path.to_string(),
hash,
language: ext.to_string(),
line_count,
token_count,
exports,
summary: String::new(),
});
(total_nodes, total_edges)
}
pub(super) fn handle_build(root: &str, fmt: OutputFormat) -> String {
let t0 = std::time::Instant::now();
let root_path = Path::new(root);
let graph = match open_graph(root) {
Ok(g) => g,
Err(e) => return e,
};
let incremental_hint: Option<&'static str> = {
let nodes_ok = graph.node_count().unwrap_or(0) > 0;
let has_head = lean_ctx::core::property_graph::load_meta(root)
.and_then(|m| m.git_head)
.is_some_and(|s| !s.is_empty());
if nodes_ok && has_head {
Some(
"Hint: Graph already indexed — for faster refresh, use ctx_impact action='update' \
to apply incremental git-based updates instead of a full rebuild.",
)
} else {
None
}
};
if let Err(e) = graph.clear() {
return format!("Failed to clear graph: {e}");
}
let (file_paths, file_contents) = walk_supported_sources(root_path);
let cs_contents: std::collections::HashMap<String, String> = file_contents
.iter()
.filter(|(_, _, e)| e.eq_ignore_ascii_case("cs"))
.map(|(p, c, _)| (p.clone(), c.clone()))
.collect();
let resolver_ctx = lean_ctx::core::import_resolver::ResolverContext::new(
root_path,
file_paths.clone(),
&cs_contents,
);
let mut total_nodes = 0usize;
let mut total_edges = 0usize;
let (per_file, def_index, ext_method_index) = analyze_all(&file_contents);
#[cfg(feature = "embeddings")]
for (rel_path, _content, ext, analysis) in &per_file {
let (n, e) = index_graph_file_embeddings(
&graph,
rel_path,
ext,
analysis,
&resolver_ctx,
&def_index,
&ext_method_index,
);
total_nodes += n;
total_edges += e;
}
#[cfg(not(feature = "embeddings"))]
for (rel_path, content, ext, analysis) in &per_file {
let (n, e) = index_graph_file_minimal(
&graph,
rel_path,
content,
ext,
analysis,
&resolver_ctx,
&def_index,
&ext_method_index,
);
total_nodes += n;
total_edges += e;
}
let build_time_ms = t0.elapsed().as_millis() as u64;
let db_display = graph.db_path().display();
let mut result = format!(
"Graph built: {total_nodes} nodes, {total_edges} edges from {} files\n\
Stored at: {db_display}\n\
Build time: {build_time_ms}ms",
file_contents.len(),
);
if let Some(h) = incremental_hint {
result.push('\n');
result.push_str(h);
}
let _ = lean_ctx::core::property_graph::write_meta(
root,
&lean_ctx::core::property_graph::PropertyGraphMetaV1 {
schema_version: 1,
engine_version: lean_ctx::core::property_graph::GRAPH_ENGINE_VERSION,
built_with: env!("CARGO_PKG_VERSION").to_string(),
project_root: lean_ctx::core::graph_index::normalize_project_root(root),
built_at: chrono::Utc::now().to_rfc3339(),
git_head: git_out(root_path, &["rev-parse", "--short", "HEAD"]),
git_dirty: Some(git_dirty(root_path)),
nodes: graph.node_count().ok(),
edges: graph.edge_count().ok(),
files_indexed: Some(file_contents.len()),
build_time_ms: Some(build_time_ms),
},
);
let tokens = count_tokens(&result);
match fmt {
OutputFormat::Json => {
let mut v = serde_json::json!({
"schema_version": lean_ctx::core::contracts::GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION,
"tool": "ctx_impact",
"action": "build",
"project": project_meta(root),
"graph": graph_summary(root),
"graph_meta": lean_ctx::core::property_graph::load_meta(root),
"indexed_files": file_contents.len(),
"nodes": total_nodes,
"edges": total_edges,
"build_time_ms": build_time_ms,
"db_path": graph.db_path().display().to_string()
});
if let Some(h) = incremental_hint {
v.as_object_mut()
.map(|m| m.insert("incremental_hint".to_string(), json!(h)));
}
serde_json::to_string_pretty(&v).unwrap_or_else(|_| "{}".to_string())
}
OutputFormat::Text => format!("{result}\n[ctx_impact build: {tokens} tok]"),
}
}
pub(super) fn handle_update(root: &str, fmt: OutputFormat) -> String {
let t0 = std::time::Instant::now();
let root_path = Path::new(root);
let graph = match open_graph(root) {
Ok(g) => g,
Err(e) => return e,
};
if graph.node_count().unwrap_or(0) == 0 {
return handle_build(root, fmt);
}
let Some(meta) = lean_ctx::core::property_graph::load_meta(root) else {
return handle_build(root, fmt);
};
let Some(last_git_head) = meta.git_head.filter(|s| !s.is_empty()) else {
return handle_build(root, fmt);
};
let Some(changed) = collect_git_changed_paths(root_path, &last_git_head) else {
return handle_build(root, fmt);
};
let changed_count = changed.len();
let (file_paths, file_contents) = walk_supported_sources(root_path);
let cs_contents: std::collections::HashMap<String, String> = file_contents
.iter()
.filter(|(_, _, e)| e.eq_ignore_ascii_case("cs"))
.map(|(p, c, _)| (p.clone(), c.clone()))
.collect();
let resolver_ctx = lean_ctx::core::import_resolver::ResolverContext::new(
root_path,
file_paths.clone(),
&cs_contents,
);
let (per_file, def_index, ext_method_index) = analyze_all(&file_contents);
let mut total_nodes = 0usize;
let mut total_edges = 0usize;
for rel_path in &changed {
let p = Path::new(rel_path);
let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
let supported = GRAPH_SOURCE_EXTS.contains(&ext);
let abs = root_path.join(rel_path);
if !abs.exists() {
if supported {
let _ = graph.remove_file_nodes(rel_path);
}
continue;
}
if !supported {
continue;
}
if let Err(e) = graph.remove_file_nodes(rel_path) {
return format!("Failed to remove old nodes for {rel_path}: {e}");
}
let Some((_, _content, ext_owned, analysis)) =
per_file.iter().find(|(p, _, _, _)| *p == rel_path)
else {
continue;
};
#[cfg(feature = "embeddings")]
{
let (n, e) = index_graph_file_embeddings(
&graph,
rel_path,
ext_owned,
analysis,
&resolver_ctx,
&def_index,
&ext_method_index,
);
total_nodes += n;
total_edges += e;
}
#[cfg(not(feature = "embeddings"))]
{
let (n, e) = index_graph_file_minimal(
&graph,
rel_path,
_content,
ext_owned,
analysis,
&resolver_ctx,
&def_index,
&ext_method_index,
);
total_nodes += n;
total_edges += e;
}
}
let elapsed_ms = t0.elapsed().as_millis() as u64;
let _ = lean_ctx::core::property_graph::write_meta(
root,
&lean_ctx::core::property_graph::PropertyGraphMetaV1 {
schema_version: 1,
engine_version: lean_ctx::core::property_graph::GRAPH_ENGINE_VERSION,
built_with: env!("CARGO_PKG_VERSION").to_string(),
project_root: lean_ctx::core::graph_index::normalize_project_root(root),
built_at: chrono::Utc::now().to_rfc3339(),
git_head: git_out(root_path, &["rev-parse", "--short", "HEAD"]),
git_dirty: Some(git_dirty(root_path)),
nodes: graph.node_count().ok(),
edges: graph.edge_count().ok(),
files_indexed: Some(file_contents.len()),
build_time_ms: Some(elapsed_ms),
},
);
let summary = format!(
"Incremental update: {changed_count} files changed, {total_nodes} nodes updated, {total_edges} edges added ({elapsed_ms}ms)"
);
let tokens = count_tokens(&summary);
match fmt {
OutputFormat::Json => {
let v = json!({
"schema_version": lean_ctx::core::contracts::GRAPH_REPRODUCIBILITY_V1_SCHEMA_VERSION,
"tool": "ctx_impact",
"action": "update",
"project": project_meta(root),
"graph": graph_summary(root),
"graph_meta": lean_ctx::core::property_graph::load_meta(root),
"git_range_from": last_git_head,
"files_changed_reported": changed_count,
"nodes_added": total_nodes,
"edges_added": total_edges,
"update_time_ms": elapsed_ms,
"db_path": graph.db_path().display().to_string()
});
serde_json::to_string_pretty(&v).unwrap_or_else(|_| "{}".to_string())
}
OutputFormat::Text => format!("{summary}\n[ctx_impact update: {tokens} tok]"),
}
}
#[cfg(test)]
mod admission_tests {
use super::walk_supported_sources;
const AWS_KEY: &str = concat!("AK", "IAIOSFODNN7EXAMPLE");
#[test]
fn the_graph_is_built_from_admitted_storable_sources_only() {
let _data = lean_ctx::core::data_dir::isolated_data_dir();
let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
let project = tempfile::tempdir().unwrap();
let clean = "pub fn clean() {}\n";
std::fs::write(project.path().join("clean.rs"), clean).unwrap();
std::fs::write(
project.path().join("keyed.rs"),
format!("pub fn connect() {{\n let aws_key = \"{AWS_KEY}\";\n}}\n"),
)
.unwrap();
std::fs::write(
project.path().join("marked.rs"),
"// TOP SECRET\npub fn plan() {}\n",
)
.unwrap();
let (paths, contents) = walk_supported_sources(project.path());
assert_eq!(paths, ["clean.rs", "keyed.rs", "marked.rs"]);
let content_of = |name: &str| {
contents
.iter()
.find(|(path, _, _)| path == name)
.map(|(_, content, _)| content.as_str())
};
assert_eq!(content_of("clean.rs"), Some(clean));
assert!(content_of("keyed.rs").is_some_and(|text| !text.contains(AWS_KEY)));
assert_eq!(
content_of("marked.rs"),
None,
"a restricted source must not enter the graph"
);
}
#[test]
fn a_withheld_source_contributes_only_its_path() {
let _data = lean_ctx::core::data_dir::isolated_data_dir();
let policy = lean_ctx::core::policy::load(
"name = \"ctx-impact-test\"\nversion = \"1.0.0\"\ndescription = \"test\"\n\
[filters]\nclassification = \"block\"\nblocked_labels = [\"CONFIDENTIAL\"]\n",
)
.unwrap();
let _policy = lean_ctx::core::policy::runtime::TestPolicyOverride::set(Some(policy));
let project = tempfile::tempdir().unwrap();
std::fs::write(
project.path().join("plan.rs"),
"CONFIDENTIAL\npub fn hidden_marker() {}\n",
)
.unwrap();
let (paths, contents) = walk_supported_sources(project.path());
assert_eq!(paths, ["plan.rs"]);
assert!(contents.is_empty());
}
}
#[cfg(all(test, feature = "embeddings"))]
mod tests {
use super::resolve_call_callee_site;
use lean_ctx::core::semantic::EvidenceGrade;
use lean_ctx::core::type_ref_edges::DefIndex;
#[test]
fn callee_site_follows_caller_scope_and_never_guesses() {
let mut index = DefIndex::new();
index.insert(
"save".into(),
vec![("a.rs".into(), None, 3, 5), ("b.rs".into(), None, 7, 9)],
);
index.insert("only".into(), vec![("u.rs".into(), None, 1, 2)]);
let none: [String; 0] = [];
assert_eq!(
resolve_call_callee_site(&index, "save", "c.rs", &none),
None
);
assert_eq!(
resolve_call_callee_site(&index, "save", "c.rs", &["b.rs".to_string()]),
Some(("b.rs".into(), 7, 9, EvidenceGrade::ResolvedStructural))
);
assert_eq!(
resolve_call_callee_site(&index, "save", "a.rs", &none),
Some(("a.rs".into(), 3, 5, EvidenceGrade::ResolvedStructural))
);
assert_eq!(
resolve_call_callee_site(&index, "only", "c.rs", &none),
Some(("u.rs".into(), 1, 2, EvidenceGrade::HeuristicStructural))
);
index.insert(
"Widget".into(),
vec![
("w.cs".into(), Some("N1".into()), 3, 5),
("w.cs".into(), Some("N2".into()), 9, 11),
("lib.cs".into(), None, 1, 2),
],
);
assert_eq!(
resolve_call_callee_site(&index, "Widget", "w.cs", &["lib.cs".to_string()]),
None
);
}
}