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