Skip to main content

khive_runtime/graph_lint/
types.rs

1//! Typed graph-lint response and settlement evidence (ADR-048).
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use uuid::Uuid;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
7#[serde(rename_all = "kebab-case")]
8pub enum LintRuleId {
9    ConceptNoParent,
10    DanglingEdge,
11    DirectionViolation,
12    DuplicateName,
13    LowWeightEdge,
14    MinEdgeDensity,
15    MissingEntityType,
16    MissingImplements,
17    MissingIntroducedBy,
18    NoteNoAnnotates,
19    OrphanEntity,
20    PaperMissingMetadata,
21    SupersededNotMarked,
22}
23impl LintRuleId {
24    pub const ALL: [Self; 13] = [
25        Self::ConceptNoParent,
26        Self::DanglingEdge,
27        Self::DirectionViolation,
28        Self::DuplicateName,
29        Self::LowWeightEdge,
30        Self::MinEdgeDensity,
31        Self::MissingEntityType,
32        Self::MissingImplements,
33        Self::MissingIntroducedBy,
34        Self::NoteNoAnnotates,
35        Self::OrphanEntity,
36        Self::PaperMissingMetadata,
37        Self::SupersededNotMarked,
38    ];
39    pub fn as_str(self) -> &'static str {
40        match self {
41            Self::ConceptNoParent => "concept-no-parent",
42            Self::DanglingEdge => "dangling-edge",
43            Self::DirectionViolation => "direction-violation",
44            Self::DuplicateName => "duplicate-name",
45            Self::LowWeightEdge => "low-weight-edge",
46            Self::MinEdgeDensity => "min-edge-density",
47            Self::MissingEntityType => "missing-entity-type",
48            Self::MissingImplements => "missing-implements",
49            Self::MissingIntroducedBy => "missing-introduced-by",
50            Self::NoteNoAnnotates => "note-no-annotates",
51            Self::OrphanEntity => "orphan-entity",
52            Self::PaperMissingMetadata => "paper-missing-metadata",
53            Self::SupersededNotMarked => "superseded-not-marked",
54        }
55    }
56}
57#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum LintSeverity {
60    Info,
61    Warn,
62    Error,
63}
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(tag = "substrate", rename_all = "lowercase")]
66pub enum LintSubject {
67    Entity {
68        namespace: String,
69        entity_id: Uuid,
70        entity_name: String,
71        entity_kind: String,
72    },
73    Edge {
74        namespace: String,
75        edge_id: Uuid,
76    },
77    Note {
78        namespace: String,
79        note_id: Uuid,
80        note_kind: String,
81    },
82}
83impl LintSubject {
84    pub fn namespace(&self) -> &str {
85        match self {
86            Self::Entity { namespace, .. }
87            | Self::Edge { namespace, .. }
88            | Self::Note { namespace, .. } => namespace,
89        }
90    }
91    pub fn id(&self) -> Uuid {
92        match self {
93            Self::Entity { entity_id, .. } => *entity_id,
94            Self::Edge { edge_id, .. } => *edge_id,
95            Self::Note { note_id, .. } => *note_id,
96        }
97    }
98    pub fn substrate(&self) -> &'static str {
99        match self {
100            Self::Entity { .. } => "entity",
101            Self::Edge { .. } => "edge",
102            Self::Note { .. } => "note",
103        }
104    }
105}
106#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
107pub struct LintViolation {
108    #[serde(flatten)]
109    pub subject: LintSubject,
110    pub rule: LintRuleId,
111    pub severity: LintSeverity,
112    pub message: String,
113    pub suggestion: String,
114    pub auto_fixable: bool,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub member_ids: Option<Vec<Uuid>>,
117}
118#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
119pub struct LintSummary {
120    pub total: u64,
121    pub errors: u64,
122    pub warnings: u64,
123    pub infos: u64,
124    pub by_rule: BTreeMap<LintRuleId, u64>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub fixed: Option<u64>,
127}
128#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
129pub struct PropertyCoverageEntry {
130    pub present: u64,
131    pub total: u64,
132}
133#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
134pub struct PropertyCoverage {
135    pub entities: BTreeMap<String, PropertyCoverageEntry>,
136    pub notes: BTreeMap<String, PropertyCoverageEntry>,
137}
138#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
139pub struct LintStats {
140    pub total_entities: u64,
141    pub total_notes: u64,
142    pub total_edges: u64,
143    pub entity_kinds: BTreeMap<String, u64>,
144    pub note_kinds: BTreeMap<String, u64>,
145    pub edge_relations: BTreeMap<String, u64>,
146    pub avg_density: f64,
147    pub density_histogram: BTreeMap<String, u64>,
148    pub property_coverage: PropertyCoverage,
149}
150#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
151pub struct LintReport {
152    pub violations: Vec<LintViolation>,
153    pub summary: LintSummary,
154    pub stats: LintStats,
155}
156#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum FixAction {
159    SoftDeleteEdge,
160    ProposeDuplicateMerge,
161    MarkDeprecated,
162}
163#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
164pub struct ActionIdentity {
165    pub violation_index: u64,
166    pub action: FixAction,
167}
168#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(tag = "status", rename_all = "snake_case")]
170pub enum FixReceipt {
171    Committed {
172        #[serde(flatten)]
173        action: CommittedFixAction,
174    },
175    Conflict {
176        #[serde(flatten)]
177        identity: ActionIdentity,
178        reason: String,
179    },
180    Unsupported {
181        #[serde(flatten)]
182        identity: ActionIdentity,
183        reason: String,
184    },
185}
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(tag = "action", rename_all = "snake_case")]
188pub enum CommittedFixAction {
189    SoftDeleteEdge {
190        violation_index: u64,
191    },
192    MarkDeprecated {
193        violation_index: u64,
194    },
195    ProposeDuplicateMerge {
196        violation_index: u64,
197        question_note_id: Uuid,
198    },
199}
200impl FixReceipt {
201    pub fn is_committed(&self) -> bool {
202        matches!(self, Self::Committed { .. })
203    }
204}
205#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "lowercase")]
207pub enum LintFixOutcome {
208    Unknown,
209    Failed,
210}
211#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
212pub struct LintFixFailureDetail {
213    pub outcome: LintFixOutcome,
214    pub failed_action: ActionIdentity,
215    pub confirmed_receipts: Vec<FixReceipt>,
216    pub report: LintReport,
217}