Skip to main content

presolve_compiler/
production_runtime_artifact.rs

1//! K8 packed, validated production runtime artifact v1.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8use crate::{
9    validate_production_chunk_graph, OptimizationPolicyId, ProductionChunkGraph, ProductionChunkId,
10    ProductionChunkKind, ResumeBuildId, ResumeManifest, RuntimeTableId,
11    RESUME_RUNTIME_PROTOCOL_VERSION,
12};
13
14pub const PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION: u32 = 1;
15pub const PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION: u32 = 1;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum ProductionOrdinalWidth {
20    U8,
21    U16,
22    U32,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct ProductionOrdinalMapping {
28    pub canonical_id: String,
29    pub ordinal: u32,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct ProductionRuntimeTable {
35    pub table_id: RuntimeTableId,
36    pub table_kind: String,
37    pub schema_version: u32,
38    pub count: u32,
39    pub ordinal_width: ProductionOrdinalWidth,
40    pub checksum: String,
41    pub mappings: Vec<ProductionOrdinalMapping>,
42    pub referenced_table_ids: Vec<RuntimeTableId>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct ProductionRuntimeTableRegistry {
48    pub tables: Vec<ProductionRuntimeTable>,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct ProductionArtifactChunkRecord {
54    pub chunk_id: ProductionChunkId,
55    pub kind: String,
56    pub module_filename: String,
57    pub dependency_chunk_ids: Vec<ProductionChunkId>,
58}
59
60#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
61#[serde(deny_unknown_fields)]
62pub struct ProductionArtifactActivationEntry {
63    pub activation_root_id: String,
64    pub root_chunk_id: ProductionChunkId,
65    pub shared_chunk_ids: Vec<ProductionChunkId>,
66}
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct ProductionArtifactEntry {
71    pub eager_chunk_id: ProductionChunkId,
72    pub activations: Vec<ProductionArtifactActivationEntry>,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct ProductionArtifactIntegrity {
78    pub artifact_checksum: String,
79    pub table_checksums: Vec<String>,
80}
81
82#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ProductionRuntimeArtifactV1 {
85    pub schema_version: u32,
86    pub build_id: ResumeBuildId,
87    pub runtime_protocol_version: u32,
88    pub optimization_policy: OptimizationPolicyId,
89    pub tables: ProductionRuntimeTableRegistry,
90    pub programs: Vec<String>,
91    pub chunks: Vec<ProductionArtifactChunkRecord>,
92    pub entry: ProductionArtifactEntry,
93    pub integrity: ProductionArtifactIntegrity,
94}
95
96#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
97pub enum ProductionArtifactIntegrityViolation {
98    ArtifactChecksumMismatch,
99    BuildIdMismatch,
100    ChunkReferenceMismatch,
101    GraphTopologyMismatch,
102    InvalidOrdinalWidth,
103    RuntimeProtocolMismatch,
104    SchemaVersionMismatch,
105    TableChecksumMismatch(RuntimeTableId),
106    TableMappingMismatch(RuntimeTableId),
107    UnknownTableReference(RuntimeTableId),
108}
109
110/// Assigns dense compiler-owned ordinals in canonical ID order.
111#[must_use]
112pub fn build_production_runtime_table(
113    table_kind: &str,
114    canonical_ids: &[String],
115    referenced_table_ids: &[RuntimeTableId],
116) -> Option<ProductionRuntimeTable> {
117    let table_id = RuntimeTableId::for_artifact_table("production_runtime", table_kind)?;
118    let mut ids = canonical_ids.to_vec();
119    ids.sort();
120    if ids.windows(2).any(|pair| pair[0] == pair[1]) {
121        return None;
122    }
123    let mut references = referenced_table_ids.to_vec();
124    references.sort();
125    if references.windows(2).any(|pair| pair[0] == pair[1]) {
126        return None;
127    }
128    let mut mappings = Vec::with_capacity(ids.len());
129    for canonical_id in ids {
130        mappings.push(ProductionOrdinalMapping {
131            canonical_id,
132            ordinal: u32::try_from(mappings.len()).ok()?,
133        });
134    }
135    let mut table = ProductionRuntimeTable {
136        table_id,
137        table_kind: table_kind.to_string(),
138        schema_version: PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION,
139        count: u32::try_from(mappings.len()).ok()?,
140        ordinal_width: ordinal_width(mappings.len()),
141        checksum: String::new(),
142        mappings,
143        referenced_table_ids: references,
144    };
145    table.checksum = table_checksum(&table);
146    Some(table)
147}
148
149/// Builds the production-only pack from frozen resume data and the validated K7 graph.
150///
151/// # Errors
152///
153/// Returns graph-integrity evidence rather than packing an invalid chunk topology.
154///
155/// # Panics
156///
157/// Panics only if compiler-derived canonical IDs cannot form one of the fixed
158/// K8 tables, which indicates an earlier compiler invariant failure.
159#[allow(clippy::too_many_lines)]
160pub fn build_production_runtime_artifact(
161    resume: &ResumeManifest,
162    chunk_graph: &ProductionChunkGraph,
163) -> Result<ProductionRuntimeArtifactV1, Vec<ProductionArtifactIntegrityViolation>> {
164    if validate_production_chunk_graph(chunk_graph).is_err() {
165        return Err(vec![
166            ProductionArtifactIntegrityViolation::GraphTopologyMismatch,
167        ]);
168    }
169    let programs = production_programs(resume);
170    let activation_roots = chunk_graph
171        .activation_plans
172        .iter()
173        .map(|plan| plan.activation_root_id.clone())
174        .collect::<Vec<_>>();
175    let anchor_ids = resume
176        .anchors
177        .iter()
178        .map(|anchor| anchor.anchor_id.to_string())
179        .collect::<Vec<_>>();
180    let event_ids = resume
181        .events
182        .iter()
183        .map(|event| event.resume_event_id.to_string())
184        .collect::<Vec<_>>();
185    let activation_ids = resume
186        .activations
187        .iter()
188        .map(|activation| activation.activation_id.to_string())
189        .collect::<Vec<_>>();
190    let chunk_ids = chunk_graph
191        .chunks
192        .iter()
193        .map(|chunk| chunk.id.to_string())
194        .collect::<Vec<_>>();
195    let program_table = build_production_runtime_table("programs", &programs, &[])
196        .expect("compiler-derived program IDs are canonical and unique");
197    let chunk_table = build_production_runtime_table("chunks", &chunk_ids, &[])
198        .expect("validated graph chunk IDs are canonical and unique");
199    let activation_table = build_production_runtime_table(
200        "activation_roots",
201        &activation_roots,
202        std::slice::from_ref(&chunk_table.table_id),
203    )
204    .expect("graph activation roots are canonical and unique");
205    let anchor_table = build_production_runtime_table("anchors", &anchor_ids, &[])
206        .expect("manifest anchor IDs are canonical and unique");
207    let event_table = build_production_runtime_table(
208        "events",
209        &event_ids,
210        std::slice::from_ref(&anchor_table.table_id),
211    )
212    .expect("manifest event IDs are canonical and unique");
213    let activation_id_table = build_production_runtime_table(
214        "activations",
215        &activation_ids,
216        std::slice::from_ref(&chunk_table.table_id),
217    )
218    .expect("manifest activation IDs are canonical and unique");
219    let tables = ProductionRuntimeTableRegistry {
220        tables: vec![
221            activation_id_table,
222            activation_table,
223            anchor_table,
224            chunk_table,
225            event_table,
226            program_table,
227        ],
228    };
229    let dependencies = chunk_graph.dependencies.iter().fold(
230        BTreeMap::<ProductionChunkId, Vec<ProductionChunkId>>::new(),
231        |mut map, edge| {
232            map.entry(edge.dependent_chunk_id.clone())
233                .or_default()
234                .push(edge.dependency_chunk_id.clone());
235            map
236        },
237    );
238    let mut chunks = chunk_graph
239        .chunks
240        .iter()
241        .map(|chunk| ProductionArtifactChunkRecord {
242            chunk_id: chunk.id.clone(),
243            kind: chunk_kind_name(chunk.kind).to_string(),
244            module_filename: chunk.provisional_module_filename.clone(),
245            dependency_chunk_ids: dependencies.get(&chunk.id).cloned().unwrap_or_default(),
246        })
247        .collect::<Vec<_>>();
248    chunks.sort_by(|left, right| left.chunk_id.cmp(&right.chunk_id));
249    for chunk in &mut chunks {
250        chunk.dependency_chunk_ids.sort();
251        chunk.dependency_chunk_ids.dedup();
252    }
253    let mut activations = chunk_graph
254        .activation_plans
255        .iter()
256        .map(|plan| ProductionArtifactActivationEntry {
257            activation_root_id: plan.activation_root_id.clone(),
258            root_chunk_id: plan.root_chunk_id.clone(),
259            shared_chunk_ids: plan.shared_chunk_ids.clone(),
260        })
261        .collect::<Vec<_>>();
262    activations.sort_by(|left, right| left.activation_root_id.cmp(&right.activation_root_id));
263    let mut artifact = ProductionRuntimeArtifactV1 {
264        schema_version: PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION,
265        build_id: resume.build_id.clone(),
266        runtime_protocol_version: resume.runtime_protocol_version,
267        optimization_policy: OptimizationPolicyId::production_v1(),
268        tables,
269        programs,
270        chunks,
271        entry: ProductionArtifactEntry {
272            eager_chunk_id: chunk_graph.eager_chunk_id.clone(),
273            activations,
274        },
275        integrity: ProductionArtifactIntegrity {
276            artifact_checksum: String::new(),
277            table_checksums: Vec::new(),
278        },
279    };
280    artifact.integrity.table_checksums = artifact
281        .tables
282        .tables
283        .iter()
284        .map(|table| table.checksum.clone())
285        .collect();
286    artifact.integrity.artifact_checksum = artifact_checksum(&artifact);
287    Ok(artifact)
288}
289
290#[must_use]
291///
292/// # Panics
293///
294/// Panics only if an in-memory compiler-produced artifact cannot serialize.
295pub fn production_runtime_artifact_json(artifact: &ProductionRuntimeArtifactV1) -> String {
296    serde_json::to_string(artifact).expect("production runtime artifact should serialize") + "\n"
297}
298
299/// Parses and validates the closed v1 production artifact before runtime use.
300///
301/// # Errors
302///
303/// Returns all deterministic integrity violations; unknown fields are rejected
304/// by the v1 deserializer before this validation begins.
305pub fn parse_production_runtime_artifact_v1(
306    json: &str,
307    expected_build_id: &ResumeBuildId,
308) -> Result<ProductionRuntimeArtifactV1, Vec<ProductionArtifactIntegrityViolation>> {
309    let artifact = serde_json::from_str::<ProductionRuntimeArtifactV1>(json)
310        .map_err(|_| vec![ProductionArtifactIntegrityViolation::SchemaVersionMismatch])?;
311    let errors = validate_production_runtime_artifact(&artifact, expected_build_id);
312    errors.is_empty().then_some(artifact).ok_or(errors)
313}
314
315#[must_use]
316pub fn validate_production_runtime_artifact(
317    artifact: &ProductionRuntimeArtifactV1,
318    expected_build_id: &ResumeBuildId,
319) -> Vec<ProductionArtifactIntegrityViolation> {
320    let mut violations = Vec::new();
321    if artifact.schema_version != PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION {
322        violations.push(ProductionArtifactIntegrityViolation::SchemaVersionMismatch);
323    }
324    if artifact.runtime_protocol_version != RESUME_RUNTIME_PROTOCOL_VERSION {
325        violations.push(ProductionArtifactIntegrityViolation::RuntimeProtocolMismatch);
326    }
327    if &artifact.build_id != expected_build_id {
328        violations.push(ProductionArtifactIntegrityViolation::BuildIdMismatch);
329    }
330    let table_ids = artifact
331        .tables
332        .tables
333        .iter()
334        .map(|table| table.table_id.clone())
335        .collect::<BTreeSet<_>>();
336    for table in &artifact.tables.tables {
337        if table.checksum != table_checksum(table) {
338            violations.push(ProductionArtifactIntegrityViolation::TableChecksumMismatch(
339                table.table_id.clone(),
340            ));
341        }
342        let mappings_are_canonical = u32::try_from(table.mappings.len()) == Ok(table.count)
343            && table.ordinal_width == ordinal_width(table.mappings.len())
344            && table
345                .mappings
346                .iter()
347                .enumerate()
348                .all(|(ordinal, mapping)| u32::try_from(ordinal) == Ok(mapping.ordinal))
349            && table
350                .mappings
351                .windows(2)
352                .all(|pair| pair[0].canonical_id < pair[1].canonical_id);
353        if !mappings_are_canonical
354            || table.schema_version != PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION
355        {
356            violations.push(ProductionArtifactIntegrityViolation::TableMappingMismatch(
357                table.table_id.clone(),
358            ));
359        }
360        if table.ordinal_width != ordinal_width(table.count as usize) {
361            violations.push(ProductionArtifactIntegrityViolation::InvalidOrdinalWidth);
362        }
363        for reference in &table.referenced_table_ids {
364            if !table_ids.contains(reference) {
365                violations.push(ProductionArtifactIntegrityViolation::UnknownTableReference(
366                    reference.clone(),
367                ));
368            }
369        }
370    }
371    let chunk_ids = artifact
372        .chunks
373        .iter()
374        .map(|chunk| chunk.chunk_id.clone())
375        .collect::<BTreeSet<_>>();
376    let chunk_references_valid = chunk_ids.contains(&artifact.entry.eager_chunk_id)
377        && artifact.chunks.iter().all(|chunk| {
378            chunk
379                .dependency_chunk_ids
380                .iter()
381                .all(|dependency| chunk_ids.contains(dependency))
382        })
383        && artifact.entry.activations.iter().all(|activation| {
384            chunk_ids.contains(&activation.root_chunk_id)
385                && activation
386                    .shared_chunk_ids
387                    .iter()
388                    .all(|shared| chunk_ids.contains(shared))
389        });
390    if !chunk_references_valid {
391        violations.push(ProductionArtifactIntegrityViolation::ChunkReferenceMismatch);
392    }
393    if artifact.integrity.artifact_checksum != artifact_checksum(artifact) {
394        violations.push(ProductionArtifactIntegrityViolation::ArtifactChecksumMismatch);
395    }
396    violations.sort();
397    violations.dedup();
398    violations
399}
400
401fn production_programs(resume: &ResumeManifest) -> Vec<String> {
402    let mut programs = BTreeSet::new();
403    for chunk in &resume.chunks {
404        programs.extend(chunk.provided_program_ids.iter().cloned());
405    }
406    programs.extend(
407        resume
408            .capture_programs
409            .iter()
410            .map(|program| program.program_id.to_string()),
411    );
412    programs.extend(
413        resume
414            .restore_programs
415            .iter()
416            .map(|program| program.program_id.to_string()),
417    );
418    programs.extend(
419        resume
420            .events
421            .iter()
422            .map(|event| event.action_or_submit_program_id.clone()),
423    );
424    programs.into_iter().collect()
425}
426
427fn ordinal_width(count: usize) -> ProductionOrdinalWidth {
428    if count <= 255 {
429        ProductionOrdinalWidth::U8
430    } else if count <= 65_535 {
431        ProductionOrdinalWidth::U16
432    } else {
433        ProductionOrdinalWidth::U32
434    }
435}
436
437fn table_checksum(table: &ProductionRuntimeTable) -> String {
438    let mut bytes = format!(
439        "table:{}\nschema:{}\ncount:{}\nwidth:{:?}\n",
440        table.table_id, table.schema_version, table.count, table.ordinal_width
441    )
442    .into_bytes();
443    for mapping in &table.mappings {
444        bytes.extend_from_slice(mapping.canonical_id.as_bytes());
445        bytes.extend_from_slice(format!("\0{}\n", mapping.ordinal).as_bytes());
446    }
447    for reference in &table.referenced_table_ids {
448        bytes.extend_from_slice(reference.to_string().as_bytes());
449        bytes.push(b'\n');
450    }
451    format!("{:x}", Sha256::digest(bytes))
452}
453
454fn artifact_checksum(artifact: &ProductionRuntimeArtifactV1) -> String {
455    let mut canonical = artifact.clone();
456    canonical.integrity.artifact_checksum.clear();
457    format!(
458        "{:x}",
459        Sha256::digest(
460            serde_json::to_vec(&canonical).expect("production artifact should serialize")
461        )
462    )
463}
464
465const fn chunk_kind_name(kind: ProductionChunkKind) -> &'static str {
466    match kind {
467        ProductionChunkKind::Eager => "eager",
468        ProductionChunkKind::Root => "root",
469        ProductionChunkKind::Shared => "shared",
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::{
477        extract_production_chunk_graph, ExecutableProgramFingerprint, ProductionRootChunkInput,
478        ResumeBoundaryId, ResumeManifest, SharedChunkCandidatePlan,
479    };
480    use std::str::FromStr;
481
482    fn manifest() -> ResumeManifest {
483        ResumeManifest {
484            schema_version: 6,
485            build_id: ResumeBuildId::zero_sentinel(),
486            snapshot_schema_version: 1,
487            runtime_protocol_version: 1,
488            application_root_boundary_id: ResumeBoundaryId::from_str("resume-boundary:root")
489                .expect("boundary"),
490            boundaries: Vec::new(),
491            slot_schemas: Vec::new(),
492            capture_programs: Vec::new(),
493            restore_programs: Vec::new(),
494            chunks: Vec::new(),
495            activations: Vec::new(),
496            anchors: Vec::new(),
497            events: Vec::new(),
498            phase_i_component_resume_records: Vec::new(),
499            phase_i_form_resume_records: Vec::new(),
500        }
501    }
502
503    fn graph() -> ProductionChunkGraph {
504        extract_production_chunk_graph(
505            &SharedChunkCandidatePlan {
506                candidates: Vec::new(),
507                rejections: Vec::new(),
508            },
509            &[ProductionRootChunkInput {
510                activation_root_id: "root-a".to_string(),
511                root_kind: "interaction".to_string(),
512                programs: vec![ExecutableProgramFingerprint::for_canonical_opcode_stream(
513                    b"a",
514                )],
515            }],
516        )
517        .expect("graph")
518        .0
519    }
520
521    #[test]
522    fn k8_assigns_canonical_dense_ordinals_and_width_boundaries() {
523        let ids = (0..256)
524            .map(|index| format!("id-{index:03}"))
525            .collect::<Vec<_>>();
526        let table = build_production_runtime_table("programs", &ids, &[]).expect("table");
527        assert_eq!(table.mappings[0].canonical_id, "id-000");
528        assert_eq!(table.mappings[255].ordinal, 255);
529        assert_eq!(table.ordinal_width, ProductionOrdinalWidth::U16);
530        assert!(build_production_runtime_table(
531            "programs",
532            &["same".to_string(), "same".to_string()],
533            &[]
534        )
535        .is_none());
536    }
537
538    #[test]
539    fn k8_round_trips_canonical_artifact_and_rejects_integrity_drift() {
540        let manifest = manifest();
541        let artifact = build_production_runtime_artifact(&manifest, &graph()).expect("artifact");
542        let json = production_runtime_artifact_json(&artifact);
543        assert!(json.ends_with('\n'));
544        assert!(!json.contains("src/"));
545        assert_eq!(
546            parse_production_runtime_artifact_v1(&json, &manifest.build_id),
547            Ok(artifact.clone())
548        );
549        let mut checksum_drift = artifact.clone();
550        checksum_drift.tables.tables[0].checksum = "bad".to_string();
551        assert!(
552            validate_production_runtime_artifact(&checksum_drift, &manifest.build_id)
553                .iter()
554                .any(|violation| matches!(
555                    violation,
556                    ProductionArtifactIntegrityViolation::TableChecksumMismatch(_)
557                ))
558        );
559    }
560
561    #[test]
562    fn k8_rejects_unknown_chunk_reference_and_build_mismatch() {
563        let manifest = manifest();
564        let mut artifact =
565            build_production_runtime_artifact(&manifest, &graph()).expect("artifact");
566        artifact.chunks[0]
567            .dependency_chunk_ids
568            .push(ProductionChunkId::from_str("production-chunk:unknown").expect("chunk ID"));
569        let errors = validate_production_runtime_artifact(
570            &artifact,
571            &ResumeBuildId::from_str("resume-build:other").expect("build ID"),
572        );
573        assert!(errors.contains(&ProductionArtifactIntegrityViolation::BuildIdMismatch));
574        assert!(errors.contains(&ProductionArtifactIntegrityViolation::ChunkReferenceMismatch));
575        assert!(errors.contains(&ProductionArtifactIntegrityViolation::ArtifactChecksumMismatch));
576    }
577}