Skip to content
Open
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
46 changes: 46 additions & 0 deletions crates/tracedecay-contracts/src/retrieval/git_context_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,28 @@ pub struct GitContextSymbolV1 {
pub line: u32,
}

/// A file that historically changes with `partner_of` but is missing from the
/// change set under review, e.g. a migration beside its schema.
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CoChangePartnerV1 {
pub file: String,
pub partner_of: String,
/// Commits that changed both `partner_of` and `file`.
pub co_changes: u64,
/// Commits that changed `partner_of`.
pub partner_of_changes: u64,
}

/// Why co-change mining produced no answer, so an empty
/// `missing_co_change_partners` does not read as "no partner is missing".
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CoChangeUnavailableV1 {
pub reason: String,
pub retryable: bool,
}

#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiffContextResultV1 {
Expand All @@ -228,6 +250,12 @@ pub struct DiffContextResultV1 {
/// still unexplored.
pub impact_complete: bool,
pub affected_tests: Vec<String>,
/// Files that usually change with a changed file, per bounded Git
/// history, but are absent from this change set.
pub missing_co_change_partners: Vec<CoChangePartnerV1>,
/// Present when Git history could not be mined for co-change partners.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub co_change_unavailable: Option<CoChangeUnavailableV1>,
/// The worktree verdict a served graph read opens with.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub freshness: Option<PrimitiveSearchFreshnessV1>,
Expand Down Expand Up @@ -490,6 +518,12 @@ pub struct PrContextCompleteV1 {
pub commits: Vec<GitCommitSubjectV1>,
pub files_changed: usize,
pub changes: Vec<GitFileChangeV1>,
/// Files that usually change with a changed file, per bounded Git
/// history, but are absent from this change set.
pub missing_co_change_partners: Vec<CoChangePartnerV1>,
/// Present when Git history could not be mined for co-change partners.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub co_change_unavailable: Option<CoChangeUnavailableV1>,
pub symbols_added: usize,
pub symbols_removed: usize,
pub symbols_modified: usize,
Expand Down Expand Up @@ -526,6 +560,12 @@ pub struct PrContextSymbolsUnavailableV1 {
pub commits: Vec<GitCommitSubjectV1>,
pub files_changed: usize,
pub changes: Vec<GitFileChangeV1>,
/// Files that usually change with a changed file, per bounded Git
/// history, but are absent from this change set.
pub missing_co_change_partners: Vec<CoChangePartnerV1>,
/// Present when Git history could not be mined for co-change partners.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub co_change_unavailable: Option<CoChangeUnavailableV1>,
pub symbols_added: usize,
pub symbols_removed: usize,
pub symbols_modified: usize,
Expand Down Expand Up @@ -555,6 +595,12 @@ pub struct PrContextGraphPendingV1 {
pub commits: Vec<GitCommitSubjectV1>,
pub files_changed: usize,
pub changes: Vec<GitFileChangeV1>,
/// Files that usually change with a changed file, per bounded Git
/// history, but are absent from this change set.
pub missing_co_change_partners: Vec<CoChangePartnerV1>,
/// Present when Git history could not be mined for co-change partners.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub co_change_unavailable: Option<CoChangeUnavailableV1>,
pub symbols_added: usize,
pub symbols_modified: usize,
pub added: Vec<PrSymbolEntryV1>,
Expand Down
21 changes: 11 additions & 10 deletions crates/tracedecay-contracts/src/retrieval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,17 @@ pub use git_context_surface::{
BranchSearchHitV1, BranchSearchPageV1, BranchSearchResultV1, BranchSearchSurfaceRequestV1,
BranchSearchUnavailableV1, BranchSnapshotEntryV1, BranchSymbolChangeV1, BranchSymbolV1,
ChangelogCompleteV1, ChangelogPartialV1, ChangelogResultV1, ChangelogSurfaceRequestV1,
CommitCategoryV1, CommitContextResultV1, CommitContextSummaryV1, CommitContextSurfaceRequestV1,
CommitFileRoleV1, CommitSymbolEntryV1, CommitSymbolV1, ConfigSummaryKindV1, ConfigSummaryV1,
DiffContextResultV1, DiffContextSurfaceRequestV1, GitCommitSubjectV1, GitComparedSymbolV1,
GitContextSymbolV1, GitFileChangeStatusV1, GitFileChangeV1, GitFileRoleV1, GitPageStatusV1,
GitReadCompleteV1, GitReadPartialV1, GitReadUnavailableV1, GitReferenceLimitV1,
GitResultLimitV1, GitToolErrorKindV1, GitToolErrorV1, GitToolFailureV1, GitToolOperationV1,
PrAnalysisCoverageV1, PrContextCompleteV1, PrContextGraphPendingV1, PrContextResultV1,
PrContextSurfaceRequestV1, PrContextSymbolsUnavailableV1, PrCoverageSelectionV1,
PrSelectionCoverageV1, PrSymbolChangesCompleteV1, PrSymbolEntryV1, PrSymbolPageV1,
PrSymbolSelectionV1, SymbolChangesCompleteV1, SymbolChangesUnavailableV1,
CoChangePartnerV1, CoChangeUnavailableV1, CommitCategoryV1, CommitContextResultV1,
CommitContextSummaryV1, CommitContextSurfaceRequestV1, CommitFileRoleV1, CommitSymbolEntryV1,
CommitSymbolV1, ConfigSummaryKindV1, ConfigSummaryV1, DiffContextResultV1,
DiffContextSurfaceRequestV1, GitCommitSubjectV1, GitComparedSymbolV1, GitContextSymbolV1,
GitFileChangeStatusV1, GitFileChangeV1, GitFileRoleV1, GitPageStatusV1, GitReadCompleteV1,
GitReadPartialV1, GitReadUnavailableV1, GitReferenceLimitV1, GitResultLimitV1,
GitToolErrorKindV1, GitToolErrorV1, GitToolFailureV1, GitToolOperationV1, PrAnalysisCoverageV1,
PrContextCompleteV1, PrContextGraphPendingV1, PrContextResultV1, PrContextSurfaceRequestV1,
PrContextSymbolsUnavailableV1, PrCoverageSelectionV1, PrSelectionCoverageV1,
PrSymbolChangesCompleteV1, PrSymbolEntryV1, PrSymbolPageV1, PrSymbolSelectionV1,
SymbolChangesCompleteV1, SymbolChangesUnavailableV1,
};
pub use git_topology_anchor::{
GitTopologyAnchorAuthority, GitTopologyAnchorAuthorityError, GitTopologyAnchorFuture,
Expand Down
124 changes: 116 additions & 8 deletions crates/tracedecay-mcp/src/handlers/git/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tracedecay_code_index::graph_projection::CodeGraphSymbolSummaryV1;
use tracedecay_contracts::retrieval::{
ChangelogCompleteV1, ChangelogPartialV1, ChangelogSurfaceRequestV1, CommitCategoryV1,
CommitContextSummaryV1, CommitContextSurfaceRequestV1, CommitFileRoleV1, CommitSymbolEntryV1,
CommitSymbolV1, ConfigSummaryKindV1, ConfigSummaryV1, DiffContextResultV1,
DiffContextSurfaceRequestV1, GitComparedSymbolV1, GitContextSymbolV1, GitReadCompleteV1,
GitReadPartialV1, GitReadUnavailableV1, PrAnalysisCoverageV1, PrContextCompleteV1,
PrContextGraphPendingV1, PrContextSurfaceRequestV1, PrContextSymbolsUnavailableV1,
PrCoverageSelectionV1, PrSelectionCoverageV1, PrSymbolChangesCompleteV1, PrSymbolEntryV1,
PrSymbolPageV1, PrSymbolSelectionV1, SymbolChangesCompleteV1, SymbolChangesUnavailableV1,
ChangelogCompleteV1, ChangelogPartialV1, ChangelogSurfaceRequestV1, CoChangePartnerV1,
CoChangeUnavailableV1, CommitCategoryV1, CommitContextSummaryV1, CommitContextSurfaceRequestV1,
CommitFileRoleV1, CommitSymbolEntryV1, CommitSymbolV1, ConfigSummaryKindV1, ConfigSummaryV1,
DiffContextResultV1, DiffContextSurfaceRequestV1, GitComparedSymbolV1, GitContextSymbolV1,
GitReadCompleteV1, GitReadPartialV1, GitReadUnavailableV1, PrAnalysisCoverageV1,
PrContextCompleteV1, PrContextGraphPendingV1, PrContextSurfaceRequestV1,
PrContextSymbolsUnavailableV1, PrCoverageSelectionV1, PrSelectionCoverageV1,
PrSymbolChangesCompleteV1, PrSymbolEntryV1, PrSymbolPageV1, PrSymbolSelectionV1,
SymbolChangesCompleteV1, SymbolChangesUnavailableV1,
};
use tracedecay_contracts::{InvocationAnalyticsV1, PrContextAnalyticsV1, PrContextStageTimingsV1};
use tracedecay_domain::{RelationEdgeKindV1, SymbolOccurrenceId};
Expand Down Expand Up @@ -305,6 +306,84 @@ fn context_symbol(symbol: &CodeGraphSymbolSummaryV1) -> Result<GitContextSymbolV
})
}

/// Co-change partners missing from a change set. Mining is enrichment: when
/// it fails the primary context still answers and says why partners are absent.
struct CoChangeEvidence {
missing: Vec<CoChangePartnerV1>,
unavailable: Option<CoChangeUnavailableV1>,
}

struct CancelGitReadOnDrop(tracedecay_runtime_core::cancellation::CancellationToken);

impl Drop for CancelGitReadOnDrop {
fn drop(&mut self) {
self.0.cancel();
}
}

async fn co_change_evidence(
ctx: &McpToolContext<'_>,
history: String,
tree: String,
changed_files: Vec<String>,
) -> CoChangeEvidence {
let cancel = tracedecay_runtime_core::cancellation::CancellationToken::new();
let _cancel_on_drop = CancelGitReadOnDrop(cancel.clone());
let mut bounds = tracedecay_runtime_core::git::GitCommandBounds {
cancel: Some(cancel.clone()),
..Default::default()
};
if let Some(deadline) = ctx.deadline() {
let remaining =
tracedecay_daemon_protocol::deadline_remaining(deadline).unwrap_or_default();
bounds.deadline = bounds.deadline.min(std::time::Instant::now() + remaining);
}
let root = ctx.project_root().to_path_buf();
let mut worker = tokio::task::spawn_blocking(move || {
tracedecay_runtime_core::git::co_change::missing_co_change_partners(
&root,
&history,
&tree,
&changed_files,
&bounds,
)
});
let joined = match ctx.cancellation() {
Some(signal) => tokio::select! {
biased;
joined = &mut worker => joined,
() = signal.cancelled() => {
cancel.cancel();
worker.await
}
},
None => worker.await,
};
let unavailable = |reason: String, retryable: bool| CoChangeEvidence {
missing: Vec::new(),
unavailable: Some(CoChangeUnavailableV1 { reason, retryable }),
};
match joined {
Ok(Ok(partners)) => CoChangeEvidence {
missing: partners
.into_iter()
.map(|partner| CoChangePartnerV1 {
file: partner.file,
partner_of: partner.partner_of,
co_changes: partner.co_changes as u64,
partner_of_changes: partner.partner_of_changes as u64,
})
.collect(),
unavailable: None,
},
Ok(Err(error)) => {
let retryable = !matches!(error, TraceDecayError::HostCliUnavailable { .. });
unavailable(error.to_string(), retryable)
}
Err(join_error) => unavailable(format!("co-change task failed: {join_error}"), true),
}
}

fn all_symbols_in_files(
graph: &VerifiedGraphQuery,
files: &HashSet<String>,
Expand Down Expand Up @@ -574,6 +653,12 @@ where
let mut tests_sorted: Vec<String> = affected_tests.into_iter().collect();
tests_sorted.sort();

let co_change = tracing::Instrument::instrument(
co_change_evidence(ctx, "HEAD".to_owned(), "HEAD".to_owned(), files.clone()),
tracing::trace_span!("mcp.git.diff_context.co_change"),
)
.await;

let touched_files = unique_file_paths(
all_touched_files
.iter()
Expand All @@ -588,6 +673,8 @@ where
impacted_symbols,
impact_complete: impacted.complete,
affected_tests: tests_sorted,
missing_co_change_partners: co_change.missing,
co_change_unavailable: co_change.unavailable,
freshness: None,
};
Ok(graph_tool_completion(
Expand Down Expand Up @@ -1038,6 +1125,7 @@ struct PrContextGitEvidence {
merge_base: String,
commits: Vec<GitCommitSubjectV1>,
changes: Vec<GitFileChangeV1>,
co_change: CoChangeEvidence,
}

impl PrContextGitEvidence {
Expand All @@ -1059,6 +1147,8 @@ impl PrContextGitEvidence {
commits: self.commits,
files_changed: self.changes.len(),
changes: self.changes,
missing_co_change_partners: self.co_change.missing,
co_change_unavailable: self.co_change.unavailable,
symbols_added: 0,
symbols_removed: 0,
symbols_modified: 0,
Expand Down Expand Up @@ -1142,6 +1232,19 @@ where
});
let changed_files: Vec<String> = changes.iter().map(|change| change.path.clone()).collect();
let changed_paths = changed_files.iter().cloned().collect::<HashSet<_>>();
// History up to the merge base is the evidence; the compared change set
// is what it is checked against, and partners must survive at head.
let co_change = tracing::Instrument::instrument(
co_change_evidence(
ctx,
merge_base.clone(),
head_oid.clone(),
changed_files.clone(),
),
tracing::trace_span!("mcp.pr_context.co_change"),
)
.await;
controls.checkpoint()?;

let maximum_symbols = request
.maximum_symbols
Expand All @@ -1157,6 +1260,7 @@ where
merge_base,
commits,
changes,
co_change,
};

let stage_started = std::time::Instant::now();
Expand Down Expand Up @@ -1205,6 +1309,8 @@ where
commits: evidence.commits,
files_changed: evidence.changes.len(),
changes: evidence.changes,
missing_co_change_partners: evidence.co_change.missing,
co_change_unavailable: evidence.co_change.unavailable,
symbols_added: 0,
symbols_modified: 0,
added: Vec::new(),
Expand Down Expand Up @@ -1549,6 +1655,8 @@ where
commits: evidence.commits,
files_changed: evidence.changes.len(),
changes: evidence.changes,
missing_co_change_partners: evidence.co_change.missing,
co_change_unavailable: evidence.co_change.unavailable,
symbols_added: added.len(),
symbols_removed: removed.len(),
symbols_modified: modified.len(),
Expand Down
1 change: 1 addition & 0 deletions crates/tracedecay-runtime-core/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//! recovery, and reads where exact porcelain semantics remain the authority.

pub mod churn;
pub mod co_change;

use std::ffi::{OsStr, OsString};
#[cfg(unix)]
Expand Down
Loading
Loading