Skip to main content

sim_lib_standard_core/
characterization.rs

1//! Landed evidence inventory for behavior characterization.
2//!
3//! This module records the typed owners that a characterization scenario can
4//! compose. It is intentionally not a capture format: scenario execution and
5//! capture identity remain extensions of [`crate::ConformanceHarness`].
6
7use sim_kernel::Symbol;
8
9use crate::{SourceConformanceCase, SourceConformanceCaseKind, SourceExpectation};
10
11/// A stable evidence lane already available to a conformance case.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum EvidenceLane {
14    /// The scenario's declared inputs.
15    Input,
16    /// A successful result value.
17    Value,
18    /// A failed result.
19    Failure,
20    /// Ordered runtime events.
21    Event,
22    /// Ordered operation or library receipts.
23    Receipt,
24    /// The browseable Card face.
25    Browse,
26    /// Conformance outcome and fidelity evidence.
27    Conformance,
28}
29
30/// Existing owner and canonical projection for an evidence lane.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct EvidenceLaneInventory {
33    /// Stable lane.
34    pub lane: EvidenceLane,
35    /// Crate or product contract that owns the typed source record.
36    pub owner: &'static str,
37    /// Semantic projection used by a future capture.
38    pub canonical_projection: &'static str,
39}
40
41/// Every stable typed evidence lane available to characterization scenarios.
42///
43/// No entry admits `Debug` text. Values without a canonical `Datum` face need
44/// an explicit profile projection; failures similarly retain their typed
45/// class, detail, and location rather than an error's host formatting.
46pub const EVIDENCE_LANE_INVENTORY: &[EvidenceLaneInventory] = &[
47    EvidenceLaneInventory {
48        lane: EvidenceLane::Input,
49        owner: "sim-lib-standard-core scenario contract",
50        canonical_projection: "ordered declared input Datum records",
51    },
52    EvidenceLaneInventory {
53        lane: EvidenceLane::Value,
54        owner: "sim-kernel Value and DatumStore",
55        canonical_projection: "canonical Datum or named profile projection",
56    },
57    EvidenceLaneInventory {
58        lane: EvidenceLane::Failure,
59        owner: "sim-kernel Error, Diagnostic, and Origin",
60        canonical_projection: "stable failure class, detail, and source location",
61    },
62    EvidenceLaneInventory {
63        lane: EvidenceLane::Event,
64        owner: "sim-kernel Event and EventLedger",
65        canonical_projection: "ordered typed event records",
66    },
67    EvidenceLaneInventory {
68        lane: EvidenceLane::Receipt,
69        owner: "the operation or library defining each typed receipt",
70        canonical_projection: "ordered receipt identity and semantic fields",
71    },
72    EvidenceLaneInventory {
73        lane: EvidenceLane::Browse,
74        owner: "sim-kernel Card projection",
75        canonical_projection: "ordered Card fields projected to Datum",
76    },
77    EvidenceLaneInventory {
78        lane: EvidenceLane::Conformance,
79        owner: "sim-lib-standard-core ConformanceHarness, ProfileDiff, and FidelityBadge",
80        canonical_projection: "content-addressed test-run Datum and typed profile evidence",
81    },
82];
83
84/// A formatting-only or ambient field that must not enter capture identity
85/// without an explicit, scenario-declared projection.
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct ExplicitProjectionField {
88    /// Unstable field category.
89    pub field: &'static str,
90    /// Required treatment before capture identity is computed.
91    pub required_projection: &'static str,
92}
93
94/// Known formatting-only and ambient inputs excluded from canonical identity.
95pub const EXPLICIT_PROJECTION_FIELDS: &[ExplicitProjectionField] = &[
96    ExplicitProjectionField {
97        field: "Debug or Display rendering",
98        required_projection: "project the typed value or record to canonical Datum",
99    },
100    ExplicitProjectionField {
101        field: "wall-clock time and elapsed duration",
102        required_projection: "omit, or replace with a named deterministic semantic field",
103    },
104    ExplicitProjectionField {
105        field: "random seed and nondeterministic identifiers",
106        required_projection: "declare the seed or map ids through a stable ordered projection",
107    },
108    ExplicitProjectionField {
109        field: "host paths, process state, and environment",
110        required_projection: "replace with scenario-declared logical identities",
111    },
112    ExplicitProjectionField {
113        field: "unordered host collections",
114        required_projection: "project with an explicit semantic ordering",
115    },
116];
117
118/// Two existing source cases frozen as parity fixtures for capture development.
119///
120/// The pair deliberately covers a successful typed lowering and a declared,
121/// coded gap. They preserve the pre-capture matrix behavior while later phases
122/// add scenario execution and observation projection around it.
123pub fn characterization_source_fixtures() -> [SourceConformanceCase; 2] {
124    [
125        SourceConformanceCase {
126            symbol: Symbol::qualified("characterize", "lowering-parity"),
127            organ: Symbol::qualified("standard", "lowering"),
128            source_name: "lowering-parity.sim".to_owned(),
129            source: "answer".to_owned(),
130            kind: SourceConformanceCaseKind::Observed,
131            expectation: SourceExpectation::LowersTo("answer".to_owned()),
132            affects_badge: None,
133        },
134        SourceConformanceCase {
135            symbol: Symbol::qualified("characterize", "declared-gap-parity"),
136            organ: Symbol::qualified("standard", "unsupported"),
137            source_name: "declared-gap-parity.sim".to_owned(),
138            source: "ambient-clock".to_owned(),
139            kind: SourceConformanceCaseKind::Observed,
140            expectation: SourceExpectation::ExpectedGap {
141                code: Symbol::qualified("characterize", "ambient-input"),
142                reason: "ambient time is not a declared scenario input".to_owned(),
143            },
144            affects_badge: None,
145        },
146    ]
147}
148
149#[cfg(test)]
150mod tests {
151    use std::{collections::BTreeSet, sync::Arc};
152
153    use super::*;
154    use crate::{MatrixRunner, SourceObservation};
155    use sim_kernel::{Cx, DefaultFactory, NoopEvalPolicy};
156
157    #[test]
158    fn inventory_has_one_semantic_owner_and_projection_per_lane() {
159        let mut lanes = BTreeSet::new();
160        for entry in EVIDENCE_LANE_INVENTORY {
161            assert!(
162                lanes.insert(entry.lane as u8),
163                "duplicate lane: {:?}",
164                entry.lane
165            );
166            assert!(!entry.owner.is_empty());
167            assert!(!entry.canonical_projection.is_empty());
168            assert!(!entry.canonical_projection.contains("Debug"));
169            assert!(!entry.canonical_projection.contains("debug"));
170        }
171        assert_eq!(lanes.len(), 7);
172    }
173
174    #[test]
175    fn unstable_fields_all_require_explicit_projection() {
176        assert!(
177            EXPLICIT_PROJECTION_FIELDS
178                .iter()
179                .any(|field| field.field.contains("Debug"))
180        );
181        for field in EXPLICIT_PROJECTION_FIELDS {
182            assert!(!field.required_projection.is_empty());
183        }
184    }
185
186    #[test]
187    fn frozen_source_fixtures_preserve_pass_and_gap_matrix_behavior() {
188        let [pass, gap] = characterization_source_fixtures();
189        let profile = crate::LanguageProfile::new(Symbol::qualified("characterize", "profile"));
190        let row = crate::LanguageRowBuilder::new(Symbol::new("characterize"), profile)
191            .with_cases([pass, gap])
192            .build();
193        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
194        let report = MatrixRunner::run_source_row(&mut cx, &row, |_cx, case| {
195            Ok(match &case.expectation {
196                SourceExpectation::LowersTo(value) => SourceObservation::LowersTo(value.clone()),
197                SourceExpectation::ExpectedGap { code, reason } => SourceObservation::Gap {
198                    code: code.clone(),
199                    reason: reason.clone(),
200                },
201            })
202        });
203
204        assert_eq!(report.pass_count(), 1);
205        assert_eq!(report.gap_count(), 1);
206        assert_eq!(report.fail_count(), 0);
207    }
208}