1use serde::{Deserialize, Serialize};
2
3use super::{DomainError, GraphVersion, error::required_text};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum WorkerKind {
9 Embedding,
10 Ocr,
11 Vision,
12 Extractor,
13}
14
15impl WorkerKind {
16 pub const ALL: [Self; 4] = [Self::Embedding, Self::Ocr, Self::Vision, Self::Extractor];
17
18 pub const fn as_str(self) -> &'static str {
20 match self {
21 Self::Embedding => "embedding",
22 Self::Ocr => "ocr",
23 Self::Vision => "vision",
24 Self::Extractor => "extractor",
25 }
26 }
27
28 pub fn parse(value: &str) -> Result<Self, DomainError> {
30 match value {
31 "embedding" => Ok(Self::Embedding),
32 "ocr" => Ok(Self::Ocr),
33 "vision" => Ok(Self::Vision),
34 "extractor" => Ok(Self::Extractor),
35 _ => Err(DomainError::invalid("worker_kind", "unknown worker kind")),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum WorkerTaskState {
44 Queued,
45 Running,
46 Succeeded,
47 Retrying,
48 Failed,
49 DeadLetter,
50}
51
52impl WorkerTaskState {
53 pub const fn as_str(self) -> &'static str {
55 match self {
56 Self::Queued => "queued",
57 Self::Running => "running",
58 Self::Succeeded => "succeeded",
59 Self::Retrying => "retrying",
60 Self::Failed => "failed",
61 Self::DeadLetter => "dead_letter",
62 }
63 }
64
65 pub fn parse(value: &str) -> Result<Self, DomainError> {
67 match value {
68 "queued" => Ok(Self::Queued),
69 "running" => Ok(Self::Running),
70 "succeeded" => Ok(Self::Succeeded),
71 "retrying" => Ok(Self::Retrying),
72 "failed" => Ok(Self::Failed),
73 "dead_letter" => Ok(Self::DeadLetter),
74 _ => Err(DomainError::invalid(
75 "worker_task_state",
76 "unknown worker task state",
77 )),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum WorkerBackendState {
86 Fallback,
87 Configured,
88 Degraded,
89 Unavailable,
90}
91
92impl WorkerBackendState {
93 pub const fn as_str(self) -> &'static str {
95 match self {
96 Self::Fallback => "fallback",
97 Self::Configured => "configured",
98 Self::Degraded => "degraded",
99 Self::Unavailable => "unavailable",
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct WorkerTaskRecord {
107 pub task_id: String,
108 pub kind: WorkerKind,
109 pub source_scope: String,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub evidence_id: Option<String>,
112 pub target_graph_version: GraphVersion,
113 pub state: WorkerTaskState,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub lease_owner: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub lease_expires_at_ms: Option<u64>,
118 pub attempt_count: u32,
119 pub next_retry_at_ms: u64,
120 pub input_fingerprint: String,
121 pub payload_json: String,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub last_error_kind: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub last_error_message: Option<String>,
126 pub created_at_ms: u64,
127 pub updated_at_ms: u64,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct WorkerStatus {
133 pub kind: WorkerKind,
134 pub backend_state: WorkerBackendState,
135 pub endpoint_configured: bool,
136 pub queue_depth: usize,
137 pub running_count: usize,
138 pub retrying_count: usize,
139 pub dead_letter_count: usize,
140 pub last_error: Option<String>,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum ProposalKind {
147 Evidence,
148 Relation,
149 Claim,
150 Event,
151}
152
153impl ProposalKind {
154 pub const fn as_str(self) -> &'static str {
156 match self {
157 Self::Evidence => "evidence",
158 Self::Relation => "relation",
159 Self::Claim => "claim",
160 Self::Event => "event",
161 }
162 }
163
164 pub fn parse(value: &str) -> Result<Self, DomainError> {
166 match value {
167 "evidence" => Ok(Self::Evidence),
168 "relation" => Ok(Self::Relation),
169 "claim" => Ok(Self::Claim),
170 "event" => Ok(Self::Event),
171 _ => Err(DomainError::invalid(
172 "proposal_kind",
173 "unknown proposal kind",
174 )),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "snake_case")]
182pub enum ProposalState {
183 Proposed,
184 Accepted,
185 Rejected,
186 Superseded,
187}
188
189impl ProposalState {
190 pub const fn as_str(self) -> &'static str {
192 match self {
193 Self::Proposed => "proposed",
194 Self::Accepted => "accepted",
195 Self::Rejected => "rejected",
196 Self::Superseded => "superseded",
197 }
198 }
199
200 pub fn parse(value: &str) -> Result<Self, DomainError> {
202 match value {
203 "proposed" => Ok(Self::Proposed),
204 "accepted" => Ok(Self::Accepted),
205 "rejected" => Ok(Self::Rejected),
206 "superseded" => Ok(Self::Superseded),
207 _ => Err(DomainError::invalid(
208 "proposal_state",
209 "unknown proposal state",
210 )),
211 }
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "snake_case")]
218pub enum ProposalConflictSeverity {
219 Info,
220 Warning,
221 Blocking,
222}
223
224impl ProposalConflictSeverity {
225 pub const fn as_str(self) -> &'static str {
227 match self {
228 Self::Info => "info",
229 Self::Warning => "warning",
230 Self::Blocking => "blocking",
231 }
232 }
233
234 pub fn parse(value: &str) -> Result<Self, DomainError> {
236 match value {
237 "info" => Ok(Self::Info),
238 "warning" => Ok(Self::Warning),
239 "blocking" => Ok(Self::Blocking),
240 _ => Err(DomainError::invalid(
241 "proposal_conflict_severity",
242 "unknown proposal conflict severity",
243 )),
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct ProposalRecord {
251 pub proposal_id: String,
252 pub source_scope: String,
253 pub kind: ProposalKind,
254 pub state: ProposalState,
255 pub title: String,
256 pub summary: String,
257 pub payload_json: String,
258 pub origin: String,
259 pub provenance: ProposalProvenance,
260 pub confidence_basis_points: u16,
261 pub conflict_count: usize,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub decided_by: Option<String>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub decision_reason: Option<String>,
266 pub created_at_ms: u64,
267 pub updated_at_ms: u64,
268}
269
270impl ProposalRecord {
271 pub fn payload_value(&self) -> serde_json::Value {
273 serde_json::from_str(&self.payload_json).unwrap_or(serde_json::Value::Null)
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct ProposalProvenance {
280 pub producer: String,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub provider: Option<String>,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub model: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub prompt_id: Option<String>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub prompt_version: Option<String>,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub schema_version: Option<String>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub input_source_hash: Option<String>,
293 #[serde(default, skip_serializing_if = "Vec::is_empty")]
294 pub input_fact_ids: Vec<String>,
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
296 pub stale_when: Vec<String>,
297 #[serde(default, skip_serializing_if = "Vec::is_empty")]
298 pub budget_notes: Vec<String>,
299}
300
301impl Default for ProposalProvenance {
302 fn default() -> Self {
303 Self::new("unspecified")
304 }
305}
306
307impl ProposalProvenance {
308 pub fn new(producer: impl Into<String>) -> Self {
310 Self {
311 producer: producer.into(),
312 provider: None,
313 model: None,
314 prompt_id: None,
315 prompt_version: None,
316 schema_version: None,
317 input_source_hash: None,
318 input_fact_ids: Vec::new(),
319 stale_when: Vec::new(),
320 budget_notes: Vec::new(),
321 }
322 }
323
324 pub fn from_json(value: &str) -> Result<Self, DomainError> {
326 if value.trim().is_empty() || value.trim() == "{}" {
327 return Ok(Self::default());
328 }
329
330 serde_json::from_str::<Self>(value)
331 .map_err(|_| DomainError::invalid("proposal_provenance", "must be valid JSON"))?
332 .validate()
333 }
334
335 pub fn to_json(&self) -> String {
337 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_owned())
338 }
339
340 pub fn validate(mut self) -> Result<Self, DomainError> {
342 self.producer = required_text("proposal_producer", self.producer)?;
343 self.provider = normalize_optional_text("proposal_provider", self.provider)?;
344 self.model = normalize_optional_text("proposal_model", self.model)?;
345 self.prompt_id = normalize_optional_text("proposal_prompt_id", self.prompt_id)?;
346 self.prompt_version =
347 normalize_optional_text("proposal_prompt_version", self.prompt_version)?;
348 self.schema_version =
349 normalize_optional_text("proposal_schema_version", self.schema_version)?;
350 self.input_source_hash =
351 normalize_optional_text("proposal_input_source_hash", self.input_source_hash)?;
352 self.input_fact_ids = normalize_text_list("proposal_input_fact_id", self.input_fact_ids)?;
353 self.stale_when = normalize_text_list("proposal_stale_condition", self.stale_when)?;
354 self.budget_notes = normalize_text_list("proposal_budget_note", self.budget_notes)?;
355
356 Ok(self)
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct ProposalConflictRecord {
363 pub conflict_id: String,
364 pub proposal_id: String,
365 pub existing_fact_kind: String,
366 pub existing_fact_id: String,
367 pub severity: ProposalConflictSeverity,
368 pub reason: String,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum AuditStatus {
375 Started,
376 Completed,
377 Failed,
378 Cancelled,
379}
380
381impl AuditStatus {
382 pub const fn as_str(self) -> &'static str {
384 match self {
385 Self::Started => "started",
386 Self::Completed => "completed",
387 Self::Failed => "failed",
388 Self::Cancelled => "cancelled",
389 }
390 }
391
392 pub fn parse(value: &str) -> Result<Self, DomainError> {
394 match value {
395 "started" => Ok(Self::Started),
396 "completed" => Ok(Self::Completed),
397 "failed" => Ok(Self::Failed),
398 "cancelled" => Ok(Self::Cancelled),
399 _ => Err(DomainError::invalid("audit_status", "unknown audit status")),
400 }
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406pub struct AuditEventRecord {
407 pub sequence: u64,
408 pub operation: String,
409 pub interface: String,
410 pub request_id: String,
411 pub trace_id: String,
412 pub status: AuditStatus,
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub actor: Option<String>,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub source_scope: Option<String>,
417 pub graph_version: u64,
418 pub detail_json: String,
419 #[serde(skip_serializing_if = "Option::is_none")]
420 pub message: Option<String>,
421 pub created_at_ms: u64,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426#[serde(rename_all = "snake_case")]
427pub enum ServiceOperatorState {
428 Disabled,
429 Enabled,
430 Paused,
431 Degraded,
432 Failed,
433}
434
435impl ServiceOperatorState {
436 pub const fn as_str(self) -> &'static str {
438 match self {
439 Self::Disabled => "disabled",
440 Self::Enabled => "enabled",
441 Self::Paused => "paused",
442 Self::Degraded => "degraded",
443 Self::Failed => "failed",
444 }
445 }
446
447 pub fn parse(value: &str) -> Result<Self, DomainError> {
449 match value {
450 "disabled" => Ok(Self::Disabled),
451 "enabled" => Ok(Self::Enabled),
452 "paused" => Ok(Self::Paused),
453 "degraded" => Ok(Self::Degraded),
454 "failed" => Ok(Self::Failed),
455 _ => Err(DomainError::invalid(
456 "service_operator_state",
457 "unknown service operator state",
458 )),
459 }
460 }
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465pub struct ServiceOperatorStatus {
466 pub state: ServiceOperatorState,
467 pub silent_updates_enabled: bool,
468 pub allowed_scopes: Vec<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
470 pub last_run_at_ms: Option<u64>,
471 #[serde(skip_serializing_if = "Option::is_none")]
472 pub next_retry_at_ms: Option<u64>,
473 #[serde(skip_serializing_if = "Option::is_none")]
474 pub last_error: Option<String>,
475 pub updated_at_ms: u64,
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
480#[serde(rename_all = "snake_case")]
481pub enum ServiceManagerAction {
482 Install,
483 Upgrade,
484 Rollback,
485 Uninstall,
486}
487
488impl ServiceManagerAction {
489 pub const fn as_str(self) -> &'static str {
491 match self {
492 Self::Install => "install",
493 Self::Upgrade => "upgrade",
494 Self::Rollback => "rollback",
495 Self::Uninstall => "uninstall",
496 }
497 }
498
499 pub fn parse(value: &str) -> Result<Self, DomainError> {
501 match value {
502 "install" => Ok(Self::Install),
503 "upgrade" => Ok(Self::Upgrade),
504 "rollback" => Ok(Self::Rollback),
505 "uninstall" => Ok(Self::Uninstall),
506 _ => Err(DomainError::invalid(
507 "service_manager_action",
508 "unknown service manager action",
509 )),
510 }
511 }
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
516pub struct ServiceDefinitionPlan {
517 pub action: ServiceManagerAction,
518 pub dry_run: bool,
519 pub platform: String,
520 pub service_name: String,
521 #[serde(skip_serializing_if = "Option::is_none")]
522 pub target_version: Option<String>,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub install_dir: Option<String>,
525 pub binary_path: String,
526 pub definition_path: String,
527 pub definition: String,
528 pub install_command: Vec<String>,
529 pub uninstall_command: Vec<String>,
530 pub start_command: Vec<String>,
531 pub stop_command: Vec<String>,
532 pub lifecycle_steps: Vec<ServiceLifecycleStep>,
533 pub rollback_steps: Vec<ServiceLifecycleStep>,
534 pub permission_requirements: Vec<ServicePermissionRequirement>,
535 pub package_manifest_checks: Vec<ServicePackageManifestCheck>,
536 pub runtime_state_paths: Vec<String>,
537 pub checkpoint_path: String,
538 pub warnings: Vec<String>,
539 pub checksum: String,
540}
541
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544pub struct ServiceLifecycleStep {
545 pub id: String,
546 pub phase: String,
547 pub description: String,
548 pub command: Vec<String>,
549 pub writes_paths: Vec<String>,
550 pub removes_paths: Vec<String>,
551 pub requires_privilege: bool,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
556pub struct ServicePermissionRequirement {
557 pub scope: String,
558 pub reason: String,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
563pub struct ServicePackageManifestCheck {
564 pub manager: String,
565 pub artifact_source: String,
566 pub verification: String,
567}
568
569#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
571pub struct ServiceLifecycleStepResult {
572 pub step_id: String,
573 pub status: String,
574 pub message: String,
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579pub struct ServiceLifecycleExecutionReport {
580 pub executed: bool,
581 pub dry_run: bool,
582 pub completed_steps: Vec<ServiceLifecycleStepResult>,
583 pub rollback_steps: Vec<ServiceLifecycleStepResult>,
584 pub rolled_back: bool,
585 #[serde(skip_serializing_if = "Option::is_none")]
586 pub failed_step_id: Option<String>,
587}
588
589pub fn normalize_actor(value: impl Into<String>) -> Result<String, DomainError> {
591 required_text("actor", value)
592}
593
594fn normalize_optional_text(
595 field: &'static str,
596 value: Option<String>,
597) -> Result<Option<String>, DomainError> {
598 value.map(|inner| required_text(field, inner)).transpose()
599}
600
601fn normalize_text_list(
602 field: &'static str,
603 values: Vec<String>,
604) -> Result<Vec<String>, DomainError> {
605 let mut normalized = Vec::new();
606 for value in values {
607 let value = required_text(field, value)?;
608 if !normalized.contains(&value) {
609 normalized.push(value);
610 }
611 }
612
613 Ok(normalized)
614}
615
616#[cfg(test)]
617#[path = "mod_tests.rs"]
618mod tests;