mant_protocol/explanation/
classification.rs1use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5#[derive(
8 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
9)]
10#[serde(rename_all = "kebab-case")]
11pub enum EvidenceClass {
12 DirectEntry,
14 RelatedEntry,
16 EntryMention,
18 ContextMention,
20}
21
22impl EvidenceClass {
23 pub const ALL: [Self; 4] = [
25 Self::DirectEntry,
26 Self::RelatedEntry,
27 Self::EntryMention,
28 Self::ContextMention,
29 ];
30
31 #[must_use]
33 pub const fn title(self) -> &'static str {
34 match self {
35 Self::DirectEntry => "Direct entries",
36 Self::RelatedEntry => "Explicitly related entries",
37 Self::EntryMention => "Mentions in other entries",
38 Self::ContextMention => "Mentions in ordinary content",
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45#[serde(rename_all = "kebab-case")]
46pub enum EvidenceOrder {
47 #[default]
49 ClassThenSource,
50}
51
52#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55pub struct EvidenceCount {
56 pub total: u32,
58 pub returned: u32,
60}
61
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct EvidenceCounts {
66 pub direct_entry: EvidenceCount,
68 pub related_entry: EvidenceCount,
70 pub entry_mention: EvidenceCount,
72 pub context_mention: EvidenceCount,
74}
75impl EvidenceCounts {
76 #[must_use]
78 pub const fn get(&self, class: EvidenceClass) -> EvidenceCount {
79 match class {
80 EvidenceClass::DirectEntry => self.direct_entry,
81 EvidenceClass::RelatedEntry => self.related_entry,
82 EvidenceClass::EntryMention => self.entry_mention,
83 EvidenceClass::ContextMention => self.context_mention,
84 }
85 }
86 pub fn record(&mut self, class: EvidenceClass, returned: bool) {
88 let count = match class {
89 EvidenceClass::DirectEntry => &mut self.direct_entry,
90 EvidenceClass::RelatedEntry => &mut self.related_entry,
91 EvidenceClass::EntryMention => &mut self.entry_mention,
92 EvidenceClass::ContextMention => &mut self.context_mention,
93 };
94 count.total = count.total.saturating_add(1);
95 count.returned = count.returned.saturating_add(u32::from(returned));
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
101#[serde(rename_all = "camelCase", deny_unknown_fields)]
102pub struct ExplanationPreview {
103 pub block_path: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub source: Option<mant_ir::SourceSpan>,
110 pub text: String,
112 pub match_start_char: u32,
114 pub match_end_char: u32,
116 pub content_ranges: Vec<super::ExplanationContentRange>,
119 pub clipped_before: bool,
121 pub clipped_after: bool,
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 #[test]
129 fn class_order_counts_and_closed_preview_are_explicit() {
130 assert!(EvidenceClass::ALL.windows(2).all(|w| w[0] < w[1]));
131 let mut counts = EvidenceCounts::default();
132 counts.record(EvidenceClass::DirectEntry, true);
133 counts.record(EvidenceClass::ContextMention, false);
134 assert_eq!(
135 counts.direct_entry,
136 EvidenceCount {
137 total: 1,
138 returned: 1
139 }
140 );
141 assert_eq!(
142 counts.context_mention,
143 EvidenceCount {
144 total: 1,
145 returned: 0
146 }
147 );
148 assert_eq!(
149 serde_json::to_value(EvidenceOrder::default()).unwrap(),
150 "class-then-source"
151 );
152 assert!(
153 serde_json::from_value::<EvidenceCounts>(
154 serde_json::json!({"directEntry":{"total":0,"returned":0}})
155 )
156 .is_err()
157 );
158 assert!(
159 serde_json::from_value::<ExplanationPreview>(serde_json::json!({
160 "blockPath":"root/b0", "text":"日本", "matchStartChar":0, "matchEndChar":2,
161 "clippedBefore":false, "clippedAfter":false, "unknown":true
162 }))
163 .is_err()
164 );
165 }
166}