1use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12use crate::{ClaimCeiling, PolicyContribution, PolicyError, PolicyOutcome};
13
14#[derive(
16 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
17)]
18#[serde(rename_all = "snake_case")]
19pub enum ProvenanceClass {
20 UnknownProvenance,
22 SimulatedOrHypothetical,
24 ExternalClaimed,
26 SummaryDerived,
28 RuntimeDerived,
30 ToolObserved,
32 OperatorAttested,
34}
35
36impl ProvenanceClass {
37 #[must_use]
40 pub const fn is_derived(self) -> bool {
41 matches!(
42 self,
43 Self::RuntimeDerived | Self::SummaryDerived | Self::SimulatedOrHypothetical
44 )
45 }
46
47 #[must_use]
50 pub const fn can_corroborate(self) -> bool {
51 matches!(self, Self::ToolObserved | Self::OperatorAttested)
52 }
53
54 #[must_use]
59 pub const fn claim_ceiling(self) -> ClaimCeiling {
60 match self {
61 Self::UnknownProvenance | Self::SimulatedOrHypothetical => ClaimCeiling::DevOnly,
62 Self::ExternalClaimed | Self::SummaryDerived | Self::RuntimeDerived => {
63 ClaimCeiling::LocalUnsigned
64 }
65 Self::ToolObserved | Self::OperatorAttested => ClaimCeiling::AuthorityGrade,
66 }
67 }
68}
69
70#[derive(
72 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
73)]
74#[serde(rename_all = "snake_case")]
75pub enum SemanticTrustClass {
76 Unknown,
78 CandidateOnly,
80 SingleFamily,
82 Corroborated,
84 FalsificationTested,
87}
88
89impl SemanticTrustClass {
90 #[must_use]
92 pub const fn claim_ceiling(self) -> ClaimCeiling {
93 match self {
94 Self::Unknown => ClaimCeiling::DevOnly,
95 Self::CandidateOnly | Self::SingleFamily => ClaimCeiling::LocalUnsigned,
96 Self::Corroborated => ClaimCeiling::SignedLocalLedger,
97 Self::FalsificationTested => ClaimCeiling::AuthorityGrade,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "snake_case")]
105pub enum SemanticUse {
106 CandidateMemory,
108 DefaultContext,
110 AdvisoryDoctrine,
112 HighForceDoctrine,
114 TrustedExternalUse,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
120pub struct SemanticTrustInput {
121 pub intended_use: SemanticUse,
123 pub provenance_classes: Vec<ProvenanceClass>,
125 pub independent_source_families: u16,
127 pub falsification_evidence: bool,
129 pub unresolved_unknowns: bool,
131}
132
133impl SemanticTrustInput {
134 #[must_use]
136 pub fn new(intended_use: SemanticUse) -> Self {
137 Self {
138 intended_use,
139 provenance_classes: Vec::new(),
140 independent_source_families: 0,
141 falsification_evidence: false,
142 unresolved_unknowns: true,
143 }
144 }
145
146 #[must_use]
148 pub fn with_provenance<I>(mut self, provenance_classes: I) -> Self
149 where
150 I: IntoIterator<Item = ProvenanceClass>,
151 {
152 self.provenance_classes = provenance_classes.into_iter().collect();
153 self
154 }
155
156 #[must_use]
158 pub const fn with_independent_source_families(mut self, count: u16) -> Self {
159 self.independent_source_families = count;
160 self
161 }
162
163 #[must_use]
165 pub const fn with_falsification_evidence(mut self, present: bool) -> Self {
166 self.falsification_evidence = present;
167 self
168 }
169
170 #[must_use]
172 pub const fn with_unresolved_unknowns(mut self, present: bool) -> Self {
173 self.unresolved_unknowns = present;
174 self
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
180pub struct SemanticTrustReport {
181 pub semantic_trust: SemanticTrustClass,
183 pub weakest_provenance: ProvenanceClass,
185 pub policy_outcome: PolicyOutcome,
187 pub claim_ceiling: ClaimCeiling,
189 pub reasons: Vec<String>,
191}
192
193impl SemanticTrustReport {
194 #[must_use]
196 pub const fn is_allow(&self) -> bool {
197 matches!(self.policy_outcome, PolicyOutcome::Allow)
198 }
199
200 pub fn policy_contribution(&self) -> Result<PolicyContribution, PolicyError> {
202 PolicyContribution::new(
203 "semantic_trust.provenance",
204 self.policy_outcome,
205 self.reasons
206 .first()
207 .cloned()
208 .unwrap_or_else(|| "semantic trust evaluated".to_string()),
209 )
210 }
211}
212
213#[must_use]
215pub fn evaluate_semantic_trust(input: &SemanticTrustInput) -> SemanticTrustReport {
216 let weakest_provenance = input
217 .provenance_classes
218 .iter()
219 .copied()
220 .min()
221 .unwrap_or(ProvenanceClass::UnknownProvenance);
222
223 let has_unknown = input.provenance_classes.is_empty()
224 || input.unresolved_unknowns
225 || input
226 .provenance_classes
227 .contains(&ProvenanceClass::UnknownProvenance);
228 let corroborating_families = input
229 .provenance_classes
230 .iter()
231 .filter(|class| class.can_corroborate())
232 .count() as u16;
233 let runtime_or_weaker_only = !input.provenance_classes.is_empty()
234 && input
235 .provenance_classes
236 .iter()
237 .all(|class| !class.can_corroborate());
238
239 let semantic_trust = if has_unknown {
240 SemanticTrustClass::Unknown
241 } else if input.falsification_evidence
242 && input.independent_source_families >= 2
243 && corroborating_families >= 1
244 {
245 SemanticTrustClass::FalsificationTested
246 } else if input.independent_source_families >= 2 && corroborating_families >= 1 {
247 SemanticTrustClass::Corroborated
248 } else if !input.provenance_classes.is_empty() {
249 SemanticTrustClass::SingleFamily
250 } else {
251 SemanticTrustClass::CandidateOnly
252 };
253
254 let mut reasons = Vec::new();
255 let policy_outcome = match input.intended_use {
256 SemanticUse::CandidateMemory => {
257 if has_unknown {
258 reasons.push("unknown provenance retained as candidate-only".to_string());
259 PolicyOutcome::Warn
260 } else {
261 reasons
262 .push("candidate memory may retain classified semantic evidence".to_string());
263 PolicyOutcome::Allow
264 }
265 }
266 SemanticUse::DefaultContext | SemanticUse::AdvisoryDoctrine => {
267 if has_unknown {
268 reasons.push("unknown provenance cannot enter default authority use".to_string());
269 PolicyOutcome::Quarantine
270 } else if runtime_or_weaker_only {
271 reasons.push(
272 "runtime-derived or weak-only provenance is advisory/candidate only"
273 .to_string(),
274 );
275 PolicyOutcome::Warn
276 } else {
277 reasons.push("semantic provenance permits bounded advisory use".to_string());
278 PolicyOutcome::Allow
279 }
280 }
281 SemanticUse::HighForceDoctrine | SemanticUse::TrustedExternalUse => {
282 if has_unknown {
283 reasons.push("unknown provenance cannot satisfy high-authority use".to_string());
284 PolicyOutcome::Reject
285 } else if runtime_or_weaker_only {
286 reasons.push(
287 "runtime-only or weak-only support cannot satisfy high-authority use"
288 .to_string(),
289 );
290 PolicyOutcome::Reject
291 } else if semantic_trust < SemanticTrustClass::FalsificationTested {
292 reasons.push(
293 "high-authority use requires corroboration plus falsification evidence"
294 .to_string(),
295 );
296 PolicyOutcome::Reject
297 } else {
298 reasons.push(
299 "semantic trust permits high-authority use subject to other gates".to_string(),
300 );
301 PolicyOutcome::Allow
302 }
303 }
304 };
305
306 let claim_ceiling = weakest_provenance
307 .claim_ceiling()
308 .min(semantic_trust.claim_ceiling())
309 .min(policy_outcome.claim_ceiling());
310
311 SemanticTrustReport {
312 semantic_trust,
313 weakest_provenance,
314 policy_outcome,
315 claim_ceiling,
316 reasons,
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use serde_json::json;
323
324 use super::*;
325
326 #[test]
327 fn provenance_wire_strings_are_stable() {
328 assert_eq!(
329 serde_json::to_value(ProvenanceClass::OperatorAttested).unwrap(),
330 json!("operator_attested")
331 );
332 assert_eq!(
333 serde_json::to_value(ProvenanceClass::ToolObserved).unwrap(),
334 json!("tool_observed")
335 );
336 assert_eq!(
337 serde_json::to_value(ProvenanceClass::RuntimeDerived).unwrap(),
338 json!("runtime_derived")
339 );
340 assert_eq!(
341 serde_json::to_value(ProvenanceClass::UnknownProvenance).unwrap(),
342 json!("unknown_provenance")
343 );
344 }
345
346 #[test]
347 fn unknown_provenance_fails_closed_for_high_force_doctrine() {
348 let report = evaluate_semantic_trust(
349 &SemanticTrustInput::new(SemanticUse::HighForceDoctrine)
350 .with_provenance([ProvenanceClass::UnknownProvenance])
351 .with_independent_source_families(1)
352 .with_unresolved_unknowns(true),
353 );
354
355 assert_eq!(report.semantic_trust, SemanticTrustClass::Unknown);
356 assert_eq!(report.policy_outcome, PolicyOutcome::Reject);
357 assert_eq!(report.claim_ceiling, ClaimCeiling::DevOnly);
358 }
359
360 #[test]
361 fn runtime_only_support_cannot_promote_high_force_doctrine() {
362 let report = evaluate_semantic_trust(
363 &SemanticTrustInput::new(SemanticUse::HighForceDoctrine)
364 .with_provenance([
365 ProvenanceClass::RuntimeDerived,
366 ProvenanceClass::SummaryDerived,
367 ])
368 .with_independent_source_families(2)
369 .with_falsification_evidence(true)
370 .with_unresolved_unknowns(false),
371 );
372
373 assert_eq!(report.policy_outcome, PolicyOutcome::Reject);
374 assert_eq!(report.semantic_trust, SemanticTrustClass::SingleFamily);
375 assert!(report
376 .reasons
377 .iter()
378 .any(|reason| reason.contains("runtime-only")));
379 }
380
381 #[test]
382 fn corroborated_and_falsified_support_can_pass_semantic_gate() {
383 let report = evaluate_semantic_trust(
384 &SemanticTrustInput::new(SemanticUse::HighForceDoctrine)
385 .with_provenance([
386 ProvenanceClass::ToolObserved,
387 ProvenanceClass::OperatorAttested,
388 ])
389 .with_independent_source_families(2)
390 .with_falsification_evidence(true)
391 .with_unresolved_unknowns(false),
392 );
393
394 assert_eq!(
395 report.semantic_trust,
396 SemanticTrustClass::FalsificationTested
397 );
398 assert_eq!(report.policy_outcome, PolicyOutcome::Allow);
399 assert_eq!(report.claim_ceiling, ClaimCeiling::AuthorityGrade);
400 }
401
402 #[test]
403 fn policy_contribution_is_machine_readable() {
404 let report = evaluate_semantic_trust(
405 &SemanticTrustInput::new(SemanticUse::DefaultContext)
406 .with_provenance([ProvenanceClass::RuntimeDerived])
407 .with_independent_source_families(1)
408 .with_unresolved_unknowns(false),
409 );
410
411 let contribution = report.policy_contribution().unwrap();
412 assert_eq!(contribution.rule_id.as_str(), "semantic_trust.provenance");
413 assert_eq!(contribution.outcome, PolicyOutcome::Warn);
414 }
415}