Skip to main content

difflore_core/cloud/observations/
events.rs

1use 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        // Outbox row ids of mcp_rule_served events that surfaced this rule
54        // shortly before the accepted edit for the same scope. Populated at
55        // enqueue time so the audited cross-link
56        // `acceptedOutcomesLinkedToMcpRuleServe` can fire even when
57        // session_id/file_path heuristics would otherwise miss it.
58        // Older outbox rows without this field deserialize as `Vec::new()`.
59        #[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}