From a3e4b54c01fc9f3b751848baa67bde68a2291eb8 Mon Sep 17 00:00:00 2001 From: joshlf's Agent Date: Tue, 25 Aug 2026 00:48:36 +0000 Subject: [PATCH] [ci] Derive deterministic build and Miri plans Expand the checked policy and repository inventory into typed build and Miri cells. Keep planning pure and deterministic so local tools and CI consume the same decisions without granting generated data any workflow authority. Reject unknown events, duplicate cells, and oversized plans. Compare all reduced and full plans with the independently captured legacy baselines on every invocation, and expose explanations from the same evaluator so coverage changes cannot silently diverge from their diagnostics. gherrit-pr-id: Giv5z2wwyud4nqo75yn6y52kndx6niqkn --- tools/zc/src/lib.rs | 1 + tools/zc/src/plan.rs | 1208 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1209 insertions(+) create mode 100644 tools/zc/src/plan.rs diff --git a/tools/zc/src/lib.rs b/tools/zc/src/lib.rs index 4502971942..92e3cfe4a8 100644 --- a/tools/zc/src/lib.rs +++ b/tools/zc/src/lib.rs @@ -13,6 +13,7 @@ pub mod ci; mod identifier; pub mod inventory; pub mod metadata; +pub mod plan; pub mod policy; mod repository_file; pub mod workflow; diff --git a/tools/zc/src/plan.rs b/tools/zc/src/plan.rs new file mode 100644 index 0000000000..0554f36a35 --- /dev/null +++ b/tools/zc/src/plan.rs @@ -0,0 +1,1208 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +//! Pure, deterministic selection of CI matrix membership. +//! +//! [`CiInputs`] is the only public input boundary. By the time data reaches +//! this module, `ci/zc.toml`, live Cargo metadata, repository structure, and +//! the frozen legacy files have each passed their owning validator. Planning +//! therefore performs no file-system access and has no ambient inputs. +//! +//! This module selects ordinary build and Miri matrix members; it does not +//! prove how GitHub executes them. In particular, a plan never contains +//! permissions, secrets, runner labels, action references, publication +//! choices, or shell commands. Keep those security-sensitive concerns in the +//! small hand-written workflows. +//! +//! The types below record the intended semantics that a later workflow +//! projection must preserve. Before a workflow consumes a plan, that +//! projection and its command behavior must be validated separately. Matrix +//! membership equality alone cannot prove, for example, that native tests are +//! executed, cross-target tests are only compiled, or Miri tests are +//! interpreted. +//! +//! The selectors below coordinate three independently reviewed sources: +//! policy supplies stable IDs and scopes, inventory supplies exact compiler +//! versions and Cargo package paths, and `ci/baselines/*.tsv` freezes the old +//! build and Miri sets. Every call compares all four reduced/full membership +//! sets before returning a plan, so a policy edit cannot silently redefine the +//! selected legacy matrix. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt, + path::{Path, PathBuf}, + str::FromStr, +}; + +use thiserror::Error; + +use crate::{ + baseline::{BaselineId, BuildCell, LegacyBaselines, MiriCell, SetDifference}, + ci::CiInputs, + inventory::RepositoryInventory, + policy::{EventCategory, FeatureProfile, Id, Policy, Scope, TargetMode}, +}; + +/// Whether the event receives reduced or full policy coverage. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum EventClass { + /// Latency-optimized coverage, currently used by pull requests. + Reduced, + /// Full coverage, used by merge queue, manual, and push events. + Full, +} + +impl From for EventClass { + fn from(category: EventCategory) -> Self { + match category { + EventCategory::Reduced => Self::Reduced, + EventCategory::Full => Self::Full, + } + } +} + +impl fmt::Display for EventClass { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Reduced => formatter.write_str("reduced"), + Self::Full => formatter.write_str("full"), + } + } +} + +// The row-oriented TSV baselines freeze reduced and full matrix membership, +// but they cannot say which GitHub event selects each set. Keep this small, +// independent event-class baseline until that relationship has its own +// versioned input. The invariant below makes a policy-only event change fail +// instead of silently changing which event receives which legacy matrix. +const LEGACY_EVENT_CLASSES: [(&str, EventClass); 4] = [ + ("merge_group", EventClass::Full), + ("pull_request", EventClass::Reduced), + ("push", EventClass::Full), + ("workflow_dispatch", EventClass::Full), +]; + +/// How a later workflow projection must handle an ordinary compilation target. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ExecutionMode { + /// Build and execute tests on the runner. + Native, + /// Compile test code and build library code without executing tests. + Cross, + /// Check only library code because test dependencies cannot be compiled. + Thumb, +} + +impl From for ExecutionMode { + fn from(mode: TargetMode) -> Self { + match mode { + TargetMode::Native => Self::Native, + TargetMode::Cross => Self::Cross, + TargetMode::Thumb => Self::Thumb, + } + } +} + +/// A semantic Cargo feature selection. +/// +/// The variants record meaning, and [`Self::cargo_args`] is the one checked +/// translation of that meaning to exact Cargo argument boundaries. Keeping the +/// translation here makes adding a variant a compiler-enforced update instead +/// of requiring unrelated projection and execution code to agree on strings. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum FeatureSelection { + /// Use Cargo's default feature selection. + Default, + /// Disable Cargo defaults without selecting another feature. + NoDefault, + /// Disable defaults and select the repository's stable aggregate feature. + StableAggregate { + /// The Cargo feature name obtained from validated policy. + feature: String, + }, + /// Select every Cargo feature. + All, +} + +impl FeatureSelection { + /// Returns the exact ordered Cargo arguments for this selection. + /// + /// The vector preserves argument boundaries; callers must pass its entries + /// as arguments rather than joining them into shell text. Until the typed + /// executor replaces the live adapter, keep this exhaustive translation + /// coordinated with the `FEATURE_PROFILE` case statement in + /// `.github/workflows/ci.yml`. + pub fn cargo_args(&self) -> Vec { + match self { + Self::Default => Vec::new(), + Self::NoDefault => vec!["--no-default-features".to_owned()], + Self::StableAggregate { feature } => { + vec!["--no-default-features".to_owned(), "--features".to_owned(), feature.clone()] + } + Self::All => vec!["--all-features".to_owned()], + } + } +} + +/// A package selected for work. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct PackageSelector { + id: String, + manifest: PathBuf, +} + +impl PackageSelector { + /// Returns the stable policy and Cargo package ID. + pub fn id(&self) -> &str { + &self.id + } + + /// Returns the package manifest relative to the repository root. + pub fn manifest(&self) -> &Path { + &self.manifest + } +} + +/// An exact Rust toolchain selected for work. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct ToolchainSelector { + id: String, + version: String, +} + +impl ToolchainSelector { + /// Returns the stable policy toolchain ID. + pub fn id(&self) -> &str { + &self.id + } + + /// Returns the exact compiler descriptor resolved by repository inventory. + pub fn version(&self) -> &str { + &self.version + } +} + +/// A named semantic feature profile selected for work. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct FeatureSelector { + profile: String, + selection: FeatureSelection, +} + +impl FeatureSelector { + /// Returns the stable policy profile ID. + pub fn profile(&self) -> &str { + &self.profile + } + + /// Returns the profile's Cargo semantics. + pub fn selection(&self) -> &FeatureSelection { + &self.selection + } +} + +/// An ordinary Rust compilation target selected for matrix membership. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct TargetSelector { + triple: String, + mode: ExecutionMode, +} + +impl TargetSelector { + /// Returns the exact Rust target triple. + pub fn triple(&self) -> &str { + &self.triple + } + + /// Returns the intended execution behavior for this ordinary target. + pub fn mode(&self) -> ExecutionMode { + self.mode + } +} + +/// A target whose tests Miri interprets. +/// +/// This deliberately does not expose [`ExecutionMode`]. A target's ordinary +/// build mode describes what a native or cross-compilation job can execute; +/// it does not apply to Miri. Every selected Miri target runs tests inside the +/// Miri interpreter, including targets whose ordinary build mode is `Cross`. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct MiriTargetSelector { + triple: String, +} + +impl MiriTargetSelector { + /// Returns the target triple whose tests Miri must interpret. + pub fn triple(&self) -> &str { + &self.triple + } +} + +/// A Miri borrow model selected for work. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct MiriModelSelector { + id: String, + flags: Vec, +} + +impl MiriModelSelector { + /// Returns the stable policy model ID. + pub fn id(&self) -> &str { + &self.id + } + + /// Returns exact model-specific flags in argument order. + pub fn flags(&self) -> &[String] { + &self.flags + } +} + +/// One selected ordinary build matrix member with typed semantic intent. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct BuildPlanCell { + package: PackageSelector, + toolchain: ToolchainSelector, + features: FeatureSelector, + target: TargetSelector, +} + +impl BuildPlanCell { + /// Returns the package selector. + pub fn package(&self) -> &PackageSelector { + &self.package + } + + /// Returns the exact toolchain selector. + pub fn toolchain(&self) -> &ToolchainSelector { + &self.toolchain + } + + /// Returns the semantic feature selector. + pub fn features(&self) -> &FeatureSelector { + &self.features + } + + /// Returns the compilation-target selector. + pub fn target(&self) -> &TargetSelector { + &self.target + } +} + +/// One selected Miri matrix member with typed semantic intent. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct MiriPlanCell { + package: PackageSelector, + toolchain: ToolchainSelector, + features: FeatureSelector, + target: MiriTargetSelector, + model: MiriModelSelector, +} + +impl MiriPlanCell { + /// Returns the package selector. + pub fn package(&self) -> &PackageSelector { + &self.package + } + + /// Returns the exact toolchain selector. + pub fn toolchain(&self) -> &ToolchainSelector { + &self.toolchain + } + + /// Returns the semantic feature selector. + pub fn features(&self) -> &FeatureSelector { + &self.features + } + + /// Returns the target whose tests Miri must interpret. + pub fn target(&self) -> &MiriTargetSelector { + &self.target + } + + /// Returns the Miri model selector. + pub fn model(&self) -> &MiriModelSelector { + &self.model + } +} + +/// Why the shared evaluator included or excluded one cell. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DecisionReason { + /// Full events include every ordinary build candidate. + FullEventIncludesBuild, + /// A reduced event includes this target because policy marks it eligible. + ReducedEventIncludesEligibleTarget, + /// A reduced event excludes this target because it is not eligible. + ReducedEventExcludesIneligibleTarget, + /// This event category matches the category configured for Miri. + MiriEventCategoryMatches, + /// This event category does not match the category configured for Miri. + MiriEventCategoryDoesNotMatch, +} + +impl fmt::Display for DecisionReason { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FullEventIncludesBuild => { + formatter.write_str("included because full events run every build cell") + } + Self::ReducedEventIncludesEligibleTarget => formatter + .write_str("included because policy marks the target eligible for reduced events"), + Self::ReducedEventExcludesIneligibleTarget => formatter.write_str( + "excluded because policy does not mark the target eligible for reduced events", + ), + Self::MiriEventCategoryMatches => { + formatter.write_str("included because this event category runs Miri") + } + Self::MiriEventCategoryDoesNotMatch => { + formatter.write_str("excluded because Miri runs in the other event category") + } + } + } +} + +/// The inclusion decision produced by the shared evaluator. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CellDecision { + /// The cell appears in the selected matrix membership. + Included(DecisionReason), + /// The candidate remains visible only in explanations. + Excluded(DecisionReason), +} + +impl CellDecision { + /// Returns whether this candidate appears in the selected matrix. + pub fn is_included(&self) -> bool { + matches!(self, Self::Included(_)) + } + + /// Returns the typed reason for this decision. + pub fn reason(&self) -> DecisionReason { + match *self { + Self::Included(reason) | Self::Excluded(reason) => reason, + } + } +} + +impl fmt::Display for CellDecision { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.reason().fmt(formatter) + } +} + +/// One explained ordinary build candidate. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExplainedBuildCell { + cell: BuildPlanCell, + decision: CellDecision, +} + +impl ExplainedBuildCell { + /// Returns the fully resolved candidate. + pub fn cell(&self) -> &BuildPlanCell { + &self.cell + } + + /// Returns whether and why the candidate is selected. + pub fn decision(&self) -> CellDecision { + self.decision + } +} + +impl fmt::Display for ExplainedBuildCell { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "build {}/{}/{}/{}: {}", + self.cell.package.id, + self.cell.toolchain.id, + self.cell.features.profile, + self.cell.target.triple, + self.decision + ) + } +} + +/// One explained Miri candidate. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExplainedMiriCell { + cell: MiriPlanCell, + decision: CellDecision, +} + +impl ExplainedMiriCell { + /// Returns the fully resolved candidate. + pub fn cell(&self) -> &MiriPlanCell { + &self.cell + } + + /// Returns whether and why the candidate is selected. + pub fn decision(&self) -> CellDecision { + self.decision + } +} + +impl fmt::Display for ExplainedMiriCell { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "miri {}/{}/{}/{}/{}: {}", + self.cell.package.id, + self.cell.toolchain.id, + self.cell.features.profile, + self.cell.target.triple, + self.cell.model.id, + self.decision + ) + } +} + +/// Membership decisions for every policy-generated candidate for one event. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanExplanation { + event: String, + class: EventClass, + builds: Vec, + miri: Vec, +} + +impl PlanExplanation { + /// Applies the same membership selection path used by [`Plan::create`]. + pub fn create(inputs: &CiInputs, event: &str) -> Result { + EvaluatedPlan::evaluate(inputs, event).map(EvaluatedPlan::into_explanation) + } + + /// Returns the exact GitHub event name. + pub fn event(&self) -> &str { + &self.event + } + + /// Returns the event's policy category. + pub fn class(&self) -> EventClass { + self.class + } + + /// Returns all ordinary candidates in deterministic order. + pub fn builds(&self) -> &[ExplainedBuildCell] { + &self.builds + } + + /// Returns all Miri candidates in deterministic order. + pub fn miri(&self) -> &[ExplainedMiriCell] { + &self.miri + } +} + +impl fmt::Display for PlanExplanation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(formatter, "event `{}` has {} coverage:", self.event, self.class)?; + for cell in &self.builds { + writeln!(formatter, "- {cell}")?; + } + for cell in &self.miri { + writeln!(formatter, "- {cell}")?; + } + Ok(()) + } +} + +/// The selected, data-only matrix membership for one exact event. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Plan { + event: String, + class: EventClass, + builds: Vec, + miri: Vec, +} + +impl Plan { + /// Expands one exact event from fully validated inputs. + pub fn create(inputs: &CiInputs, event: &str) -> Result { + EvaluatedPlan::evaluate(inputs, event).map(EvaluatedPlan::into_plan) + } + + /// Returns the exact GitHub event name. + pub fn event(&self) -> &str { + &self.event + } + + /// Returns the event's policy category. + pub fn class(&self) -> EventClass { + self.class + } + + /// Returns ordinary build cells in deterministic order. + pub fn builds(&self) -> &[BuildPlanCell] { + &self.builds + } + + /// Returns Miri cells in deterministic order. + pub fn miri(&self) -> &[MiriPlanCell] { + &self.miri + } +} + +#[derive(Clone, Debug)] +struct BuildCandidate { + cell: BuildPlanCell, + reduced_eligible: bool, +} + +#[derive(Clone, Debug)] +struct EvaluatedPlan { + event: String, + class: EventClass, + builds: Vec, + miri: Vec, +} + +impl EvaluatedPlan { + fn evaluate(inputs: &CiInputs, event: &str) -> Result { + let policy = inputs.policy(); + validate_legacy_event_classes(policy)?; + let class = classify_event(policy, event)?; + let build_candidates = enumerate_build_candidates(policy, inputs.repository())?; + let miri_candidates = enumerate_miri_candidates(policy, inputs.repository())?; + + validate_legacy_membership(policy, inputs.legacy(), &build_candidates, &miri_candidates)?; + + let builds = build_candidates + .values() + .map(|candidate| ExplainedBuildCell { + cell: candidate.cell.clone(), + decision: evaluate_build(class, candidate.reduced_eligible), + }) + .collect::>(); + let miri_class = EventClass::from(policy.miri().event_category()); + let miri = miri_candidates + .iter() + .cloned() + .map(|cell| ExplainedMiriCell { cell, decision: evaluate_miri(class, miri_class) }) + .collect::>(); + + let selected = builds.iter().filter(|cell| cell.decision.is_included()).count() + + miri.iter().filter(|cell| cell.decision.is_included()).count(); + enforce_plan_limit(selected, policy.limits().max_plan_cells())?; + + Ok(Self { event: event.to_owned(), class, builds, miri }) + } + + fn into_plan(self) -> Plan { + let builds = self + .builds + .into_iter() + .filter_map(|candidate| candidate.decision.is_included().then_some(candidate.cell)) + .collect(); + let miri = self + .miri + .into_iter() + .filter_map(|candidate| candidate.decision.is_included().then_some(candidate.cell)) + .collect(); + Plan { event: self.event, class: self.class, builds, miri } + } + + fn into_explanation(self) -> PlanExplanation { + PlanExplanation { + event: self.event, + class: self.class, + builds: self.builds, + miri: self.miri, + } + } +} + +fn classify_event(policy: &Policy, event: &str) -> Result { + policy.events().category(event).map(EventClass::from).ok_or_else(|| { + let known = policy + .events() + .reduced() + .iter() + .chain(policy.events().full()) + .map(Id::as_str) + .collect::>() + .join(", "); + PlanError::UnknownEvent { + event: event.to_owned(), + display_event: escape_control_characters(event), + known, + } + }) +} + +fn validate_legacy_event_classes(policy: &Policy) -> Result<(), PlanError> { + let actual = policy + .events() + .reduced() + .iter() + .map(|event| (event.as_str(), EventClass::Reduced)) + .chain(policy.events().full().iter().map(|event| (event.as_str(), EventClass::Full))) + .collect::>(); + let expected = LEGACY_EVENT_CLASSES.into_iter().collect::>(); + if actual == expected { + return Ok(()); + } + + let format_classes = |classes: &BTreeMap<&str, EventClass>| { + classes + .iter() + .map(|(event, class)| format!("{event}={class}")) + .collect::>() + .join(", ") + }; + Err(PlanError::LegacyEventClassMismatch { + expected: format_classes(&expected), + actual: format_classes(&actual), + }) +} + +fn enumerate_build_candidates( + policy: &Policy, + repository: &RepositoryInventory, +) -> Result, PlanError> { + let mut cells = BTreeMap::new(); + for (toolchain_id, toolchain_policy) in policy.toolchains() { + let toolchain = toolchain_selector(repository, toolchain_id)?; + for scope in toolchain_policy.scopes() { + for package_id in scope.packages() { + let package = package_selector(repository, package_id)?; + for profile_id in scope.profiles() { + let features = feature_selector(policy, profile_id)?; + for target_id in target_set(policy, scope)? { + let target_policy = + policy.targets().get(target_id.as_str()).ok_or_else(|| { + PlanError::MissingValidatedInput { + location: format!("targets.{}", target_id.as_str()), + } + })?; + let cell = BuildPlanCell { + package: package.clone(), + toolchain: toolchain.clone(), + features: features.clone(), + target: target_selector(target_id, target_policy.mode()), + }; + let candidate = BuildCandidate { + cell: cell.clone(), + reduced_eligible: target_policy.pr_eligible(), + }; + if cells.insert(cell.clone(), candidate).is_some() { + return Err(PlanError::DuplicateCell { + kind: "ordinary build", + selector: format_build_selector(&cell), + }); + } + } + } + } + } + } + Ok(cells) +} + +fn enumerate_miri_candidates( + policy: &Policy, + repository: &RepositoryInventory, +) -> Result, PlanError> { + let mut cells = BTreeSet::new(); + let miri = policy.miri(); + let toolchain = toolchain_selector(repository, miri.toolchain())?; + for scope in miri.scopes() { + for package_id in scope.packages() { + let package = package_selector(repository, package_id)?; + for profile_id in scope.profiles() { + let features = feature_selector(policy, profile_id)?; + for target_id in target_set(policy, scope)? { + if !policy.targets().contains_key(target_id.as_str()) { + return Err(PlanError::MissingValidatedInput { + location: format!("targets.{}", target_id.as_str()), + }); + } + let target = miri_target_selector(target_id); + for (model_id, model) in policy.miri_models() { + let cell = MiriPlanCell { + package: package.clone(), + toolchain: toolchain.clone(), + features: features.clone(), + target: target.clone(), + model: MiriModelSelector { + id: model_id.as_str().to_owned(), + flags: model.flags().to_vec(), + }, + }; + if !cells.insert(cell.clone()) { + return Err(PlanError::DuplicateCell { + kind: "Miri", + selector: format_miri_selector(&cell), + }); + } + } + } + } + } + } + Ok(cells) +} + +fn target_set<'a>(policy: &'a Policy, scope: &Scope) -> Result<&'a BTreeSet, PlanError> { + policy.target_sets().get(scope.target_set().as_str()).ok_or_else(|| { + PlanError::MissingValidatedInput { + location: format!("target_sets.{}", scope.target_set().as_str()), + } + }) +} + +fn package_selector( + repository: &RepositoryInventory, + id: &Id, +) -> Result { + let package = repository.policy_packages().get(id.as_str()).ok_or_else(|| { + PlanError::MissingValidatedInput { location: format!("inventory.packages.{}", id.as_str()) } + })?; + Ok(PackageSelector { + id: id.as_str().to_owned(), + manifest: package.cargo().manifest().to_path_buf(), + }) +} + +fn toolchain_selector( + repository: &RepositoryInventory, + id: &Id, +) -> Result { + let version = repository.toolchain_versions().get(id.as_str()).ok_or_else(|| { + PlanError::MissingValidatedInput { + location: format!("inventory.toolchains.{}", id.as_str()), + } + })?; + Ok(ToolchainSelector { id: id.as_str().to_owned(), version: version.clone() }) +} + +fn feature_selector(policy: &Policy, id: &Id) -> Result { + let profile = policy.features().profiles().get(id.as_str()).ok_or_else(|| { + PlanError::MissingValidatedInput { location: format!("feature_profiles.{}", id.as_str()) } + })?; + let selection = match profile { + FeatureProfile::Default => FeatureSelection::Default, + FeatureProfile::NoDefault => FeatureSelection::NoDefault, + FeatureProfile::StableAggregate => FeatureSelection::StableAggregate { + feature: policy.features().stable_feature_root().as_str().to_owned(), + }, + FeatureProfile::All => FeatureSelection::All, + }; + Ok(FeatureSelector { profile: id.as_str().to_owned(), selection }) +} + +fn target_selector(id: &Id, mode: TargetMode) -> TargetSelector { + TargetSelector { triple: id.as_str().to_owned(), mode: ExecutionMode::from(mode) } +} + +fn miri_target_selector(id: &Id) -> MiriTargetSelector { + MiriTargetSelector { triple: id.as_str().to_owned() } +} + +fn evaluate_build(class: EventClass, reduced_eligible: bool) -> CellDecision { + match (class, reduced_eligible) { + (EventClass::Full, _) => CellDecision::Included(DecisionReason::FullEventIncludesBuild), + (EventClass::Reduced, true) => { + CellDecision::Included(DecisionReason::ReducedEventIncludesEligibleTarget) + } + (EventClass::Reduced, false) => { + CellDecision::Excluded(DecisionReason::ReducedEventExcludesIneligibleTarget) + } + } +} + +fn evaluate_miri(actual: EventClass, configured: EventClass) -> CellDecision { + if actual == configured { + CellDecision::Included(DecisionReason::MiriEventCategoryMatches) + } else { + CellDecision::Excluded(DecisionReason::MiriEventCategoryDoesNotMatch) + } +} + +fn validate_legacy_membership( + policy: &Policy, + legacy: &LegacyBaselines, + builds: &BTreeMap, + miri: &BTreeSet, +) -> Result<(), PlanError> { + let build_full = builds.keys().map(build_baseline_cell).collect::, _>>()?; + let build_reduced = builds + .values() + .filter(|candidate| candidate.reduced_eligible) + .map(|candidate| build_baseline_cell(&candidate.cell)) + .collect::, _>>()?; + compare_build("reduced", legacy.compare_build_reduced(&build_reduced))?; + compare_build("full", legacy.compare_build_full(&build_full))?; + + let miri_all = miri.iter().map(miri_baseline_cell).collect::, _>>()?; + let configured = EventClass::from(policy.miri().event_category()); + let empty = BTreeSet::new(); + let (miri_reduced, miri_full) = match configured { + EventClass::Reduced => (&miri_all, &empty), + EventClass::Full => (&empty, &miri_all), + }; + compare_miri("reduced", legacy.compare_miri_reduced(miri_reduced))?; + compare_miri("full", legacy.compare_miri_full(miri_full))?; + Ok(()) +} + +fn build_baseline_cell(cell: &BuildPlanCell) -> Result { + Ok(BuildCell::new( + baseline_id("package", cell.package.id())?, + baseline_id("toolchain", cell.toolchain.id())?, + baseline_id("feature profile", cell.features.profile())?, + baseline_id("target", cell.target.triple())?, + )) +} + +fn miri_baseline_cell(cell: &MiriPlanCell) -> Result { + MiriCell::new( + baseline_id("package", cell.package.id())?, + baseline_id("toolchain", cell.toolchain.id())?, + baseline_id("feature profile", cell.features.profile())?, + baseline_id("target", cell.target.triple())?, + baseline_id("Miri model", cell.model.id())?, + cell.model.flags().to_vec(), + ) + .map_err(|message| PlanError::InvalidLegacyProjection { + selector: format_miri_selector(cell), + message, + }) +} + +fn baseline_id(kind: &'static str, value: &str) -> Result { + BaselineId::from_str(value).map_err(|message| PlanError::InvalidLegacyProjection { + selector: format!("{kind} `{value}`"), + message, + }) +} + +fn compare_build( + category: &'static str, + result: Result<(), SetDifference>, +) -> Result<(), PlanError> { + result.map_err(|difference| legacy_mismatch("ordinary build", category, &difference)) +} + +fn compare_miri( + category: &'static str, + result: Result<(), SetDifference>, +) -> Result<(), PlanError> { + result.map_err(|difference| legacy_mismatch("Miri", category, &difference)) +} + +fn legacy_mismatch( + kind: &'static str, + category: &'static str, + difference: &SetDifference, +) -> PlanError { + PlanError::LegacyMismatch { + kind, + category, + missing: difference.missing_from_plan().len(), + extra: difference.extra_in_plan().len(), + details: difference.to_string(), + } +} + +fn enforce_plan_limit(planned: usize, maximum: u64) -> Result<(), PlanError> { + let planned = u64::try_from(planned).unwrap_or(u64::MAX); + if planned <= maximum { + Ok(()) + } else { + Err(PlanError::PlanLimitExceeded { planned, maximum }) + } +} + +fn format_build_selector(cell: &BuildPlanCell) -> String { + format!( + "{}/{}/{}/{}", + cell.package.id, cell.toolchain.id, cell.features.profile, cell.target.triple + ) +} + +fn format_miri_selector(cell: &MiriPlanCell) -> String { + format!( + "{}/{}/{}/{}/{}", + cell.package.id, + cell.toolchain.id, + cell.features.profile, + cell.target.triple, + cell.model.id + ) +} + +fn escape_control_characters(value: &str) -> String { + let mut escaped = String::with_capacity(value.len()); + for character in value.chars() { + if character.is_control() { + escaped.extend(character.escape_default()); + } else { + escaped.push(character); + } + } + escaped +} + +/// A deterministic planning or legacy-membership failure. +#[derive(Debug, Error, Eq, PartialEq)] +pub enum PlanError { + /// The event is absent from both policy categories. + #[error("unknown GitHub event `{display_event}`; configured events are: {known}")] + UnknownEvent { + /// The rejected event name, retained exactly for programmatic use. + event: String, + /// The rejected name with control characters escaped for diagnostics. + display_event: String, + /// Deterministically ordered configured names. + known: String, + }, + /// Event names or classes differed from their independent legacy baseline. + #[error( + "event classes differ from the legacy baseline: expected [{expected}]; found [{actual}]" + )] + LegacyEventClassMismatch { + /// Deterministically ordered baseline event assignments. + expected: String, + /// Deterministically ordered policy event assignments. + actual: String, + }, + /// Data disappeared after the checked input boundary. + #[error("validated CI input `{location}` is missing")] + MissingValidatedInput { + /// The missing policy or inventory location. + location: String, + }, + /// Policy scopes expanded the same logical cell twice. + #[error("{kind} selector `{selector}` was generated more than once")] + DuplicateCell { + /// The kind of planned work. + kind: &'static str, + /// The stable human-readable selector. + selector: String, + }, + /// A validated selector could not be represented by the legacy row type. + #[error("cannot compare {selector} with the legacy baseline: {message}")] + InvalidLegacyProjection { + /// The selector being converted. + selector: String, + /// The baseline type's strict parse diagnostic. + message: String, + }, + /// Selected matrix membership differed from frozen legacy evidence. + #[error( + "{category} {kind} matrix membership differs from the legacy baseline: {missing} missing, {extra} extra\n{details}" + )] + LegacyMismatch { + /// The kind of planned work. + kind: &'static str, + /// The reduced or full category. + category: &'static str, + /// Baseline cells missing from the plan. + missing: usize, + /// Planned cells absent from the baseline. + extra: usize, + /// Exact missing and extra rows. + details: String, + }, + /// Expanded work exceeded the policy's pre-sharding safety bound. + #[error("plan expands to {planned} cells, above limits.max_plan_cells ({maximum})")] + PlanLimitExceeded { + /// Included logical cells. + planned: u64, + /// The configured maximum. + maximum: u64, + }, +} + +impl PlanError { + /// Returns the rejected event name for an unknown-event error. + pub fn unknown_event(&self) -> Option<&str> { + match self { + Self::UnknownEvent { event, .. } => Some(event), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use std::{path::Path, sync::OnceLock}; + + use super::{ + enforce_plan_limit, CellDecision, DecisionReason, EventClass, ExecutionMode, + FeatureSelection, Plan, PlanError, PlanExplanation, + }; + use crate::ci::CiInputs; + + fn inputs() -> &'static CiInputs { + static INPUTS: OnceLock = OnceLock::new(); + INPUTS.get_or_init(|| { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + CiInputs::load(root).unwrap() + }) + } + + #[test] + fn plans_each_exact_legacy_event_class() { + for event in ["pull_request", "merge_group", "push", "workflow_dispatch"] { + let plan = Plan::create(inputs(), event).unwrap(); + if event == "pull_request" { + assert_eq!(plan.class(), EventClass::Reduced); + assert_eq!(plan.builds().len(), 60); + assert!(plan.miri().is_empty()); + } else { + assert_eq!(plan.class(), EventClass::Full); + assert_eq!(plan.builds().len(), 182); + assert_eq!(plan.miri().len(), 64); + } + } + } + + #[test] + fn rejects_unknown_events_without_a_fallback() { + let error = Plan::create(inputs(), "pull_request_target").unwrap_err(); + + assert_eq!(error.unknown_event(), Some("pull_request_target")); + let diagnostic = error.to_string(); + assert!(diagnostic.contains("pull_request")); + assert!(diagnostic.contains("merge_group")); + } + + #[test] + fn escapes_unknown_event_diagnostics_but_preserves_the_raw_name() { + let raw = "pull\nrequest\u{7}"; + let error = Plan::create(inputs(), raw).unwrap_err(); + + assert_eq!(error.unknown_event(), Some(raw)); + let diagnostic = error.to_string(); + assert!(diagnostic.contains(r"pull\nrequest\u{7}")); + assert!(!diagnostic.contains('\n')); + assert!(!diagnostic.contains('\u{7}')); + } + + #[test] + fn current_plans_have_exact_legacy_membership() { + let reduced = Plan::create(inputs(), "pull_request").unwrap(); + let full = Plan::create(inputs(), "merge_group").unwrap(); + + assert_eq!(reduced.builds().len(), inputs().legacy().build_reduced().len()); + assert_eq!(reduced.miri().len(), inputs().legacy().miri_reduced().len()); + assert_eq!(full.builds().len(), inputs().legacy().build_full().len()); + assert_eq!(full.miri().len(), inputs().legacy().miri_full().len()); + assert_eq!(full, Plan::create(inputs(), "merge_group").unwrap()); + } + + #[test] + fn selectors_expose_data_without_commands_or_workflow_authority() { + let plan = Plan::create(inputs(), "merge_group").unwrap(); + let cell = plan + .builds() + .iter() + .find(|cell| { + cell.package().id() == "zerocopy" + && cell.toolchain().id() == "nightly" + && cell.features().profile() == "stable" + && cell.target().triple() == "x86_64-unknown-linux-gnu" + }) + .unwrap(); + + assert_eq!(cell.package().manifest(), Path::new("zerocopy/Cargo.toml")); + assert_eq!(cell.toolchain().version(), "nightly-2026-01-25"); + assert_eq!(cell.target().mode(), ExecutionMode::Native); + assert!(matches!( + cell.features().selection(), + FeatureSelection::StableAggregate { feature } + if feature == "__internal_use_only_features_that_work_on_stable" + )); + } + + #[test] + fn every_feature_selection_has_exact_cargo_argument_boundaries() { + assert_eq!(FeatureSelection::Default.cargo_args(), Vec::::new()); + assert_eq!( + FeatureSelection::NoDefault.cargo_args(), + ["--no-default-features"].map(str::to_owned) + ); + assert_eq!( + FeatureSelection::StableAggregate { feature: "stable-root".to_owned() }.cargo_args(), + ["--no-default-features", "--features", "stable-root"].map(str::to_owned) + ); + assert_eq!(FeatureSelection::All.cargo_args(), ["--all-features"].map(str::to_owned)); + } + + #[test] + fn miri_is_full_only() { + let reduced = PlanExplanation::create(inputs(), "pull_request").unwrap(); + let full = PlanExplanation::create(inputs(), "push").unwrap(); + + assert!(reduced.miri().iter().all(|cell| !cell.decision().is_included())); + assert!(full.miri().iter().all(|cell| cell.decision().is_included())); + assert_eq!(full.miri().len(), 64); + } + + #[test] + fn miri_interprets_tests_for_an_ordinary_cross_target() { + let plan = Plan::create(inputs(), "merge_group").unwrap(); + let cross_target = "arm-unknown-linux-gnueabi"; + let ordinary = + plan.builds().iter().find(|cell| cell.target().triple() == cross_target).unwrap(); + assert_eq!(ordinary.target().mode(), ExecutionMode::Cross); + + // A Miri target deliberately has no ordinary `ExecutionMode`: being a + // `MiriTargetSelector` means that Miri interprets this cell's tests. + let interpreted = plan.miri().iter().find(|cell| cell.target().triple() == cross_target); + assert!(interpreted.is_some()); + } + + #[test] + fn explanations_cover_included_and_excluded_candidates() { + let explanation = PlanExplanation::create(inputs(), "pull_request").unwrap(); + let included = explanation + .builds() + .iter() + .find(|cell| { + cell.cell().target().triple() == "i686-unknown-linux-gnu" + && cell.decision().is_included() + }) + .unwrap(); + let excluded = explanation + .builds() + .iter() + .find(|cell| { + cell.cell().target().triple() == "arm-unknown-linux-gnueabi" + && !cell.decision().is_included() + }) + .unwrap(); + + assert_eq!( + included.decision(), + CellDecision::Included(DecisionReason::ReducedEventIncludesEligibleTarget) + ); + assert_eq!( + excluded.decision(), + CellDecision::Excluded(DecisionReason::ReducedEventExcludesIneligibleTarget) + ); + assert!(included.to_string().contains("included because")); + assert!(excluded.to_string().contains("excluded because")); + let rendered = explanation.to_string(); + assert!(rendered.contains("event `pull_request` has reduced coverage")); + assert!(rendered.contains("included because")); + assert!(rendered.contains("excluded because")); + } + + #[test] + fn reports_plan_limit_with_both_values() { + assert!(enforce_plan_limit(100, 100).is_ok()); + let error = enforce_plan_limit(101, 100).unwrap_err(); + + assert_eq!(error, PlanError::PlanLimitExceeded { planned: 101, maximum: 100 }); + assert!(error.to_string().contains("101")); + assert!(error.to_string().contains("100")); + } +}