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
98 changes: 32 additions & 66 deletions crates/tracedecay-graph-query/src/verified_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,56 +548,40 @@ impl VerifiedGraphQuery {
.map_err(graph_projection_error)
}

/// Finds files containing functions targeted by canonical annotation
/// edges whose source is a recognized test annotation marker.
/// Finds the requested files that contain functions targeted by canonical
/// annotation edges whose source is a recognized test annotation marker.
///
/// A request scoped to specific files resolves through the per-file
/// catalog index instead of the whole-corpus symbol stream: the four
/// recognized markers are lexically attached attributes, so a marker
/// always occupies the same file as the function it annotates, and only
/// the requested files can contribute either endpoint. The unscoped
/// census keeps the corpus sweep, that is its job.
/// The request resolves through the per-file catalog index instead of
/// the whole-corpus symbol stream: the four recognized markers are
/// lexically attached attributes, so a marker always occupies the same
/// file as the function it annotates, and only the requested files can
/// contribute either endpoint.
#[hotpath::measure(label = "usecases.graph.verified.test_annotated_files")]
pub fn test_annotated_logical_files(
&self,
logical_paths: Option<&HashSet<String>>,
logical_paths: &HashSet<String>,
max_symbols: usize,
max_relations: usize,
) -> Result<HashSet<String>> {
self.refuse_if_bound_closed()?;
let symbols = match logical_paths {
Some(requested) => {
let mut symbols = Vec::new();
for path in requested {
let budget = max_symbols
.checked_sub(symbols.len())
.filter(|remaining| *remaining > 0)
.ok_or_else(|| {
graph_budget_exhausted(
"verified test-attribution census exceeded its symbol budget",
)
})?;
let mut in_file =
self.symbols_in_logical_file(path, budget.saturating_add(1))?;
if in_file.len() > budget {
return Err(graph_budget_exhausted(
"verified test-attribution census exceeded its symbol budget",
));
}
symbols.append(&mut in_file);
}
symbols
}
None => {
let page = self.symbols_page(None, max_symbols)?;
if page.has_more {
return Err(graph_budget_exhausted(
let mut symbols = Vec::new();
for path in logical_paths {
let budget = max_symbols
.checked_sub(symbols.len())
.filter(|remaining| *remaining > 0)
.ok_or_else(|| {
graph_budget_exhausted(
"verified test-attribution census exceeded its symbol budget",
));
}
page.symbols
)
})?;
let mut in_file = self.symbols_in_logical_file(path, budget.saturating_add(1))?;
if in_file.len() > budget {
return Err(graph_budget_exhausted(
"verified test-attribution census exceeded its symbol budget",
));
}
};
symbols.append(&mut in_file);
}
let mut paths = HashMap::new();
let mut test_markers = HashSet::new();
for symbol in &symbols {
Expand All @@ -615,35 +599,17 @@ impl VerifiedGraphQuery {
test_markers.insert(symbol.occurrence.clone());
}
}
if logical_paths.is_some() {
if test_markers.is_empty() {
return Ok(HashSet::new());
}
// Outgoing annotation edges from the scoped markers alone: the
// corpus-seeded `edges_among` variant below needs every endpoint
// in its seed set, which is exactly the full-corpus hydration a
// file-scoped request must not pay.
let markers = test_markers.iter().cloned().collect::<Vec<_>>();
return Ok(self
.callees(&markers, &[RelationEdgeKindV1::Annotates], max_relations)?
.into_iter()
.flatten()
.filter_map(|edge| paths.get(&edge.edge.to_occurrence).cloned())
.collect());
if test_markers.is_empty() {
return Ok(HashSet::new());
}
let occurrences = symbols
.iter()
.map(|symbol| symbol.occurrence.clone())
.collect::<Vec<_>>();
// Outgoing annotation edges from the scoped markers alone; an
// `edges_among` read would need every endpoint in its seed set.
let markers = test_markers.iter().cloned().collect::<Vec<_>>();
Ok(self
.edges_among(
&occurrences,
&[RelationEdgeKindV1::Annotates],
max_relations,
)?
.callees(&markers, &[RelationEdgeKindV1::Annotates], max_relations)?
.into_iter()
.filter(|edge| test_markers.contains(&edge.from_occurrence))
.filter_map(|edge| paths.get(&edge.to_occurrence).cloned())
.flatten()
.filter_map(|edge| paths.get(&edge.edge.to_occurrence).cloned())
.collect())
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay-mcp/src/handlers/git/affected.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub(super) async fn collect_verified_affected_test_files(
files,
max_depth,
custom_glob,
|paths| graph.test_annotated_logical_files(Some(paths), 500_000, 2_000_000),
|paths| graph.test_annotated_logical_files(paths, 500_000, 2_000_000),
)
.await
}
Expand Down
8 changes: 4 additions & 4 deletions crates/tracedecay-mcp/src/handlers/git/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ where
let files_with_inline_tests = hotpath::measure_block!(
"mcp.git.diff_context.test_annotations",
graph.test_annotated_logical_files(
Some(&annotation_paths),
&annotation_paths,
VERIFIED_GRAPH_MAX_SYMBOLS,
VERIFIED_GRAPH_MAX_RELATIONS,
)?
Expand Down Expand Up @@ -744,7 +744,7 @@ where
let files_with_inline_tests = hotpath::measure_block!(
"mcp.git.commit_context.test_annotations",
graph.test_annotated_logical_files(
Some(&changed_paths),
&changed_paths,
VERIFIED_GRAPH_MAX_SYMBOLS,
VERIFIED_GRAPH_MAX_RELATIONS,
)?
Expand Down Expand Up @@ -1331,7 +1331,7 @@ where
let mut files_with_inline_tests = hotpath::measure_block!(
"mcp.pr_context.test_annotations.changed",
graph.test_annotated_logical_files(
Some(&changed_paths),
&changed_paths,
VERIFIED_GRAPH_MAX_SYMBOLS,
VERIFIED_GRAPH_MAX_RELATIONS,
)?
Expand Down Expand Up @@ -1440,7 +1440,7 @@ where
files_with_inline_tests.extend(hotpath::measure_block!(
"mcp.pr_context.test_annotations.impacted",
graph.test_annotated_logical_files(
Some(&impact_path_set),
&impact_path_set,
VERIFIED_GRAPH_MAX_SYMBOLS,
VERIFIED_GRAPH_MAX_RELATIONS,
)?
Expand Down
17 changes: 13 additions & 4 deletions crates/tracedecay-mcp/src/handlers/graph/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ use super::primitive_surface::{
use super::search::execute_code_index_search;
use super::search_evidence::{bind_verified_graph_to_search, race_primary_search_with_graph};
use super::search_freshness::{
ServedGenerationV1, search_freshness, worktree_freshness_from_payload,
ServedGenerationV1, lanes_under_scheduler_freshness, search_freshness,
worktree_freshness_from_payload,
};
use super::{
graph_symbol_end_line, graph_symbol_paths, required_graph_file_path, required_graph_metadata,
Expand Down Expand Up @@ -321,10 +322,15 @@ where
let search_matches = context_search_matches(&complete, scope_prefix);
let lexical_anchors = context_lexical_anchors(&complete, scope_prefix);
let code_generation = Some(complete.code_generation.clone());
let coverage = primitive_search_coverage(&complete.coverage);
let lanes = lanes_under_scheduler_freshness(
complete.coverage.clone(),
&complete.code_generation,
&worktree_freshness,
);
let coverage = primitive_search_coverage(&lanes);
let freshness = search_freshness(
ServedGenerationV1::Served(&complete.code_generation),
&complete.coverage,
&lanes,
&worktree_freshness,
);
(
Expand Down Expand Up @@ -474,6 +480,9 @@ where
),
..retrieval
};
let cost = graph
.as_ref()
.map(tracedecay_graph_query::VerifiedGraphQuery::read_cost);
let result = ContextResultV1 {
task: request.task,
mode,
Expand All @@ -498,7 +507,7 @@ where
touched_files,
code_graph: None,
analytics: Some(analytics),
cost: None,
cost,
})
}

Expand Down
12 changes: 9 additions & 3 deletions crates/tracedecay-mcp/src/handlers/graph/context_markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ pub(super) fn verified_plan_context(
test_files: None,
});
}
let annotated_files = graph.test_annotated_logical_files(None, 500_000, 2_000_000)?;
let mut test_files = HashSet::new();
let mut unnamed_caller_files = HashSet::new();
for symbol in symbols {
for caller in traverse_verified_neighbors(
graph,
Expand All @@ -295,12 +295,18 @@ pub(super) fn verified_plan_context(
2,
)? {
let file_path = required_graph_file_path(&caller.symbol)?;
if tracedecay_code_index::is_test_file(file_path) || annotated_files.contains(file_path)
{
if tracedecay_code_index::is_test_file(file_path) {
test_files.insert(file_path.to_owned());
} else {
unnamed_caller_files.insert(file_path.to_owned());
}
}
}
test_files.extend(graph.test_annotated_logical_files(
&unnamed_caller_files,
500_000,
2_000_000,
)?);
let mut test_files = test_files.into_iter().collect::<Vec<_>>();
test_files.sort();
Ok(ContextPlanV1 {
Expand Down
12 changes: 9 additions & 3 deletions crates/tracedecay-mcp/src/handlers/graph/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ use super::search_evidence::{
SearchGraphEvidence, bind_verified_graph_to_search, race_primary_search_with_graph,
};
use super::search_freshness::{
ServedGenerationV1, freshness_lines, search_freshness, worktree_freshness_from_payload,
ServedGenerationV1, freshness_lines, lanes_under_scheduler_freshness, search_freshness,
worktree_freshness_from_payload,
};
use super::verified::CODE_SYMBOL_EVIDENCE_PREFIX;
use super::{graph_occurrence_id, graph_tool_completion};
Expand Down Expand Up @@ -269,10 +270,15 @@ where
} else {
None
};
let coverage = lanes_under_scheduler_freshness(
complete.coverage.clone(),
&complete.code_generation,
&worktree_freshness,
);
let result = SearchCompleteV1 {
freshness: search_freshness(
ServedGenerationV1::Served(&complete.code_generation),
&complete.coverage,
&coverage,
&worktree_freshness,
),
query_fallback_digest: complete.query_fallback.digest.as_str().to_owned(),
Expand All @@ -281,7 +287,7 @@ where
.as_ref()
.map(serde_json::to_string)
.transpose()?,
coverage: search_coverage(&complete.coverage),
coverage: search_coverage(&coverage),
code_generation: complete.code_generation,
results,
lexical_routes,
Expand Down
57 changes: 52 additions & 5 deletions crates/tracedecay-mcp/src/handlers/graph/search_freshness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,57 @@ fn stale_lanes(coverage: &CodeIndexSearchCoverageV1) -> Vec<String> {
.collect()
}

/// Whether the scheduler's reading proves `served_generation` current: the
/// worktree is fresh, nothing is rebuilding, and no newer generation sealed.
fn scheduler_proves_current(
state: &CodeIndexWorktreeFreshnessV1,
served_generation: Option<&str>,
) -> bool {
state.staleness_state == Some(CodeIndexStalenessStateV1::Fresh)
&& !state.rebuild_in_flight
&& (state.latest_generation_id.is_none()
|| served_generation.is_none()
|| state.latest_generation_id.as_deref() == served_generation)
}

/// The executor's lane coverage restated under the scheduler's reading, so
/// the lanes and the verdict answer to one freshness authority. The query
/// gate marks a lane stale when it could not prove freshness itself; once
/// the scheduler proves the generation that lane served current, the lane
/// is complete for it.
pub(super) fn lanes_under_scheduler_freshness(
mut coverage: CodeIndexSearchCoverageV1,
served_generation: &str,
worktree: &WorktreeFreshnessSourceV1,
) -> CodeIndexSearchCoverageV1 {
let WorktreeFreshnessSourceV1::Worktree(state) = worktree else {
return coverage;
};
if state.latest_generation_id.as_deref() != Some(served_generation)
|| !scheduler_proves_current(state, Some(served_generation))
{
return coverage;
}
for lane in [
&mut coverage.exact,
&mut coverage.lexical,
&mut coverage.graph,
] {
match lane {
CodeIndexLaneStatusV1::Stale { generation } if generation == served_generation => {
*lane = CodeIndexLaneStatusV1::Complete;
Comment on lines +115 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve lane-local stale outcomes

Do not convert every matching Stale lane to Complete: CodeIndexSearchCoverageV1::from_fallback_lane_coverage uses this variant both for a whole-generation fallback and when an authenticated retriever independently returns PublicRetrieverStatus::Stale. In the latter case, a scheduler report that the worktree generation is current does not make that lane's evidence current; this rewrite can turn an empty/stale exact, lexical, or graph lane into complete and make the overall response report fresh. The conversion needs provenance that the status came specifically from served_stale, or it must preserve lane-local stale statuses.

AGENTS.md reference: AGENTS.md:L9-L12

Useful? React with 👍 / 👎.

}
CodeIndexLaneStatusV1::Partial { generation, .. }
if generation.as_deref() == Some(served_generation) =>
{
*generation = None;
}
_ => {}
}
}
coverage
}

/// Derive the verdict from the executor's lane coverage and the scheduler's
/// worktree state.
pub(super) fn search_freshness(
Expand All @@ -89,11 +140,7 @@ pub(super) fn search_freshness(
};
let scheduler_says_stale = match worktree {
WorktreeFreshnessSourceV1::Worktree(state) => {
state.staleness_state != Some(CodeIndexStalenessStateV1::Fresh)
|| state.rebuild_in_flight
|| (state.latest_generation_id.is_some()
&& served_generation.is_some()
&& state.latest_generation_id != served_generation)
!scheduler_proves_current(state, served_generation.as_deref())
}
WorktreeFreshnessSourceV1::NotMounted | WorktreeFreshnessSourceV1::Unattached => false,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ fn scoped_test_annotation_lookup_needs_only_a_file_scale_budget() {
.collect();

let annotated = graph
.test_annotated_logical_files(Some(&requested), 8, 64)
.test_annotated_logical_files(&requested, 8, 64)
.expect("a two-file question must not require a corpus-scale symbol budget");
assert_eq!(
annotated,
Expand All @@ -955,18 +955,11 @@ fn scoped_test_annotation_lookup_needs_only_a_file_scale_budget() {
"only the file whose function carries a test marker is reported"
);

// The unscoped census keeps its corpus sweep and its budget contract.
let census = graph.test_annotated_logical_files(None, 8, 64);
assert!(
census.is_err(),
"the whole-corpus census still refuses a budget below the corpus size"
);

// The scoped budget still bounds the requested files themselves.
let hot_only: HashSet<String> = ["src/hot.rs".to_owned()].into_iter().collect();
assert!(
graph
.test_annotated_logical_files(Some(&hot_only), 1, 64)
.test_annotated_logical_files(&hot_only, 1, 64)
.is_err(),
"requested files larger than the budget stay a typed refusal"
);
Expand Down
Loading
Loading