Skip to main content

guarded_continuation_checker/
lib.rs

1//! Stable client APIs for GCC predicate and named event-contract workflows.
2//!
3//! The verifier implementation remains in the separately versioned executable.
4//! This library invokes it directly without a shell and validates its advertised
5//! versioned CLI contract before exposing typed certificate operations.
6
7pub mod aiger_obligation;
8pub mod btor2;
9pub mod btor2_bitblast;
10pub mod btor2_bounded;
11pub mod btor2_braking;
12pub mod btor2_component;
13pub mod btor2_family;
14pub mod btor2_family_orbit;
15pub mod btor2_family_proof;
16pub mod btor2_invariant_chain;
17pub mod btor2_motion;
18pub mod btor2_phase;
19pub mod btor2_predicate_set;
20pub mod btor2_region;
21pub mod btor2_region_equivalence;
22pub mod btor2_region_extract;
23pub mod btor2_region_property;
24pub mod btor2_search;
25pub mod compiled_mmio_branching_dag;
26pub mod compiled_mmio_certificate;
27pub mod compiled_mmio_decode_graph;
28pub mod compiled_mmio_explicit_transcript;
29pub mod compiled_mmio_file;
30pub mod compiled_mmio_predicate_certificate;
31pub mod compiled_mmio_predicate_portfolio;
32pub mod compiled_mmio_quotient;
33pub mod compiled_mmio_rtl_certificate;
34pub mod compiled_mmio_rtl_mapping;
35pub mod composed_witness;
36pub mod controller_mtbdd;
37pub mod controller_mtbdd_proof;
38pub mod controller_plant;
39pub mod controller_plant_aiger;
40pub mod controller_plant_artifact;
41pub mod controller_transducer;
42pub mod dense_relation;
43pub mod firmware_transaction_contract;
44#[cfg(feature = "research-qatq-transport")]
45pub mod qatq_transport;
46pub mod revision_batch;
47pub mod revision_impact;
48pub mod revision_local;
49pub mod riscv32imc;
50pub mod riscv32imc_predicate;
51pub mod riscv32imc_predicate_checker;
52pub mod source_model_attestation;
53pub mod unsat_proof;
54
55use std::collections::BTreeMap;
56use std::fmt;
57use std::io::Read;
58#[cfg(unix)]
59use std::os::unix::process::CommandExt;
60use std::path::{Path, PathBuf};
61use std::process::{Command, ExitStatus, Stdio};
62use std::thread;
63use std::time::{Duration, Instant};
64
65/// Predicate CLI contract understood by this crate release.
66pub const PREDICATE_API_VERSION: u32 = 1;
67pub const EVENT_CONTRACT_API_VERSION: u32 = 1;
68pub const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(300);
69pub const DEFAULT_OUTPUT_LIMIT_BYTES: usize = 1024 * 1024;
70pub const DEFAULT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024;
71#[cfg(all(unix, not(target_os = "macos")))]
72pub const DEFAULT_MEMORY_LIMIT_BYTES: Option<u64> = Some(2 * 1024 * 1024 * 1024);
73#[cfg(any(not(unix), target_os = "macos"))]
74pub const DEFAULT_MEMORY_LIMIT_BYTES: Option<u64> = None;
75const MAX_OUTPUT_LIMIT_BYTES: usize = 64 * 1024 * 1024;
76const MIN_MEMORY_LIMIT_BYTES: u64 = 64 * 1024 * 1024;
77const MAX_MEMORY_LIMIT_BYTES: u64 = 1024 * 1024 * 1024 * 1024;
78const MAX_FILE_LIMIT_BYTES: u64 = 1024 * 1024 * 1024;
79
80/// Runtime bounds applied independently to every executable invocation.
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub struct ExecutionPolicy {
83    timeout: Duration,
84    output_limit_bytes: usize,
85    memory_limit_bytes: Option<u64>,
86    file_limit_bytes: u64,
87}
88
89impl ExecutionPolicy {
90    pub fn new(timeout: Duration, output_limit_bytes: usize) -> Result<Self, PredicateApiError> {
91        if timeout.is_zero() {
92            return Err(PredicateApiError::InvalidPolicy(
93                "timeout must be greater than zero".to_string(),
94            ));
95        }
96        if !(1..=MAX_OUTPUT_LIMIT_BYTES).contains(&output_limit_bytes) {
97            return Err(PredicateApiError::InvalidPolicy(format!(
98                "output limit must be in 1..={MAX_OUTPUT_LIMIT_BYTES} bytes"
99            )));
100        }
101        Ok(Self {
102            timeout,
103            output_limit_bytes,
104            memory_limit_bytes: DEFAULT_MEMORY_LIMIT_BYTES,
105            file_limit_bytes: DEFAULT_FILE_LIMIT_BYTES,
106        })
107    }
108
109    pub fn timeout(self) -> Duration {
110        self.timeout
111    }
112
113    pub fn output_limit_bytes(self) -> usize {
114        self.output_limit_bytes
115    }
116
117    pub fn memory_limit_bytes(self) -> Option<u64> {
118        self.memory_limit_bytes
119    }
120
121    pub fn file_limit_bytes(self) -> u64 {
122        self.file_limit_bytes
123    }
124
125    #[cfg(all(unix, not(target_os = "macos")))]
126    pub fn with_memory_limit(mut self, bytes: u64) -> Result<Self, PredicateApiError> {
127        if !(MIN_MEMORY_LIMIT_BYTES..=MAX_MEMORY_LIMIT_BYTES).contains(&bytes) {
128            return Err(PredicateApiError::InvalidPolicy(format!(
129                "memory limit must be in {MIN_MEMORY_LIMIT_BYTES}..={MAX_MEMORY_LIMIT_BYTES} bytes"
130            )));
131        }
132        self.memory_limit_bytes = Some(bytes);
133        Ok(self)
134    }
135
136    #[cfg(any(not(unix), target_os = "macos"))]
137    pub fn with_memory_limit(self, bytes: u64) -> Result<Self, PredicateApiError> {
138        if !(MIN_MEMORY_LIMIT_BYTES..=MAX_MEMORY_LIMIT_BYTES).contains(&bytes) {
139            return Err(PredicateApiError::InvalidPolicy(format!(
140                "memory limit must be in {MIN_MEMORY_LIMIT_BYTES}..={MAX_MEMORY_LIMIT_BYTES} bytes"
141            )));
142        }
143        Err(PredicateApiError::InvalidPolicy(
144            "address-space limits are unavailable on this platform".to_string(),
145        ))
146    }
147
148    pub fn with_file_limit(mut self, bytes: u64) -> Result<Self, PredicateApiError> {
149        if !(1..=MAX_FILE_LIMIT_BYTES).contains(&bytes) {
150            return Err(PredicateApiError::InvalidPolicy(format!(
151                "file limit must be in 1..={MAX_FILE_LIMIT_BYTES} bytes"
152            )));
153        }
154        self.file_limit_bytes = bytes;
155        Ok(self)
156    }
157}
158
159impl Default for ExecutionPolicy {
160    fn default() -> Self {
161        Self {
162            timeout: DEFAULT_EXECUTION_TIMEOUT,
163            output_limit_bytes: DEFAULT_OUTPUT_LIMIT_BYTES,
164            memory_limit_bytes: DEFAULT_MEMORY_LIMIT_BYTES,
165            file_limit_bytes: DEFAULT_FILE_LIMIT_BYTES,
166        }
167    }
168}
169
170pub const INVOCATION_METRICS_SCHEMA_VERSION: u32 = 1;
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub enum OperationKind {
174    Discover,
175    DiscoverEventContract,
176    CertifyV1,
177    CertifyV2,
178    CertifyEventContractV3,
179    VerifyV1,
180    VerifyV2,
181    VerifyEventContractV3,
182    EventContractPortfolio,
183    VerifyEventContractPortfolioReport,
184    DiscoverControllerMtbdd,
185    CertifyControllerMtbddPlantBatch,
186    VerifyControllerMtbddPlantBatch,
187    DiscoverControllerProofMtbdd,
188    CertifyControllerProofMtbddPlantBatch,
189    VerifyControllerProofMtbddPlantBatch,
190    DiscoverControllerPlantPortfolio,
191    CertifyControllerPlantPortfolio,
192    VerifyControllerPlantPortfolio,
193    DiscoverControllerPlantResource,
194    VerifyControllerPlantPortfolioResources,
195    DiscoverControllerProofMtbddResource,
196    VerifyControllerProofMtbddPlantResources,
197    DiscoverControllerProofMtbddPortfolio,
198    VerifyControllerProofMtbddPortfolioResources,
199    VerifyControllerProofMtbddPortfolioResourcesAttested,
200    DiscoverControllerSplitEvidence,
201    CertifyControllerProofEvidence,
202    CertifyBoundPlantResults,
203    VerifyBoundPlantResultSet,
204    DiscoverControllerSplitResource,
205    VerifyBoundPlantResultSetResources,
206    DiscoverControllerSplitObservability,
207    VerifyBoundPlantResultSetResourcesObserved,
208    DiscoverControllerSplitAllocationObservability,
209    VerifyBoundPlantResultSetResourcesAllocationObserved,
210    DiscoverControllerSplitCacheObservability,
211    VerifyBoundPlantResultSetResourcesCacheObserved,
212    DiscoverRevisionImpact,
213    CertifyRevisionImpact,
214    VerifyRevisionImpact,
215    DiscoverBtor2ChannelProperty,
216    CertifyBtor2ChannelProperty,
217    VerifyBtor2ChannelProperty,
218    DiscoverBtor2ChannelPropertyObservability,
219    CertifyBtor2ChannelPropertyObserved,
220    VerifyBtor2ChannelPropertyObserved,
221}
222
223impl OperationKind {
224    pub fn as_str(self) -> &'static str {
225        match self {
226            Self::Discover => "discover",
227            Self::DiscoverEventContract => "discover_event_contract",
228            Self::CertifyV1 => "certify_v1",
229            Self::CertifyV2 => "certify_v2",
230            Self::CertifyEventContractV3 => "certify_event_contract_v3",
231            Self::VerifyV1 => "verify_v1",
232            Self::VerifyV2 => "verify_v2",
233            Self::VerifyEventContractV3 => "verify_event_contract_v3",
234            Self::EventContractPortfolio => "event_contract_portfolio",
235            Self::VerifyEventContractPortfolioReport => "verify_event_contract_portfolio_report",
236            Self::DiscoverControllerMtbdd => "discover_controller_mtbdd",
237            Self::CertifyControllerMtbddPlantBatch => "certify_controller_mtbdd_plant_batch",
238            Self::VerifyControllerMtbddPlantBatch => "verify_controller_mtbdd_plant_batch",
239            Self::DiscoverControllerProofMtbdd => "discover_controller_proof_mtbdd",
240            Self::CertifyControllerProofMtbddPlantBatch => {
241                "certify_controller_proof_mtbdd_plant_batch"
242            }
243            Self::VerifyControllerProofMtbddPlantBatch => {
244                "verify_controller_proof_mtbdd_plant_batch"
245            }
246            Self::DiscoverControllerPlantPortfolio => "discover_controller_plant_portfolio",
247            Self::CertifyControllerPlantPortfolio => "certify_controller_plant_portfolio",
248            Self::VerifyControllerPlantPortfolio => "verify_controller_plant_portfolio",
249            Self::DiscoverControllerPlantResource => "discover_controller_plant_resource",
250            Self::VerifyControllerPlantPortfolioResources => {
251                "verify_controller_plant_portfolio_resources"
252            }
253            Self::DiscoverControllerProofMtbddResource => {
254                "discover_controller_proof_mtbdd_resource"
255            }
256            Self::VerifyControllerProofMtbddPlantResources => {
257                "verify_controller_proof_mtbdd_plant_resources"
258            }
259            Self::DiscoverControllerProofMtbddPortfolio => {
260                "discover_controller_proof_mtbdd_portfolio"
261            }
262            Self::VerifyControllerProofMtbddPortfolioResources => {
263                "verify_controller_proof_mtbdd_portfolio_resources"
264            }
265            Self::VerifyControllerProofMtbddPortfolioResourcesAttested => {
266                "verify_controller_proof_mtbdd_portfolio_resources_attested"
267            }
268            Self::DiscoverControllerSplitEvidence => "discover_controller_split_evidence",
269            Self::CertifyControllerProofEvidence => "certify_controller_proof_evidence",
270            Self::CertifyBoundPlantResults => "certify_bound_plant_results",
271            Self::VerifyBoundPlantResultSet => "verify_bound_plant_result_set",
272            Self::DiscoverControllerSplitResource => "discover_controller_split_resource",
273            Self::VerifyBoundPlantResultSetResources => "verify_bound_plant_result_set_resources",
274            Self::DiscoverControllerSplitObservability => "discover_controller_split_observability",
275            Self::VerifyBoundPlantResultSetResourcesObserved => {
276                "verify_bound_plant_result_set_resources_observed"
277            }
278            Self::DiscoverControllerSplitAllocationObservability => {
279                "discover_controller_split_allocation_observability"
280            }
281            Self::VerifyBoundPlantResultSetResourcesAllocationObserved => {
282                "verify_bound_plant_result_set_resources_allocation_observed"
283            }
284            Self::DiscoverControllerSplitCacheObservability => {
285                "discover_controller_split_cache_observability"
286            }
287            Self::VerifyBoundPlantResultSetResourcesCacheObserved => {
288                "verify_bound_plant_result_set_resources_cache_observed"
289            }
290            Self::DiscoverRevisionImpact => "discover_revision_impact",
291            Self::CertifyRevisionImpact => "certify_revision_impact",
292            Self::VerifyRevisionImpact => "verify_revision_impact",
293            Self::DiscoverBtor2ChannelProperty => "discover_btor2_channel_property",
294            Self::CertifyBtor2ChannelProperty => "certify_btor2_channel_property",
295            Self::VerifyBtor2ChannelProperty => "verify_btor2_channel_property",
296            Self::DiscoverBtor2ChannelPropertyObservability => {
297                "discover_btor2_channel_property_observability"
298            }
299            Self::CertifyBtor2ChannelPropertyObserved => "certify_btor2_channel_property_observed",
300            Self::VerifyBtor2ChannelPropertyObserved => "verify_btor2_channel_property_observed",
301        }
302    }
303}
304
305#[derive(Clone, Copy, Debug, Eq, PartialEq)]
306pub enum FailureClass {
307    Policy,
308    Io,
309    Timeout,
310    OutputLimit,
311    ExitStatus,
312    ResourceRefusal,
313    Compatibility,
314    Response,
315}
316
317impl FailureClass {
318    pub fn as_str(self) -> &'static str {
319        match self {
320            Self::Policy => "policy",
321            Self::Io => "io",
322            Self::Timeout => "timeout",
323            Self::OutputLimit => "output_limit",
324            Self::ExitStatus => "exit_status",
325            Self::ResourceRefusal => "resource_refusal",
326            Self::Compatibility => "compatibility",
327            Self::Response => "response",
328        }
329    }
330}
331
332#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333pub enum InvocationStatus {
334    Success,
335    Failed(FailureClass),
336}
337
338#[derive(Clone, Debug, Eq, PartialEq)]
339pub struct InvocationMetrics {
340    pub schema_version: u32,
341    pub operation: OperationKind,
342    pub duration: Duration,
343    pub stdout_bytes: usize,
344    pub stderr_bytes: usize,
345    pub timeout: Duration,
346    pub output_limit_bytes: usize,
347    pub memory_limit_bytes: Option<u64>,
348    pub file_limit_bytes: u64,
349    pub process_group_containment: bool,
350    pub exit_code: Option<i32>,
351    pub status: InvocationStatus,
352}
353
354impl InvocationMetrics {
355    pub const fn csv_header() -> &'static str {
356        "schema_version,operation,duration_ns,stdout_bytes,stderr_bytes,timeout_ms,output_limit_bytes,exit_code,status,failure_class"
357    }
358
359    pub fn to_csv_row(&self) -> String {
360        let (status, failure_class) = match self.status {
361            InvocationStatus::Success => ("ok", "-"),
362            InvocationStatus::Failed(class) => ("error", class.as_str()),
363        };
364        format!(
365            "{},{},{},{},{},{},{},{},{},{}",
366            self.schema_version,
367            self.operation.as_str(),
368            self.duration.as_nanos(),
369            self.stdout_bytes,
370            self.stderr_bytes,
371            self.timeout.as_millis(),
372            self.output_limit_bytes,
373            self.exit_code
374                .map(|code| code.to_string())
375                .unwrap_or_else(|| "-".to_string()),
376            status,
377            failure_class,
378        )
379    }
380}
381
382pub const INVOCATION_METRICS_AGGREGATE_SCHEMA_VERSION: u32 = 1;
383pub const MAX_AGGREGATED_INVOCATIONS: usize = 1_000_000;
384
385/// Bounded, canonical aggregation of process-client observations.
386#[derive(Clone, Debug, Eq, PartialEq)]
387#[non_exhaustive]
388pub struct InvocationMetricsAggregate {
389    pub schema_version: u32,
390    pub jobs: u64,
391    pub successes: u64,
392    pub failures: u64,
393    pub total_duration_ns: u128,
394    pub maximum_duration_ns: u128,
395    pub total_stdout_bytes: u128,
396    pub total_stderr_bytes: u128,
397    pub process_group_contained_jobs: u64,
398    pub memory_limited_jobs: u64,
399    pub operation_counts: BTreeMap<&'static str, u64>,
400    pub failure_counts: BTreeMap<&'static str, u64>,
401}
402
403impl InvocationMetricsAggregate {
404    pub const fn csv_header() -> &'static str {
405        "schema_version,jobs,successes,failures,total_duration_ns,maximum_duration_ns,total_stdout_bytes,total_stderr_bytes,process_group_contained_jobs,memory_limited_jobs,operation_counts,failure_counts"
406    }
407
408    pub fn to_csv_row(&self) -> String {
409        let counts = |values: &BTreeMap<&'static str, u64>| {
410            if values.is_empty() {
411                "none".to_string()
412            } else {
413                values
414                    .iter()
415                    .map(|(name, count)| format!("{name}={count}"))
416                    .collect::<Vec<_>>()
417                    .join(";")
418            }
419        };
420        format!(
421            "{},{},{},{},{},{},{},{},{},{},{},{}",
422            self.schema_version,
423            self.jobs,
424            self.successes,
425            self.failures,
426            self.total_duration_ns,
427            self.maximum_duration_ns,
428            self.total_stdout_bytes,
429            self.total_stderr_bytes,
430            self.process_group_contained_jobs,
431            self.memory_limited_jobs,
432            counts(&self.operation_counts),
433            counts(&self.failure_counts),
434        )
435    }
436}
437
438#[derive(Clone, Copy, Debug, Eq, PartialEq)]
439#[non_exhaustive]
440pub enum MetricsAggregationError {
441    Empty,
442    TooManyJobs,
443    UnsupportedSchema(u32),
444    Overflow,
445}
446
447impl fmt::Display for MetricsAggregationError {
448    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
449        match self {
450            Self::Empty => write!(formatter, "invocation metrics set is empty"),
451            Self::TooManyJobs => write!(
452                formatter,
453                "invocation metrics set exceeds {MAX_AGGREGATED_INVOCATIONS} jobs"
454            ),
455            Self::UnsupportedSchema(version) => {
456                write!(formatter, "unsupported invocation metrics schema {version}")
457            }
458            Self::Overflow => write!(formatter, "invocation metrics aggregate overflows"),
459        }
460    }
461}
462
463impl std::error::Error for MetricsAggregationError {}
464
465/// Aggregate process observations without dropping failed jobs.
466pub fn aggregate_invocation_metrics<'a>(
467    metrics: impl IntoIterator<Item = &'a InvocationMetrics>,
468) -> Result<InvocationMetricsAggregate, MetricsAggregationError> {
469    let mut aggregate = InvocationMetricsAggregate {
470        schema_version: INVOCATION_METRICS_AGGREGATE_SCHEMA_VERSION,
471        jobs: 0,
472        successes: 0,
473        failures: 0,
474        total_duration_ns: 0,
475        maximum_duration_ns: 0,
476        total_stdout_bytes: 0,
477        total_stderr_bytes: 0,
478        process_group_contained_jobs: 0,
479        memory_limited_jobs: 0,
480        operation_counts: BTreeMap::new(),
481        failure_counts: BTreeMap::new(),
482    };
483    for metric in metrics {
484        if metric.schema_version != INVOCATION_METRICS_SCHEMA_VERSION {
485            return Err(MetricsAggregationError::UnsupportedSchema(
486                metric.schema_version,
487            ));
488        }
489        if aggregate.jobs as usize == MAX_AGGREGATED_INVOCATIONS {
490            return Err(MetricsAggregationError::TooManyJobs);
491        }
492        aggregate.jobs = aggregate
493            .jobs
494            .checked_add(1)
495            .ok_or(MetricsAggregationError::Overflow)?;
496        let duration = metric.duration.as_nanos();
497        aggregate.total_duration_ns = aggregate
498            .total_duration_ns
499            .checked_add(duration)
500            .ok_or(MetricsAggregationError::Overflow)?;
501        aggregate.maximum_duration_ns = aggregate.maximum_duration_ns.max(duration);
502        aggregate.total_stdout_bytes = aggregate
503            .total_stdout_bytes
504            .checked_add(metric.stdout_bytes as u128)
505            .ok_or(MetricsAggregationError::Overflow)?;
506        aggregate.total_stderr_bytes = aggregate
507            .total_stderr_bytes
508            .checked_add(metric.stderr_bytes as u128)
509            .ok_or(MetricsAggregationError::Overflow)?;
510        if metric.process_group_containment {
511            aggregate.process_group_contained_jobs = aggregate
512                .process_group_contained_jobs
513                .checked_add(1)
514                .ok_or(MetricsAggregationError::Overflow)?;
515        }
516        if metric.memory_limit_bytes.is_some() {
517            aggregate.memory_limited_jobs = aggregate
518                .memory_limited_jobs
519                .checked_add(1)
520                .ok_or(MetricsAggregationError::Overflow)?;
521        }
522        let operation_count = aggregate
523            .operation_counts
524            .entry(metric.operation.as_str())
525            .or_default();
526        *operation_count = operation_count
527            .checked_add(1)
528            .ok_or(MetricsAggregationError::Overflow)?;
529        match metric.status {
530            InvocationStatus::Success => {
531                aggregate.successes = aggregate
532                    .successes
533                    .checked_add(1)
534                    .ok_or(MetricsAggregationError::Overflow)?;
535            }
536            InvocationStatus::Failed(class) => {
537                aggregate.failures = aggregate
538                    .failures
539                    .checked_add(1)
540                    .ok_or(MetricsAggregationError::Overflow)?;
541                let failure_count = aggregate.failure_counts.entry(class.as_str()).or_default();
542                *failure_count = failure_count
543                    .checked_add(1)
544                    .ok_or(MetricsAggregationError::Overflow)?;
545            }
546        }
547    }
548    if aggregate.jobs == 0 {
549        return Err(MetricsAggregationError::Empty);
550    }
551    if aggregate.successes.checked_add(aggregate.failures) != Some(aggregate.jobs) {
552        return Err(MetricsAggregationError::Overflow);
553    }
554    Ok(aggregate)
555}
556
557#[derive(Clone, Debug, Eq, PartialEq)]
558pub struct Observed<T> {
559    pub value: T,
560    pub metrics: InvocationMetrics,
561}
562
563/// A supported predicate certificate encoding.
564#[derive(Clone, Copy, Debug, Eq, PartialEq)]
565pub enum CertificateVersion {
566    V1,
567    V2,
568}
569
570impl CertificateVersion {
571    fn producer_command(self) -> &'static str {
572        match self {
573            Self::V1 => "certify-aiger-predicate",
574            Self::V2 => "certify-aiger-predicate-v2",
575        }
576    }
577
578    fn verifier_command(self) -> &'static str {
579        match self {
580            Self::V1 => "verify-aiger-predicate-certificate",
581            Self::V2 => "verify-aiger-predicate-certificate-v2",
582        }
583    }
584
585    fn certify_operation(self) -> OperationKind {
586        match self {
587            Self::V1 => OperationKind::CertifyV1,
588            Self::V2 => OperationKind::CertifyV2,
589        }
590    }
591
592    fn verify_operation(self) -> OperationKind {
593        match self {
594            Self::V1 => OperationKind::VerifyV1,
595            Self::V2 => OperationKind::VerifyV2,
596        }
597    }
598}
599
600/// The logical result carried by a successfully checked certificate.
601#[derive(Clone, Copy, Debug, Eq, PartialEq)]
602pub enum PredicateResult {
603    Avoidable,
604    Unavoidable,
605}
606
607/// Machine-discovered limits and formats for a compatible executable.
608#[derive(Clone, Debug, Eq, PartialEq)]
609pub struct PredicateCapabilities {
610    pub cli_version: u32,
611    pub certificate_versions: Vec<CertificateVersion>,
612    pub portfolio_certificate_version: CertificateVersion,
613    pub proof_format: String,
614    pub min_relevant_inputs: usize,
615    pub max_relevant_inputs: usize,
616    pub max_latches: usize,
617    pub max_horizon: usize,
618    pub max_certificate_v2_bytes: u64,
619    pub max_proof_bytes: usize,
620    pub max_total_proof_bytes: usize,
621}
622
623/// Machine-discovered limits and formats for event-contract CLI v1.
624#[derive(Clone, Debug, Eq, PartialEq)]
625pub struct EventContractCapabilities {
626    pub cli_version: u32,
627    pub certificate_version: u32,
628    pub portfolio_version: u32,
629    pub semantics: String,
630    pub proof_format: String,
631    pub min_relevant_inputs: usize,
632    pub max_relevant_inputs: usize,
633    pub max_latches: usize,
634    pub max_horizon: usize,
635    pub max_contract_bytes: u64,
636    pub max_certificate_bytes: u64,
637    pub max_proof_bytes: usize,
638    pub max_total_proof_bytes: usize,
639}
640
641/// Machine-discovered limits for controller MTBDD plant CLI v1.
642#[derive(Clone, Debug, Eq, PartialEq)]
643pub struct ControllerMtbddCapabilities {
644    pub cli_version: u32,
645    pub mtbdd_version: u32,
646    pub plant_artifact_version: u32,
647    pub manifest_version: u32,
648    pub max_manifest_bytes: usize,
649    pub max_artifact_bytes: usize,
650    pub max_members: usize,
651    pub max_state_bits: usize,
652    pub max_inputs: usize,
653    pub max_outputs: usize,
654    pub max_nodes: usize,
655    pub max_terminals: usize,
656    pub max_assignments: usize,
657    pub max_horizon: usize,
658}
659
660/// Machine-discovered limits and semantics for revision-impact CLI v2.
661#[derive(Clone, Debug, Eq, PartialEq)]
662pub struct RevisionImpactCapabilities {
663    pub cli_version: u32,
664    pub impact_version: u32,
665    pub query_manifest_version: u32,
666    pub max_query_manifest_bytes: usize,
667    pub max_input_bytes: usize,
668    pub max_evidence_bytes: usize,
669    pub max_bundle_bytes: usize,
670    pub max_atoms: usize,
671    pub max_combinations: usize,
672    pub max_queries: usize,
673}
674
675/// Paths and projected output nodes bound into one revision-impact job.
676#[derive(Clone, Debug)]
677pub struct RevisionImpactFiles<'a> {
678    pub left_old: &'a Path,
679    pub left_new: &'a Path,
680    pub left_outputs: &'a [u64],
681    pub right_old: &'a Path,
682    pub right_new: &'a Path,
683    pub right_outputs: &'a [u64],
684    pub interface_old: &'a Path,
685    pub interface_new: &'a Path,
686    pub queries: &'a Path,
687}
688
689/// Canonical summary returned by revision-impact production or verification.
690#[derive(Clone, Debug, Eq, PartialEq)]
691pub struct RevisionImpactProcessSummary {
692    pub impact_version: u32,
693    pub atoms: usize,
694    pub queries: usize,
695    pub combinations: usize,
696    pub reusable_observations: usize,
697    pub invalidated_observations: usize,
698    pub minimal_invalidating_sets: usize,
699    pub minimal_semantic_change_sets: usize,
700    pub evidence_members: usize,
701    pub certificate_bytes: usize,
702    pub parsed_evidence_bytes: usize,
703    pub semantic_replays: usize,
704    pub component_validations: usize,
705    pub composed_pair_checks: usize,
706    pub final_transition_checks: usize,
707    pub result_comparisons: usize,
708    pub elapsed_micros: usize,
709    pub transitions: Vec<RevisionImpactQueryTransition>,
710    pub semantic_change_sets: Vec<RevisionImpactSemanticChangeSet>,
711}
712
713#[derive(Clone, Debug, Eq, PartialEq)]
714pub struct RevisionImpactQueryTransition {
715    pub index: usize,
716    pub horizon: u32,
717    pub bad_side: revision_local::ComponentSide,
718    pub bad_output: u64,
719    pub old_result: revision_local::BoundedResult,
720    pub new_result: revision_local::BoundedResult,
721}
722
723#[derive(Clone, Debug, Eq, PartialEq)]
724pub struct RevisionImpactSemanticChangeSet {
725    pub query_index: usize,
726    pub changed_mask: u16,
727    pub baseline_result: revision_local::BoundedResult,
728    pub changed_result: revision_local::BoundedResult,
729}
730
731#[derive(Clone, Debug, Eq, PartialEq)]
732pub struct Btor2ChannelPropertyCapabilities {
733    pub cli_version: u32,
734    pub artifact_version: u32,
735    pub query_manifest_version: u32,
736    pub policy_version: u32,
737    pub max_query_manifest_bytes: usize,
738    pub max_policy_bytes: usize,
739    pub max_model_bytes: usize,
740    pub max_channels: usize,
741    pub max_queries: usize,
742    pub max_evidence_bytes: usize,
743    pub max_artifact_bytes: usize,
744    pub max_projected_work: u64,
745    pub refusal_exit_code: i32,
746}
747
748#[derive(Clone, Debug)]
749pub struct Btor2ChannelPropertyFiles<'a> {
750    pub model: &'a Path,
751    pub queries: &'a Path,
752    pub policy: &'a Path,
753}
754
755#[derive(Clone, Copy, Debug, Eq, PartialEq)]
756pub enum Btor2ChannelPropertyAnswer {
757    Safe,
758    Unsafe,
759}
760
761#[derive(Clone, Copy, Debug, Eq, PartialEq)]
762pub enum Btor2ChannelPropertyKind {
763    OutputHigh,
764    OutputLow,
765}
766
767#[derive(Clone, Copy, Debug, Eq, PartialEq)]
768pub enum Btor2ChannelPropertyProcessBackend {
769    RepresentativeClass,
770    DirectExact,
771}
772
773#[derive(Clone, Copy, Debug, Eq, PartialEq)]
774pub enum Btor2ChannelPropertyProcessSolver {
775    ExplicitState,
776    BitblastCnf,
777}
778
779#[derive(Clone, Debug, Eq, PartialEq)]
780pub struct Btor2ChannelPropertyProcessResult {
781    pub index: usize,
782    pub query_id: u32,
783    pub channel: usize,
784    pub property: Btor2ChannelPropertyKind,
785    pub horizon: u32,
786    pub answer: Btor2ChannelPropertyAnswer,
787    pub bad_frame: Option<u32>,
788    pub backend: Btor2ChannelPropertyProcessBackend,
789    pub solver: Btor2ChannelPropertyProcessSolver,
790    pub representative_channel: usize,
791    pub witness_valuations: usize,
792}
793
794#[derive(Clone, Debug, Eq, PartialEq)]
795pub struct Btor2ChannelPropertyProcessSummary {
796    pub artifact_version: u32,
797    pub channels: usize,
798    pub logical_queries: usize,
799    pub proof_members: usize,
800    pub reused_queries: usize,
801    pub explicit_members: usize,
802    pub bitblast_members: usize,
803    pub evidence_bytes: usize,
804    pub artifact_bytes: usize,
805    pub projected_work: Option<u64>,
806    pub elapsed_micros: usize,
807    pub results: Vec<Btor2ChannelPropertyProcessResult>,
808}
809
810#[derive(Clone, Copy, Debug, Eq, PartialEq)]
811pub struct Btor2ChannelPropertyObservabilityCapabilities {
812    pub cli_version: u32,
813    pub phase_metrics_version: u32,
814}
815
816#[derive(Clone, Copy, Debug, Eq, PartialEq)]
817pub enum Btor2ChannelPropertyObservedOperation {
818    Certify,
819    Verify,
820}
821
822#[derive(Clone, Copy, Debug, Eq, PartialEq)]
823pub struct Btor2ChannelPropertyPhaseMetrics {
824    pub version: u32,
825    pub operation: Btor2ChannelPropertyObservedOperation,
826    pub input_micros: u128,
827    pub structural_admission_micros: u128,
828    pub preflight_micros: u128,
829    pub proof_construction_micros: u128,
830    pub encoding_micros: u128,
831    pub artifact_decode_micros: u128,
832    pub source_replay_micros: u128,
833    pub publication_micros: u128,
834    pub total_micros: u128,
835}
836
837#[derive(Clone, Debug, Eq, PartialEq)]
838pub struct Btor2ChannelPropertyPhaseObservedSummary {
839    pub verification: Btor2ChannelPropertyProcessSummary,
840    pub phases: Btor2ChannelPropertyPhaseMetrics,
841}
842
843#[derive(Clone, Copy, Debug, Eq, PartialEq)]
844pub enum Btor2ChannelPropertyResourceRefusalReason {
845    QueryCount,
846    MemberCount,
847    ProjectedWork,
848    EvidenceBytes,
849    ArtifactBytes,
850}
851
852impl Btor2ChannelPropertyResourceRefusalReason {
853    pub fn as_str(self) -> &'static str {
854        match self {
855            Self::QueryCount => "query-count",
856            Self::MemberCount => "member-count",
857            Self::ProjectedWork => "projected-work",
858            Self::EvidenceBytes => "evidence-bytes",
859            Self::ArtifactBytes => "artifact-bytes",
860        }
861    }
862}
863
864/// Machine-discovered limits for proof-carrying controller MTBDD CLI v1.
865#[derive(Clone, Debug, Eq, PartialEq)]
866pub struct ControllerProofMtbddCapabilities {
867    pub cli_version: u32,
868    pub mtbdd_version: u32,
869    pub equivalence_proof_version: u32,
870    pub plant_artifact_version: u32,
871    pub manifest_version: u32,
872    pub max_manifest_bytes: usize,
873    pub max_artifact_bytes: usize,
874    pub max_equivalence_artifact_bytes: usize,
875    pub max_unsat_proof_bytes: usize,
876    pub max_members: usize,
877    pub max_state_bits: usize,
878    pub max_inputs: usize,
879    pub max_outputs: usize,
880    pub max_nodes: usize,
881    pub max_terminals: usize,
882    pub max_horizon: usize,
883}
884
885impl ControllerProofMtbddCapabilities {
886    fn common(&self) -> ControllerMtbddCapabilities {
887        ControllerMtbddCapabilities {
888            cli_version: self.cli_version,
889            mtbdd_version: self.mtbdd_version,
890            plant_artifact_version: self.plant_artifact_version,
891            manifest_version: self.manifest_version,
892            max_manifest_bytes: self.max_manifest_bytes,
893            max_artifact_bytes: self.max_artifact_bytes,
894            max_members: self.max_members,
895            max_state_bits: self.max_state_bits,
896            max_inputs: self.max_inputs,
897            max_outputs: self.max_outputs,
898            max_nodes: self.max_nodes,
899            max_terminals: self.max_terminals,
900            max_assignments: 0,
901            max_horizon: self.max_horizon,
902        }
903    }
904}
905
906#[derive(Clone, Debug, Eq, PartialEq)]
907pub struct ControllerSplitEvidenceCapabilities {
908    pub cli_version: u32,
909    pub controller_artifact_version: u32,
910    pub plant_artifact_version: u32,
911    pub manifest_version: u32,
912    pub max_manifest_bytes: usize,
913    pub max_artifact_bytes: usize,
914    pub max_batches: usize,
915}
916
917#[derive(Clone, Debug, Eq, PartialEq)]
918pub struct ControllerSplitArtifactSummary {
919    pub artifact_version: u32,
920    pub members: Option<usize>,
921    pub mtbdd_nodes: Option<usize>,
922    pub mtbdd_terminals: Option<usize>,
923    pub artifact_bytes: usize,
924    pub elapsed_micros: usize,
925}
926
927#[derive(Clone, Debug, Eq, PartialEq)]
928pub struct ControllerSplitBatchSummary {
929    pub index: usize,
930    pub members: usize,
931    pub safe: usize,
932    pub unsafe_count: usize,
933    pub reachable_product_states: usize,
934    pub explored_transitions: usize,
935    pub artifact_bytes: usize,
936    pub verification_micros: usize,
937}
938
939#[derive(Clone, Debug, Eq, PartialEq)]
940pub struct ControllerSplitSetSummary {
941    pub controller_admissions: usize,
942    pub members: usize,
943    pub safe: usize,
944    pub unsafe_count: usize,
945    pub reachable_product_states: usize,
946    pub explored_transitions: usize,
947    pub controller_evidence_bytes: usize,
948    pub admission_micros: usize,
949    pub elapsed_micros: usize,
950    pub batches: Vec<ControllerSplitBatchSummary>,
951}
952
953#[derive(Clone, Debug, Eq, PartialEq)]
954pub struct ControllerSplitResourceCapabilities {
955    pub cli_version: u32,
956    pub policy_version: u32,
957    pub controller_envelope_version: u32,
958    pub plant_envelope_version: u32,
959    pub controller_artifact_version: u32,
960    pub plant_artifact_version: u32,
961    pub manifest_version: u32,
962    pub max_policy_bytes: usize,
963    pub max_controller_artifact_bytes: usize,
964    pub max_unsat_proof_bytes: usize,
965    pub max_plant_artifact_bytes: usize,
966    pub max_batches: usize,
967    pub max_members_per_batch: usize,
968    pub max_horizon: usize,
969    pub max_product_states: usize,
970    pub refusal_exit_code: i32,
971}
972
973#[derive(Clone, Debug, Eq, PartialEq)]
974pub struct ControllerSplitResourceBatchSummary {
975    pub index: usize,
976    pub members: usize,
977    pub maximum_member_horizon: usize,
978    pub maximum_product_states: usize,
979    pub transition_evaluation_bound: usize,
980    pub safe: usize,
981    pub unsafe_count: usize,
982    pub reachable_product_states: usize,
983    pub explored_transitions: usize,
984    pub artifact_bytes: usize,
985    pub verification_micros: usize,
986}
987
988#[derive(Clone, Debug, Eq, PartialEq)]
989pub struct ControllerSplitResourceSetSummary {
990    pub controller_admissions: usize,
991    pub members: usize,
992    pub safe: usize,
993    pub unsafe_count: usize,
994    pub reachable_product_states: usize,
995    pub explored_transitions: usize,
996    pub controller_evidence_bytes: usize,
997    pub controller_mtbdd_bytes: usize,
998    pub equivalence_artifact_bytes: usize,
999    pub unsat_proof_bytes: usize,
1000    pub total_plant_artifact_bytes: usize,
1001    pub total_transition_evaluation_bound: usize,
1002    pub admission_micros: usize,
1003    pub elapsed_micros: usize,
1004    pub batches: Vec<ControllerSplitResourceBatchSummary>,
1005}
1006
1007#[derive(Clone, Debug, Eq, PartialEq)]
1008pub struct ControllerSplitObservabilityCapabilities {
1009    pub cli_version: u32,
1010    pub phase_metrics_version: u32,
1011    pub resource: ControllerSplitResourceCapabilities,
1012}
1013
1014#[derive(Clone, Debug, Eq, PartialEq)]
1015pub struct ControllerSplitPhaseMetrics {
1016    pub version: u32,
1017    pub policy_and_input_micros: u128,
1018    pub controller_admission_micros: u128,
1019    pub complete_set_preflight_micros: u128,
1020    pub semantic_replay_micros: u128,
1021    pub total_micros: u128,
1022    pub controller_admissions: usize,
1023    pub manifest_loads: usize,
1024    pub plant_artifact_reads: usize,
1025    pub resource_assessments: usize,
1026    pub batch_verifications: usize,
1027    pub buffered_result_rows: usize,
1028    pub prepared_batches: usize,
1029    pub prepared_members: usize,
1030    pub controller_evidence_bytes: usize,
1031    pub total_plant_artifact_bytes: usize,
1032    pub total_transition_evaluation_bound: usize,
1033}
1034
1035#[derive(Clone, Debug, Eq, PartialEq)]
1036pub struct ControllerSplitObservedSummary {
1037    pub verification: ControllerSplitResourceSetSummary,
1038    pub phases: ControllerSplitPhaseMetrics,
1039}
1040
1041#[derive(Clone, Debug, Eq, PartialEq)]
1042pub struct ControllerSplitAllocationObservabilityCapabilities {
1043    pub cli_version: u32,
1044    pub observability: ControllerSplitObservabilityCapabilities,
1045}
1046
1047#[derive(Clone, Debug, Eq, PartialEq)]
1048pub struct ControllerSplitAllocationMetrics {
1049    pub version: u32,
1050    pub allocation_calls: u64,
1051    pub allocated_bytes: u64,
1052    pub deallocation_calls: u64,
1053    pub deallocated_bytes: u64,
1054    pub reallocation_calls: u64,
1055    pub reallocated_bytes: u64,
1056}
1057
1058#[derive(Clone, Debug, Eq, PartialEq)]
1059pub struct ControllerSplitAllocationObservedSummary {
1060    pub observed: ControllerSplitObservedSummary,
1061    pub allocations: ControllerSplitAllocationMetrics,
1062}
1063
1064#[derive(Clone, Debug, Eq, PartialEq)]
1065pub struct ControllerSplitCacheObservabilityCapabilities {
1066    pub cli_version: u32,
1067    pub allocation_observability: ControllerSplitAllocationObservabilityCapabilities,
1068}
1069
1070#[derive(Clone, Debug, Eq, PartialEq)]
1071pub struct ControllerSplitCacheMetrics {
1072    pub version: u32,
1073    pub lookups: usize,
1074    pub hits: usize,
1075    pub misses: usize,
1076    pub entries: usize,
1077}
1078
1079#[derive(Clone, Debug, Eq, PartialEq)]
1080pub struct ControllerSplitCacheObservedSummary {
1081    pub observed: ControllerSplitAllocationObservedSummary,
1082    pub cache: ControllerSplitCacheMetrics,
1083}
1084
1085#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1086pub enum ControllerMtbddAnswer {
1087    Safe,
1088    Unsafe,
1089}
1090
1091#[derive(Clone, Debug, Eq, PartialEq)]
1092pub struct ControllerMtbddMemberResult {
1093    pub index: usize,
1094    pub answer: ControllerMtbddAnswer,
1095    pub horizon: usize,
1096    pub bad_frame: Option<usize>,
1097    pub trace_steps: usize,
1098    pub reachable_product_states: usize,
1099    pub explored_transitions: usize,
1100}
1101
1102#[derive(Clone, Debug, Eq, PartialEq)]
1103pub struct ControllerMtbddBatchSummary {
1104    pub artifact_version: u32,
1105    pub safe: usize,
1106    pub unsafe_count: usize,
1107    pub mtbdd_nodes: usize,
1108    pub mtbdd_terminals: usize,
1109    pub assignments_checked: usize,
1110    pub reachable_product_states: usize,
1111    pub explored_transitions: usize,
1112    pub artifact_bytes: usize,
1113    pub elapsed_micros: usize,
1114    pub members: Vec<ControllerMtbddMemberResult>,
1115}
1116
1117pub type ControllerMtbddApiError = PredicateApiError;
1118pub type ControllerMtbddOperationError = PredicateOperationError;
1119
1120#[derive(Clone, Debug, Eq, PartialEq)]
1121pub struct ControllerPlantPortfolioCapabilities {
1122    pub cli_version: u32,
1123    pub artifact_version: u32,
1124    pub manifest_version: u32,
1125    pub max_manifest_bytes: usize,
1126    pub max_artifact_bytes: usize,
1127    pub max_members: usize,
1128}
1129
1130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1131pub enum ControllerPlantPortfolioBackend {
1132    Mtbdd,
1133    DirectExact,
1134}
1135
1136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1137pub enum ControllerPlantPortfolioReason {
1138    MtbddAdmitted,
1139    BoundaryLimit,
1140    TerminalLimit,
1141    NodeLimit,
1142}
1143
1144#[derive(Clone, Debug, Eq, PartialEq)]
1145pub struct ControllerPlantPortfolioBatchSummary {
1146    pub artifact_version: u32,
1147    pub backend: ControllerPlantPortfolioBackend,
1148    pub reason: ControllerPlantPortfolioReason,
1149    pub safe: usize,
1150    pub unsafe_count: usize,
1151    pub reachable_product_states: usize,
1152    pub explored_transitions: usize,
1153    pub artifact_bytes: usize,
1154    pub load_micros: usize,
1155    pub artifact_micros: usize,
1156    pub verification_micros: usize,
1157    pub write_micros: usize,
1158    pub elapsed_micros: usize,
1159    pub members: Vec<ControllerMtbddMemberResult>,
1160}
1161
1162pub type ControllerPlantPortfolioApiError = PredicateApiError;
1163pub type ControllerPlantPortfolioOperationError = PredicateOperationError;
1164
1165#[derive(Clone, Debug, Eq, PartialEq)]
1166pub struct ControllerPlantResourceCapabilities {
1167    pub cli_version: u32,
1168    pub policy_version: u32,
1169    pub envelope_version: u32,
1170    pub manifest_version: u32,
1171    pub portfolio_artifact_version: u32,
1172    pub max_policy_bytes: usize,
1173    pub max_artifact_bytes: usize,
1174    pub max_members: usize,
1175    pub max_horizon: usize,
1176    pub max_product_states: usize,
1177    pub refusal_exit_code: i32,
1178}
1179
1180#[derive(Clone, Debug, Eq, PartialEq)]
1181pub struct ControllerProofMtbddResourceCapabilities {
1182    pub cli_version: u32,
1183    pub policy_version: u32,
1184    pub envelope_version: u32,
1185    pub manifest_version: u32,
1186    pub artifact_version: u32,
1187    pub max_policy_bytes: usize,
1188    pub max_artifact_bytes: usize,
1189    pub max_equivalence_artifact_bytes: usize,
1190    pub max_unsat_proof_bytes: usize,
1191    pub max_members: usize,
1192    pub max_horizon: usize,
1193    pub max_product_states: usize,
1194    pub refusal_exit_code: i32,
1195}
1196
1197#[derive(Clone, Debug, Eq, PartialEq)]
1198pub struct ControllerProofMtbddResourceSummary {
1199    pub policy_version: u32,
1200    pub envelope_version: u32,
1201    pub artifact_version: u32,
1202    pub members: usize,
1203    pub maximum_member_horizon: usize,
1204    pub maximum_product_states: usize,
1205    pub transition_evaluation_bound: usize,
1206    pub equivalence_artifact_bytes: usize,
1207    pub unsat_proof_bytes: usize,
1208    pub safe: usize,
1209    pub unsafe_count: usize,
1210    pub reachable_product_states: usize,
1211    pub explored_transitions: usize,
1212    pub artifact_bytes: usize,
1213    pub assignments_checked: usize,
1214    pub load_micros: usize,
1215    pub artifact_micros: usize,
1216    pub verification_micros: usize,
1217    pub elapsed_micros: usize,
1218    pub member_results: Vec<ControllerMtbddMemberResult>,
1219}
1220
1221#[derive(Clone, Debug, Eq, PartialEq)]
1222pub struct ControllerProofMtbddPortfolioCapabilities {
1223    pub cli_version: u32,
1224    pub policy_version: u32,
1225    pub envelope_version: u32,
1226    pub artifact_version: u32,
1227    pub proof_artifact_version: u32,
1228    pub direct_artifact_version: u32,
1229    pub manifest_version: u32,
1230    pub source_model_attestation_version: u32,
1231    pub max_policy_bytes: usize,
1232    pub max_artifact_bytes: usize,
1233    pub max_equivalence_artifact_bytes: usize,
1234    pub max_unsat_proof_bytes: usize,
1235    pub max_members: usize,
1236    pub max_horizon: usize,
1237    pub max_product_states: usize,
1238    pub max_attestation_bytes: usize,
1239    pub refusal_exit_code: i32,
1240}
1241
1242#[derive(Clone, Debug, Eq, PartialEq)]
1243pub struct ControllerProofMtbddPortfolioResourceSummary {
1244    pub policy_version: u32,
1245    pub envelope_version: u32,
1246    pub artifact_version: u32,
1247    pub backend: ControllerPlantPortfolioBackend,
1248    pub reason: ControllerPlantPortfolioReason,
1249    pub members: usize,
1250    pub maximum_member_horizon: usize,
1251    pub maximum_product_states: usize,
1252    pub transition_evaluation_bound: usize,
1253    pub equivalence_artifact_bytes: usize,
1254    pub unsat_proof_bytes: usize,
1255    pub safe: usize,
1256    pub unsafe_count: usize,
1257    pub reachable_product_states: usize,
1258    pub explored_transitions: usize,
1259    pub artifact_bytes: usize,
1260    pub assignments_checked: usize,
1261    pub load_micros: usize,
1262    pub artifact_micros: usize,
1263    pub verification_micros: usize,
1264    pub elapsed_micros: usize,
1265    pub member_results: Vec<ControllerMtbddMemberResult>,
1266}
1267
1268#[derive(Clone, Debug, Eq, PartialEq)]
1269pub struct ControllerPlantResourceSummary {
1270    pub policy_version: u32,
1271    pub envelope_version: u32,
1272    pub artifact_version: u32,
1273    pub backend: ControllerPlantPortfolioBackend,
1274    pub members: usize,
1275    pub maximum_member_horizon: usize,
1276    pub maximum_product_states: usize,
1277    pub transition_evaluation_bound: usize,
1278    pub safe: usize,
1279    pub unsafe_count: usize,
1280    pub reachable_product_states: usize,
1281    pub explored_transitions: usize,
1282    pub artifact_bytes: usize,
1283    pub load_micros: usize,
1284    pub artifact_micros: usize,
1285    pub verification_micros: usize,
1286    pub elapsed_micros: usize,
1287    pub member_results: Vec<ControllerMtbddMemberResult>,
1288}
1289
1290#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1291pub enum ControllerPlantResourceRefusalReason {
1292    ArtifactBytes,
1293    ControllerArtifactBytes,
1294    PlantArtifactBytes,
1295    EquivalenceArtifactBytes,
1296    UnsatProofBytes,
1297    Batches,
1298    Members,
1299    MembersPerBatch,
1300    Horizon,
1301    ProductStates,
1302    TransitionEvaluations,
1303    TransitionsPerBatch,
1304    TotalPlantArtifactBytes,
1305    TotalMembers,
1306    TotalTransitionEvaluations,
1307}
1308
1309impl ControllerPlantResourceRefusalReason {
1310    pub fn as_str(self) -> &'static str {
1311        match self {
1312            Self::ArtifactBytes => "artifact-bytes",
1313            Self::ControllerArtifactBytes => "controller-artifact-bytes",
1314            Self::PlantArtifactBytes => "plant-artifact-bytes",
1315            Self::EquivalenceArtifactBytes => "equivalence-artifact-bytes",
1316            Self::UnsatProofBytes => "unsat-proof-bytes",
1317            Self::Batches => "batches",
1318            Self::Members => "members",
1319            Self::MembersPerBatch => "members-per-batch",
1320            Self::Horizon => "horizon",
1321            Self::ProductStates => "product-states",
1322            Self::TransitionEvaluations => "transition-evaluations",
1323            Self::TransitionsPerBatch => "transitions-per-batch",
1324            Self::TotalPlantArtifactBytes => "total-plant-artifact-bytes",
1325            Self::TotalMembers => "total-members",
1326            Self::TotalTransitionEvaluations => "total-transition-evaluations",
1327        }
1328    }
1329}
1330
1331pub type ControllerPlantResourceApiError = PredicateApiError;
1332pub type ControllerPlantResourceOperationError = PredicateOperationError;
1333
1334/// Event-contract v1 uses the same two logical outcomes as predicate checks.
1335pub type EventContractResult = PredicateResult;
1336pub type EventContractApiError = PredicateApiError;
1337pub type EventContractOperationError = PredicateOperationError;
1338
1339/// A stable API error. Logical certificate results are not errors.
1340#[derive(Debug)]
1341pub enum PredicateApiError {
1342    Io(std::io::Error),
1343    InvalidPolicy(String),
1344    TimedOut {
1345        timeout: Duration,
1346    },
1347    OutputLimitExceeded {
1348        stream: &'static str,
1349        limit_bytes: usize,
1350    },
1351    CommandFailed {
1352        exit_code: Option<i32>,
1353        stderr: String,
1354    },
1355    ResourceRefused {
1356        reason: ControllerPlantResourceRefusalReason,
1357    },
1358    Btor2ChannelPropertyResourceRefused {
1359        reason: Btor2ChannelPropertyResourceRefusalReason,
1360    },
1361    IncompatibleContract(String),
1362    InvalidResponse(String),
1363}
1364
1365impl PredicateApiError {
1366    pub fn failure_class(&self) -> FailureClass {
1367        match self {
1368            Self::InvalidPolicy(_) => FailureClass::Policy,
1369            Self::Io(_) => FailureClass::Io,
1370            Self::TimedOut { .. } => FailureClass::Timeout,
1371            Self::OutputLimitExceeded { .. } => FailureClass::OutputLimit,
1372            Self::CommandFailed { .. } => FailureClass::ExitStatus,
1373            Self::ResourceRefused { .. } | Self::Btor2ChannelPropertyResourceRefused { .. } => {
1374                FailureClass::ResourceRefusal
1375            }
1376            Self::IncompatibleContract(_) => FailureClass::Compatibility,
1377            Self::InvalidResponse(_) => FailureClass::Response,
1378        }
1379    }
1380}
1381
1382#[derive(Debug)]
1383pub struct PredicateOperationError {
1384    pub error: Box<PredicateApiError>,
1385    pub metrics: InvocationMetrics,
1386}
1387
1388impl fmt::Display for PredicateOperationError {
1389    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1390        self.error.fmt(formatter)
1391    }
1392}
1393
1394impl std::error::Error for PredicateOperationError {
1395    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1396        Some(self.error.as_ref())
1397    }
1398}
1399
1400impl fmt::Display for PredicateApiError {
1401    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1402        match self {
1403            Self::Io(error) => write!(formatter, "predicate tool I/O failed: {error}"),
1404            Self::InvalidPolicy(message) => {
1405                write!(formatter, "invalid execution policy: {message}")
1406            }
1407            Self::TimedOut { timeout } => {
1408                write!(formatter, "predicate tool exceeded {timeout:?} deadline")
1409            }
1410            Self::OutputLimitExceeded {
1411                stream,
1412                limit_bytes,
1413            } => write!(
1414                formatter,
1415                "predicate tool {stream} exceeded {limit_bytes}-byte limit"
1416            ),
1417            Self::CommandFailed { exit_code, stderr } => write!(
1418                formatter,
1419                "predicate tool exited with {}: {}",
1420                exit_code
1421                    .map(|code| code.to_string())
1422                    .unwrap_or_else(|| "no status".to_string()),
1423                stderr.trim()
1424            ),
1425            Self::ResourceRefused { reason } => write!(
1426                formatter,
1427                "controller plant verification refused by resource policy: {}",
1428                reason.as_str()
1429            ),
1430            Self::Btor2ChannelPropertyResourceRefused { reason } => write!(
1431                formatter,
1432                "BTOR2 channel property production refused by resource policy: {}",
1433                reason.as_str()
1434            ),
1435            Self::IncompatibleContract(message) => {
1436                write!(formatter, "incompatible predicate contract: {message}")
1437            }
1438            Self::InvalidResponse(message) => {
1439                write!(formatter, "invalid predicate tool response: {message}")
1440            }
1441        }
1442    }
1443}
1444
1445impl std::error::Error for PredicateApiError {}
1446
1447impl From<std::io::Error> for PredicateApiError {
1448    fn from(error: std::io::Error) -> Self {
1449        Self::Io(error)
1450    }
1451}
1452
1453/// Typed, shell-free client for one CQ-SAT/GCC executable.
1454#[derive(Clone, Debug)]
1455pub struct PredicateTool {
1456    executable: PathBuf,
1457    capabilities: PredicateCapabilities,
1458    policy: ExecutionPolicy,
1459}
1460
1461impl PredicateTool {
1462    /// Discover and validate the executable's predicate CLI contract.
1463    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
1464        Self::discover_with_policy(executable, ExecutionPolicy::default())
1465    }
1466
1467    /// Discover with explicit runtime bounds for discovery and later jobs.
1468    pub fn discover_with_policy(
1469        executable: impl Into<PathBuf>,
1470        policy: ExecutionPolicy,
1471    ) -> Result<Self, PredicateApiError> {
1472        Self::discover_observed(executable, policy)
1473            .map(|observed| observed.value)
1474            .map_err(|failure| *failure.error)
1475    }
1476
1477    pub fn discover_observed(
1478        executable: impl Into<PathBuf>,
1479        policy: ExecutionPolicy,
1480    ) -> Result<Observed<Self>, PredicateOperationError> {
1481        let executable = executable.into();
1482        let mut command = Command::new(&executable);
1483        command.arg("predicate-cli-version");
1484        let output = run_bounded(OperationKind::Discover, command, policy)?;
1485        let (stdout, mut metrics) = successful_stdout(output)?;
1486        let capabilities = parse_capabilities(&stdout).map_err(|error| {
1487            metrics.status = InvocationStatus::Failed(error.failure_class());
1488            PredicateOperationError {
1489                error: Box::new(error),
1490                metrics: metrics.clone(),
1491            }
1492        })?;
1493        Ok(Observed {
1494            value: Self {
1495                executable,
1496                capabilities,
1497                policy,
1498            },
1499            metrics,
1500        })
1501    }
1502
1503    pub fn executable(&self) -> &Path {
1504        &self.executable
1505    }
1506
1507    pub fn capabilities(&self) -> &PredicateCapabilities {
1508        &self.capabilities
1509    }
1510
1511    pub fn execution_policy(&self) -> ExecutionPolicy {
1512        self.policy
1513    }
1514
1515    /// Return a handle with different validated bounds and unchanged capabilities.
1516    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1517        self.policy = policy;
1518        self
1519    }
1520
1521    /// Produce a deterministic certificate without overwriting `certificate`.
1522    pub fn certify(
1523        &self,
1524        version: CertificateVersion,
1525        model: &Path,
1526        bad_output: usize,
1527        transcript: &Path,
1528        certificate: &Path,
1529    ) -> Result<PredicateResult, PredicateApiError> {
1530        self.certify_observed(version, model, bad_output, transcript, certificate)
1531            .map(|observed| observed.value)
1532            .map_err(|failure| *failure.error)
1533    }
1534
1535    pub fn certify_observed(
1536        &self,
1537        version: CertificateVersion,
1538        model: &Path,
1539        bad_output: usize,
1540        transcript: &Path,
1541        certificate: &Path,
1542    ) -> Result<Observed<PredicateResult>, PredicateOperationError> {
1543        self.require_version_observed(version, version.certify_operation())?;
1544        let mut command = Command::new(&self.executable);
1545        command
1546            .arg(version.producer_command())
1547            .arg(model)
1548            .arg(bad_output.to_string())
1549            .arg(transcript)
1550            .arg(certificate);
1551        let output = run_bounded(version.certify_operation(), command, self.policy)?;
1552        parse_observed_result(output)
1553    }
1554
1555    /// Verify a certificate against the selected model and return its result.
1556    pub fn verify(
1557        &self,
1558        version: CertificateVersion,
1559        model: &Path,
1560        certificate: &Path,
1561    ) -> Result<PredicateResult, PredicateApiError> {
1562        self.verify_observed(version, model, certificate)
1563            .map(|observed| observed.value)
1564            .map_err(|failure| *failure.error)
1565    }
1566
1567    pub fn verify_observed(
1568        &self,
1569        version: CertificateVersion,
1570        model: &Path,
1571        certificate: &Path,
1572    ) -> Result<Observed<PredicateResult>, PredicateOperationError> {
1573        self.require_version_observed(version, version.verify_operation())?;
1574        let mut command = Command::new(&self.executable);
1575        command
1576            .arg(version.verifier_command())
1577            .arg(model)
1578            .arg(certificate);
1579        let output = run_bounded(version.verify_operation(), command, self.policy)?;
1580        parse_observed_result(output)
1581    }
1582
1583    fn require_version_observed(
1584        &self,
1585        version: CertificateVersion,
1586        operation: OperationKind,
1587    ) -> Result<(), PredicateOperationError> {
1588        if !self.capabilities.certificate_versions.contains(&version) {
1589            let error = PredicateApiError::IncompatibleContract(format!(
1590                "certificate version {version:?} is not advertised"
1591            ));
1592            return Err(PredicateOperationError {
1593                metrics: empty_metrics(
1594                    operation,
1595                    self.policy,
1596                    InvocationStatus::Failed(error.failure_class()),
1597                ),
1598                error: Box::new(error),
1599            });
1600        }
1601        Ok(())
1602    }
1603}
1604
1605/// Typed, shell-free client for revision-impact CLI v2.
1606#[derive(Clone, Debug)]
1607pub struct RevisionImpactTool {
1608    executable: PathBuf,
1609    capabilities: RevisionImpactCapabilities,
1610    policy: ExecutionPolicy,
1611}
1612
1613impl RevisionImpactTool {
1614    /// Discover and validate the executable contract with a file limit that
1615    /// admits the largest advertised v1 bundle.
1616    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
1617        let policy = ExecutionPolicy::default()
1618            .with_file_limit(revision_impact::MAX_REVISION_IMPACT_CERTIFICATE_BYTES as u64)?;
1619        Self::discover_with_policy(executable, policy)
1620    }
1621
1622    pub fn discover_with_policy(
1623        executable: impl Into<PathBuf>,
1624        policy: ExecutionPolicy,
1625    ) -> Result<Self, PredicateApiError> {
1626        Self::discover_observed(executable, policy)
1627            .map(|observed| observed.value)
1628            .map_err(|failure| *failure.error)
1629    }
1630
1631    pub fn discover_observed(
1632        executable: impl Into<PathBuf>,
1633        policy: ExecutionPolicy,
1634    ) -> Result<Observed<Self>, PredicateOperationError> {
1635        let executable = executable.into();
1636        let mut command = Command::new(&executable);
1637        command.arg("btor2-revision-impact-cli-version");
1638        let output = run_bounded(OperationKind::DiscoverRevisionImpact, command, policy)?;
1639        let (stdout, mut metrics) = successful_stdout(output)?;
1640        let capabilities = parse_revision_impact_capabilities(&stdout).map_err(|error| {
1641            metrics.status = InvocationStatus::Failed(error.failure_class());
1642            PredicateOperationError {
1643                error: Box::new(error),
1644                metrics: metrics.clone(),
1645            }
1646        })?;
1647        Ok(Observed {
1648            value: Self {
1649                executable,
1650                capabilities,
1651                policy,
1652            },
1653            metrics,
1654        })
1655    }
1656
1657    pub fn executable(&self) -> &Path {
1658        &self.executable
1659    }
1660
1661    pub fn capabilities(&self) -> &RevisionImpactCapabilities {
1662        &self.capabilities
1663    }
1664
1665    pub fn execution_policy(&self) -> ExecutionPolicy {
1666        self.policy
1667    }
1668
1669    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1670        self.policy = policy;
1671        self
1672    }
1673
1674    pub fn certify(
1675        &self,
1676        files: &RevisionImpactFiles<'_>,
1677        artifact: &Path,
1678    ) -> Result<RevisionImpactProcessSummary, PredicateApiError> {
1679        self.certify_observed(files, artifact)
1680            .map(|observed| observed.value)
1681            .map_err(|failure| *failure.error)
1682    }
1683
1684    pub fn certify_observed(
1685        &self,
1686        files: &RevisionImpactFiles<'_>,
1687        artifact: &Path,
1688    ) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
1689        self.invoke(
1690            OperationKind::CertifyRevisionImpact,
1691            "check-btor2-revision-impact",
1692            "CREATED",
1693            files,
1694            artifact,
1695        )
1696    }
1697
1698    pub fn verify(
1699        &self,
1700        files: &RevisionImpactFiles<'_>,
1701        artifact: &Path,
1702    ) -> Result<RevisionImpactProcessSummary, PredicateApiError> {
1703        self.verify_observed(files, artifact)
1704            .map(|observed| observed.value)
1705            .map_err(|failure| *failure.error)
1706    }
1707
1708    pub fn verify_observed(
1709        &self,
1710        files: &RevisionImpactFiles<'_>,
1711        artifact: &Path,
1712    ) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
1713        self.invoke(
1714            OperationKind::VerifyRevisionImpact,
1715            "verify-btor2-revision-impact",
1716            "VERIFIED",
1717            files,
1718            artifact,
1719        )
1720    }
1721
1722    fn invoke(
1723        &self,
1724        operation: OperationKind,
1725        command_name: &str,
1726        expected_status: &str,
1727        files: &RevisionImpactFiles<'_>,
1728        artifact: &Path,
1729    ) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
1730        let left_outputs =
1731            canonical_node_list(files.left_outputs).map_err(|error| PredicateOperationError {
1732                metrics: empty_metrics(
1733                    operation,
1734                    self.policy,
1735                    InvocationStatus::Failed(error.failure_class()),
1736                ),
1737                error: Box::new(error),
1738            })?;
1739        let right_outputs =
1740            canonical_node_list(files.right_outputs).map_err(|error| PredicateOperationError {
1741                metrics: empty_metrics(
1742                    operation,
1743                    self.policy,
1744                    InvocationStatus::Failed(error.failure_class()),
1745                ),
1746                error: Box::new(error),
1747            })?;
1748        let mut command = Command::new(&self.executable);
1749        command
1750            .arg(command_name)
1751            .arg(files.left_old)
1752            .arg(files.left_new)
1753            .arg(left_outputs)
1754            .arg(files.right_old)
1755            .arg(files.right_new)
1756            .arg(right_outputs)
1757            .arg(files.interface_old)
1758            .arg(files.interface_new)
1759            .arg(files.queries)
1760            .arg(artifact);
1761        let output = run_bounded(operation, command, self.policy)?;
1762        parse_revision_impact_summary(output, expected_status, &self.capabilities)
1763    }
1764}
1765
1766/// Typed, shell-free client for BTOR2 channel-property CLI v1.
1767#[derive(Clone, Debug)]
1768pub struct Btor2ChannelPropertyTool {
1769    executable: PathBuf,
1770    capabilities: Btor2ChannelPropertyCapabilities,
1771    policy: ExecutionPolicy,
1772}
1773
1774impl Btor2ChannelPropertyTool {
1775    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
1776        let policy = ExecutionPolicy::default()
1777            .with_file_limit(btor2_region_property::MAX_CHANNEL_PROPERTY_ARTIFACT_BYTES as u64)?;
1778        Self::discover_with_policy(executable, policy)
1779    }
1780
1781    pub fn discover_with_policy(
1782        executable: impl Into<PathBuf>,
1783        policy: ExecutionPolicy,
1784    ) -> Result<Self, PredicateApiError> {
1785        Self::discover_observed(executable, policy)
1786            .map(|observed| observed.value)
1787            .map_err(|failure| *failure.error)
1788    }
1789
1790    pub fn discover_observed(
1791        executable: impl Into<PathBuf>,
1792        policy: ExecutionPolicy,
1793    ) -> Result<Observed<Self>, PredicateOperationError> {
1794        let executable = executable.into();
1795        let mut command = Command::new(&executable);
1796        command.arg("btor2-channel-property-cli-version");
1797        let output = run_bounded(OperationKind::DiscoverBtor2ChannelProperty, command, policy)?;
1798        let (stdout, mut metrics) = successful_stdout(output)?;
1799        let capabilities = parse_btor2_channel_property_capabilities(&stdout).map_err(|error| {
1800            metrics.status = InvocationStatus::Failed(error.failure_class());
1801            PredicateOperationError {
1802                error: Box::new(error),
1803                metrics: metrics.clone(),
1804            }
1805        })?;
1806        Ok(Observed {
1807            value: Self {
1808                executable,
1809                capabilities,
1810                policy,
1811            },
1812            metrics,
1813        })
1814    }
1815
1816    pub fn capabilities(&self) -> &Btor2ChannelPropertyCapabilities {
1817        &self.capabilities
1818    }
1819
1820    pub fn execution_policy(&self) -> ExecutionPolicy {
1821        self.policy
1822    }
1823
1824    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1825        self.policy = policy;
1826        self
1827    }
1828
1829    pub fn observability(
1830        &self,
1831    ) -> Result<Btor2ChannelPropertyObservabilityTool, PredicateApiError> {
1832        self.observability_observed()
1833            .map(|observed| observed.value)
1834            .map_err(|failure| *failure.error)
1835    }
1836
1837    pub fn observability_observed(
1838        &self,
1839    ) -> Result<Observed<Btor2ChannelPropertyObservabilityTool>, PredicateOperationError> {
1840        let mut command = Command::new(&self.executable);
1841        command.arg("btor2-channel-property-observability-cli-version");
1842        let output = run_bounded(
1843            OperationKind::DiscoverBtor2ChannelPropertyObservability,
1844            command,
1845            self.policy,
1846        )?;
1847        let (stdout, mut metrics) = successful_stdout(output)?;
1848        let capabilities =
1849            parse_btor2_channel_property_observability_capabilities(&stdout, &self.capabilities)
1850                .map_err(|error| {
1851                    metrics.status = InvocationStatus::Failed(error.failure_class());
1852                    PredicateOperationError {
1853                        error: Box::new(error),
1854                        metrics: metrics.clone(),
1855                    }
1856                })?;
1857        Ok(Observed {
1858            value: Btor2ChannelPropertyObservabilityTool {
1859                executable: self.executable.clone(),
1860                base_capabilities: self.capabilities.clone(),
1861                capabilities,
1862                policy: self.policy,
1863            },
1864            metrics,
1865        })
1866    }
1867
1868    pub fn certify(
1869        &self,
1870        files: &Btor2ChannelPropertyFiles<'_>,
1871        artifact: &Path,
1872    ) -> Result<Btor2ChannelPropertyProcessSummary, PredicateApiError> {
1873        self.certify_observed(files, artifact)
1874            .map(|observed| observed.value)
1875            .map_err(|failure| *failure.error)
1876    }
1877
1878    pub fn certify_observed(
1879        &self,
1880        files: &Btor2ChannelPropertyFiles<'_>,
1881        artifact: &Path,
1882    ) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
1883        self.invoke(
1884            OperationKind::CertifyBtor2ChannelProperty,
1885            "certify-btor2-channel-properties",
1886            "CREATED",
1887            files,
1888            artifact,
1889        )
1890    }
1891
1892    pub fn verify(
1893        &self,
1894        files: &Btor2ChannelPropertyFiles<'_>,
1895        artifact: &Path,
1896    ) -> Result<Btor2ChannelPropertyProcessSummary, PredicateApiError> {
1897        self.verify_observed(files, artifact)
1898            .map(|observed| observed.value)
1899            .map_err(|failure| *failure.error)
1900    }
1901
1902    pub fn verify_observed(
1903        &self,
1904        files: &Btor2ChannelPropertyFiles<'_>,
1905        artifact: &Path,
1906    ) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
1907        self.invoke(
1908            OperationKind::VerifyBtor2ChannelProperty,
1909            "verify-btor2-channel-properties",
1910            "VERIFIED",
1911            files,
1912            artifact,
1913        )
1914    }
1915
1916    fn invoke(
1917        &self,
1918        operation: OperationKind,
1919        command_name: &str,
1920        expected_status: &str,
1921        files: &Btor2ChannelPropertyFiles<'_>,
1922        artifact: &Path,
1923    ) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
1924        let mut command = Command::new(&self.executable);
1925        command
1926            .arg(command_name)
1927            .arg(files.model)
1928            .arg(files.queries)
1929            .arg(files.policy)
1930            .arg(artifact);
1931        let output = run_bounded(operation, command, self.policy)?;
1932        parse_btor2_channel_property_summary(output, expected_status, &self.capabilities)
1933    }
1934}
1935
1936/// Additive phase-observed client for BTOR2 channel-property CLI v1.
1937#[derive(Clone, Debug)]
1938pub struct Btor2ChannelPropertyObservabilityTool {
1939    executable: PathBuf,
1940    base_capabilities: Btor2ChannelPropertyCapabilities,
1941    capabilities: Btor2ChannelPropertyObservabilityCapabilities,
1942    policy: ExecutionPolicy,
1943}
1944
1945impl Btor2ChannelPropertyObservabilityTool {
1946    pub fn capabilities(&self) -> &Btor2ChannelPropertyObservabilityCapabilities {
1947        &self.capabilities
1948    }
1949
1950    pub fn execution_policy(&self) -> ExecutionPolicy {
1951        self.policy
1952    }
1953
1954    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1955        self.policy = policy;
1956        self
1957    }
1958
1959    pub fn certify(
1960        &self,
1961        files: &Btor2ChannelPropertyFiles<'_>,
1962        artifact: &Path,
1963    ) -> Result<Btor2ChannelPropertyPhaseObservedSummary, PredicateApiError> {
1964        self.certify_observed(files, artifact)
1965            .map(|observed| observed.value)
1966            .map_err(|failure| *failure.error)
1967    }
1968
1969    pub fn certify_observed(
1970        &self,
1971        files: &Btor2ChannelPropertyFiles<'_>,
1972        artifact: &Path,
1973    ) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
1974        self.invoke(
1975            OperationKind::CertifyBtor2ChannelPropertyObserved,
1976            "certify-btor2-channel-properties-observed",
1977            "CREATED",
1978            Btor2ChannelPropertyObservedOperation::Certify,
1979            files,
1980            artifact,
1981        )
1982    }
1983
1984    pub fn verify(
1985        &self,
1986        files: &Btor2ChannelPropertyFiles<'_>,
1987        artifact: &Path,
1988    ) -> Result<Btor2ChannelPropertyPhaseObservedSummary, PredicateApiError> {
1989        self.verify_observed(files, artifact)
1990            .map(|observed| observed.value)
1991            .map_err(|failure| *failure.error)
1992    }
1993
1994    pub fn verify_observed(
1995        &self,
1996        files: &Btor2ChannelPropertyFiles<'_>,
1997        artifact: &Path,
1998    ) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
1999        self.invoke(
2000            OperationKind::VerifyBtor2ChannelPropertyObserved,
2001            "verify-btor2-channel-properties-observed",
2002            "VERIFIED",
2003            Btor2ChannelPropertyObservedOperation::Verify,
2004            files,
2005            artifact,
2006        )
2007    }
2008
2009    fn invoke(
2010        &self,
2011        operation: OperationKind,
2012        command_name: &str,
2013        expected_status: &str,
2014        expected_operation: Btor2ChannelPropertyObservedOperation,
2015        files: &Btor2ChannelPropertyFiles<'_>,
2016        artifact: &Path,
2017    ) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
2018        let mut command = Command::new(&self.executable);
2019        command
2020            .arg(command_name)
2021            .arg(files.model)
2022            .arg(files.queries)
2023            .arg(files.policy)
2024            .arg(artifact);
2025        let output = run_bounded(operation, command, self.policy)?;
2026        parse_btor2_channel_property_phase_observed_summary(
2027            output,
2028            expected_status,
2029            expected_operation,
2030            &self.base_capabilities,
2031            &self.capabilities,
2032        )
2033    }
2034}
2035
2036/// Typed, shell-free client for controller MTBDD plant CLI v1.
2037#[derive(Clone, Debug)]
2038pub struct ControllerMtbddTool {
2039    executable: PathBuf,
2040    capabilities: ControllerMtbddCapabilities,
2041    policy: ExecutionPolicy,
2042}
2043
2044impl ControllerMtbddTool {
2045    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, ControllerMtbddApiError> {
2046        Self::discover_with_policy(executable, ExecutionPolicy::default())
2047    }
2048
2049    pub fn discover_with_policy(
2050        executable: impl Into<PathBuf>,
2051        policy: ExecutionPolicy,
2052    ) -> Result<Self, ControllerMtbddApiError> {
2053        Self::discover_observed(executable, policy)
2054            .map(|observed| observed.value)
2055            .map_err(|failure| *failure.error)
2056    }
2057
2058    pub fn discover_observed(
2059        executable: impl Into<PathBuf>,
2060        policy: ExecutionPolicy,
2061    ) -> Result<Observed<Self>, ControllerMtbddOperationError> {
2062        let executable = executable.into();
2063        let mut command = Command::new(&executable);
2064        command.arg("controller-mtbdd-cli-version");
2065        let output = run_bounded(OperationKind::DiscoverControllerMtbdd, command, policy)?;
2066        let (stdout, mut metrics) = successful_stdout(output)?;
2067        let capabilities = parse_controller_mtbdd_capabilities(&stdout).map_err(|error| {
2068            metrics.status = InvocationStatus::Failed(error.failure_class());
2069            PredicateOperationError {
2070                error: Box::new(error),
2071                metrics: metrics.clone(),
2072            }
2073        })?;
2074        Ok(Observed {
2075            value: Self {
2076                executable,
2077                capabilities,
2078                policy,
2079            },
2080            metrics,
2081        })
2082    }
2083
2084    pub fn executable(&self) -> &Path {
2085        &self.executable
2086    }
2087
2088    pub fn capabilities(&self) -> &ControllerMtbddCapabilities {
2089        &self.capabilities
2090    }
2091
2092    pub fn execution_policy(&self) -> ExecutionPolicy {
2093        self.policy
2094    }
2095
2096    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2097        self.policy = policy;
2098        self
2099    }
2100
2101    pub fn certify(
2102        &self,
2103        manifest: &Path,
2104        artifact: &Path,
2105    ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2106        self.certify_observed(manifest, artifact)
2107            .map(|observed| observed.value)
2108            .map_err(|failure| *failure.error)
2109    }
2110
2111    pub fn certify_observed(
2112        &self,
2113        manifest: &Path,
2114        artifact: &Path,
2115    ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2116        let mut command = Command::new(&self.executable);
2117        command
2118            .arg("certify-controller-mtbdd-plant-batch")
2119            .arg(manifest)
2120            .arg(artifact);
2121        let output = run_bounded(
2122            OperationKind::CertifyControllerMtbddPlantBatch,
2123            command,
2124            self.policy,
2125        )?;
2126        parse_controller_mtbdd_summary(
2127            output,
2128            "CREATED",
2129            Some(artifact),
2130            &self.capabilities,
2131            "controller-mtbdd-plant-batch",
2132            "controller-mtbdd-plant-member",
2133        )
2134    }
2135
2136    pub fn verify(
2137        &self,
2138        manifest: &Path,
2139        artifact: &Path,
2140    ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2141        self.verify_observed(manifest, artifact)
2142            .map(|observed| observed.value)
2143            .map_err(|failure| *failure.error)
2144    }
2145
2146    pub fn verify_observed(
2147        &self,
2148        manifest: &Path,
2149        artifact: &Path,
2150    ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2151        let mut command = Command::new(&self.executable);
2152        command
2153            .arg("verify-controller-mtbdd-plant-batch")
2154            .arg(manifest)
2155            .arg(artifact);
2156        let output = run_bounded(
2157            OperationKind::VerifyControllerMtbddPlantBatch,
2158            command,
2159            self.policy,
2160        )?;
2161        parse_controller_mtbdd_summary(
2162            output,
2163            "VERIFIED",
2164            None,
2165            &self.capabilities,
2166            "controller-mtbdd-plant-batch",
2167            "controller-mtbdd-plant-member",
2168        )
2169    }
2170}
2171
2172/// Typed, shell-free client for proof-carrying controller MTBDD plant CLI v1.
2173#[derive(Clone, Debug)]
2174pub struct ControllerProofMtbddTool {
2175    executable: PathBuf,
2176    capabilities: ControllerProofMtbddCapabilities,
2177    policy: ExecutionPolicy,
2178}
2179
2180impl ControllerProofMtbddTool {
2181    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, ControllerMtbddApiError> {
2182        Self::discover_with_policy(executable, ExecutionPolicy::default())
2183    }
2184
2185    pub fn discover_with_policy(
2186        executable: impl Into<PathBuf>,
2187        policy: ExecutionPolicy,
2188    ) -> Result<Self, ControllerMtbddApiError> {
2189        Self::discover_observed(executable, policy)
2190            .map(|observed| observed.value)
2191            .map_err(|failure| *failure.error)
2192    }
2193
2194    pub fn discover_observed(
2195        executable: impl Into<PathBuf>,
2196        policy: ExecutionPolicy,
2197    ) -> Result<Observed<Self>, ControllerMtbddOperationError> {
2198        let executable = executable.into();
2199        let mut command = Command::new(&executable);
2200        command.arg("controller-proof-mtbdd-cli-version");
2201        let output = run_bounded(OperationKind::DiscoverControllerProofMtbdd, command, policy)?;
2202        let (stdout, mut metrics) = successful_stdout(output)?;
2203        let capabilities = parse_controller_proof_mtbdd_capabilities(&stdout).map_err(|error| {
2204            metrics.status = InvocationStatus::Failed(error.failure_class());
2205            PredicateOperationError {
2206                error: Box::new(error),
2207                metrics: metrics.clone(),
2208            }
2209        })?;
2210        Ok(Observed {
2211            value: Self {
2212                executable,
2213                capabilities,
2214                policy,
2215            },
2216            metrics,
2217        })
2218    }
2219
2220    pub fn executable(&self) -> &Path {
2221        &self.executable
2222    }
2223
2224    pub fn capabilities(&self) -> &ControllerProofMtbddCapabilities {
2225        &self.capabilities
2226    }
2227
2228    pub fn execution_policy(&self) -> ExecutionPolicy {
2229        self.policy
2230    }
2231
2232    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2233        self.policy = policy;
2234        self
2235    }
2236
2237    pub fn certify(
2238        &self,
2239        manifest: &Path,
2240        artifact: &Path,
2241    ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2242        self.certify_observed(manifest, artifact)
2243            .map(|observed| observed.value)
2244            .map_err(|failure| *failure.error)
2245    }
2246
2247    pub fn certify_observed(
2248        &self,
2249        manifest: &Path,
2250        artifact: &Path,
2251    ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2252        let mut command = Command::new(&self.executable);
2253        command
2254            .arg("certify-controller-proof-mtbdd-plant-batch")
2255            .arg(manifest)
2256            .arg(artifact);
2257        let output = run_bounded(
2258            OperationKind::CertifyControllerProofMtbddPlantBatch,
2259            command,
2260            self.policy,
2261        )?;
2262        parse_controller_mtbdd_summary(
2263            output,
2264            "CREATED",
2265            Some(artifact),
2266            &self.capabilities.common(),
2267            "controller-proof-mtbdd-plant-batch",
2268            "controller-proof-mtbdd-plant-member",
2269        )
2270    }
2271
2272    pub fn verify(
2273        &self,
2274        manifest: &Path,
2275        artifact: &Path,
2276    ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2277        self.verify_observed(manifest, artifact)
2278            .map(|observed| observed.value)
2279            .map_err(|failure| *failure.error)
2280    }
2281
2282    pub fn verify_observed(
2283        &self,
2284        manifest: &Path,
2285        artifact: &Path,
2286    ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2287        let mut command = Command::new(&self.executable);
2288        command
2289            .arg("verify-controller-proof-mtbdd-plant-batch")
2290            .arg(manifest)
2291            .arg(artifact);
2292        let output = run_bounded(
2293            OperationKind::VerifyControllerProofMtbddPlantBatch,
2294            command,
2295            self.policy,
2296        )?;
2297        parse_controller_mtbdd_summary(
2298            output,
2299            "VERIFIED",
2300            None,
2301            &self.capabilities.common(),
2302            "controller-proof-mtbdd-plant-batch",
2303            "controller-proof-mtbdd-plant-member",
2304        )
2305    }
2306}
2307
2308/// Typed, shell-free client for split controller evidence and plant batches.
2309#[derive(Clone, Debug)]
2310pub struct ControllerSplitEvidenceTool {
2311    executable: PathBuf,
2312    capabilities: ControllerSplitEvidenceCapabilities,
2313    policy: ExecutionPolicy,
2314}
2315
2316impl ControllerSplitEvidenceTool {
2317    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2318        Self::discover_with_policy(executable, ExecutionPolicy::default())
2319    }
2320
2321    pub fn discover_with_policy(
2322        executable: impl Into<PathBuf>,
2323        policy: ExecutionPolicy,
2324    ) -> Result<Self, PredicateApiError> {
2325        Self::discover_observed(executable, policy)
2326            .map(|observed| observed.value)
2327            .map_err(|failure| *failure.error)
2328    }
2329
2330    pub fn discover_observed(
2331        executable: impl Into<PathBuf>,
2332        policy: ExecutionPolicy,
2333    ) -> Result<Observed<Self>, PredicateOperationError> {
2334        let executable = executable.into();
2335        let mut command = Command::new(&executable);
2336        command.arg("controller-split-evidence-cli-version");
2337        let output = run_bounded(
2338            OperationKind::DiscoverControllerSplitEvidence,
2339            command,
2340            policy,
2341        )?;
2342        let (stdout, mut metrics) = successful_stdout(output)?;
2343        let capabilities =
2344            parse_controller_split_evidence_capabilities(&stdout).map_err(|error| {
2345                metrics.status = InvocationStatus::Failed(error.failure_class());
2346                PredicateOperationError {
2347                    error: Box::new(error),
2348                    metrics: metrics.clone(),
2349                }
2350            })?;
2351        Ok(Observed {
2352            value: Self {
2353                executable,
2354                capabilities,
2355                policy,
2356            },
2357            metrics,
2358        })
2359    }
2360
2361    pub fn capabilities(&self) -> &ControllerSplitEvidenceCapabilities {
2362        &self.capabilities
2363    }
2364
2365    pub fn execution_policy(&self) -> ExecutionPolicy {
2366        self.policy
2367    }
2368
2369    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2370        self.policy = policy;
2371        self
2372    }
2373
2374    pub fn certify_controller_evidence(
2375        &self,
2376        manifest: &Path,
2377        output: &Path,
2378    ) -> Result<ControllerSplitArtifactSummary, PredicateApiError> {
2379        self.certify_controller_evidence_observed(manifest, output)
2380            .map(|observed| observed.value)
2381            .map_err(|failure| *failure.error)
2382    }
2383
2384    pub fn certify_controller_evidence_observed(
2385        &self,
2386        manifest: &Path,
2387        output: &Path,
2388    ) -> Result<Observed<ControllerSplitArtifactSummary>, PredicateOperationError> {
2389        let mut command = Command::new(&self.executable);
2390        command
2391            .arg("certify-controller-proof-evidence-v1")
2392            .arg(manifest)
2393            .arg(output);
2394        let bounded = run_bounded(
2395            OperationKind::CertifyControllerProofEvidence,
2396            command,
2397            self.policy,
2398        )?;
2399        parse_controller_split_artifact_summary(
2400            bounded,
2401            "controller-split-evidence",
2402            false,
2403            output,
2404            &self.capabilities,
2405        )
2406    }
2407
2408    pub fn certify_plant_results(
2409        &self,
2410        manifest: &Path,
2411        evidence: &Path,
2412        output: &Path,
2413    ) -> Result<ControllerSplitArtifactSummary, PredicateApiError> {
2414        self.certify_plant_results_observed(manifest, evidence, output)
2415            .map(|observed| observed.value)
2416            .map_err(|failure| *failure.error)
2417    }
2418
2419    pub fn certify_plant_results_observed(
2420        &self,
2421        manifest: &Path,
2422        evidence: &Path,
2423        output: &Path,
2424    ) -> Result<Observed<ControllerSplitArtifactSummary>, PredicateOperationError> {
2425        let mut command = Command::new(&self.executable);
2426        command
2427            .arg("certify-bound-plant-results-v1")
2428            .arg(manifest)
2429            .arg(evidence)
2430            .arg(output);
2431        let bounded = run_bounded(
2432            OperationKind::CertifyBoundPlantResults,
2433            command,
2434            self.policy,
2435        )?;
2436        parse_controller_split_artifact_summary(
2437            bounded,
2438            "controller-split-plant",
2439            true,
2440            output,
2441            &self.capabilities,
2442        )
2443    }
2444
2445    pub fn verify_set(
2446        &self,
2447        evidence: &Path,
2448        batches: &[(&Path, &Path)],
2449    ) -> Result<ControllerSplitSetSummary, PredicateApiError> {
2450        self.verify_set_observed(evidence, batches)
2451            .map(|observed| observed.value)
2452            .map_err(|failure| *failure.error)
2453    }
2454
2455    pub fn verify_set_observed(
2456        &self,
2457        evidence: &Path,
2458        batches: &[(&Path, &Path)],
2459    ) -> Result<Observed<ControllerSplitSetSummary>, PredicateOperationError> {
2460        if batches.is_empty() || batches.len() > self.capabilities.max_batches {
2461            let error = PredicateApiError::InvalidPolicy(
2462                "split-evidence batch count is outside discovered limits".to_string(),
2463            );
2464            return Err(PredicateOperationError {
2465                metrics: empty_metrics(
2466                    OperationKind::VerifyBoundPlantResultSet,
2467                    self.policy,
2468                    InvocationStatus::Failed(error.failure_class()),
2469                ),
2470                error: Box::new(error),
2471            });
2472        }
2473        let mut command = Command::new(&self.executable);
2474        command
2475            .arg("verify-bound-plant-result-set-v1")
2476            .arg(evidence);
2477        for &(manifest, result) in batches {
2478            command.arg(manifest).arg(result);
2479        }
2480        let bounded = run_bounded(
2481            OperationKind::VerifyBoundPlantResultSet,
2482            command,
2483            self.policy,
2484        )?;
2485        parse_controller_split_set_summary(bounded, batches.len(), &self.capabilities)
2486    }
2487}
2488
2489/// Typed, shell-free client for governed split-evidence verification v1.
2490#[derive(Clone, Debug)]
2491pub struct ControllerSplitResourceTool {
2492    executable: PathBuf,
2493    capabilities: ControllerSplitResourceCapabilities,
2494    policy: ExecutionPolicy,
2495}
2496
2497impl ControllerSplitResourceTool {
2498    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2499        Self::discover_with_policy(executable, ExecutionPolicy::default())
2500    }
2501
2502    pub fn discover_with_policy(
2503        executable: impl Into<PathBuf>,
2504        policy: ExecutionPolicy,
2505    ) -> Result<Self, PredicateApiError> {
2506        Self::discover_observed(executable, policy)
2507            .map(|observed| observed.value)
2508            .map_err(|failure| *failure.error)
2509    }
2510
2511    pub fn discover_observed(
2512        executable: impl Into<PathBuf>,
2513        policy: ExecutionPolicy,
2514    ) -> Result<Observed<Self>, PredicateOperationError> {
2515        let executable = executable.into();
2516        let mut command = Command::new(&executable);
2517        command.arg("controller-split-resource-cli-version");
2518        let output = run_bounded(
2519            OperationKind::DiscoverControllerSplitResource,
2520            command,
2521            policy,
2522        )?;
2523        let (stdout, mut metrics) = successful_stdout(output)?;
2524        let capabilities =
2525            parse_controller_split_resource_capabilities(&stdout).map_err(|error| {
2526                metrics.status = InvocationStatus::Failed(error.failure_class());
2527                PredicateOperationError {
2528                    error: Box::new(error),
2529                    metrics: metrics.clone(),
2530                }
2531            })?;
2532        Ok(Observed {
2533            value: Self {
2534                executable,
2535                capabilities,
2536                policy,
2537            },
2538            metrics,
2539        })
2540    }
2541
2542    pub fn capabilities(&self) -> &ControllerSplitResourceCapabilities {
2543        &self.capabilities
2544    }
2545
2546    pub fn execution_policy(&self) -> ExecutionPolicy {
2547        self.policy
2548    }
2549
2550    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2551        self.policy = policy;
2552        self
2553    }
2554
2555    pub fn verify_set(
2556        &self,
2557        evidence: &Path,
2558        resource_policy: &Path,
2559        batches: &[(&Path, &Path)],
2560    ) -> Result<ControllerSplitResourceSetSummary, PredicateApiError> {
2561        self.verify_set_observed(evidence, resource_policy, batches)
2562            .map(|observed| observed.value)
2563            .map_err(|failure| *failure.error)
2564    }
2565
2566    pub fn verify_set_observed(
2567        &self,
2568        evidence: &Path,
2569        resource_policy: &Path,
2570        batches: &[(&Path, &Path)],
2571    ) -> Result<Observed<ControllerSplitResourceSetSummary>, PredicateOperationError> {
2572        if batches.is_empty() || batches.len() > self.capabilities.max_batches {
2573            let error = PredicateApiError::InvalidPolicy(
2574                "governed split-evidence batch count is outside discovered limits".to_string(),
2575            );
2576            return Err(PredicateOperationError {
2577                metrics: empty_metrics(
2578                    OperationKind::VerifyBoundPlantResultSetResources,
2579                    self.policy,
2580                    InvocationStatus::Failed(error.failure_class()),
2581                ),
2582                error: Box::new(error),
2583            });
2584        }
2585        let mut command = Command::new(&self.executable);
2586        command
2587            .arg("verify-bound-plant-result-set-with-resources-v1")
2588            .arg(evidence)
2589            .arg(resource_policy);
2590        for &(manifest, result) in batches {
2591            command.arg(manifest).arg(result);
2592        }
2593        let output = run_bounded(
2594            OperationKind::VerifyBoundPlantResultSetResources,
2595            command,
2596            self.policy,
2597        )?;
2598        parse_controller_split_resource_set_summary(output, batches.len(), &self.capabilities)
2599            .map_err(classify_controller_split_resource_refusal)
2600    }
2601}
2602
2603/// Typed, shell-free client for governed split verification with phase metrics.
2604#[derive(Clone, Debug)]
2605pub struct ControllerSplitObservabilityTool {
2606    executable: PathBuf,
2607    capabilities: ControllerSplitObservabilityCapabilities,
2608    policy: ExecutionPolicy,
2609}
2610
2611impl ControllerSplitObservabilityTool {
2612    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2613        Self::discover_with_policy(executable, ExecutionPolicy::default())
2614    }
2615
2616    pub fn discover_with_policy(
2617        executable: impl Into<PathBuf>,
2618        policy: ExecutionPolicy,
2619    ) -> Result<Self, PredicateApiError> {
2620        Self::discover_observed(executable, policy)
2621            .map(|observed| observed.value)
2622            .map_err(|failure| *failure.error)
2623    }
2624
2625    pub fn discover_observed(
2626        executable: impl Into<PathBuf>,
2627        policy: ExecutionPolicy,
2628    ) -> Result<Observed<Self>, PredicateOperationError> {
2629        let executable = executable.into();
2630        let mut command = Command::new(&executable);
2631        command.arg("controller-split-observability-cli-version");
2632        let output = run_bounded(
2633            OperationKind::DiscoverControllerSplitObservability,
2634            command,
2635            policy,
2636        )?;
2637        let (stdout, mut metrics) = successful_stdout(output)?;
2638        let capabilities =
2639            parse_controller_split_observability_capabilities(&stdout).map_err(|error| {
2640                metrics.status = InvocationStatus::Failed(error.failure_class());
2641                PredicateOperationError {
2642                    error: Box::new(error),
2643                    metrics: metrics.clone(),
2644                }
2645            })?;
2646        Ok(Observed {
2647            value: Self {
2648                executable,
2649                capabilities,
2650                policy,
2651            },
2652            metrics,
2653        })
2654    }
2655
2656    pub fn capabilities(&self) -> &ControllerSplitObservabilityCapabilities {
2657        &self.capabilities
2658    }
2659
2660    pub fn execution_policy(&self) -> ExecutionPolicy {
2661        self.policy
2662    }
2663
2664    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2665        self.policy = policy;
2666        self
2667    }
2668
2669    pub fn verify_set(
2670        &self,
2671        evidence: &Path,
2672        resource_policy: &Path,
2673        batches: &[(&Path, &Path)],
2674    ) -> Result<ControllerSplitObservedSummary, PredicateApiError> {
2675        self.verify_set_observed(evidence, resource_policy, batches)
2676            .map(|observed| observed.value)
2677            .map_err(|failure| *failure.error)
2678    }
2679
2680    pub fn verify_set_observed(
2681        &self,
2682        evidence: &Path,
2683        resource_policy: &Path,
2684        batches: &[(&Path, &Path)],
2685    ) -> Result<Observed<ControllerSplitObservedSummary>, PredicateOperationError> {
2686        if batches.is_empty() || batches.len() > self.capabilities.resource.max_batches {
2687            let error = PredicateApiError::InvalidPolicy(
2688                "observed split-evidence batch count is outside discovered limits".to_string(),
2689            );
2690            return Err(PredicateOperationError {
2691                metrics: empty_metrics(
2692                    OperationKind::VerifyBoundPlantResultSetResourcesObserved,
2693                    self.policy,
2694                    InvocationStatus::Failed(error.failure_class()),
2695                ),
2696                error: Box::new(error),
2697            });
2698        }
2699        let mut command = Command::new(&self.executable);
2700        command
2701            .arg("verify-bound-plant-result-set-with-resources-observed-v1")
2702            .arg(evidence)
2703            .arg(resource_policy);
2704        for &(manifest, result) in batches {
2705            command.arg(manifest).arg(result);
2706        }
2707        let output = run_bounded(
2708            OperationKind::VerifyBoundPlantResultSetResourcesObserved,
2709            command,
2710            self.policy,
2711        )?;
2712        parse_controller_split_observed_summary(output, batches.len(), &self.capabilities)
2713            .map_err(classify_controller_split_resource_refusal)
2714    }
2715}
2716
2717/// Typed client for governed split verification with allocator accounting.
2718#[derive(Clone, Debug)]
2719pub struct ControllerSplitAllocationObservabilityTool {
2720    executable: PathBuf,
2721    capabilities: ControllerSplitAllocationObservabilityCapabilities,
2722    policy: ExecutionPolicy,
2723}
2724
2725impl ControllerSplitAllocationObservabilityTool {
2726    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2727        Self::discover_with_policy(executable, ExecutionPolicy::default())
2728    }
2729
2730    pub fn discover_with_policy(
2731        executable: impl Into<PathBuf>,
2732        policy: ExecutionPolicy,
2733    ) -> Result<Self, PredicateApiError> {
2734        Self::discover_observed(executable, policy)
2735            .map(|observed| observed.value)
2736            .map_err(|failure| *failure.error)
2737    }
2738
2739    pub fn discover_observed(
2740        executable: impl Into<PathBuf>,
2741        policy: ExecutionPolicy,
2742    ) -> Result<Observed<Self>, PredicateOperationError> {
2743        let executable = executable.into();
2744        let mut command = Command::new(&executable);
2745        command.arg("controller-split-allocation-observability-cli-version");
2746        let output = run_bounded(
2747            OperationKind::DiscoverControllerSplitAllocationObservability,
2748            command,
2749            policy,
2750        )?;
2751        let (stdout, mut metrics) = successful_stdout(output)?;
2752        let capabilities = parse_controller_split_allocation_observability_capabilities(&stdout)
2753            .map_err(|error| {
2754                metrics.status = InvocationStatus::Failed(error.failure_class());
2755                PredicateOperationError {
2756                    error: Box::new(error),
2757                    metrics: metrics.clone(),
2758                }
2759            })?;
2760        Ok(Observed {
2761            value: Self {
2762                executable,
2763                capabilities,
2764                policy,
2765            },
2766            metrics,
2767        })
2768    }
2769
2770    pub fn capabilities(&self) -> &ControllerSplitAllocationObservabilityCapabilities {
2771        &self.capabilities
2772    }
2773
2774    pub fn execution_policy(&self) -> ExecutionPolicy {
2775        self.policy
2776    }
2777
2778    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2779        self.policy = policy;
2780        self
2781    }
2782
2783    pub fn verify_set(
2784        &self,
2785        evidence: &Path,
2786        resource_policy: &Path,
2787        batches: &[(&Path, &Path)],
2788    ) -> Result<ControllerSplitAllocationObservedSummary, PredicateApiError> {
2789        self.verify_set_observed(evidence, resource_policy, batches)
2790            .map(|observed| observed.value)
2791            .map_err(|failure| *failure.error)
2792    }
2793
2794    pub fn verify_set_observed(
2795        &self,
2796        evidence: &Path,
2797        resource_policy: &Path,
2798        batches: &[(&Path, &Path)],
2799    ) -> Result<Observed<ControllerSplitAllocationObservedSummary>, PredicateOperationError> {
2800        if batches.is_empty()
2801            || batches.len() > self.capabilities.observability.resource.max_batches
2802        {
2803            let error = PredicateApiError::InvalidPolicy(
2804                "allocation-observed split-evidence batch count is outside discovered limits"
2805                    .to_string(),
2806            );
2807            return Err(PredicateOperationError {
2808                metrics: empty_metrics(
2809                    OperationKind::VerifyBoundPlantResultSetResourcesAllocationObserved,
2810                    self.policy,
2811                    InvocationStatus::Failed(error.failure_class()),
2812                ),
2813                error: Box::new(error),
2814            });
2815        }
2816        let mut command = Command::new(&self.executable);
2817        command
2818            .arg("verify-bound-plant-result-set-with-resources-allocation-observed-v1")
2819            .arg(evidence)
2820            .arg(resource_policy);
2821        for &(manifest, result) in batches {
2822            command.arg(manifest).arg(result);
2823        }
2824        let output = run_bounded(
2825            OperationKind::VerifyBoundPlantResultSetResourcesAllocationObserved,
2826            command,
2827            self.policy,
2828        )?;
2829        parse_controller_split_allocation_observed_summary(
2830            output,
2831            batches.len(),
2832            &self.capabilities,
2833        )
2834        .map_err(classify_controller_split_resource_refusal)
2835    }
2836}
2837
2838/// Typed client for governed split verification with integrity-preserving
2839/// semantic replay caching and allocator accounting.
2840#[derive(Clone, Debug)]
2841pub struct ControllerSplitCacheObservabilityTool {
2842    executable: PathBuf,
2843    capabilities: ControllerSplitCacheObservabilityCapabilities,
2844    policy: ExecutionPolicy,
2845}
2846
2847impl ControllerSplitCacheObservabilityTool {
2848    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2849        Self::discover_with_policy(executable, ExecutionPolicy::default())
2850    }
2851
2852    pub fn discover_with_policy(
2853        executable: impl Into<PathBuf>,
2854        policy: ExecutionPolicy,
2855    ) -> Result<Self, PredicateApiError> {
2856        Self::discover_observed(executable, policy)
2857            .map(|observed| observed.value)
2858            .map_err(|failure| *failure.error)
2859    }
2860
2861    pub fn discover_observed(
2862        executable: impl Into<PathBuf>,
2863        policy: ExecutionPolicy,
2864    ) -> Result<Observed<Self>, PredicateOperationError> {
2865        let executable = executable.into();
2866        let mut command = Command::new(&executable);
2867        command.arg("controller-split-cache-observability-cli-version");
2868        let output = run_bounded(
2869            OperationKind::DiscoverControllerSplitCacheObservability,
2870            command,
2871            policy,
2872        )?;
2873        let (stdout, mut metrics) = successful_stdout(output)?;
2874        let capabilities = parse_controller_split_cache_observability_capabilities(&stdout)
2875            .map_err(|error| {
2876                metrics.status = InvocationStatus::Failed(error.failure_class());
2877                PredicateOperationError {
2878                    error: Box::new(error),
2879                    metrics: metrics.clone(),
2880                }
2881            })?;
2882        Ok(Observed {
2883            value: Self {
2884                executable,
2885                capabilities,
2886                policy,
2887            },
2888            metrics,
2889        })
2890    }
2891
2892    pub fn capabilities(&self) -> &ControllerSplitCacheObservabilityCapabilities {
2893        &self.capabilities
2894    }
2895
2896    pub fn execution_policy(&self) -> ExecutionPolicy {
2897        self.policy
2898    }
2899
2900    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2901        self.policy = policy;
2902        self
2903    }
2904
2905    pub fn verify_set(
2906        &self,
2907        evidence: &Path,
2908        resource_policy: &Path,
2909        batches: &[(&Path, &Path)],
2910    ) -> Result<ControllerSplitCacheObservedSummary, PredicateApiError> {
2911        self.verify_set_observed(evidence, resource_policy, batches)
2912            .map(|observed| observed.value)
2913            .map_err(|failure| *failure.error)
2914    }
2915
2916    pub fn verify_set_observed(
2917        &self,
2918        evidence: &Path,
2919        resource_policy: &Path,
2920        batches: &[(&Path, &Path)],
2921    ) -> Result<Observed<ControllerSplitCacheObservedSummary>, PredicateOperationError> {
2922        if batches.is_empty()
2923            || batches.len()
2924                > self
2925                    .capabilities
2926                    .allocation_observability
2927                    .observability
2928                    .resource
2929                    .max_batches
2930        {
2931            let error = PredicateApiError::InvalidPolicy(
2932                "cache-observed split-evidence batch count is outside discovered limits"
2933                    .to_string(),
2934            );
2935            return Err(PredicateOperationError {
2936                metrics: empty_metrics(
2937                    OperationKind::VerifyBoundPlantResultSetResourcesCacheObserved,
2938                    self.policy,
2939                    InvocationStatus::Failed(error.failure_class()),
2940                ),
2941                error: Box::new(error),
2942            });
2943        }
2944        let mut command = Command::new(&self.executable);
2945        command
2946            .arg("verify-bound-plant-result-set-with-resources-cache-observed-v1")
2947            .arg(evidence)
2948            .arg(resource_policy);
2949        for &(manifest, result) in batches {
2950            command.arg(manifest).arg(result);
2951        }
2952        let output = run_bounded(
2953            OperationKind::VerifyBoundPlantResultSetResourcesCacheObserved,
2954            command,
2955            self.policy,
2956        )?;
2957        parse_controller_split_cache_observed_summary(output, batches.len(), &self.capabilities)
2958            .map_err(classify_controller_split_resource_refusal)
2959    }
2960}
2961
2962/// Typed, shell-free client for statically routed controller/plant portfolio v1.
2963#[derive(Clone, Debug)]
2964pub struct ControllerPlantPortfolioTool {
2965    executable: PathBuf,
2966    capabilities: ControllerPlantPortfolioCapabilities,
2967    policy: ExecutionPolicy,
2968}
2969
2970impl ControllerPlantPortfolioTool {
2971    pub fn discover(
2972        executable: impl Into<PathBuf>,
2973    ) -> Result<Self, ControllerPlantPortfolioApiError> {
2974        Self::discover_with_policy(executable, ExecutionPolicy::default())
2975    }
2976
2977    pub fn discover_with_policy(
2978        executable: impl Into<PathBuf>,
2979        policy: ExecutionPolicy,
2980    ) -> Result<Self, ControllerPlantPortfolioApiError> {
2981        Self::discover_observed(executable, policy)
2982            .map(|observed| observed.value)
2983            .map_err(|failure| *failure.error)
2984    }
2985
2986    pub fn discover_observed(
2987        executable: impl Into<PathBuf>,
2988        policy: ExecutionPolicy,
2989    ) -> Result<Observed<Self>, ControllerPlantPortfolioOperationError> {
2990        let executable = executable.into();
2991        let mut command = Command::new(&executable);
2992        command.arg("controller-plant-portfolio-cli-version");
2993        let output = run_bounded(
2994            OperationKind::DiscoverControllerPlantPortfolio,
2995            command,
2996            policy,
2997        )?;
2998        let (stdout, mut metrics) = successful_stdout(output)?;
2999        let capabilities =
3000            parse_controller_plant_portfolio_capabilities(&stdout).map_err(|error| {
3001                metrics.status = InvocationStatus::Failed(error.failure_class());
3002                PredicateOperationError {
3003                    error: Box::new(error),
3004                    metrics: metrics.clone(),
3005                }
3006            })?;
3007        Ok(Observed {
3008            value: Self {
3009                executable,
3010                capabilities,
3011                policy,
3012            },
3013            metrics,
3014        })
3015    }
3016
3017    pub fn executable(&self) -> &Path {
3018        &self.executable
3019    }
3020
3021    pub fn capabilities(&self) -> &ControllerPlantPortfolioCapabilities {
3022        &self.capabilities
3023    }
3024
3025    pub fn execution_policy(&self) -> ExecutionPolicy {
3026        self.policy
3027    }
3028
3029    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3030        self.policy = policy;
3031        self
3032    }
3033
3034    pub fn certify(
3035        &self,
3036        manifest: &Path,
3037        artifact: &Path,
3038    ) -> Result<ControllerPlantPortfolioBatchSummary, ControllerPlantPortfolioApiError> {
3039        self.certify_observed(manifest, artifact)
3040            .map(|observed| observed.value)
3041            .map_err(|failure| *failure.error)
3042    }
3043
3044    pub fn certify_observed(
3045        &self,
3046        manifest: &Path,
3047        artifact: &Path,
3048    ) -> Result<
3049        Observed<ControllerPlantPortfolioBatchSummary>,
3050        ControllerPlantPortfolioOperationError,
3051    > {
3052        let mut command = Command::new(&self.executable);
3053        command
3054            .arg("certify-controller-plant-portfolio")
3055            .arg(manifest)
3056            .arg(artifact);
3057        let output = run_bounded(
3058            OperationKind::CertifyControllerPlantPortfolio,
3059            command,
3060            self.policy,
3061        )?;
3062        parse_controller_plant_portfolio_summary(
3063            output,
3064            "CREATED",
3065            Some(artifact),
3066            &self.capabilities,
3067        )
3068    }
3069
3070    pub fn verify(
3071        &self,
3072        manifest: &Path,
3073        artifact: &Path,
3074    ) -> Result<ControllerPlantPortfolioBatchSummary, ControllerPlantPortfolioApiError> {
3075        self.verify_observed(manifest, artifact)
3076            .map(|observed| observed.value)
3077            .map_err(|failure| *failure.error)
3078    }
3079
3080    pub fn verify_observed(
3081        &self,
3082        manifest: &Path,
3083        artifact: &Path,
3084    ) -> Result<
3085        Observed<ControllerPlantPortfolioBatchSummary>,
3086        ControllerPlantPortfolioOperationError,
3087    > {
3088        let mut command = Command::new(&self.executable);
3089        command
3090            .arg("verify-controller-plant-portfolio")
3091            .arg(manifest)
3092            .arg(artifact);
3093        let output = run_bounded(
3094            OperationKind::VerifyControllerPlantPortfolio,
3095            command,
3096            self.policy,
3097        )?;
3098        parse_controller_plant_portfolio_summary(output, "VERIFIED", None, &self.capabilities)
3099    }
3100}
3101
3102/// Typed, shell-free client for governed controller/plant verification v1.
3103#[derive(Clone, Debug)]
3104pub struct ControllerPlantResourceTool {
3105    executable: PathBuf,
3106    capabilities: ControllerPlantResourceCapabilities,
3107    policy: ExecutionPolicy,
3108}
3109
3110impl ControllerPlantResourceTool {
3111    pub fn discover(
3112        executable: impl Into<PathBuf>,
3113    ) -> Result<Self, ControllerPlantResourceApiError> {
3114        Self::discover_with_policy(executable, ExecutionPolicy::default())
3115    }
3116
3117    pub fn discover_with_policy(
3118        executable: impl Into<PathBuf>,
3119        policy: ExecutionPolicy,
3120    ) -> Result<Self, ControllerPlantResourceApiError> {
3121        Self::discover_observed(executable, policy)
3122            .map(|observed| observed.value)
3123            .map_err(|failure| *failure.error)
3124    }
3125
3126    pub fn discover_observed(
3127        executable: impl Into<PathBuf>,
3128        policy: ExecutionPolicy,
3129    ) -> Result<Observed<Self>, ControllerPlantResourceOperationError> {
3130        let executable = executable.into();
3131        let mut command = Command::new(&executable);
3132        command.arg("controller-plant-resource-cli-version");
3133        let output = run_bounded(
3134            OperationKind::DiscoverControllerPlantResource,
3135            command,
3136            policy,
3137        )?;
3138        let (stdout, mut metrics) = successful_stdout(output)?;
3139        let capabilities =
3140            parse_controller_plant_resource_capabilities(&stdout).map_err(|error| {
3141                metrics.status = InvocationStatus::Failed(error.failure_class());
3142                PredicateOperationError {
3143                    error: Box::new(error),
3144                    metrics: metrics.clone(),
3145                }
3146            })?;
3147        Ok(Observed {
3148            value: Self {
3149                executable,
3150                capabilities,
3151                policy,
3152            },
3153            metrics,
3154        })
3155    }
3156
3157    pub fn capabilities(&self) -> &ControllerPlantResourceCapabilities {
3158        &self.capabilities
3159    }
3160
3161    pub fn execution_policy(&self) -> ExecutionPolicy {
3162        self.policy
3163    }
3164
3165    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3166        self.policy = policy;
3167        self
3168    }
3169
3170    pub fn verify(
3171        &self,
3172        manifest: &Path,
3173        resource_policy: &Path,
3174        artifact: &Path,
3175    ) -> Result<ControllerPlantResourceSummary, ControllerPlantResourceApiError> {
3176        self.verify_observed(manifest, resource_policy, artifact)
3177            .map(|observed| observed.value)
3178            .map_err(|failure| *failure.error)
3179    }
3180
3181    pub fn verify_observed(
3182        &self,
3183        manifest: &Path,
3184        resource_policy: &Path,
3185        artifact: &Path,
3186    ) -> Result<Observed<ControllerPlantResourceSummary>, ControllerPlantResourceOperationError>
3187    {
3188        let mut command = Command::new(&self.executable);
3189        command
3190            .arg("verify-controller-plant-portfolio-resources")
3191            .arg(manifest)
3192            .arg(resource_policy)
3193            .arg(artifact);
3194        let output = run_bounded(
3195            OperationKind::VerifyControllerPlantPortfolioResources,
3196            command,
3197            self.policy,
3198        )?;
3199        parse_controller_plant_resource_summary(output, &self.capabilities)
3200            .map_err(classify_controller_plant_resource_refusal)
3201    }
3202}
3203
3204/// Typed, shell-free client for governed proof-carrying MTBDD verification v1.
3205#[derive(Clone, Debug)]
3206pub struct ControllerProofMtbddResourceTool {
3207    executable: PathBuf,
3208    capabilities: ControllerProofMtbddResourceCapabilities,
3209    policy: ExecutionPolicy,
3210}
3211
3212impl ControllerProofMtbddResourceTool {
3213    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
3214        Self::discover_with_policy(executable, ExecutionPolicy::default())
3215    }
3216
3217    pub fn discover_with_policy(
3218        executable: impl Into<PathBuf>,
3219        policy: ExecutionPolicy,
3220    ) -> Result<Self, PredicateApiError> {
3221        Self::discover_observed(executable, policy)
3222            .map(|observed| observed.value)
3223            .map_err(|failure| *failure.error)
3224    }
3225
3226    pub fn discover_observed(
3227        executable: impl Into<PathBuf>,
3228        policy: ExecutionPolicy,
3229    ) -> Result<Observed<Self>, PredicateOperationError> {
3230        let executable = executable.into();
3231        let mut command = Command::new(&executable);
3232        command.arg("controller-proof-mtbdd-resource-cli-version");
3233        let output = run_bounded(
3234            OperationKind::DiscoverControllerProofMtbddResource,
3235            command,
3236            policy,
3237        )?;
3238        let (stdout, mut metrics) = successful_stdout(output)?;
3239        let capabilities =
3240            parse_controller_proof_mtbdd_resource_capabilities(&stdout).map_err(|error| {
3241                metrics.status = InvocationStatus::Failed(error.failure_class());
3242                PredicateOperationError {
3243                    error: Box::new(error),
3244                    metrics: metrics.clone(),
3245                }
3246            })?;
3247        Ok(Observed {
3248            value: Self {
3249                executable,
3250                capabilities,
3251                policy,
3252            },
3253            metrics,
3254        })
3255    }
3256
3257    pub fn capabilities(&self) -> &ControllerProofMtbddResourceCapabilities {
3258        &self.capabilities
3259    }
3260
3261    pub fn execution_policy(&self) -> ExecutionPolicy {
3262        self.policy
3263    }
3264
3265    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3266        self.policy = policy;
3267        self
3268    }
3269
3270    pub fn verify(
3271        &self,
3272        manifest: &Path,
3273        resource_policy: &Path,
3274        artifact: &Path,
3275    ) -> Result<ControllerProofMtbddResourceSummary, PredicateApiError> {
3276        self.verify_observed(manifest, resource_policy, artifact)
3277            .map(|observed| observed.value)
3278            .map_err(|failure| *failure.error)
3279    }
3280
3281    pub fn verify_observed(
3282        &self,
3283        manifest: &Path,
3284        resource_policy: &Path,
3285        artifact: &Path,
3286    ) -> Result<Observed<ControllerProofMtbddResourceSummary>, PredicateOperationError> {
3287        let mut command = Command::new(&self.executable);
3288        command
3289            .arg("verify-controller-proof-mtbdd-plant-resources")
3290            .arg(manifest)
3291            .arg(resource_policy)
3292            .arg(artifact);
3293        let output = run_bounded(
3294            OperationKind::VerifyControllerProofMtbddPlantResources,
3295            command,
3296            self.policy,
3297        )?;
3298        parse_controller_proof_mtbdd_resource_summary(output, &self.capabilities)
3299            .map_err(classify_controller_proof_mtbdd_resource_refusal)
3300    }
3301}
3302
3303/// Typed, shell-free client for the governed proof/direct controller portfolio.
3304#[derive(Clone, Debug)]
3305pub struct ControllerProofMtbddPortfolioTool {
3306    executable: PathBuf,
3307    capabilities: ControllerProofMtbddPortfolioCapabilities,
3308    policy: ExecutionPolicy,
3309}
3310
3311impl ControllerProofMtbddPortfolioTool {
3312    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
3313        Self::discover_with_policy(executable, ExecutionPolicy::default())
3314    }
3315
3316    pub fn discover_with_policy(
3317        executable: impl Into<PathBuf>,
3318        policy: ExecutionPolicy,
3319    ) -> Result<Self, PredicateApiError> {
3320        Self::discover_observed(executable, policy)
3321            .map(|observed| observed.value)
3322            .map_err(|failure| *failure.error)
3323    }
3324
3325    pub fn discover_observed(
3326        executable: impl Into<PathBuf>,
3327        policy: ExecutionPolicy,
3328    ) -> Result<Observed<Self>, PredicateOperationError> {
3329        let executable = executable.into();
3330        let mut command = Command::new(&executable);
3331        command.arg("controller-proof-mtbdd-portfolio-cli-version");
3332        let output = run_bounded(
3333            OperationKind::DiscoverControllerProofMtbddPortfolio,
3334            command,
3335            policy,
3336        )?;
3337        let (stdout, mut metrics) = successful_stdout(output)?;
3338        let capabilities =
3339            parse_controller_proof_mtbdd_portfolio_capabilities(&stdout).map_err(|error| {
3340                metrics.status = InvocationStatus::Failed(error.failure_class());
3341                PredicateOperationError {
3342                    error: Box::new(error),
3343                    metrics: metrics.clone(),
3344                }
3345            })?;
3346        Ok(Observed {
3347            value: Self {
3348                executable,
3349                capabilities,
3350                policy,
3351            },
3352            metrics,
3353        })
3354    }
3355
3356    pub fn capabilities(&self) -> &ControllerProofMtbddPortfolioCapabilities {
3357        &self.capabilities
3358    }
3359
3360    pub fn execution_policy(&self) -> ExecutionPolicy {
3361        self.policy
3362    }
3363
3364    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3365        self.policy = policy;
3366        self
3367    }
3368
3369    pub fn verify(
3370        &self,
3371        manifest: &Path,
3372        resource_policy: &Path,
3373        artifact: &Path,
3374    ) -> Result<ControllerProofMtbddPortfolioResourceSummary, PredicateApiError> {
3375        self.verify_observed(manifest, resource_policy, artifact)
3376            .map(|observed| observed.value)
3377            .map_err(|failure| *failure.error)
3378    }
3379
3380    pub fn verify_observed(
3381        &self,
3382        manifest: &Path,
3383        resource_policy: &Path,
3384        artifact: &Path,
3385    ) -> Result<Observed<ControllerProofMtbddPortfolioResourceSummary>, PredicateOperationError>
3386    {
3387        let mut command = Command::new(&self.executable);
3388        command
3389            .arg("verify-controller-proof-mtbdd-portfolio-resources")
3390            .arg(manifest)
3391            .arg(resource_policy)
3392            .arg(artifact);
3393        let output = run_bounded(
3394            OperationKind::VerifyControllerProofMtbddPortfolioResources,
3395            command,
3396            self.policy,
3397        )?;
3398        parse_controller_proof_mtbdd_portfolio_resource_summary(output, &self.capabilities, false)
3399            .map_err(classify_controller_proof_mtbdd_resource_refusal)
3400    }
3401
3402    pub fn verify_attested(
3403        &self,
3404        manifest: &Path,
3405        resource_policy: &Path,
3406        artifact: &Path,
3407        provenance_manifest: &Path,
3408        attestation: &Path,
3409    ) -> Result<ControllerProofMtbddPortfolioResourceSummary, PredicateApiError> {
3410        self.verify_attested_observed(
3411            manifest,
3412            resource_policy,
3413            artifact,
3414            provenance_manifest,
3415            attestation,
3416        )
3417        .map(|observed| observed.value)
3418        .map_err(|failure| *failure.error)
3419    }
3420
3421    pub fn verify_attested_observed(
3422        &self,
3423        manifest: &Path,
3424        resource_policy: &Path,
3425        artifact: &Path,
3426        provenance_manifest: &Path,
3427        attestation: &Path,
3428    ) -> Result<Observed<ControllerProofMtbddPortfolioResourceSummary>, PredicateOperationError>
3429    {
3430        let mut command = Command::new(&self.executable);
3431        command
3432            .arg("verify-controller-proof-mtbdd-portfolio-resources-attested")
3433            .arg(manifest)
3434            .arg(resource_policy)
3435            .arg(artifact)
3436            .arg(provenance_manifest)
3437            .arg(attestation);
3438        let output = run_bounded(
3439            OperationKind::VerifyControllerProofMtbddPortfolioResourcesAttested,
3440            command,
3441            self.policy,
3442        )?;
3443        parse_controller_proof_mtbdd_portfolio_resource_summary(output, &self.capabilities, true)
3444            .map_err(classify_controller_proof_mtbdd_resource_refusal)
3445    }
3446}
3447
3448/// Typed, shell-free client for event-contract certificate v3 and portfolio v1.
3449#[derive(Clone, Debug)]
3450pub struct EventContractTool {
3451    executable: PathBuf,
3452    capabilities: EventContractCapabilities,
3453    policy: ExecutionPolicy,
3454}
3455
3456impl EventContractTool {
3457    pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, EventContractApiError> {
3458        Self::discover_with_policy(executable, ExecutionPolicy::default())
3459    }
3460
3461    pub fn discover_with_policy(
3462        executable: impl Into<PathBuf>,
3463        policy: ExecutionPolicy,
3464    ) -> Result<Self, EventContractApiError> {
3465        Self::discover_observed(executable, policy)
3466            .map(|observed| observed.value)
3467            .map_err(|failure| *failure.error)
3468    }
3469
3470    pub fn discover_observed(
3471        executable: impl Into<PathBuf>,
3472        policy: ExecutionPolicy,
3473    ) -> Result<Observed<Self>, EventContractOperationError> {
3474        let executable = executable.into();
3475        let mut command = Command::new(&executable);
3476        command.arg("event-contract-cli-version");
3477        let output = run_bounded(OperationKind::DiscoverEventContract, command, policy)?;
3478        let (stdout, mut metrics) = successful_stdout(output)?;
3479        let capabilities = parse_event_contract_capabilities(&stdout).map_err(|error| {
3480            metrics.status = InvocationStatus::Failed(error.failure_class());
3481            PredicateOperationError {
3482                error: Box::new(error),
3483                metrics: metrics.clone(),
3484            }
3485        })?;
3486        Ok(Observed {
3487            value: Self {
3488                executable,
3489                capabilities,
3490                policy,
3491            },
3492            metrics,
3493        })
3494    }
3495
3496    pub fn executable(&self) -> &Path {
3497        &self.executable
3498    }
3499
3500    pub fn capabilities(&self) -> &EventContractCapabilities {
3501        &self.capabilities
3502    }
3503
3504    pub fn execution_policy(&self) -> ExecutionPolicy {
3505        self.policy
3506    }
3507
3508    pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3509        self.policy = policy;
3510        self
3511    }
3512
3513    pub fn certify_v3(
3514        &self,
3515        model: &Path,
3516        bad_output: usize,
3517        contract: &Path,
3518        certificate: &Path,
3519    ) -> Result<EventContractResult, EventContractApiError> {
3520        self.certify_v3_observed(model, bad_output, contract, certificate)
3521            .map(|observed| observed.value)
3522            .map_err(|failure| *failure.error)
3523    }
3524
3525    pub fn certify_v3_observed(
3526        &self,
3527        model: &Path,
3528        bad_output: usize,
3529        contract: &Path,
3530        certificate: &Path,
3531    ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3532        let operation = OperationKind::CertifyEventContractV3;
3533        self.require_contract(operation)?;
3534        let mut command = Command::new(&self.executable);
3535        command
3536            .arg("certify-aiger-event-contract-v3")
3537            .arg(model)
3538            .arg(bad_output.to_string())
3539            .arg(contract)
3540            .arg(certificate);
3541        parse_observed_result(run_bounded(operation, command, self.policy)?)
3542    }
3543
3544    pub fn verify_v3(
3545        &self,
3546        model: &Path,
3547        contract: &Path,
3548        certificate: &Path,
3549    ) -> Result<EventContractResult, EventContractApiError> {
3550        self.verify_v3_observed(model, contract, certificate)
3551            .map(|observed| observed.value)
3552            .map_err(|failure| *failure.error)
3553    }
3554
3555    pub fn verify_v3_observed(
3556        &self,
3557        model: &Path,
3558        contract: &Path,
3559        certificate: &Path,
3560    ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3561        let operation = OperationKind::VerifyEventContractV3;
3562        self.require_contract(operation)?;
3563        let mut command = Command::new(&self.executable);
3564        command
3565            .arg("verify-aiger-event-contract-certificate-v3")
3566            .arg(model)
3567            .arg(contract)
3568            .arg(certificate);
3569        parse_observed_result(run_bounded(operation, command, self.policy)?)
3570    }
3571
3572    pub fn verify_portfolio(
3573        &self,
3574        model: &Path,
3575        bad_output: usize,
3576        contract: &Path,
3577        report: &Path,
3578        certificate: &Path,
3579    ) -> Result<EventContractResult, EventContractApiError> {
3580        self.verify_portfolio_observed(model, bad_output, contract, report, certificate)
3581            .map(|observed| observed.value)
3582            .map_err(|failure| *failure.error)
3583    }
3584
3585    pub fn verify_portfolio_observed(
3586        &self,
3587        model: &Path,
3588        bad_output: usize,
3589        contract: &Path,
3590        report: &Path,
3591        certificate: &Path,
3592    ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3593        let operation = OperationKind::EventContractPortfolio;
3594        self.require_contract(operation)?;
3595        let mut command = Command::new(&self.executable);
3596        command
3597            .arg("verify-aiger-event-contract-portfolio")
3598            .arg(model)
3599            .arg(bad_output.to_string())
3600            .arg(contract)
3601            .arg(report)
3602            .arg(certificate);
3603        parse_observed_result(run_bounded(operation, command, self.policy)?)
3604    }
3605
3606    pub fn verify_portfolio_report(
3607        &self,
3608        model: &Path,
3609        bad_output: usize,
3610        contract: &Path,
3611        report: &Path,
3612        certificate: &Path,
3613    ) -> Result<EventContractResult, EventContractApiError> {
3614        self.verify_portfolio_report_observed(model, bad_output, contract, report, certificate)
3615            .map(|observed| observed.value)
3616            .map_err(|failure| *failure.error)
3617    }
3618
3619    pub fn verify_portfolio_report_observed(
3620        &self,
3621        model: &Path,
3622        bad_output: usize,
3623        contract: &Path,
3624        report: &Path,
3625        certificate: &Path,
3626    ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3627        let operation = OperationKind::VerifyEventContractPortfolioReport;
3628        self.require_contract(operation)?;
3629        let mut command = Command::new(&self.executable);
3630        command
3631            .arg("verify-aiger-event-contract-portfolio-report")
3632            .arg(model)
3633            .arg(bad_output.to_string())
3634            .arg(contract)
3635            .arg(report)
3636            .arg(certificate);
3637        parse_observed_result(run_bounded(operation, command, self.policy)?)
3638    }
3639
3640    fn require_contract(
3641        &self,
3642        operation: OperationKind,
3643    ) -> Result<(), EventContractOperationError> {
3644        if self.capabilities.certificate_version != 3 || self.capabilities.portfolio_version != 1 {
3645            let error = PredicateApiError::IncompatibleContract(
3646                "event-contract certificate v3 and portfolio v1 are required".to_string(),
3647            );
3648            return Err(PredicateOperationError {
3649                metrics: empty_metrics(
3650                    operation,
3651                    self.policy,
3652                    InvocationStatus::Failed(error.failure_class()),
3653                ),
3654                error: Box::new(error),
3655            });
3656        }
3657        Ok(())
3658    }
3659}
3660
3661#[derive(Debug)]
3662struct ManagedOutput {
3663    status: ExitStatus,
3664    stdout: Vec<u8>,
3665    stderr: Vec<u8>,
3666    metrics: InvocationMetrics,
3667}
3668
3669fn empty_metrics(
3670    operation: OperationKind,
3671    policy: ExecutionPolicy,
3672    status: InvocationStatus,
3673) -> InvocationMetrics {
3674    InvocationMetrics {
3675        schema_version: INVOCATION_METRICS_SCHEMA_VERSION,
3676        operation,
3677        duration: Duration::ZERO,
3678        stdout_bytes: 0,
3679        stderr_bytes: 0,
3680        timeout: policy.timeout,
3681        output_limit_bytes: policy.output_limit_bytes,
3682        memory_limit_bytes: policy.memory_limit_bytes,
3683        file_limit_bytes: policy.file_limit_bytes,
3684        process_group_containment: cfg!(unix),
3685        exit_code: None,
3686        status,
3687    }
3688}
3689
3690fn operation_failure(
3691    operation: OperationKind,
3692    policy: ExecutionPolicy,
3693    started: Instant,
3694    stdout_bytes: usize,
3695    stderr_bytes: usize,
3696    exit_code: Option<i32>,
3697    error: PredicateApiError,
3698) -> PredicateOperationError {
3699    PredicateOperationError {
3700        metrics: InvocationMetrics {
3701            schema_version: INVOCATION_METRICS_SCHEMA_VERSION,
3702            operation,
3703            duration: started.elapsed(),
3704            stdout_bytes,
3705            stderr_bytes,
3706            timeout: policy.timeout,
3707            output_limit_bytes: policy.output_limit_bytes,
3708            memory_limit_bytes: policy.memory_limit_bytes,
3709            file_limit_bytes: policy.file_limit_bytes,
3710            process_group_containment: cfg!(unix),
3711            exit_code,
3712            status: InvocationStatus::Failed(error.failure_class()),
3713        },
3714        error: Box::new(error),
3715    }
3716}
3717
3718fn read_limited(
3719    reader: impl Read + Send + 'static,
3720    limit: usize,
3721) -> thread::JoinHandle<Result<Vec<u8>, std::io::Error>> {
3722    thread::spawn(move || {
3723        let mut bytes = Vec::new();
3724        reader.take(limit as u64 + 1).read_to_end(&mut bytes)?;
3725        Ok(bytes)
3726    })
3727}
3728
3729fn join_output(
3730    handle: thread::JoinHandle<Result<Vec<u8>, std::io::Error>>,
3731) -> Result<Vec<u8>, PredicateApiError> {
3732    handle
3733        .join()
3734        .map_err(|_| PredicateApiError::InvalidResponse("output reader stopped".to_string()))?
3735        .map_err(PredicateApiError::Io)
3736}
3737
3738fn configure_process(
3739    command: &mut Command,
3740    policy: ExecutionPolicy,
3741) -> Result<(), PredicateApiError> {
3742    #[cfg(unix)]
3743    {
3744        let file_limit = libc::rlim_t::try_from(policy.file_limit_bytes).map_err(|_| {
3745            PredicateApiError::InvalidPolicy(
3746                "file limit is not representable on this platform".to_string(),
3747            )
3748        })?;
3749        #[cfg(not(target_os = "macos"))]
3750        let memory_limit = policy
3751            .memory_limit_bytes
3752            .map(libc::rlim_t::try_from)
3753            .transpose()
3754            .map_err(|_| {
3755                PredicateApiError::InvalidPolicy(
3756                    "memory limit is not representable on this platform".to_string(),
3757                )
3758            })?;
3759        // SAFETY: this closure runs after fork and before exec and only calls
3760        // async-signal-safe libc functions with values prepared above.
3761        unsafe {
3762            command.pre_exec(move || {
3763                if libc::setsid() == -1 {
3764                    return Err(std::io::Error::last_os_error());
3765                }
3766                #[cfg(not(target_os = "macos"))]
3767                if let Some(memory_limit) = memory_limit {
3768                    let memory = libc::rlimit {
3769                        rlim_cur: memory_limit,
3770                        rlim_max: memory_limit,
3771                    };
3772                    if libc::setrlimit(libc::RLIMIT_AS, &memory) == -1 {
3773                        return Err(std::io::Error::last_os_error());
3774                    }
3775                }
3776                let file = libc::rlimit {
3777                    rlim_cur: file_limit,
3778                    rlim_max: file_limit,
3779                };
3780                if libc::setrlimit(libc::RLIMIT_FSIZE, &file) == -1 {
3781                    return Err(std::io::Error::last_os_error());
3782                }
3783                Ok(())
3784            });
3785        }
3786    }
3787    #[cfg(not(unix))]
3788    let _ = (command, policy);
3789    Ok(())
3790}
3791
3792fn stop_process(child: &mut std::process::Child) {
3793    #[cfg(unix)]
3794    {
3795        if let Ok(group) = i32::try_from(child.id()) {
3796            // SAFETY: the child creates a new session before exec; negating its
3797            // PID addresses that process group. ESRCH simply means it exited.
3798            let _ = unsafe { libc::kill(-group, libc::SIGKILL) };
3799        }
3800    }
3801    #[cfg(not(unix))]
3802    let _ = child.kill();
3803    let _ = child.wait();
3804}
3805
3806fn run_bounded(
3807    operation: OperationKind,
3808    mut command: Command,
3809    policy: ExecutionPolicy,
3810) -> Result<ManagedOutput, PredicateOperationError> {
3811    let started = Instant::now();
3812    command.stdout(Stdio::piped()).stderr(Stdio::piped());
3813    configure_process(&mut command, policy)
3814        .map_err(|error| operation_failure(operation, policy, started, 0, 0, None, error))?;
3815    let mut child = command
3816        .spawn()
3817        .map_err(|error| operation_failure(operation, policy, started, 0, 0, None, error.into()))?;
3818    let stdout = child.stdout.take().ok_or_else(|| {
3819        operation_failure(
3820            operation,
3821            policy,
3822            started,
3823            0,
3824            0,
3825            None,
3826            PredicateApiError::InvalidResponse("stdout pipe is missing".to_string()),
3827        )
3828    })?;
3829    let stderr = child.stderr.take().ok_or_else(|| {
3830        operation_failure(
3831            operation,
3832            policy,
3833            started,
3834            0,
3835            0,
3836            None,
3837            PredicateApiError::InvalidResponse("stderr pipe is missing".to_string()),
3838        )
3839    })?;
3840    let stdout_reader = read_limited(stdout, policy.output_limit_bytes);
3841    let stderr_reader = read_limited(stderr, policy.output_limit_bytes);
3842    let deadline = Instant::now() + policy.timeout;
3843    let status = loop {
3844        if let Some(status) = child.try_wait().map_err(|error| {
3845            operation_failure(operation, policy, started, 0, 0, None, error.into())
3846        })? {
3847            break status;
3848        }
3849        if Instant::now() >= deadline {
3850            stop_process(&mut child);
3851            let stdout_bytes = join_output(stdout_reader)
3852                .map(|bytes| bytes.len())
3853                .unwrap_or(0);
3854            let stderr_bytes = join_output(stderr_reader)
3855                .map(|bytes| bytes.len())
3856                .unwrap_or(0);
3857            return Err(operation_failure(
3858                operation,
3859                policy,
3860                started,
3861                stdout_bytes,
3862                stderr_bytes,
3863                None,
3864                PredicateApiError::TimedOut {
3865                    timeout: policy.timeout,
3866                },
3867            ));
3868        }
3869        thread::sleep(Duration::from_millis(5));
3870    };
3871    let stdout = join_output(stdout_reader).map_err(|error| {
3872        operation_failure(operation, policy, started, 0, 0, status.code(), error)
3873    })?;
3874    let stderr = join_output(stderr_reader).map_err(|error| {
3875        operation_failure(
3876            operation,
3877            policy,
3878            started,
3879            stdout.len(),
3880            0,
3881            status.code(),
3882            error,
3883        )
3884    })?;
3885    if stdout.len() > policy.output_limit_bytes {
3886        return Err(operation_failure(
3887            operation,
3888            policy,
3889            started,
3890            stdout.len(),
3891            stderr.len(),
3892            status.code(),
3893            PredicateApiError::OutputLimitExceeded {
3894                stream: "stdout",
3895                limit_bytes: policy.output_limit_bytes,
3896            },
3897        ));
3898    }
3899    if stderr.len() > policy.output_limit_bytes {
3900        return Err(operation_failure(
3901            operation,
3902            policy,
3903            started,
3904            stdout.len(),
3905            stderr.len(),
3906            status.code(),
3907            PredicateApiError::OutputLimitExceeded {
3908                stream: "stderr",
3909                limit_bytes: policy.output_limit_bytes,
3910            },
3911        ));
3912    }
3913    let metrics = InvocationMetrics {
3914        schema_version: INVOCATION_METRICS_SCHEMA_VERSION,
3915        operation,
3916        duration: started.elapsed(),
3917        stdout_bytes: stdout.len(),
3918        stderr_bytes: stderr.len(),
3919        timeout: policy.timeout,
3920        output_limit_bytes: policy.output_limit_bytes,
3921        memory_limit_bytes: policy.memory_limit_bytes,
3922        file_limit_bytes: policy.file_limit_bytes,
3923        process_group_containment: cfg!(unix),
3924        exit_code: status.code(),
3925        status: InvocationStatus::Success,
3926    };
3927    Ok(ManagedOutput {
3928        status,
3929        stdout,
3930        stderr,
3931        metrics,
3932    })
3933}
3934
3935fn successful_stdout(
3936    mut output: ManagedOutput,
3937) -> Result<(String, InvocationMetrics), PredicateOperationError> {
3938    if !output.status.success() {
3939        let error = PredicateApiError::CommandFailed {
3940            exit_code: output.status.code(),
3941            stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
3942        };
3943        output.metrics.status = InvocationStatus::Failed(error.failure_class());
3944        return Err(PredicateOperationError {
3945            error: Box::new(error),
3946            metrics: output.metrics,
3947        });
3948    }
3949    let stdout = String::from_utf8(output.stdout).map_err(|_| {
3950        let error = PredicateApiError::InvalidResponse("stdout is not UTF-8".to_string());
3951        output.metrics.status = InvocationStatus::Failed(error.failure_class());
3952        PredicateOperationError {
3953            error: Box::new(error),
3954            metrics: output.metrics.clone(),
3955        }
3956    })?;
3957    Ok((stdout, output.metrics))
3958}
3959
3960fn parse_observed_result(
3961    output: ManagedOutput,
3962) -> Result<Observed<PredicateResult>, PredicateOperationError> {
3963    let (stdout, mut metrics) = successful_stdout(output)?;
3964    let value = parse_result(&stdout).map_err(|error| {
3965        metrics.status = InvocationStatus::Failed(error.failure_class());
3966        PredicateOperationError {
3967            error: Box::new(error),
3968            metrics: metrics.clone(),
3969        }
3970    })?;
3971    Ok(Observed { value, metrics })
3972}
3973
3974fn parse_result(stdout: &str) -> Result<PredicateResult, PredicateApiError> {
3975    let results = stdout
3976        .split_whitespace()
3977        .filter_map(|field| field.strip_prefix("result="))
3978        .collect::<Vec<_>>();
3979    let Some(&result) = results.first() else {
3980        return Err(PredicateApiError::InvalidResponse(
3981            "result field is missing".to_string(),
3982        ));
3983    };
3984    if results.iter().any(|candidate| *candidate != result) {
3985        return Err(PredicateApiError::InvalidResponse(
3986            "subprocess result fields disagree".to_string(),
3987        ));
3988    }
3989    match result {
3990        "avoidable" => Ok(PredicateResult::Avoidable),
3991        "unavoidable" => Ok(PredicateResult::Unavoidable),
3992        _ => Err(PredicateApiError::InvalidResponse(format!(
3993            "unsupported result `{result}`"
3994        ))),
3995    }
3996}
3997
3998fn canonical_usize(value: &str, field: &str) -> Result<usize, PredicateApiError> {
3999    let parsed = value.parse::<usize>().map_err(|_| {
4000        PredicateApiError::InvalidResponse(format!("{field} is not an unsigned integer"))
4001    })?;
4002    if parsed.to_string() != value {
4003        return Err(PredicateApiError::InvalidResponse(format!(
4004            "{field} is noncanonical"
4005        )));
4006    }
4007    Ok(parsed)
4008}
4009
4010fn canonical_u32(value: &str, field: &str) -> Result<u32, PredicateApiError> {
4011    let parsed = canonical_usize(value, field)?;
4012    u32::try_from(parsed).map_err(|_| {
4013        PredicateApiError::InvalidResponse(format!("{field} exceeds canonical u32 range"))
4014    })
4015}
4016
4017fn canonical_u128(value: &str, field: &str) -> Result<u128, PredicateApiError> {
4018    let parsed = value.parse::<u128>().map_err(|_| {
4019        PredicateApiError::InvalidResponse(format!("{field} is not an unsigned integer"))
4020    })?;
4021    if parsed.to_string() != value {
4022        return Err(PredicateApiError::InvalidResponse(format!(
4023            "{field} is noncanonical"
4024        )));
4025    }
4026    Ok(parsed)
4027}
4028
4029fn canonical_u64(value: &str, field: &str) -> Result<u64, PredicateApiError> {
4030    let parsed = value.parse::<u64>().map_err(|_| {
4031        PredicateApiError::InvalidResponse(format!("{field} is not an unsigned integer"))
4032    })?;
4033    if parsed.to_string() != value {
4034        return Err(PredicateApiError::InvalidResponse(format!(
4035            "{field} is noncanonical"
4036        )));
4037    }
4038    Ok(parsed)
4039}
4040
4041fn token_value<'a>(token: &'a str, key: &str) -> Result<&'a str, PredicateApiError> {
4042    token
4043        .strip_prefix(&format!("{key}="))
4044        .ok_or_else(|| PredicateApiError::InvalidResponse(format!("expected response field {key}")))
4045}
4046
4047fn parse_controller_proof_mtbdd_resource_capabilities(
4048    line: &str,
4049) -> Result<ControllerProofMtbddResourceCapabilities, PredicateApiError> {
4050    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
4051        return Err(PredicateApiError::InvalidResponse(
4052            "controller proof MTBDD resource capability line is not canonical".to_string(),
4053        ));
4054    }
4055    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
4056    let keys = [
4057        "controller_proof_mtbdd_resource_cli_version",
4058        "policy_version",
4059        "envelope_version",
4060        "manifest_version",
4061        "artifact_version",
4062        "max_policy_bytes",
4063        "max_artifact_bytes",
4064        "max_equivalence_artifact_bytes",
4065        "max_unsat_proof_bytes",
4066        "max_members",
4067        "max_horizon",
4068        "max_product_states",
4069        "refusal_exit",
4070        "verification",
4071        "exhaustive_replay",
4072        "accounting",
4073        "timing_calibration",
4074        "result_on_refusal",
4075        "refusal_schema",
4076        "unsupported",
4077    ];
4078    if fields.len() != keys.len() {
4079        return Err(PredicateApiError::InvalidResponse(
4080            "controller proof MTBDD resource capability field count is invalid".to_string(),
4081        ));
4082    }
4083    let values = fields
4084        .iter()
4085        .zip(keys)
4086        .map(|(field, key)| token_value(field, key))
4087        .collect::<Result<Vec<_>, _>>()?;
4088    if values[13..]
4089        != [
4090            "unsat-miter",
4091            "no",
4092            "conservative-static",
4093            "none",
4094            "none",
4095            "proof-reason-v1",
4096            "fail-closed",
4097        ]
4098    {
4099        return Err(PredicateApiError::IncompatibleContract(
4100            "controller proof MTBDD resource contract is unsupported".to_string(),
4101        ));
4102    }
4103    let versions = values[..5]
4104        .iter()
4105        .enumerate()
4106        .map(|(index, value)| canonical_u32(value, keys[index]))
4107        .collect::<Result<Vec<_>, _>>()?;
4108    let limits = values[5..12]
4109        .iter()
4110        .enumerate()
4111        .map(|(index, value)| canonical_usize(value, keys[index + 5]))
4112        .collect::<Result<Vec<_>, _>>()?;
4113    let refusal_exit = canonical_usize(values[12], keys[12])?;
4114    if versions != [1, 1, 1, 1, 1] {
4115        return Err(PredicateApiError::IncompatibleContract(
4116            "controller proof MTBDD resource version tuple is unsupported".to_string(),
4117        ));
4118    }
4119    if limits.contains(&0) || refusal_exit != 3 {
4120        return Err(PredicateApiError::InvalidResponse(
4121            "controller proof MTBDD resource discovered limit must be positive".to_string(),
4122        ));
4123    }
4124    Ok(ControllerProofMtbddResourceCapabilities {
4125        cli_version: versions[0],
4126        policy_version: versions[1],
4127        envelope_version: versions[2],
4128        manifest_version: versions[3],
4129        artifact_version: versions[4],
4130        max_policy_bytes: limits[0],
4131        max_artifact_bytes: limits[1],
4132        max_equivalence_artifact_bytes: limits[2],
4133        max_unsat_proof_bytes: limits[3],
4134        max_members: limits[4],
4135        max_horizon: limits[5],
4136        max_product_states: limits[6],
4137        refusal_exit_code: 3,
4138    })
4139}
4140
4141fn parse_controller_proof_mtbdd_portfolio_capabilities(
4142    line: &str,
4143) -> Result<ControllerProofMtbddPortfolioCapabilities, PredicateApiError> {
4144    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
4145        return Err(PredicateApiError::InvalidResponse(
4146            "controller proof MTBDD portfolio capability line is not canonical".to_string(),
4147        ));
4148    }
4149    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
4150    let keys = [
4151        "controller_proof_mtbdd_portfolio_cli_version",
4152        "policy_version",
4153        "envelope_version",
4154        "artifact_version",
4155        "proof_artifact_version",
4156        "direct_artifact_version",
4157        "manifest_version",
4158        "source_model_attestation_version",
4159        "max_policy_bytes",
4160        "max_artifact_bytes",
4161        "max_equivalence_artifact_bytes",
4162        "max_unsat_proof_bytes",
4163        "max_members",
4164        "max_horizon",
4165        "max_product_states",
4166        "max_attestation_bytes",
4167        "refusal_exit",
4168        "backends",
4169        "routing",
4170        "fallback",
4171        "proof_failure",
4172        "attested_verification",
4173        "accounting",
4174        "timing_calibration",
4175        "result_on_refusal",
4176        "refusal_schema",
4177        "unsupported",
4178    ];
4179    if fields.len() != keys.len() {
4180        return Err(PredicateApiError::InvalidResponse(
4181            "controller proof MTBDD portfolio capability field count is invalid".to_string(),
4182        ));
4183    }
4184    let values = fields
4185        .iter()
4186        .zip(keys)
4187        .map(|(field, key)| token_value(field, key))
4188        .collect::<Result<Vec<_>, _>>()?;
4189    if values[17..]
4190        != [
4191            "proof-mtbdd,direct-exact",
4192            "static",
4193            "exact",
4194            "fail-closed",
4195            "required",
4196            "conservative-static",
4197            "none",
4198            "none",
4199            "proof-reason-v1",
4200            "fail-closed",
4201        ]
4202    {
4203        return Err(PredicateApiError::IncompatibleContract(
4204            "controller proof MTBDD portfolio contract is unsupported".to_string(),
4205        ));
4206    }
4207    let versions = values[..8]
4208        .iter()
4209        .enumerate()
4210        .map(|(index, value)| canonical_u32(value, keys[index]))
4211        .collect::<Result<Vec<_>, _>>()?;
4212    let limits = values[8..16]
4213        .iter()
4214        .enumerate()
4215        .map(|(index, value)| canonical_usize(value, keys[index + 8]))
4216        .collect::<Result<Vec<_>, _>>()?;
4217    let refusal_exit = canonical_usize(values[16], keys[16])?;
4218    if versions != [1, 1, 1, 1, 1, 1, 1, 1] {
4219        return Err(PredicateApiError::IncompatibleContract(
4220            "controller proof MTBDD portfolio version tuple is unsupported".to_string(),
4221        ));
4222    }
4223    if limits.contains(&0) || refusal_exit != 3 {
4224        return Err(PredicateApiError::InvalidResponse(
4225            "controller proof MTBDD portfolio discovered limit must be positive".to_string(),
4226        ));
4227    }
4228    Ok(ControllerProofMtbddPortfolioCapabilities {
4229        cli_version: versions[0],
4230        policy_version: versions[1],
4231        envelope_version: versions[2],
4232        artifact_version: versions[3],
4233        proof_artifact_version: versions[4],
4234        direct_artifact_version: versions[5],
4235        manifest_version: versions[6],
4236        source_model_attestation_version: versions[7],
4237        max_policy_bytes: limits[0],
4238        max_artifact_bytes: limits[1],
4239        max_equivalence_artifact_bytes: limits[2],
4240        max_unsat_proof_bytes: limits[3],
4241        max_members: limits[4],
4242        max_horizon: limits[5],
4243        max_product_states: limits[6],
4244        max_attestation_bytes: limits[7],
4245        refusal_exit_code: 3,
4246    })
4247}
4248
4249fn classify_controller_proof_mtbdd_resource_refusal(
4250    mut failure: PredicateOperationError,
4251) -> PredicateOperationError {
4252    let reason = match failure.error.as_ref() {
4253        PredicateApiError::CommandFailed {
4254            exit_code: Some(3),
4255            stderr,
4256        } => stderr
4257            .trim_end_matches(['\r', '\n'])
4258            .strip_prefix("error: controller-proof-mtbdd-resource refusal=")
4259            .and_then(|value| value.strip_suffix(" result=none"))
4260            .and_then(|value| match value {
4261                "artifact-bytes" => Some(ControllerPlantResourceRefusalReason::ArtifactBytes),
4262                "equivalence-artifact-bytes" => {
4263                    Some(ControllerPlantResourceRefusalReason::EquivalenceArtifactBytes)
4264                }
4265                "unsat-proof-bytes" => Some(ControllerPlantResourceRefusalReason::UnsatProofBytes),
4266                "members" => Some(ControllerPlantResourceRefusalReason::Members),
4267                "horizon" => Some(ControllerPlantResourceRefusalReason::Horizon),
4268                "product-states" => Some(ControllerPlantResourceRefusalReason::ProductStates),
4269                "transition-evaluations" => {
4270                    Some(ControllerPlantResourceRefusalReason::TransitionEvaluations)
4271                }
4272                _ => None,
4273            }),
4274        _ => None,
4275    };
4276    if let Some(reason) = reason {
4277        let error = PredicateApiError::ResourceRefused { reason };
4278        failure.metrics.status = InvocationStatus::Failed(error.failure_class());
4279        failure.error = Box::new(error);
4280    }
4281    failure
4282}
4283
4284fn parse_controller_plant_resource_capabilities(
4285    line: &str,
4286) -> Result<ControllerPlantResourceCapabilities, PredicateApiError> {
4287    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
4288        return Err(PredicateApiError::InvalidResponse(
4289            "controller plant resource capability line is not canonical".to_string(),
4290        ));
4291    }
4292    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
4293    let keys = [
4294        "controller_plant_resource_cli_version",
4295        "policy_version",
4296        "envelope_version",
4297        "manifest_version",
4298        "portfolio_artifact_version",
4299        "max_policy_bytes",
4300        "max_artifact_bytes",
4301        "max_members",
4302        "max_horizon",
4303        "max_product_states",
4304        "refusal_exit",
4305        "accounting",
4306        "timing_calibration",
4307        "result_on_refusal",
4308        "refusal_schema",
4309        "unsupported",
4310    ];
4311    if fields.len() != keys.len() {
4312        return Err(PredicateApiError::InvalidResponse(
4313            "controller plant resource capability field count is invalid".to_string(),
4314        ));
4315    }
4316    let values = fields
4317        .iter()
4318        .zip(keys)
4319        .map(|(field, key)| token_value(field, key))
4320        .collect::<Result<Vec<_>, _>>()?;
4321    if values[11..]
4322        != [
4323            "conservative-static",
4324            "none",
4325            "none",
4326            "reason-v1",
4327            "fail-closed",
4328        ]
4329    {
4330        return Err(PredicateApiError::IncompatibleContract(
4331            "controller plant resource accounting contract is unsupported".to_string(),
4332        ));
4333    }
4334    let versions = values[..5]
4335        .iter()
4336        .enumerate()
4337        .map(|(index, value)| canonical_u32(value, keys[index]))
4338        .collect::<Result<Vec<_>, _>>()?;
4339    let limits = values[5..10]
4340        .iter()
4341        .enumerate()
4342        .map(|(index, value)| canonical_usize(value, keys[index + 5]))
4343        .collect::<Result<Vec<_>, _>>()?;
4344    let refusal_exit = canonical_usize(values[10], keys[10])?;
4345    if versions != [1, 1, 1, 1, 1] {
4346        return Err(PredicateApiError::IncompatibleContract(
4347            "controller plant resource version tuple is unsupported".to_string(),
4348        ));
4349    }
4350    if limits.contains(&0) || refusal_exit != 3 {
4351        return Err(PredicateApiError::InvalidResponse(
4352            "controller plant resource discovered limit must be positive".to_string(),
4353        ));
4354    }
4355    Ok(ControllerPlantResourceCapabilities {
4356        cli_version: versions[0],
4357        policy_version: versions[1],
4358        envelope_version: versions[2],
4359        manifest_version: versions[3],
4360        portfolio_artifact_version: versions[4],
4361        max_policy_bytes: limits[0],
4362        max_artifact_bytes: limits[1],
4363        max_members: limits[2],
4364        max_horizon: limits[3],
4365        max_product_states: limits[4],
4366        refusal_exit_code: 3,
4367    })
4368}
4369
4370fn classify_controller_plant_resource_refusal(
4371    mut failure: PredicateOperationError,
4372) -> PredicateOperationError {
4373    let reason = match failure.error.as_ref() {
4374        PredicateApiError::CommandFailed {
4375            exit_code: Some(3),
4376            stderr,
4377        } => stderr
4378            .trim_end_matches(['\r', '\n'])
4379            .strip_prefix("error: controller-plant-resource refusal=")
4380            .and_then(|value| value.strip_suffix(" result=none"))
4381            .and_then(|value| match value {
4382                "artifact-bytes" => Some(ControllerPlantResourceRefusalReason::ArtifactBytes),
4383                "members" => Some(ControllerPlantResourceRefusalReason::Members),
4384                "horizon" => Some(ControllerPlantResourceRefusalReason::Horizon),
4385                "product-states" => Some(ControllerPlantResourceRefusalReason::ProductStates),
4386                "transition-evaluations" => {
4387                    Some(ControllerPlantResourceRefusalReason::TransitionEvaluations)
4388                }
4389                _ => None,
4390            }),
4391        _ => None,
4392    };
4393    if let Some(reason) = reason {
4394        let error = PredicateApiError::ResourceRefused { reason };
4395        failure.metrics.status = InvocationStatus::Failed(error.failure_class());
4396        failure.error = Box::new(error);
4397    }
4398    failure
4399}
4400
4401fn parse_controller_plant_resource_summary(
4402    output: ManagedOutput,
4403    capabilities: &ControllerPlantResourceCapabilities,
4404) -> Result<Observed<ControllerPlantResourceSummary>, PredicateOperationError> {
4405    let (stdout, mut metrics) = successful_stdout(output)?;
4406    let parsed = (|| -> Result<ControllerPlantResourceSummary, PredicateApiError> {
4407        if stdout.contains('\r') || !stdout.ends_with('\n') {
4408            return Err(PredicateApiError::InvalidResponse(
4409                "controller plant resource response is not canonical LF text".to_string(),
4410            ));
4411        }
4412        let mut lines = stdout.lines();
4413        let first = lines.next().ok_or_else(|| {
4414            PredicateApiError::InvalidResponse(
4415                "controller plant resource summary line is missing".to_string(),
4416            )
4417        })?;
4418        let fields = first.split(' ').collect::<Vec<_>>();
4419        let keys = [
4420            "controller-plant-resource",
4421            "status",
4422            "cli_version",
4423            "policy_version",
4424            "envelope_version",
4425            "artifact_version",
4426            "backend",
4427            "members",
4428            "maximum_member_horizon",
4429            "maximum_product_states",
4430            "transition_evaluation_bound",
4431            "safe",
4432            "unsafe",
4433            "reachable_product_states",
4434            "explored_transitions",
4435            "artifact_bytes",
4436            "load_micros",
4437            "artifact_micros",
4438            "verification_micros",
4439            "elapsed_micros",
4440        ];
4441        if fields.len() != keys.len()
4442            || fields[0] != keys[0]
4443            || token_value(fields[1], "status")? != "VERIFIED"
4444        {
4445            return Err(PredicateApiError::InvalidResponse(
4446                "controller plant resource summary shape is invalid".to_string(),
4447            ));
4448        }
4449        let cli_version = canonical_u32(token_value(fields[2], keys[2])?, keys[2])?;
4450        let policy_version = canonical_u32(token_value(fields[3], keys[3])?, keys[3])?;
4451        let envelope_version = canonical_u32(token_value(fields[4], keys[4])?, keys[4])?;
4452        let artifact_version = canonical_u32(token_value(fields[5], keys[5])?, keys[5])?;
4453        if cli_version != capabilities.cli_version
4454            || policy_version != capabilities.policy_version
4455            || envelope_version != capabilities.envelope_version
4456            || artifact_version != capabilities.portfolio_artifact_version
4457        {
4458            return Err(PredicateApiError::IncompatibleContract(
4459                "controller plant resource response version changed".to_string(),
4460            ));
4461        }
4462        let backend = match token_value(fields[6], "backend")? {
4463            "MTBDD" => ControllerPlantPortfolioBackend::Mtbdd,
4464            "DIRECT_EXACT" => ControllerPlantPortfolioBackend::DirectExact,
4465            _ => {
4466                return Err(PredicateApiError::InvalidResponse(
4467                    "controller plant resource backend is invalid".to_string(),
4468                ));
4469            }
4470        };
4471        let numeric = fields[7..]
4472            .iter()
4473            .zip(&keys[7..])
4474            .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4475            .collect::<Result<Vec<_>, _>>()?;
4476        let member_count = numeric[0];
4477        if member_count == 0
4478            || member_count > capabilities.max_members
4479            || numeric[1] > capabilities.max_horizon
4480            || numeric[2] > capabilities.max_product_states
4481            || numeric[8] > capabilities.max_artifact_bytes
4482            || numeric[4].checked_add(numeric[5]) != Some(member_count)
4483        {
4484            return Err(PredicateApiError::InvalidResponse(
4485                "controller plant resource response exceeds discovered limits".to_string(),
4486            ));
4487        }
4488        let mut member_results = Vec::with_capacity(member_count);
4489        for expected_index in 0..member_count {
4490            let line = lines.next().ok_or_else(|| {
4491                PredicateApiError::InvalidResponse(
4492                    "controller plant resource member line is missing".to_string(),
4493                )
4494            })?;
4495            let fields = line.split(' ').collect::<Vec<_>>();
4496            let keys = [
4497                "controller-plant-resource-member",
4498                "index",
4499                "answer",
4500                "horizon",
4501                "bad_frame",
4502                "trace_steps",
4503                "reachable_product_states",
4504                "explored_transitions",
4505            ];
4506            if fields.len() != keys.len() || fields[0] != keys[0] {
4507                return Err(PredicateApiError::InvalidResponse(
4508                    "controller plant resource member shape is invalid".to_string(),
4509                ));
4510            }
4511            let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
4512            if index != expected_index {
4513                return Err(PredicateApiError::InvalidResponse(
4514                    "controller plant resource member ordering changed".to_string(),
4515                ));
4516            }
4517            let answer = match token_value(fields[2], "answer")? {
4518                "SAFE" => ControllerMtbddAnswer::Safe,
4519                "UNSAFE" => ControllerMtbddAnswer::Unsafe,
4520                _ => {
4521                    return Err(PredicateApiError::InvalidResponse(
4522                        "controller plant resource member answer is invalid".to_string(),
4523                    ));
4524                }
4525            };
4526            let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
4527            let bad_frame = match token_value(fields[4], "bad_frame")? {
4528                "none" => None,
4529                value => Some(canonical_usize(value, "bad_frame")?),
4530            };
4531            let values = fields[5..]
4532                .iter()
4533                .zip(&keys[5..])
4534                .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4535                .collect::<Result<Vec<_>, _>>()?;
4536            if horizon > capabilities.max_horizon
4537                || match (answer, bad_frame) {
4538                    (ControllerMtbddAnswer::Safe, None) => values[0] != 0,
4539                    (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
4540                        frame > horizon || values[0] != frame.saturating_add(1)
4541                    }
4542                    _ => true,
4543                }
4544            {
4545                return Err(PredicateApiError::InvalidResponse(
4546                    "controller plant resource member result is inconsistent".to_string(),
4547                ));
4548            }
4549            member_results.push(ControllerMtbddMemberResult {
4550                index,
4551                answer,
4552                horizon,
4553                bad_frame,
4554                trace_steps: values[0],
4555                reachable_product_states: values[1],
4556                explored_transitions: values[2],
4557            });
4558        }
4559        let reachable_total = member_results.iter().try_fold(0usize, |total, member| {
4560            total.checked_add(member.reachable_product_states)
4561        });
4562        let transition_total = member_results.iter().try_fold(0usize, |total, member| {
4563            total.checked_add(member.explored_transitions)
4564        });
4565        if lines.next().is_some()
4566            || member_results
4567                .iter()
4568                .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
4569                .count()
4570                != numeric[4]
4571            || member_results
4572                .iter()
4573                .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Unsafe))
4574                .count()
4575                != numeric[5]
4576            || reachable_total != Some(numeric[6])
4577            || transition_total != Some(numeric[7])
4578        {
4579            return Err(PredicateApiError::InvalidResponse(
4580                "controller plant resource member totals disagree".to_string(),
4581            ));
4582        }
4583        Ok(ControllerPlantResourceSummary {
4584            policy_version,
4585            envelope_version,
4586            artifact_version,
4587            backend,
4588            members: member_count,
4589            maximum_member_horizon: numeric[1],
4590            maximum_product_states: numeric[2],
4591            transition_evaluation_bound: numeric[3],
4592            safe: numeric[4],
4593            unsafe_count: numeric[5],
4594            reachable_product_states: numeric[6],
4595            explored_transitions: numeric[7],
4596            artifact_bytes: numeric[8],
4597            load_micros: numeric[9],
4598            artifact_micros: numeric[10],
4599            verification_micros: numeric[11],
4600            elapsed_micros: numeric[12],
4601            member_results,
4602        })
4603    })();
4604    match parsed {
4605        Ok(value) => Ok(Observed { value, metrics }),
4606        Err(error) => {
4607            metrics.status = InvocationStatus::Failed(error.failure_class());
4608            Err(PredicateOperationError {
4609                error: Box::new(error),
4610                metrics,
4611            })
4612        }
4613    }
4614}
4615
4616fn parse_controller_proof_mtbdd_resource_summary(
4617    output: ManagedOutput,
4618    capabilities: &ControllerProofMtbddResourceCapabilities,
4619) -> Result<Observed<ControllerProofMtbddResourceSummary>, PredicateOperationError> {
4620    let (stdout, mut metrics) = successful_stdout(output)?;
4621    let parsed = (|| -> Result<ControllerProofMtbddResourceSummary, PredicateApiError> {
4622        if stdout.contains('\r') || !stdout.ends_with('\n') {
4623            return Err(PredicateApiError::InvalidResponse(
4624                "controller proof MTBDD resource response is not canonical LF text".to_string(),
4625            ));
4626        }
4627        let mut lines = stdout.lines();
4628        let first = lines.next().ok_or_else(|| {
4629            PredicateApiError::InvalidResponse(
4630                "controller proof MTBDD resource summary line is missing".to_string(),
4631            )
4632        })?;
4633        let fields = first.split(' ').collect::<Vec<_>>();
4634        let keys = [
4635            "controller-proof-mtbdd-resource",
4636            "status",
4637            "cli_version",
4638            "policy_version",
4639            "envelope_version",
4640            "artifact_version",
4641            "members",
4642            "maximum_member_horizon",
4643            "maximum_product_states",
4644            "transition_evaluation_bound",
4645            "equivalence_artifact_bytes",
4646            "unsat_proof_bytes",
4647            "safe",
4648            "unsafe",
4649            "reachable_product_states",
4650            "explored_transitions",
4651            "artifact_bytes",
4652            "assignments_checked",
4653            "load_micros",
4654            "artifact_micros",
4655            "verification_micros",
4656            "elapsed_micros",
4657        ];
4658        if fields.len() != keys.len()
4659            || fields[0] != keys[0]
4660            || token_value(fields[1], "status")? != "VERIFIED"
4661        {
4662            return Err(PredicateApiError::InvalidResponse(
4663                "controller proof MTBDD resource summary shape is invalid".to_string(),
4664            ));
4665        }
4666        let cli_version = canonical_u32(token_value(fields[2], keys[2])?, keys[2])?;
4667        let policy_version = canonical_u32(token_value(fields[3], keys[3])?, keys[3])?;
4668        let envelope_version = canonical_u32(token_value(fields[4], keys[4])?, keys[4])?;
4669        let artifact_version = canonical_u32(token_value(fields[5], keys[5])?, keys[5])?;
4670        if cli_version != capabilities.cli_version
4671            || policy_version != capabilities.policy_version
4672            || envelope_version != capabilities.envelope_version
4673            || artifact_version != capabilities.artifact_version
4674        {
4675            return Err(PredicateApiError::IncompatibleContract(
4676                "controller proof MTBDD resource response version changed".to_string(),
4677            ));
4678        }
4679        let numeric = fields[6..]
4680            .iter()
4681            .zip(&keys[6..])
4682            .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4683            .collect::<Result<Vec<_>, _>>()?;
4684        let member_count = numeric[0];
4685        if member_count == 0
4686            || member_count > capabilities.max_members
4687            || numeric[1] > capabilities.max_horizon
4688            || numeric[2] > capabilities.max_product_states
4689            || numeric[4] > capabilities.max_equivalence_artifact_bytes
4690            || numeric[5] > capabilities.max_unsat_proof_bytes
4691            || numeric[10] > capabilities.max_artifact_bytes
4692            || numeric[11] != 0
4693            || numeric[6].checked_add(numeric[7]) != Some(member_count)
4694        {
4695            return Err(PredicateApiError::InvalidResponse(
4696                "controller proof MTBDD resource response exceeds discovered limits".to_string(),
4697            ));
4698        }
4699        let mut member_results = Vec::with_capacity(member_count);
4700        for expected_index in 0..member_count {
4701            let line = lines.next().ok_or_else(|| {
4702                PredicateApiError::InvalidResponse(
4703                    "controller proof MTBDD resource member line is missing".to_string(),
4704                )
4705            })?;
4706            let fields = line.split(' ').collect::<Vec<_>>();
4707            let keys = [
4708                "controller-proof-mtbdd-resource-member",
4709                "index",
4710                "answer",
4711                "horizon",
4712                "bad_frame",
4713                "trace_steps",
4714                "reachable_product_states",
4715                "explored_transitions",
4716            ];
4717            if fields.len() != keys.len() || fields[0] != keys[0] {
4718                return Err(PredicateApiError::InvalidResponse(
4719                    "controller proof MTBDD resource member shape is invalid".to_string(),
4720                ));
4721            }
4722            let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
4723            if index != expected_index {
4724                return Err(PredicateApiError::InvalidResponse(
4725                    "controller proof MTBDD resource member ordering changed".to_string(),
4726                ));
4727            }
4728            let answer = match token_value(fields[2], "answer")? {
4729                "SAFE" => ControllerMtbddAnswer::Safe,
4730                "UNSAFE" => ControllerMtbddAnswer::Unsafe,
4731                _ => {
4732                    return Err(PredicateApiError::InvalidResponse(
4733                        "controller proof MTBDD resource member answer is invalid".to_string(),
4734                    ));
4735                }
4736            };
4737            let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
4738            let bad_frame = match token_value(fields[4], "bad_frame")? {
4739                "none" => None,
4740                value => Some(canonical_usize(value, "bad_frame")?),
4741            };
4742            let values = fields[5..]
4743                .iter()
4744                .zip(&keys[5..])
4745                .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4746                .collect::<Result<Vec<_>, _>>()?;
4747            if horizon > capabilities.max_horizon
4748                || match (answer, bad_frame) {
4749                    (ControllerMtbddAnswer::Safe, None) => values[0] != 0,
4750                    (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
4751                        frame > horizon || values[0] != frame.saturating_add(1)
4752                    }
4753                    _ => true,
4754                }
4755            {
4756                return Err(PredicateApiError::InvalidResponse(
4757                    "controller proof MTBDD resource member result is inconsistent".to_string(),
4758                ));
4759            }
4760            member_results.push(ControllerMtbddMemberResult {
4761                index,
4762                answer,
4763                horizon,
4764                bad_frame,
4765                trace_steps: values[0],
4766                reachable_product_states: values[1],
4767                explored_transitions: values[2],
4768            });
4769        }
4770        let safe = member_results
4771            .iter()
4772            .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
4773            .count();
4774        let unsafe_count = member_results.len() - safe;
4775        let reachable = member_results.iter().try_fold(0usize, |total, member| {
4776            total.checked_add(member.reachable_product_states)
4777        });
4778        let explored = member_results.iter().try_fold(0usize, |total, member| {
4779            total.checked_add(member.explored_transitions)
4780        });
4781        if lines.next().is_some()
4782            || safe != numeric[6]
4783            || unsafe_count != numeric[7]
4784            || reachable != Some(numeric[8])
4785            || explored != Some(numeric[9])
4786        {
4787            return Err(PredicateApiError::InvalidResponse(
4788                "controller proof MTBDD resource member totals disagree".to_string(),
4789            ));
4790        }
4791        Ok(ControllerProofMtbddResourceSummary {
4792            policy_version,
4793            envelope_version,
4794            artifact_version,
4795            members: member_count,
4796            maximum_member_horizon: numeric[1],
4797            maximum_product_states: numeric[2],
4798            transition_evaluation_bound: numeric[3],
4799            equivalence_artifact_bytes: numeric[4],
4800            unsat_proof_bytes: numeric[5],
4801            safe: numeric[6],
4802            unsafe_count: numeric[7],
4803            reachable_product_states: numeric[8],
4804            explored_transitions: numeric[9],
4805            artifact_bytes: numeric[10],
4806            assignments_checked: numeric[11],
4807            load_micros: numeric[12],
4808            artifact_micros: numeric[13],
4809            verification_micros: numeric[14],
4810            elapsed_micros: numeric[15],
4811            member_results,
4812        })
4813    })();
4814    match parsed {
4815        Ok(value) => Ok(Observed { value, metrics }),
4816        Err(error) => {
4817            metrics.status = InvocationStatus::Failed(error.failure_class());
4818            Err(PredicateOperationError {
4819                error: Box::new(error),
4820                metrics,
4821            })
4822        }
4823    }
4824}
4825
4826fn parse_controller_proof_mtbdd_portfolio_resource_summary(
4827    output: ManagedOutput,
4828    capabilities: &ControllerProofMtbddPortfolioCapabilities,
4829    require_attestation: bool,
4830) -> Result<Observed<ControllerProofMtbddPortfolioResourceSummary>, PredicateOperationError> {
4831    let (stdout, mut metrics) = successful_stdout(output)?;
4832    let parsed = (|| -> Result<ControllerProofMtbddPortfolioResourceSummary, PredicateApiError> {
4833        if stdout.contains('\r') || !stdout.ends_with('\n') {
4834            return Err(PredicateApiError::InvalidResponse(
4835                "controller proof MTBDD portfolio response is not canonical LF text".to_string(),
4836            ));
4837        }
4838        let mut lines = stdout.lines();
4839        let first = lines.next().ok_or_else(|| {
4840            PredicateApiError::InvalidResponse(
4841                "controller proof MTBDD portfolio summary line is missing".to_string(),
4842            )
4843        })?;
4844        let fields = first.split(' ').collect::<Vec<_>>();
4845        let keys = [
4846            "controller-proof-mtbdd-portfolio-resource",
4847            "status",
4848            "cli_version",
4849            "policy_version",
4850            "envelope_version",
4851            "artifact_version",
4852            "backend",
4853            "reason",
4854            "members",
4855            "maximum_member_horizon",
4856            "maximum_product_states",
4857            "transition_evaluation_bound",
4858            "equivalence_artifact_bytes",
4859            "unsat_proof_bytes",
4860            "safe",
4861            "unsafe",
4862            "reachable_product_states",
4863            "explored_transitions",
4864            "artifact_bytes",
4865            "assignments_checked",
4866            "load_micros",
4867            "artifact_micros",
4868            "verification_micros",
4869            "elapsed_micros",
4870        ];
4871        let attested = fields.len() == keys.len() + 5;
4872        if attested != require_attestation
4873            || (!attested && fields.len() != keys.len())
4874            || fields[0] != keys[0]
4875            || token_value(fields[1], "status")? != "VERIFIED"
4876        {
4877            return Err(PredicateApiError::InvalidResponse(
4878                "controller proof MTBDD portfolio summary shape is invalid".to_string(),
4879            ));
4880        }
4881        if attested {
4882            let attestation_keys = [
4883                "source_model_attestation_version",
4884                "source_model_members",
4885                "source_model_tool",
4886                "source_model_tool_revision",
4887                "provenance",
4888            ];
4889            let attestation_fields = &fields[keys.len()..];
4890            let version = canonical_u32(
4891                token_value(attestation_fields[0], attestation_keys[0])?,
4892                attestation_keys[0],
4893            )?;
4894            let members = canonical_usize(
4895                token_value(attestation_fields[1], attestation_keys[1])?,
4896                attestation_keys[1],
4897            )?;
4898            let tool = token_value(attestation_fields[2], attestation_keys[2])?;
4899            let revision = token_value(attestation_fields[3], attestation_keys[3])?;
4900            let provenance = token_value(attestation_fields[4], attestation_keys[4])?;
4901            if version != capabilities.source_model_attestation_version
4902                || members == 0
4903                || members > capabilities.max_members.saturating_add(1)
4904                || tool != "yosys"
4905                || revision.len() != 40
4906                || !revision
4907                    .bytes()
4908                    .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
4909                || provenance != "BOUND"
4910            {
4911                return Err(PredicateApiError::InvalidResponse(
4912                    "controller proof MTBDD portfolio attestation response is invalid".to_string(),
4913                ));
4914            }
4915        }
4916        let cli_version = canonical_u32(token_value(fields[2], keys[2])?, keys[2])?;
4917        let policy_version = canonical_u32(token_value(fields[3], keys[3])?, keys[3])?;
4918        let envelope_version = canonical_u32(token_value(fields[4], keys[4])?, keys[4])?;
4919        let artifact_version = canonical_u32(token_value(fields[5], keys[5])?, keys[5])?;
4920        if cli_version != capabilities.cli_version
4921            || policy_version != capabilities.policy_version
4922            || envelope_version != capabilities.envelope_version
4923            || artifact_version != capabilities.artifact_version
4924        {
4925            return Err(PredicateApiError::IncompatibleContract(
4926                "controller proof MTBDD portfolio response version changed".to_string(),
4927            ));
4928        }
4929        let backend = match token_value(fields[6], keys[6])? {
4930            "PROOF_MTBDD" => ControllerPlantPortfolioBackend::Mtbdd,
4931            "DIRECT_EXACT" => ControllerPlantPortfolioBackend::DirectExact,
4932            _ => {
4933                return Err(PredicateApiError::InvalidResponse(
4934                    "controller proof MTBDD portfolio backend is invalid".to_string(),
4935                ));
4936            }
4937        };
4938        let reason = match token_value(fields[7], keys[7])? {
4939            "MTBDD_ADMITTED" => ControllerPlantPortfolioReason::MtbddAdmitted,
4940            "BOUNDARY_LIMIT" => ControllerPlantPortfolioReason::BoundaryLimit,
4941            "TERMINAL_LIMIT" => ControllerPlantPortfolioReason::TerminalLimit,
4942            "NODE_LIMIT" => ControllerPlantPortfolioReason::NodeLimit,
4943            _ => {
4944                return Err(PredicateApiError::InvalidResponse(
4945                    "controller proof MTBDD portfolio reason is invalid".to_string(),
4946                ));
4947            }
4948        };
4949        if matches!(backend, ControllerPlantPortfolioBackend::Mtbdd)
4950            != matches!(reason, ControllerPlantPortfolioReason::MtbddAdmitted)
4951        {
4952            return Err(PredicateApiError::InvalidResponse(
4953                "controller proof MTBDD portfolio route is inconsistent".to_string(),
4954            ));
4955        }
4956        let numeric = fields[8..keys.len()]
4957            .iter()
4958            .zip(&keys[8..])
4959            .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4960            .collect::<Result<Vec<_>, _>>()?;
4961        let member_count = numeric[0];
4962        if member_count == 0
4963            || member_count > capabilities.max_members
4964            || numeric[1] > capabilities.max_horizon
4965            || numeric[2] > capabilities.max_product_states
4966            || numeric[4] > capabilities.max_equivalence_artifact_bytes
4967            || numeric[5] > capabilities.max_unsat_proof_bytes
4968            || numeric[10] > capabilities.max_artifact_bytes
4969            || numeric[11] != 0
4970            || numeric[6].checked_add(numeric[7]) != Some(member_count)
4971            || (matches!(backend, ControllerPlantPortfolioBackend::DirectExact)
4972                && (numeric[4] != 0 || numeric[5] != 0))
4973        {
4974            return Err(PredicateApiError::InvalidResponse(
4975                "controller proof MTBDD portfolio response exceeds discovered limits".to_string(),
4976            ));
4977        }
4978        let mut member_results = Vec::with_capacity(member_count);
4979        for expected_index in 0..member_count {
4980            let line = lines.next().ok_or_else(|| {
4981                PredicateApiError::InvalidResponse(
4982                    "controller proof MTBDD portfolio member line is missing".to_string(),
4983                )
4984            })?;
4985            let fields = line.split(' ').collect::<Vec<_>>();
4986            let keys = [
4987                "controller-proof-mtbdd-portfolio-resource-member",
4988                "index",
4989                "answer",
4990                "horizon",
4991                "bad_frame",
4992                "trace_steps",
4993                "reachable_product_states",
4994                "explored_transitions",
4995            ];
4996            if fields.len() != keys.len() || fields[0] != keys[0] {
4997                return Err(PredicateApiError::InvalidResponse(
4998                    "controller proof MTBDD portfolio member shape is invalid".to_string(),
4999                ));
5000            }
5001            let index = canonical_usize(token_value(fields[1], keys[1])?, keys[1])?;
5002            if index != expected_index {
5003                return Err(PredicateApiError::InvalidResponse(
5004                    "controller proof MTBDD portfolio member ordering changed".to_string(),
5005                ));
5006            }
5007            let answer = match token_value(fields[2], keys[2])? {
5008                "SAFE" => ControllerMtbddAnswer::Safe,
5009                "UNSAFE" => ControllerMtbddAnswer::Unsafe,
5010                _ => {
5011                    return Err(PredicateApiError::InvalidResponse(
5012                        "controller proof MTBDD portfolio member answer is invalid".to_string(),
5013                    ));
5014                }
5015            };
5016            let horizon = canonical_usize(token_value(fields[3], keys[3])?, keys[3])?;
5017            let bad_frame = match token_value(fields[4], keys[4])? {
5018                "none" => None,
5019                value => Some(canonical_usize(value, keys[4])?),
5020            };
5021            let values = fields[5..]
5022                .iter()
5023                .zip(&keys[5..])
5024                .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
5025                .collect::<Result<Vec<_>, _>>()?;
5026            if horizon > capabilities.max_horizon
5027                || match (answer, bad_frame) {
5028                    (ControllerMtbddAnswer::Safe, None) => values[0] != 0,
5029                    (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
5030                        frame > horizon || values[0] != frame.saturating_add(1)
5031                    }
5032                    _ => true,
5033                }
5034            {
5035                return Err(PredicateApiError::InvalidResponse(
5036                    "controller proof MTBDD portfolio member result is inconsistent".to_string(),
5037                ));
5038            }
5039            member_results.push(ControllerMtbddMemberResult {
5040                index,
5041                answer,
5042                horizon,
5043                bad_frame,
5044                trace_steps: values[0],
5045                reachable_product_states: values[1],
5046                explored_transitions: values[2],
5047            });
5048        }
5049        let safe = member_results
5050            .iter()
5051            .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
5052            .count();
5053        let unsafe_count = member_results.len() - safe;
5054        let reachable = member_results.iter().try_fold(0usize, |total, member| {
5055            total.checked_add(member.reachable_product_states)
5056        });
5057        let explored = member_results.iter().try_fold(0usize, |total, member| {
5058            total.checked_add(member.explored_transitions)
5059        });
5060        if lines.next().is_some()
5061            || safe != numeric[6]
5062            || unsafe_count != numeric[7]
5063            || reachable != Some(numeric[8])
5064            || explored != Some(numeric[9])
5065        {
5066            return Err(PredicateApiError::InvalidResponse(
5067                "controller proof MTBDD portfolio member totals disagree".to_string(),
5068            ));
5069        }
5070        Ok(ControllerProofMtbddPortfolioResourceSummary {
5071            policy_version,
5072            envelope_version,
5073            artifact_version,
5074            backend,
5075            reason,
5076            members: member_count,
5077            maximum_member_horizon: numeric[1],
5078            maximum_product_states: numeric[2],
5079            transition_evaluation_bound: numeric[3],
5080            equivalence_artifact_bytes: numeric[4],
5081            unsat_proof_bytes: numeric[5],
5082            safe: numeric[6],
5083            unsafe_count: numeric[7],
5084            reachable_product_states: numeric[8],
5085            explored_transitions: numeric[9],
5086            artifact_bytes: numeric[10],
5087            assignments_checked: numeric[11],
5088            load_micros: numeric[12],
5089            artifact_micros: numeric[13],
5090            verification_micros: numeric[14],
5091            elapsed_micros: numeric[15],
5092            member_results,
5093        })
5094    })();
5095    match parsed {
5096        Ok(value) => Ok(Observed { value, metrics }),
5097        Err(error) => {
5098            metrics.status = InvocationStatus::Failed(error.failure_class());
5099            Err(PredicateOperationError {
5100                error: Box::new(error),
5101                metrics,
5102            })
5103        }
5104    }
5105}
5106
5107fn canonical_node_list(nodes: &[u64]) -> Result<String, PredicateApiError> {
5108    if nodes.is_empty()
5109        || nodes
5110            .windows(2)
5111            .any(|pair| pair[0] == 0 || pair[0] >= pair[1])
5112        || nodes.last() == Some(&0)
5113    {
5114        return Err(PredicateApiError::InvalidPolicy(
5115            "revision impact output nodes must be nonempty, nonzero, unique, and strictly increasing"
5116                .to_string(),
5117        ));
5118    }
5119    Ok(nodes
5120        .iter()
5121        .map(u64::to_string)
5122        .collect::<Vec<_>>()
5123        .join(","))
5124}
5125
5126fn parse_revision_impact_capabilities(
5127    line: &str,
5128) -> Result<RevisionImpactCapabilities, PredicateApiError> {
5129    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5130        return Err(PredicateApiError::InvalidResponse(
5131            "revision impact capability line is not canonical".to_string(),
5132        ));
5133    }
5134    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5135    let keys = [
5136        "revision_impact_cli_version",
5137        "impact_version",
5138        "query_manifest_version",
5139        "max_query_manifest_bytes",
5140        "max_input_bytes",
5141        "max_evidence_bytes",
5142        "max_bundle_bytes",
5143        "max_atoms",
5144        "max_combinations",
5145        "max_queries",
5146        "semantics",
5147        "work_schema",
5148        "query_schema",
5149        "routing",
5150        "fallback",
5151        "unsupported",
5152    ];
5153    if fields.len() != keys.len() {
5154        return Err(PredicateApiError::InvalidResponse(
5155            "revision impact capability field count is invalid".to_string(),
5156        ));
5157    }
5158    let values = fields
5159        .iter()
5160        .zip(keys)
5161        .map(|(field, key)| token_value(field, key))
5162        .collect::<Result<Vec<_>, _>>()?;
5163    if values[10..]
5164        != [
5165            "exact-counterfactual-v1",
5166            "verification-v1",
5167            "transition-semantic-set-v1",
5168            "none",
5169            "none",
5170            "fail-closed",
5171        ]
5172    {
5173        return Err(PredicateApiError::IncompatibleContract(
5174            "revision impact semantic or fallback contract is unsupported".to_string(),
5175        ));
5176    }
5177    let versions = values[..3]
5178        .iter()
5179        .enumerate()
5180        .map(|(index, value)| canonical_u32(value, keys[index]))
5181        .collect::<Result<Vec<_>, _>>()?;
5182    let limits = values[3..10]
5183        .iter()
5184        .enumerate()
5185        .map(|(index, value)| canonical_usize(value, keys[index + 3]))
5186        .collect::<Result<Vec<_>, _>>()?;
5187    if versions != [2, 1, 1] {
5188        return Err(PredicateApiError::IncompatibleContract(
5189            "revision impact version tuple is unsupported".to_string(),
5190        ));
5191    }
5192    let expected = [
5193        16 * 1024,
5194        64 * 1024 * 1024,
5195        16 * 1024 * 1024,
5196        64 * 1024 * 1024,
5197        revision_impact::MAX_IMPACT_ATOMS,
5198        revision_impact::MAX_IMPACT_COMBINATIONS,
5199        revision_impact::MAX_IMPACT_QUERIES,
5200    ];
5201    if limits != expected {
5202        return Err(PredicateApiError::IncompatibleContract(
5203            "revision impact limit tuple is unsupported".to_string(),
5204        ));
5205    }
5206    Ok(RevisionImpactCapabilities {
5207        cli_version: versions[0],
5208        impact_version: versions[1],
5209        query_manifest_version: versions[2],
5210        max_query_manifest_bytes: limits[0],
5211        max_input_bytes: limits[1],
5212        max_evidence_bytes: limits[2],
5213        max_bundle_bytes: limits[3],
5214        max_atoms: limits[4],
5215        max_combinations: limits[5],
5216        max_queries: limits[6],
5217    })
5218}
5219
5220fn parse_btor2_channel_property_capabilities(
5221    line: &str,
5222) -> Result<Btor2ChannelPropertyCapabilities, PredicateApiError> {
5223    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5224        return Err(PredicateApiError::InvalidResponse(
5225            "BTOR2 channel property capability line is not canonical".to_string(),
5226        ));
5227    }
5228    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5229    let keys = [
5230        "btor2_channel_property_cli_version",
5231        "artifact_version",
5232        "query_manifest_version",
5233        "policy_version",
5234        "max_query_manifest_bytes",
5235        "max_policy_bytes",
5236        "max_model_bytes",
5237        "max_channels",
5238        "max_queries",
5239        "max_evidence_bytes",
5240        "max_artifact_bytes",
5241        "max_projected_work",
5242        "refusal_exit",
5243        "routing",
5244        "fallback",
5245        "result_on_refusal",
5246        "refusal_schema",
5247        "unsupported",
5248        "verification",
5249    ];
5250    if fields.len() != keys.len() {
5251        return Err(PredicateApiError::InvalidResponse(
5252            "BTOR2 channel property capability field count is invalid".to_string(),
5253        ));
5254    }
5255    let values = fields
5256        .iter()
5257        .zip(keys)
5258        .map(|(field, key)| token_value(field, key))
5259        .collect::<Result<Vec<_>, _>>()?;
5260    if values[13..]
5261        != [
5262            "static-explicit-or-bitblast",
5263            "exact",
5264            "none",
5265            "reason-v1",
5266            "fail-closed",
5267            "source-replay",
5268        ]
5269    {
5270        return Err(PredicateApiError::IncompatibleContract(
5271            "BTOR2 channel property route or refusal contract is unsupported".to_string(),
5272        ));
5273    }
5274    let versions = values[..4]
5275        .iter()
5276        .enumerate()
5277        .map(|(index, value)| canonical_u32(value, keys[index]))
5278        .collect::<Result<Vec<_>, _>>()?;
5279    if versions != [1, 1, 1, 1] {
5280        return Err(PredicateApiError::IncompatibleContract(
5281            "BTOR2 channel property version tuple is unsupported".to_string(),
5282        ));
5283    }
5284    let limits = values[4..11]
5285        .iter()
5286        .enumerate()
5287        .map(|(index, value)| canonical_usize(value, keys[index + 4]))
5288        .collect::<Result<Vec<_>, _>>()?;
5289    let max_projected_work = canonical_u64(values[11], keys[11])?;
5290    let refusal_exit = canonical_usize(values[12], keys[12])?;
5291    let expected = [
5292        256 * 1024,
5293        4096,
5294        btor2::MAX_BTOR2_BYTES,
5295        btor2_region_extract::MAX_REGION_CHANNELS,
5296        btor2_region_property::MAX_CHANNEL_PROPERTY_QUERIES,
5297        btor2_region_property::MAX_CHANNEL_PROPERTY_EVIDENCE_BYTES,
5298        btor2_region_property::MAX_CHANNEL_PROPERTY_ARTIFACT_BYTES,
5299    ];
5300    if limits != expected
5301        || max_projected_work != btor2_region_property::MAX_CHANNEL_PROPERTY_PROJECTED_WORK
5302        || refusal_exit != 3
5303    {
5304        return Err(PredicateApiError::IncompatibleContract(
5305            "BTOR2 channel property limit tuple is unsupported".to_string(),
5306        ));
5307    }
5308    Ok(Btor2ChannelPropertyCapabilities {
5309        cli_version: versions[0],
5310        artifact_version: versions[1],
5311        query_manifest_version: versions[2],
5312        policy_version: versions[3],
5313        max_query_manifest_bytes: limits[0],
5314        max_policy_bytes: limits[1],
5315        max_model_bytes: limits[2],
5316        max_channels: limits[3],
5317        max_queries: limits[4],
5318        max_evidence_bytes: limits[5],
5319        max_artifact_bytes: limits[6],
5320        max_projected_work,
5321        refusal_exit_code: 3,
5322    })
5323}
5324
5325fn parse_btor2_channel_property_observability_capabilities(
5326    line: &str,
5327    base: &Btor2ChannelPropertyCapabilities,
5328) -> Result<Btor2ChannelPropertyObservabilityCapabilities, PredicateApiError> {
5329    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5330        return Err(PredicateApiError::InvalidResponse(
5331            "BTOR2 channel property observability capability line is not canonical".to_string(),
5332        ));
5333    }
5334    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5335    let keys = [
5336        "btor2_channel_property_observability_cli_version",
5337        "base_cli_version",
5338        "phase_metrics_version",
5339        "phases",
5340        "timing_calibration",
5341        "correctness_dependency",
5342        "partial_metrics_on_failure",
5343        "unsupported",
5344    ];
5345    if fields.len() != keys.len() {
5346        return Err(PredicateApiError::InvalidResponse(
5347            "BTOR2 channel property observability capability field count is invalid".to_string(),
5348        ));
5349    }
5350    let values = fields
5351        .iter()
5352        .zip(keys)
5353        .map(|(field, key)| token_value(field, key))
5354        .collect::<Result<Vec<_>, _>>()?;
5355    let cli_version = canonical_u32(values[0], keys[0])?;
5356    let base_cli_version = canonical_u32(values[1], keys[1])?;
5357    let phase_metrics_version = canonical_u32(values[2], keys[2])?;
5358    if cli_version != 1
5359        || base_cli_version != base.cli_version
5360        || phase_metrics_version != 1
5361        || values[3]
5362            != "input,structural-admission,preflight,proof-construction,encoding,artifact-decode,source-replay,publication"
5363        || values[4..] != ["none", "none", "none", "fail-closed"]
5364    {
5365        return Err(PredicateApiError::IncompatibleContract(
5366            "BTOR2 channel property observability contract is unsupported".to_string(),
5367        ));
5368    }
5369    Ok(Btor2ChannelPropertyObservabilityCapabilities {
5370        cli_version,
5371        phase_metrics_version,
5372    })
5373}
5374
5375fn parse_btor2_channel_property_phase_line(
5376    line: &str,
5377    expected_operation: Btor2ChannelPropertyObservedOperation,
5378    capabilities: &Btor2ChannelPropertyObservabilityCapabilities,
5379) -> Result<Btor2ChannelPropertyPhaseMetrics, PredicateApiError> {
5380    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5381        return Err(PredicateApiError::InvalidResponse(
5382            "BTOR2 channel property phase row is not canonical".to_string(),
5383        ));
5384    }
5385    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5386    let keys = [
5387        "status",
5388        "observability_cli_version",
5389        "phase_metrics_version",
5390        "operation",
5391        "input_micros",
5392        "structural_admission_micros",
5393        "preflight_micros",
5394        "proof_construction_micros",
5395        "encoding_micros",
5396        "artifact_decode_micros",
5397        "source_replay_micros",
5398        "publication_micros",
5399        "total_micros",
5400        "timing_calibration",
5401        "correctness_dependency",
5402    ];
5403    if fields.len() != keys.len() + 1
5404        || fields.first().copied() != Some("btor2-channel-property-phases")
5405    {
5406        return Err(PredicateApiError::InvalidResponse(
5407            "BTOR2 channel property phase row shape is invalid".to_string(),
5408        ));
5409    }
5410    let values = fields[1..]
5411        .iter()
5412        .zip(keys)
5413        .map(|(field, key)| token_value(field, key))
5414        .collect::<Result<Vec<_>, _>>()?;
5415    let cli_version = canonical_u32(values[1], keys[1])?;
5416    let phase_metrics_version = canonical_u32(values[2], keys[2])?;
5417    let operation = match values[3] {
5418        "certify" => Btor2ChannelPropertyObservedOperation::Certify,
5419        "verify" => Btor2ChannelPropertyObservedOperation::Verify,
5420        _ => {
5421            return Err(PredicateApiError::InvalidResponse(
5422                "unknown BTOR2 channel property observed operation".to_string(),
5423            ));
5424        }
5425    };
5426    let timings = values[4..13]
5427        .iter()
5428        .enumerate()
5429        .map(|(index, value)| canonical_u128(value, keys[index + 4]))
5430        .collect::<Result<Vec<_>, _>>()?;
5431    let phase_sum = timings[..8]
5432        .iter()
5433        .try_fold(0u128, |total, value| total.checked_add(*value))
5434        .ok_or_else(|| {
5435            PredicateApiError::InvalidResponse(
5436                "BTOR2 channel property phase total overflows".to_string(),
5437            )
5438        })?;
5439    if values[0] != "MEASURED"
5440        || cli_version != capabilities.cli_version
5441        || phase_metrics_version != capabilities.phase_metrics_version
5442        || operation != expected_operation
5443        || phase_sum > timings[8]
5444        || values[13..] != ["none", "none"]
5445        || (operation == Btor2ChannelPropertyObservedOperation::Verify
5446            && [timings[1], timings[2], timings[3], timings[4], timings[7]] != [0; 5])
5447    {
5448        return Err(PredicateApiError::InvalidResponse(
5449            "BTOR2 channel property phase row violates the observed contract".to_string(),
5450        ));
5451    }
5452    Ok(Btor2ChannelPropertyPhaseMetrics {
5453        version: phase_metrics_version,
5454        operation,
5455        input_micros: timings[0],
5456        structural_admission_micros: timings[1],
5457        preflight_micros: timings[2],
5458        proof_construction_micros: timings[3],
5459        encoding_micros: timings[4],
5460        artifact_decode_micros: timings[5],
5461        source_replay_micros: timings[6],
5462        publication_micros: timings[7],
5463        total_micros: timings[8],
5464    })
5465}
5466
5467fn parse_btor2_channel_property_phase_observed_summary(
5468    mut output: ManagedOutput,
5469    expected_status: &str,
5470    expected_operation: Btor2ChannelPropertyObservedOperation,
5471    base_capabilities: &Btor2ChannelPropertyCapabilities,
5472    capabilities: &Btor2ChannelPropertyObservabilityCapabilities,
5473) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
5474    if !output.status.success() {
5475        return match parse_btor2_channel_property_summary(
5476            output,
5477            expected_status,
5478            base_capabilities,
5479        ) {
5480            Err(error) => Err(error),
5481            Ok(observed) => {
5482                let error = PredicateApiError::InvalidResponse(
5483                    "non-successful observed process produced a successful summary".to_string(),
5484                );
5485                let mut metrics = observed.metrics;
5486                metrics.status = InvocationStatus::Failed(error.failure_class());
5487                Err(PredicateOperationError {
5488                    error: Box::new(error),
5489                    metrics,
5490                })
5491            }
5492        };
5493    }
5494    let phase_start = output.stdout[..output.stdout.len().saturating_sub(1)]
5495        .iter()
5496        .rposition(|byte| *byte == b'\n')
5497        .map_or(0, |index| index + 1);
5498    let phase_bytes = output.stdout.split_off(phase_start);
5499    let phase_line = String::from_utf8(phase_bytes).map_err(|_| {
5500        let error = PredicateApiError::InvalidResponse(
5501            "BTOR2 channel property phase row is not UTF-8".to_string(),
5502        );
5503        output.metrics.status = InvocationStatus::Failed(error.failure_class());
5504        PredicateOperationError {
5505            error: Box::new(error),
5506            metrics: output.metrics.clone(),
5507        }
5508    })?;
5509    let phases =
5510        parse_btor2_channel_property_phase_line(&phase_line, expected_operation, capabilities)
5511            .map_err(|error| {
5512                output.metrics.status = InvocationStatus::Failed(error.failure_class());
5513                PredicateOperationError {
5514                    error: Box::new(error),
5515                    metrics: output.metrics.clone(),
5516                }
5517            })?;
5518    let verification =
5519        parse_btor2_channel_property_summary(output, expected_status, base_capabilities)?;
5520    Ok(Observed {
5521        value: Btor2ChannelPropertyPhaseObservedSummary {
5522            verification: verification.value,
5523            phases,
5524        },
5525        metrics: verification.metrics,
5526    })
5527}
5528
5529fn parse_btor2_channel_property_summary(
5530    mut output: ManagedOutput,
5531    expected_status: &str,
5532    capabilities: &Btor2ChannelPropertyCapabilities,
5533) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
5534    if output.status.code() == Some(capabilities.refusal_exit_code) {
5535        let stderr = String::from_utf8_lossy(&output.stderr);
5536        let reason = stderr
5537            .strip_prefix("error: btor2-channel-property-resource refusal=")
5538            .and_then(|value| value.strip_suffix(" result=none\n"))
5539            .and_then(|value| match value {
5540                "query-count" => Some(Btor2ChannelPropertyResourceRefusalReason::QueryCount),
5541                "member-count" => Some(Btor2ChannelPropertyResourceRefusalReason::MemberCount),
5542                "projected-work" => Some(Btor2ChannelPropertyResourceRefusalReason::ProjectedWork),
5543                "evidence-bytes" => Some(Btor2ChannelPropertyResourceRefusalReason::EvidenceBytes),
5544                "artifact-bytes" => Some(Btor2ChannelPropertyResourceRefusalReason::ArtifactBytes),
5545                _ => None,
5546            });
5547        if !output.stdout.is_empty() {
5548            let error = PredicateApiError::InvalidResponse(
5549                "BTOR2 channel property refusal emitted stdout".to_string(),
5550            );
5551            output.metrics.status = InvocationStatus::Failed(error.failure_class());
5552            return Err(PredicateOperationError {
5553                error: Box::new(error),
5554                metrics: output.metrics,
5555            });
5556        }
5557        let Some(reason) = reason else {
5558            let error = PredicateApiError::InvalidResponse(
5559                "BTOR2 channel property refusal is malformed".to_string(),
5560            );
5561            output.metrics.status = InvocationStatus::Failed(error.failure_class());
5562            return Err(PredicateOperationError {
5563                error: Box::new(error),
5564                metrics: output.metrics,
5565            });
5566        };
5567        let error = PredicateApiError::Btor2ChannelPropertyResourceRefused { reason };
5568        output.metrics.status = InvocationStatus::Failed(error.failure_class());
5569        return Err(PredicateOperationError {
5570            error: Box::new(error),
5571            metrics: output.metrics,
5572        });
5573    }
5574    let (stdout, mut metrics) = successful_stdout(output)?;
5575    let parsed = (|| -> Result<Btor2ChannelPropertyProcessSummary, PredicateApiError> {
5576        if stdout.contains('\r') || !stdout.ends_with('\n') {
5577            return Err(PredicateApiError::InvalidResponse(
5578                "BTOR2 channel property response is not canonical LF text".to_string(),
5579            ));
5580        }
5581        let lines = stdout.lines().collect::<Vec<_>>();
5582        let fields = lines
5583            .first()
5584            .ok_or_else(|| {
5585                PredicateApiError::InvalidResponse(
5586                    "BTOR2 channel property response is empty".to_string(),
5587                )
5588            })?
5589            .split(' ')
5590            .collect::<Vec<_>>();
5591        let keys = [
5592            "status",
5593            "cli_version",
5594            "artifact_version",
5595            "channels",
5596            "logical_queries",
5597            "proof_members",
5598            "reused_queries",
5599            "explicit_members",
5600            "bitblast_members",
5601            "evidence_bytes",
5602            "artifact_bytes",
5603            "projected_work",
5604            "elapsed_micros",
5605        ];
5606        if fields.len() != keys.len() + 1
5607            || fields.first().copied() != Some("btor2-channel-properties")
5608        {
5609            return Err(PredicateApiError::InvalidResponse(
5610                "BTOR2 channel property summary shape is invalid".to_string(),
5611            ));
5612        }
5613        let values = fields[1..]
5614            .iter()
5615            .zip(keys)
5616            .map(|(field, key)| token_value(field, key))
5617            .collect::<Result<Vec<_>, _>>()?;
5618        if values[0] != expected_status {
5619            return Err(PredicateApiError::InvalidResponse(
5620                "BTOR2 channel property status differs from requested operation".to_string(),
5621            ));
5622        }
5623        let cli_version = canonical_u32(values[1], keys[1])?;
5624        let artifact_version = canonical_u32(values[2], keys[2])?;
5625        let numeric = values[3..11]
5626            .iter()
5627            .enumerate()
5628            .map(|(index, value)| canonical_usize(value, keys[index + 3]))
5629            .collect::<Result<Vec<_>, _>>()?;
5630        let projected_work = match values[11] {
5631            "not-applied" if expected_status == "VERIFIED" => None,
5632            value if expected_status == "CREATED" => Some(canonical_u64(value, keys[11])?),
5633            _ => {
5634                return Err(PredicateApiError::InvalidResponse(
5635                    "BTOR2 channel property projected work differs from operation".to_string(),
5636                ));
5637            }
5638        };
5639        let elapsed_micros = canonical_usize(values[12], keys[12])?;
5640        if cli_version != capabilities.cli_version
5641            || artifact_version != capabilities.artifact_version
5642            || numeric[0] == 0
5643            || numeric[0] > capabilities.max_channels
5644            || numeric[1] == 0
5645            || numeric[1] > capabilities.max_queries
5646            || numeric[2] == 0
5647            || numeric[2] > numeric[1]
5648            || numeric[3].checked_add(numeric[2]) != Some(numeric[1])
5649            || numeric[4].checked_add(numeric[5]) != Some(numeric[2])
5650            || numeric[6] == 0
5651            || numeric[6] > capabilities.max_evidence_bytes
5652            || numeric[7] == 0
5653            || numeric[7] > capabilities.max_artifact_bytes
5654            || projected_work
5655                .is_some_and(|work| work == 0 || work > capabilities.max_projected_work)
5656        {
5657            return Err(PredicateApiError::InvalidResponse(
5658                "BTOR2 channel property summary violates advertised dimensions".to_string(),
5659            ));
5660        }
5661        if lines.len() != numeric[1] + 1 {
5662            return Err(PredicateApiError::InvalidResponse(
5663                "BTOR2 channel property result count is inconsistent".to_string(),
5664            ));
5665        }
5666        let mut results = Vec::with_capacity(numeric[1]);
5667        for (expected_index, line) in lines[1..].iter().enumerate() {
5668            let fields = line.split(' ').collect::<Vec<_>>();
5669            let result_keys = [
5670                "index",
5671                "query_id",
5672                "channel",
5673                "property",
5674                "horizon",
5675                "answer",
5676                "bad_frame",
5677                "backend",
5678                "solver",
5679                "representative_channel",
5680                "witness_valuations",
5681            ];
5682            if fields.len() != result_keys.len() + 1
5683                || fields.first().copied() != Some("btor2-channel-property")
5684            {
5685                return Err(PredicateApiError::InvalidResponse(
5686                    "BTOR2 channel property result shape is invalid".to_string(),
5687                ));
5688            }
5689            let values = fields[1..]
5690                .iter()
5691                .zip(result_keys)
5692                .map(|(field, key)| token_value(field, key))
5693                .collect::<Result<Vec<_>, _>>()?;
5694            let index = canonical_usize(values[0], result_keys[0])?;
5695            let query_id = canonical_u32(values[1], result_keys[1])?;
5696            let channel = canonical_usize(values[2], result_keys[2])?;
5697            let property = match values[3] {
5698                "output-high" => Btor2ChannelPropertyKind::OutputHigh,
5699                "output-low" => Btor2ChannelPropertyKind::OutputLow,
5700                _ => {
5701                    return Err(PredicateApiError::InvalidResponse(
5702                        "unknown BTOR2 channel property kind".to_string(),
5703                    ));
5704                }
5705            };
5706            let horizon = canonical_u32(values[4], result_keys[4])?;
5707            let answer = match values[5] {
5708                "SAFE" => Btor2ChannelPropertyAnswer::Safe,
5709                "UNSAFE" => Btor2ChannelPropertyAnswer::Unsafe,
5710                _ => {
5711                    return Err(PredicateApiError::InvalidResponse(
5712                        "unknown BTOR2 channel property answer".to_string(),
5713                    ));
5714                }
5715            };
5716            let bad_frame = if values[6] == "none" {
5717                None
5718            } else {
5719                Some(canonical_u32(values[6], result_keys[6])?)
5720            };
5721            let backend = match values[7] {
5722                "representative-class" => Btor2ChannelPropertyProcessBackend::RepresentativeClass,
5723                "direct-exact" => Btor2ChannelPropertyProcessBackend::DirectExact,
5724                _ => {
5725                    return Err(PredicateApiError::InvalidResponse(
5726                        "unknown BTOR2 channel property backend".to_string(),
5727                    ));
5728                }
5729            };
5730            let solver = match values[8] {
5731                "explicit-state" => Btor2ChannelPropertyProcessSolver::ExplicitState,
5732                "bitblast-cnf" => Btor2ChannelPropertyProcessSolver::BitblastCnf,
5733                _ => {
5734                    return Err(PredicateApiError::InvalidResponse(
5735                        "unknown BTOR2 channel property solver".to_string(),
5736                    ));
5737                }
5738            };
5739            let representative_channel = canonical_usize(values[9], result_keys[9])?;
5740            let witness_valuations = canonical_usize(values[10], result_keys[10])?;
5741            if index != expected_index
5742                || channel >= numeric[0]
5743                || representative_channel >= numeric[0]
5744                || results
5745                    .last()
5746                    .is_some_and(|previous: &Btor2ChannelPropertyProcessResult| {
5747                        previous.query_id >= query_id
5748                    })
5749                || (answer == Btor2ChannelPropertyAnswer::Safe
5750                    && (bad_frame.is_some() || witness_valuations != 0))
5751                || (answer == Btor2ChannelPropertyAnswer::Unsafe
5752                    && (bad_frame.is_none()
5753                        || bad_frame > Some(horizon)
5754                        || witness_valuations == 0))
5755            {
5756                return Err(PredicateApiError::InvalidResponse(
5757                    "BTOR2 channel property result violates semantic invariants".to_string(),
5758                ));
5759            }
5760            results.push(Btor2ChannelPropertyProcessResult {
5761                index,
5762                query_id,
5763                channel,
5764                property,
5765                horizon,
5766                answer,
5767                bad_frame,
5768                backend,
5769                solver,
5770                representative_channel,
5771                witness_valuations,
5772            });
5773        }
5774        Ok(Btor2ChannelPropertyProcessSummary {
5775            artifact_version,
5776            channels: numeric[0],
5777            logical_queries: numeric[1],
5778            proof_members: numeric[2],
5779            reused_queries: numeric[3],
5780            explicit_members: numeric[4],
5781            bitblast_members: numeric[5],
5782            evidence_bytes: numeric[6],
5783            artifact_bytes: numeric[7],
5784            projected_work,
5785            elapsed_micros,
5786            results,
5787        })
5788    })();
5789    match parsed {
5790        Ok(value) => Ok(Observed { value, metrics }),
5791        Err(error) => {
5792            metrics.status = InvocationStatus::Failed(error.failure_class());
5793            Err(PredicateOperationError {
5794                error: Box::new(error),
5795                metrics,
5796            })
5797        }
5798    }
5799}
5800
5801fn parse_revision_impact_summary(
5802    output: ManagedOutput,
5803    expected_status: &str,
5804    capabilities: &RevisionImpactCapabilities,
5805) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
5806    let (stdout, mut metrics) = successful_stdout(output)?;
5807    let parsed = (|| -> Result<RevisionImpactProcessSummary, PredicateApiError> {
5808        if stdout.contains('\r') || !stdout.ends_with('\n') {
5809            return Err(PredicateApiError::InvalidResponse(
5810                "revision impact response is not canonical LF text".to_string(),
5811            ));
5812        }
5813        let lines = stdout.lines().collect::<Vec<_>>();
5814        let fields = lines
5815            .first()
5816            .ok_or_else(|| {
5817                PredicateApiError::InvalidResponse("revision impact response is empty".to_string())
5818            })?
5819            .split(' ')
5820            .collect::<Vec<_>>();
5821        if fields.len() != 19 || fields.first().copied() != Some("btor2-revision-impact") {
5822            return Err(PredicateApiError::InvalidResponse(
5823                "revision impact summary shape is invalid".to_string(),
5824            ));
5825        }
5826        let keys = [
5827            "status",
5828            "impact_version",
5829            "atoms",
5830            "queries",
5831            "combinations",
5832            "reusable_observations",
5833            "invalidated_observations",
5834            "minimal_invalidating_sets",
5835            "minimal_semantic_change_sets",
5836            "evidence_members",
5837            "certificate_bytes",
5838            "parsed_evidence_bytes",
5839            "semantic_replays",
5840            "component_validations",
5841            "composed_pair_checks",
5842            "final_transition_checks",
5843            "result_comparisons",
5844            "elapsed_micros",
5845        ];
5846        let values = fields[1..19]
5847            .iter()
5848            .zip(keys)
5849            .map(|(field, key)| token_value(field, key))
5850            .collect::<Result<Vec<_>, _>>()?;
5851        if values[0] != expected_status {
5852            return Err(PredicateApiError::InvalidResponse(
5853                "revision impact status differs from requested operation".to_string(),
5854            ));
5855        }
5856        let impact_version = canonical_u32(values[1], keys[1])?;
5857        let numeric = values[2..]
5858            .iter()
5859            .enumerate()
5860            .map(|(index, value)| canonical_usize(value, keys[index + 2]))
5861            .collect::<Result<Vec<_>, _>>()?;
5862        if impact_version != capabilities.impact_version
5863            || numeric[0] == 0
5864            || numeric[0] > capabilities.max_atoms
5865            || numeric[1] == 0
5866            || numeric[1] > capabilities.max_queries
5867            || numeric[2] != 1usize << numeric[0]
5868            || numeric[2] > capabilities.max_combinations
5869            || numeric[3].checked_add(numeric[4]) != Some(numeric[2] * numeric[1])
5870            || numeric[7] != numeric[2] * numeric[1]
5871            || numeric[8] == 0
5872            || numeric[8] > capabilities.max_bundle_bytes
5873            || numeric[9] == 0
5874            || numeric[9] > capabilities.max_bundle_bytes
5875            || numeric[10] != numeric[7]
5876            || numeric[11] != numeric[7] * 2
5877            || numeric[14] != numeric[7]
5878        {
5879            return Err(PredicateApiError::InvalidResponse(
5880                "revision impact summary violates advertised dimensions".to_string(),
5881            ));
5882        }
5883        if lines.len() != numeric[1] + numeric[6] + 1 {
5884            return Err(PredicateApiError::InvalidResponse(
5885                "revision impact query transition count is incomplete".to_string(),
5886            ));
5887        }
5888        let query_keys = [
5889            "index",
5890            "horizon",
5891            "bad_side",
5892            "bad_output",
5893            "old_result",
5894            "new_result",
5895        ];
5896        let mut transitions = Vec::with_capacity(numeric[1]);
5897        let mut previous_key = None;
5898        for (index, line) in lines[1..=numeric[1]].iter().enumerate() {
5899            let fields = line.split(' ').collect::<Vec<_>>();
5900            if fields.len() != 7 || fields.first().copied() != Some("btor2-revision-impact-query") {
5901                return Err(PredicateApiError::InvalidResponse(
5902                    "revision impact query transition shape is invalid".to_string(),
5903                ));
5904            }
5905            let values = fields[1..]
5906                .iter()
5907                .zip(query_keys)
5908                .map(|(field, key)| token_value(field, key))
5909                .collect::<Result<Vec<_>, _>>()?;
5910            if canonical_usize(values[0], query_keys[0])? != index {
5911                return Err(PredicateApiError::InvalidResponse(
5912                    "revision impact query transition index is noncanonical".to_string(),
5913                ));
5914            }
5915            let horizon = canonical_u32(values[1], query_keys[1])?;
5916            let (bad_side, side_key) = match values[2] {
5917                "left" => (revision_local::ComponentSide::Left, 0_u8),
5918                "right" => (revision_local::ComponentSide::Right, 1_u8),
5919                _ => {
5920                    return Err(PredicateApiError::InvalidResponse(
5921                        "revision impact query side is invalid".to_string(),
5922                    ));
5923                }
5924            };
5925            let bad_output = canonical_u64(values[3], query_keys[3])?;
5926            if bad_output == 0 {
5927                return Err(PredicateApiError::InvalidResponse(
5928                    "revision impact query output is zero".to_string(),
5929                ));
5930            }
5931            let key = (horizon, side_key, bad_output);
5932            if previous_key.is_some_and(|previous| previous >= key) {
5933                return Err(PredicateApiError::InvalidResponse(
5934                    "revision impact query transitions are not strictly ordered".to_string(),
5935                ));
5936            }
5937            previous_key = Some(key);
5938            let parse_result = |value| match value {
5939                "SAFE" => Ok(revision_local::BoundedResult::Safe),
5940                "UNSAFE" => Ok(revision_local::BoundedResult::Unsafe),
5941                _ => Err(PredicateApiError::InvalidResponse(
5942                    "revision impact query result is invalid".to_string(),
5943                )),
5944            };
5945            transitions.push(RevisionImpactQueryTransition {
5946                index,
5947                horizon,
5948                bad_side,
5949                bad_output,
5950                old_result: parse_result(values[4])?,
5951                new_result: parse_result(values[5])?,
5952            });
5953        }
5954        let semantic_keys = [
5955            "query_index",
5956            "changed_mask",
5957            "baseline_result",
5958            "changed_result",
5959        ];
5960        let parse_result = |value| match value {
5961            "SAFE" => Ok(revision_local::BoundedResult::Safe),
5962            "UNSAFE" => Ok(revision_local::BoundedResult::Unsafe),
5963            _ => Err(PredicateApiError::InvalidResponse(
5964                "revision impact semantic-set result is invalid".to_string(),
5965            )),
5966        };
5967        let mut semantic_change_sets = Vec::with_capacity(numeric[6]);
5968        let mut previous_semantic_key = None;
5969        for line in &lines[numeric[1] + 1..] {
5970            let fields = line.split(' ').collect::<Vec<_>>();
5971            if fields.len() != 5
5972                || fields.first().copied() != Some("btor2-revision-impact-semantic-set")
5973            {
5974                return Err(PredicateApiError::InvalidResponse(
5975                    "revision impact semantic-set shape is invalid".to_string(),
5976                ));
5977            }
5978            let values = fields[1..]
5979                .iter()
5980                .zip(semantic_keys)
5981                .map(|(field, key)| token_value(field, key))
5982                .collect::<Result<Vec<_>, _>>()?;
5983            let query_index = canonical_usize(values[0], semantic_keys[0])?;
5984            let changed_mask_usize = canonical_usize(values[1], semantic_keys[1])?;
5985            if query_index >= numeric[1]
5986                || changed_mask_usize == 0
5987                || changed_mask_usize >= numeric[2]
5988            {
5989                return Err(PredicateApiError::InvalidResponse(
5990                    "revision impact semantic-set index or mask is out of range".to_string(),
5991                ));
5992            }
5993            let key = (query_index, changed_mask_usize);
5994            if previous_semantic_key.is_some_and(|previous| previous >= key) {
5995                return Err(PredicateApiError::InvalidResponse(
5996                    "revision impact semantic sets are not strictly ordered".to_string(),
5997                ));
5998            }
5999            previous_semantic_key = Some(key);
6000            let baseline_result = parse_result(values[2])?;
6001            let changed_result = parse_result(values[3])?;
6002            if baseline_result == changed_result {
6003                return Err(PredicateApiError::InvalidResponse(
6004                    "revision impact semantic set does not change the result".to_string(),
6005                ));
6006            }
6007            semantic_change_sets.push(RevisionImpactSemanticChangeSet {
6008                query_index,
6009                changed_mask: u16::try_from(changed_mask_usize).map_err(|_| {
6010                    PredicateApiError::InvalidResponse(
6011                        "revision impact semantic-set mask exceeds u16".to_string(),
6012                    )
6013                })?,
6014                baseline_result,
6015                changed_result,
6016            });
6017        }
6018        Ok(RevisionImpactProcessSummary {
6019            impact_version,
6020            atoms: numeric[0],
6021            queries: numeric[1],
6022            combinations: numeric[2],
6023            reusable_observations: numeric[3],
6024            invalidated_observations: numeric[4],
6025            minimal_invalidating_sets: numeric[5],
6026            minimal_semantic_change_sets: numeric[6],
6027            evidence_members: numeric[7],
6028            certificate_bytes: numeric[8],
6029            parsed_evidence_bytes: numeric[9],
6030            semantic_replays: numeric[10],
6031            component_validations: numeric[11],
6032            composed_pair_checks: numeric[12],
6033            final_transition_checks: numeric[13],
6034            result_comparisons: numeric[14],
6035            elapsed_micros: numeric[15],
6036            transitions,
6037            semantic_change_sets,
6038        })
6039    })();
6040    match parsed {
6041        Ok(value) => Ok(Observed { value, metrics }),
6042        Err(error) => {
6043            metrics.status = InvocationStatus::Failed(error.failure_class());
6044            Err(PredicateOperationError {
6045                error: Box::new(error),
6046                metrics,
6047            })
6048        }
6049    }
6050}
6051
6052fn parse_controller_plant_portfolio_capabilities(
6053    line: &str,
6054) -> Result<ControllerPlantPortfolioCapabilities, PredicateApiError> {
6055    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6056        return Err(PredicateApiError::InvalidResponse(
6057            "controller plant portfolio capability line is not canonical".to_string(),
6058        ));
6059    }
6060    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6061    let keys = [
6062        "controller_plant_portfolio_cli_version",
6063        "artifact_version",
6064        "manifest_version",
6065        "max_manifest_bytes",
6066        "max_artifact_bytes",
6067        "max_members",
6068        "backends",
6069        "routing",
6070        "fallback",
6071        "unsupported",
6072    ];
6073    if fields.len() != keys.len() {
6074        return Err(PredicateApiError::InvalidResponse(
6075            "controller plant portfolio capability field count is invalid".to_string(),
6076        ));
6077    }
6078    let values = fields
6079        .iter()
6080        .zip(keys)
6081        .map(|(field, key)| token_value(field, key))
6082        .collect::<Result<Vec<_>, _>>()?;
6083    if values[6..] != ["mtbdd,direct-exact", "static", "exact", "fail-closed"] {
6084        return Err(PredicateApiError::IncompatibleContract(
6085            "controller plant portfolio routing contract is unsupported".to_string(),
6086        ));
6087    }
6088    let versions = values[..3]
6089        .iter()
6090        .enumerate()
6091        .map(|(index, value)| canonical_u32(value, keys[index]))
6092        .collect::<Result<Vec<_>, _>>()?;
6093    let limits = values[3..6]
6094        .iter()
6095        .enumerate()
6096        .map(|(index, value)| canonical_usize(value, keys[index + 3]))
6097        .collect::<Result<Vec<_>, _>>()?;
6098    if versions != [1, 1, 1] {
6099        return Err(PredicateApiError::IncompatibleContract(
6100            "controller plant portfolio version tuple is unsupported".to_string(),
6101        ));
6102    }
6103    if limits.contains(&0) {
6104        return Err(PredicateApiError::InvalidResponse(
6105            "controller plant portfolio discovered limit must be positive".to_string(),
6106        ));
6107    }
6108    Ok(ControllerPlantPortfolioCapabilities {
6109        cli_version: versions[0],
6110        artifact_version: versions[1],
6111        manifest_version: versions[2],
6112        max_manifest_bytes: limits[0],
6113        max_artifact_bytes: limits[1],
6114        max_members: limits[2],
6115    })
6116}
6117
6118fn parse_controller_plant_portfolio_summary(
6119    output: ManagedOutput,
6120    expected_action: &str,
6121    expected_output: Option<&Path>,
6122    capabilities: &ControllerPlantPortfolioCapabilities,
6123) -> Result<Observed<ControllerPlantPortfolioBatchSummary>, PredicateOperationError> {
6124    let (stdout, mut metrics) = successful_stdout(output)?;
6125    let parsed = (|| -> Result<ControllerPlantPortfolioBatchSummary, PredicateApiError> {
6126        if stdout.contains('\r') || !stdout.ends_with('\n') {
6127            return Err(PredicateApiError::InvalidResponse(
6128                "controller plant portfolio response is not canonical LF text".to_string(),
6129            ));
6130        }
6131        let mut lines = stdout.lines();
6132        let first = lines.next().ok_or_else(|| {
6133            PredicateApiError::InvalidResponse(
6134                "controller plant portfolio summary line is missing".to_string(),
6135            )
6136        })?;
6137        let summary_text = if let Some(expected_output) = expected_output {
6138            first
6139                .split_once(" output=")
6140                .and_then(|(summary, output)| {
6141                    (output == expected_output.to_string_lossy()).then_some(summary)
6142                })
6143                .ok_or_else(|| {
6144                    PredicateApiError::InvalidResponse(
6145                        "controller plant portfolio output path disagrees".to_string(),
6146                    )
6147                })?
6148        } else if first.contains(" output=") {
6149            return Err(PredicateApiError::InvalidResponse(
6150                "verified controller plant portfolio unexpectedly names output".to_string(),
6151            ));
6152        } else {
6153            first
6154        };
6155        let fields = summary_text.split(' ').collect::<Vec<_>>();
6156        let keys = [
6157            "controller-plant-portfolio",
6158            "status",
6159            "cli_version",
6160            "artifact_version",
6161            "backend",
6162            "reason",
6163            "members",
6164            "safe",
6165            "unsafe",
6166            "reachable_product_states",
6167            "explored_transitions",
6168            "artifact_bytes",
6169            "load_micros",
6170            "artifact_micros",
6171            "verification_micros",
6172            "write_micros",
6173            "elapsed_micros",
6174        ];
6175        if fields.len() != keys.len() || fields[0] != keys[0] {
6176            return Err(PredicateApiError::InvalidResponse(
6177                "controller plant portfolio summary shape is invalid".to_string(),
6178            ));
6179        }
6180        if token_value(fields[1], "status")? != expected_action {
6181            return Err(PredicateApiError::InvalidResponse(
6182                "controller plant portfolio action disagrees".to_string(),
6183            ));
6184        }
6185        let cli_version = canonical_u32(token_value(fields[2], "cli_version")?, "cli_version")?;
6186        let artifact_version = canonical_u32(
6187            token_value(fields[3], "artifact_version")?,
6188            "artifact_version",
6189        )?;
6190        if cli_version != capabilities.cli_version
6191            || artifact_version != capabilities.artifact_version
6192        {
6193            return Err(PredicateApiError::IncompatibleContract(
6194                "controller plant portfolio response version changed".to_string(),
6195            ));
6196        }
6197        let backend = match token_value(fields[4], "backend")? {
6198            "MTBDD" => ControllerPlantPortfolioBackend::Mtbdd,
6199            "DIRECT_EXACT" => ControllerPlantPortfolioBackend::DirectExact,
6200            _ => {
6201                return Err(PredicateApiError::InvalidResponse(
6202                    "controller plant portfolio backend is invalid".to_string(),
6203                ));
6204            }
6205        };
6206        let reason = match token_value(fields[5], "reason")? {
6207            "mtbdd-admitted" => ControllerPlantPortfolioReason::MtbddAdmitted,
6208            "boundary-limit" => ControllerPlantPortfolioReason::BoundaryLimit,
6209            "terminal-limit" => ControllerPlantPortfolioReason::TerminalLimit,
6210            "node-limit" => ControllerPlantPortfolioReason::NodeLimit,
6211            _ => {
6212                return Err(PredicateApiError::InvalidResponse(
6213                    "controller plant portfolio reason is invalid".to_string(),
6214                ));
6215            }
6216        };
6217        if !matches!(
6218            (backend, reason),
6219            (
6220                ControllerPlantPortfolioBackend::Mtbdd,
6221                ControllerPlantPortfolioReason::MtbddAdmitted
6222            ) | (
6223                ControllerPlantPortfolioBackend::DirectExact,
6224                ControllerPlantPortfolioReason::BoundaryLimit
6225                    | ControllerPlantPortfolioReason::TerminalLimit
6226                    | ControllerPlantPortfolioReason::NodeLimit
6227            )
6228        ) {
6229            return Err(PredicateApiError::InvalidResponse(
6230                "controller plant portfolio route and reason disagree".to_string(),
6231            ));
6232        }
6233        let numeric = fields[6..]
6234            .iter()
6235            .zip(&keys[6..])
6236            .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
6237            .collect::<Result<Vec<_>, _>>()?;
6238        let member_count = numeric[0];
6239        if member_count == 0
6240            || member_count > capabilities.max_members
6241            || numeric[5] > capabilities.max_artifact_bytes
6242        {
6243            return Err(PredicateApiError::InvalidResponse(
6244                "controller plant portfolio response exceeds discovered limits".to_string(),
6245            ));
6246        }
6247        let mut members = Vec::with_capacity(member_count);
6248        for expected_index in 0..member_count {
6249            let line = lines.next().ok_or_else(|| {
6250                PredicateApiError::InvalidResponse(
6251                    "controller plant portfolio member line is missing".to_string(),
6252                )
6253            })?;
6254            let fields = line.split(' ').collect::<Vec<_>>();
6255            let keys = [
6256                "controller-plant-portfolio-member",
6257                "index",
6258                "answer",
6259                "horizon",
6260                "bad_frame",
6261                "trace_steps",
6262                "reachable_product_states",
6263                "explored_transitions",
6264            ];
6265            if fields.len() != keys.len() || fields[0] != keys[0] {
6266                return Err(PredicateApiError::InvalidResponse(
6267                    "controller plant portfolio member shape is invalid".to_string(),
6268                ));
6269            }
6270            let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
6271            if index != expected_index {
6272                return Err(PredicateApiError::InvalidResponse(
6273                    "controller plant portfolio member order is invalid".to_string(),
6274                ));
6275            }
6276            let answer = match token_value(fields[2], "answer")? {
6277                "SAFE" => ControllerMtbddAnswer::Safe,
6278                "UNSAFE" => ControllerMtbddAnswer::Unsafe,
6279                _ => {
6280                    return Err(PredicateApiError::InvalidResponse(
6281                        "controller plant portfolio member answer is invalid".to_string(),
6282                    ));
6283                }
6284            };
6285            let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
6286            let bad = token_value(fields[4], "bad_frame")?;
6287            let bad_frame = if bad == "none" {
6288                None
6289            } else {
6290                Some(canonical_usize(bad, "bad_frame")?)
6291            };
6292            let trace_steps =
6293                canonical_usize(token_value(fields[5], "trace_steps")?, "trace_steps")?;
6294            if horizon > crate::controller_plant::MAX_COMPOSITION_HORIZON
6295                || match (answer, bad_frame) {
6296                    (ControllerMtbddAnswer::Safe, None) => trace_steps != 0,
6297                    (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
6298                        frame > horizon || trace_steps != frame.saturating_add(1)
6299                    }
6300                    _ => true,
6301                }
6302            {
6303                return Err(PredicateApiError::InvalidResponse(
6304                    "controller plant portfolio member trace boundary is invalid".to_string(),
6305                ));
6306            }
6307            members.push(ControllerMtbddMemberResult {
6308                index,
6309                answer,
6310                horizon,
6311                bad_frame,
6312                trace_steps,
6313                reachable_product_states: canonical_usize(
6314                    token_value(fields[6], "reachable_product_states")?,
6315                    "reachable_product_states",
6316                )?,
6317                explored_transitions: canonical_usize(
6318                    token_value(fields[7], "explored_transitions")?,
6319                    "explored_transitions",
6320                )?,
6321            });
6322        }
6323        let reachable_total = members.iter().try_fold(0usize, |total, member| {
6324            total.checked_add(member.reachable_product_states)
6325        });
6326        let transition_total = members.iter().try_fold(0usize, |total, member| {
6327            total.checked_add(member.explored_transitions)
6328        });
6329        if lines.next().is_some()
6330            || members
6331                .iter()
6332                .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
6333                .count()
6334                != numeric[1]
6335            || members
6336                .iter()
6337                .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Unsafe))
6338                .count()
6339                != numeric[2]
6340            || reachable_total != Some(numeric[3])
6341            || transition_total != Some(numeric[4])
6342        {
6343            return Err(PredicateApiError::InvalidResponse(
6344                "controller plant portfolio member totals disagree".to_string(),
6345            ));
6346        }
6347        Ok(ControllerPlantPortfolioBatchSummary {
6348            artifact_version,
6349            backend,
6350            reason,
6351            safe: numeric[1],
6352            unsafe_count: numeric[2],
6353            reachable_product_states: numeric[3],
6354            explored_transitions: numeric[4],
6355            artifact_bytes: numeric[5],
6356            load_micros: numeric[6],
6357            artifact_micros: numeric[7],
6358            verification_micros: numeric[8],
6359            write_micros: numeric[9],
6360            elapsed_micros: numeric[10],
6361            members,
6362        })
6363    })();
6364    match parsed {
6365        Ok(value) => Ok(Observed { value, metrics }),
6366        Err(error) => {
6367            metrics.status = InvocationStatus::Failed(error.failure_class());
6368            Err(PredicateOperationError {
6369                error: Box::new(error),
6370                metrics,
6371            })
6372        }
6373    }
6374}
6375
6376fn parse_controller_split_evidence_capabilities(
6377    line: &str,
6378) -> Result<ControllerSplitEvidenceCapabilities, PredicateApiError> {
6379    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6380        return Err(PredicateApiError::InvalidResponse(
6381            "controller split-evidence capability line is not canonical".to_string(),
6382        ));
6383    }
6384    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6385    let keys = [
6386        "controller_split_evidence_cli_version",
6387        "controller_artifact_version",
6388        "plant_artifact_version",
6389        "manifest_version",
6390        "max_manifest_bytes",
6391        "max_artifact_bytes",
6392        "max_batches",
6393        "admission",
6394        "verification",
6395        "exhaustive_replay",
6396        "source_binding",
6397        "obligation_binding",
6398        "unsupported",
6399    ];
6400    if fields.len() != keys.len() {
6401        return Err(PredicateApiError::InvalidResponse(
6402            "controller split-evidence capability field count is invalid".to_string(),
6403        ));
6404    }
6405    let values = fields
6406        .iter()
6407        .zip(keys)
6408        .map(|(field, key)| token_value(field, key))
6409        .collect::<Result<Vec<_>, _>>()?;
6410    if values[7..]
6411        != [
6412            "once",
6413            "unsat-miter",
6414            "no",
6415            "sha256",
6416            "complete-ordered",
6417            "fail-closed",
6418        ]
6419    {
6420        return Err(PredicateApiError::IncompatibleContract(
6421            "controller split-evidence trust contract is unsupported".to_string(),
6422        ));
6423    }
6424    let versions = values[..4]
6425        .iter()
6426        .enumerate()
6427        .map(|(index, value)| canonical_u32(value, keys[index]))
6428        .collect::<Result<Vec<_>, _>>()?;
6429    let limits = values[4..7]
6430        .iter()
6431        .enumerate()
6432        .map(|(index, value)| canonical_usize(value, keys[index + 4]))
6433        .collect::<Result<Vec<_>, _>>()?;
6434    if versions != [1, 1, 1, 1] {
6435        return Err(PredicateApiError::IncompatibleContract(
6436            "controller split-evidence version tuple is unsupported".to_string(),
6437        ));
6438    }
6439    if limits.contains(&0) {
6440        return Err(PredicateApiError::InvalidResponse(
6441            "controller split-evidence discovered limit must be positive".to_string(),
6442        ));
6443    }
6444    if limits[0] > 64 * 1024
6445        || limits[1] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES
6446        || limits[2] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6447    {
6448        return Err(PredicateApiError::IncompatibleContract(
6449            "controller split-evidence limits exceed client safety ceilings".to_string(),
6450        ));
6451    }
6452    Ok(ControllerSplitEvidenceCapabilities {
6453        cli_version: versions[0],
6454        controller_artifact_version: versions[1],
6455        plant_artifact_version: versions[2],
6456        manifest_version: versions[3],
6457        max_manifest_bytes: limits[0],
6458        max_artifact_bytes: limits[1],
6459        max_batches: limits[2],
6460    })
6461}
6462
6463fn parse_controller_split_resource_capabilities(
6464    line: &str,
6465) -> Result<ControllerSplitResourceCapabilities, PredicateApiError> {
6466    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6467        return Err(PredicateApiError::InvalidResponse(
6468            "controller split resource capability line is not canonical".to_string(),
6469        ));
6470    }
6471    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6472    let keys = [
6473        "controller_split_resource_cli_version",
6474        "policy_version",
6475        "controller_envelope_version",
6476        "plant_envelope_version",
6477        "controller_artifact_version",
6478        "plant_artifact_version",
6479        "manifest_version",
6480        "max_policy_bytes",
6481        "max_controller_artifact_bytes",
6482        "max_unsat_proof_bytes",
6483        "max_plant_artifact_bytes",
6484        "max_batches",
6485        "max_members_per_batch",
6486        "max_horizon",
6487        "max_product_states",
6488        "refusal_exit",
6489        "admission",
6490        "verification",
6491        "exhaustive_replay",
6492        "accounting",
6493        "timing_calibration",
6494        "result_on_refusal",
6495        "refusal_schema",
6496        "unsupported",
6497    ];
6498    if fields.len() != keys.len() {
6499        return Err(PredicateApiError::InvalidResponse(
6500            "controller split resource capability field count is invalid".to_string(),
6501        ));
6502    }
6503    let values = fields
6504        .iter()
6505        .zip(keys)
6506        .map(|(field, key)| token_value(field, key))
6507        .collect::<Result<Vec<_>, _>>()?;
6508    if values[16..]
6509        != [
6510            "once",
6511            "unsat-miter",
6512            "no",
6513            "conservative-static-per-batch-and-total",
6514            "none",
6515            "none",
6516            "split-reason-v1",
6517            "fail-closed",
6518        ]
6519    {
6520        return Err(PredicateApiError::IncompatibleContract(
6521            "controller split resource trust contract is unsupported".to_string(),
6522        ));
6523    }
6524    let versions = values[..7]
6525        .iter()
6526        .enumerate()
6527        .map(|(index, value)| canonical_u32(value, keys[index]))
6528        .collect::<Result<Vec<_>, _>>()?;
6529    let limits = values[7..15]
6530        .iter()
6531        .enumerate()
6532        .map(|(index, value)| canonical_usize(value, keys[index + 7]))
6533        .collect::<Result<Vec<_>, _>>()?;
6534    let refusal_exit = canonical_usize(values[15], keys[15])?;
6535    if versions != [1, 1, 1, 1, 1, 1, 1] {
6536        return Err(PredicateApiError::IncompatibleContract(
6537            "controller split resource version tuple is unsupported".to_string(),
6538        ));
6539    }
6540    if limits.contains(&0) || refusal_exit != 3 {
6541        return Err(PredicateApiError::InvalidResponse(
6542            "controller split resource discovered limits are invalid".to_string(),
6543        ));
6544    }
6545    if limits[0] > 4096
6546        || limits[1] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES
6547        || limits[2] > unsat_proof::MAX_UNSAT_PROOF_BYTES
6548        || limits[3] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES
6549        || limits[4] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6550        || limits[5] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6551        || limits[6] > controller_plant::MAX_COMPOSITION_HORIZON
6552        || limits[7] > controller_plant::MAX_PRODUCT_STATES
6553    {
6554        return Err(PredicateApiError::IncompatibleContract(
6555            "controller split resource limits exceed client safety ceilings".to_string(),
6556        ));
6557    }
6558    Ok(ControllerSplitResourceCapabilities {
6559        cli_version: versions[0],
6560        policy_version: versions[1],
6561        controller_envelope_version: versions[2],
6562        plant_envelope_version: versions[3],
6563        controller_artifact_version: versions[4],
6564        plant_artifact_version: versions[5],
6565        manifest_version: versions[6],
6566        max_policy_bytes: limits[0],
6567        max_controller_artifact_bytes: limits[1],
6568        max_unsat_proof_bytes: limits[2],
6569        max_plant_artifact_bytes: limits[3],
6570        max_batches: limits[4],
6571        max_members_per_batch: limits[5],
6572        max_horizon: limits[6],
6573        max_product_states: limits[7],
6574        refusal_exit_code: 3,
6575    })
6576}
6577
6578fn parse_controller_split_observability_capabilities(
6579    text: &str,
6580) -> Result<ControllerSplitObservabilityCapabilities, PredicateApiError> {
6581    if text.contains('\r') || !text.ends_with('\n') || text.lines().count() != 2 {
6582        return Err(PredicateApiError::InvalidResponse(
6583            "controller split observability capability response is not canonical".to_string(),
6584        ));
6585    }
6586    let lines = text.lines().collect::<Vec<_>>();
6587    let resource = parse_controller_split_resource_capabilities(&format!("{}\n", lines[0]))?;
6588    let fields = lines[1].split(' ').collect::<Vec<_>>();
6589    let keys = [
6590        "controller_split_observability_cli_version",
6591        "base_cli_version",
6592        "phase_metrics_version",
6593        "phases",
6594        "counters",
6595        "timing_calibration",
6596        "partial_metrics_on_failure",
6597        "result_on_refusal",
6598        "unsupported",
6599    ];
6600    if fields.len() != keys.len() {
6601        return Err(PredicateApiError::InvalidResponse(
6602            "controller split observability capability field count is invalid".to_string(),
6603        ));
6604    }
6605    let values = fields
6606        .iter()
6607        .zip(keys)
6608        .map(|(field, key)| token_value(field, key))
6609        .collect::<Result<Vec<_>, _>>()?;
6610    let cli_version = canonical_u32(values[0], keys[0])?;
6611    let base_cli_version = canonical_u32(values[1], keys[1])?;
6612    let phase_metrics_version = canonical_u32(values[2], keys[2])?;
6613    if cli_version != 1
6614        || base_cli_version != resource.cli_version
6615        || phase_metrics_version != 1
6616        || values[3]
6617            != "policy-and-input,controller-admission,complete-set-preflight,semantic-replay"
6618        || values[4]
6619            != "controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound"
6620        || values[5..] != ["none", "none", "none", "fail-closed"]
6621    {
6622        return Err(PredicateApiError::IncompatibleContract(
6623            "controller split observability contract is unsupported".to_string(),
6624        ));
6625    }
6626    Ok(ControllerSplitObservabilityCapabilities {
6627        cli_version,
6628        phase_metrics_version,
6629        resource,
6630    })
6631}
6632
6633fn parse_controller_split_allocation_observability_capabilities(
6634    text: &str,
6635) -> Result<ControllerSplitAllocationObservabilityCapabilities, PredicateApiError> {
6636    if text.contains('\r') || !text.ends_with('\n') || text.lines().count() != 3 {
6637        return Err(PredicateApiError::InvalidResponse(
6638            "controller split allocation capability response is not canonical".to_string(),
6639        ));
6640    }
6641    let lines = text.lines().collect::<Vec<_>>();
6642    let observability = parse_controller_split_observability_capabilities(&format!(
6643        "{}\n{}\n",
6644        lines[0], lines[1]
6645    ))?;
6646    let fields = lines[2].split(' ').collect::<Vec<_>>();
6647    let keys = [
6648        "controller_split_allocation_observability_cli_version",
6649        "base_observability_cli_version",
6650        "allocator",
6651        "scope",
6652        "counters",
6653        "overflow",
6654        "timing_calibration",
6655        "partial_metrics_on_failure",
6656        "result_on_refusal",
6657        "unsupported",
6658    ];
6659    if fields.len() != keys.len() {
6660        return Err(PredicateApiError::InvalidResponse(
6661            "controller split allocation capability field count is invalid".to_string(),
6662        ));
6663    }
6664    let values = fields
6665        .iter()
6666        .zip(keys)
6667        .map(|(field, key)| token_value(field, key))
6668        .collect::<Result<Vec<_>, _>>()?;
6669    let cli_version = canonical_u32(values[0], keys[0])?;
6670    let base_version = canonical_u32(values[1], keys[1])?;
6671    if cli_version != 1
6672        || base_version != observability.cli_version
6673        || values[2] != "system"
6674        || values[3] != "policy-through-replay"
6675        || values[4]
6676            != "allocation-calls,allocated-bytes,deallocation-calls,deallocated-bytes,reallocation-calls,reallocated-bytes"
6677        || values[5..] != ["fail-closed", "none", "none", "none", "fail-closed"]
6678    {
6679        return Err(PredicateApiError::IncompatibleContract(
6680            "controller split allocation observability contract is unsupported".to_string(),
6681        ));
6682    }
6683    Ok(ControllerSplitAllocationObservabilityCapabilities {
6684        cli_version,
6685        observability,
6686    })
6687}
6688
6689fn parse_controller_split_cache_observability_capabilities(
6690    text: &str,
6691) -> Result<ControllerSplitCacheObservabilityCapabilities, PredicateApiError> {
6692    if text.contains('\r') || !text.ends_with('\n') || text.lines().count() != 4 {
6693        return Err(PredicateApiError::InvalidResponse(
6694            "controller split cache capability response is not canonical".to_string(),
6695        ));
6696    }
6697    let lines = text.lines().collect::<Vec<_>>();
6698    let allocation_observability = parse_controller_split_allocation_observability_capabilities(
6699        &format!("{}\n{}\n{}\n", lines[0], lines[1], lines[2]),
6700    )?;
6701    let fields = lines[3].split(' ').collect::<Vec<_>>();
6702    let keys = [
6703        "controller_split_cache_observability_cli_version",
6704        "base_allocation_observability_cli_version",
6705        "scope",
6706        "key",
6707        "counters",
6708        "integrity_preflight",
6709        "overflow",
6710        "timing_calibration",
6711        "partial_metrics_on_failure",
6712        "result_on_refusal",
6713        "unsupported",
6714    ];
6715    if fields.len() != keys.len() {
6716        return Err(PredicateApiError::InvalidResponse(
6717            "controller split cache capability field count is invalid".to_string(),
6718        ));
6719    }
6720    let values = fields
6721        .iter()
6722        .zip(keys)
6723        .map(|(field, key)| token_value(field, key))
6724        .collect::<Result<Vec<_>, _>>()?;
6725    let cli_version = canonical_u32(values[0], keys[0])?;
6726    let base_version = canonical_u32(values[1], keys[1])?;
6727    if cli_version != 1
6728        || base_version != allocation_observability.cli_version
6729        || values[2] != "semantic-replay"
6730        || values[3] != "manifest-snapshot,resource-assessment,result-sha256"
6731        || values[4] != "lookups,hits,misses,entries"
6732        || values[5..]
6733            != [
6734                "required",
6735                "fail-closed",
6736                "none",
6737                "none",
6738                "none",
6739                "fail-closed",
6740            ]
6741    {
6742        return Err(PredicateApiError::IncompatibleContract(
6743            "controller split cache observability contract is unsupported".to_string(),
6744        ));
6745    }
6746    Ok(ControllerSplitCacheObservabilityCapabilities {
6747        cli_version,
6748        allocation_observability,
6749    })
6750}
6751
6752fn parse_controller_mtbdd_capabilities(
6753    line: &str,
6754) -> Result<ControllerMtbddCapabilities, PredicateApiError> {
6755    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6756        return Err(PredicateApiError::InvalidResponse(
6757            "controller MTBDD capability line is not canonical".to_string(),
6758        ));
6759    }
6760    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6761    let keys = [
6762        "controller_mtbdd_cli_version",
6763        "mtbdd_version",
6764        "plant_artifact_version",
6765        "manifest_version",
6766        "max_manifest_bytes",
6767        "max_artifact_bytes",
6768        "max_members",
6769        "max_state_bits",
6770        "max_inputs",
6771        "max_outputs",
6772        "max_nodes",
6773        "max_terminals",
6774        "max_assignments",
6775        "max_horizon",
6776        "unsupported",
6777    ];
6778    if fields.len() != keys.len() {
6779        return Err(PredicateApiError::InvalidResponse(
6780            "controller MTBDD capability field count is invalid".to_string(),
6781        ));
6782    }
6783    let values = fields
6784        .iter()
6785        .zip(keys)
6786        .map(|(field, key)| token_value(field, key))
6787        .collect::<Result<Vec<_>, _>>()?;
6788    if values[14] != "fail-closed" {
6789        return Err(PredicateApiError::IncompatibleContract(
6790            "controller MTBDD unsupported-input policy is not fail-closed".to_string(),
6791        ));
6792    }
6793    let versions = values[..4]
6794        .iter()
6795        .enumerate()
6796        .map(|(index, value)| canonical_u32(value, keys[index]))
6797        .collect::<Result<Vec<_>, _>>()?;
6798    let limits = values[4..14]
6799        .iter()
6800        .enumerate()
6801        .map(|(index, value)| canonical_usize(value, keys[index + 4]))
6802        .collect::<Result<Vec<_>, _>>()?;
6803    if versions != [1, 1, 1, 1] {
6804        return Err(PredicateApiError::IncompatibleContract(
6805            "controller MTBDD version tuple is unsupported".to_string(),
6806        ));
6807    }
6808    if limits.contains(&0) {
6809        return Err(PredicateApiError::InvalidResponse(
6810            "controller MTBDD discovered limit must be positive".to_string(),
6811        ));
6812    }
6813    let capabilities = ControllerMtbddCapabilities {
6814        cli_version: versions[0],
6815        mtbdd_version: versions[1],
6816        plant_artifact_version: versions[2],
6817        manifest_version: versions[3],
6818        max_manifest_bytes: limits[0],
6819        max_artifact_bytes: limits[1],
6820        max_members: limits[2],
6821        max_state_bits: limits[3],
6822        max_inputs: limits[4],
6823        max_outputs: limits[5],
6824        max_nodes: limits[6],
6825        max_terminals: limits[7],
6826        max_assignments: limits[8],
6827        max_horizon: limits[9],
6828    };
6829    Ok(capabilities)
6830}
6831
6832fn parse_controller_proof_mtbdd_capabilities(
6833    line: &str,
6834) -> Result<ControllerProofMtbddCapabilities, PredicateApiError> {
6835    if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6836        return Err(PredicateApiError::InvalidResponse(
6837            "proof-carrying controller MTBDD capability line is not canonical".to_string(),
6838        ));
6839    }
6840    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6841    let keys = [
6842        "controller_proof_mtbdd_cli_version",
6843        "mtbdd_version",
6844        "equivalence_proof_version",
6845        "plant_artifact_version",
6846        "manifest_version",
6847        "max_manifest_bytes",
6848        "max_artifact_bytes",
6849        "max_equivalence_artifact_bytes",
6850        "max_unsat_proof_bytes",
6851        "max_members",
6852        "max_state_bits",
6853        "max_inputs",
6854        "max_outputs",
6855        "max_nodes",
6856        "max_terminals",
6857        "max_horizon",
6858        "verification",
6859        "exhaustive_replay",
6860        "unsupported",
6861    ];
6862    if fields.len() != keys.len() {
6863        return Err(PredicateApiError::InvalidResponse(
6864            "proof-carrying controller MTBDD capability field count is invalid".to_string(),
6865        ));
6866    }
6867    let values = fields
6868        .iter()
6869        .zip(keys)
6870        .map(|(field, key)| token_value(field, key))
6871        .collect::<Result<Vec<_>, _>>()?;
6872    if values[16] != "unsat-miter" || values[17] != "no" || values[18] != "fail-closed" {
6873        return Err(PredicateApiError::IncompatibleContract(
6874            "proof-carrying controller MTBDD verification contract changed".to_string(),
6875        ));
6876    }
6877    let versions = values[..5]
6878        .iter()
6879        .enumerate()
6880        .map(|(index, value)| canonical_u32(value, keys[index]))
6881        .collect::<Result<Vec<_>, _>>()?;
6882    if versions != [1, 1, 1, 1, 1] {
6883        return Err(PredicateApiError::IncompatibleContract(
6884            "proof-carrying controller MTBDD version tuple is unsupported".to_string(),
6885        ));
6886    }
6887    let limits = values[5..16]
6888        .iter()
6889        .enumerate()
6890        .map(|(index, value)| canonical_usize(value, keys[index + 5]))
6891        .collect::<Result<Vec<_>, _>>()?;
6892    if limits.contains(&0) {
6893        return Err(PredicateApiError::InvalidResponse(
6894            "proof-carrying controller MTBDD discovered limit must be positive".to_string(),
6895        ));
6896    }
6897    Ok(ControllerProofMtbddCapabilities {
6898        cli_version: versions[0],
6899        mtbdd_version: versions[1],
6900        equivalence_proof_version: versions[2],
6901        plant_artifact_version: versions[3],
6902        manifest_version: versions[4],
6903        max_manifest_bytes: limits[0],
6904        max_artifact_bytes: limits[1],
6905        max_equivalence_artifact_bytes: limits[2],
6906        max_unsat_proof_bytes: limits[3],
6907        max_members: limits[4],
6908        max_state_bits: limits[5],
6909        max_inputs: limits[6],
6910        max_outputs: limits[7],
6911        max_nodes: limits[8],
6912        max_terminals: limits[9],
6913        max_horizon: limits[10],
6914    })
6915}
6916
6917fn parse_controller_split_artifact_summary(
6918    output: ManagedOutput,
6919    prefix: &str,
6920    has_members: bool,
6921    expected_output: &Path,
6922    capabilities: &ControllerSplitEvidenceCapabilities,
6923) -> Result<Observed<ControllerSplitArtifactSummary>, PredicateOperationError> {
6924    let (stdout, mut metrics) = successful_stdout(output)?;
6925    let parsed = (|| -> Result<ControllerSplitArtifactSummary, PredicateApiError> {
6926        if stdout.contains('\r') || !stdout.ends_with('\n') || stdout.lines().count() != 1 {
6927            return Err(PredicateApiError::InvalidResponse(
6928                "controller split artifact response is not canonical".to_string(),
6929            ));
6930        }
6931        let line = stdout.trim_end_matches('\n');
6932        let (summary, output_path) = line.rsplit_once(" output=").ok_or_else(|| {
6933            PredicateApiError::InvalidResponse(
6934                "controller split artifact output field is missing".to_string(),
6935            )
6936        })?;
6937        if output_path != expected_output.to_string_lossy() {
6938            return Err(PredicateApiError::InvalidResponse(
6939                "controller split artifact output disagrees".to_string(),
6940            ));
6941        }
6942        let fields = summary.split(' ').collect::<Vec<_>>();
6943        let keys = if has_members {
6944            vec![
6945                prefix,
6946                "status",
6947                "cli_version",
6948                "artifact_version",
6949                "members",
6950                "artifact_bytes",
6951                "elapsed_micros",
6952            ]
6953        } else {
6954            vec![
6955                prefix,
6956                "status",
6957                "cli_version",
6958                "artifact_version",
6959                "mtbdd_nodes",
6960                "mtbdd_terminals",
6961                "artifact_bytes",
6962                "elapsed_micros",
6963            ]
6964        };
6965        if fields.len() != keys.len() || fields[0] != prefix {
6966            return Err(PredicateApiError::InvalidResponse(
6967                "controller split artifact field count is invalid".to_string(),
6968            ));
6969        }
6970        let values = fields[1..]
6971            .iter()
6972            .zip(&keys[1..])
6973            .map(|(field, key)| token_value(field, key))
6974            .collect::<Result<Vec<_>, _>>()?;
6975        if values[0] != "CREATED"
6976            || canonical_u32(values[1], "cli_version")? != capabilities.cli_version
6977        {
6978            return Err(PredicateApiError::IncompatibleContract(
6979                "controller split artifact creation contract changed".to_string(),
6980            ));
6981        }
6982        let artifact_version = canonical_u32(values[2], "artifact_version")?;
6983        let expected_version = if has_members {
6984            capabilities.plant_artifact_version
6985        } else {
6986            capabilities.controller_artifact_version
6987        };
6988        if artifact_version != expected_version {
6989            return Err(PredicateApiError::IncompatibleContract(
6990                "controller split artifact version changed".to_string(),
6991            ));
6992        }
6993        let members = has_members
6994            .then(|| canonical_usize(values[3], "members"))
6995            .transpose()?;
6996        if matches!(members, Some(0))
6997            || members.is_some_and(|count| {
6998                count > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6999            })
7000        {
7001            return Err(PredicateApiError::InvalidResponse(
7002                "controller split artifact member count is zero".to_string(),
7003            ));
7004        }
7005        let mtbdd_nodes = (!has_members)
7006            .then(|| canonical_usize(values[3], "mtbdd_nodes"))
7007            .transpose()?;
7008        let mtbdd_terminals = (!has_members)
7009            .then(|| canonical_usize(values[4], "mtbdd_terminals"))
7010            .transpose()?;
7011        if matches!(mtbdd_nodes, Some(0))
7012            || mtbdd_nodes.is_some_and(|count| count > controller_mtbdd::MAX_MTBDD_NODES)
7013            || matches!(mtbdd_terminals, Some(0))
7014            || mtbdd_terminals.is_some_and(|count| count > controller_mtbdd::MAX_MTBDD_TERMINALS)
7015        {
7016            return Err(PredicateApiError::InvalidResponse(
7017                "controller split evidence MTBDD dimensions are outside limits".to_string(),
7018            ));
7019        }
7020        let offset = if has_members { 4 } else { 5 };
7021        let artifact_bytes = canonical_usize(values[offset], "artifact_bytes")?;
7022        let elapsed_micros = canonical_usize(values[offset + 1], "elapsed_micros")?;
7023        if artifact_bytes == 0 || artifact_bytes > capabilities.max_artifact_bytes {
7024            return Err(PredicateApiError::InvalidResponse(
7025                "controller split artifact byte count is outside limits".to_string(),
7026            ));
7027        }
7028        Ok(ControllerSplitArtifactSummary {
7029            artifact_version,
7030            members,
7031            mtbdd_nodes,
7032            mtbdd_terminals,
7033            artifact_bytes,
7034            elapsed_micros,
7035        })
7036    })();
7037    match parsed {
7038        Ok(value) => Ok(Observed { value, metrics }),
7039        Err(error) => {
7040            metrics.status = InvocationStatus::Failed(error.failure_class());
7041            Err(PredicateOperationError {
7042                error: Box::new(error),
7043                metrics,
7044            })
7045        }
7046    }
7047}
7048
7049fn parse_controller_split_set_summary(
7050    output: ManagedOutput,
7051    expected_batches: usize,
7052    capabilities: &ControllerSplitEvidenceCapabilities,
7053) -> Result<Observed<ControllerSplitSetSummary>, PredicateOperationError> {
7054    let (stdout, mut metrics) = successful_stdout(output)?;
7055    let parsed = (|| -> Result<ControllerSplitSetSummary, PredicateApiError> {
7056        if stdout.contains('\r') || !stdout.ends_with('\n') {
7057            return Err(PredicateApiError::InvalidResponse(
7058                "controller split set response is not canonical LF text".to_string(),
7059            ));
7060        }
7061        let lines = stdout.lines().collect::<Vec<_>>();
7062        if lines.len() != expected_batches + 1 {
7063            return Err(PredicateApiError::InvalidResponse(
7064                "controller split set response line count is invalid".to_string(),
7065            ));
7066        }
7067        let mut batches = Vec::with_capacity(expected_batches);
7068        for (expected_index, line) in lines[..expected_batches].iter().enumerate() {
7069            let fields = line.split(' ').collect::<Vec<_>>();
7070            let keys = [
7071                "controller-split-batch",
7072                "index",
7073                "status",
7074                "members",
7075                "safe",
7076                "unsafe",
7077                "reachable_product_states",
7078                "explored_transitions",
7079                "artifact_bytes",
7080                "verification_micros",
7081            ];
7082            if fields.len() != keys.len() || fields[0] != keys[0] {
7083                return Err(PredicateApiError::InvalidResponse(
7084                    "controller split batch fields are invalid".to_string(),
7085                ));
7086            }
7087            let values = fields[1..]
7088                .iter()
7089                .zip(&keys[1..])
7090                .map(|(field, key)| token_value(field, key))
7091                .collect::<Result<Vec<_>, _>>()?;
7092            let index = canonical_usize(values[0], "index")?;
7093            if index != expected_index || values[1] != "VERIFIED" {
7094                return Err(PredicateApiError::InvalidResponse(
7095                    "controller split batch order or status is invalid".to_string(),
7096                ));
7097            }
7098            let numbers = values[2..]
7099                .iter()
7100                .enumerate()
7101                .map(|(index, value)| canonical_usize(value, keys[index + 3]))
7102                .collect::<Result<Vec<_>, _>>()?;
7103            if numbers[0] == 0
7104                || numbers[0] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
7105                || numbers[1].checked_add(numbers[2]) != Some(numbers[0])
7106                || numbers[5] == 0
7107                || numbers[5] > capabilities.max_artifact_bytes
7108            {
7109                return Err(PredicateApiError::InvalidResponse(
7110                    "controller split batch dimensions are invalid".to_string(),
7111                ));
7112            }
7113            batches.push(ControllerSplitBatchSummary {
7114                index,
7115                members: numbers[0],
7116                safe: numbers[1],
7117                unsafe_count: numbers[2],
7118                reachable_product_states: numbers[3],
7119                explored_transitions: numbers[4],
7120                artifact_bytes: numbers[5],
7121                verification_micros: numbers[6],
7122            });
7123        }
7124        let fields = lines[expected_batches].split(' ').collect::<Vec<_>>();
7125        let keys = [
7126            "controller-split-set",
7127            "status",
7128            "cli_version",
7129            "controller_admissions",
7130            "batches",
7131            "members",
7132            "safe",
7133            "unsafe",
7134            "reachable_product_states",
7135            "explored_transitions",
7136            "controller_evidence_bytes",
7137            "admission_micros",
7138            "elapsed_micros",
7139        ];
7140        if fields.len() != keys.len() || fields[0] != keys[0] {
7141            return Err(PredicateApiError::InvalidResponse(
7142                "controller split aggregate fields are invalid".to_string(),
7143            ));
7144        }
7145        let values = fields[1..]
7146            .iter()
7147            .zip(&keys[1..])
7148            .map(|(field, key)| token_value(field, key))
7149            .collect::<Result<Vec<_>, _>>()?;
7150        if values[0] != "VERIFIED"
7151            || canonical_u32(values[1], "cli_version")? != capabilities.cli_version
7152        {
7153            return Err(PredicateApiError::IncompatibleContract(
7154                "controller split aggregate contract changed".to_string(),
7155            ));
7156        }
7157        let numbers = values[2..]
7158            .iter()
7159            .enumerate()
7160            .map(|(index, value)| canonical_usize(value, keys[index + 3]))
7161            .collect::<Result<Vec<_>, _>>()?;
7162        let summary = ControllerSplitSetSummary {
7163            controller_admissions: numbers[0],
7164            members: numbers[2],
7165            safe: numbers[3],
7166            unsafe_count: numbers[4],
7167            reachable_product_states: numbers[5],
7168            explored_transitions: numbers[6],
7169            controller_evidence_bytes: numbers[7],
7170            admission_micros: numbers[8],
7171            elapsed_micros: numbers[9],
7172            batches,
7173        };
7174        let batch_members = summary
7175            .batches
7176            .iter()
7177            .try_fold(0usize, |total, batch| total.checked_add(batch.members));
7178        let batch_safe = summary
7179            .batches
7180            .iter()
7181            .try_fold(0usize, |total, batch| total.checked_add(batch.safe));
7182        let batch_unsafe = summary
7183            .batches
7184            .iter()
7185            .try_fold(0usize, |total, batch| total.checked_add(batch.unsafe_count));
7186        let batch_reachable = summary.batches.iter().try_fold(0usize, |total, batch| {
7187            total.checked_add(batch.reachable_product_states)
7188        });
7189        let batch_transitions = summary.batches.iter().try_fold(0usize, |total, batch| {
7190            total.checked_add(batch.explored_transitions)
7191        });
7192        if summary.controller_admissions != 1
7193            || numbers[1] != expected_batches
7194            || Some(summary.members) != batch_members
7195            || Some(summary.safe) != batch_safe
7196            || Some(summary.unsafe_count) != batch_unsafe
7197            || Some(summary.reachable_product_states) != batch_reachable
7198            || Some(summary.explored_transitions) != batch_transitions
7199            || summary.controller_evidence_bytes == 0
7200            || summary.controller_evidence_bytes > capabilities.max_artifact_bytes
7201        {
7202            return Err(PredicateApiError::InvalidResponse(
7203                "controller split aggregate does not reconcile".to_string(),
7204            ));
7205        }
7206        Ok(summary)
7207    })();
7208    match parsed {
7209        Ok(value) => Ok(Observed { value, metrics }),
7210        Err(error) => {
7211            metrics.status = InvocationStatus::Failed(error.failure_class());
7212            Err(PredicateOperationError {
7213                error: Box::new(error),
7214                metrics,
7215            })
7216        }
7217    }
7218}
7219
7220fn parse_controller_split_resource_set_summary(
7221    output: ManagedOutput,
7222    expected_batches: usize,
7223    capabilities: &ControllerSplitResourceCapabilities,
7224) -> Result<Observed<ControllerSplitResourceSetSummary>, PredicateOperationError> {
7225    let (stdout, mut metrics) = successful_stdout(output)?;
7226    let parsed = (|| -> Result<ControllerSplitResourceSetSummary, PredicateApiError> {
7227        if stdout.contains('\r') || !stdout.ends_with('\n') {
7228            return Err(PredicateApiError::InvalidResponse(
7229                "controller split resource response is not canonical LF text".to_string(),
7230            ));
7231        }
7232        let lines = stdout.lines().collect::<Vec<_>>();
7233        if lines.len() != expected_batches + 1 {
7234            return Err(PredicateApiError::InvalidResponse(
7235                "controller split resource response line count is invalid".to_string(),
7236            ));
7237        }
7238        let mut batches = Vec::with_capacity(expected_batches);
7239        for (expected_index, line) in lines[..expected_batches].iter().enumerate() {
7240            let fields = line.split(' ').collect::<Vec<_>>();
7241            let keys = [
7242                "controller-split-resource-batch",
7243                "index",
7244                "status",
7245                "policy_version",
7246                "envelope_version",
7247                "artifact_version",
7248                "members",
7249                "maximum_member_horizon",
7250                "maximum_product_states",
7251                "transition_evaluation_bound",
7252                "safe",
7253                "unsafe",
7254                "reachable_product_states",
7255                "explored_transitions",
7256                "artifact_bytes",
7257                "verification_micros",
7258            ];
7259            if fields.len() != keys.len() || fields[0] != keys[0] {
7260                return Err(PredicateApiError::InvalidResponse(
7261                    "controller split resource batch fields are invalid".to_string(),
7262                ));
7263            }
7264            let values = fields[1..]
7265                .iter()
7266                .zip(&keys[1..])
7267                .map(|(field, key)| token_value(field, key))
7268                .collect::<Result<Vec<_>, _>>()?;
7269            let index = canonical_usize(values[0], "index")?;
7270            if index != expected_index || values[1] != "VERIFIED" {
7271                return Err(PredicateApiError::InvalidResponse(
7272                    "controller split resource batch order or status is invalid".to_string(),
7273                ));
7274            }
7275            let policy_version = canonical_u32(values[2], "policy_version")?;
7276            let envelope_version = canonical_u32(values[3], "envelope_version")?;
7277            let artifact_version = canonical_u32(values[4], "artifact_version")?;
7278            if policy_version != capabilities.policy_version
7279                || envelope_version != capabilities.plant_envelope_version
7280                || artifact_version != capabilities.plant_artifact_version
7281            {
7282                return Err(PredicateApiError::IncompatibleContract(
7283                    "controller split resource batch versions changed".to_string(),
7284                ));
7285            }
7286            let numbers = values[5..]
7287                .iter()
7288                .enumerate()
7289                .map(|(index, value)| canonical_usize(value, keys[index + 6]))
7290                .collect::<Result<Vec<_>, _>>()?;
7291            if numbers[0] == 0
7292                || numbers[0] > capabilities.max_members_per_batch
7293                || numbers[1] > capabilities.max_horizon
7294                || numbers[2] > capabilities.max_product_states
7295                || numbers[4].checked_add(numbers[5]) != Some(numbers[0])
7296                || numbers[8] == 0
7297                || numbers[8] > capabilities.max_plant_artifact_bytes
7298            {
7299                return Err(PredicateApiError::InvalidResponse(
7300                    "controller split resource batch dimensions are invalid".to_string(),
7301                ));
7302            }
7303            batches.push(ControllerSplitResourceBatchSummary {
7304                index,
7305                members: numbers[0],
7306                maximum_member_horizon: numbers[1],
7307                maximum_product_states: numbers[2],
7308                transition_evaluation_bound: numbers[3],
7309                safe: numbers[4],
7310                unsafe_count: numbers[5],
7311                reachable_product_states: numbers[6],
7312                explored_transitions: numbers[7],
7313                artifact_bytes: numbers[8],
7314                verification_micros: numbers[9],
7315            });
7316        }
7317        let fields = lines[expected_batches].split(' ').collect::<Vec<_>>();
7318        let keys = [
7319            "controller-split-resource-set",
7320            "status",
7321            "cli_version",
7322            "policy_version",
7323            "controller_envelope_version",
7324            "plant_envelope_version",
7325            "controller_admissions",
7326            "batches",
7327            "members",
7328            "safe",
7329            "unsafe",
7330            "reachable_product_states",
7331            "explored_transitions",
7332            "controller_evidence_bytes",
7333            "controller_mtbdd_bytes",
7334            "equivalence_artifact_bytes",
7335            "unsat_proof_bytes",
7336            "total_plant_artifact_bytes",
7337            "total_transition_evaluation_bound",
7338            "admission_micros",
7339            "elapsed_micros",
7340        ];
7341        if fields.len() != keys.len() || fields[0] != keys[0] {
7342            return Err(PredicateApiError::InvalidResponse(
7343                "controller split resource aggregate fields are invalid".to_string(),
7344            ));
7345        }
7346        let values = fields[1..]
7347            .iter()
7348            .zip(&keys[1..])
7349            .map(|(field, key)| token_value(field, key))
7350            .collect::<Result<Vec<_>, _>>()?;
7351        if values[0] != "VERIFIED"
7352            || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7353            || canonical_u32(values[2], keys[3])? != capabilities.policy_version
7354            || canonical_u32(values[3], keys[4])? != capabilities.controller_envelope_version
7355            || canonical_u32(values[4], keys[5])? != capabilities.plant_envelope_version
7356        {
7357            return Err(PredicateApiError::IncompatibleContract(
7358                "controller split resource aggregate contract changed".to_string(),
7359            ));
7360        }
7361        let numbers = values[5..]
7362            .iter()
7363            .enumerate()
7364            .map(|(index, value)| canonical_usize(value, keys[index + 6]))
7365            .collect::<Result<Vec<_>, _>>()?;
7366        let summary = ControllerSplitResourceSetSummary {
7367            controller_admissions: numbers[0],
7368            members: numbers[2],
7369            safe: numbers[3],
7370            unsafe_count: numbers[4],
7371            reachable_product_states: numbers[5],
7372            explored_transitions: numbers[6],
7373            controller_evidence_bytes: numbers[7],
7374            controller_mtbdd_bytes: numbers[8],
7375            equivalence_artifact_bytes: numbers[9],
7376            unsat_proof_bytes: numbers[10],
7377            total_plant_artifact_bytes: numbers[11],
7378            total_transition_evaluation_bound: numbers[12],
7379            admission_micros: numbers[13],
7380            elapsed_micros: numbers[14],
7381            batches,
7382        };
7383        let batch_members = summary
7384            .batches
7385            .iter()
7386            .try_fold(0usize, |total, batch| total.checked_add(batch.members));
7387        let batch_safe = summary
7388            .batches
7389            .iter()
7390            .try_fold(0usize, |total, batch| total.checked_add(batch.safe));
7391        let batch_unsafe = summary
7392            .batches
7393            .iter()
7394            .try_fold(0usize, |total, batch| total.checked_add(batch.unsafe_count));
7395        let batch_reachable = summary.batches.iter().try_fold(0usize, |total, batch| {
7396            total.checked_add(batch.reachable_product_states)
7397        });
7398        let batch_explored = summary.batches.iter().try_fold(0usize, |total, batch| {
7399            total.checked_add(batch.explored_transitions)
7400        });
7401        let batch_artifact_bytes = summary.batches.iter().try_fold(0usize, |total, batch| {
7402            total.checked_add(batch.artifact_bytes)
7403        });
7404        let batch_transition_bound = summary.batches.iter().try_fold(0usize, |total, batch| {
7405            total.checked_add(batch.transition_evaluation_bound)
7406        });
7407        if summary.controller_admissions != 1
7408            || numbers[1] != expected_batches
7409            || Some(summary.members) != batch_members
7410            || Some(summary.safe) != batch_safe
7411            || Some(summary.unsafe_count) != batch_unsafe
7412            || Some(summary.reachable_product_states) != batch_reachable
7413            || Some(summary.explored_transitions) != batch_explored
7414            || Some(summary.total_plant_artifact_bytes) != batch_artifact_bytes
7415            || Some(summary.total_transition_evaluation_bound) != batch_transition_bound
7416            || summary.controller_evidence_bytes == 0
7417            || summary.controller_evidence_bytes > capabilities.max_controller_artifact_bytes
7418            || summary.controller_mtbdd_bytes == 0
7419            || summary.equivalence_artifact_bytes == 0
7420            || summary.unsat_proof_bytes == 0
7421            || summary.unsat_proof_bytes > capabilities.max_unsat_proof_bytes
7422            || summary.controller_mtbdd_bytes >= summary.controller_evidence_bytes
7423            || summary.equivalence_artifact_bytes >= summary.controller_evidence_bytes
7424            || summary
7425                .controller_mtbdd_bytes
7426                .checked_add(summary.equivalence_artifact_bytes)
7427                .is_none_or(|payload| payload >= summary.controller_evidence_bytes)
7428            || summary.unsat_proof_bytes > summary.equivalence_artifact_bytes
7429        {
7430            return Err(PredicateApiError::InvalidResponse(
7431                "controller split resource aggregate does not reconcile".to_string(),
7432            ));
7433        }
7434        Ok(summary)
7435    })();
7436    match parsed {
7437        Ok(value) => Ok(Observed { value, metrics }),
7438        Err(error) => {
7439            metrics.status = InvocationStatus::Failed(error.failure_class());
7440            Err(PredicateOperationError {
7441                error: Box::new(error),
7442                metrics,
7443            })
7444        }
7445    }
7446}
7447
7448fn parse_controller_split_observed_summary(
7449    output: ManagedOutput,
7450    expected_batches: usize,
7451    capabilities: &ControllerSplitObservabilityCapabilities,
7452) -> Result<Observed<ControllerSplitObservedSummary>, PredicateOperationError> {
7453    let status = output.status;
7454    let (stdout, mut invocation_metrics) = successful_stdout(output)?;
7455    let parsed = (|| -> Result<ControllerSplitObservedSummary, PredicateApiError> {
7456        if stdout.contains('\r')
7457            || !stdout.ends_with('\n')
7458            || stdout.lines().count() != expected_batches + 2
7459        {
7460            return Err(PredicateApiError::InvalidResponse(
7461                "controller split observed response line count is invalid".to_string(),
7462            ));
7463        }
7464        let lines = stdout.lines().collect::<Vec<_>>();
7465        let base_stdout = format!("{}\n", lines[..expected_batches + 1].join("\n"));
7466        let base_output = ManagedOutput {
7467            status,
7468            stdout: base_stdout.into_bytes(),
7469            stderr: Vec::new(),
7470            metrics: invocation_metrics.clone(),
7471        };
7472        let verification = parse_controller_split_resource_set_summary(
7473            base_output,
7474            expected_batches,
7475            &capabilities.resource,
7476        )
7477        .map_err(|failure| *failure.error)?
7478        .value;
7479
7480        let fields = lines[expected_batches + 1].split(' ').collect::<Vec<_>>();
7481        let keys = [
7482            "controller-split-resource-observability",
7483            "status",
7484            "cli_version",
7485            "phase_metrics_version",
7486            "policy_and_input_micros",
7487            "controller_admission_micros",
7488            "complete_set_preflight_micros",
7489            "semantic_replay_micros",
7490            "total_micros",
7491            "controller_admissions",
7492            "manifest_loads",
7493            "plant_artifact_reads",
7494            "resource_assessments",
7495            "batch_verifications",
7496            "buffered_result_rows",
7497            "prepared_batches",
7498            "prepared_members",
7499            "controller_evidence_bytes",
7500            "total_plant_artifact_bytes",
7501            "total_transition_evaluation_bound",
7502            "timing_calibration",
7503        ];
7504        if fields.len() != keys.len() || fields[0] != keys[0] {
7505            return Err(PredicateApiError::InvalidResponse(
7506                "controller split observed metrics fields are invalid".to_string(),
7507            ));
7508        }
7509        let values = fields[1..]
7510            .iter()
7511            .zip(&keys[1..])
7512            .map(|(field, key)| token_value(field, key))
7513            .collect::<Result<Vec<_>, _>>()?;
7514        if values[0] != "MEASURED"
7515            || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7516            || canonical_u32(values[2], keys[3])? != capabilities.phase_metrics_version
7517            || values[19] != "none"
7518        {
7519            return Err(PredicateApiError::IncompatibleContract(
7520                "controller split observed metrics contract changed".to_string(),
7521            ));
7522        }
7523        let timings = values[3..8]
7524            .iter()
7525            .enumerate()
7526            .map(|(index, value)| canonical_u128(value, keys[index + 4]))
7527            .collect::<Result<Vec<_>, _>>()?;
7528        let counts = values[8..19]
7529            .iter()
7530            .enumerate()
7531            .map(|(index, value)| canonical_usize(value, keys[index + 9]))
7532            .collect::<Result<Vec<_>, _>>()?;
7533        let expected_manifest_loads = expected_batches
7534            .checked_mul(2)
7535            .and_then(|count| count.checked_add(1));
7536        let expected_double_batches = expected_batches.checked_mul(2);
7537        let expected_rows = expected_batches.checked_add(1);
7538        let phase_sum = timings[..4]
7539            .iter()
7540            .try_fold(0u128, |total, value| total.checked_add(*value));
7541        if phase_sum.is_none_or(|sum| sum > timings[4])
7542            || u128::try_from(verification.elapsed_micros) != Ok(timings[4])
7543            || counts[0] != 1
7544            || Some(counts[1]) != expected_manifest_loads
7545            || Some(counts[2]) != expected_double_batches
7546            || Some(counts[3]) != expected_double_batches
7547            || counts[4] != expected_batches
7548            || Some(counts[5]) != expected_rows
7549            || counts[6] != expected_batches
7550            || counts[7] != verification.members
7551            || counts[8] != verification.controller_evidence_bytes
7552            || counts[9] != verification.total_plant_artifact_bytes
7553            || counts[10] != verification.total_transition_evaluation_bound
7554        {
7555            return Err(PredicateApiError::InvalidResponse(
7556                "controller split observed metrics do not reconcile".to_string(),
7557            ));
7558        }
7559        Ok(ControllerSplitObservedSummary {
7560            verification,
7561            phases: ControllerSplitPhaseMetrics {
7562                version: capabilities.phase_metrics_version,
7563                policy_and_input_micros: timings[0],
7564                controller_admission_micros: timings[1],
7565                complete_set_preflight_micros: timings[2],
7566                semantic_replay_micros: timings[3],
7567                total_micros: timings[4],
7568                controller_admissions: counts[0],
7569                manifest_loads: counts[1],
7570                plant_artifact_reads: counts[2],
7571                resource_assessments: counts[3],
7572                batch_verifications: counts[4],
7573                buffered_result_rows: counts[5],
7574                prepared_batches: counts[6],
7575                prepared_members: counts[7],
7576                controller_evidence_bytes: counts[8],
7577                total_plant_artifact_bytes: counts[9],
7578                total_transition_evaluation_bound: counts[10],
7579            },
7580        })
7581    })();
7582    match parsed {
7583        Ok(value) => Ok(Observed {
7584            value,
7585            metrics: invocation_metrics,
7586        }),
7587        Err(error) => {
7588            invocation_metrics.status = InvocationStatus::Failed(error.failure_class());
7589            Err(PredicateOperationError {
7590                error: Box::new(error),
7591                metrics: invocation_metrics,
7592            })
7593        }
7594    }
7595}
7596
7597fn parse_controller_split_allocation_observed_summary(
7598    output: ManagedOutput,
7599    expected_batches: usize,
7600    capabilities: &ControllerSplitAllocationObservabilityCapabilities,
7601) -> Result<Observed<ControllerSplitAllocationObservedSummary>, PredicateOperationError> {
7602    let status = output.status;
7603    let (stdout, mut invocation_metrics) = successful_stdout(output)?;
7604    let parsed = (|| -> Result<ControllerSplitAllocationObservedSummary, PredicateApiError> {
7605        if stdout.contains('\r')
7606            || !stdout.ends_with('\n')
7607            || stdout.lines().count() != expected_batches + 3
7608        {
7609            return Err(PredicateApiError::InvalidResponse(
7610                "controller split allocation response line count is invalid".to_string(),
7611            ));
7612        }
7613        let lines = stdout.lines().collect::<Vec<_>>();
7614        let observed_stdout = format!("{}\n", lines[..expected_batches + 2].join("\n"));
7615        let observed_output = ManagedOutput {
7616            status,
7617            stdout: observed_stdout.into_bytes(),
7618            stderr: Vec::new(),
7619            metrics: invocation_metrics.clone(),
7620        };
7621        let observed = parse_controller_split_observed_summary(
7622            observed_output,
7623            expected_batches,
7624            &capabilities.observability,
7625        )
7626        .map_err(|failure| *failure.error)?
7627        .value;
7628
7629        let fields = lines[expected_batches + 2].split(' ').collect::<Vec<_>>();
7630        let keys = [
7631            "controller-split-allocation-observability",
7632            "status",
7633            "cli_version",
7634            "allocator",
7635            "scope",
7636            "allocation_calls",
7637            "allocated_bytes",
7638            "deallocation_calls",
7639            "deallocated_bytes",
7640            "reallocation_calls",
7641            "reallocated_bytes",
7642            "overflow",
7643            "timing_calibration",
7644        ];
7645        if fields.len() != keys.len() || fields[0] != keys[0] {
7646            return Err(PredicateApiError::InvalidResponse(
7647                "controller split allocation metrics fields are invalid".to_string(),
7648            ));
7649        }
7650        let values = fields[1..]
7651            .iter()
7652            .zip(&keys[1..])
7653            .map(|(field, key)| token_value(field, key))
7654            .collect::<Result<Vec<_>, _>>()?;
7655        if values[0] != "MEASURED"
7656            || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7657            || values[2] != "system"
7658            || values[3] != "policy-through-replay"
7659            || values[10] != "none"
7660            || values[11] != "none"
7661        {
7662            return Err(PredicateApiError::IncompatibleContract(
7663                "controller split allocation metrics contract changed".to_string(),
7664            ));
7665        }
7666        let counts = values[4..10]
7667            .iter()
7668            .enumerate()
7669            .map(|(index, value)| canonical_u64(value, keys[index + 5]))
7670            .collect::<Result<Vec<_>, _>>()?;
7671        if counts[0] == 0 || counts[1] == 0 {
7672            return Err(PredicateApiError::InvalidResponse(
7673                "controller split allocation metrics are empty".to_string(),
7674            ));
7675        }
7676        Ok(ControllerSplitAllocationObservedSummary {
7677            observed,
7678            allocations: ControllerSplitAllocationMetrics {
7679                version: capabilities.cli_version,
7680                allocation_calls: counts[0],
7681                allocated_bytes: counts[1],
7682                deallocation_calls: counts[2],
7683                deallocated_bytes: counts[3],
7684                reallocation_calls: counts[4],
7685                reallocated_bytes: counts[5],
7686            },
7687        })
7688    })();
7689    match parsed {
7690        Ok(value) => Ok(Observed {
7691            value,
7692            metrics: invocation_metrics,
7693        }),
7694        Err(error) => {
7695            invocation_metrics.status = InvocationStatus::Failed(error.failure_class());
7696            Err(PredicateOperationError {
7697                error: Box::new(error),
7698                metrics: invocation_metrics,
7699            })
7700        }
7701    }
7702}
7703
7704fn parse_controller_split_cache_observed_summary(
7705    output: ManagedOutput,
7706    expected_batches: usize,
7707    capabilities: &ControllerSplitCacheObservabilityCapabilities,
7708) -> Result<Observed<ControllerSplitCacheObservedSummary>, PredicateOperationError> {
7709    let status = output.status;
7710    let (stdout, mut invocation_metrics) = successful_stdout(output)?;
7711    let parsed = (|| -> Result<ControllerSplitCacheObservedSummary, PredicateApiError> {
7712        if stdout.contains('\r')
7713            || !stdout.ends_with('\n')
7714            || stdout.lines().count() != expected_batches + 4
7715        {
7716            return Err(PredicateApiError::InvalidResponse(
7717                "controller split cache response line count is invalid".to_string(),
7718            ));
7719        }
7720        let lines = stdout.lines().collect::<Vec<_>>();
7721        let allocation_stdout = format!("{}\n", lines[..expected_batches + 3].join("\n"));
7722        let allocation_output = ManagedOutput {
7723            status,
7724            stdout: allocation_stdout.into_bytes(),
7725            stderr: Vec::new(),
7726            metrics: invocation_metrics.clone(),
7727        };
7728        let observed = parse_controller_split_allocation_observed_summary(
7729            allocation_output,
7730            expected_batches,
7731            &capabilities.allocation_observability,
7732        )
7733        .map_err(|failure| *failure.error)?
7734        .value;
7735
7736        let fields = lines[expected_batches + 3].split(' ').collect::<Vec<_>>();
7737        let keys = [
7738            "controller-split-cache-observability",
7739            "status",
7740            "cli_version",
7741            "scope",
7742            "key",
7743            "lookups",
7744            "hits",
7745            "misses",
7746            "entries",
7747            "integrity_preflight",
7748            "overflow",
7749            "timing_calibration",
7750        ];
7751        if fields.len() != keys.len() || fields[0] != keys[0] {
7752            return Err(PredicateApiError::InvalidResponse(
7753                "controller split cache metrics fields are invalid".to_string(),
7754            ));
7755        }
7756        let values = fields[1..]
7757            .iter()
7758            .zip(&keys[1..])
7759            .map(|(field, key)| token_value(field, key))
7760            .collect::<Result<Vec<_>, _>>()?;
7761        if values[0] != "MEASURED"
7762            || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7763            || values[2] != "semantic-replay"
7764            || values[3] != "manifest-snapshot,resource-assessment,result-sha256"
7765            || values[8] != "required"
7766            || values[9] != "none"
7767            || values[10] != "none"
7768        {
7769            return Err(PredicateApiError::IncompatibleContract(
7770                "controller split cache metrics contract changed".to_string(),
7771            ));
7772        }
7773        let counts = values[4..8]
7774            .iter()
7775            .enumerate()
7776            .map(|(index, value)| canonical_usize(value, keys[index + 5]))
7777            .collect::<Result<Vec<_>, _>>()?;
7778        let accounted = counts[1].checked_add(counts[2]);
7779        if counts[0] != expected_batches
7780            || accounted != Some(counts[0])
7781            || counts[3] != counts[2]
7782            || counts[3] > counts[0]
7783        {
7784            return Err(PredicateApiError::InvalidResponse(
7785                "controller split cache metrics do not reconcile".to_string(),
7786            ));
7787        }
7788        Ok(ControllerSplitCacheObservedSummary {
7789            observed,
7790            cache: ControllerSplitCacheMetrics {
7791                version: capabilities.cli_version,
7792                lookups: counts[0],
7793                hits: counts[1],
7794                misses: counts[2],
7795                entries: counts[3],
7796            },
7797        })
7798    })();
7799    match parsed {
7800        Ok(value) => Ok(Observed {
7801            value,
7802            metrics: invocation_metrics,
7803        }),
7804        Err(error) => {
7805            invocation_metrics.status = InvocationStatus::Failed(error.failure_class());
7806            Err(PredicateOperationError {
7807                error: Box::new(error),
7808                metrics: invocation_metrics,
7809            })
7810        }
7811    }
7812}
7813
7814fn classify_controller_split_resource_refusal(
7815    mut failure: PredicateOperationError,
7816) -> PredicateOperationError {
7817    let reason = match failure.error.as_ref() {
7818        PredicateApiError::CommandFailed {
7819            exit_code: Some(3),
7820            stderr,
7821        } => stderr
7822            .trim_end_matches(['\r', '\n'])
7823            .strip_prefix("error: controller-split-resource refusal=")
7824            .and_then(|value| value.strip_suffix(" result=none"))
7825            .and_then(|value| match value {
7826                "controller-artifact-bytes" => {
7827                    Some(ControllerPlantResourceRefusalReason::ControllerArtifactBytes)
7828                }
7829                "unsat-proof-bytes" => Some(ControllerPlantResourceRefusalReason::UnsatProofBytes),
7830                "batches" => Some(ControllerPlantResourceRefusalReason::Batches),
7831                "plant-artifact-bytes" => {
7832                    Some(ControllerPlantResourceRefusalReason::PlantArtifactBytes)
7833                }
7834                "members-per-batch" => Some(ControllerPlantResourceRefusalReason::MembersPerBatch),
7835                "horizon" => Some(ControllerPlantResourceRefusalReason::Horizon),
7836                "product-states" => Some(ControllerPlantResourceRefusalReason::ProductStates),
7837                "transitions-per-batch" => {
7838                    Some(ControllerPlantResourceRefusalReason::TransitionsPerBatch)
7839                }
7840                "total-plant-artifact-bytes" => {
7841                    Some(ControllerPlantResourceRefusalReason::TotalPlantArtifactBytes)
7842                }
7843                "total-members" => Some(ControllerPlantResourceRefusalReason::TotalMembers),
7844                "total-transition-evaluations" => {
7845                    Some(ControllerPlantResourceRefusalReason::TotalTransitionEvaluations)
7846                }
7847                _ => None,
7848            }),
7849        _ => None,
7850    };
7851    if let Some(reason) = reason {
7852        let error = PredicateApiError::ResourceRefused { reason };
7853        failure.metrics.status = InvocationStatus::Failed(error.failure_class());
7854        failure.error = Box::new(error);
7855    }
7856    failure
7857}
7858
7859fn parse_controller_mtbdd_summary(
7860    output: ManagedOutput,
7861    expected_action: &str,
7862    expected_output: Option<&Path>,
7863    capabilities: &ControllerMtbddCapabilities,
7864    summary_prefix: &str,
7865    member_prefix: &str,
7866) -> Result<Observed<ControllerMtbddBatchSummary>, PredicateOperationError> {
7867    let (stdout, mut metrics) = successful_stdout(output)?;
7868    let parsed = (|| -> Result<ControllerMtbddBatchSummary, PredicateApiError> {
7869        if stdout.contains('\r') || !stdout.ends_with('\n') {
7870            return Err(PredicateApiError::InvalidResponse(
7871                "controller MTBDD response is not canonical LF text".to_string(),
7872            ));
7873        }
7874        let mut lines = stdout.lines();
7875        let first = lines.next().ok_or_else(|| {
7876            PredicateApiError::InvalidResponse(
7877                "controller MTBDD summary line is missing".to_string(),
7878            )
7879        })?;
7880        let summary_text = if let Some(expected_output) = expected_output {
7881            first
7882                .split_once(" output=")
7883                .and_then(|(summary, output)| {
7884                    (output == expected_output.to_string_lossy()).then_some(summary)
7885                })
7886                .ok_or_else(|| {
7887                    PredicateApiError::InvalidResponse(
7888                        "controller MTBDD created response output disagrees".to_string(),
7889                    )
7890                })?
7891        } else if first.contains(" output=") {
7892            return Err(PredicateApiError::InvalidResponse(
7893                "controller MTBDD verified response unexpectedly names output".to_string(),
7894            ));
7895        } else {
7896            first
7897        };
7898        let fields = summary_text.split(' ').collect::<Vec<_>>();
7899        let keys = [
7900            summary_prefix,
7901            "status",
7902            "cli_version",
7903            "artifact_version",
7904            "members",
7905            "safe",
7906            "unsafe",
7907            "mtbdd_nodes",
7908            "mtbdd_terminals",
7909            "assignments_checked",
7910            "reachable_product_states",
7911            "explored_transitions",
7912            "artifact_bytes",
7913            "elapsed_micros",
7914        ];
7915        if fields.len() != keys.len() || fields[0] != keys[0] {
7916            return Err(PredicateApiError::InvalidResponse(
7917                "controller MTBDD summary shape is invalid".to_string(),
7918            ));
7919        }
7920        if token_value(fields[1], keys[1])? != expected_action {
7921            return Err(PredicateApiError::InvalidResponse(
7922                "controller MTBDD response action disagrees".to_string(),
7923            ));
7924        }
7925        let numeric = fields[2..]
7926            .iter()
7927            .zip(&keys[2..])
7928            .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
7929            .collect::<Result<Vec<_>, _>>()?;
7930        if numeric[0] != capabilities.cli_version as usize {
7931            return Err(PredicateApiError::IncompatibleContract(
7932                "controller MTBDD response CLI version changed".to_string(),
7933            ));
7934        }
7935        if numeric[1] != capabilities.plant_artifact_version as usize {
7936            return Err(PredicateApiError::IncompatibleContract(
7937                "controller MTBDD response artifact version changed".to_string(),
7938            ));
7939        }
7940        let member_count = numeric[2];
7941        if member_count == 0
7942            || member_count > capabilities.max_members
7943            || numeric[5] > capabilities.max_nodes
7944            || numeric[6] > capabilities.max_terminals
7945            || numeric[7] > capabilities.max_assignments
7946            || numeric[10] > capabilities.max_artifact_bytes
7947        {
7948            return Err(PredicateApiError::InvalidResponse(
7949                "controller MTBDD response exceeds discovered limits".to_string(),
7950            ));
7951        }
7952        let mut members = Vec::with_capacity(member_count);
7953        for expected_index in 0..member_count {
7954            let line = lines.next().ok_or_else(|| {
7955                PredicateApiError::InvalidResponse(
7956                    "controller MTBDD member line is missing".to_string(),
7957                )
7958            })?;
7959            let fields = line.split(' ').collect::<Vec<_>>();
7960            let keys = [
7961                member_prefix,
7962                "index",
7963                "answer",
7964                "horizon",
7965                "bad_frame",
7966                "trace_steps",
7967                "reachable_product_states",
7968                "explored_transitions",
7969            ];
7970            if fields.len() != keys.len() || fields[0] != keys[0] {
7971                return Err(PredicateApiError::InvalidResponse(
7972                    "controller MTBDD member shape is invalid".to_string(),
7973                ));
7974            }
7975            let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
7976            if index != expected_index {
7977                return Err(PredicateApiError::InvalidResponse(
7978                    "controller MTBDD member order is invalid".to_string(),
7979                ));
7980            }
7981            let answer = match token_value(fields[2], "answer")? {
7982                "SAFE" => ControllerMtbddAnswer::Safe,
7983                "UNSAFE" => ControllerMtbddAnswer::Unsafe,
7984                _ => {
7985                    return Err(PredicateApiError::InvalidResponse(
7986                        "controller MTBDD member answer is invalid".to_string(),
7987                    ));
7988                }
7989            };
7990            let bad = token_value(fields[4], "bad_frame")?;
7991            let bad_frame = if bad == "none" {
7992                None
7993            } else {
7994                Some(canonical_usize(bad, "bad_frame")?)
7995            };
7996            if matches!(answer, ControllerMtbddAnswer::Safe) != bad_frame.is_none() {
7997                return Err(PredicateApiError::InvalidResponse(
7998                    "controller MTBDD answer and bad frame disagree".to_string(),
7999                ));
8000            }
8001            let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
8002            let trace_steps =
8003                canonical_usize(token_value(fields[5], "trace_steps")?, "trace_steps")?;
8004            if horizon > capabilities.max_horizon
8005                || match (answer, bad_frame) {
8006                    (ControllerMtbddAnswer::Safe, None) => trace_steps != 0,
8007                    (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
8008                        frame > horizon || trace_steps != frame.saturating_add(1)
8009                    }
8010                    _ => true,
8011                }
8012            {
8013                return Err(PredicateApiError::InvalidResponse(
8014                    "controller MTBDD member trace boundary is invalid".to_string(),
8015                ));
8016            }
8017            members.push(ControllerMtbddMemberResult {
8018                index,
8019                answer,
8020                horizon,
8021                bad_frame,
8022                trace_steps,
8023                reachable_product_states: canonical_usize(
8024                    token_value(fields[6], "reachable_product_states")?,
8025                    "reachable_product_states",
8026                )?,
8027                explored_transitions: canonical_usize(
8028                    token_value(fields[7], "explored_transitions")?,
8029                    "explored_transitions",
8030                )?,
8031            });
8032        }
8033        let reachable_total = members
8034            .iter()
8035            .try_fold(0usize, |total, member| {
8036                total.checked_add(member.reachable_product_states)
8037            })
8038            .ok_or_else(|| {
8039                PredicateApiError::InvalidResponse(
8040                    "controller MTBDD reachable-state total overflows".to_string(),
8041                )
8042            })?;
8043        let transition_total = members
8044            .iter()
8045            .try_fold(0usize, |total, member| {
8046                total.checked_add(member.explored_transitions)
8047            })
8048            .ok_or_else(|| {
8049                PredicateApiError::InvalidResponse(
8050                    "controller MTBDD transition total overflows".to_string(),
8051                )
8052            })?;
8053        if lines.next().is_some()
8054            || members
8055                .iter()
8056                .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
8057                .count()
8058                != numeric[3]
8059            || members
8060                .iter()
8061                .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Unsafe))
8062                .count()
8063                != numeric[4]
8064            || reachable_total != numeric[8]
8065            || transition_total != numeric[9]
8066        {
8067            return Err(PredicateApiError::InvalidResponse(
8068                "controller MTBDD member totals disagree".to_string(),
8069            ));
8070        }
8071        Ok(ControllerMtbddBatchSummary {
8072            artifact_version: numeric[1] as u32,
8073            safe: numeric[3],
8074            unsafe_count: numeric[4],
8075            mtbdd_nodes: numeric[5],
8076            mtbdd_terminals: numeric[6],
8077            assignments_checked: numeric[7],
8078            reachable_product_states: numeric[8],
8079            explored_transitions: numeric[9],
8080            artifact_bytes: numeric[10],
8081            elapsed_micros: numeric[11],
8082            members,
8083        })
8084    })();
8085    match parsed {
8086        Ok(value) => Ok(Observed { value, metrics }),
8087        Err(error) => {
8088            metrics.status = InvocationStatus::Failed(error.failure_class());
8089            Err(PredicateOperationError {
8090                error: Box::new(error),
8091                metrics,
8092            })
8093        }
8094    }
8095}
8096
8097fn parse_capabilities(line: &str) -> Result<PredicateCapabilities, PredicateApiError> {
8098    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
8099    if fields.len() != 11 || line.contains('\r') || !line.ends_with('\n') {
8100        return Err(PredicateApiError::InvalidResponse(
8101            "capability line shape is not canonical".to_string(),
8102        ));
8103    }
8104    let value = |index: usize, key: &str| -> Result<&str, PredicateApiError> {
8105        fields[index]
8106            .strip_prefix(&format!("{key}="))
8107            .ok_or_else(|| {
8108                PredicateApiError::InvalidResponse(format!("expected capability `{key}`"))
8109            })
8110    };
8111    let number = |index: usize, key: &str| -> Result<u64, PredicateApiError> {
8112        let text = value(index, key)?;
8113        if text.is_empty() || (text.len() > 1 && text.starts_with('0')) {
8114            return Err(PredicateApiError::InvalidResponse(format!(
8115                "capability `{key}` is not canonical decimal"
8116            )));
8117        }
8118        text.parse::<u64>().map_err(|_| {
8119            PredicateApiError::InvalidResponse(format!("capability `{key}` is invalid"))
8120        })
8121    };
8122    let cli_version = u32::try_from(number(0, "predicate_cli_version")?).map_err(|_| {
8123        PredicateApiError::InvalidResponse("predicate CLI version exceeds u32".to_string())
8124    })?;
8125    if cli_version != PREDICATE_API_VERSION {
8126        return Err(PredicateApiError::IncompatibleContract(format!(
8127            "expected CLI v{PREDICATE_API_VERSION}, found v{cli_version}"
8128        )));
8129    }
8130    if value(1, "certificate_versions")? != "1,2" {
8131        return Err(PredicateApiError::IncompatibleContract(
8132            "certificate versions must be 1,2".to_string(),
8133        ));
8134    }
8135    if number(2, "portfolio_certificate_version")? != 1 {
8136        return Err(PredicateApiError::IncompatibleContract(
8137            "portfolio certificate version must be 1".to_string(),
8138        ));
8139    }
8140    let proof_format = value(3, "proof_format")?.to_string();
8141    if proof_format != "varisat-native-0.2.2" {
8142        return Err(PredicateApiError::IncompatibleContract(format!(
8143            "unsupported proof format `{proof_format}`"
8144        )));
8145    }
8146    let as_usize = |index: usize, key: &str| -> Result<usize, PredicateApiError> {
8147        usize::try_from(number(index, key)?).map_err(|_| {
8148            PredicateApiError::InvalidResponse(format!("capability `{key}` exceeds usize"))
8149        })
8150    };
8151    Ok(PredicateCapabilities {
8152        cli_version,
8153        certificate_versions: vec![CertificateVersion::V1, CertificateVersion::V2],
8154        portfolio_certificate_version: CertificateVersion::V1,
8155        proof_format,
8156        min_relevant_inputs: as_usize(4, "min_relevant_inputs")?,
8157        max_relevant_inputs: as_usize(5, "max_relevant_inputs")?,
8158        max_latches: as_usize(6, "max_latches")?,
8159        max_horizon: as_usize(7, "max_horizon")?,
8160        max_certificate_v2_bytes: number(8, "max_certificate_v2_bytes")?,
8161        max_proof_bytes: as_usize(9, "max_proof_bytes")?,
8162        max_total_proof_bytes: as_usize(10, "max_total_proof_bytes")?,
8163    })
8164}
8165
8166fn parse_event_contract_capabilities(
8167    line: &str,
8168) -> Result<EventContractCapabilities, EventContractApiError> {
8169    let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
8170    if fields.len() != 13 || line.contains('\r') || !line.ends_with('\n') {
8171        return Err(PredicateApiError::InvalidResponse(
8172            "event-contract capability line shape is not canonical".to_string(),
8173        ));
8174    }
8175    let value = |index: usize, key: &str| -> Result<&str, PredicateApiError> {
8176        fields[index]
8177            .strip_prefix(&format!("{key}="))
8178            .ok_or_else(|| {
8179                PredicateApiError::InvalidResponse(format!(
8180                    "expected event-contract capability `{key}`"
8181                ))
8182            })
8183    };
8184    let number = |index: usize, key: &str| -> Result<u64, PredicateApiError> {
8185        let text = value(index, key)?;
8186        if text.is_empty()
8187            || !text.bytes().all(|byte| byte.is_ascii_digit())
8188            || (text.len() > 1 && text.starts_with('0'))
8189        {
8190            return Err(PredicateApiError::InvalidResponse(format!(
8191                "event-contract capability `{key}` is not canonical decimal"
8192            )));
8193        }
8194        text.parse::<u64>().map_err(|_| {
8195            PredicateApiError::InvalidResponse(format!(
8196                "event-contract capability `{key}` is invalid"
8197            ))
8198        })
8199    };
8200    let as_usize = |index: usize, key: &str| -> Result<usize, PredicateApiError> {
8201        usize::try_from(number(index, key)?).map_err(|_| {
8202            PredicateApiError::InvalidResponse(format!(
8203                "event-contract capability `{key}` exceeds usize"
8204            ))
8205        })
8206    };
8207    let cli_version = u32::try_from(number(0, "event_contract_cli_version")?).map_err(|_| {
8208        PredicateApiError::InvalidResponse("event-contract CLI version exceeds u32".to_string())
8209    })?;
8210    if cli_version != EVENT_CONTRACT_API_VERSION {
8211        return Err(PredicateApiError::IncompatibleContract(format!(
8212            "expected event-contract CLI v{EVENT_CONTRACT_API_VERSION}, found v{cli_version}"
8213        )));
8214    }
8215    let certificate_version = u32::try_from(number(1, "certificate_version")?).map_err(|_| {
8216        PredicateApiError::InvalidResponse(
8217            "event-contract certificate version exceeds u32".to_string(),
8218        )
8219    })?;
8220    let portfolio_version = u32::try_from(number(2, "portfolio_version")?).map_err(|_| {
8221        PredicateApiError::InvalidResponse(
8222            "event-contract portfolio version exceeds u32".to_string(),
8223        )
8224    })?;
8225    if certificate_version != 3 || portfolio_version != 1 {
8226        return Err(PredicateApiError::IncompatibleContract(
8227            "event-contract certificate v3 and portfolio v1 are required".to_string(),
8228        ));
8229    }
8230    let semantics = value(3, "semantics")?.to_string();
8231    if semantics != "bounded-named-cnf-terminal-bad-avoidance" {
8232        return Err(PredicateApiError::IncompatibleContract(format!(
8233            "unsupported event-contract semantics `{semantics}`"
8234        )));
8235    }
8236    let proof_format = value(4, "proof_format")?.to_string();
8237    if proof_format != "varisat-native-0.2.2" {
8238        return Err(PredicateApiError::IncompatibleContract(format!(
8239            "unsupported proof format `{proof_format}`"
8240        )));
8241    }
8242    Ok(EventContractCapabilities {
8243        cli_version,
8244        certificate_version,
8245        portfolio_version,
8246        semantics,
8247        proof_format,
8248        min_relevant_inputs: as_usize(5, "min_relevant_inputs")?,
8249        max_relevant_inputs: as_usize(6, "max_relevant_inputs")?,
8250        max_latches: as_usize(7, "max_latches")?,
8251        max_horizon: as_usize(8, "max_horizon")?,
8252        max_contract_bytes: number(9, "max_contract_bytes")?,
8253        max_certificate_bytes: number(10, "max_certificate_bytes")?,
8254        max_proof_bytes: as_usize(11, "max_proof_bytes")?,
8255        max_total_proof_bytes: as_usize(12, "max_total_proof_bytes")?,
8256    })
8257}
8258
8259#[cfg(test)]
8260mod tests {
8261    use super::*;
8262
8263    #[test]
8264    fn capability_parser_accepts_v1_and_rejects_drift() {
8265        let canonical = "predicate_cli_version=1 certificate_versions=1,2 portfolio_certificate_version=1 proof_format=varisat-native-0.2.2 min_relevant_inputs=9 max_relevant_inputs=16 max_latches=4 max_horizon=64 max_certificate_v2_bytes=16777216 max_proof_bytes=1048576 max_total_proof_bytes=8388608\n";
8266        let parsed = parse_capabilities(canonical).unwrap();
8267        assert_eq!(parsed.cli_version, 1);
8268        assert_eq!(parsed.certificate_versions.len(), 2);
8269        assert!(matches!(
8270            parse_capabilities(&canonical.replacen(
8271                "predicate_cli_version=1",
8272                "predicate_cli_version=2",
8273                1
8274            )),
8275            Err(PredicateApiError::IncompatibleContract(_))
8276        ));
8277        assert!(parse_capabilities(&canonical.replace(" max_latches=4", "")).is_err());
8278    }
8279
8280    #[test]
8281    fn event_contract_capability_parser_accepts_v1_and_rejects_drift() {
8282        let canonical = "event_contract_cli_version=1 certificate_version=3 portfolio_version=1 semantics=bounded-named-cnf-terminal-bad-avoidance proof_format=varisat-native-0.2.2 min_relevant_inputs=9 max_relevant_inputs=16 max_latches=4 max_horizon=64 max_contract_bytes=1048576 max_certificate_bytes=33554432 max_proof_bytes=1048576 max_total_proof_bytes=8388608\n";
8283        let parsed = parse_event_contract_capabilities(canonical).unwrap();
8284        assert_eq!(parsed.cli_version, 1);
8285        assert_eq!(parsed.certificate_version, 3);
8286        assert!(matches!(
8287            parse_event_contract_capabilities(&canonical.replacen(
8288                "event_contract_cli_version=1",
8289                "event_contract_cli_version=2",
8290                1
8291            )),
8292            Err(PredicateApiError::IncompatibleContract(_))
8293        ));
8294        assert!(
8295            parse_event_contract_capabilities(&canonical.replace(" max_latches=4", "")).is_err()
8296        );
8297    }
8298
8299    #[test]
8300    fn revision_impact_capability_parser_is_strict_and_fail_closed() {
8301        let canonical = "revision_impact_cli_version=2 impact_version=1 query_manifest_version=1 max_query_manifest_bytes=16384 max_input_bytes=67108864 max_evidence_bytes=16777216 max_bundle_bytes=67108864 max_atoms=8 max_combinations=256 max_queries=32 semantics=exact-counterfactual-v1 work_schema=verification-v1 query_schema=transition-semantic-set-v1 routing=none fallback=none unsupported=fail-closed\n";
8302        let parsed = parse_revision_impact_capabilities(canonical).unwrap();
8303        assert_eq!(parsed.cli_version, 2);
8304        assert_eq!(parsed.max_bundle_bytes, 64 * 1024 * 1024);
8305        for hostile in [
8306            canonical.replace("impact_version=1", "impact_version=2"),
8307            canonical.replace("max_atoms=8", "max_atoms=08"),
8308            canonical.replace("max_queries=32", "max_queries=33"),
8309            canonical.replace("routing=none", "routing=heuristic"),
8310            canonical.replace("work_schema=verification-v1", "work_schema=timing-v1"),
8311            canonical.replace(
8312                "query_schema=transition-semantic-set-v1",
8313                "query_schema=summary-v1",
8314            ),
8315            canonical.replace("fallback=none", "fallback=exact"),
8316            canonical.replace("unsupported=fail-closed", "unsupported=ignore"),
8317            canonical.replace('\n', "\r\n"),
8318            canonical.trim_end().to_string(),
8319            format!("{canonical}unexpected=1\n"),
8320        ] {
8321            assert!(parse_revision_impact_capabilities(&hostile).is_err());
8322        }
8323        assert!(canonical_node_list(&[]).is_err());
8324        assert!(canonical_node_list(&[0]).is_err());
8325        assert!(canonical_node_list(&[7, 7]).is_err());
8326        assert!(canonical_node_list(&[10, 7]).is_err());
8327        assert_eq!(canonical_node_list(&[7, 10]).unwrap(), "7,10");
8328    }
8329
8330    #[test]
8331    fn btor2_channel_property_capability_parser_is_strict_and_fail_closed() {
8332        let canonical = "btor2_channel_property_cli_version=1 artifact_version=1 query_manifest_version=1 policy_version=1 max_query_manifest_bytes=262144 max_policy_bytes=4096 max_model_bytes=8388608 max_channels=64 max_queries=4096 max_evidence_bytes=67108864 max_artifact_bytes=69206016 max_projected_work=100000000000 refusal_exit=3 routing=static-explicit-or-bitblast fallback=exact result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed verification=source-replay\n";
8333        let parsed = parse_btor2_channel_property_capabilities(canonical).unwrap();
8334        assert_eq!(parsed.cli_version, 1);
8335        assert_eq!(parsed.max_artifact_bytes, 66 * 1024 * 1024);
8336        for hostile in [
8337            canonical.replace("artifact_version=1", "artifact_version=2"),
8338            canonical.replace("max_channels=64", "max_channels=064"),
8339            canonical.replace("max_queries=4096", "max_queries=4095"),
8340            canonical.replace("refusal_exit=3", "refusal_exit=2"),
8341            canonical.replace("fallback=exact", "fallback=partial"),
8342            canonical.replace("result_on_refusal=none", "result_on_refusal=SAFE"),
8343            canonical.replace("verification=source-replay", "verification=trusted"),
8344            canonical.replace('\n', "\r\n"),
8345            canonical.trim_end().to_string(),
8346            format!("{canonical}unexpected=1\n"),
8347        ] {
8348            assert!(parse_btor2_channel_property_capabilities(&hostile).is_err());
8349        }
8350    }
8351
8352    #[test]
8353    fn btor2_channel_property_phase_parser_is_strict_and_fail_closed() {
8354        let base = parse_btor2_channel_property_capabilities("btor2_channel_property_cli_version=1 artifact_version=1 query_manifest_version=1 policy_version=1 max_query_manifest_bytes=262144 max_policy_bytes=4096 max_model_bytes=8388608 max_channels=64 max_queries=4096 max_evidence_bytes=67108864 max_artifact_bytes=69206016 max_projected_work=100000000000 refusal_exit=3 routing=static-explicit-or-bitblast fallback=exact result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed verification=source-replay\n").unwrap();
8355        let capability = "btor2_channel_property_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=input,structural-admission,preflight,proof-construction,encoding,artifact-decode,source-replay,publication timing_calibration=none correctness_dependency=none partial_metrics_on_failure=none unsupported=fail-closed\n";
8356        let parsed =
8357            parse_btor2_channel_property_observability_capabilities(capability, &base).unwrap();
8358        for hostile in [
8359            capability.replace("phase_metrics_version=1", "phase_metrics_version=2"),
8360            capability.replace("proof-construction", "proof-generation"),
8361            capability.replace(
8362                "correctness_dependency=none",
8363                "correctness_dependency=timing",
8364            ),
8365            capability.replace(
8366                "partial_metrics_on_failure=none",
8367                "partial_metrics_on_failure=yes",
8368            ),
8369            capability.replace('\n', "\r\n"),
8370            capability.trim_end().to_string(),
8371            format!("{capability}unexpected=1\n"),
8372        ] {
8373            assert!(
8374                parse_btor2_channel_property_observability_capabilities(&hostile, &base).is_err()
8375            );
8376        }
8377
8378        let canonical = "btor2-channel-property-phases status=MEASURED observability_cli_version=1 phase_metrics_version=1 operation=certify input_micros=1 structural_admission_micros=2 preflight_micros=3 proof_construction_micros=4 encoding_micros=5 artifact_decode_micros=6 source_replay_micros=7 publication_micros=8 total_micros=40 timing_calibration=none correctness_dependency=none\n";
8379        let phases = parse_btor2_channel_property_phase_line(
8380            canonical,
8381            Btor2ChannelPropertyObservedOperation::Certify,
8382            &parsed,
8383        )
8384        .unwrap();
8385        assert_eq!(phases.total_micros, 40);
8386        for hostile in [
8387            canonical.replace("operation=certify", "operation=verify"),
8388            canonical.replace("input_micros=1", "input_micros=01"),
8389            canonical.replace("total_micros=40", "total_micros=35"),
8390            canonical.replace("timing_calibration=none", "timing_calibration=local"),
8391            canonical.replace(
8392                "correctness_dependency=none",
8393                "correctness_dependency=timing",
8394            ),
8395            canonical.replace("status=MEASURED", "status=PARTIAL"),
8396            canonical.replace('\n', "\r\n"),
8397            canonical.trim_end().to_string(),
8398            format!("{canonical}unexpected=1\n"),
8399        ] {
8400            assert!(
8401                parse_btor2_channel_property_phase_line(
8402                    &hostile,
8403                    Btor2ChannelPropertyObservedOperation::Certify,
8404                    &parsed,
8405                )
8406                .is_err()
8407            );
8408        }
8409
8410        let verify = "btor2-channel-property-phases status=MEASURED observability_cli_version=1 phase_metrics_version=1 operation=verify input_micros=1 structural_admission_micros=0 preflight_micros=0 proof_construction_micros=0 encoding_micros=0 artifact_decode_micros=2 source_replay_micros=3 publication_micros=0 total_micros=6 timing_calibration=none correctness_dependency=none\n";
8411        assert!(
8412            parse_btor2_channel_property_phase_line(
8413                verify,
8414                Btor2ChannelPropertyObservedOperation::Verify,
8415                &parsed,
8416            )
8417            .is_ok()
8418        );
8419        assert!(
8420            parse_btor2_channel_property_phase_line(
8421                &verify.replace("preflight_micros=0", "preflight_micros=1"),
8422                Btor2ChannelPropertyObservedOperation::Verify,
8423                &parsed,
8424            )
8425            .is_err()
8426        );
8427    }
8428
8429    #[test]
8430    fn controller_mtbdd_capability_parser_is_strict_and_fail_closed() {
8431        let canonical = "controller_mtbdd_cli_version=1 mtbdd_version=1 plant_artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=33554432 max_members=64 max_state_bits=20 max_inputs=32 max_outputs=8 max_nodes=1048576 max_terminals=1048576 max_assignments=1048576 max_horizon=4096 unsupported=fail-closed\n";
8432        let parsed = parse_controller_mtbdd_capabilities(canonical).unwrap();
8433        assert_eq!(parsed.cli_version, 1);
8434        assert_eq!(parsed.max_outputs, 8);
8435        assert!(
8436            parse_controller_mtbdd_capabilities(&canonical.replacen(
8437                "controller_mtbdd_cli_version=1",
8438                "controller_mtbdd_cli_version=4294967297",
8439                1
8440            ))
8441            .is_err()
8442        );
8443        assert!(
8444            parse_controller_mtbdd_capabilities(
8445                &canonical.replace("max_outputs=8", "max_outputs=08")
8446            )
8447            .is_err()
8448        );
8449        assert!(
8450            parse_controller_mtbdd_capabilities(
8451                &canonical.replace("max_members=64", "max_members=0")
8452            )
8453            .is_err()
8454        );
8455        assert!(
8456            parse_controller_mtbdd_capabilities(
8457                &canonical.replace("unsupported=fail-closed", "unsupported=best-effort")
8458            )
8459            .is_err()
8460        );
8461        assert!(parse_controller_mtbdd_capabilities(&canonical.replace('\n', "\r\n")).is_err());
8462    }
8463
8464    #[test]
8465    fn controller_proof_mtbdd_capability_parser_is_strict() {
8466        let canonical = "controller_proof_mtbdd_cli_version=1 mtbdd_version=1 equivalence_proof_version=1 plant_artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=16777216 max_equivalence_artifact_bytes=2097152 max_unsat_proof_bytes=1048576 max_members=64 max_state_bits=6 max_inputs=12 max_outputs=8 max_nodes=512 max_terminals=1024 max_horizon=1024 verification=unsat-miter exhaustive_replay=no unsupported=fail-closed\n";
8467        let parsed = parse_controller_proof_mtbdd_capabilities(canonical).unwrap();
8468        assert_eq!(parsed.equivalence_proof_version, 1);
8469        assert_eq!(parsed.max_equivalence_artifact_bytes, 2_097_152);
8470        assert_eq!(parsed.max_unsat_proof_bytes, 1_048_576);
8471        for hostile in [
8472            canonical.replace("verification=unsat-miter", "verification=trusted"),
8473            canonical.replace("exhaustive_replay=no", "exhaustive_replay=yes"),
8474            canonical.replace(
8475                "max_equivalence_artifact_bytes=2097152",
8476                "max_equivalence_artifact_bytes=0",
8477            ),
8478            canonical.replace("max_unsat_proof_bytes=1048576", "max_unsat_proof_bytes=0"),
8479            canonical.replace("unsupported=fail-closed", "unsupported=best-effort"),
8480            canonical.replace('\n', "\r\n"),
8481        ] {
8482            assert!(parse_controller_proof_mtbdd_capabilities(&hostile).is_err());
8483        }
8484    }
8485
8486    #[test]
8487    fn controller_split_evidence_capability_parser_is_strict() {
8488        let canonical = "controller_split_evidence_cli_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=16777216 max_batches=64 admission=once verification=unsat-miter exhaustive_replay=no source_binding=sha256 obligation_binding=complete-ordered unsupported=fail-closed\n";
8489        let parsed = parse_controller_split_evidence_capabilities(canonical).unwrap();
8490        assert_eq!(parsed.cli_version, 1);
8491        assert_eq!(parsed.max_batches, 64);
8492        for hostile in [
8493            canonical.replace("admission=once", "admission=per-batch"),
8494            canonical.replace("verification=unsat-miter", "verification=trusted"),
8495            canonical.replace("exhaustive_replay=no", "exhaustive_replay=yes"),
8496            canonical.replace("source_binding=sha256", "source_binding=path"),
8497            canonical.replace(
8498                "obligation_binding=complete-ordered",
8499                "obligation_binding=partial",
8500            ),
8501            canonical.replace("max_batches=64", "max_batches=0"),
8502            canonical.replace("max_batches=64", "max_batches=064"),
8503            canonical.replace("max_batches=64", "max_batches=65"),
8504            canonical.replace("max_artifact_bytes=16777216", "max_artifact_bytes=16777217"),
8505            canonical.replace("unsupported=fail-closed", "unsupported=best-effort"),
8506            canonical.replace('\n', "\r\n"),
8507        ] {
8508            assert!(parse_controller_split_evidence_capabilities(&hostile).is_err());
8509        }
8510    }
8511
8512    #[test]
8513    fn controller_split_resource_capability_parser_is_strict() {
8514        let canonical = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed\n";
8515        let parsed = parse_controller_split_resource_capabilities(canonical).unwrap();
8516        assert_eq!(parsed.policy_version, 1);
8517        assert_eq!(parsed.max_batches, 64);
8518        for hostile in [
8519            canonical.replace("admission=once", "admission=per-batch"),
8520            canonical.replace("verification=unsat-miter", "verification=trusted"),
8521            canonical.replace(
8522                "accounting=conservative-static-per-batch-and-total",
8523                "accounting=measured",
8524            ),
8525            canonical.replace("timing_calibration=none", "timing_calibration=per-formula"),
8526            canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8527            canonical.replace("refusal_exit=3", "refusal_exit=2"),
8528            canonical.replace("max_batches=64", "max_batches=65"),
8529            canonical.replace("max_policy_bytes=4096", "max_policy_bytes=0"),
8530            canonical.replace("refusal_schema=split-reason-v1", "refusal_schema=free-text"),
8531            canonical.replace('\n', "\r\n"),
8532        ] {
8533            assert!(parse_controller_split_resource_capabilities(&hostile).is_err());
8534        }
8535    }
8536
8537    #[test]
8538    fn controller_split_observability_capability_parser_is_strict() {
8539        let resource = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed";
8540        let observability = "controller_split_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=policy-and-input,controller-admission,complete-set-preflight,semantic-replay counters=controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8541        let canonical = format!("{resource}\n{observability}\n");
8542        let parsed = parse_controller_split_observability_capabilities(&canonical).unwrap();
8543        assert_eq!(parsed.resource.cli_version, 1);
8544        assert_eq!(parsed.cli_version, 1);
8545        assert_eq!(parsed.phase_metrics_version, 1);
8546        for hostile in [
8547            canonical.replace("phase_metrics_version=1", "phase_metrics_version=2"),
8548            canonical.replace("semantic-replay", "trusted-replay"),
8549            canonical.replace("manifest-loads", "manifest-hints"),
8550            canonical.replace(
8551                "partial_metrics_on_failure=none",
8552                "partial_metrics_on_failure=yes",
8553            ),
8554            canonical.replace("timing_calibration=none", "timing_calibration=per-formula"),
8555            canonical.replace('\n', "\r\n"),
8556            format!("{resource}\n"),
8557            format!("{observability}\n{resource}\n"),
8558        ] {
8559            assert!(parse_controller_split_observability_capabilities(&hostile).is_err());
8560        }
8561    }
8562
8563    #[test]
8564    fn controller_split_allocation_capability_parser_is_strict() {
8565        let resource = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed";
8566        let observability = "controller_split_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=policy-and-input,controller-admission,complete-set-preflight,semantic-replay counters=controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8567        let allocation = "controller_split_allocation_observability_cli_version=1 base_observability_cli_version=1 allocator=system scope=policy-through-replay counters=allocation-calls,allocated-bytes,deallocation-calls,deallocated-bytes,reallocation-calls,reallocated-bytes overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8568        let canonical = format!("{resource}\n{observability}\n{allocation}\n");
8569        let parsed =
8570            parse_controller_split_allocation_observability_capabilities(&canonical).unwrap();
8571        assert_eq!(parsed.cli_version, 1);
8572        assert_eq!(parsed.observability.cli_version, 1);
8573        for hostile in [
8574            canonical.replace("allocator=system", "allocator=jemalloc"),
8575            canonical.replace("scope=policy-through-replay", "scope=whole-process"),
8576            canonical.replace("allocated-bytes", "live-bytes"),
8577            canonical.replace("overflow=fail-closed", "overflow=saturate"),
8578            canonical.replace(
8579                "base_observability_cli_version=1",
8580                "base_observability_cli_version=2",
8581            ),
8582            canonical.replace('\n', "\r\n"),
8583            format!("{resource}\n{observability}\n"),
8584        ] {
8585            assert!(
8586                parse_controller_split_allocation_observability_capabilities(&hostile).is_err()
8587            );
8588        }
8589    }
8590
8591    #[test]
8592    fn controller_split_cache_capability_parser_is_strict() {
8593        let resource = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed";
8594        let observability = "controller_split_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=policy-and-input,controller-admission,complete-set-preflight,semantic-replay counters=controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8595        let allocation = "controller_split_allocation_observability_cli_version=1 base_observability_cli_version=1 allocator=system scope=policy-through-replay counters=allocation-calls,allocated-bytes,deallocation-calls,deallocated-bytes,reallocation-calls,reallocated-bytes overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8596        let cache = "controller_split_cache_observability_cli_version=1 base_allocation_observability_cli_version=1 scope=semantic-replay key=manifest-snapshot,resource-assessment,result-sha256 counters=lookups,hits,misses,entries integrity_preflight=required overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8597        let canonical = format!("{resource}\n{observability}\n{allocation}\n{cache}\n");
8598        let parsed = parse_controller_split_cache_observability_capabilities(&canonical).unwrap();
8599        assert_eq!(parsed.cli_version, 1);
8600        assert_eq!(parsed.allocation_observability.cli_version, 1);
8601        for hostile in [
8602            canonical.replace("scope=semantic-replay", "scope=preflight"),
8603            canonical.replace("manifest-snapshot", "manifest-path"),
8604            canonical.replace(
8605                "integrity_preflight=required",
8606                "integrity_preflight=optional",
8607            ),
8608            canonical.replace("hits,misses", "misses,hits"),
8609            canonical.replace("overflow=fail-closed", "overflow=saturate"),
8610            canonical.replace(
8611                "base_allocation_observability_cli_version=1",
8612                "base_allocation_observability_cli_version=2",
8613            ),
8614            canonical.replace('\n', "\r\n"),
8615            format!("{resource}\n{observability}\n{allocation}\n"),
8616        ] {
8617            assert!(parse_controller_split_cache_observability_capabilities(&hostile).is_err());
8618        }
8619    }
8620
8621    #[test]
8622    fn controller_plant_portfolio_capability_parser_is_strict() {
8623        let canonical = "controller_plant_portfolio_cli_version=1 artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=16777216 max_members=64 backends=mtbdd,direct-exact routing=static fallback=exact unsupported=fail-closed\n";
8624        let parsed = parse_controller_plant_portfolio_capabilities(canonical).unwrap();
8625        assert_eq!(parsed.cli_version, 1);
8626        assert_eq!(parsed.max_members, 64);
8627        assert!(
8628            parse_controller_plant_portfolio_capabilities(
8629                &canonical.replace("routing=static", "routing=timed")
8630            )
8631            .is_err()
8632        );
8633        assert!(
8634            parse_controller_plant_portfolio_capabilities(
8635                &canonical.replace("max_members=64", "max_members=064")
8636            )
8637            .is_err()
8638        );
8639        assert!(
8640            parse_controller_plant_portfolio_capabilities(&canonical.replace('\n', "\r\n"))
8641                .is_err()
8642        );
8643    }
8644
8645    #[test]
8646    fn controller_plant_resource_capability_parser_is_strict() {
8647        let canonical = "controller_plant_resource_cli_version=1 policy_version=1 envelope_version=1 manifest_version=1 portfolio_artifact_version=1 max_policy_bytes=4096 max_artifact_bytes=16777216 max_members=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed\n";
8648        let parsed = parse_controller_plant_resource_capabilities(canonical).unwrap();
8649        assert_eq!(parsed.envelope_version, 1);
8650        assert_eq!(parsed.max_product_states, 4096);
8651        for hostile in [
8652            canonical.replace("accounting=conservative-static", "accounting=measured"),
8653            canonical.replace("timing_calibration=none", "timing_calibration=per-formula"),
8654            canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8655            canonical.replace("refusal_exit=3", "refusal_exit=2"),
8656            canonical.replace("refusal_schema=reason-v1", "refusal_schema=free-text"),
8657            canonical.replace("max_policy_bytes=4096", "max_policy_bytes=0"),
8658            canonical.replace("max_members=64", "max_members=064"),
8659            canonical.replace('\n', "\r\n"),
8660        ] {
8661            assert!(parse_controller_plant_resource_capabilities(&hostile).is_err());
8662        }
8663    }
8664
8665    #[test]
8666    fn controller_proof_mtbdd_resource_capability_parser_is_strict() {
8667        let canonical = "controller_proof_mtbdd_resource_cli_version=1 policy_version=1 envelope_version=1 manifest_version=1 artifact_version=1 max_policy_bytes=4096 max_artifact_bytes=16777216 max_equivalence_artifact_bytes=2097152 max_unsat_proof_bytes=1048576 max_members=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 verification=unsat-miter exhaustive_replay=no accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=proof-reason-v1 unsupported=fail-closed\n";
8668        let parsed = parse_controller_proof_mtbdd_resource_capabilities(canonical).unwrap();
8669        assert_eq!(parsed.max_equivalence_artifact_bytes, 2_097_152);
8670        assert_eq!(parsed.max_unsat_proof_bytes, 1_048_576);
8671        for hostile in [
8672            canonical.replace("verification=unsat-miter", "verification=trusted"),
8673            canonical.replace("exhaustive_replay=no", "exhaustive_replay=yes"),
8674            canonical.replace("accounting=conservative-static", "accounting=measured"),
8675            canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8676            canonical.replace("refusal_schema=proof-reason-v1", "refusal_schema=free-text"),
8677            canonical.replace("max_unsat_proof_bytes=1048576", "max_unsat_proof_bytes=0"),
8678            canonical.replace("max_members=64", "max_members=064"),
8679            canonical.replace('\n', "\r\n"),
8680        ] {
8681            assert!(parse_controller_proof_mtbdd_resource_capabilities(&hostile).is_err());
8682        }
8683    }
8684
8685    #[test]
8686    fn controller_proof_mtbdd_portfolio_capability_parser_is_strict() {
8687        let canonical = "controller_proof_mtbdd_portfolio_cli_version=1 policy_version=1 envelope_version=1 artifact_version=1 proof_artifact_version=1 direct_artifact_version=1 manifest_version=1 source_model_attestation_version=1 max_policy_bytes=4096 max_artifact_bytes=16777216 max_equivalence_artifact_bytes=2097152 max_unsat_proof_bytes=1048576 max_members=64 max_horizon=1024 max_product_states=4096 max_attestation_bytes=65536 refusal_exit=3 backends=proof-mtbdd,direct-exact routing=static fallback=exact proof_failure=fail-closed attested_verification=required accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=proof-reason-v1 unsupported=fail-closed\n";
8688        let parsed = parse_controller_proof_mtbdd_portfolio_capabilities(canonical).unwrap();
8689        assert_eq!(parsed.artifact_version, 1);
8690        assert_eq!(parsed.max_unsat_proof_bytes, 1_048_576);
8691        for hostile in [
8692            canonical.replace("backends=proof-mtbdd,direct-exact", "backends=direct-exact"),
8693            canonical.replace("routing=static", "routing=timed"),
8694            canonical.replace("fallback=exact", "fallback=heuristic"),
8695            canonical.replace("proof_failure=fail-closed", "proof_failure=fallback"),
8696            canonical.replace(
8697                "attested_verification=required",
8698                "attested_verification=optional",
8699            ),
8700            canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8701            canonical.replace("max_members=64", "max_members=064"),
8702            canonical.replace('\n', "\r\n"),
8703        ] {
8704            assert!(parse_controller_proof_mtbdd_portfolio_capabilities(&hostile).is_err());
8705        }
8706    }
8707
8708    #[test]
8709    fn result_parser_rejects_missing_ambiguous_and_unknown_answers() {
8710        assert_eq!(
8711            parse_result("status=VERIFIED result=avoidable\n").unwrap(),
8712            PredicateResult::Avoidable
8713        );
8714        assert!(parse_result("status=VERIFIED\n").is_err());
8715        assert_eq!(
8716            parse_result("result=avoidable result=avoidable\n").unwrap(),
8717            PredicateResult::Avoidable
8718        );
8719        assert!(parse_result("result=avoidable result=unavoidable\n").is_err());
8720        assert!(parse_result("result=maybe\n").is_err());
8721    }
8722
8723    #[test]
8724    fn execution_policy_rejects_zero_and_excessive_bounds() {
8725        assert!(ExecutionPolicy::new(Duration::ZERO, 1).is_err());
8726        assert!(ExecutionPolicy::new(Duration::from_secs(1), 0).is_err());
8727        assert!(ExecutionPolicy::new(Duration::from_secs(1), MAX_OUTPUT_LIMIT_BYTES + 1).is_err());
8728        assert!(
8729            ExecutionPolicy::new(Duration::from_secs(1), 1024)
8730                .unwrap()
8731                .with_file_limit(0)
8732                .is_err()
8733        );
8734        assert_eq!(
8735            ExecutionPolicy::new(Duration::from_secs(2), 4096)
8736                .unwrap()
8737                .output_limit_bytes(),
8738            4096
8739        );
8740    }
8741
8742    #[test]
8743    fn invocation_metrics_csv_schema_is_stable() {
8744        let metrics = InvocationMetrics {
8745            schema_version: 1,
8746            operation: OperationKind::VerifyV2,
8747            duration: Duration::from_nanos(123),
8748            stdout_bytes: 45,
8749            stderr_bytes: 6,
8750            timeout: Duration::from_millis(700),
8751            output_limit_bytes: 8192,
8752            memory_limit_bytes: Some(1024 * 1024 * 1024),
8753            file_limit_bytes: 32 * 1024 * 1024,
8754            process_group_containment: true,
8755            exit_code: Some(2),
8756            status: InvocationStatus::Failed(FailureClass::ExitStatus),
8757        };
8758        assert_eq!(
8759            InvocationMetrics::csv_header(),
8760            "schema_version,operation,duration_ns,stdout_bytes,stderr_bytes,timeout_ms,output_limit_bytes,exit_code,status,failure_class"
8761        );
8762        assert_eq!(
8763            metrics.to_csv_row(),
8764            "1,verify_v2,123,45,6,700,8192,2,error,exit_status"
8765        );
8766
8767        let successful = InvocationMetrics {
8768            schema_version: 1,
8769            operation: OperationKind::Discover,
8770            duration: Duration::from_nanos(7),
8771            stdout_bytes: 10,
8772            stderr_bytes: 0,
8773            timeout: Duration::from_millis(700),
8774            output_limit_bytes: 8192,
8775            memory_limit_bytes: None,
8776            file_limit_bytes: 32 * 1024 * 1024,
8777            process_group_containment: true,
8778            exit_code: Some(0),
8779            status: InvocationStatus::Success,
8780        };
8781        let aggregate = aggregate_invocation_metrics([&metrics, &successful]).unwrap();
8782        assert_eq!(aggregate.jobs, 2);
8783        assert_eq!(aggregate.successes, 1);
8784        assert_eq!(aggregate.failures, 1);
8785        assert_eq!(aggregate.total_duration_ns, 130);
8786        assert_eq!(aggregate.maximum_duration_ns, 123);
8787        assert_eq!(aggregate.total_stdout_bytes, 55);
8788        assert_eq!(aggregate.total_stderr_bytes, 6);
8789        assert_eq!(aggregate.process_group_contained_jobs, 2);
8790        assert_eq!(aggregate.memory_limited_jobs, 1);
8791        assert_eq!(aggregate.operation_counts["discover"], 1);
8792        assert_eq!(aggregate.operation_counts["verify_v2"], 1);
8793        assert_eq!(aggregate.failure_counts["exit_status"], 1);
8794        assert_eq!(
8795            InvocationMetricsAggregate::csv_header(),
8796            "schema_version,jobs,successes,failures,total_duration_ns,maximum_duration_ns,total_stdout_bytes,total_stderr_bytes,process_group_contained_jobs,memory_limited_jobs,operation_counts,failure_counts"
8797        );
8798        assert_eq!(
8799            aggregate.to_csv_row(),
8800            "1,2,1,1,130,123,55,6,2,1,discover=1;verify_v2=1,exit_status=1"
8801        );
8802
8803        let mut incompatible = successful.clone();
8804        incompatible.schema_version = 2;
8805        assert_eq!(
8806            aggregate_invocation_metrics([&incompatible]),
8807            Err(MetricsAggregationError::UnsupportedSchema(2))
8808        );
8809        assert_eq!(
8810            aggregate_invocation_metrics(std::iter::empty()),
8811            Err(MetricsAggregationError::Empty)
8812        );
8813    }
8814
8815    #[cfg(unix)]
8816    #[test]
8817    fn bounded_runner_reports_deadline_and_output_classes() {
8818        let mut delayed = Command::new("sh");
8819        delayed.arg("-c").arg("sleep 5 & wait");
8820        let started = Instant::now();
8821        let failure = run_bounded(
8822            OperationKind::VerifyV2,
8823            delayed,
8824            ExecutionPolicy::new(Duration::from_millis(10), 1024).unwrap(),
8825        )
8826        .unwrap_err();
8827        assert!(matches!(*failure.error, PredicateApiError::TimedOut { .. }));
8828        assert_eq!(
8829            failure.metrics.status,
8830            InvocationStatus::Failed(FailureClass::Timeout)
8831        );
8832        assert!(started.elapsed() < Duration::from_secs(1));
8833
8834        let mut verbose = Command::new("printf");
8835        verbose.arg("0123456789");
8836        let failure = run_bounded(
8837            OperationKind::CertifyV2,
8838            verbose,
8839            ExecutionPolicy::new(Duration::from_secs(1), 4).unwrap(),
8840        )
8841        .unwrap_err();
8842        assert!(matches!(
8843            *failure.error,
8844            PredicateApiError::OutputLimitExceeded {
8845                stream: "stdout",
8846                limit_bytes: 4,
8847            }
8848        ));
8849        assert_eq!(failure.metrics.operation, OperationKind::CertifyV2);
8850        assert_eq!(failure.metrics.stdout_bytes, 5);
8851        assert_eq!(
8852            failure.metrics.status,
8853            InvocationStatus::Failed(FailureClass::OutputLimit)
8854        );
8855    }
8856
8857    #[cfg(all(unix, not(target_os = "macos")))]
8858    #[test]
8859    fn bounded_runner_applies_address_space_ceiling() {
8860        let bytes = 128 * 1024 * 1024;
8861        let policy = ExecutionPolicy::new(Duration::from_secs(1), 1024)
8862            .unwrap()
8863            .with_memory_limit(bytes)
8864            .unwrap();
8865        let mut command = Command::new("sh");
8866        command.arg("-c").arg("ulimit -v");
8867        let output = run_bounded(OperationKind::Discover, command, policy).unwrap();
8868        let reported_kib = String::from_utf8(output.stdout)
8869            .unwrap()
8870            .trim()
8871            .parse::<u64>()
8872            .unwrap();
8873        assert!(reported_kib > 0);
8874        assert!(reported_kib <= bytes / 1024);
8875        assert_eq!(output.metrics.memory_limit_bytes, Some(bytes));
8876    }
8877}