1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use stack_ids::{
4 CausalAttributionBundleId, CertificateId, ClaimId, ClaimStateId, DegradationRecordId,
5 ExactnessBudgetId, OracleSliceId, RefutationResultId, ScopeKey, SemanticDiffId,
6 SemanticsProfileId, WitnessId,
7};
8
9pub const SEMANTICS_PROFILE_V1_SCHEMA: &str = "semantics_profile_v1";
10pub const CLAIM_STATE_V1_SCHEMA: &str = "claim_state_v1";
11pub const WITNESS_ARTIFACT_V1_SCHEMA: &str = "witness_artifact_v1";
12pub const CERTIFICATE_ARTIFACT_V1_SCHEMA: &str = "certificate_artifact_v1";
13pub const REFUTATION_ARTIFACT_V1_SCHEMA: &str = "refutation_artifact_v1";
14pub const SEMANTIC_DIFF_V1_SCHEMA: &str = "semantic_diff_v1";
15pub const ORACLE_SLICE_CONTRACT_V1_SCHEMA: &str = "oracle_slice_contract_v1";
16pub const CAUSAL_ATTRIBUTION_BUNDLE_V1_SCHEMA: &str = "causal_attribution_bundle_v1";
17pub const DEGRADATION_RECORD_V1_SCHEMA: &str = "degradation_record_v1";
18pub const EXACTNESS_BUDGET_V1_SCHEMA: &str = "exactness_budget_v1";
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
21#[serde(rename_all = "snake_case")]
22pub enum TruthStateV1 {
23 Asserted,
24 Supported,
25 Refuted,
26 Abstained,
27 Unknown,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
31#[serde(rename_all = "snake_case")]
32pub enum DegradationKindV1 {
33 ThinExport,
34 MissingProof,
35 MissingReplay,
36 ScopeWithheld,
37 EvidenceUnavailable,
38 OracleUnavailable,
39 BudgetExceeded,
40 ExactnessDowngraded,
41 AdvisoryOnly,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45#[serde(rename_all = "snake_case")]
46pub enum ExactnessLevelV1 {
47 Exact,
48 Conservative,
49 Approximate,
50 Heuristic,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
54#[serde(rename_all = "snake_case")]
55pub enum SemanticViewV1 {
56 Canonical,
57 Projection,
58 Runtime,
59 CausalAdvisory,
60 Operator,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
64#[serde(rename_all = "snake_case")]
65pub enum EvidenceAdmissibilityV1 {
66 Admissible,
67 Restricted,
68 Inadmissible,
69 Unknown,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73pub struct SemanticsProfileV1 {
74 pub schema_version: String,
75 pub semantics_profile_id: SemanticsProfileId,
76 pub profile_name: String,
77 pub governing_spec_version: String,
78 pub default_view: SemanticViewV1,
79 pub allowed_views: Vec<SemanticViewV1>,
80 pub truth_state_vocabulary: Vec<TruthStateV1>,
81 pub degradation_vocabulary: Vec<DegradationKindV1>,
82 pub exactness_vocabulary: Vec<ExactnessLevelV1>,
83 pub admissibility_vocabulary: Vec<EvidenceAdmissibilityV1>,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub notes: Vec<String>,
86}
87
88impl SemanticsProfileV1 {
89 pub fn validate(&self) -> Result<(), String> {
91 ensure_schema(&self.schema_version, SEMANTICS_PROFILE_V1_SCHEMA)?;
92 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
93 ensure_non_empty(&self.profile_name, "profile_name")?;
94 ensure_non_empty(&self.governing_spec_version, "governing_spec_version")?;
95 ensure_non_empty_vec(&self.allowed_views, "allowed_views")?;
96 ensure_non_empty_vec(&self.truth_state_vocabulary, "truth_state_vocabulary")?;
97 ensure_non_empty_vec(&self.degradation_vocabulary, "degradation_vocabulary")?;
98 ensure_non_empty_vec(&self.exactness_vocabulary, "exactness_vocabulary")?;
99 ensure_non_empty_vec(&self.admissibility_vocabulary, "admissibility_vocabulary")?;
100 if !self.allowed_views.contains(&self.default_view) {
101 return Err("default_view must be included in allowed_views".into());
102 }
103 Ok(())
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
108pub struct ClaimStateV1 {
109 pub schema_version: String,
110 pub claim_state_id: ClaimStateId,
111 pub claim_id: ClaimId,
112 pub semantics_profile_id: SemanticsProfileId,
113 pub view: SemanticViewV1,
114 pub truth_state: TruthStateV1,
115 pub exactness: ExactnessLevelV1,
116 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub degradation: Vec<DegradationKindV1>,
118 pub evidence_admissibility: EvidenceAdmissibilityV1,
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub proof_obligations_remaining: Vec<String>,
121 pub policy_action_allowed: bool,
122}
123
124impl ClaimStateV1 {
125 pub fn validate(&self) -> Result<(), String> {
127 ensure_schema(&self.schema_version, CLAIM_STATE_V1_SCHEMA)?;
128 ensure_non_empty_id(self.claim_state_id.as_str(), "claim_state_id")?;
129 ensure_non_empty_id(self.claim_id.as_str(), "claim_id")?;
130 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
131 if self.policy_action_allowed
132 && matches!(
133 self.truth_state,
134 TruthStateV1::Unknown | TruthStateV1::Abstained | TruthStateV1::Refuted
135 )
136 {
137 return Err(
138 "policy_action_allowed cannot be true for unknown, abstained, or refuted truth"
139 .into(),
140 );
141 }
142 Ok(())
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147pub struct WitnessArtifactV1 {
148 pub schema_version: String,
149 pub witness_id: WitnessId,
150 pub semantics_profile_id: SemanticsProfileId,
151 pub claim_id: ClaimId,
152 pub statement: String,
153 pub evidence_refs: Vec<String>,
154 pub evidence_admissibility: EvidenceAdmissibilityV1,
155 pub exactness: ExactnessLevelV1,
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub degradation: Vec<DegradationKindV1>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub stricter_check_hint: Option<String>,
160}
161
162impl WitnessArtifactV1 {
163 pub fn validate(&self) -> Result<(), String> {
165 ensure_schema(&self.schema_version, WITNESS_ARTIFACT_V1_SCHEMA)?;
166 ensure_non_empty_id(self.witness_id.as_str(), "witness_id")?;
167 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
168 ensure_non_empty_id(self.claim_id.as_str(), "claim_id")?;
169 ensure_non_empty(&self.statement, "statement")?;
170 ensure_non_empty_vec(&self.evidence_refs, "evidence_refs")
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175#[serde(rename_all = "snake_case")]
176pub enum CertificateKindV1 {
177 Execution,
178 Replay,
179 Consistency,
180 ExactnessBound,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
184pub struct CertificateArtifactV1 {
185 pub schema_version: String,
186 pub certificate_id: CertificateId,
187 pub semantics_profile_id: SemanticsProfileId,
188 pub claim_id: ClaimId,
189 pub certificate_kind: CertificateKindV1,
190 pub certified_statement: String,
191 pub supporting_witness_ids: Vec<WitnessId>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub oracle_slice_id: Option<OracleSliceId>,
194 pub exactness: ExactnessLevelV1,
195 #[serde(default, skip_serializing_if = "Vec::is_empty")]
196 pub degradation: Vec<DegradationKindV1>,
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
198 pub proof_obligations_remaining: Vec<String>,
199}
200
201impl CertificateArtifactV1 {
202 pub fn validate(&self) -> Result<(), String> {
204 ensure_schema(&self.schema_version, CERTIFICATE_ARTIFACT_V1_SCHEMA)?;
205 ensure_non_empty_id(self.certificate_id.as_str(), "certificate_id")?;
206 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
207 ensure_non_empty_id(self.claim_id.as_str(), "claim_id")?;
208 ensure_non_empty(&self.certified_statement, "certified_statement")?;
209 ensure_non_empty_vec(&self.supporting_witness_ids, "supporting_witness_ids")
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
214#[serde(rename_all = "snake_case")]
215pub enum RefutationOutcomeV1 {
216 Sustained,
217 Refuted,
218 Inconclusive,
219 NotApplicable,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
223pub struct RefutationArtifactV1 {
224 pub schema_version: String,
225 pub refutation_result_id: RefutationResultId,
226 pub semantics_profile_id: SemanticsProfileId,
227 pub claim_id: ClaimId,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub target_artifact_id: Option<String>,
230 pub refuter: String,
231 pub outcome: RefutationOutcomeV1,
232 pub exactness: ExactnessLevelV1,
233 #[serde(default, skip_serializing_if = "Vec::is_empty")]
234 pub degradation: Vec<DegradationKindV1>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub searched_budget_units: Option<u64>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub witness_id: Option<WitnessId>,
239 pub reason: String,
240}
241
242impl RefutationArtifactV1 {
243 pub fn validate(&self) -> Result<(), String> {
245 ensure_schema(&self.schema_version, REFUTATION_ARTIFACT_V1_SCHEMA)?;
246 ensure_non_empty_id(self.refutation_result_id.as_str(), "refutation_result_id")?;
247 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
248 ensure_non_empty_id(self.claim_id.as_str(), "claim_id")?;
249 ensure_non_empty(&self.refuter, "refuter")?;
250 ensure_non_empty(&self.reason, "reason")?;
251 if matches!(self.outcome, RefutationOutcomeV1::Refuted) && self.witness_id.is_none() {
252 return Err("refuted outcomes require a witness_id".into());
253 }
254 Ok(())
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
259pub struct SemanticDiffV1 {
260 pub schema_version: String,
261 pub semantic_diff_id: SemanticDiffId,
262 pub semantics_profile_id: SemanticsProfileId,
263 pub subject_kind: String,
264 pub subject_id: String,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub from_truth_state: Option<TruthStateV1>,
267 pub to_truth_state: TruthStateV1,
268 pub changed_fields: Vec<String>,
269 pub reason: String,
270 #[serde(default, skip_serializing_if = "Vec::is_empty")]
271 pub evidence_refs: Vec<String>,
272}
273
274impl SemanticDiffV1 {
275 pub fn validate(&self) -> Result<(), String> {
277 ensure_schema(&self.schema_version, SEMANTIC_DIFF_V1_SCHEMA)?;
278 ensure_non_empty_id(self.semantic_diff_id.as_str(), "semantic_diff_id")?;
279 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
280 ensure_non_empty(&self.subject_kind, "subject_kind")?;
281 ensure_non_empty(&self.subject_id, "subject_id")?;
282 ensure_non_empty_vec(&self.changed_fields, "changed_fields")?;
283 ensure_non_empty(&self.reason, "reason")
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
288#[serde(rename_all = "snake_case")]
289pub enum DegradationActionV1 {
290 Block,
291 AdvisoryOnly,
292 ConservativeFallback,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
296pub struct OracleSliceContractV1 {
297 pub schema_version: String,
298 pub slice_id: OracleSliceId,
299 pub semantics_profile_id: SemanticsProfileId,
300 pub slice_name: String,
301 pub scope_key: ScopeKey,
302 #[serde(default, skip_serializing_if = "Vec::is_empty")]
303 pub evidence_refs: Vec<String>,
304 pub exactness: ExactnessLevelV1,
305 pub degradation_action: DegradationActionV1,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub max_rows: Option<u32>,
308 #[serde(default, skip_serializing_if = "Vec::is_empty")]
309 pub admissible_regions: Vec<String>,
310}
311
312impl OracleSliceContractV1 {
313 pub fn validate(&self) -> Result<(), String> {
315 ensure_schema(&self.schema_version, ORACLE_SLICE_CONTRACT_V1_SCHEMA)?;
316 ensure_non_empty_id(self.slice_id.as_str(), "slice_id")?;
317 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
318 ensure_non_empty(&self.slice_name, "slice_name")?;
319 if self.max_rows == Some(0) {
320 return Err("max_rows must be greater than zero when present".into());
321 }
322 Ok(())
323 }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
327#[serde(rename_all = "snake_case")]
328pub enum CausalRoleV1 {
329 Treatment,
330 Outcome,
331 Confounder,
332 Mediator,
333 Instrument,
334 EffectModifier,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
338#[serde(rename_all = "snake_case")]
339pub enum CausalDirectionV1 {
340 Supports,
341 Attenuates,
342 Refutes,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
346pub struct CausalContributorV1 {
347 pub factor_id: String,
348 pub role: CausalRoleV1,
349 pub direction: CausalDirectionV1,
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
351 pub evidence_refs: Vec<String>,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
355pub struct CausalAttributionBundleV1 {
356 pub schema_version: String,
357 pub causal_attribution_bundle_id: CausalAttributionBundleId,
358 pub semantics_profile_id: SemanticsProfileId,
359 pub claim_id: ClaimId,
360 pub treatment: String,
361 pub outcome: String,
362 pub view: SemanticViewV1,
363 pub contributors: Vec<CausalContributorV1>,
364 #[serde(default, skip_serializing_if = "Vec::is_empty")]
365 pub refutation_result_ids: Vec<RefutationResultId>,
366 #[serde(default, skip_serializing_if = "Vec::is_empty")]
367 pub replay_slice_refs: Vec<String>,
368 pub advisory_only: bool,
369}
370
371impl CausalAttributionBundleV1 {
372 pub fn validate(&self) -> Result<(), String> {
374 ensure_schema(&self.schema_version, CAUSAL_ATTRIBUTION_BUNDLE_V1_SCHEMA)?;
375 ensure_non_empty_id(
376 self.causal_attribution_bundle_id.as_str(),
377 "causal_attribution_bundle_id",
378 )?;
379 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
380 ensure_non_empty_id(self.claim_id.as_str(), "claim_id")?;
381 ensure_non_empty(&self.treatment, "treatment")?;
382 ensure_non_empty(&self.outcome, "outcome")?;
383 ensure_non_empty_vec(&self.contributors, "contributors")?;
384 if !self.advisory_only {
385 return Err("causal attribution outputs must remain advisory_only".into());
386 }
387 Ok(())
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
392pub struct DegradationRecordV1 {
393 pub schema_version: String,
394 pub degradation_record_id: DegradationRecordId,
395 pub semantics_profile_id: SemanticsProfileId,
396 pub artifact_family: String,
397 pub artifact_id: String,
398 pub degradation: Vec<DegradationKindV1>,
399 #[serde(default, skip_serializing_if = "Vec::is_empty")]
400 pub triggered_by: Vec<String>,
401 pub exactness_impact: ExactnessLevelV1,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub fallback_used: Option<String>,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub blocked_action: Option<String>,
406}
407
408impl DegradationRecordV1 {
409 pub fn validate(&self) -> Result<(), String> {
411 ensure_schema(&self.schema_version, DEGRADATION_RECORD_V1_SCHEMA)?;
412 ensure_non_empty_id(self.degradation_record_id.as_str(), "degradation_record_id")?;
413 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
414 ensure_non_empty(&self.artifact_family, "artifact_family")?;
415 ensure_non_empty(&self.artifact_id, "artifact_id")?;
416 ensure_non_empty_vec(&self.degradation, "degradation")
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
421pub struct ExactnessEscalationRuleV1 {
422 pub when_degraded: DegradationKindV1,
423 pub escalate_to: ExactnessLevelV1,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub block_action: Option<String>,
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
429pub struct ExactnessBudgetV1 {
430 pub schema_version: String,
431 pub exactness_budget_id: ExactnessBudgetId,
432 pub semantics_profile_id: SemanticsProfileId,
433 pub subject: String,
434 pub requested_exactness: ExactnessLevelV1,
435 pub achieved_exactness: ExactnessLevelV1,
436 pub budget_units: u64,
437 pub units_consumed: u64,
438 #[serde(default, skip_serializing_if = "Vec::is_empty")]
439 pub degradation: Vec<DegradationKindV1>,
440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
441 pub escalation_rules: Vec<ExactnessEscalationRuleV1>,
442 #[serde(default, skip_serializing_if = "Vec::is_empty")]
443 pub failure_artifact_refs: Vec<String>,
444}
445
446impl ExactnessBudgetV1 {
447 pub fn validate(&self) -> Result<(), String> {
449 ensure_schema(&self.schema_version, EXACTNESS_BUDGET_V1_SCHEMA)?;
450 ensure_non_empty_id(self.exactness_budget_id.as_str(), "exactness_budget_id")?;
451 ensure_non_empty_id(self.semantics_profile_id.as_str(), "semantics_profile_id")?;
452 ensure_non_empty(&self.subject, "subject")?;
453 if self.units_consumed > self.budget_units {
454 return Err("units_consumed cannot exceed budget_units".into());
455 }
456 if self.achieved_exactness != self.requested_exactness && self.degradation.is_empty() {
457 return Err("exactness downgrades require degradation markers".into());
458 }
459 Ok(())
460 }
461}
462
463fn ensure_schema(actual: &str, expected: &str) -> Result<(), String> {
464 if actual == expected {
465 Ok(())
466 } else {
467 Err(format!(
468 "schema_version must be '{expected}', got '{actual}'"
469 ))
470 }
471}
472
473fn ensure_non_empty(value: &str, field: &str) -> Result<(), String> {
474 if value.trim().is_empty() {
475 Err(format!("{field} must not be empty"))
476 } else {
477 Ok(())
478 }
479}
480
481fn ensure_non_empty_id(value: &str, field: &str) -> Result<(), String> {
482 ensure_non_empty(value, field)
483}
484
485fn ensure_non_empty_vec<T>(value: &[T], field: &str) -> Result<(), String> {
486 if value.is_empty() {
487 Err(format!("{field} must not be empty"))
488 } else {
489 Ok(())
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use serde_json::json;
497
498 fn sample_profile() -> SemanticsProfileV1 {
499 SemanticsProfileV1 {
500 schema_version: SEMANTICS_PROFILE_V1_SCHEMA.into(),
501 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
502 profile_name: "canonical-v11".into(),
503 governing_spec_version: "v11".into(),
504 default_view: SemanticViewV1::Canonical,
505 allowed_views: vec![SemanticViewV1::Canonical, SemanticViewV1::Projection],
506 truth_state_vocabulary: vec![TruthStateV1::Supported, TruthStateV1::Refuted],
507 degradation_vocabulary: vec![
508 DegradationKindV1::MissingProof,
509 DegradationKindV1::ExactnessDowngraded,
510 ],
511 exactness_vocabulary: vec![ExactnessLevelV1::Exact, ExactnessLevelV1::Conservative],
512 admissibility_vocabulary: vec![
513 EvidenceAdmissibilityV1::Admissible,
514 EvidenceAdmissibilityV1::Restricted,
515 ],
516 notes: vec!["proof-bearing outputs only".into()],
517 }
518 }
519
520 #[test]
521 fn semantics_profile_roundtrips_and_validates() {
522 let profile = sample_profile();
523 profile.validate().unwrap();
524
525 let encoded = serde_json::to_string(&profile).unwrap();
526 let decoded: SemanticsProfileV1 = serde_json::from_str(&encoded).unwrap();
527 assert_eq!(decoded, profile);
528 }
529
530 #[test]
531 fn claim_state_roundtrips_and_blocks_unsafe_policy() {
532 let claim_state = ClaimStateV1 {
533 schema_version: CLAIM_STATE_V1_SCHEMA.into(),
534 claim_state_id: ClaimStateId::new("claim-state-1"),
535 claim_id: ClaimId::new("claim-1"),
536 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
537 view: SemanticViewV1::Canonical,
538 truth_state: TruthStateV1::Supported,
539 exactness: ExactnessLevelV1::Conservative,
540 degradation: vec![DegradationKindV1::MissingProof],
541 evidence_admissibility: EvidenceAdmissibilityV1::Restricted,
542 proof_obligations_remaining: vec!["replay slice missing".into()],
543 policy_action_allowed: false,
544 };
545 claim_state.validate().unwrap();
546
547 let encoded = serde_json::to_string(&claim_state).unwrap();
548 let decoded: ClaimStateV1 = serde_json::from_str(&encoded).unwrap();
549 assert_eq!(decoded, claim_state);
550
551 let mut invalid = claim_state.clone();
552 invalid.truth_state = TruthStateV1::Unknown;
553 invalid.policy_action_allowed = true;
554 assert!(invalid.validate().is_err());
555 }
556
557 #[test]
558 fn artifact_contracts_roundtrip_and_validate() {
559 let witness = WitnessArtifactV1 {
560 schema_version: WITNESS_ARTIFACT_V1_SCHEMA.into(),
561 witness_id: WitnessId::new("witness-1"),
562 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
563 claim_id: ClaimId::new("claim-1"),
564 statement: "bounded replay reproduced the observed behavior".into(),
565 evidence_refs: vec!["receipt:1".into()],
566 evidence_admissibility: EvidenceAdmissibilityV1::Admissible,
567 exactness: ExactnessLevelV1::Exact,
568 degradation: Vec::new(),
569 stricter_check_hint: Some("full proof certificate".into()),
570 };
571 witness.validate().unwrap();
572 assert_eq!(
573 serde_json::from_str::<WitnessArtifactV1>(&serde_json::to_string(&witness).unwrap())
574 .unwrap(),
575 witness
576 );
577
578 let certificate = CertificateArtifactV1 {
579 schema_version: CERTIFICATE_ARTIFACT_V1_SCHEMA.into(),
580 certificate_id: CertificateId::new("certificate-1"),
581 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
582 claim_id: ClaimId::new("claim-1"),
583 certificate_kind: CertificateKindV1::Replay,
584 certified_statement: "replay remained within bounded semantics".into(),
585 supporting_witness_ids: vec![WitnessId::new("witness-1")],
586 oracle_slice_id: Some(OracleSliceId::new("oracle-slice-1")),
587 exactness: ExactnessLevelV1::Conservative,
588 degradation: vec![DegradationKindV1::AdvisoryOnly],
589 proof_obligations_remaining: vec!["strict oracle parity".into()],
590 };
591 certificate.validate().unwrap();
592 assert_eq!(
593 serde_json::from_str::<CertificateArtifactV1>(
594 &serde_json::to_string(&certificate).unwrap()
595 )
596 .unwrap(),
597 certificate
598 );
599
600 let refutation = RefutationArtifactV1 {
601 schema_version: REFUTATION_ARTIFACT_V1_SCHEMA.into(),
602 refutation_result_id: RefutationResultId::new("refutation-1"),
603 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
604 claim_id: ClaimId::new("claim-1"),
605 target_artifact_id: Some("certificate-1".into()),
606 refuter: "bounded_kernel_slice".into(),
607 outcome: RefutationOutcomeV1::Refuted,
608 exactness: ExactnessLevelV1::Conservative,
609 degradation: vec![DegradationKindV1::BudgetExceeded],
610 searched_budget_units: Some(25),
611 witness_id: Some(WitnessId::new("witness-2")),
612 reason: "counterexample survives stricter replay".into(),
613 };
614 refutation.validate().unwrap();
615 assert_eq!(
616 serde_json::from_str::<RefutationArtifactV1>(
617 &serde_json::to_string(&refutation).unwrap()
618 )
619 .unwrap(),
620 refutation
621 );
622
623 let diff = SemanticDiffV1 {
624 schema_version: SEMANTIC_DIFF_V1_SCHEMA.into(),
625 semantic_diff_id: SemanticDiffId::new("semantic-diff-1"),
626 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
627 subject_kind: "claim".into(),
628 subject_id: "claim-1".into(),
629 from_truth_state: Some(TruthStateV1::Asserted),
630 to_truth_state: TruthStateV1::Supported,
631 changed_fields: vec!["truth_state".into(), "proof_obligations_remaining".into()],
632 reason: "proof-carrying replay completed".into(),
633 evidence_refs: vec!["receipt:1".into()],
634 };
635 diff.validate().unwrap();
636 assert_eq!(
637 serde_json::from_str::<SemanticDiffV1>(&serde_json::to_string(&diff).unwrap()).unwrap(),
638 diff
639 );
640
641 let oracle_slice = OracleSliceContractV1 {
642 schema_version: ORACLE_SLICE_CONTRACT_V1_SCHEMA.into(),
643 slice_id: OracleSliceId::new("oracle-slice-1"),
644 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
645 slice_name: "bounded-proof-slice".into(),
646 scope_key: ScopeKey::namespace_only("ops"),
647 evidence_refs: vec!["receipt:1".into()],
648 exactness: ExactnessLevelV1::Conservative,
649 degradation_action: DegradationActionV1::AdvisoryOnly,
650 max_rows: Some(64),
651 admissible_regions: vec!["us-central".into()],
652 };
653 oracle_slice.validate().unwrap();
654 assert_eq!(
655 serde_json::from_str::<OracleSliceContractV1>(
656 &serde_json::to_string(&oracle_slice).unwrap()
657 )
658 .unwrap(),
659 oracle_slice
660 );
661
662 let causal = CausalAttributionBundleV1 {
663 schema_version: CAUSAL_ATTRIBUTION_BUNDLE_V1_SCHEMA.into(),
664 causal_attribution_bundle_id: CausalAttributionBundleId::new("causal-attribution-1"),
665 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
666 claim_id: ClaimId::new("claim-1"),
667 treatment: "enable bounded replay".into(),
668 outcome: "promotion eligibility".into(),
669 view: SemanticViewV1::CausalAdvisory,
670 contributors: vec![CausalContributorV1 {
671 factor_id: "replay-proof".into(),
672 role: CausalRoleV1::Treatment,
673 direction: CausalDirectionV1::Supports,
674 evidence_refs: vec!["receipt:1".into()],
675 }],
676 refutation_result_ids: vec![RefutationResultId::new("refutation-1")],
677 replay_slice_refs: vec!["replay:slice:1".into()],
678 advisory_only: true,
679 };
680 causal.validate().unwrap();
681 assert_eq!(
682 serde_json::from_str::<CausalAttributionBundleV1>(
683 &serde_json::to_string(&causal).unwrap()
684 )
685 .unwrap(),
686 causal
687 );
688
689 let degradation = DegradationRecordV1 {
690 schema_version: DEGRADATION_RECORD_V1_SCHEMA.into(),
691 degradation_record_id: DegradationRecordId::new("degradation-record-1"),
692 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
693 artifact_family: "certificate_artifact_v1".into(),
694 artifact_id: "certificate-1".into(),
695 degradation: vec![DegradationKindV1::MissingProof],
696 triggered_by: vec!["proof obligation not satisfied".into()],
697 exactness_impact: ExactnessLevelV1::Conservative,
698 fallback_used: Some("advisory-only output".into()),
699 blocked_action: Some("promotion".into()),
700 };
701 degradation.validate().unwrap();
702 assert_eq!(
703 serde_json::from_str::<DegradationRecordV1>(
704 &serde_json::to_string(°radation).unwrap()
705 )
706 .unwrap(),
707 degradation
708 );
709
710 let budget = ExactnessBudgetV1 {
711 schema_version: EXACTNESS_BUDGET_V1_SCHEMA.into(),
712 exactness_budget_id: ExactnessBudgetId::new("exactness-budget-1"),
713 semantics_profile_id: SemanticsProfileId::new("semantics-profile-1"),
714 subject: "claim-1".into(),
715 requested_exactness: ExactnessLevelV1::Exact,
716 achieved_exactness: ExactnessLevelV1::Conservative,
717 budget_units: 100,
718 units_consumed: 80,
719 degradation: vec![DegradationKindV1::ExactnessDowngraded],
720 escalation_rules: vec![ExactnessEscalationRuleV1 {
721 when_degraded: DegradationKindV1::ExactnessDowngraded,
722 escalate_to: ExactnessLevelV1::Exact,
723 block_action: Some("promotion".into()),
724 }],
725 failure_artifact_refs: vec!["degradation-record-1".into()],
726 };
727 budget.validate().unwrap();
728 assert_eq!(
729 serde_json::from_str::<ExactnessBudgetV1>(&serde_json::to_string(&budget).unwrap())
730 .unwrap(),
731 budget
732 );
733 }
734
735 #[test]
736 fn good_json_fixtures_parse_and_validate() {
737 let good_profile = json!({
738 "schema_version": "semantics_profile_v1",
739 "semantics_profile_id": "semantics-profile-1",
740 "profile_name": "canonical-v11",
741 "governing_spec_version": "v11",
742 "default_view": "canonical",
743 "allowed_views": ["canonical", "projection"],
744 "truth_state_vocabulary": ["supported", "refuted"],
745 "degradation_vocabulary": ["missing_proof", "exactness_downgraded"],
746 "exactness_vocabulary": ["exact", "conservative"],
747 "admissibility_vocabulary": ["admissible", "restricted"]
748 });
749 let profile: SemanticsProfileV1 = serde_json::from_value(good_profile).unwrap();
750 profile.validate().unwrap();
751
752 let good_claim_state = json!({
753 "schema_version": "claim_state_v1",
754 "claim_state_id": "claim-state-1",
755 "claim_id": "claim-1",
756 "semantics_profile_id": "semantics-profile-1",
757 "view": "canonical",
758 "truth_state": "supported",
759 "exactness": "conservative",
760 "degradation": ["missing_proof"],
761 "evidence_admissibility": "restricted",
762 "proof_obligations_remaining": ["replay slice missing"],
763 "policy_action_allowed": false
764 });
765 let claim_state: ClaimStateV1 = serde_json::from_value(good_claim_state).unwrap();
766 claim_state.validate().unwrap();
767
768 let good_witness = json!({
769 "schema_version": "witness_artifact_v1",
770 "witness_id": "witness-1",
771 "semantics_profile_id": "semantics-profile-1",
772 "claim_id": "claim-1",
773 "statement": "bounded replay reproduced the observed behavior",
774 "evidence_refs": ["receipt:1"],
775 "evidence_admissibility": "admissible",
776 "exactness": "exact"
777 });
778 let witness: WitnessArtifactV1 = serde_json::from_value(good_witness).unwrap();
779 witness.validate().unwrap();
780
781 let good_certificate = json!({
782 "schema_version": "certificate_artifact_v1",
783 "certificate_id": "certificate-1",
784 "semantics_profile_id": "semantics-profile-1",
785 "claim_id": "claim-1",
786 "certificate_kind": "replay",
787 "certified_statement": "replay remained within bounded semantics",
788 "supporting_witness_ids": ["witness-1"],
789 "oracle_slice_id": "oracle-slice-1",
790 "exactness": "conservative",
791 "degradation": ["advisory_only"],
792 "proof_obligations_remaining": ["strict oracle parity"]
793 });
794 let certificate: CertificateArtifactV1 = serde_json::from_value(good_certificate).unwrap();
795 certificate.validate().unwrap();
796
797 let good_refutation = json!({
798 "schema_version": "refutation_artifact_v1",
799 "refutation_result_id": "refutation-1",
800 "semantics_profile_id": "semantics-profile-1",
801 "claim_id": "claim-1",
802 "target_artifact_id": "certificate-1",
803 "refuter": "bounded_kernel_slice",
804 "outcome": "refuted",
805 "exactness": "conservative",
806 "degradation": ["budget_exceeded"],
807 "searched_budget_units": 25,
808 "witness_id": "witness-2",
809 "reason": "counterexample survives stricter replay"
810 });
811 let refutation: RefutationArtifactV1 = serde_json::from_value(good_refutation).unwrap();
812 refutation.validate().unwrap();
813
814 let good_diff = json!({
815 "schema_version": "semantic_diff_v1",
816 "semantic_diff_id": "semantic-diff-1",
817 "semantics_profile_id": "semantics-profile-1",
818 "subject_kind": "claim",
819 "subject_id": "claim-1",
820 "from_truth_state": "asserted",
821 "to_truth_state": "supported",
822 "changed_fields": ["truth_state", "proof_obligations_remaining"],
823 "reason": "proof-carrying replay completed",
824 "evidence_refs": ["receipt:1"]
825 });
826 let diff: SemanticDiffV1 = serde_json::from_value(good_diff).unwrap();
827 diff.validate().unwrap();
828
829 let good_oracle_slice = json!({
830 "schema_version": "oracle_slice_contract_v1",
831 "slice_id": "oracle-slice-1",
832 "semantics_profile_id": "semantics-profile-1",
833 "slice_name": "bounded-proof-slice",
834 "scope_key": { "namespace": "ops" },
835 "evidence_refs": ["receipt:1"],
836 "exactness": "conservative",
837 "degradation_action": "advisory_only",
838 "max_rows": 64,
839 "admissible_regions": ["us-central"]
840 });
841 let oracle_slice: OracleSliceContractV1 =
842 serde_json::from_value(good_oracle_slice).unwrap();
843 oracle_slice.validate().unwrap();
844
845 let good_causal = json!({
846 "schema_version": "causal_attribution_bundle_v1",
847 "causal_attribution_bundle_id": "causal-attribution-1",
848 "semantics_profile_id": "semantics-profile-1",
849 "claim_id": "claim-1",
850 "treatment": "enable bounded replay",
851 "outcome": "promotion eligibility",
852 "view": "causal_advisory",
853 "contributors": [{
854 "factor_id": "replay-proof",
855 "role": "treatment",
856 "direction": "supports",
857 "evidence_refs": ["receipt:1"]
858 }],
859 "refutation_result_ids": ["refutation-1"],
860 "replay_slice_refs": ["replay:slice:1"],
861 "advisory_only": true
862 });
863 let causal: CausalAttributionBundleV1 = serde_json::from_value(good_causal).unwrap();
864 causal.validate().unwrap();
865
866 let good_degradation = json!({
867 "schema_version": "degradation_record_v1",
868 "degradation_record_id": "degradation-record-1",
869 "semantics_profile_id": "semantics-profile-1",
870 "artifact_family": "certificate_artifact_v1",
871 "artifact_id": "certificate-1",
872 "degradation": ["missing_proof"],
873 "triggered_by": ["proof obligation not satisfied"],
874 "exactness_impact": "conservative",
875 "fallback_used": "advisory-only output",
876 "blocked_action": "promotion"
877 });
878 let degradation: DegradationRecordV1 = serde_json::from_value(good_degradation).unwrap();
879 degradation.validate().unwrap();
880
881 let good_budget = json!({
882 "schema_version": "exactness_budget_v1",
883 "exactness_budget_id": "exactness-budget-1",
884 "semantics_profile_id": "semantics-profile-1",
885 "subject": "claim-1",
886 "requested_exactness": "exact",
887 "achieved_exactness": "conservative",
888 "budget_units": 100,
889 "units_consumed": 90,
890 "degradation": ["exactness_downgraded"],
891 "escalation_rules": [{
892 "when_degraded": "exactness_downgraded",
893 "escalate_to": "exact",
894 "block_action": "promotion"
895 }]
896 });
897 let budget: ExactnessBudgetV1 = serde_json::from_value(good_budget).unwrap();
898 budget.validate().unwrap();
899 }
900
901 #[test]
902 fn bad_json_fixtures_are_rejected() {
903 let bad_profile = json!({
904 "schema_version": "semantics_profile_v1",
905 "semantics_profile_id": "semantics-profile-1",
906 "profile_name": "",
907 "governing_spec_version": "v11",
908 "default_view": "runtime",
909 "allowed_views": ["canonical"],
910 "truth_state_vocabulary": ["supported"],
911 "degradation_vocabulary": ["missing_proof"],
912 "exactness_vocabulary": ["exact"],
913 "admissibility_vocabulary": ["admissible"]
914 });
915 let profile: SemanticsProfileV1 = serde_json::from_value(bad_profile).unwrap();
916 assert!(profile.validate().is_err());
917
918 let bad_claim_state = json!({
919 "schema_version": "claim_state_v1",
920 "claim_state_id": "claim-state-1",
921 "claim_id": "claim-1",
922 "semantics_profile_id": "semantics-profile-1",
923 "view": "canonical",
924 "truth_state": "unknown",
925 "exactness": "conservative",
926 "evidence_admissibility": "unknown",
927 "policy_action_allowed": true
928 });
929 let claim_state: ClaimStateV1 = serde_json::from_value(bad_claim_state).unwrap();
930 assert!(claim_state.validate().is_err());
931
932 let bad_witness = json!({
933 "schema_version": "witness_artifact_v1",
934 "witness_id": "witness-1",
935 "semantics_profile_id": "semantics-profile-1",
936 "claim_id": "claim-1",
937 "statement": "",
938 "evidence_refs": [],
939 "evidence_admissibility": "admissible",
940 "exactness": "exact"
941 });
942 let witness: WitnessArtifactV1 = serde_json::from_value(bad_witness).unwrap();
943 assert!(witness.validate().is_err());
944
945 let bad_certificate = json!({
946 "schema_version": "certificate_artifact_v1",
947 "certificate_id": "certificate-1",
948 "semantics_profile_id": "semantics-profile-1",
949 "claim_id": "claim-1",
950 "certificate_kind": "replay",
951 "certified_statement": "",
952 "supporting_witness_ids": [],
953 "exactness": "conservative"
954 });
955 let certificate: CertificateArtifactV1 = serde_json::from_value(bad_certificate).unwrap();
956 assert!(certificate.validate().is_err());
957
958 let bad_refutation = json!({
959 "schema_version": "refutation_artifact_v1",
960 "refutation_result_id": "refutation-1",
961 "semantics_profile_id": "semantics-profile-1",
962 "claim_id": "claim-1",
963 "refuter": "bounded_kernel_slice",
964 "outcome": "refuted",
965 "exactness": "conservative",
966 "reason": "counterexample found"
967 });
968 let refutation: RefutationArtifactV1 = serde_json::from_value(bad_refutation).unwrap();
969 assert!(refutation.validate().is_err());
970
971 let bad_diff = json!({
972 "schema_version": "semantic_diff_v1",
973 "semantic_diff_id": "semantic-diff-1",
974 "semantics_profile_id": "semantics-profile-1",
975 "subject_kind": "claim",
976 "subject_id": "claim-1",
977 "to_truth_state": "supported",
978 "changed_fields": [],
979 "reason": ""
980 });
981 let diff: SemanticDiffV1 = serde_json::from_value(bad_diff).unwrap();
982 assert!(diff.validate().is_err());
983
984 let bad_oracle_slice = json!({
985 "schema_version": "oracle_slice_contract_v1",
986 "slice_id": "oracle-slice-1",
987 "semantics_profile_id": "semantics-profile-1",
988 "slice_name": "",
989 "scope_key": { "namespace": "ops" },
990 "exactness": "conservative",
991 "degradation_action": "advisory_only",
992 "max_rows": 0
993 });
994 let oracle_slice: OracleSliceContractV1 = serde_json::from_value(bad_oracle_slice).unwrap();
995 assert!(oracle_slice.validate().is_err());
996
997 let bad_causal = json!({
998 "schema_version": "causal_attribution_bundle_v1",
999 "causal_attribution_bundle_id": "causal-attribution-1",
1000 "semantics_profile_id": "semantics-profile-1",
1001 "claim_id": "claim-1",
1002 "treatment": "enable bounded replay",
1003 "outcome": "promotion eligibility",
1004 "view": "causal_advisory",
1005 "contributors": [],
1006 "advisory_only": false
1007 });
1008 let causal: CausalAttributionBundleV1 = serde_json::from_value(bad_causal).unwrap();
1009 assert!(causal.validate().is_err());
1010
1011 let bad_degradation = json!({
1012 "schema_version": "degradation_record_v1",
1013 "degradation_record_id": "degradation-record-1",
1014 "semantics_profile_id": "semantics-profile-1",
1015 "artifact_family": "certificate_artifact_v1",
1016 "artifact_id": "certificate-1",
1017 "degradation": [],
1018 "exactness_impact": "conservative"
1019 });
1020 let degradation: DegradationRecordV1 = serde_json::from_value(bad_degradation).unwrap();
1021 assert!(degradation.validate().is_err());
1022
1023 let bad_budget = json!({
1024 "schema_version": "exactness_budget_v1",
1025 "exactness_budget_id": "exactness-budget-1",
1026 "semantics_profile_id": "semantics-profile-1",
1027 "subject": "claim-1",
1028 "requested_exactness": "exact",
1029 "achieved_exactness": "conservative",
1030 "budget_units": 10,
1031 "units_consumed": 11,
1032 "degradation": []
1033 });
1034 let budget: ExactnessBudgetV1 = serde_json::from_value(bad_budget).unwrap();
1035 assert!(budget.validate().is_err());
1036 }
1037}