Skip to main content

openkind_engine/
profile.rs

1//! Immutable model/execution identity and numerical parity contracts.
2
3use thiserror::Error;
4
5/// Validation errors for model execution profile metadata.
6#[derive(Debug, Clone, PartialEq, Error)]
7pub enum ProfileValidationError {
8    /// A required text field was empty.
9    #[error("profile field `{field}` cannot be empty")]
10    EmptyField {
11        /// Name of the invalid field.
12        field: &'static str,
13    },
14
15    /// The reference bundle digest was not a lowercase SHA-256 value.
16    #[error("bundle SHA-256 must contain exactly 64 lowercase hexadecimal characters")]
17    InvalidBundleSha256,
18
19    /// Calibration temperature must be finite and strictly positive.
20    #[error("calibration temperature must be finite and greater than zero")]
21    InvalidCalibrationTemperature,
22
23    /// Policy threshold must be a finite probability.
24    #[error("policy threshold must be finite and lie in [0, 1]")]
25    InvalidPolicyThreshold,
26
27    /// A parity tolerance must be finite and non-negative.
28    #[error("{field} must be finite and non-negative")]
29    InvalidTolerance {
30        /// Name of the invalid tolerance.
31        field: &'static str,
32    },
33}
34
35fn required(
36    value: impl Into<String>,
37    field: &'static str,
38) -> Result<String, ProfileValidationError> {
39    let value = value.into();
40    if value.trim().is_empty() {
41        Err(ProfileValidationError::EmptyField { field })
42    } else {
43        Ok(value)
44    }
45}
46
47/// Immutable identity of a model or fitted artifact.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ArtifactIdentity {
50    id: String,
51    revision: String,
52}
53
54impl ArtifactIdentity {
55    /// Construct an artifact identity from a stable ID and immutable revision.
56    pub fn new(
57        id: impl Into<String>,
58        revision: impl Into<String>,
59    ) -> Result<Self, ProfileValidationError> {
60        Ok(Self {
61            id: required(id, "artifact.id")?,
62            revision: required(revision, "artifact.revision")?,
63        })
64    }
65
66    /// Stable model or artifact identifier.
67    pub fn id(&self) -> &str {
68        &self.id
69    }
70
71    /// Immutable artifact revision.
72    pub fn revision(&self) -> &str {
73        &self.revision
74    }
75}
76
77/// Provenance for the exported model bundle used as the parity reference.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ProfileSource {
80    profile_id: String,
81    repository: String,
82    repository_revision: String,
83    bundle_sha256: String,
84}
85
86impl ProfileSource {
87    /// Construct pinned reference-bundle provenance.
88    pub fn new(
89        profile_id: impl Into<String>,
90        repository: impl Into<String>,
91        repository_revision: impl Into<String>,
92        bundle_sha256: impl Into<String>,
93    ) -> Result<Self, ProfileValidationError> {
94        let bundle_sha256 = bundle_sha256.into();
95        if bundle_sha256.len() != 64
96            || !bundle_sha256
97                .bytes()
98                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
99        {
100            return Err(ProfileValidationError::InvalidBundleSha256);
101        }
102
103        Ok(Self {
104            profile_id: required(profile_id, "profile_id")?,
105            repository: required(repository, "source.repository")?,
106            repository_revision: required(repository_revision, "source.repository_revision")?,
107            bundle_sha256,
108        })
109    }
110
111    /// Immutable model/execution profile ID.
112    pub fn profile_id(&self) -> &str {
113        &self.profile_id
114    }
115
116    /// Repository containing the exported reference bundle.
117    pub fn repository(&self) -> &str {
118        &self.repository
119    }
120
121    /// Immutable repository revision from which fixtures were taken.
122    pub fn repository_revision(&self) -> &str {
123        &self.repository_revision
124    }
125
126    /// SHA-256 of the selected exported reference-bundle archive.
127    pub fn bundle_sha256(&self) -> &str {
128        &self.bundle_sha256
129    }
130}
131
132/// Probability semantics a profile's scores live in.
133///
134/// This makes the meaning of returned probabilities an explicit, versioned
135/// profile property instead of an implicit adapter behavior. An adapter that
136/// cannot serve the declared space must fail explicitly rather than discard
137/// or renormalize mass the space promises — silently dropping semantic-none
138/// mass and renormalizing the remainder is exactly the drift this declaration
139/// exists to prevent.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum ProbabilitySpace {
142    /// Probabilities are conditional on the offered options only: the
143    /// distribution over those options sums to one and no none mass is
144    /// modeled. A finite-token or direct-logit profile may legitimately
145    /// declare this space.
146    ConditionalOnOfferedOptions,
147    /// Probabilities cover the offered options plus explicit semantic-none
148    /// mass: the full distribution includes the reserved none class and sums
149    /// to one over options-plus-none. The selected native profile declares
150    /// this space.
151    OfferedOptionsPlusSemanticNone,
152}
153
154impl ProbabilitySpace {
155    /// Stable lowercase identifier used in reports and evidence artifacts.
156    #[must_use]
157    pub const fn as_str(self) -> &'static str {
158        match self {
159            Self::ConditionalOnOfferedOptions => "conditional_on_offered_options",
160            Self::OfferedOptionsPlusSemanticNone => "offered_options_plus_semantic_none",
161        }
162    }
163}
164
165/// Names the learned input and readout semantics of a profile.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct ExecutionSemantics {
168    renderer: String,
169    head: String,
170    rejection: String,
171    probability_space: ProbabilitySpace,
172}
173
174impl ExecutionSemantics {
175    /// Construct the renderer, head, rejection, and probability-space
176    /// identities.
177    pub fn new(
178        renderer: impl Into<String>,
179        head: impl Into<String>,
180        rejection: impl Into<String>,
181        probability_space: ProbabilitySpace,
182    ) -> Result<Self, ProfileValidationError> {
183        Ok(Self {
184            renderer: required(renderer, "execution.renderer")?,
185            head: required(head, "execution.head")?,
186            rejection: required(rejection, "execution.rejection")?,
187            probability_space,
188        })
189    }
190
191    /// Renderer identity, including ordering semantics.
192    pub fn renderer(&self) -> &str {
193        &self.renderer
194    }
195
196    /// Fitted head artifact identity.
197    pub fn head(&self) -> &str {
198        &self.head
199    }
200
201    /// Rejection mechanism identity.
202    pub fn rejection(&self) -> &str {
203        &self.rejection
204    }
205
206    /// Probability space the profile's scores live in.
207    #[must_use]
208    pub const fn probability_space(&self) -> ProbabilitySpace {
209        self.probability_space
210    }
211}
212
213/// Numerical calibration, policy, and parity requirements for one profile.
214#[derive(Debug, Clone, PartialEq)]
215pub struct ParityContract {
216    calibration_temperature: f64,
217    policy_threshold: f64,
218    probability_tolerance: f64,
219    ordering_tolerance: f64,
220}
221
222impl ParityContract {
223    /// Construct numerical parity requirements.
224    pub fn new(
225        calibration_temperature: f64,
226        policy_threshold: f64,
227        probability_tolerance: f64,
228        ordering_tolerance: f64,
229    ) -> Result<Self, ProfileValidationError> {
230        if !calibration_temperature.is_finite() || calibration_temperature <= 0.0 {
231            return Err(ProfileValidationError::InvalidCalibrationTemperature);
232        }
233        if !policy_threshold.is_finite() || !(0.0..=1.0).contains(&policy_threshold) {
234            return Err(ProfileValidationError::InvalidPolicyThreshold);
235        }
236        for (field, value) in [
237            ("probability_tolerance", probability_tolerance),
238            ("ordering_tolerance", ordering_tolerance),
239        ] {
240            if !value.is_finite() || value < 0.0 {
241                return Err(ProfileValidationError::InvalidTolerance { field });
242            }
243        }
244
245        Ok(Self {
246            calibration_temperature,
247            policy_threshold,
248            probability_tolerance,
249            ordering_tolerance,
250        })
251    }
252
253    /// Selected softmax temperature.
254    pub fn calibration_temperature(&self) -> f64 {
255        self.calibration_temperature
256    }
257
258    /// Minimum top probability required for the selected application policy to accept.
259    pub fn policy_threshold(&self) -> f64 {
260        self.policy_threshold
261    }
262
263    /// Maximum full-distribution delta accepted by the exported fixture contract.
264    pub fn probability_tolerance(&self) -> f64 {
265        self.probability_tolerance
266    }
267
268    /// Tolerance used by exported ordering and logit comparisons.
269    pub fn ordering_tolerance(&self) -> f64 {
270        self.ordering_tolerance
271    }
272}
273
274/// Immutable contract binding one model, renderer, readout, and numerical policy.
275#[derive(Debug, Clone, PartialEq)]
276pub struct ModelExecutionProfile {
277    source: ProfileSource,
278    backbone: ArtifactIdentity,
279    execution: ExecutionSemantics,
280    parity: ParityContract,
281}
282
283impl ModelExecutionProfile {
284    /// Construct a complete immutable execution profile.
285    pub fn new(
286        source: ProfileSource,
287        backbone: ArtifactIdentity,
288        execution: ExecutionSemantics,
289        parity: ParityContract,
290    ) -> Self {
291        Self {
292            source,
293            backbone,
294            execution,
295            parity,
296        }
297    }
298
299    /// Reference-bundle provenance.
300    pub fn source(&self) -> &ProfileSource {
301        &self.source
302    }
303
304    /// Pinned base-model identity.
305    pub fn backbone(&self) -> &ArtifactIdentity {
306        &self.backbone
307    }
308
309    /// Renderer, head, and rejection semantics.
310    pub fn execution(&self) -> &ExecutionSemantics {
311        &self.execution
312    }
313
314    /// Calibration, policy, and parity tolerances.
315    pub fn parity(&self) -> &ParityContract {
316        &self.parity
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn profile_contract_is_constructed_from_valid_components() {
326        let source = ProfileSource::new(
327            "profile",
328            "owner/repository",
329            "revision",
330            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
331        )
332        .unwrap();
333        let backbone = ArtifactIdentity::new("model", "model-revision").unwrap();
334        let execution = ExecutionSemantics::new(
335            "state-first",
336            "head.safetensors",
337            "score-summary",
338            ProbabilitySpace::OfferedOptionsPlusSemanticNone,
339        )
340        .unwrap();
341        let parity = ParityContract::new(1.5, 0.98, 0.005, 0.00001).unwrap();
342
343        let profile = ModelExecutionProfile::new(source, backbone, execution, parity);
344
345        assert_eq!(profile.source().profile_id(), "profile");
346        assert_eq!(profile.backbone().id(), "model");
347        assert_eq!(profile.execution().renderer(), "state-first");
348        assert_eq!(profile.parity().policy_threshold(), 0.98);
349        assert_eq!(
350            profile.execution().probability_space(),
351            ProbabilitySpace::OfferedOptionsPlusSemanticNone
352        );
353        assert_eq!(
354            ProbabilitySpace::ConditionalOnOfferedOptions.as_str(),
355            "conditional_on_offered_options"
356        );
357        assert_eq!(
358            ProbabilitySpace::OfferedOptionsPlusSemanticNone.as_str(),
359            "offered_options_plus_semantic_none"
360        );
361    }
362
363    #[test]
364    fn profile_components_reject_invalid_values() {
365        assert!(matches!(
366            ArtifactIdentity::new("", "revision"),
367            Err(ProfileValidationError::EmptyField { .. })
368        ));
369        assert_eq!(
370            ProfileSource::new("profile", "repository", "revision", "ABC"),
371            Err(ProfileValidationError::InvalidBundleSha256)
372        );
373        assert_eq!(
374            ParityContract::new(0.0, 0.98, 0.005, 0.00001),
375            Err(ProfileValidationError::InvalidCalibrationTemperature)
376        );
377        assert_eq!(
378            ParityContract::new(1.0, 1.1, 0.005, 0.00001),
379            Err(ProfileValidationError::InvalidPolicyThreshold)
380        );
381        assert!(matches!(
382            ParityContract::new(1.0, 0.98, f64::NAN, 0.00001),
383            Err(ProfileValidationError::InvalidTolerance { .. })
384        ));
385    }
386}