#[derive(Debug, Clone, Default)]
pub struct TraceRecord {
pub timestamp: String,
pub vcs: Option<TraceVcs>,
pub tool_name: Option<String>,
pub tool_version: Option<String>,
pub files: Vec<TraceFile>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceVcs {
pub vcs_type: TraceVcsType,
pub revision: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraceVcsType {
Git,
Jj,
Other,
}
#[derive(Debug, Clone, Default)]
pub struct TraceFile {
pub path: String,
pub conversations: Vec<TraceConversation>,
}
#[derive(Debug, Clone, Default)]
pub struct TraceConversation {
pub url: Option<String>,
pub contributor: Option<TraceContributor>,
pub ranges: Vec<TraceRange>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceContributor {
pub kind: ContributorKind,
pub model_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContributorKind {
Human,
Ai,
Mixed,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceRange {
pub start_line: usize,
pub end_line: usize,
pub contributor: Option<TraceContributor>,
}
const PSEUDO_FILES: [&str; 2] = [".shell-history", ".sessions"];
impl TraceRecord {
pub fn has_ai_contribution(&self) -> bool {
let code_files: Vec<&TraceFile> = self
.files
.iter()
.filter(|f| !PSEUDO_FILES.contains(&f.path.as_str()))
.collect();
if code_files.is_empty() {
return false;
}
let mut saw_contributor = false;
for file in &code_files {
for conv in &file.conversations {
if let Some(c) = &conv.contributor {
saw_contributor = true;
if matches!(c.kind, ContributorKind::Ai | ContributorKind::Mixed) {
return true;
}
}
for range in &conv.ranges {
if let Some(c) = &range.contributor {
saw_contributor = true;
if matches!(c.kind, ContributorKind::Ai | ContributorKind::Mixed) {
return true;
}
}
}
}
}
!saw_contributor && self.tool_name.is_some()
}
pub fn primary_url(&self) -> Option<&str> {
self.files
.iter()
.flat_map(|f| &f.conversations)
.find_map(|c| c.url.as_deref())
}
pub fn primary_model_id(&self) -> Option<&str> {
self.files
.iter()
.flat_map(|f| &f.conversations)
.find_map(|c| {
c.contributor
.as_ref()
.and_then(|ct| ct.model_id.as_deref())
.or_else(|| {
c.ranges
.iter()
.find_map(|r| r.contributor.as_ref()?.model_id.as_deref())
})
})
}
pub fn code_file_count(&self) -> usize {
self.files
.iter()
.filter(|f| !PSEUDO_FILES.contains(&f.path.as_str()))
.count()
}
}