1use crate::common::{
4 ExtensionsV1, ValidationError, deserialize_schema_version, validate_schema_version,
5};
6use serde::de::Error as DeError;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum KnowledgeSourceType {
14 Repository,
15 Documentation,
16 Provider,
17 UserInput,
18 BuildArtifact,
19 TestResult,
20 Other,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum KnowledgeAuthority {
27 Primary,
28 Verified,
29 Secondary,
30 Derived,
31 UserProvided,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct KnowledgeValidityV1 {
38 pub valid_from: String,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub valid_until: Option<String>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct SourceReference {
49 #[serde(rename = "type")]
50 pub source_type: String,
51 pub uri: String,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub commit_sha: Option<String>,
54 pub access_timestamp: String,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct AuthorityMetadata {
60 pub owner: String,
61 pub confidence_level: f32,
62 pub review_status: String,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct ValidityWindow {
68 pub valid_from: String,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub valid_until: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub superseded_by: Option<String>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub enum ClassificationLevel {
78 Public,
79 Internal,
80 Confidential,
81 Restricted,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct DataClassification {
87 pub level: ClassificationLevel,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub retention_days: Option<u32>,
90}
91
92impl DataClassification {
93 fn from_legacy_value(value: &str) -> Result<Self, String> {
94 let level = match value.to_ascii_lowercase().as_str() {
95 "public" => ClassificationLevel::Public,
96 "internal" => ClassificationLevel::Internal,
97 "confidential" => ClassificationLevel::Confidential,
98 "restricted" => ClassificationLevel::Restricted,
99 other => return Err(format!("unknown data classification: {other}")),
100 };
101 Ok(Self {
102 level,
103 retention_days: None,
104 })
105 }
106}
107
108fn deserialize_optional_source_ref<'de, D>(
109 deserializer: D,
110) -> Result<Option<SourceReference>, D::Error>
111where
112 D: serde::Deserializer<'de>,
113{
114 let value = Option::<Value>::deserialize(deserializer)?;
115 match value {
116 None => Ok(None),
117 Some(Value::String(uri)) => Ok(Some(SourceReference {
118 source_type: "legacy".to_owned(),
119 uri,
120 commit_sha: None,
121 access_timestamp: String::new(),
122 })),
123 Some(value) => serde_json::from_value(value)
124 .map(Some)
125 .map_err(D::Error::custom),
126 }
127}
128
129fn deserialize_optional_authority<'de, D>(
130 deserializer: D,
131) -> Result<Option<AuthorityMetadata>, D::Error>
132where
133 D: serde::Deserializer<'de>,
134{
135 let value = Option::<Value>::deserialize(deserializer)?;
136 match value {
137 None => Ok(None),
138 Some(Value::String(review_status)) => Ok(Some(AuthorityMetadata {
139 owner: String::new(),
140 confidence_level: 0.0,
141 review_status,
142 })),
143 Some(value) => serde_json::from_value(value)
144 .map(Some)
145 .map_err(D::Error::custom),
146 }
147}
148
149fn deserialize_optional_classification<'de, D>(
150 deserializer: D,
151) -> Result<Option<DataClassification>, D::Error>
152where
153 D: serde::Deserializer<'de>,
154{
155 let value = Option::<Value>::deserialize(deserializer)?;
156 match value {
157 None => Ok(None),
158 Some(Value::String(level)) => DataClassification::from_legacy_value(&level)
159 .map(Some)
160 .map_err(D::Error::custom),
161 Some(value) => serde_json::from_value(value)
162 .map(Some)
163 .map_err(D::Error::custom),
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169pub struct KnowledgeObjectV1 {
170 #[serde(deserialize_with = "deserialize_schema_version")]
171 pub schema_version: u32,
172 #[serde(deserialize_with = "deserialize_optional_source_ref")]
175 pub source_ref: Option<SourceReference>,
176 pub source_type: KnowledgeSourceType,
177 #[serde(
178 default,
179 skip_serializing_if = "Option::is_none",
180 deserialize_with = "deserialize_optional_authority"
181 )]
182 pub authority: Option<AuthorityMetadata>,
183 pub owner: String,
184 #[serde(
185 default,
186 skip_serializing_if = "Option::is_none",
187 deserialize_with = "deserialize_optional_classification"
188 )]
189 pub classification: Option<DataClassification>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub validity: Option<ValidityWindow>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub supersedes: Option<String>,
194 pub content_hash: String,
195 pub evidence_digest: String,
196 pub policy_ref: String,
197 #[serde(default)]
198 pub evidence_refs: Vec<String>,
199 #[serde(default, flatten)]
200 pub extra: ExtensionsV1,
201}
202
203const KNOWLEDGE_RESERVED_FIELDS: &[&str] = &[
204 "schema_version",
205 "source_ref",
206 "source_type",
207 "authority",
208 "owner",
209 "classification",
210 "validity",
211 "supersedes",
212 "content_hash",
213 "evidence_digest",
214 "policy_ref",
215 "evidence_refs",
216];
217
218impl KnowledgeObjectV1 {
219 pub fn validate(&self) -> Result<(), ValidationError> {
221 self.extra.validate_reserved(KNOWLEDGE_RESERVED_FIELDS)?;
222 validate_schema_version(self.schema_version)
223 }
224
225 pub fn object_id(&self) -> &str {
227 &self.content_hash
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn serialization_round_trip() {
237 let knowledge = KnowledgeObjectV1 {
238 schema_version: 1,
239 source_ref: Some(SourceReference {
240 source_type: "documentation".to_owned(),
241 uri: "docs:architecture".to_owned(),
242 commit_sha: None,
243 access_timestamp: "2026-08-09T00:00:00Z".to_owned(),
244 }),
245 source_type: KnowledgeSourceType::Documentation,
246 authority: Some(AuthorityMetadata {
247 owner: "team-platform".to_owned(),
248 confidence_level: 0.95,
249 review_status: "verified".to_owned(),
250 }),
251 owner: "team-platform".to_owned(),
252 classification: Some(DataClassification {
253 level: ClassificationLevel::Internal,
254 retention_days: Some(365),
255 }),
256 validity: Some(ValidityWindow {
257 valid_from: "2026-08-09T00:00:00Z".to_owned(),
258 valid_until: None,
259 superseded_by: None,
260 }),
261 supersedes: Some("docs:architecture-old".to_owned()),
262 content_hash: "sha256:content".to_owned(),
263 evidence_digest: "sha256:evidence".to_owned(),
264 policy_ref: "policy:knowledge".to_owned(),
265 evidence_refs: vec!["evidence:architecture".to_owned()],
266 extra: {
267 let mut extra = ExtensionsV1::default();
268 extra
269 .insert("extension", Value::from("kept"))
270 .expect("extension should be valid");
271 extra
272 },
273 };
274 let json = serde_json::to_string(&knowledge).expect("knowledge should serialize");
275 let decoded: KnowledgeObjectV1 =
276 serde_json::from_str(&json).expect("knowledge should deserialize");
277 assert_eq!(knowledge, decoded);
278 knowledge
279 .validate()
280 .expect("knowledge should satisfy invariants");
281 }
282
283 #[test]
284 fn legacy_scalar_metadata_deserializes_into_optional_fields() {
285 let json = r#"{
286 "schema_version": 1,
287 "source_ref": "docs:architecture",
288 "source_type": "documentation",
289 "authority": "verified",
290 "owner": "team-platform",
291 "classification": "Internal",
292 "validity": {"valid_from": "2026-08-09T00:00:00Z"},
293 "content_hash": "sha256:content",
294 "evidence_digest": "sha256:evidence",
295 "policy_ref": "policy:knowledge"
296 }"#;
297
298 let knowledge: KnowledgeObjectV1 = serde_json::from_str(json).expect("legacy JSON parses");
299 assert_eq!(
300 knowledge
301 .source_ref
302 .as_ref()
303 .map(|source| source.uri.as_str()),
304 Some("docs:architecture")
305 );
306 assert_eq!(
307 knowledge
308 .authority
309 .as_ref()
310 .map(|authority| authority.review_status.as_str()),
311 Some("verified")
312 );
313 assert_eq!(
314 knowledge
315 .classification
316 .as_ref()
317 .map(|classification| classification.level),
318 Some(ClassificationLevel::Internal)
319 );
320 assert_eq!(knowledge.evidence_refs, Vec::<String>::new());
321 }
322}