diff --git a/tools/zc/src/ci.rs b/tools/zc/src/ci.rs index 59015f1a37..01be53cc98 100644 --- a/tools/zc/src/ci.rs +++ b/tools/zc/src/ci.rs @@ -11,9 +11,10 @@ //! Loading CI inputs is intentionally all-or-nothing. A caller cannot obtain a //! [`CiInputs`] until the policy is valid, its references agree with live Cargo //! metadata and repository files, every workflow job has an exact reviewed -//! role, and every independently recorded legacy baseline parses canonically. -//! Planners therefore consume checked data rather than remembering which -//! validation passes must precede which lookups. +//! role, every independently recorded legacy baseline parses canonically, and +//! the typed execution model exactly reproduces that legacy evidence. Planners +//! therefore consume checked data rather than remembering which validation +//! passes must precede which lookups. use std::{ collections::HashMap, @@ -25,6 +26,7 @@ use thiserror::Error; use crate::{ baseline::{BaselineError, LegacyBaselineFiles, LegacyBaselinePaths, LegacyBaselines}, + execution::{audit_execution, ExecutionAuditError}, inventory::{AuditError, RepositoryInventory}, policy::{Baselines, Policy, ReadPolicyError}, repository_file::{self, OpenRepositoryFileError, OpenedRepositoryFile}, @@ -101,7 +103,12 @@ impl CiInputs { reject_duplicate_baseline_inputs(&baseline_files)?; let legacy = LegacyBaselines::read_open(&paths, baseline_files.files()) .map_err(LoadCiError::Baseline)?; - Ok(Self { policy, repository, workflow_jobs, legacy }) + let inputs = Self { policy, repository, workflow_jobs, legacy }; + // Keep the pure parity proof inside this checked boundary. Returning a + // `CiInputs` without this call would make correctness depend on every + // planner and CLI entry point remembering a second validation pass. + audit_execution(&inputs).map_err(LoadCiError::Execution)?; + Ok(inputs) } /// Returns the checked coverage policy. @@ -321,6 +328,9 @@ pub enum LoadCiError { /// The frozen legacy evidence was unreadable or noncanonical. #[error(transparent)] Baseline(BaselineError), + /// Typed execution behavior differed from frozen legacy evidence. + #[error(transparent)] + Execution(ExecutionAuditError), } #[cfg(test)] diff --git a/tools/zc/src/cli.rs b/tools/zc/src/cli.rs index 2eddc91362..7c15cf4e41 100644 --- a/tools/zc/src/cli.rs +++ b/tools/zc/src/cli.rs @@ -30,7 +30,6 @@ use thiserror::Error; use crate::{ ci::{CiInputs, LoadCiError}, - execution::{audit_execution, ExecutionAuditError}, github::{GitHubProjection, ProjectionError, ProjectionWriteError}, plan::{ BuildPlanCell, ExecutionMode, FeatureSelection, MiriPlanCell, Plan, PlanError, @@ -53,14 +52,6 @@ pub fn run( } let inputs = CiInputs::load(repository_root).map_err(|error| CliError::LoadInputs(Box::new(error)))?; - // The execution model lands before its validation becomes part of - // `CiInputs::load`. Keep every current CLI path safe at this boundary in - // the meantime: in particular, `audit` cannot report success and - // `github-plan` cannot publish until exact command parity has passed. The - // follow-up input-boundary commit centralizes this call and removes the - // temporary CLI-specific error variant. - audit_execution(&inputs)?; - let result = match command { Command::Audit => audit(&inputs, &mut output), Command::Plan { event } => print_plan(&inputs, &event, &mut output), @@ -517,9 +508,6 @@ pub enum CliError { /// A checked plan could not be constructed. #[error(transparent)] Plan(#[from] PlanError), - /// Typed execution behavior differed from frozen legacy evidence. - #[error(transparent)] - Execution(#[from] ExecutionAuditError), /// A checked plan could not be serialized for GitHub Actions. #[error(transparent)] Projection(#[from] ProjectionError),