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