1use crate::model::Severity;
7use crate::recommendation::{MetricSnapshot, RecStatus};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use std::collections::BTreeMap;
11
12pub const SCHEMA_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct LoopPersisted {
18 #[serde(default = "default_schema_version")]
19 pub schema_version: u32,
20 #[serde(default)]
22 pub config: BTreeMap<String, AnalyzerConfig>,
23 #[serde(default)]
24 pub state: LoopState,
25 #[serde(default)]
27 pub status_index: BTreeMap<String, RecStatus>,
28 #[serde(default)]
30 pub audit_heads: BTreeMap<String, String>,
31 #[serde(default)]
33 pub creators: BTreeMap<String, String>,
34 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
40 pub co_creators: BTreeMap<String, String>,
41 #[serde(default)]
43 pub cooldowns: BTreeMap<String, i64>,
44 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
49 pub cooldown_strikes: BTreeMap<String, u32>,
50 #[serde(default)]
52 pub applied: BTreeMap<String, AppliedRecord>,
53 #[serde(default)]
56 pub measured: BTreeMap<String, Vec<i64>>,
57 #[serde(default)]
60 pub outcomes: BTreeMap<String, Vec<crate::recommendation::OutcomeResult>>,
61}
62
63fn default_schema_version() -> u32 {
64 SCHEMA_VERSION
65}
66
67impl Default for LoopPersisted {
68 fn default() -> Self {
69 LoopPersisted {
70 schema_version: SCHEMA_VERSION,
71 config: BTreeMap::new(),
72 state: LoopState::default(),
73 status_index: BTreeMap::new(),
74 audit_heads: BTreeMap::new(),
75 creators: BTreeMap::new(),
76 co_creators: BTreeMap::new(),
77 cooldowns: BTreeMap::new(),
78 cooldown_strikes: BTreeMap::new(),
79 applied: BTreeMap::new(),
80 measured: BTreeMap::new(),
81 outcomes: BTreeMap::new(),
82 }
83 }
84}
85
86impl LoopPersisted {
87 pub fn from_value(v: Value) -> crate::error::Result<Self> {
90 if v.is_null() {
91 return Ok(Self::default());
92 }
93 serde_json::from_value(v)
94 .map_err(|e| crate::error::Error::Internal(format!("decode loop state: {e}")))
95 }
96
97 pub fn to_value(&self) -> crate::error::Result<Value> {
98 serde_json::to_value(self)
99 .map_err(|e| crate::error::Error::Internal(format!("encode loop state: {e}")))
100 }
101}
102
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106pub struct AnalyzerConfig {
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub enabled: Option<bool>,
110 #[serde(default)]
111 pub params: Map<String, Value>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub severity_floor: Option<Severity>,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
115 pub namespaces: Vec<String>,
116}
117
118#[derive(Debug, Clone, Default, Deserialize)]
123pub struct AnalyzerConfigUpdate {
124 #[serde(default)]
126 pub enabled: Option<bool>,
127 #[serde(default)]
130 pub severity_floor: Option<Severity>,
131 #[serde(default)]
132 pub clear_floor: bool,
133 #[serde(default)]
136 pub params: Option<Map<String, Value>>,
137 #[serde(default)]
139 pub namespaces: Option<Vec<String>>,
140}
141
142#[derive(Debug, Clone, Serialize)]
145pub struct AnalyzerSetting {
146 pub id: String,
147 pub title: String,
148 pub description: String,
151 pub tier: String,
152 pub trust_class: String,
153 pub default_on: bool,
154 pub enabled: bool,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub severity_floor: Option<String>,
158}
159
160#[derive(Debug, Clone, Default, Serialize, Deserialize)]
162pub struct LoopState {
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub last_run_ms: Option<i64>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub watermark_ms: Option<i64>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AppliedRecord {
173 pub applied_at_ms: i64,
174 pub target_ref: String,
175 pub rollbackable: bool,
176 #[serde(default)]
178 pub created_hashes: Vec<String>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub metric: Option<MetricSnapshot>,
181}