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
95 changes: 90 additions & 5 deletions cli/src/services/agent_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ fn default_agent_trace_metadata() -> AgentTraceMetadata {
AgentTraceMetadata {
sce: AgentTraceSceMetadata {
version: PACKAGE_VERSION.to_owned(),
line_changes: LineChangeAttribution::default(),
},
}
}
Expand Down Expand Up @@ -122,6 +123,53 @@ pub struct AgentTraceMetadata {
#[serde(rename_all = "snake_case")]
pub struct AgentTraceSceMetadata {
pub version: String,
/// Exact touched-line attribution counts derived from canonical
/// `post_commit_patch` hunks, bucketed by hunk classification.
#[serde(default)]
pub line_changes: LineChangeAttribution,
}

/// Exact added/removed touched-line counts for one hunk-classification bucket.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct LineChangeCounts {
pub added: u64,
pub removed: u64,
}

/// Exact touched-line attribution counts, bucketed by hunk classification.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct LineChangeAttribution {
#[serde(default)]
pub ai: LineChangeCounts,
#[serde(default)]
pub mixed: LineChangeCounts,
#[serde(default)]
pub unknown: LineChangeCounts,
}

/// Tally one `post_commit_patch` hunk's touched lines into the bucket matching
/// `kind`. A hunk's entire touched-line count is recorded in a single bucket:
/// a `mixed` hunk contributes all of its touched lines, not just the subset
/// that also appears in the AI intersection.
fn record_hunk_line_changes(
counts: &mut LineChangeAttribution,
kind: HunkContributor,
hunk: &PatchHunk,
) {
let bucket = match kind {
HunkContributor::Ai => &mut counts.ai,
HunkContributor::Mixed => &mut counts.mixed,
HunkContributor::Unknown => &mut counts.unknown,
};

for line in &hunk.lines {
match line.kind {
TouchedLineKind::Added => bucket.added += 1,
TouchedLineKind::Removed => bucket.removed += 1,
}
}
}

fn parse_commit_timestamp(commit_timestamp: &str) -> Result<DateTime<FixedOffset>> {
Expand Down Expand Up @@ -444,6 +492,7 @@ fn build_trace_file(
post_commit_file: &PatchFileChange,
intersection_patch: &ParsedPatch,
conversation_url: &str,
line_changes: &mut LineChangeAttribution,
) -> Option<TraceFile> {
if post_commit_file.hunks.is_empty() {
return None;
Expand Down Expand Up @@ -480,6 +529,7 @@ fn build_trace_file(
}
None => (HunkContributor::Unknown, None, None),
};
record_hunk_line_changes(line_changes, contributor_kind, post_commit_hunk);
let related_session_ids = matched_intersection_hunk
.into_iter()
.flat_map(|hunk| hunk.lines.iter())
Expand Down Expand Up @@ -540,19 +590,49 @@ pub fn build_agent_trace(
let intersection_patch = intersect_patches(constructed_patch, post_commit_patch);

let mut files = Vec::new();
let mut line_changes = LineChangeAttribution::default();

for post_commit_file in &post_commit_patch.files {
if let Some(embedded_patch) = parse_embedded_deleted_patch(post_commit_file) {
// The literal deleted-`.patch` file's own hunks describe the actual
// canonical commit content, so they are classified and counted here
// against the top-level `intersection_patch` even though they never
// produce a `Conversation` in this branch. The embedded reconstructed
// hunks below describe the deleted patch's logical content, not the
// canonical commit, and must never be counted toward `line_changes`.
// Matched by `old_path` (always non-empty for a deleted file), not
// `new_path` (always empty for every deleted file, which would
// otherwise collide across multiple deleted files in the same patch).
for hunk in &post_commit_file.hunks {
let kind = intersection_patch
.files
.iter()
.find(|ifile| ifile.old_path == post_commit_file.old_path)
.map_or(HunkContributor::Unknown, |ifile| {
classify_hunk(hunk, &ifile.hunks)
});
record_hunk_line_changes(&mut line_changes, kind, hunk);
}

let embedded_intersection = intersect_patches(constructed_patch, &embedded_patch);
let mut discarded_line_changes = LineChangeAttribution::default();
files.extend(embedded_patch.files.iter().filter_map(|embedded_file| {
build_trace_file(embedded_file, &embedded_intersection, &conversation_url)
build_trace_file(
embedded_file,
&embedded_intersection,
&conversation_url,
&mut discarded_line_changes,
)
}));
continue;
}

if let Some(trace_file) =
build_trace_file(post_commit_file, &intersection_patch, &conversation_url)
{
if let Some(trace_file) = build_trace_file(
post_commit_file,
&intersection_patch,
&conversation_url,
&mut line_changes,
) {
files.push(trace_file);
}
}
Expand All @@ -577,7 +657,12 @@ pub fn build_agent_trace(
revision: metadata.commit_revision.to_owned(),
}),
tool,
metadata: default_agent_trace_metadata(),
metadata: AgentTraceMetadata {
sce: AgentTraceSceMetadata {
version: PACKAGE_VERSION.to_owned(),
line_changes,
},
},
files,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
},
"metadata": {
"sce": {
"version": "0.2.0"
"version": "0.2.0",
"line_changes": {
"ai": { "added": 91, "removed": 9 },
"mixed": { "added": 0, "removed": 0 },
"unknown": { "added": 0, "removed": 0 }
}
}
},
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
},
"metadata": {
"sce": {
"version": "0.2.0"
"version": "0.2.0",
"line_changes": {
"ai": { "added": 0, "removed": 0 },
"mixed": { "added": 0, "removed": 0 },
"unknown": { "added": 0, "removed": 0 }
}
}
},
"files": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
},
"metadata": {
"sce": {
"version": "0.2.0"
"version": "0.2.0",
"line_changes": {
"ai": { "added": 5, "removed": 0 },
"mixed": { "added": 0, "removed": 0 },
"unknown": { "added": 0, "removed": 0 }
}
}
},
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
},
"metadata": {
"sce": {
"version": "0.2.0"
"version": "0.2.0",
"line_changes": {
"ai": { "added": 5, "removed": 2 },
"mixed": { "added": 0, "removed": 0 },
"unknown": { "added": 1, "removed": 15 }
}
}
},
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
},
"metadata": {
"sce": {
"version": "0.2.0"
"version": "0.2.0",
"line_changes": {
"ai": { "added": 1, "removed": 1 },
"mixed": { "added": 3, "removed": 3 },
"unknown": { "added": 1, "removed": 1 }
}
}
},
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
},
"metadata": {
"sce": {
"version": "0.2.0"
"version": "0.2.0",
"line_changes": {
"ai": { "added": 0, "removed": 0 },
"mixed": { "added": 24, "removed": 0 },
"unknown": { "added": 0, "removed": 0 }
}
}
},
"files": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
},
"metadata": {
"sce": {
"version": "0.2.0"
"version": "0.2.0",
"line_changes": {
"ai": { "added": 64, "removed": 0 },
"mixed": { "added": 0, "removed": 0 },
"unknown": { "added": 4, "removed": 0 }
}
}
},
"files": [
Expand Down
4 changes: 4 additions & 0 deletions cli/src/services/agent_trace/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ fn assert_builds_expected_agent_trace(scenario: AgentTraceScenario) {
!metadata_version.is_empty(),
"metadata.sce.version should not be empty"
);
assert_eq!(
actual_json["metadata"]["sce"]["line_changes"], golden["metadata"]["sce"]["line_changes"],
"line_changes should match golden fixture exactly"
);
assert_eq!(actual_json["vcs"], golden["vcs"]);
assert_eq!(actual_json["files"], expected_files);
}
Expand Down
2 changes: 1 addition & 1 deletion context/context-map.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion context/glossary.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading