Skip to main content

semantic_memory_forge/
v11.rs

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    /// Validates the semantics profile contract and required vocabularies.
90    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    /// Validates a v1 claim-state artifact before publication or transport.
126    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    /// Validates a witness artifact before publication or transport.
164    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    /// Validates a certificate artifact before publication or transport.
203    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    /// Validates a refutation artifact before publication or transport.
244    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    /// Validates a semantic-diff artifact before publication or transport.
276    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    /// Validates an oracle-slice contract before publication or transport.
314    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    /// Validates an advisory-only causal-attribution bundle before publication or transport.
373    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    /// Validates a degradation record before publication or transport.
410    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    /// Validates an exactness-budget record before publication or transport.
448    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(&degradation).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}