difflore_core/cloud/observations/
events.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(tag = "event_type", rename_all = "snake_case")]
6pub enum ObservationEvent {
7 RuleFired {
8 rule_ids: Vec<String>,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 file_path: Option<String>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 intent: Option<String>,
13 session_id: String,
14 fired_at: DateTime<Utc>,
15 },
16 McpRuleServed {
17 tool: String,
18 session_id: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 repo_full_name: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 file_path: Option<String>,
23 query_hash: String,
24 rule_ids: Vec<String>,
25 top_k: i64,
26 was_empty: bool,
27 strict_match_count: i64,
28 estimated_tokens: i64,
29 served_at: DateTime<Utc>,
30 },
31 RuleCitedInEdit {
32 rule_id: String,
33 session_id: String,
34 file_path: String,
35 diff_excerpt: String,
36 cited_at: DateTime<Utc>,
37 },
38 RuleActuallyCited {
39 rule_id: String,
40 session_id: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 file_path: Option<String>,
43 citation_excerpt: String,
44 cited_at: DateTime<Utc>,
45 },
46 FixOutcome {
47 rule_id: String,
48 session_id: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 file_path: Option<String>,
51 accepted: bool,
52 occurred_at: DateTime<Utc>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 mcp_serve_event_ids: Vec<i64>,
61 },
62}
63
64impl ObservationEvent {
65 pub const fn event_type(&self) -> &'static str {
66 match self {
67 Self::RuleFired { .. } => "rule_fired",
68 Self::McpRuleServed { .. } => "mcp_rule_served",
69 Self::RuleCitedInEdit { .. } => "rule_cited_in_edit",
70 Self::RuleActuallyCited { .. } => "rule_actually_cited",
71 Self::FixOutcome { .. } => "fix_outcome",
72 }
73 }
74
75 pub fn session_id(&self) -> &str {
76 match self {
77 Self::RuleFired { session_id, .. }
78 | Self::McpRuleServed { session_id, .. }
79 | Self::RuleCitedInEdit { session_id, .. }
80 | Self::RuleActuallyCited { session_id, .. }
81 | Self::FixOutcome { session_id, .. } => session_id,
82 }
83 }
84
85 pub(super) fn file_path(&self) -> Option<&str> {
86 match self {
87 Self::RuleFired { file_path, .. }
88 | Self::McpRuleServed { file_path, .. }
89 | Self::RuleActuallyCited { file_path, .. }
90 | Self::FixOutcome { file_path, .. } => file_path.as_deref(),
91 Self::RuleCitedInEdit { file_path, .. } => Some(file_path),
92 }
93 }
94
95 pub(super) fn rule_id(&self) -> Option<&str> {
96 match self {
97 Self::RuleCitedInEdit { rule_id, .. }
98 | Self::RuleActuallyCited { rule_id, .. }
99 | Self::FixOutcome { rule_id, .. } => Some(rule_id),
100 Self::RuleFired { .. } | Self::McpRuleServed { .. } => None,
101 }
102 }
103
104 pub(super) fn rule_ids(&self) -> Vec<String> {
105 match self {
106 Self::RuleFired { rule_ids, .. } | Self::McpRuleServed { rule_ids, .. } => {
107 rule_ids.clone()
108 }
109 Self::RuleCitedInEdit { rule_id, .. }
110 | Self::RuleActuallyCited { rule_id, .. }
111 | Self::FixOutcome { rule_id, .. } => vec![rule_id.clone()],
112 }
113 }
114
115 pub(super) const fn occurred_at_ms(&self) -> i64 {
116 match self {
117 Self::RuleFired { fired_at, .. } => fired_at.timestamp_millis(),
118 Self::McpRuleServed { served_at, .. } => served_at.timestamp_millis(),
119 Self::RuleCitedInEdit { cited_at, .. } | Self::RuleActuallyCited { cited_at, .. } => {
120 cited_at.timestamp_millis()
121 }
122 Self::FixOutcome { occurred_at, .. } => occurred_at.timestamp_millis(),
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct CitedEdit {
129 pub rule_id: String,
130 pub file_path: String,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct RuleFireSnapshot {
135 pub rule_ids: Vec<String>,
136 pub file_path: Option<String>,
137}
138
139#[derive(Debug, Clone, Default, PartialEq, Eq)]
140pub struct ActualCitationSummary {
141 pub actual_citations: i64,
142 pub rule_fires: i64,
143 pub pending_uploads: i64,
144 pub pending_upload_issue: Option<ObservationUploadIssue>,
145}
146
147#[derive(Debug, Clone, Default, PartialEq, Eq)]
148pub struct AcceptedRecallLinkSummary {
149 pub accepted_outcomes: i64,
150 pub linked_to_prior_recall: i64,
151 pub linked_to_rule_recall: i64,
152 pub linked_to_mcp_rule_serve: i64,
153 pub linked_to_edit_attribution: i64,
154}
155
156#[derive(Debug, Clone, Default, PartialEq, Eq)]
157pub struct AcceptedFixOutcomeRuleSummary {
158 pub rule_id: String,
159 pub accepted_outcomes: i64,
160 pub linked_to_prior_recall: i64,
161 pub linked_to_rule_recall: i64,
162 pub linked_to_mcp_rule_serve: i64,
163 pub linked_to_edit_attribution: i64,
164 pub sample_file: Option<String>,
165 pub latest_occurred_at_ms: i64,
166}
167
168#[derive(Debug, Clone, Default, PartialEq, Eq)]
169pub(super) struct PriorRuleUseLinks {
170 pub rule_recall: bool,
171 pub mcp_rule_serve: bool,
172 pub edit_attribution: bool,
173}
174
175impl PriorRuleUseLinks {
176 pub(super) const fn any(&self) -> bool {
177 self.rule_recall || self.mcp_rule_serve || self.edit_attribution
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum ObservationUploadIssue {
184 MissingCloudScope,
185 RateLimited,
186 InvalidBatch,
187 ServerRejected,
188 Unknown,
189}
190
191impl ObservationUploadIssue {
192 pub const fn as_str(self) -> &'static str {
193 match self {
194 Self::MissingCloudScope => "missing_cloud_scope",
195 Self::RateLimited => "rate_limited",
196 Self::InvalidBatch => "invalid_batch",
197 Self::ServerRejected => "server_rejected",
198 Self::Unknown => "unknown",
199 }
200 }
201}