1use objects::object::{Agent, Attribution, ConflictRange, ConflictRegion, ConflictSide, Principal};
5use oplog::ConflictResolutionMode;
6use schemars::JsonSchema;
7use serde::Serialize;
8
9use crate::{
10 HeddleReport, MachineOutputKind, OutputDiscriminator, ReportContract, schema_for_report,
11};
12
13#[derive(Clone, Debug, Serialize, JsonSchema)]
14pub struct ResolveReport {
15 pub output_kind: String,
16 pub message: Option<String>,
17 pub resolved: Vec<String>,
18 pub remaining: Vec<String>,
19 pub conflict_paths: Vec<String>,
21 pub conflicts: Vec<ConflictRegionReport>,
23 pub resolutions: Vec<ConflictResolutionReport>,
25 pub continued: bool,
26 pub continuation_status: Option<String>,
27 pub continuation_message: Option<String>,
28 pub next_action: Option<String>,
29 pub recommended_action: Option<String>,
30}
31
32impl ResolveReport {
33 pub const CONTRACT: ReportContract = ReportContract {
34 schema_name: "resolve",
35 machine_output_kind: MachineOutputKind::Json,
36 output_discriminator: Some(OutputDiscriminator {
37 field: "output_kind",
38 value: "resolve",
39 }),
40 schema: schema_for_report::<Self>,
41 };
42}
43
44impl HeddleReport for ResolveReport {
45 const CONTRACT: ReportContract = Self::CONTRACT;
46}
47
48#[derive(Clone, Debug, Serialize, JsonSchema)]
49pub struct ConflictRegionReport {
50 pub id: String,
51 pub path: String,
52 pub symbol: Option<String>,
53 pub occurrence: u32,
54 pub merged_range: ConflictRangeReport,
55 pub base: ConflictSideReport,
56 pub ours: ConflictSideReport,
57 pub theirs: ConflictSideReport,
58}
59
60impl From<&ConflictRegion> for ConflictRegionReport {
61 fn from(conflict: &ConflictRegion) -> Self {
62 Self {
63 id: conflict.id.clone(),
64 path: conflict.path.clone(),
65 symbol: conflict.symbol.clone(),
66 occurrence: conflict.occurrence,
67 merged_range: conflict.merged_range.into(),
68 base: (&conflict.base).into(),
69 ours: (&conflict.ours).into(),
70 theirs: (&conflict.theirs).into(),
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, Serialize, JsonSchema)]
76pub struct ConflictRangeReport {
77 pub start_line: u32,
78 pub end_line: u32,
79}
80
81impl From<ConflictRange> for ConflictRangeReport {
82 fn from(range: ConflictRange) -> Self {
83 Self {
84 start_line: range.start_line,
85 end_line: range.end_line,
86 }
87 }
88}
89
90#[derive(Clone, Debug, Serialize, JsonSchema)]
91pub struct ConflictSideReport {
92 pub source_state: String,
93 pub blob_id: Option<String>,
94 pub range: ConflictRangeReport,
95 pub hunk_hash: String,
96}
97
98impl From<&ConflictSide> for ConflictSideReport {
99 fn from(side: &ConflictSide) -> Self {
100 Self {
101 source_state: side.source_state.to_string_full(),
102 blob_id: side.blob_id.map(|id| id.to_hex()),
103 range: side.range.into(),
104 hunk_hash: side.hunk_hash.to_hex(),
105 }
106 }
107}
108
109#[derive(Clone, Debug, Serialize, JsonSchema)]
110pub struct ConflictResolutionReport {
111 pub conflict_id: String,
112 pub path: String,
113 pub resolution: String,
114 pub mode: ConflictResolutionModeReport,
115 pub resolver: ResolverAttributionReport,
116}
117
118impl ConflictResolutionReport {
119 pub fn new(
120 conflict_id: impl Into<String>,
121 path: impl Into<String>,
122 resolver: &Attribution,
123 mode: ConflictResolutionMode,
124 ) -> Self {
125 Self {
126 conflict_id: conflict_id.into(),
127 path: path.into(),
128 resolution: mode.as_str().to_string(),
129 mode: mode.into(),
130 resolver: resolver.into(),
131 }
132 }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, JsonSchema)]
136#[serde(rename_all = "snake_case")]
137pub enum ConflictResolutionModeReport {
138 Ours,
139 Theirs,
140 Edit,
141 Auto,
142}
143
144impl From<ConflictResolutionMode> for ConflictResolutionModeReport {
145 fn from(mode: ConflictResolutionMode) -> Self {
146 match mode {
147 ConflictResolutionMode::Ours => Self::Ours,
148 ConflictResolutionMode::Theirs => Self::Theirs,
149 ConflictResolutionMode::Edit => Self::Edit,
150 ConflictResolutionMode::Auto => Self::Auto,
151 }
152 }
153}
154
155impl std::fmt::Display for ConflictResolutionModeReport {
156 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 let value = match self {
158 Self::Ours => "ours",
159 Self::Theirs => "theirs",
160 Self::Edit => "edit",
161 Self::Auto => "auto",
162 };
163 formatter.write_str(value)
164 }
165}
166
167#[derive(Clone, Debug, Serialize, JsonSchema)]
168pub struct ResolverAttributionReport {
169 pub kind: ResolverKindReport,
170 pub principal: PrincipalReport,
171 pub agent: Option<AgentReport>,
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, JsonSchema)]
175#[serde(rename_all = "snake_case")]
176pub enum ResolverKindReport {
177 Human,
178 Agent,
179}
180
181impl From<&Attribution> for ResolverAttributionReport {
182 fn from(attribution: &Attribution) -> Self {
183 Self {
184 kind: if attribution.agent.is_some() {
185 ResolverKindReport::Agent
186 } else {
187 ResolverKindReport::Human
188 },
189 principal: (&attribution.principal).into(),
190 agent: attribution.agent.as_ref().map(Into::into),
191 }
192 }
193}
194
195#[derive(Clone, Debug, Serialize, JsonSchema)]
196pub struct PrincipalReport {
197 pub name: String,
198 pub email: String,
199}
200
201impl From<&Principal> for PrincipalReport {
202 fn from(principal: &Principal) -> Self {
203 Self {
204 name: principal.name_lossy().into_owned(),
205 email: principal.email_lossy().into_owned(),
206 }
207 }
208}
209
210#[derive(Clone, Debug, Serialize, JsonSchema)]
211pub struct AgentReport {
212 pub provider: String,
213 pub model: String,
214 pub session_id: Option<String>,
215 pub segment_id: Option<String>,
216 pub policy_id: Option<String>,
217}
218
219impl From<&Agent> for AgentReport {
220 fn from(agent: &Agent) -> Self {
221 Self {
222 provider: agent.provider.clone(),
223 model: agent.model.clone(),
224 session_id: agent.session_id.clone(),
225 segment_id: agent.segment_id.clone(),
226 policy_id: agent.policy_id.clone(),
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn resolver_kind_distinguishes_human_and_agent_without_dropping_identity() {
237 let principal = Principal::new("Ada", "ada@example.com");
238 let human = ResolverAttributionReport::from(&Attribution::human(principal.clone()));
239 assert_eq!(human.kind, ResolverKindReport::Human);
240 assert!(human.agent.is_none());
241
242 let agent = ResolverAttributionReport::from(&Attribution::with_agent(
243 principal,
244 Agent::new("openai", "gpt-resolver"),
245 ));
246 assert_eq!(agent.kind, ResolverKindReport::Agent);
247 assert_eq!(agent.principal.email, "ada@example.com");
248 assert_eq!(agent.agent.unwrap().provider, "openai");
249 }
250
251 #[test]
252 fn resolution_report_preserves_automatic_vs_edited_mode() {
253 let resolver = Attribution::human(Principal::new("Ada", "ada@example.com"));
254 let automatic = ConflictResolutionReport::new(
255 "conflict-a",
256 "src/lib.rs",
257 &resolver,
258 ConflictResolutionMode::Auto,
259 );
260 let edited = ConflictResolutionReport::new(
261 "conflict-b",
262 "src/lib.rs",
263 &resolver,
264 ConflictResolutionMode::Edit,
265 );
266 assert_eq!(automatic.mode, ConflictResolutionModeReport::Auto);
267 assert_eq!(edited.mode, ConflictResolutionModeReport::Edit);
268 }
269}