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