Skip to main content

ferrum_interfaces/vnext/
resolved.rs

1use ferrum_types::AttentionExecutionPolicy;
2use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Visitor};
3use serde::{Deserialize, Deserializer, Serialize};
4use sha2::{Digest, Sha256};
5use std::cmp::Ordering;
6use std::collections::{BTreeMap, BTreeSet};
7use std::fmt;
8use std::io::{self, Write};
9
10use super::model::PreparedModelFamilyWire;
11use super::numerical_resolution::NumericalProfileResolutionWire;
12use super::NumericalProfileResolution;
13use super::{
14    AdmissionFitPolicy, CapabilityCatalog, CheckpointCapacityPolicy, CompletionRetentionSpec,
15    ContractVersion, DeviceDescriptor, DynamicStorageProfile, ExecutablePlanView,
16    ExecutionDeterminismRequirement, ExecutionPlan, ModelFamilyRegistry, PlanNodeResolution,
17    PreparedModelFamily, ProviderId, ReusableExecutionPolicy, RuntimePolicy, SpecialTokenRole,
18    TokenizerDescriptor, UnvalidatedExecutionPlan, UnvalidatedExecutionPlanWire,
19    UnvalidatedPreparedModelFamily, VNextError,
20};
21
22/// Maximum raw byte length accepted for one resolution source artifact.
23pub const MAX_RESOLUTION_SOURCE_BYTES: usize = 32 * 1024 * 1024;
24/// Maximum serialized byte length accepted by resolved-plan wire decoding.
25pub const MAX_RESOLVED_MODEL_PLAN_WIRE_BYTES: usize = 16 * 1024 * 1024;
26/// Maximum container nesting depth in a parsed resolution source document.
27pub const MAX_RESOLUTION_JSON_DEPTH: usize = 128;
28/// Maximum total JSON values in a parsed resolution source document.
29pub const MAX_RESOLUTION_JSON_NODES: usize = 1_000_000;
30/// Maximum cumulative bytes across object keys and string values.
31pub const MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES: usize = MAX_RESOLUTION_SOURCE_BYTES;
32/// Maximum cumulative string bytes in one resolution source provenance record.
33pub const MAX_RESOLUTION_PROVENANCE_BYTES: usize = 4 * 1024;
34/// Maximum number of source JSON pointers recorded by one artifact.
35pub const MAX_RESOLUTION_FIELD_PATHS: usize = 4_096;
36/// Maximum byte length of one source JSON pointer.
37pub const MAX_RESOLUTION_FIELD_PATH_BYTES: usize = 512;
38/// Maximum cumulative bytes across all source JSON pointers in one artifact.
39pub const MAX_RESOLUTION_FIELD_PATH_TOTAL_BYTES: usize = 1024 * 1024;
40
41fn is_canonical_sha256(value: &str) -> bool {
42    value.len() == 64
43        && value
44            .bytes()
45            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
46}
47
48fn canonical_fingerprint<T: Serialize + ?Sized>(
49    value: &T,
50    context: &'static str,
51) -> Result<String, VNextError> {
52    let value = serde_json::to_value(value).map_err(|error| VNextError::Serialization {
53        context,
54        message: error.to_string(),
55    })?;
56    canonical_value_fingerprint(&canonicalize_json(value), context)
57}
58
59struct Sha256Writer(Sha256);
60
61impl Write for Sha256Writer {
62    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
63        self.0.update(bytes);
64        Ok(bytes.len())
65    }
66
67    fn flush(&mut self) -> io::Result<()> {
68        Ok(())
69    }
70}
71
72fn canonical_value_fingerprint(
73    value: &serde_json::Value,
74    context: &'static str,
75) -> Result<String, VNextError> {
76    let mut writer = Sha256Writer(Sha256::new());
77    serde_json::to_writer(&mut writer, value).map_err(|error| VNextError::Serialization {
78        context,
79        message: error.to_string(),
80    })?;
81    Ok(format!("{:x}", writer.0.finalize()))
82}
83
84fn canonical_json_value<T: Serialize + ?Sized>(
85    value: &T,
86    context: &'static str,
87) -> Result<serde_json::Value, VNextError> {
88    serde_json::to_value(value)
89        .map(canonicalize_json)
90        .map_err(|error| VNextError::Serialization {
91            context,
92            message: error.to_string(),
93        })
94}
95
96fn canonicalize_json(value: serde_json::Value) -> serde_json::Value {
97    match value {
98        serde_json::Value::Array(values) => {
99            serde_json::Value::Array(values.into_iter().map(canonicalize_json).collect())
100        }
101        serde_json::Value::Object(values) => serde_json::Value::Object(
102            values
103                .into_iter()
104                .map(|(key, value)| (key, canonicalize_json(value)))
105                .collect::<BTreeMap<_, _>>()
106                .into_iter()
107                .collect(),
108        ),
109        value => value,
110    }
111}
112
113fn invalid_plan(field: impl Into<String>, reason: impl Into<String>) -> VNextError {
114    VNextError::InvalidResolvedModelPlan {
115        field: field.into(),
116        reason: reason.into(),
117    }
118}
119
120fn validate_resolution_source_bytes(source_bytes: &[u8]) -> Result<(), VNextError> {
121    if source_bytes.is_empty() || source_bytes.len() > MAX_RESOLUTION_SOURCE_BYTES {
122        return Err(invalid_plan(
123            "resolution_source_evidence.source_bytes",
124            format!("must contain between 1 and {MAX_RESOLUTION_SOURCE_BYTES} bytes"),
125        ));
126    }
127    Ok(())
128}
129
130#[derive(Clone, Copy)]
131struct ResolutionJsonBudget {
132    maximum_depth: usize,
133    maximum_nodes: usize,
134    maximum_key_and_string_bytes: usize,
135}
136
137impl ResolutionJsonBudget {
138    const SOURCE: Self = Self {
139        maximum_depth: MAX_RESOLUTION_JSON_DEPTH,
140        maximum_nodes: MAX_RESOLUTION_JSON_NODES,
141        maximum_key_and_string_bytes: MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES,
142    };
143}
144
145struct ResolutionJsonPreflight {
146    budget: ResolutionJsonBudget,
147    nodes: usize,
148    key_and_string_bytes: usize,
149    violation: Option<VNextError>,
150}
151
152impl ResolutionJsonPreflight {
153    fn new(budget: ResolutionJsonBudget) -> Self {
154        Self {
155            budget,
156            nodes: 0,
157            key_and_string_bytes: 0,
158            violation: None,
159        }
160    }
161
162    fn fail<E: serde::de::Error>(&mut self, error: VNextError) -> Result<(), E> {
163        self.violation = Some(error);
164        Err(E::custom(
165            "resolution source JSON exceeds its structural budget",
166        ))
167    }
168
169    fn account_node<E: serde::de::Error>(&mut self, depth: usize) -> Result<(), E> {
170        if depth > self.budget.maximum_depth {
171            return self.fail(invalid_plan(
172                "resolution_source_evidence.document.depth",
173                format!("must not exceed {}", self.budget.maximum_depth),
174            ));
175        }
176        let Some(nodes) = self.nodes.checked_add(1) else {
177            return self.fail(invalid_plan(
178                "resolution_source_evidence.document.nodes",
179                "node count overflowed",
180            ));
181        };
182        if nodes > self.budget.maximum_nodes {
183            return self.fail(invalid_plan(
184                "resolution_source_evidence.document.nodes",
185                format!("must not exceed {}", self.budget.maximum_nodes),
186            ));
187        }
188        self.nodes = nodes;
189        Ok(())
190    }
191
192    fn account_text<E: serde::de::Error>(&mut self, bytes: usize) -> Result<(), E> {
193        let Some(key_and_string_bytes) = self.key_and_string_bytes.checked_add(bytes) else {
194            return self.fail(invalid_plan(
195                "resolution_source_evidence.document.key_and_string_bytes",
196                "byte count overflowed",
197            ));
198        };
199        if key_and_string_bytes > self.budget.maximum_key_and_string_bytes {
200            return self.fail(invalid_plan(
201                "resolution_source_evidence.document.key_and_string_bytes",
202                format!(
203                    "must not exceed {}",
204                    self.budget.maximum_key_and_string_bytes
205                ),
206            ));
207        }
208        self.key_and_string_bytes = key_and_string_bytes;
209        Ok(())
210    }
211}
212
213struct ResolutionJsonValueSeed<'a> {
214    preflight: &'a mut ResolutionJsonPreflight,
215    depth: usize,
216}
217
218impl<'de> DeserializeSeed<'de> for ResolutionJsonValueSeed<'_> {
219    type Value = ();
220
221    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
222    where
223        D: Deserializer<'de>,
224    {
225        self.preflight.account_node::<D::Error>(self.depth)?;
226        deserializer.deserialize_any(ResolutionJsonValueVisitor {
227            preflight: self.preflight,
228            depth: self.depth,
229        })
230    }
231}
232
233struct ResolutionJsonValueVisitor<'a> {
234    preflight: &'a mut ResolutionJsonPreflight,
235    depth: usize,
236}
237
238impl<'de> Visitor<'de> for ResolutionJsonValueVisitor<'_> {
239    type Value = ();
240
241    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242        formatter.write_str("one JSON value")
243    }
244
245    fn visit_unit<E>(self) -> Result<Self::Value, E> {
246        Ok(())
247    }
248
249    fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
250        Ok(())
251    }
252
253    fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> {
254        Ok(())
255    }
256
257    fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> {
258        Ok(())
259    }
260
261    fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E> {
262        Ok(())
263    }
264
265    fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
266    where
267        E: serde::de::Error,
268    {
269        self.preflight.account_text::<E>(value.len())
270    }
271
272    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
273    where
274        E: serde::de::Error,
275    {
276        self.preflight.account_text::<E>(value.len())
277    }
278
279    fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
280    where
281        E: serde::de::Error,
282    {
283        self.preflight.account_text::<E>(value.len())
284    }
285
286    fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
287    where
288        A: SeqAccess<'de>,
289    {
290        let child_depth = self
291            .depth
292            .checked_add(1)
293            .ok_or_else(|| serde::de::Error::custom("resolution source JSON depth overflowed"))?;
294        while sequence
295            .next_element_seed(ResolutionJsonValueSeed {
296                preflight: &mut *self.preflight,
297                depth: child_depth,
298            })?
299            .is_some()
300        {}
301        Ok(())
302    }
303
304    fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error>
305    where
306        A: MapAccess<'de>,
307    {
308        let child_depth = self
309            .depth
310            .checked_add(1)
311            .ok_or_else(|| serde::de::Error::custom("resolution source JSON depth overflowed"))?;
312        while object
313            .next_key_seed(ResolutionJsonKeySeed {
314                preflight: &mut *self.preflight,
315            })?
316            .is_some()
317        {
318            object.next_value_seed(ResolutionJsonValueSeed {
319                preflight: &mut *self.preflight,
320                depth: child_depth,
321            })?;
322        }
323        Ok(())
324    }
325}
326
327struct ResolutionJsonKeySeed<'a> {
328    preflight: &'a mut ResolutionJsonPreflight,
329}
330
331impl<'de> DeserializeSeed<'de> for ResolutionJsonKeySeed<'_> {
332    type Value = ();
333
334    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
335    where
336        D: Deserializer<'de>,
337    {
338        deserializer.deserialize_str(ResolutionJsonKeyVisitor {
339            preflight: self.preflight,
340        })
341    }
342}
343
344struct ResolutionJsonKeyVisitor<'a> {
345    preflight: &'a mut ResolutionJsonPreflight,
346}
347
348impl<'de> Visitor<'de> for ResolutionJsonKeyVisitor<'_> {
349    type Value = ();
350
351    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
352        formatter.write_str("a JSON object key")
353    }
354
355    fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
356    where
357        E: serde::de::Error,
358    {
359        self.preflight.account_text::<E>(value.len())
360    }
361
362    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
363    where
364        E: serde::de::Error,
365    {
366        self.preflight.account_text::<E>(value.len())
367    }
368
369    fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
370    where
371        E: serde::de::Error,
372    {
373        self.preflight.account_text::<E>(value.len())
374    }
375}
376
377fn preflight_resolution_json_depth(
378    source_bytes: &[u8],
379    maximum_depth: usize,
380) -> Result<(), VNextError> {
381    let mut closing_delimiters = Vec::with_capacity(maximum_depth.saturating_add(1));
382    let mut in_string = false;
383    let mut escaped = false;
384
385    for byte in source_bytes.iter().copied() {
386        if in_string {
387            if escaped {
388                escaped = false;
389            } else if byte == b'\\' {
390                escaped = true;
391            } else if byte == b'"' {
392                in_string = false;
393            }
394            continue;
395        }
396
397        match byte {
398            b'"' => {
399                if closing_delimiters.len() > maximum_depth {
400                    return Err(invalid_plan(
401                        "resolution_source_evidence.document.depth",
402                        format!("must not exceed {maximum_depth}"),
403                    ));
404                }
405                in_string = true;
406            }
407            b'[' | b'{' => {
408                if closing_delimiters.len() > maximum_depth {
409                    return Err(invalid_plan(
410                        "resolution_source_evidence.document.depth",
411                        format!("must not exceed {maximum_depth}"),
412                    ));
413                }
414                closing_delimiters.push(if byte == b'[' { b']' } else { b'}' });
415            }
416            b']' | b'}' => {
417                if closing_delimiters.last() == Some(&byte) {
418                    closing_delimiters.pop();
419                }
420            }
421            b' ' | b'\t' | b'\r' | b'\n' | b',' | b':' => {}
422            _ if closing_delimiters.len() > maximum_depth => {
423                return Err(invalid_plan(
424                    "resolution_source_evidence.document.depth",
425                    format!("must not exceed {maximum_depth}"),
426                ));
427            }
428            _ => {}
429        }
430    }
431    Ok(())
432}
433
434fn preflight_resolution_json(
435    source_bytes: &[u8],
436    budget: ResolutionJsonBudget,
437) -> Result<(), VNextError> {
438    // serde_json's built-in recursion guard can fire before our public depth
439    // contract. This quote-aware pass preserves the contract's structured
440    // depth error without allocating a JSON tree.
441    preflight_resolution_json_depth(source_bytes, budget.maximum_depth)?;
442
443    let mut preflight = ResolutionJsonPreflight::new(budget);
444    let result = {
445        let mut deserializer = serde_json::Deserializer::from_slice(source_bytes);
446        ResolutionJsonValueSeed {
447            preflight: &mut preflight,
448            depth: 0,
449        }
450        .deserialize(&mut deserializer)
451        .and_then(|()| deserializer.end())
452    };
453    if let Some(error) = preflight.violation {
454        return Err(error);
455    }
456    result.map_err(|error| VNextError::Serialization {
457        context: "parse resolution source JSON",
458        message: error.to_string(),
459    })
460}
461
462fn validate_resolution_json_tree(document: &serde_json::Value) -> Result<(), VNextError> {
463    let mut stack = vec![(document, 0usize)];
464    let mut nodes = 0usize;
465    let mut key_and_string_bytes = 0usize;
466
467    while let Some((value, depth)) = stack.pop() {
468        if depth > MAX_RESOLUTION_JSON_DEPTH {
469            return Err(invalid_plan(
470                "resolution_source_evidence.document.depth",
471                format!("must not exceed {MAX_RESOLUTION_JSON_DEPTH}"),
472            ));
473        }
474        nodes = nodes.checked_add(1).ok_or_else(|| {
475            invalid_plan(
476                "resolution_source_evidence.document.nodes",
477                "node count overflowed",
478            )
479        })?;
480        if nodes > MAX_RESOLUTION_JSON_NODES {
481            return Err(invalid_plan(
482                "resolution_source_evidence.document.nodes",
483                format!("must not exceed {MAX_RESOLUTION_JSON_NODES}"),
484            ));
485        }
486
487        match value {
488            serde_json::Value::String(value) => {
489                key_and_string_bytes =
490                    key_and_string_bytes
491                        .checked_add(value.len())
492                        .ok_or_else(|| {
493                            invalid_plan(
494                                "resolution_source_evidence.document.key_and_string_bytes",
495                                "byte count overflowed",
496                            )
497                        })?;
498            }
499            serde_json::Value::Array(values) => {
500                let child_depth = depth.checked_add(1).ok_or_else(|| {
501                    invalid_plan(
502                        "resolution_source_evidence.document.depth",
503                        "depth overflowed",
504                    )
505                })?;
506                stack.extend(values.iter().map(|value| (value, child_depth)));
507            }
508            serde_json::Value::Object(values) => {
509                let child_depth = depth.checked_add(1).ok_or_else(|| {
510                    invalid_plan(
511                        "resolution_source_evidence.document.depth",
512                        "depth overflowed",
513                    )
514                })?;
515                for (key, value) in values {
516                    key_and_string_bytes =
517                        key_and_string_bytes.checked_add(key.len()).ok_or_else(|| {
518                            invalid_plan(
519                                "resolution_source_evidence.document.key_and_string_bytes",
520                                "byte count overflowed",
521                            )
522                        })?;
523                    stack.push((value, child_depth));
524                }
525            }
526            serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {
527            }
528        }
529
530        if key_and_string_bytes > MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES {
531            return Err(invalid_plan(
532                "resolution_source_evidence.document.key_and_string_bytes",
533                format!("must not exceed {MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES}"),
534            ));
535        }
536    }
537
538    Ok(())
539}
540
541fn drop_json_iteratively(document: serde_json::Value) {
542    let mut stack = vec![document];
543    while let Some(value) = stack.pop() {
544        match value {
545            serde_json::Value::Array(mut values) => stack.append(&mut values),
546            serde_json::Value::Object(values) => stack.extend(values.into_values()),
547            serde_json::Value::Null
548            | serde_json::Value::Bool(_)
549            | serde_json::Value::Number(_)
550            | serde_json::Value::String(_) => {}
551        }
552    }
553}
554
555fn validate_and_canonicalize_resolution_json(
556    document: serde_json::Value,
557    fingerprint_context: &'static str,
558) -> Result<(serde_json::Value, String), VNextError> {
559    if let Err(error) = validate_resolution_json_tree(&document) {
560        drop_json_iteratively(document);
561        return Err(error);
562    }
563    let document = canonicalize_json(document);
564    let fingerprint = canonical_value_fingerprint(&document, fingerprint_context)?;
565    Ok((document, fingerprint))
566}
567
568fn validate_portable_identifier(
569    kind: &'static str,
570    value: &str,
571    maximum_length: usize,
572) -> Result<(), VNextError> {
573    if value.is_empty() || value.len() > maximum_length {
574        return Err(invalid_plan(
575            kind,
576            format!("must contain between 1 and {maximum_length} bytes"),
577        ));
578    }
579    if !value.bytes().all(|byte| {
580        byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':' | b'/')
581    }) {
582        return Err(invalid_plan(kind, "contains a non-portable character"));
583    }
584    Ok(())
585}
586
587fn validate_source_path(path: &str) -> bool {
588    !path.is_empty()
589        && !path.starts_with('/')
590        && !path.ends_with('/')
591        && !path.contains('\\')
592        && path
593            .split('/')
594            .all(|component| !matches!(component, "" | "." | ".."))
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
598#[serde(rename_all = "snake_case")]
599pub enum ModelSourceKind {
600    LocalDirectory,
601    LocalFile,
602    Repository,
603    ReleaseArtifact,
604}
605
606#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
607pub struct OriginalModelSource {
608    pub kind: ModelSourceKind,
609    pub location: String,
610    pub requested_revision: Option<String>,
611}
612
613#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
614pub struct FileFingerprint {
615    pub relative_path: String,
616    pub size_bytes: u64,
617    pub sha256: String,
618}
619
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621pub struct ResolvedModelSource {
622    pub canonical_location: String,
623    pub resolved_revision: String,
624    pub files: Vec<FileFingerprint>,
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
628#[serde(rename_all = "snake_case")]
629pub enum ModelArtifactSourceRole {
630    Semantic,
631    Tokenizer,
632    Weights,
633}
634
635impl ModelArtifactSourceRole {
636    pub const ALL: [Self; 3] = [Self::Semantic, Self::Tokenizer, Self::Weights];
637
638    pub const fn as_str(self) -> &'static str {
639        match self {
640            Self::Semantic => "semantic",
641            Self::Tokenizer => "tokenizer",
642            Self::Weights => "weights",
643        }
644    }
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648pub struct OriginalModelSources {
649    pub semantic: OriginalModelSource,
650    pub tokenizer: OriginalModelSource,
651    pub weights: OriginalModelSource,
652}
653
654impl OriginalModelSources {
655    pub const fn for_role(&self, role: ModelArtifactSourceRole) -> &OriginalModelSource {
656        match role {
657            ModelArtifactSourceRole::Semantic => &self.semantic,
658            ModelArtifactSourceRole::Tokenizer => &self.tokenizer,
659            ModelArtifactSourceRole::Weights => &self.weights,
660        }
661    }
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
665pub struct ResolvedModelSources {
666    pub semantic: ResolvedModelSource,
667    pub tokenizer: ResolvedModelSource,
668    pub weights: ResolvedModelSource,
669}
670
671impl ResolvedModelSources {
672    pub const fn for_role(&self, role: ModelArtifactSourceRole) -> &ResolvedModelSource {
673        match role {
674            ModelArtifactSourceRole::Semantic => &self.semantic,
675            ModelArtifactSourceRole::Tokenizer => &self.tokenizer,
676            ModelArtifactSourceRole::Weights => &self.weights,
677        }
678    }
679
680    fn for_role_mut(&mut self, role: ModelArtifactSourceRole) -> &mut ResolvedModelSource {
681        match role {
682            ModelArtifactSourceRole::Semantic => &mut self.semantic,
683            ModelArtifactSourceRole::Tokenizer => &mut self.tokenizer,
684            ModelArtifactSourceRole::Weights => &mut self.weights,
685        }
686    }
687}
688
689pub const PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION: u32 = 1;
690
691/// One selected artifact inside a role-specific model source.
692///
693/// `container_sha256` binds the complete source file. `content_sha256` is
694/// present when runtime selects content inside that file, for example the
695/// chat-template string inside `tokenizer_config.json`.
696#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
697#[serde(deny_unknown_fields)]
698pub struct ProductModelArtifactBinding {
699    pub role: ModelArtifactSourceRole,
700    pub source_file: String,
701    pub container_sha256: String,
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub content_sha256: Option<String>,
704}
705
706impl ProductModelArtifactBinding {
707    pub fn new(
708        role: ModelArtifactSourceRole,
709        source_file: impl Into<String>,
710        container_sha256: impl Into<String>,
711        content_sha256: Option<String>,
712    ) -> Result<Self, VNextError> {
713        let binding = Self {
714            role,
715            source_file: source_file.into(),
716            container_sha256: container_sha256.into(),
717            content_sha256,
718        };
719        binding.validate("product_model_artifact_binding")?;
720        Ok(binding)
721    }
722
723    fn validate(&self, field: &str) -> Result<(), VNextError> {
724        if !validate_source_path(&self.source_file) {
725            return Err(invalid_plan(
726                format!("{field}.source_file"),
727                "must be a portable relative source path",
728            ));
729        }
730        if !is_canonical_sha256(&self.container_sha256) {
731            return Err(invalid_plan(
732                format!("{field}.container_sha256"),
733                "must be a canonical SHA-256",
734            ));
735        }
736        if self
737            .content_sha256
738            .as_deref()
739            .is_some_and(|sha256| !is_canonical_sha256(sha256))
740        {
741            return Err(invalid_plan(
742                format!("{field}.content_sha256"),
743                "must be a canonical SHA-256",
744            ));
745        }
746        Ok(())
747    }
748}
749
750/// Immutable product-facing identity for the exact model sources selected by
751/// resolution and family preparation.
752///
753/// Keeping the raw request separate from the stable resolved model id avoids
754/// exposing machine-local cache paths as public model names. The role-specific
755/// sources and selected artifacts are the same inputs consumed by the typed
756/// model family and `ResolvedModelPlan`.
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(deny_unknown_fields)]
759pub struct ProductModelSourceIdentity {
760    pub schema_version: u32,
761    pub requested_model: String,
762    pub resolved_model: String,
763    pub original_sources: OriginalModelSources,
764    pub resolved_sources: ResolvedModelSources,
765    pub semantic_config: ProductModelArtifactBinding,
766    pub tokenizer: ProductModelArtifactBinding,
767    pub template: ProductModelArtifactBinding,
768    #[serde(skip_serializing_if = "Option::is_none")]
769    pub weight_config: Option<ProductModelArtifactBinding>,
770}
771
772impl ProductModelSourceIdentity {
773    #[allow(clippy::too_many_arguments)]
774    pub fn new(
775        requested_model: impl Into<String>,
776        resolved_model: impl Into<String>,
777        original_sources: OriginalModelSources,
778        resolved_sources: ResolvedModelSources,
779        semantic_config: ProductModelArtifactBinding,
780        tokenizer: ProductModelArtifactBinding,
781        template: ProductModelArtifactBinding,
782        weight_config: Option<ProductModelArtifactBinding>,
783    ) -> Result<Self, VNextError> {
784        let identity = Self {
785            schema_version: PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION,
786            requested_model: requested_model.into(),
787            resolved_model: resolved_model.into(),
788            original_sources,
789            resolved_sources,
790            semantic_config,
791            tokenizer,
792            template,
793            weight_config,
794        };
795        identity.validate()?;
796        Ok(identity)
797    }
798
799    pub fn validate(&self) -> Result<(), VNextError> {
800        if self.schema_version != PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION {
801            return Err(invalid_plan(
802                "product_model_source_identity.schema_version",
803                format!("must be {}", PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION),
804            ));
805        }
806        for (field, value) in [
807            ("requested_model", self.requested_model.as_str()),
808            ("resolved_model", self.resolved_model.as_str()),
809        ] {
810            if value.is_empty() || value.trim() != value {
811                return Err(invalid_plan(
812                    format!("product_model_source_identity.{field}"),
813                    "must be non-empty and have no surrounding whitespace",
814                ));
815            }
816        }
817        for role in ModelArtifactSourceRole::ALL {
818            self.validate_original_source(role)?;
819            self.validate_resolved_source(role)?;
820        }
821        self.validate_binding(
822            "semantic_config",
823            &self.semantic_config,
824            ModelArtifactSourceRole::Semantic,
825        )?;
826        self.validate_binding(
827            "tokenizer",
828            &self.tokenizer,
829            ModelArtifactSourceRole::Tokenizer,
830        )?;
831        self.validate_binding(
832            "template",
833            &self.template,
834            ModelArtifactSourceRole::Tokenizer,
835        )?;
836        if self.template.content_sha256.is_none() {
837            return Err(invalid_plan(
838                "product_model_source_identity.template.content_sha256",
839                "selected template content must be fingerprinted",
840            ));
841        }
842        if let Some(binding) = &self.weight_config {
843            self.validate_binding("weight_config", binding, ModelArtifactSourceRole::Weights)?;
844        }
845        Ok(())
846    }
847
848    fn validate_original_source(&self, role: ModelArtifactSourceRole) -> Result<(), VNextError> {
849        let source = self.original_sources.for_role(role);
850        if source.location.is_empty() || source.location.trim() != source.location {
851            return Err(invalid_plan(
852                format!(
853                    "product_model_source_identity.original_sources.{}.location",
854                    role.as_str()
855                ),
856                "must be non-empty and have no surrounding whitespace",
857            ));
858        }
859        if source
860            .requested_revision
861            .as_deref()
862            .is_some_and(|revision| revision.is_empty() || revision.trim() != revision)
863        {
864            return Err(invalid_plan(
865                format!(
866                    "product_model_source_identity.original_sources.{}.requested_revision",
867                    role.as_str()
868                ),
869                "must be non-empty and have no surrounding whitespace when present",
870            ));
871        }
872        Ok(())
873    }
874
875    fn validate_resolved_source(&self, role: ModelArtifactSourceRole) -> Result<(), VNextError> {
876        let source = self.resolved_sources.for_role(role);
877        for (field, value) in [
878            ("canonical_location", source.canonical_location.as_str()),
879            ("resolved_revision", source.resolved_revision.as_str()),
880        ] {
881            if value.is_empty() || value.trim() != value {
882                return Err(invalid_plan(
883                    format!(
884                        "product_model_source_identity.resolved_sources.{}.{field}",
885                        role.as_str()
886                    ),
887                    "must be non-empty and have no surrounding whitespace",
888                ));
889            }
890        }
891        if source.files.is_empty() {
892            return Err(invalid_plan(
893                format!(
894                    "product_model_source_identity.resolved_sources.{}.files",
895                    role.as_str()
896                ),
897                "must not be empty",
898            ));
899        }
900        for (index, file) in source.files.iter().enumerate() {
901            let field = format!(
902                "product_model_source_identity.resolved_sources.{}.files[{index}]",
903                role.as_str()
904            );
905            if !validate_source_path(&file.relative_path) {
906                return Err(invalid_plan(
907                    format!("{field}.relative_path"),
908                    "must be a portable relative source path",
909                ));
910            }
911            if file.size_bytes == 0 {
912                return Err(invalid_plan(
913                    format!("{field}.size_bytes"),
914                    "must be positive",
915                ));
916            }
917            if !is_canonical_sha256(&file.sha256) {
918                return Err(invalid_plan(
919                    format!("{field}.sha256"),
920                    "must be a canonical SHA-256",
921                ));
922            }
923        }
924        if source
925            .files
926            .windows(2)
927            .any(|pair| pair[0].relative_path >= pair[1].relative_path)
928        {
929            return Err(invalid_plan(
930                format!(
931                    "product_model_source_identity.resolved_sources.{}.files",
932                    role.as_str()
933                ),
934                "must be strictly sorted by relative_path without duplicates",
935            ));
936        }
937        Ok(())
938    }
939
940    fn validate_binding(
941        &self,
942        field: &str,
943        binding: &ProductModelArtifactBinding,
944        expected_role: ModelArtifactSourceRole,
945    ) -> Result<(), VNextError> {
946        binding.validate(&format!("product_model_source_identity.{field}"))?;
947        if binding.role != expected_role {
948            return Err(invalid_plan(
949                format!("product_model_source_identity.{field}.role"),
950                format!("must be {expected_role:?}"),
951            ));
952        }
953        let source = self.resolved_sources.for_role(binding.role);
954        match source
955            .files
956            .iter()
957            .find(|file| file.relative_path == binding.source_file)
958        {
959            Some(file) if file.sha256 == binding.container_sha256 => Ok(()),
960            Some(file) => Err(invalid_plan(
961                format!("product_model_source_identity.{field}.container_sha256"),
962                format!("does not match resolved source file {}", file.relative_path),
963            )),
964            None => Err(invalid_plan(
965                format!("product_model_source_identity.{field}.source_file"),
966                format!("is absent from resolved {} source", binding.role.as_str()),
967            )),
968        }
969    }
970}
971
972#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
973pub struct ModelConfigFingerprint {
974    pub source_file: String,
975    pub sha256: String,
976    pub typed_config_sha256: String,
977}
978
979#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
980pub struct EngineSelection {
981    pub provider_id: ProviderId,
982    pub contract_version: ContractVersion,
983    pub implementation_fingerprint: String,
984}
985
986#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(rename_all = "snake_case")]
988pub enum SchedulingDiscipline {
989    FirstReady,
990    Priority,
991    Deadline,
992}
993
994#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
995pub struct RuntimeMemoryPolicy {
996    pub capacity_bytes: u64,
997    pub reserve_bytes: u64,
998    pub maximum_active_sequences: u32,
999    pub dynamic_storage_profile_order: Vec<DynamicStorageProfile>,
1000    #[serde(default, skip_serializing_if = "Option::is_none")]
1001    pub checkpoint_capacity: Option<CheckpointCapacityPolicy>,
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1005pub struct AdmissionPolicy {
1006    pub maximum_queue_depth: u32,
1007    pub maximum_scheduled_tokens: u64,
1008    pub sequence_fit_policy: AdmissionFitPolicy,
1009    pub allow_defer: bool,
1010    pub cancellation_check_interval_steps: u32,
1011}
1012
1013#[derive(Serialize)]
1014struct RuntimePolicyFingerprintPayload<'a> {
1015    policy_id: &'a str,
1016    version: ContractVersion,
1017    scheduling: SchedulingDiscipline,
1018    memory: &'a RuntimeMemoryPolicy,
1019    admission: &'a AdmissionPolicy,
1020    attention_execution: AttentionExecutionPolicy,
1021    execution_determinism: ExecutionDeterminismRequirement,
1022    reusable_execution: &'a Option<ReusableExecutionPolicy>,
1023}
1024
1025#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1026pub struct ResolvedRuntimePolicy {
1027    policy_id: String,
1028    version: ContractVersion,
1029    scheduling: SchedulingDiscipline,
1030    memory: RuntimeMemoryPolicy,
1031    admission: AdmissionPolicy,
1032    attention_execution: AttentionExecutionPolicy,
1033    execution_determinism: ExecutionDeterminismRequirement,
1034    reusable_execution: Option<ReusableExecutionPolicy>,
1035    fingerprint: String,
1036}
1037
1038#[derive(Deserialize)]
1039#[serde(deny_unknown_fields)]
1040struct ResolvedRuntimePolicyWire {
1041    policy_id: String,
1042    version: ContractVersion,
1043    scheduling: SchedulingDiscipline,
1044    memory: RuntimeMemoryPolicy,
1045    admission: AdmissionPolicy,
1046    attention_execution: AttentionExecutionPolicy,
1047    execution_determinism: ExecutionDeterminismRequirement,
1048    reusable_execution: Option<ReusableExecutionPolicy>,
1049    fingerprint: String,
1050}
1051
1052impl<'de> Deserialize<'de> for ResolvedRuntimePolicy {
1053    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1054    where
1055        D: Deserializer<'de>,
1056    {
1057        let wire = ResolvedRuntimePolicyWire::deserialize(deserializer)?;
1058        let policy = Self::new(
1059            wire.policy_id,
1060            wire.version,
1061            wire.scheduling,
1062            wire.memory,
1063            wire.admission,
1064            wire.attention_execution,
1065            wire.execution_determinism,
1066            wire.reusable_execution,
1067        )
1068        .map_err(serde::de::Error::custom)?;
1069        if wire.fingerprint != policy.fingerprint {
1070            return Err(serde::de::Error::custom(format!(
1071                "runtime policy fingerprint mismatch: expected `{}`, actual `{}`",
1072                policy.fingerprint, wire.fingerprint
1073            )));
1074        }
1075        Ok(policy)
1076    }
1077}
1078
1079impl ResolvedRuntimePolicy {
1080    pub fn new(
1081        policy_id: impl Into<String>,
1082        version: ContractVersion,
1083        scheduling: SchedulingDiscipline,
1084        memory: RuntimeMemoryPolicy,
1085        admission: AdmissionPolicy,
1086        attention_execution: AttentionExecutionPolicy,
1087        execution_determinism: ExecutionDeterminismRequirement,
1088        reusable_execution: Option<ReusableExecutionPolicy>,
1089    ) -> Result<Self, VNextError> {
1090        let policy_id = policy_id.into();
1091        Self::validate_fields(
1092            &policy_id,
1093            version,
1094            &memory,
1095            &admission,
1096            attention_execution,
1097            reusable_execution.as_ref(),
1098        )?;
1099        let fingerprint = Self::compute_fingerprint(
1100            &policy_id,
1101            version,
1102            scheduling,
1103            &memory,
1104            &admission,
1105            attention_execution,
1106            execution_determinism,
1107            &reusable_execution,
1108        )?;
1109        Ok(Self {
1110            policy_id,
1111            version,
1112            scheduling,
1113            memory,
1114            admission,
1115            attention_execution,
1116            execution_determinism,
1117            reusable_execution,
1118            fingerprint,
1119        })
1120    }
1121
1122    fn validate_fields(
1123        policy_id: &str,
1124        version: ContractVersion,
1125        memory: &RuntimeMemoryPolicy,
1126        admission: &AdmissionPolicy,
1127        attention_execution: AttentionExecutionPolicy,
1128        reusable_execution: Option<&ReusableExecutionPolicy>,
1129    ) -> Result<(), VNextError> {
1130        validate_portable_identifier("runtime_policy.policy_id", policy_id, 160)?;
1131        if version.major == 0 {
1132            return Err(invalid_plan(
1133                "runtime_policy.version",
1134                "major version must be non-zero",
1135            ));
1136        }
1137        if memory.capacity_bytes == 0
1138            || memory.reserve_bytes >= memory.capacity_bytes
1139            || memory.maximum_active_sequences == 0
1140            || memory.dynamic_storage_profile_order.is_empty()
1141            || memory
1142                .dynamic_storage_profile_order
1143                .iter()
1144                .enumerate()
1145                .any(|(index, profile)| {
1146                    memory.dynamic_storage_profile_order[index + 1..].contains(profile)
1147                })
1148        {
1149            return Err(invalid_plan(
1150                "runtime_policy.memory",
1151                "capacity and reserve must be valid and maximum_active_sequences must be non-zero",
1152            ));
1153        }
1154        if admission.maximum_queue_depth == 0
1155            || admission.maximum_scheduled_tokens == 0
1156            || admission.cancellation_check_interval_steps == 0
1157        {
1158            return Err(invalid_plan(
1159                "runtime_policy.admission",
1160                "queue depth, scheduled-token ceiling, and cancellation interval must be non-zero",
1161            ));
1162        }
1163        if !attention_execution.is_resolved() {
1164            return Err(invalid_plan(
1165                "runtime_policy.attention_execution",
1166                "attention execution policy must be resolved before plan compilation",
1167            ));
1168        }
1169        if let Some(reusable_execution) = reusable_execution {
1170            reusable_execution.validate()?;
1171            if reusable_execution.buckets().iter().any(|bucket| {
1172                bucket.capacity().maximum_sequences() > memory.maximum_active_sequences
1173                    || bucket.capacity().maximum_tokens() > admission.maximum_scheduled_tokens
1174            }) {
1175                return Err(invalid_plan(
1176                    "runtime_policy.reusable_execution",
1177                    "bucket capacity exceeds the scheduler policy ceiling",
1178                ));
1179            }
1180            if reusable_execution
1181                .program_policy()
1182                .is_some_and(|program_policy| {
1183                    program_policy.programs().iter().any(|program| {
1184                        let shape = program.shape();
1185                        shape.request_capacity() > memory.maximum_active_sequences
1186                            || shape.token_capacity() > admission.maximum_scheduled_tokens
1187                    })
1188                })
1189            {
1190                return Err(invalid_plan(
1191                    "runtime_policy.reusable_execution",
1192                    "program shape exceeds the scheduler policy ceiling",
1193                ));
1194            }
1195        }
1196        Ok(())
1197    }
1198
1199    fn compute_fingerprint(
1200        policy_id: &str,
1201        version: ContractVersion,
1202        scheduling: SchedulingDiscipline,
1203        memory: &RuntimeMemoryPolicy,
1204        admission: &AdmissionPolicy,
1205        attention_execution: AttentionExecutionPolicy,
1206        execution_determinism: ExecutionDeterminismRequirement,
1207        reusable_execution: &Option<ReusableExecutionPolicy>,
1208    ) -> Result<String, VNextError> {
1209        canonical_fingerprint(
1210            &RuntimePolicyFingerprintPayload {
1211                policy_id,
1212                version,
1213                scheduling,
1214                memory,
1215                admission,
1216                attention_execution,
1217                execution_determinism,
1218                reusable_execution,
1219            },
1220            "serialize resolved runtime policy",
1221        )
1222    }
1223
1224    pub fn policy_id(&self) -> &str {
1225        &self.policy_id
1226    }
1227
1228    pub fn version(&self) -> ContractVersion {
1229        self.version
1230    }
1231
1232    pub fn scheduling(&self) -> SchedulingDiscipline {
1233        self.scheduling
1234    }
1235
1236    pub fn memory(&self) -> &RuntimeMemoryPolicy {
1237        &self.memory
1238    }
1239
1240    pub fn admission(&self) -> &AdmissionPolicy {
1241        &self.admission
1242    }
1243
1244    pub const fn attention_execution(&self) -> AttentionExecutionPolicy {
1245        self.attention_execution
1246    }
1247
1248    pub const fn execution_determinism(&self) -> ExecutionDeterminismRequirement {
1249        self.execution_determinism
1250    }
1251
1252    pub fn reusable_execution(&self) -> Option<&ReusableExecutionPolicy> {
1253        self.reusable_execution.as_ref()
1254    }
1255
1256    pub fn fingerprint_str(&self) -> &str {
1257        &self.fingerprint
1258    }
1259}
1260
1261impl RuntimePolicy for ResolvedRuntimePolicy {
1262    fn version(&self) -> ContractVersion {
1263        self.version
1264    }
1265
1266    fn memory_capacity_bytes(&self) -> u64 {
1267        self.memory.capacity_bytes
1268    }
1269
1270    fn memory_reserve_bytes(&self) -> u64 {
1271        self.memory.reserve_bytes
1272    }
1273
1274    fn maximum_active_sequences(&self) -> u32 {
1275        self.memory.maximum_active_sequences
1276    }
1277
1278    fn maximum_scheduled_tokens(&self) -> u64 {
1279        self.admission.maximum_scheduled_tokens
1280    }
1281
1282    fn attention_execution_policy(&self) -> AttentionExecutionPolicy {
1283        self.attention_execution
1284    }
1285
1286    fn execution_determinism_requirement(&self) -> ExecutionDeterminismRequirement {
1287        self.execution_determinism
1288    }
1289
1290    fn dynamic_storage_profile_order(&self) -> &[DynamicStorageProfile] {
1291        &self.memory.dynamic_storage_profile_order
1292    }
1293
1294    fn reusable_execution_policy(&self) -> Option<&ReusableExecutionPolicy> {
1295        self.reusable_execution.as_ref()
1296    }
1297
1298    fn checkpoint_capacity_policy(&self) -> Option<&CheckpointCapacityPolicy> {
1299        self.memory.checkpoint_capacity.as_ref()
1300    }
1301
1302    fn validate(&self) -> Result<(), VNextError> {
1303        Self::validate_fields(
1304            &self.policy_id,
1305            self.version,
1306            &self.memory,
1307            &self.admission,
1308            self.attention_execution,
1309            self.reusable_execution.as_ref(),
1310        )?;
1311        let computed = Self::compute_fingerprint(
1312            &self.policy_id,
1313            self.version,
1314            self.scheduling,
1315            &self.memory,
1316            &self.admission,
1317            self.attention_execution,
1318            self.execution_determinism,
1319            &self.reusable_execution,
1320        )?;
1321        if self.fingerprint != computed {
1322            return Err(invalid_plan(
1323                "runtime_policy.fingerprint",
1324                format!(
1325                    "does not match typed fields: expected `{computed}`, actual `{}`",
1326                    self.fingerprint
1327                ),
1328            ));
1329        }
1330        Ok(())
1331    }
1332}
1333
1334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1335pub struct RationalValue {
1336    numerator: i64,
1337    denominator: u64,
1338}
1339
1340#[derive(Deserialize)]
1341#[serde(deny_unknown_fields)]
1342struct RationalValueWire {
1343    numerator: i64,
1344    denominator: u64,
1345}
1346
1347impl<'de> Deserialize<'de> for RationalValue {
1348    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1349    where
1350        D: Deserializer<'de>,
1351    {
1352        let wire = RationalValueWire::deserialize(deserializer)?;
1353        Self::new(wire.numerator, wire.denominator).map_err(serde::de::Error::custom)
1354    }
1355}
1356
1357impl RationalValue {
1358    pub fn new(numerator: i64, denominator: u64) -> Result<Self, VNextError> {
1359        if denominator == 0 {
1360            return Err(invalid_plan(
1361                "sampling.rational",
1362                "denominator must be non-zero",
1363            ));
1364        }
1365        let divisor = greatest_common_divisor(numerator.unsigned_abs(), denominator);
1366        let numerator = ((numerator as i128) / (divisor as i128)) as i64;
1367        let denominator = denominator / divisor;
1368        Ok(Self {
1369            numerator,
1370            denominator,
1371        })
1372    }
1373
1374    fn validate(&self, field: &str) -> Result<(), VNextError> {
1375        if self.denominator == 0
1376            || greatest_common_divisor(self.numerator.unsigned_abs(), self.denominator) != 1
1377            || (self.numerator == 0 && self.denominator != 1)
1378        {
1379            return Err(invalid_plan(
1380                field,
1381                "rational value must be reduced with a non-zero denominator",
1382            ));
1383        }
1384        Ok(())
1385    }
1386
1387    fn compare(&self, numerator: i64, denominator: u64) -> Ordering {
1388        ((self.numerator as i128) * (denominator as i128))
1389            .cmp(&((numerator as i128) * (self.denominator as i128)))
1390    }
1391
1392    pub fn numerator(&self) -> i64 {
1393        self.numerator
1394    }
1395
1396    pub fn denominator(&self) -> u64 {
1397        self.denominator
1398    }
1399}
1400
1401fn greatest_common_divisor(mut left: u64, mut right: u64) -> u64 {
1402    while right != 0 {
1403        let remainder = left % right;
1404        left = right;
1405        right = remainder;
1406    }
1407    left
1408}
1409
1410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1411#[serde(rename_all = "snake_case")]
1412pub enum TriStatePolicy {
1413    ModelDefault,
1414    Enabled,
1415    Disabled,
1416}
1417
1418#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1419pub struct SamplingPolicy {
1420    temperature: RationalValue,
1421    top_p: RationalValue,
1422    top_k: Option<u32>,
1423    min_p: RationalValue,
1424    presence_penalty: RationalValue,
1425    repetition_penalty: RationalValue,
1426    seed: u64,
1427    thinking_policy: TriStatePolicy,
1428}
1429
1430#[derive(Deserialize)]
1431#[serde(deny_unknown_fields)]
1432struct SamplingPolicyWire {
1433    temperature: RationalValue,
1434    top_p: RationalValue,
1435    top_k: Option<u32>,
1436    min_p: RationalValue,
1437    presence_penalty: RationalValue,
1438    repetition_penalty: RationalValue,
1439    seed: u64,
1440    thinking_policy: TriStatePolicy,
1441}
1442
1443impl<'de> Deserialize<'de> for SamplingPolicy {
1444    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1445    where
1446        D: Deserializer<'de>,
1447    {
1448        let wire = SamplingPolicyWire::deserialize(deserializer)?;
1449        Self::new(
1450            wire.temperature,
1451            wire.top_p,
1452            wire.top_k,
1453            wire.min_p,
1454            wire.presence_penalty,
1455            wire.repetition_penalty,
1456            wire.seed,
1457            wire.thinking_policy,
1458        )
1459        .map_err(serde::de::Error::custom)
1460    }
1461}
1462
1463impl SamplingPolicy {
1464    #[allow(clippy::too_many_arguments)]
1465    pub fn new(
1466        temperature: RationalValue,
1467        top_p: RationalValue,
1468        top_k: Option<u32>,
1469        min_p: RationalValue,
1470        presence_penalty: RationalValue,
1471        repetition_penalty: RationalValue,
1472        seed: u64,
1473        thinking_policy: TriStatePolicy,
1474    ) -> Result<Self, VNextError> {
1475        let policy = Self {
1476            temperature,
1477            top_p,
1478            top_k,
1479            min_p,
1480            presence_penalty,
1481            repetition_penalty,
1482            seed,
1483            thinking_policy,
1484        };
1485        policy.validate()?;
1486        Ok(policy)
1487    }
1488
1489    fn validate(&self) -> Result<(), VNextError> {
1490        self.temperature.validate("sampling.temperature")?;
1491        self.top_p.validate("sampling.top_p")?;
1492        self.min_p.validate("sampling.min_p")?;
1493        self.presence_penalty
1494            .validate("sampling.presence_penalty")?;
1495        self.repetition_penalty
1496            .validate("sampling.repetition_penalty")?;
1497
1498        if self.temperature.compare(0, 1) == Ordering::Less {
1499            return Err(invalid_plan(
1500                "sampling.temperature",
1501                "must be greater than or equal to zero",
1502            ));
1503        }
1504        if self.top_p.compare(0, 1) != Ordering::Greater
1505            || self.top_p.compare(1, 1) == Ordering::Greater
1506        {
1507            return Err(invalid_plan(
1508                "sampling.top_p",
1509                "must be in the interval (0, 1]",
1510            ));
1511        }
1512        if self.top_k == Some(0) {
1513            return Err(invalid_plan(
1514                "sampling.top_k",
1515                "must be absent or greater than zero",
1516            ));
1517        }
1518        if self.min_p.compare(0, 1) == Ordering::Less
1519            || self.min_p.compare(1, 1) == Ordering::Greater
1520        {
1521            return Err(invalid_plan(
1522                "sampling.min_p",
1523                "must be in the interval [0, 1]",
1524            ));
1525        }
1526        if self.presence_penalty.compare(-2, 1) == Ordering::Less
1527            || self.presence_penalty.compare(2, 1) == Ordering::Greater
1528        {
1529            return Err(invalid_plan(
1530                "sampling.presence_penalty",
1531                "must be in the interval [-2, 2]",
1532            ));
1533        }
1534        if self.repetition_penalty.compare(0, 1) != Ordering::Greater {
1535            return Err(invalid_plan(
1536                "sampling.repetition_penalty",
1537                "must be greater than zero",
1538            ));
1539        }
1540        Ok(())
1541    }
1542
1543    pub fn temperature(&self) -> RationalValue {
1544        self.temperature
1545    }
1546
1547    pub fn top_p(&self) -> RationalValue {
1548        self.top_p
1549    }
1550
1551    pub fn top_k(&self) -> Option<u32> {
1552        self.top_k
1553    }
1554
1555    pub fn min_p(&self) -> RationalValue {
1556        self.min_p
1557    }
1558
1559    pub fn presence_penalty(&self) -> RationalValue {
1560        self.presence_penalty
1561    }
1562
1563    pub fn repetition_penalty(&self) -> RationalValue {
1564        self.repetition_penalty
1565    }
1566
1567    pub fn seed(&self) -> u64 {
1568        self.seed
1569    }
1570
1571    pub fn thinking_policy(&self) -> TriStatePolicy {
1572        self.thinking_policy
1573    }
1574}
1575
1576#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1577pub struct StopTokenCollisionPolicy {
1578    allowed_model_roles: BTreeSet<SpecialTokenRole>,
1579}
1580
1581impl StopTokenCollisionPolicy {
1582    pub fn new(allowed_model_roles: BTreeSet<SpecialTokenRole>) -> Result<Self, VNextError> {
1583        if allowed_model_roles.contains(&SpecialTokenRole::Stop) {
1584            return Err(invalid_plan(
1585                "stop.collision_policy",
1586                "a stop-token collision policy can only name model-owned roles",
1587            ));
1588        }
1589        Ok(Self {
1590            allowed_model_roles,
1591        })
1592    }
1593
1594    pub fn require_distinct() -> Self {
1595        Self {
1596            allowed_model_roles: BTreeSet::new(),
1597        }
1598    }
1599
1600    pub fn allows(&self, model_role: SpecialTokenRole) -> bool {
1601        self.allowed_model_roles.contains(&model_role)
1602    }
1603
1604    pub fn allowed_model_roles(&self) -> &BTreeSet<SpecialTokenRole> {
1605        &self.allowed_model_roles
1606    }
1607}
1608
1609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1610pub struct StopPolicy {
1611    pub maximum_output_tokens: u32,
1612    pub token_ids: BTreeSet<u32>,
1613    pub strings: Vec<String>,
1614    pub collision_policy: StopTokenCollisionPolicy,
1615}
1616
1617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1618#[serde(rename_all = "snake_case")]
1619pub enum StructuredOutputPolicy {
1620    Disabled,
1621    JsonObject,
1622    JsonSchema { schema_sha256: String },
1623}
1624
1625#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1626#[serde(rename_all = "snake_case")]
1627pub enum ResolutionField {
1628    OriginalSources,
1629    ResolvedSources,
1630    Config,
1631    ExternalMetadata,
1632    Family,
1633    NumericalExecution,
1634    WeightSchema,
1635    WeightFormat,
1636    Tokenizer,
1637    Template,
1638    SpecialTokens,
1639    Device,
1640    Capabilities,
1641    RuntimePreset,
1642    RuntimeMemory,
1643    Admission,
1644    Engine,
1645    ExecutionPlan,
1646    Sampling,
1647    Stop,
1648    StructuredOutput,
1649}
1650
1651#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1652#[serde(rename_all = "snake_case")]
1653pub enum ResolutionDecisionSource {
1654    UserInput,
1655    CommandLine,
1656    ConfigFile,
1657    ModelMetadata,
1658    TypedModelResolution,
1659    ProductDefault,
1660    RuntimePreset,
1661    CapabilityResolution,
1662    Planner,
1663}
1664
1665impl ResolutionField {
1666    pub const fn as_str(self) -> &'static str {
1667        match self {
1668            Self::OriginalSources => "original_sources",
1669            Self::ResolvedSources => "resolved_sources",
1670            Self::Config => "config",
1671            Self::ExternalMetadata => "external_metadata",
1672            Self::Family => "family",
1673            Self::NumericalExecution => "numerical_execution",
1674            Self::WeightSchema => "weight_schema",
1675            Self::WeightFormat => "weight_format",
1676            Self::Tokenizer => "tokenizer",
1677            Self::Template => "template",
1678            Self::SpecialTokens => "special_tokens",
1679            Self::Device => "device",
1680            Self::Capabilities => "capabilities",
1681            Self::RuntimePreset => "runtime_preset",
1682            Self::RuntimeMemory => "runtime_memory",
1683            Self::Admission => "admission",
1684            Self::Engine => "engine",
1685            Self::ExecutionPlan => "execution_plan",
1686            Self::Sampling => "sampling",
1687            Self::Stop => "stop",
1688            Self::StructuredOutput => "structured_output",
1689        }
1690    }
1691
1692    /// Returns whether a provenance source is allowed to author this decision.
1693    /// This matrix prevents a self-consistent package from relabeling a
1694    /// planner, model, or capability decision as a generic product default.
1695    pub const fn accepts_source(self, source: ResolutionDecisionSource) -> bool {
1696        use ResolutionDecisionSource as Source;
1697        use ResolutionField as Field;
1698
1699        match self {
1700            Field::OriginalSources => matches!(
1701                source,
1702                Source::UserInput | Source::CommandLine | Source::ConfigFile
1703            ),
1704            Field::ResolvedSources => {
1705                matches!(source, Source::ModelMetadata | Source::TypedModelResolution)
1706            }
1707            Field::Config => matches!(
1708                source,
1709                Source::ConfigFile | Source::ModelMetadata | Source::TypedModelResolution
1710            ),
1711            Field::ExternalMetadata
1712            | Field::Family
1713            | Field::WeightSchema
1714            | Field::Tokenizer
1715            | Field::Template
1716            | Field::SpecialTokens => {
1717                matches!(source, Source::ModelMetadata | Source::TypedModelResolution)
1718            }
1719            Field::WeightFormat => matches!(
1720                source,
1721                Source::CommandLine
1722                    | Source::ConfigFile
1723                    | Source::ModelMetadata
1724                    | Source::TypedModelResolution
1725            ),
1726            Field::Device => matches!(
1727                source,
1728                Source::CommandLine
1729                    | Source::ConfigFile
1730                    | Source::ProductDefault
1731                    | Source::CapabilityResolution
1732            ),
1733            Field::Capabilities => matches!(source, Source::CapabilityResolution),
1734            Field::RuntimePreset => matches!(
1735                source,
1736                Source::CommandLine
1737                    | Source::ConfigFile
1738                    | Source::ProductDefault
1739                    | Source::RuntimePreset
1740            ),
1741            Field::RuntimeMemory | Field::Admission => matches!(
1742                source,
1743                Source::CommandLine | Source::ConfigFile | Source::RuntimePreset
1744            ),
1745            Field::Engine => {
1746                matches!(source, Source::RuntimePreset | Source::CapabilityResolution)
1747            }
1748            Field::ExecutionPlan | Field::NumericalExecution => matches!(source, Source::Planner),
1749            Field::Sampling | Field::StructuredOutput => matches!(
1750                source,
1751                Source::UserInput
1752                    | Source::CommandLine
1753                    | Source::ConfigFile
1754                    | Source::ProductDefault
1755            ),
1756            Field::Stop => matches!(
1757                source,
1758                Source::UserInput
1759                    | Source::CommandLine
1760                    | Source::ConfigFile
1761                    | Source::ModelMetadata
1762                    | Source::ProductDefault
1763            ),
1764        }
1765    }
1766}
1767
1768#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1769#[serde(try_from = "String", into = "String")]
1770pub struct ResolutionReasonId(String);
1771
1772impl ResolutionReasonId {
1773    pub fn new(value: impl Into<String>) -> Result<Self, VNextError> {
1774        let value = value.into();
1775        validate_portable_identifier("resolution_reason_id", &value, 160)?;
1776        Ok(Self(value))
1777    }
1778
1779    pub fn as_str(&self) -> &str {
1780        &self.0
1781    }
1782}
1783
1784impl TryFrom<String> for ResolutionReasonId {
1785    type Error = VNextError;
1786
1787    fn try_from(value: String) -> Result<Self, Self::Error> {
1788        Self::new(value)
1789    }
1790}
1791
1792impl From<ResolutionReasonId> for String {
1793    fn from(value: ResolutionReasonId) -> Self {
1794        value.0
1795    }
1796}
1797
1798impl fmt::Display for ResolutionReasonId {
1799    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1800        formatter.write_str(&self.0)
1801    }
1802}
1803
1804#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1805#[serde(try_from = "String", into = "String")]
1806pub struct ResolutionFingerprint(String);
1807
1808impl ResolutionFingerprint {
1809    pub fn new(value: impl Into<String>) -> Result<Self, VNextError> {
1810        let value = value.into();
1811        if !is_canonical_sha256(&value) {
1812            return Err(invalid_plan(
1813                "resolution_fingerprint",
1814                "must be a canonical lowercase SHA-256",
1815            ));
1816        }
1817        Ok(Self(value))
1818    }
1819
1820    pub fn as_str(&self) -> &str {
1821        &self.0
1822    }
1823}
1824
1825impl TryFrom<String> for ResolutionFingerprint {
1826    type Error = VNextError;
1827
1828    fn try_from(value: String) -> Result<Self, Self::Error> {
1829        Self::new(value)
1830    }
1831}
1832
1833impl From<ResolutionFingerprint> for String {
1834    fn from(value: ResolutionFingerprint) -> Self {
1835        value.0
1836    }
1837}
1838
1839impl fmt::Display for ResolutionFingerprint {
1840    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1841        formatter.write_str(&self.0)
1842    }
1843}
1844
1845#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1846#[serde(try_from = "String", into = "String")]
1847pub struct ResolutionArtifactId(String);
1848
1849impl ResolutionArtifactId {
1850    pub fn new(value: impl Into<String>) -> Result<Self, VNextError> {
1851        let value = value.into();
1852        validate_portable_identifier("resolution_artifact_id", &value, 160)?;
1853        Ok(Self(value))
1854    }
1855
1856    pub fn as_str(&self) -> &str {
1857        &self.0
1858    }
1859}
1860
1861impl TryFrom<String> for ResolutionArtifactId {
1862    type Error = VNextError;
1863
1864    fn try_from(value: String) -> Result<Self, Self::Error> {
1865        Self::new(value)
1866    }
1867}
1868
1869impl From<ResolutionArtifactId> for String {
1870    fn from(value: ResolutionArtifactId) -> Self {
1871        value.0
1872    }
1873}
1874
1875impl fmt::Display for ResolutionArtifactId {
1876    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1877        formatter.write_str(&self.0)
1878    }
1879}
1880
1881/// Externally anchored origin of resolution source bytes. A source is either
1882/// one exact file from the locked model snapshot or an explicitly identified
1883/// upstream producer. There is no unstructured locator variant.
1884#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1885#[serde(rename_all = "snake_case", tag = "kind")]
1886pub enum ResolutionSourceProvenance {
1887    LockedModelFile {
1888        source_role: ModelArtifactSourceRole,
1889        relative_path: String,
1890    },
1891    Upstream {
1892        producer_id: String,
1893        producer_version: ContractVersion,
1894        producer_implementation_fingerprint: ResolutionFingerprint,
1895        revision: String,
1896        artifact_locator: String,
1897    },
1898}
1899
1900impl ResolutionSourceProvenance {
1901    fn validate(&self) -> Result<(), VNextError> {
1902        match self {
1903            Self::LockedModelFile {
1904                source_role: _,
1905                relative_path,
1906            } => {
1907                if !validate_source_path(relative_path) {
1908                    return Err(invalid_plan(
1909                        "resolution_source_provenance.locked_model_file",
1910                        "relative path must identify one portable locked model file",
1911                    ));
1912                }
1913            }
1914            Self::Upstream {
1915                producer_id,
1916                producer_version,
1917                revision,
1918                artifact_locator,
1919                ..
1920            } => {
1921                validate_portable_identifier(
1922                    "resolution_source_provenance.producer_id",
1923                    producer_id,
1924                    160,
1925                )?;
1926                if producer_version.major == 0 {
1927                    return Err(invalid_plan(
1928                        "resolution_source_provenance.producer_version",
1929                        "producer contract major version must be non-zero",
1930                    ));
1931                }
1932                validate_portable_identifier(
1933                    "resolution_source_provenance.revision",
1934                    revision,
1935                    256,
1936                )?;
1937                validate_portable_identifier(
1938                    "resolution_source_provenance.artifact_locator",
1939                    artifact_locator,
1940                    512,
1941                )?;
1942            }
1943        }
1944        Ok(())
1945    }
1946
1947    pub fn locator(&self) -> &str {
1948        match self {
1949            Self::LockedModelFile {
1950                source_role: _,
1951                relative_path,
1952            } => relative_path,
1953            Self::Upstream {
1954                artifact_locator, ..
1955            } => artifact_locator,
1956        }
1957    }
1958}
1959
1960fn resolution_provenance_bytes(
1961    provenance: &ResolutionSourceProvenance,
1962) -> Result<usize, VNextError> {
1963    match provenance {
1964        ResolutionSourceProvenance::LockedModelFile {
1965            source_role,
1966            relative_path,
1967        } => source_role
1968            .as_str()
1969            .len()
1970            .checked_add(relative_path.len())
1971            .ok_or_else(|| {
1972                invalid_plan(
1973                    "resolution_source_evidence.provenance",
1974                    "provenance byte count overflowed",
1975                )
1976            }),
1977        ResolutionSourceProvenance::Upstream {
1978            producer_id,
1979            producer_implementation_fingerprint,
1980            revision,
1981            artifact_locator,
1982            ..
1983        } => [
1984            producer_id.len(),
1985            producer_implementation_fingerprint.as_str().len(),
1986            revision.len(),
1987            artifact_locator.len(),
1988        ]
1989        .into_iter()
1990        .try_fold(0usize, |total, length| {
1991            total.checked_add(length).ok_or_else(|| {
1992                invalid_plan(
1993                    "resolution_source_evidence.provenance",
1994                    "provenance byte count overflowed",
1995                )
1996            })
1997        }),
1998    }
1999}
2000
2001fn validate_resolution_field_paths(field_paths: &BTreeSet<String>) -> Result<(), VNextError> {
2002    if field_paths.is_empty() || field_paths.len() > MAX_RESOLUTION_FIELD_PATHS {
2003        return Err(invalid_plan(
2004            "resolution_source_evidence.field_paths",
2005            format!("must contain between 1 and {MAX_RESOLUTION_FIELD_PATHS} unique paths"),
2006        ));
2007    }
2008
2009    let mut total_bytes = 0usize;
2010    for path in field_paths {
2011        if !path.starts_with('/')
2012            || path.len() > MAX_RESOLUTION_FIELD_PATH_BYTES
2013            || path.trim() != path
2014            || path.bytes().any(|byte| byte.is_ascii_control())
2015        {
2016            return Err(invalid_plan(
2017                "resolution_source_evidence.field_paths",
2018                format!(
2019                    "each path must be a portable JSON pointer of at most {MAX_RESOLUTION_FIELD_PATH_BYTES} bytes"
2020                ),
2021            ));
2022        }
2023        total_bytes = total_bytes.checked_add(path.len()).ok_or_else(|| {
2024            invalid_plan(
2025                "resolution_source_evidence.field_paths",
2026                "field-path byte count overflowed",
2027            )
2028        })?;
2029        if total_bytes > MAX_RESOLUTION_FIELD_PATH_TOTAL_BYTES {
2030            return Err(invalid_plan(
2031                "resolution_source_evidence.field_paths",
2032                format!("total path bytes must not exceed {MAX_RESOLUTION_FIELD_PATH_TOTAL_BYTES}"),
2033            ));
2034        }
2035    }
2036    Ok(())
2037}
2038
2039fn validate_resolution_source_availability(
2040    source_bytes: &[u8],
2041    provenance: &ResolutionSourceProvenance,
2042    field_paths: &BTreeSet<String>,
2043) -> Result<(), VNextError> {
2044    validate_resolution_source_bytes(source_bytes)?;
2045    let provenance_bytes = resolution_provenance_bytes(provenance)?;
2046    if provenance_bytes > MAX_RESOLUTION_PROVENANCE_BYTES {
2047        return Err(invalid_plan(
2048            "resolution_source_evidence.provenance",
2049            format!("must not exceed {MAX_RESOLUTION_PROVENANCE_BYTES} bytes"),
2050        ));
2051    }
2052    validate_resolution_field_paths(field_paths)?;
2053    provenance.validate()
2054}
2055
2056#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2057pub struct ResolutionSourceArtifact {
2058    id: ResolutionArtifactId,
2059    source: ResolutionDecisionSource,
2060    provenance: ResolutionSourceProvenance,
2061    parser: ResolutionParserDescriptor,
2062    content_size_bytes: u64,
2063    content_fingerprint: ResolutionFingerprint,
2064    canonical_document_fingerprint: ResolutionFingerprint,
2065    fields: BTreeMap<String, ResolutionFingerprint>,
2066}
2067
2068#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2069#[serde(deny_unknown_fields)]
2070pub struct UnvalidatedResolutionSourceArtifact {
2071    pub id: ResolutionArtifactId,
2072    pub source: ResolutionDecisionSource,
2073    pub provenance: ResolutionSourceProvenance,
2074    pub parser: ResolutionParserDescriptor,
2075    pub content_size_bytes: u64,
2076    pub content_fingerprint: ResolutionFingerprint,
2077    pub canonical_document_fingerprint: ResolutionFingerprint,
2078    pub fields: BTreeMap<String, ResolutionFingerprint>,
2079}
2080
2081#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2082pub struct ResolutionParserDescriptor {
2083    id: String,
2084    version: ContractVersion,
2085    implementation_fingerprint: ResolutionFingerprint,
2086}
2087
2088#[derive(Deserialize)]
2089#[serde(deny_unknown_fields)]
2090struct ResolutionParserDescriptorWire {
2091    id: String,
2092    version: ContractVersion,
2093    implementation_fingerprint: ResolutionFingerprint,
2094}
2095
2096impl<'de> Deserialize<'de> for ResolutionParserDescriptor {
2097    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2098    where
2099        D: Deserializer<'de>,
2100    {
2101        let wire = ResolutionParserDescriptorWire::deserialize(deserializer)?;
2102        Self::new(wire.id, wire.version, wire.implementation_fingerprint)
2103            .map_err(serde::de::Error::custom)
2104    }
2105}
2106
2107impl ResolutionParserDescriptor {
2108    pub fn new(
2109        id: impl Into<String>,
2110        version: ContractVersion,
2111        implementation_fingerprint: ResolutionFingerprint,
2112    ) -> Result<Self, VNextError> {
2113        let descriptor = Self {
2114            id: id.into(),
2115            version,
2116            implementation_fingerprint,
2117        };
2118        descriptor.validate()?;
2119        Ok(descriptor)
2120    }
2121
2122    fn validate(&self) -> Result<(), VNextError> {
2123        validate_portable_identifier("resolution_parser.id", &self.id, 160)?;
2124        if self.version.major == 0 {
2125            return Err(invalid_plan(
2126                "resolution_parser.version",
2127                "parser contract major version must be non-zero",
2128            ));
2129        }
2130        Ok(())
2131    }
2132
2133    pub fn id(&self) -> &str {
2134        &self.id
2135    }
2136
2137    pub const fn version(&self) -> ContractVersion {
2138        self.version
2139    }
2140
2141    pub fn implementation_fingerprint(&self) -> &ResolutionFingerprint {
2142        &self.implementation_fingerprint
2143    }
2144}
2145
2146/// Trusted parser implementation supplied by the composition root. Core
2147/// records its exact identity and reruns it for every wire revalidation.
2148pub trait ResolutionSourceParser: Send + Sync {
2149    fn descriptor(&self) -> Result<ResolutionParserDescriptor, VNextError>;
2150
2151    fn parse(
2152        &self,
2153        source: ResolutionDecisionSource,
2154        provenance: &ResolutionSourceProvenance,
2155        source_bytes: &[u8],
2156    ) -> Result<serde_json::Value, VNextError>;
2157}
2158
2159pub struct JsonResolutionSourceParser;
2160
2161pub static JSON_RESOLUTION_SOURCE_PARSER: JsonResolutionSourceParser = JsonResolutionSourceParser;
2162
2163impl ResolutionSourceParser for JsonResolutionSourceParser {
2164    fn descriptor(&self) -> Result<ResolutionParserDescriptor, VNextError> {
2165        ResolutionParserDescriptor::new(
2166            "resolution-parser.core-json",
2167            ContractVersion::new(1, 0),
2168            ResolutionFingerprint::new(canonical_fingerprint(
2169                &"ferrum.resolution-parser.core-json.v1",
2170                "fingerprint core JSON resolution parser",
2171            )?)?,
2172        )
2173    }
2174
2175    fn parse(
2176        &self,
2177        _source: ResolutionDecisionSource,
2178        _provenance: &ResolutionSourceProvenance,
2179        source_bytes: &[u8],
2180    ) -> Result<serde_json::Value, VNextError> {
2181        validate_resolution_source_bytes(source_bytes)?;
2182        preflight_resolution_json(source_bytes, ResolutionJsonBudget::SOURCE)?;
2183        serde_json::from_slice(source_bytes).map_err(|error| VNextError::Serialization {
2184            context: "parse resolution source JSON",
2185            message: error.to_string(),
2186        })
2187    }
2188}
2189
2190/// External source bytes plus the exact trusted parser used to derive typed
2191/// decision fields. This evidence is never serialized into a resolved plan.
2192#[derive(Clone)]
2193pub struct ResolutionSourceEvidence<'a> {
2194    id: ResolutionArtifactId,
2195    source: ResolutionDecisionSource,
2196    provenance: ResolutionSourceProvenance,
2197    source_bytes: Vec<u8>,
2198    field_paths: BTreeSet<String>,
2199    parser: &'a dyn ResolutionSourceParser,
2200}
2201
2202impl<'a> ResolutionSourceEvidence<'a> {
2203    pub fn new(
2204        id: ResolutionArtifactId,
2205        source: ResolutionDecisionSource,
2206        provenance: ResolutionSourceProvenance,
2207        source_bytes: Vec<u8>,
2208        field_paths: BTreeSet<String>,
2209        parser: &'a dyn ResolutionSourceParser,
2210    ) -> Result<Self, VNextError> {
2211        validate_resolution_source_availability(&source_bytes, &provenance, &field_paths)?;
2212        Ok(Self {
2213            id,
2214            source,
2215            provenance,
2216            source_bytes,
2217            field_paths,
2218            parser,
2219        })
2220    }
2221
2222    /// Runs the same deterministic parser verification used when constructing
2223    /// or revalidating a resolved plan. Callers may use this for an explicit
2224    /// preflight, but plan validation never relies on a prior call.
2225    pub fn validate(&self) -> Result<(), VNextError> {
2226        self.verify().map(drop)
2227    }
2228
2229    fn verify(&self) -> Result<ResolutionSourceArtifact, VNextError> {
2230        validate_resolution_source_availability(
2231            &self.source_bytes,
2232            &self.provenance,
2233            &self.field_paths,
2234        )?;
2235        let parser = self.parser.descriptor()?;
2236        parser.validate()?;
2237        let first_document =
2238            self.parser
2239                .parse(self.source, &self.provenance, &self.source_bytes)?;
2240        let (first_document, first_fingerprint) = validate_and_canonicalize_resolution_json(
2241            first_document,
2242            "fingerprint first parsed resolution source document",
2243        )?;
2244        let parser_after_first_parse = self.parser.descriptor()?;
2245        parser_after_first_parse.validate()?;
2246        let second_document =
2247            self.parser
2248                .parse(self.source, &self.provenance, &self.source_bytes)?;
2249        let (second_document, second_fingerprint) = validate_and_canonicalize_resolution_json(
2250            second_document,
2251            "fingerprint second parsed resolution source document",
2252        )?;
2253        let parser_after_second_parse = self.parser.descriptor()?;
2254        parser_after_second_parse.validate()?;
2255        if parser != parser_after_first_parse
2256            || parser != parser_after_second_parse
2257            || first_fingerprint != second_fingerprint
2258            || first_document != second_document
2259        {
2260            return Err(invalid_plan(
2261                "resolution_source_evidence.parser",
2262                "parser identity changed or repeated parsing of identical bytes was nondeterministic",
2263            ));
2264        }
2265        ResolutionSourceArtifact::from_verified_document(
2266            self.id.clone(),
2267            self.source,
2268            self.provenance.clone(),
2269            &self.source_bytes,
2270            parser,
2271            &first_document,
2272            first_fingerprint,
2273            self.field_paths.clone(),
2274        )
2275    }
2276
2277    pub fn id(&self) -> &ResolutionArtifactId {
2278        &self.id
2279    }
2280
2281    pub const fn source(&self) -> ResolutionDecisionSource {
2282        self.source
2283    }
2284
2285    pub fn provenance(&self) -> &ResolutionSourceProvenance {
2286        &self.provenance
2287    }
2288
2289    pub fn locator(&self) -> &str {
2290        self.provenance.locator()
2291    }
2292
2293    pub fn source_bytes(&self) -> &[u8] {
2294        &self.source_bytes
2295    }
2296
2297    pub fn field_paths(&self) -> &BTreeSet<String> {
2298        &self.field_paths
2299    }
2300}
2301
2302impl ResolutionSourceArtifact {
2303    fn from_verified_document(
2304        id: ResolutionArtifactId,
2305        source: ResolutionDecisionSource,
2306        provenance: ResolutionSourceProvenance,
2307        bytes: &[u8],
2308        parser: ResolutionParserDescriptor,
2309        document: &serde_json::Value,
2310        canonical_document_fingerprint: String,
2311        field_paths: BTreeSet<String>,
2312    ) -> Result<Self, VNextError> {
2313        validate_resolution_source_availability(bytes, &provenance, &field_paths)?;
2314        let content_size_bytes = u64::try_from(bytes.len()).map_err(|_| {
2315            invalid_plan(
2316                "resolution_source_artifact.content_size_bytes",
2317                "source byte length does not fit u64",
2318            )
2319        })?;
2320        let fields = field_paths
2321            .into_iter()
2322            .map(|path| {
2323                let value = document.pointer(&path).ok_or_else(|| {
2324                    invalid_plan(
2325                        "resolution_source_artifact.fields",
2326                        format!("JSON pointer `{path}` is absent from the source document"),
2327                    )
2328                })?;
2329                ResolutionFingerprint::new(canonical_value_fingerprint(
2330                    value,
2331                    "fingerprint resolution source field",
2332                )?)
2333                .map(|fingerprint| (path, fingerprint))
2334            })
2335            .collect::<Result<BTreeMap<_, _>, VNextError>>()?;
2336        Ok(Self {
2337            id,
2338            source,
2339            provenance,
2340            parser,
2341            content_size_bytes,
2342            content_fingerprint: ResolutionFingerprint::new(format!(
2343                "{:x}",
2344                Sha256::digest(bytes)
2345            ))?,
2346            canonical_document_fingerprint: ResolutionFingerprint::new(
2347                canonical_document_fingerprint,
2348            )?,
2349            fields,
2350        })
2351    }
2352
2353    pub fn id(&self) -> &ResolutionArtifactId {
2354        &self.id
2355    }
2356
2357    pub fn source(&self) -> ResolutionDecisionSource {
2358        self.source
2359    }
2360
2361    pub fn provenance(&self) -> &ResolutionSourceProvenance {
2362        &self.provenance
2363    }
2364
2365    pub fn locator(&self) -> &str {
2366        self.provenance.locator()
2367    }
2368
2369    pub fn content_size_bytes(&self) -> u64 {
2370        self.content_size_bytes
2371    }
2372
2373    pub fn content_fingerprint(&self) -> &ResolutionFingerprint {
2374        &self.content_fingerprint
2375    }
2376
2377    pub fn parser(&self) -> &ResolutionParserDescriptor {
2378        &self.parser
2379    }
2380
2381    pub fn canonical_document_fingerprint(&self) -> &ResolutionFingerprint {
2382        &self.canonical_document_fingerprint
2383    }
2384
2385    pub fn fields(&self) -> &BTreeMap<String, ResolutionFingerprint> {
2386        &self.fields
2387    }
2388}
2389
2390impl UnvalidatedResolutionSourceArtifact {
2391    fn revalidate(
2392        self,
2393        expected: &ResolutionSourceArtifact,
2394    ) -> Result<ResolutionSourceArtifact, VNextError> {
2395        if self.id != expected.id
2396            || self.source != expected.source
2397            || self.provenance != expected.provenance
2398            || self.parser != expected.parser
2399            || self.content_size_bytes != expected.content_size_bytes
2400            || self.content_fingerprint != expected.content_fingerprint
2401            || self.canonical_document_fingerprint != expected.canonical_document_fingerprint
2402            || self.fields != expected.fields
2403        {
2404            return Err(invalid_plan(
2405                "source_artifacts",
2406                format!(
2407                    "serialized source artifact `{}` differs from externally verified evidence",
2408                    self.id
2409                ),
2410            ));
2411        }
2412        Ok(expected.clone())
2413    }
2414}
2415
2416#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2417pub struct ResolutionDecisionEvidence {
2418    source_artifact_id: ResolutionArtifactId,
2419    source_field_path: String,
2420    chosen_value_fingerprint: ResolutionFingerprint,
2421}
2422
2423#[derive(Deserialize)]
2424#[serde(deny_unknown_fields)]
2425struct ResolutionDecisionEvidenceWire {
2426    source_artifact_id: ResolutionArtifactId,
2427    source_field_path: String,
2428    chosen_value_fingerprint: ResolutionFingerprint,
2429}
2430
2431impl<'de> Deserialize<'de> for ResolutionDecisionEvidence {
2432    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2433    where
2434        D: Deserializer<'de>,
2435    {
2436        let wire = ResolutionDecisionEvidenceWire::deserialize(deserializer)?;
2437        Self::new(
2438            wire.source_artifact_id,
2439            wire.source_field_path,
2440            wire.chosen_value_fingerprint,
2441        )
2442        .map_err(serde::de::Error::custom)
2443    }
2444}
2445
2446impl ResolutionDecisionEvidence {
2447    fn new(
2448        source_artifact_id: ResolutionArtifactId,
2449        source_field_path: impl Into<String>,
2450        chosen_value_fingerprint: ResolutionFingerprint,
2451    ) -> Result<Self, VNextError> {
2452        let source_field_path = source_field_path.into();
2453        if source_field_path.is_empty()
2454            || source_field_path.len() > 512
2455            || source_field_path.trim() != source_field_path
2456            || source_field_path
2457                .bytes()
2458                .any(|byte| byte.is_ascii_control())
2459        {
2460            return Err(invalid_plan(
2461                "decisions.evidence.source_field_path",
2462                "must be a non-empty portable path of at most 512 bytes",
2463            ));
2464        }
2465        Ok(Self {
2466            source_artifact_id,
2467            source_field_path,
2468            chosen_value_fingerprint,
2469        })
2470    }
2471
2472    pub fn source_artifact_id(&self) -> &ResolutionArtifactId {
2473        &self.source_artifact_id
2474    }
2475
2476    pub fn source_field_path(&self) -> &str {
2477        &self.source_field_path
2478    }
2479
2480    pub fn chosen_value_fingerprint(&self) -> &ResolutionFingerprint {
2481        &self.chosen_value_fingerprint
2482    }
2483}
2484
2485#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2486pub struct ResolutionDecision {
2487    field: ResolutionField,
2488    source: ResolutionDecisionSource,
2489    reason_id: ResolutionReasonId,
2490    evidence: ResolutionDecisionEvidence,
2491}
2492
2493#[derive(Deserialize)]
2494#[serde(deny_unknown_fields)]
2495struct ResolutionDecisionWire {
2496    field: ResolutionField,
2497    source: ResolutionDecisionSource,
2498    reason_id: ResolutionReasonId,
2499    evidence: ResolutionDecisionEvidence,
2500}
2501
2502impl<'de> Deserialize<'de> for ResolutionDecision {
2503    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2504    where
2505        D: Deserializer<'de>,
2506    {
2507        let wire = ResolutionDecisionWire::deserialize(deserializer)?;
2508        Ok(Self::new(
2509            wire.field,
2510            wire.source,
2511            wire.reason_id,
2512            wire.evidence,
2513        ))
2514    }
2515}
2516
2517impl ResolutionDecision {
2518    fn new(
2519        field: ResolutionField,
2520        source: ResolutionDecisionSource,
2521        reason_id: ResolutionReasonId,
2522        evidence: ResolutionDecisionEvidence,
2523    ) -> Self {
2524        Self {
2525            field,
2526            source,
2527            reason_id,
2528            evidence,
2529        }
2530    }
2531
2532    pub fn field(&self) -> ResolutionField {
2533        self.field
2534    }
2535
2536    pub fn source(&self) -> ResolutionDecisionSource {
2537        self.source
2538    }
2539
2540    pub fn reason_id(&self) -> &ResolutionReasonId {
2541        &self.reason_id
2542    }
2543
2544    pub fn evidence(&self) -> &ResolutionDecisionEvidence {
2545        &self.evidence
2546    }
2547}
2548
2549/// Construction-time link from a resolved field to externally supplied source
2550/// evidence. It intentionally carries no chosen-value fingerprint; core derives
2551/// that fingerprint independently from both sides after parsing raw evidence.
2552#[derive(Debug, Clone, PartialEq, Eq)]
2553pub struct ResolutionDecisionBinding {
2554    field: ResolutionField,
2555    source: ResolutionDecisionSource,
2556    reason_id: ResolutionReasonId,
2557    source_artifact_id: ResolutionArtifactId,
2558    source_field_path: String,
2559}
2560
2561impl ResolutionDecisionBinding {
2562    pub fn new(
2563        field: ResolutionField,
2564        source: ResolutionDecisionSource,
2565        reason_id: ResolutionReasonId,
2566        source_artifact_id: ResolutionArtifactId,
2567        source_field_path: impl Into<String>,
2568    ) -> Result<Self, VNextError> {
2569        let source_field_path = source_field_path.into();
2570        if !field.accepts_source(source) {
2571            return Err(invalid_plan(
2572                "decision_bindings.source",
2573                format!("source `{source:?}` cannot author field `{field:?}`"),
2574            ));
2575        }
2576        if !source_field_path.starts_with('/')
2577            || source_field_path.len() > 512
2578            || source_field_path.trim() != source_field_path
2579            || source_field_path
2580                .bytes()
2581                .any(|byte| byte.is_ascii_control())
2582        {
2583            return Err(invalid_plan(
2584                "decision_bindings.source_field_path",
2585                "must be a portable JSON pointer of at most 512 bytes",
2586            ));
2587        }
2588        Ok(Self {
2589            field,
2590            source,
2591            reason_id,
2592            source_artifact_id,
2593            source_field_path,
2594        })
2595    }
2596
2597    pub fn field(&self) -> ResolutionField {
2598        self.field
2599    }
2600
2601    pub fn source(&self) -> ResolutionDecisionSource {
2602        self.source
2603    }
2604
2605    pub fn reason_id(&self) -> &ResolutionReasonId {
2606        &self.reason_id
2607    }
2608
2609    pub fn source_artifact_id(&self) -> &ResolutionArtifactId {
2610        &self.source_artifact_id
2611    }
2612
2613    pub fn source_field_path(&self) -> &str {
2614        &self.source_field_path
2615    }
2616}
2617
2618#[derive(Debug, Clone, PartialEq, Eq)]
2619pub struct ResolvedModelPlanInputs {
2620    pub original_sources: OriginalModelSources,
2621    pub resolved_sources: ResolvedModelSources,
2622    pub config: ModelConfigFingerprint,
2623    pub external_metadata_id: super::ExternalModelMetadataId,
2624    pub prepared_family: PreparedModelFamily,
2625    pub numerical_execution: NumericalProfileResolution,
2626    pub tokenizer: TokenizerDescriptor,
2627    pub device: DeviceDescriptor,
2628    pub capabilities: CapabilityCatalog,
2629    pub runtime: ResolvedRuntimePolicy,
2630    pub engine: EngineSelection,
2631    pub execution_plan: ExecutionPlan,
2632    pub sampling: SamplingPolicy,
2633    pub stop: StopPolicy,
2634    pub structured_output: StructuredOutputPolicy,
2635}
2636
2637#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2638pub struct ResolvedModelPlanParts {
2639    source_artifacts: Vec<ResolutionSourceArtifact>,
2640    pub original_sources: OriginalModelSources,
2641    pub resolved_sources: ResolvedModelSources,
2642    pub config: ModelConfigFingerprint,
2643    pub external_metadata_id: super::ExternalModelMetadataId,
2644    pub prepared_family: PreparedModelFamily,
2645    pub numerical_execution: NumericalProfileResolution,
2646    pub tokenizer: TokenizerDescriptor,
2647    pub device: DeviceDescriptor,
2648    pub capabilities: CapabilityCatalog,
2649    pub runtime: ResolvedRuntimePolicy,
2650    pub engine: EngineSelection,
2651    pub execution_plan: ExecutionPlan,
2652    pub sampling: SamplingPolicy,
2653    pub stop: StopPolicy,
2654    pub structured_output: StructuredOutputPolicy,
2655    decisions: Vec<ResolutionDecision>,
2656}
2657
2658impl ResolvedModelPlanParts {
2659    pub fn source_artifacts(&self) -> &[ResolutionSourceArtifact] {
2660        &self.source_artifacts
2661    }
2662
2663    pub fn decisions(&self) -> &[ResolutionDecision] {
2664        &self.decisions
2665    }
2666}
2667
2668/// Trusted inputs that are intentionally outside a serialized resolved plan.
2669/// A wire payload cannot choose its model registry, source evidence, physical
2670/// node bindings, provider preference, or resource estimate and then validate
2671/// itself against those same values.
2672pub struct ResolvedPlanValidationContext<'a> {
2673    registry: &'a dyn ModelFamilyRegistry,
2674    source_evidence: &'a [ResolutionSourceEvidence<'a>],
2675    node_resolutions: &'a [PlanNodeResolution],
2676    device: &'a DeviceDescriptor,
2677    capabilities: &'a CapabilityCatalog,
2678    runtime: &'a ResolvedRuntimePolicy,
2679    numerical_execution: NumericalProfileResolution,
2680    completion_retention: CompletionRetentionSpec,
2681}
2682
2683impl<'a> ResolvedPlanValidationContext<'a> {
2684    pub fn new(
2685        registry: &'a dyn ModelFamilyRegistry,
2686        source_evidence: &'a [ResolutionSourceEvidence<'a>],
2687        node_resolutions: &'a [PlanNodeResolution],
2688        device: &'a DeviceDescriptor,
2689        capabilities: &'a CapabilityCatalog,
2690        runtime: &'a ResolvedRuntimePolicy,
2691        numerical_execution: &NumericalProfileResolution,
2692    ) -> Self {
2693        Self {
2694            registry,
2695            source_evidence,
2696            node_resolutions,
2697            device,
2698            capabilities,
2699            runtime,
2700            numerical_execution: numerical_execution.clone(),
2701            completion_retention: CompletionRetentionSpec::default(),
2702        }
2703    }
2704
2705    /// Installs the trusted diagnostic retention selected before compilation.
2706    /// The serialized execution plan cannot grant itself additional retained
2707    /// activations during semantic revalidation.
2708    pub fn with_completion_retention(
2709        mut self,
2710        completion_retention: CompletionRetentionSpec,
2711    ) -> Self {
2712        self.completion_retention = completion_retention;
2713        self
2714    }
2715
2716    pub fn registry(&self) -> &dyn ModelFamilyRegistry {
2717        self.registry
2718    }
2719
2720    fn verify_source_artifacts(&self) -> Result<Vec<ResolutionSourceArtifact>, VNextError> {
2721        self.source_evidence
2722            .iter()
2723            .map(ResolutionSourceEvidence::verify)
2724            .collect()
2725    }
2726
2727    pub fn node_resolutions(&self) -> &[PlanNodeResolution] {
2728        self.node_resolutions
2729    }
2730
2731    pub fn device(&self) -> &DeviceDescriptor {
2732        self.device
2733    }
2734
2735    pub fn capabilities(&self) -> &CapabilityCatalog {
2736        self.capabilities
2737    }
2738
2739    pub fn runtime(&self) -> &ResolvedRuntimePolicy {
2740        self.runtime
2741    }
2742
2743    pub fn numerical_execution(&self) -> &NumericalProfileResolution {
2744        &self.numerical_execution
2745    }
2746
2747    pub fn completion_retention(&self) -> &CompletionRetentionSpec {
2748        &self.completion_retention
2749    }
2750}
2751
2752/// The single validated, data-only result consumed by a product entrypoint.
2753#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2754pub struct ResolvedModelPlan {
2755    wire_version: u32,
2756    parts: ResolvedModelPlanParts,
2757    fingerprint: ResolutionFingerprint,
2758}
2759
2760impl ExecutablePlanView for ResolvedModelPlan {
2761    fn execution_plan(&self) -> &ExecutionPlan {
2762        &self.parts.execution_plan
2763    }
2764
2765    fn device(&self) -> &DeviceDescriptor {
2766        &self.parts.device
2767    }
2768
2769    fn capabilities(&self) -> &CapabilityCatalog {
2770        &self.parts.capabilities
2771    }
2772}
2773
2774#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2775#[serde(deny_unknown_fields)]
2776pub struct UnvalidatedResolvedModelPlanParts {
2777    source_artifacts: Vec<UnvalidatedResolutionSourceArtifact>,
2778    original_sources: OriginalModelSources,
2779    resolved_sources: ResolvedModelSources,
2780    config: ModelConfigFingerprint,
2781    external_metadata_id: super::ExternalModelMetadataId,
2782    prepared_family: PreparedModelFamilyWire,
2783    numerical_execution: NumericalProfileResolutionWire,
2784    tokenizer: TokenizerDescriptor,
2785    device: DeviceDescriptor,
2786    capabilities: CapabilityCatalog,
2787    runtime: ResolvedRuntimePolicy,
2788    engine: EngineSelection,
2789    execution_plan: UnvalidatedExecutionPlanWire,
2790    sampling: SamplingPolicy,
2791    stop: StopPolicy,
2792    structured_output: StructuredOutputPolicy,
2793    decisions: Vec<ResolutionDecision>,
2794}
2795
2796#[derive(Debug, Clone, PartialEq, Eq)]
2797pub struct UnvalidatedResolvedModelPlan {
2798    parts: UnvalidatedResolvedModelPlanParts,
2799    fingerprint: ResolutionFingerprint,
2800}
2801
2802#[derive(Serialize)]
2803struct ResolvedModelPlanWire {
2804    wire_version: u32,
2805    parts: UnvalidatedResolvedModelPlanParts,
2806    fingerprint: ResolutionFingerprint,
2807}
2808
2809#[derive(Deserialize, Serialize)]
2810#[serde(deny_unknown_fields)]
2811struct ResolvedModelPlanWireFields {
2812    wire_version: u32,
2813    parts: UnvalidatedResolvedModelPlanParts,
2814    fingerprint: ResolutionFingerprint,
2815}
2816
2817impl<'de> Deserialize<'de> for ResolvedModelPlanWire {
2818    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2819    where
2820        D: Deserializer<'de>,
2821    {
2822        let raw = serde_json::Value::deserialize(deserializer)?;
2823        if raw.get("wire_version").and_then(serde_json::Value::as_u64) != Some(2) {
2824            return Err(serde::de::Error::custom(
2825                "resolved model plan requires wire version 2 with explicit numerical resolution; resolve the model again",
2826            ));
2827        }
2828        let fields =
2829            ResolvedModelPlanWireFields::deserialize(&raw).map_err(serde::de::Error::custom)?;
2830        let canonical = serde_json::to_value(&fields).map_err(serde::de::Error::custom)?;
2831        if canonical != raw {
2832            return Err(serde::de::Error::custom(
2833                "resolved model plan wire contains unknown or non-canonical nested fields",
2834            ));
2835        }
2836        Ok(Self {
2837            wire_version: fields.wire_version,
2838            parts: fields.parts,
2839            fingerprint: fields.fingerprint,
2840        })
2841    }
2842}
2843
2844impl From<ResolvedModelPlanWire> for UnvalidatedResolvedModelPlan {
2845    fn from(wire: ResolvedModelPlanWire) -> Self {
2846        Self {
2847            parts: wire.parts,
2848            fingerprint: wire.fingerprint,
2849        }
2850    }
2851}
2852
2853impl UnvalidatedResolvedModelPlan {
2854    pub fn revalidate(
2855        self,
2856        context: &ResolvedPlanValidationContext<'_>,
2857    ) -> Result<ResolvedModelPlan, VNextError> {
2858        let UnvalidatedResolvedModelPlanParts {
2859            source_artifacts,
2860            original_sources,
2861            resolved_sources,
2862            config,
2863            external_metadata_id,
2864            prepared_family,
2865            numerical_execution,
2866            tokenizer,
2867            device,
2868            capabilities,
2869            runtime,
2870            engine,
2871            execution_plan,
2872            sampling,
2873            stop,
2874            structured_output,
2875            decisions,
2876        } = self.parts;
2877        let verified_source_artifacts = context.verify_source_artifacts()?;
2878        if !context
2879            .numerical_execution()
2880            .matches_wire(&numerical_execution)
2881        {
2882            return Err(invalid_plan(
2883                "validation_context.numerical_execution",
2884                "serialized numerical request or selection differs from external trusted resolution",
2885            ));
2886        }
2887        let numerical_execution = context.numerical_execution().clone();
2888        let source_artifacts =
2889            Self::revalidate_source_artifacts(source_artifacts, &verified_source_artifacts)?;
2890        let prepared_family =
2891            UnvalidatedPreparedModelFamily::from(prepared_family).revalidate(context.registry())?;
2892        if device != *context.device()
2893            || capabilities != *context.capabilities()
2894            || runtime != *context.runtime()
2895        {
2896            return Err(invalid_plan(
2897                "validation_context",
2898                "serialized device, capability catalog, or runtime policy differs from external trusted inputs",
2899            ));
2900        }
2901        let execution_plan = UnvalidatedExecutionPlan::from(execution_plan)
2902            .revalidate_with_completion_retention(
2903                &prepared_family,
2904                context.capabilities(),
2905                context.runtime(),
2906                context.node_resolutions().to_vec(),
2907                context.completion_retention().clone(),
2908            )?;
2909        let serialized_decisions = decisions;
2910        let decision_bindings = serialized_decisions
2911            .iter()
2912            .map(|decision| {
2913                ResolutionDecisionBinding::new(
2914                    decision.field,
2915                    decision.source,
2916                    decision.reason_id.clone(),
2917                    decision.evidence.source_artifact_id.clone(),
2918                    decision.evidence.source_field_path.clone(),
2919                )
2920            })
2921            .collect::<Result<Vec<_>, VNextError>>()?;
2922        let rebuilt = ResolvedModelPlan::from_verified_inputs(
2923            ResolvedModelPlanInputs {
2924                original_sources,
2925                resolved_sources,
2926                config,
2927                external_metadata_id,
2928                prepared_family,
2929                numerical_execution,
2930                tokenizer,
2931                device: context.device().clone(),
2932                capabilities: context.capabilities().clone(),
2933                runtime: context.runtime().clone(),
2934                engine,
2935                execution_plan,
2936                sampling,
2937                stop,
2938                structured_output,
2939            },
2940            decision_bindings,
2941            source_artifacts,
2942            context,
2943        )?;
2944        if rebuilt.parts.decisions != serialized_decisions {
2945            return Err(invalid_plan(
2946                "decisions",
2947                "serialized decisions differ from decisions rebuilt from external raw evidence",
2948            ));
2949        }
2950        if rebuilt.fingerprint != self.fingerprint {
2951            return Err(invalid_plan(
2952                "fingerprint",
2953                format!(
2954                    "does not match typed reconstruction: expected `{}`, actual `{}`",
2955                    rebuilt.fingerprint, self.fingerprint
2956                ),
2957            ));
2958        }
2959        Ok(rebuilt)
2960    }
2961
2962    fn revalidate_source_artifacts(
2963        serialized: Vec<UnvalidatedResolutionSourceArtifact>,
2964        expected: &[ResolutionSourceArtifact],
2965    ) -> Result<Vec<ResolutionSourceArtifact>, VNextError> {
2966        let mut expected_by_id = BTreeMap::new();
2967        for artifact in expected {
2968            if expected_by_id
2969                .insert(artifact.id().clone(), artifact)
2970                .is_some()
2971            {
2972                return Err(invalid_plan(
2973                    "validation_context.source_artifacts",
2974                    format!("duplicate externally verified artifact `{}`", artifact.id()),
2975                ));
2976            }
2977        }
2978        if serialized.len() != expected_by_id.len() {
2979            return Err(invalid_plan(
2980                "source_artifacts",
2981                "serialized and externally verified source artifact sets differ",
2982            ));
2983        }
2984
2985        let mut seen = BTreeSet::new();
2986        let mut validated = Vec::with_capacity(serialized.len());
2987        for artifact in serialized {
2988            if !seen.insert(artifact.id.clone()) {
2989                return Err(invalid_plan(
2990                    "source_artifacts",
2991                    format!("duplicate serialized artifact `{}`", artifact.id),
2992                ));
2993            }
2994            let expected = expected_by_id.get(&artifact.id).ok_or_else(|| {
2995                invalid_plan(
2996                    "source_artifacts",
2997                    format!(
2998                        "serialized artifact `{}` has no externally verified evidence",
2999                        artifact.id
3000                    ),
3001                )
3002            })?;
3003            validated.push(artifact.revalidate(expected)?);
3004        }
3005        Ok(validated)
3006    }
3007}
3008
3009impl ResolvedModelPlan {
3010    pub fn new(
3011        inputs: ResolvedModelPlanInputs,
3012        decision_bindings: Vec<ResolutionDecisionBinding>,
3013        context: &ResolvedPlanValidationContext<'_>,
3014    ) -> Result<Self, VNextError> {
3015        // Raw source bytes are parsed before any trusted plan parts or
3016        // decisions exist. This is the only public construction path.
3017        let source_artifacts = context.verify_source_artifacts()?;
3018        Self::from_verified_inputs(inputs, decision_bindings, source_artifacts, context)
3019    }
3020
3021    fn from_verified_inputs(
3022        inputs: ResolvedModelPlanInputs,
3023        decision_bindings: Vec<ResolutionDecisionBinding>,
3024        source_artifacts: Vec<ResolutionSourceArtifact>,
3025        context: &ResolvedPlanValidationContext<'_>,
3026    ) -> Result<Self, VNextError> {
3027        Self::validate_external_inputs(&inputs, context)?;
3028        let ResolvedModelPlanInputs {
3029            original_sources,
3030            resolved_sources,
3031            config,
3032            external_metadata_id,
3033            prepared_family,
3034            numerical_execution,
3035            tokenizer,
3036            device,
3037            capabilities,
3038            runtime,
3039            engine,
3040            execution_plan,
3041            sampling,
3042            stop,
3043            structured_output,
3044        } = inputs;
3045        let mut parts = ResolvedModelPlanParts {
3046            source_artifacts,
3047            original_sources,
3048            resolved_sources,
3049            config,
3050            external_metadata_id,
3051            prepared_family,
3052            numerical_execution,
3053            tokenizer,
3054            device,
3055            capabilities,
3056            runtime,
3057            engine,
3058            execution_plan,
3059            sampling,
3060            stop,
3061            structured_output,
3062            decisions: Vec::new(),
3063        };
3064        Self::normalize(&mut parts);
3065        parts.decisions = Self::bind_decisions(&parts, decision_bindings)?;
3066        Self::normalize(&mut parts);
3067        Self::validate(
3068            &parts,
3069            context.node_resolutions(),
3070            context.completion_retention(),
3071        )?;
3072        let fingerprint = ResolutionFingerprint::new(canonical_fingerprint(
3073            &parts,
3074            "serialize resolved model plan",
3075        )?)?;
3076        Ok(Self {
3077            wire_version: 2,
3078            parts,
3079            fingerprint,
3080        })
3081    }
3082
3083    fn validate_external_inputs(
3084        inputs: &ResolvedModelPlanInputs,
3085        context: &ResolvedPlanValidationContext<'_>,
3086    ) -> Result<(), VNextError> {
3087        let family_registration = context
3088            .registry()
3089            .resolve(inputs.prepared_family.family_id())?;
3090        let metadata_registration = context
3091            .registry()
3092            .resolve_external(&inputs.external_metadata_id)?;
3093        if !family_registration
3094            .external_metadata_ids()
3095            .contains(&inputs.external_metadata_id)
3096            || !std::ptr::eq(family_registration, metadata_registration)
3097            || inputs.prepared_family.external_metadata_id() != &inputs.external_metadata_id
3098        {
3099            return Err(invalid_plan(
3100                "external_metadata_id",
3101                format!(
3102                    "external metadata `{}`, prepared identity `{}`, and internal family `{}` must identify the same registration and exact typed configuration",
3103                    inputs.external_metadata_id,
3104                    inputs.prepared_family.external_metadata_id(),
3105                    inputs.prepared_family.family_id(),
3106                ),
3107            ));
3108        }
3109        let definition = family_registration.define(inputs.prepared_family.canonical_config())?;
3110        let externally_prepared = family_registration
3111            .prepare(&definition, &inputs.prepared_family.numerical_profile().id)?;
3112        if externally_prepared != inputs.prepared_family {
3113            return Err(invalid_plan(
3114                "validation_context.model_registry",
3115                "prepared model family differs from external typed registry reconstruction",
3116            ));
3117        }
3118        if inputs.device != *context.device()
3119            || inputs.capabilities != *context.capabilities()
3120            || inputs.runtime != *context.runtime()
3121        {
3122            return Err(invalid_plan(
3123                "validation_context",
3124                "device, capability catalog, or runtime policy differs from external trusted inputs",
3125            ));
3126        }
3127        if &inputs.numerical_execution != context.numerical_execution() {
3128            return Err(invalid_plan(
3129                "validation_context.numerical_execution",
3130                "numerical request or selection differs from external trusted resolution",
3131            ));
3132        }
3133        inputs.numerical_execution.validate_for(
3134            &definition,
3135            &inputs.prepared_family,
3136            context.capabilities(),
3137            context.runtime(),
3138            &inputs.execution_plan,
3139        )?;
3140        Ok(())
3141    }
3142
3143    fn bind_decisions(
3144        parts: &ResolvedModelPlanParts,
3145        bindings: Vec<ResolutionDecisionBinding>,
3146    ) -> Result<Vec<ResolutionDecision>, VNextError> {
3147        let expected_fingerprints = Self::decision_fingerprints(parts)?;
3148        let mut artifacts = BTreeMap::new();
3149        for artifact in &parts.source_artifacts {
3150            if artifacts.insert(artifact.id.clone(), artifact).is_some() {
3151                return Err(invalid_plan(
3152                    "source_artifacts",
3153                    format!("duplicate source artifact `{}`", artifact.id),
3154                ));
3155            }
3156        }
3157        let mut fields = BTreeSet::new();
3158        let mut used_artifacts = BTreeSet::new();
3159        let mut used_artifact_fields = BTreeSet::new();
3160        let mut decisions = Vec::with_capacity(bindings.len());
3161        for binding in bindings {
3162            if !fields.insert(binding.field) {
3163                return Err(invalid_plan(
3164                    "decision_bindings",
3165                    format!("field `{:?}` has more than one binding", binding.field),
3166                ));
3167            }
3168            if !binding.field.accepts_source(binding.source) {
3169                return Err(invalid_plan(
3170                    "decision_bindings.source",
3171                    format!(
3172                        "source `{:?}` cannot author field `{:?}`",
3173                        binding.source, binding.field
3174                    ),
3175                ));
3176            }
3177            let expected = expected_fingerprints.get(&binding.field).ok_or_else(|| {
3178                invalid_plan(
3179                    "decision_bindings.field",
3180                    format!("field `{:?}` is not resolvable", binding.field),
3181                )
3182            })?;
3183            let artifact = artifacts.get(&binding.source_artifact_id).ok_or_else(|| {
3184                invalid_plan(
3185                    "decision_bindings.source_artifact_id",
3186                    format!(
3187                        "binding for `{:?}` references unknown source artifact `{}`",
3188                        binding.field, binding.source_artifact_id
3189                    ),
3190                )
3191            })?;
3192            if artifact.source != binding.source {
3193                return Err(invalid_plan(
3194                    "decision_bindings.source",
3195                    format!(
3196                        "binding for `{:?}` source differs from artifact `{}`",
3197                        binding.field, artifact.id
3198                    ),
3199                ));
3200            }
3201            let source_fingerprint =
3202                artifact
3203                    .fields
3204                    .get(&binding.source_field_path)
3205                    .ok_or_else(|| {
3206                        invalid_plan(
3207                            "decision_bindings.source_field_path",
3208                            format!(
3209                                "binding for `{:?}` references a field absent from artifact `{}`",
3210                                binding.field, artifact.id
3211                            ),
3212                        )
3213                    })?;
3214            if source_fingerprint.as_str() != expected {
3215                return Err(invalid_plan(
3216                    "decision_bindings.source_field_path",
3217                    format!(
3218                        "externally parsed field for `{:?}` differs from the resolved value",
3219                        binding.field
3220                    ),
3221                ));
3222            }
3223            used_artifacts.insert(artifact.id.clone());
3224            if !used_artifact_fields
3225                .insert((artifact.id.clone(), binding.source_field_path.clone()))
3226            {
3227                return Err(invalid_plan(
3228                    "decision_bindings.source_field_path",
3229                    "one parsed artifact field cannot issue more than one decision",
3230                ));
3231            }
3232            decisions.push(ResolutionDecision::new(
3233                binding.field,
3234                binding.source,
3235                binding.reason_id,
3236                ResolutionDecisionEvidence::new(
3237                    binding.source_artifact_id,
3238                    binding.source_field_path,
3239                    source_fingerprint.clone(),
3240                )?,
3241            ));
3242        }
3243        if fields
3244            != expected_fingerprints
3245                .keys()
3246                .copied()
3247                .collect::<BTreeSet<_>>()
3248        {
3249            return Err(invalid_plan(
3250                "decision_bindings",
3251                "every resolved field requires exactly one external evidence binding",
3252            ));
3253        }
3254        if used_artifacts != artifacts.keys().cloned().collect::<BTreeSet<_>>() {
3255            return Err(invalid_plan(
3256                "source_artifacts",
3257                "externally verified source evidence and referenced binding sets differ",
3258            ));
3259        }
3260        let available_artifact_fields = artifacts
3261            .values()
3262            .flat_map(|artifact| {
3263                artifact
3264                    .fields
3265                    .keys()
3266                    .cloned()
3267                    .map(|path| (artifact.id.clone(), path))
3268            })
3269            .collect::<BTreeSet<_>>();
3270        if used_artifact_fields != available_artifact_fields {
3271            return Err(invalid_plan(
3272                "source_artifacts.fields",
3273                "externally parsed source fields and decision binding fields differ",
3274            ));
3275        }
3276        Ok(decisions)
3277    }
3278
3279    fn normalize(parts: &mut ResolvedModelPlanParts) {
3280        parts
3281            .source_artifacts
3282            .sort_by(|left, right| left.id.cmp(&right.id));
3283        for role in ModelArtifactSourceRole::ALL {
3284            parts
3285                .resolved_sources
3286                .for_role_mut(role)
3287                .files
3288                .sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
3289        }
3290        parts.stop.strings.sort();
3291        parts.decisions.sort_by_key(|decision| decision.field);
3292    }
3293
3294    fn validate(
3295        parts: &ResolvedModelPlanParts,
3296        node_resolutions: &[PlanNodeResolution],
3297        completion_retention: &CompletionRetentionSpec,
3298    ) -> Result<(), VNextError> {
3299        let mut source_artifacts = BTreeMap::new();
3300        for artifact in &parts.source_artifacts {
3301            if source_artifacts
3302                .insert(artifact.id.clone(), artifact)
3303                .is_some()
3304            {
3305                return Err(invalid_plan(
3306                    "source_artifacts",
3307                    format!("duplicate source artifact `{}`", artifact.id),
3308                ));
3309            }
3310            artifact.provenance.validate()?;
3311            match &artifact.provenance {
3312                ResolutionSourceProvenance::LockedModelFile {
3313                    source_role,
3314                    relative_path,
3315                } => {
3316                    let Some(file) = parts
3317                        .resolved_sources
3318                        .for_role(*source_role)
3319                        .files
3320                        .iter()
3321                        .find(|file| &file.relative_path == relative_path)
3322                    else {
3323                        return Err(invalid_plan(
3324                            "source_artifacts.provenance",
3325                            format!(
3326                                "artifact `{}` does not resolve to {source_role:?} locked file `{relative_path}`",
3327                                artifact.id,
3328                            ),
3329                        ));
3330                    };
3331                    if file.sha256 != artifact.content_fingerprint.as_str()
3332                        || file.size_bytes != artifact.content_size_bytes
3333                    {
3334                        return Err(invalid_plan(
3335                            "source_artifacts.provenance",
3336                            format!(
3337                                "artifact `{}` bytes differ from locked file `{relative_path}` SHA or size",
3338                                artifact.id
3339                            ),
3340                        ));
3341                    }
3342                }
3343                ResolutionSourceProvenance::Upstream { .. }
3344                    if artifact.source == ResolutionDecisionSource::ModelMetadata =>
3345                {
3346                    return Err(invalid_plan(
3347                        "source_artifacts.provenance",
3348                        format!(
3349                            "model-metadata artifact `{}` must bind a locked model file",
3350                            artifact.id
3351                        ),
3352                    ));
3353                }
3354                ResolutionSourceProvenance::Upstream { .. } => {}
3355            }
3356        }
3357        for role in ModelArtifactSourceRole::ALL {
3358            let original = parts.original_sources.for_role(role);
3359            let resolved = parts.resolved_sources.for_role(role);
3360            if original.location.trim().is_empty()
3361                || matches!(
3362                    original.requested_revision.as_deref(),
3363                    Some(revision) if revision.trim().is_empty()
3364                )
3365                || resolved.canonical_location.trim().is_empty()
3366                || resolved.resolved_revision.trim().is_empty()
3367            {
3368                return Err(invalid_plan(
3369                    format!("{}_source", role.as_str()),
3370                    "source locations and revisions must be non-empty",
3371                ));
3372            }
3373            if resolved.files.is_empty() {
3374                return Err(invalid_plan(
3375                    format!("resolved_sources.{}.files", role.as_str()),
3376                    "at least one fingerprinted file is required",
3377                ));
3378            }
3379            let mut paths = BTreeSet::new();
3380            if resolved.files.iter().any(|file| {
3381                !validate_source_path(&file.relative_path)
3382                    || file.size_bytes == 0
3383                    || !is_canonical_sha256(&file.sha256)
3384                    || !paths.insert(file.relative_path.clone())
3385            }) {
3386                return Err(invalid_plan(
3387                    format!("resolved_sources.{}.files", role.as_str()),
3388                    "file paths, sizes, and canonical hashes must be valid and unique",
3389                ));
3390            }
3391        }
3392        let family = &parts.prepared_family;
3393        let program_fingerprint = family.program().fingerprint()?;
3394        let family_fingerprint = family.fingerprint()?;
3395        if parts.tokenizer.vocabulary_size == 0
3396            || family.metadata().template.template.trim().is_empty()
3397            || family.metadata().special_tokens.eos_token_ids.is_empty()
3398        {
3399            return Err(invalid_plan(
3400                "model_metadata",
3401                "program, tokenizer, template, and special-token metadata are incomplete",
3402            ));
3403        }
3404        Self::validate_source_file_binding(
3405            ModelArtifactSourceRole::Semantic,
3406            &parts.resolved_sources.semantic,
3407            &parts.config.source_file,
3408            &parts.config.sha256,
3409            "config",
3410        )?;
3411        if parts.config.typed_config_sha256 != family.config_fingerprint() {
3412            return Err(invalid_plan(
3413                "config.typed_config_sha256",
3414                "does not match the typed model family configuration",
3415            ));
3416        }
3417        Self::validate_source_file_binding(
3418            ModelArtifactSourceRole::Tokenizer,
3419            &parts.resolved_sources.tokenizer,
3420            &parts.tokenizer.source_file,
3421            &parts.tokenizer.sha256,
3422            "tokenizer",
3423        )?;
3424        Self::validate_source_file_binding(
3425            ModelArtifactSourceRole::Tokenizer,
3426            &parts.resolved_sources.tokenizer,
3427            &family.metadata().template.source_file,
3428            &family.metadata().template.sha256,
3429            "template",
3430        )?;
3431        Self::validate_token_contract(parts)?;
3432        parts.runtime.validate()?;
3433        parts
3434            .execution_plan
3435            .validate_against_with_completion_retention(
3436                family,
3437                &parts.capabilities,
3438                &parts.runtime,
3439                node_resolutions,
3440                completion_retention.clone(),
3441            )?;
3442        let runtime_fingerprint = super::canonical_runtime_policy_fingerprint(&parts.runtime)?;
3443        let capability_fingerprint = parts.capabilities.fingerprint()?;
3444        if &parts.device != parts.capabilities.device()
3445            || &parts.device.id != parts.execution_plan.payload().device_id()
3446            || family.family_id() != parts.execution_plan.payload().family_id()
3447            || family_fingerprint != parts.execution_plan.payload().prepared_family_fingerprint()
3448            || runtime_fingerprint != parts.execution_plan.payload().policy_fingerprint()
3449            || program_fingerprint != parts.execution_plan.payload().program_fingerprint()
3450            || &parts
3451                .execution_plan
3452                .payload()
3453                .execution_weights()
3454                .schema()
3455                .format_id
3456                != parts.execution_plan.payload().weight_format()
3457            || parts
3458                .execution_plan
3459                .payload()
3460                .execution_weights()
3461                .schema()
3462                .quantization_formats()
3463                != *parts.execution_plan.payload().quantization_formats()
3464            || capability_fingerprint
3465                != parts
3466                    .execution_plan
3467                    .payload()
3468                    .capability_catalog_fingerprint()
3469        {
3470            return Err(invalid_plan(
3471                "resolved_contract_links",
3472                "family, full device descriptor, policy, capability, engine, and plan links disagree",
3473            ));
3474        }
3475        let engine = parts
3476            .capabilities
3477            .engine_provider(&parts.engine.provider_id, parts.engine.contract_version)?;
3478        if !is_canonical_sha256(&parts.engine.implementation_fingerprint)
3479            || engine.contract_version() != parts.engine.contract_version
3480            || engine.implementation_fingerprint() != parts.engine.implementation_fingerprint
3481            || engine.device_id() != &parts.device.id
3482        {
3483            return Err(invalid_plan(
3484                "engine",
3485                "engine provider version and implementation are not exactly bound to the resolved device",
3486            ));
3487        }
3488        for node in parts.execution_plan.payload().nodes() {
3489            let providers = parts
3490                .capabilities
3491                .providers_for_node(node.id(), node.operation_id())?;
3492            let selected = providers
3493                .iter()
3494                .find(|provider| provider.provider_id() == node.selection().selected_provider())
3495                .ok_or_else(|| VNextError::UnsupportedOperation {
3496                    node_id: Some(node.id().to_string()),
3497                    operation_id: node.operation_id().to_string(),
3498                    device_id: parts.device.id.to_string(),
3499                    reason: format!(
3500                        "selected provider `{}` is absent from the resolved catalog",
3501                        node.selection().selected_provider()
3502                    ),
3503                })?;
3504            if !selected.version().satisfies(node.operation_version()) {
3505                return Err(VNextError::IncompatibleOperationVersion {
3506                    node_id: Some(node.id().to_string()),
3507                    operation_id: node.operation_id().to_string(),
3508                    required_major: node.operation_version().major,
3509                    required_minor: node.operation_version().minor,
3510                    available_major: selected.version().major,
3511                    available_minor: selected.version().minor,
3512                });
3513            }
3514            // Per-node weight/quantization requirements are derived from that
3515            // node's bound semantic values by ExecutionPlan::validate_against.
3516            // Applying the family-wide format to every operation would reject
3517            // valid mixed-format programs and recreate a model-level shortcut.
3518        }
3519        parts.sampling.validate()?;
3520        if parts.stop.maximum_output_tokens == 0
3521            || parts.stop.strings.iter().any(|stop| stop.is_empty())
3522            || parts.stop.strings.windows(2).any(|pair| pair[0] == pair[1])
3523            || matches!(
3524                &parts.structured_output,
3525                StructuredOutputPolicy::JsonSchema { schema_sha256 }
3526                    if !is_canonical_sha256(schema_sha256)
3527            )
3528        {
3529            return Err(invalid_plan(
3530                "generation_policy",
3531                "stop values must be non-empty and unique and structured-output hashes must be canonical",
3532            ));
3533        }
3534        let expected_fingerprints = Self::decision_fingerprints(parts)?;
3535        let mut actual_decisions = BTreeSet::new();
3536        let mut used_source_artifacts = BTreeSet::new();
3537        let mut used_source_fields = BTreeSet::new();
3538        for decision in &parts.decisions {
3539            if !actual_decisions.insert(decision.field) {
3540                return Err(invalid_plan(
3541                    "decisions",
3542                    format!("field `{:?}` has more than one decision", decision.field),
3543                ));
3544            }
3545            let expected = expected_fingerprints.get(&decision.field).ok_or_else(|| {
3546                invalid_plan(
3547                    "decisions",
3548                    format!("field `{:?}` is not resolvable", decision.field),
3549                )
3550            })?;
3551            if !decision.field.accepts_source(decision.source) {
3552                return Err(invalid_plan(
3553                    "decisions.source",
3554                    format!(
3555                        "source `{:?}` is not allowed to author field `{:?}`",
3556                        decision.source, decision.field
3557                    ),
3558                ));
3559            }
3560            if decision.evidence.chosen_value_fingerprint.as_str() != expected {
3561                return Err(invalid_plan(
3562                    format!("decisions.{:?}.chosen_value_fingerprint", decision.field),
3563                    format!(
3564                        "does not match the resolved value: expected `{expected}`, actual `{}`",
3565                        decision.evidence.chosen_value_fingerprint
3566                    ),
3567                ));
3568            }
3569            let artifact = source_artifacts
3570                .get(decision.evidence.source_artifact_id())
3571                .ok_or_else(|| {
3572                    invalid_plan(
3573                        "decisions.evidence.source_artifact_id",
3574                        format!(
3575                            "decision for `{:?}` references unknown source artifact `{}`",
3576                            decision.field,
3577                            decision.evidence.source_artifact_id()
3578                        ),
3579                    )
3580                })?;
3581            used_source_artifacts.insert(artifact.id.clone());
3582            if !used_source_fields.insert((
3583                artifact.id.clone(),
3584                decision.evidence.source_field_path.clone(),
3585            )) {
3586                return Err(invalid_plan(
3587                    "decisions.evidence.source_field_path",
3588                    "one parsed artifact field cannot issue more than one decision",
3589                ));
3590            }
3591            if artifact.source != decision.source {
3592                return Err(invalid_plan(
3593                    "decisions.source",
3594                    format!(
3595                        "decision for `{:?}` source differs from artifact `{}`",
3596                        decision.field, artifact.id
3597                    ),
3598                ));
3599            }
3600            if let ResolutionSourceProvenance::LockedModelFile { source_role, .. } =
3601                &artifact.provenance
3602            {
3603                if !Self::locked_source_role_allowed(decision.field, *source_role) {
3604                    return Err(invalid_plan(
3605                        "decisions.source_role",
3606                        format!(
3607                            "decision for `{:?}` cannot use a {source_role:?} locked source",
3608                            decision.field
3609                        ),
3610                    ));
3611                }
3612            }
3613            let source_field = artifact
3614                .fields
3615                .get(decision.evidence.source_field_path())
3616                .ok_or_else(|| {
3617                    invalid_plan(
3618                        "decisions.evidence.source_field_path",
3619                        format!(
3620                            "decision for `{:?}` references a field absent from artifact `{}`",
3621                            decision.field, artifact.id
3622                        ),
3623                    )
3624                })?;
3625            if source_field != &decision.evidence.chosen_value_fingerprint {
3626                return Err(invalid_plan(
3627                    "decisions.evidence.chosen_value_fingerprint",
3628                    format!(
3629                        "decision for `{:?}` differs from source artifact field `{}`",
3630                        decision.field,
3631                        decision.evidence.source_field_path()
3632                    ),
3633                ));
3634            }
3635        }
3636        let required_decisions = expected_fingerprints
3637            .keys()
3638            .copied()
3639            .collect::<BTreeSet<_>>();
3640        if actual_decisions != required_decisions {
3641            return Err(invalid_plan(
3642                "decisions",
3643                "every resolved field requires exactly one typed, fingerprinted decision",
3644            ));
3645        }
3646        if used_source_artifacts != source_artifacts.keys().cloned().collect::<BTreeSet<_>>() {
3647            return Err(invalid_plan(
3648                "source_artifacts",
3649                "every source artifact must be referenced by at least one resolution decision",
3650            ));
3651        }
3652        let available_source_fields = source_artifacts
3653            .values()
3654            .flat_map(|artifact| {
3655                artifact
3656                    .fields
3657                    .keys()
3658                    .cloned()
3659                    .map(|path| (artifact.id.clone(), path))
3660            })
3661            .collect::<BTreeSet<_>>();
3662        if used_source_fields != available_source_fields {
3663            return Err(invalid_plan(
3664                "source_artifacts.fields",
3665                "parsed source fields and resolution decision fields differ",
3666            ));
3667        }
3668        Ok(())
3669    }
3670
3671    fn validate_source_file_binding(
3672        role: ModelArtifactSourceRole,
3673        source: &ResolvedModelSource,
3674        source_file: &str,
3675        sha256: &str,
3676        field: &str,
3677    ) -> Result<(), VNextError> {
3678        if !validate_source_path(source_file) || !is_canonical_sha256(sha256) {
3679            return Err(invalid_plan(
3680                format!("{field}.source_file"),
3681                "source path or hash is not canonical",
3682            ));
3683        }
3684        match source
3685            .files
3686            .iter()
3687            .find(|file| file.relative_path == source_file)
3688        {
3689            Some(file) if file.sha256 == sha256 => Ok(()),
3690            Some(file) => Err(invalid_plan(
3691                format!("{field}.sha256"),
3692                format!(
3693                    "does not match resolved source row `{source_file}`: expected `{}`, actual `{sha256}`",
3694                    file.sha256
3695                ),
3696            )),
3697            None => Err(invalid_plan(
3698                format!("{field}.source_file"),
3699                format!(
3700                    "`{source_file}` is absent from resolved_sources.{}.files",
3701                    role.as_str()
3702                ),
3703            )),
3704        }
3705    }
3706
3707    const fn locked_source_role_allowed(
3708        field: ResolutionField,
3709        role: ModelArtifactSourceRole,
3710    ) -> bool {
3711        match field {
3712            ResolutionField::Config
3713            | ResolutionField::ExternalMetadata
3714            | ResolutionField::Family => matches!(role, ModelArtifactSourceRole::Semantic),
3715            ResolutionField::Tokenizer | ResolutionField::Template => {
3716                matches!(role, ModelArtifactSourceRole::Tokenizer)
3717            }
3718            ResolutionField::SpecialTokens => matches!(
3719                role,
3720                ModelArtifactSourceRole::Semantic | ModelArtifactSourceRole::Tokenizer
3721            ),
3722            ResolutionField::WeightSchema => matches!(
3723                role,
3724                ModelArtifactSourceRole::Semantic | ModelArtifactSourceRole::Weights
3725            ),
3726            ResolutionField::WeightFormat => matches!(role, ModelArtifactSourceRole::Weights),
3727            ResolutionField::OriginalSources
3728            | ResolutionField::ResolvedSources
3729            | ResolutionField::Device
3730            | ResolutionField::Capabilities
3731            | ResolutionField::RuntimePreset
3732            | ResolutionField::RuntimeMemory
3733            | ResolutionField::Admission
3734            | ResolutionField::Engine
3735            | ResolutionField::ExecutionPlan
3736            | ResolutionField::Sampling
3737            | ResolutionField::Stop
3738            | ResolutionField::StructuredOutput
3739            | ResolutionField::NumericalExecution => true,
3740        }
3741    }
3742
3743    fn validate_token_contract(parts: &ResolvedModelPlanParts) -> Result<(), VNextError> {
3744        let vocabulary_size = parts.tokenizer.vocabulary_size;
3745        let special = &parts.prepared_family.metadata().special_tokens;
3746        if special.collision_policy.allowed().iter().any(|collision| {
3747            collision.first() == SpecialTokenRole::Stop
3748                || collision.second() == SpecialTokenRole::Stop
3749        }) {
3750            return Err(invalid_plan(
3751                "special_tokens.collision_policy",
3752                "model metadata cannot authorize product-owned stop-token collisions",
3753            ));
3754        }
3755        let mut roles = Vec::new();
3756        if let Some(token_id) = special.bos_token_id {
3757            roles.push((SpecialTokenRole::Bos, token_id));
3758        }
3759        roles.extend(
3760            special
3761                .eos_token_ids
3762                .iter()
3763                .copied()
3764                .map(|token_id| (SpecialTokenRole::Eos, token_id)),
3765        );
3766        if let Some(token_id) = special.pad_token_id {
3767            roles.push((SpecialTokenRole::Pad, token_id));
3768        }
3769        roles.extend(
3770            parts
3771                .stop
3772                .token_ids
3773                .iter()
3774                .copied()
3775                .map(|token_id| (SpecialTokenRole::Stop, token_id)),
3776        );
3777        if roles
3778            .iter()
3779            .any(|(_, token_id)| u64::from(*token_id) >= vocabulary_size)
3780        {
3781            return Err(invalid_plan(
3782                "special_tokens",
3783                "a model or stop token id exceeds the tokenizer vocabulary",
3784            ));
3785        }
3786        let mut observed_stop_collisions = BTreeSet::new();
3787        for (index, (role, token_id)) in roles.iter().enumerate() {
3788            for (other_role, other_token_id) in &roles[..index] {
3789                if token_id == other_token_id && role != other_role {
3790                    let (policy_field, allowed) = if *role == SpecialTokenRole::Stop
3791                        || *other_role == SpecialTokenRole::Stop
3792                    {
3793                        let model_role = if *role == SpecialTokenRole::Stop {
3794                            *other_role
3795                        } else {
3796                            *role
3797                        };
3798                        observed_stop_collisions.insert(model_role);
3799                        (
3800                            "stop.collision_policy",
3801                            parts.stop.collision_policy.allows(model_role),
3802                        )
3803                    } else {
3804                        (
3805                            "special_tokens.collision_policy",
3806                            special.collision_policy.allows(*role, *other_role),
3807                        )
3808                    };
3809                    if !allowed {
3810                        return Err(invalid_plan(
3811                            policy_field,
3812                            format!(
3813                                "token id {token_id} is shared by {role:?} and {other_role:?} without an explicit policy"
3814                            ),
3815                        ));
3816                    }
3817                }
3818            }
3819        }
3820        if observed_stop_collisions != *parts.stop.collision_policy.allowed_model_roles() {
3821            return Err(invalid_plan(
3822                "stop.collision_policy",
3823                "declared model-role collisions must exactly match observed stop-token aliases",
3824            ));
3825        }
3826        Ok(())
3827    }
3828
3829    fn decision_values(
3830        parts: &ResolvedModelPlanParts,
3831    ) -> Result<BTreeMap<ResolutionField, serde_json::Value>, VNextError> {
3832        #[derive(Serialize)]
3833        struct RuntimePresetValue<'a> {
3834            policy_id: &'a str,
3835            version: ContractVersion,
3836            scheduling: SchedulingDiscipline,
3837        }
3838
3839        let mut values = BTreeMap::new();
3840        macro_rules! insert_value {
3841            ($field:expr, $value:expr, $context:literal) => {
3842                values.insert($field, canonical_json_value($value, $context)?);
3843            };
3844        }
3845
3846        insert_value!(
3847            ResolutionField::OriginalSources,
3848            &parts.original_sources,
3849            "serialize original model sources decision"
3850        );
3851        insert_value!(
3852            ResolutionField::ResolvedSources,
3853            &parts.resolved_sources,
3854            "serialize resolved model sources decision"
3855        );
3856        insert_value!(
3857            ResolutionField::Config,
3858            &parts.config,
3859            "serialize model config decision"
3860        );
3861        insert_value!(
3862            ResolutionField::ExternalMetadata,
3863            &parts.external_metadata_id,
3864            "serialize external metadata decision"
3865        );
3866        insert_value!(
3867            ResolutionField::Family,
3868            parts.prepared_family.family_id(),
3869            "serialize model family decision"
3870        );
3871        insert_value!(
3872            ResolutionField::NumericalExecution,
3873            &parts.numerical_execution,
3874            "serialize numerical execution decision"
3875        );
3876        insert_value!(
3877            ResolutionField::WeightSchema,
3878            parts.prepared_family.weight_schema(),
3879            "serialize weight schema decision"
3880        );
3881        insert_value!(
3882            ResolutionField::WeightFormat,
3883            &parts.prepared_family.weight_schema().format_id,
3884            "serialize weight format decision"
3885        );
3886        insert_value!(
3887            ResolutionField::Tokenizer,
3888            &parts.tokenizer,
3889            "serialize tokenizer decision"
3890        );
3891        insert_value!(
3892            ResolutionField::Template,
3893            &parts.prepared_family.metadata().template,
3894            "serialize template decision"
3895        );
3896        insert_value!(
3897            ResolutionField::SpecialTokens,
3898            &parts.prepared_family.metadata().special_tokens,
3899            "serialize special-token decision"
3900        );
3901        insert_value!(
3902            ResolutionField::Device,
3903            &parts.device,
3904            "serialize device decision"
3905        );
3906        insert_value!(
3907            ResolutionField::Capabilities,
3908            &parts.capabilities,
3909            "serialize capability decision"
3910        );
3911        insert_value!(
3912            ResolutionField::RuntimePreset,
3913            &RuntimePresetValue {
3914                policy_id: parts.runtime.policy_id(),
3915                version: parts.runtime.version(),
3916                scheduling: parts.runtime.scheduling(),
3917            },
3918            "serialize runtime preset decision"
3919        );
3920        insert_value!(
3921            ResolutionField::RuntimeMemory,
3922            parts.runtime.memory(),
3923            "serialize runtime memory decision"
3924        );
3925        insert_value!(
3926            ResolutionField::Admission,
3927            parts.runtime.admission(),
3928            "serialize admission decision"
3929        );
3930        insert_value!(
3931            ResolutionField::Engine,
3932            &parts.engine,
3933            "serialize engine decision"
3934        );
3935        insert_value!(
3936            ResolutionField::ExecutionPlan,
3937            parts.execution_plan.plan_hash().as_str(),
3938            "serialize execution-plan decision"
3939        );
3940        insert_value!(
3941            ResolutionField::Sampling,
3942            &parts.sampling,
3943            "serialize sampling decision"
3944        );
3945        insert_value!(
3946            ResolutionField::Stop,
3947            &parts.stop,
3948            "serialize stop decision"
3949        );
3950        insert_value!(
3951            ResolutionField::StructuredOutput,
3952            &parts.structured_output,
3953            "serialize structured-output decision"
3954        );
3955        Ok(values)
3956    }
3957
3958    fn decision_fingerprints(
3959        parts: &ResolvedModelPlanParts,
3960    ) -> Result<BTreeMap<ResolutionField, String>, VNextError> {
3961        Self::decision_values(parts)?
3962            .into_iter()
3963            .map(|(field, value)| {
3964                canonical_fingerprint(&value, "fingerprint resolved decision value")
3965                    .map(|fingerprint| (field, fingerprint))
3966            })
3967            .collect()
3968    }
3969
3970    pub fn parts(&self) -> &ResolvedModelPlanParts {
3971        &self.parts
3972    }
3973
3974    pub fn execution_plan(&self) -> &ExecutionPlan {
3975        &self.parts.execution_plan
3976    }
3977
3978    pub fn fingerprint(&self) -> &str {
3979        self.fingerprint.as_str()
3980    }
3981
3982    pub fn to_json(&self) -> Result<Vec<u8>, VNextError> {
3983        serde_json::to_vec(self).map_err(|error| VNextError::Serialization {
3984            context: "serialize resolved model plan",
3985            message: error.to_string(),
3986        })
3987    }
3988
3989    pub fn decode_untrusted(bytes: &[u8]) -> Result<UnvalidatedResolvedModelPlan, VNextError> {
3990        if bytes.len() > MAX_RESOLVED_MODEL_PLAN_WIRE_BYTES {
3991            return Err(invalid_plan(
3992                "resolved_model_plan.wire_bytes",
3993                format!("must not exceed {MAX_RESOLVED_MODEL_PLAN_WIRE_BYTES} bytes"),
3994            ));
3995        }
3996        serde_json::from_slice::<ResolvedModelPlanWire>(bytes)
3997            .map(Into::into)
3998            .map_err(|error| VNextError::Serialization {
3999                context: "decode untrusted resolved model plan",
4000                message: error.to_string(),
4001            })
4002    }
4003
4004    pub fn from_json_validated(
4005        bytes: &[u8],
4006        context: &ResolvedPlanValidationContext<'_>,
4007    ) -> Result<Self, VNextError> {
4008        Self::decode_untrusted(bytes)?.revalidate(context)
4009    }
4010}