Skip to main content

provable_contracts/schema/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4pub use super::clause::Clause;
5pub use super::composition::{ShapeContract, ShapeExpr};
6pub use super::kaizen::{KaizenRecord, KAIZEN_STATUSES};
7pub use super::kind::ContractKind;
8
9/// A complete YAML kernel contract.
10///
11/// This is the root type for the contract schema defined in
12/// `docs/specifications/pv-spec.md` Section 3.
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct Contract {
15    pub metadata: Metadata,
16    /// Equations are optional — kaizen, pipeline, and registry contracts
17    /// may define only `proof_obligations` without mathematical equations.
18    ///
19    /// Accepts both map form (`equations: { silu: { formula: ... } }`, the
20    /// canonical schema) and sequence form (`equations: [{ id: silu,
21    /// formula: ... }]`, used by several diagnostic/methodology contracts
22    /// predating APR-MONO). The sequence form promotes each item's `id`
23    /// field to the map key.
24    #[serde(default, deserialize_with = "deserialize_equations")]
25    pub equations: BTreeMap<String, Equation>,
26    #[serde(default)]
27    pub proof_obligations: Vec<ProofObligation>,
28    #[serde(default)]
29    pub kernel_structure: Option<KernelStructure>,
30    #[serde(default)]
31    pub simd_dispatch: BTreeMap<String, BTreeMap<String, String>>,
32    #[serde(default)]
33    pub enforcement: BTreeMap<String, EnforcementRule>,
34    #[serde(default)]
35    pub falsification_tests: Vec<FalsificationTest>,
36    #[serde(default)]
37    pub kani_harnesses: Vec<KaniHarness>,
38    #[serde(default)]
39    pub qa_gate: Option<QaGate>,
40    /// Phase 7: Lean 4 verification summary across all obligations.
41    #[serde(default)]
42    pub verification_summary: Option<VerificationSummary>,
43    /// Type-level invariants (Meyer's class invariants).
44    #[serde(default)]
45    pub type_invariants: Vec<TypeInvariant>,
46    /// ONT-4e: preconditions, `{id, statement, formal, formal_status}` (see [`Clause`]).
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub requires: Vec<Clause>,
49    /// ONT-4e: postconditions, the same shape as `requires`.
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub ensures: Vec<Clause>,
52    /// Coq verification specification.
53    #[serde(default)]
54    pub coq_spec: Option<CoqSpec>,
55    /// BEAT-benchmark parameters (PMAT-741) — present on `metadata.kind:
56    /// beat-benchmark` contracts; pins a machine-measured incumbent baseline so
57    /// CI fails when aprender regresses below it on the incumbent's canonical task.
58    #[serde(default)]
59    pub beat: Option<Beat>,
60    /// CRUX master-registry story rows (`contracts/crux-competitive-research-ux-v1.yaml`).
61    ///
62    /// THIS is the list the competitive-research programme actually sorts by.
63    /// aprender#2555 originally range-checked only `metadata.demand_score` and
64    /// justified it as "the ranking signal the whole programme sorts by" — but
65    /// MEASURED, nothing in the repo reads `metadata.demand_score`; the 250
66    /// rows below are what §12.1 of
67    /// `docs/specifications/crux-competitive-research-ux-workflows.md` maps to
68    /// `pmat work` priority. They were entirely ungated. Validating them is
69    /// what makes that justification true.
70    #[serde(default)]
71    pub stories: Vec<CruxStory>,
72    /// Legacy free-form top-level `falsification:` block.
73    ///
74    /// 400 contracts in `contracts/` carry this key, every one of them holding
75    /// a structured list (shapes seen in the wild: `{condition, action,
76    /// severity}`, `{name, description, check}`, `{id, assertion,
77    /// test_harness}`). `Contract` is not `deny_unknown_fields`, so before this
78    /// field existed serde dropped all of it silently — the same mechanism as
79    /// #2465 (`test_harness`) and #2504. `contracts/publish-workspace-v1.yaml`
80    /// is the canonical victim: four FALSIFY-PUB-* entries live here and `pv
81    /// status` reported "Falsification tests: 0" while the file read as
82    /// governance.
83    ///
84    /// It is deliberately `serde_yaml::Value`: the block is NOT
85    /// `falsification_tests` and must never be counted as one — it is captured
86    /// so that tooling can SEE it and report the contract as inert. Migrating
87    /// these entries into real `falsification_tests` is contract-by-contract
88    /// work, not a schema change.
89    #[serde(default)]
90    pub falsification: Option<serde_yaml::Value>,
91    /// Legacy free-form top-level `falsification_conditions:` block — the same
92    /// silent-drop class as [`Contract::falsification`], used by 12 contracts.
93    /// Kept as a distinct field (not a serde `alias`) so a contract carrying
94    /// both keys still parses instead of failing on a duplicate field.
95    #[serde(default)]
96    pub falsification_conditions: Option<serde_yaml::Value>,
97    /// Top-level YAML keys that are not fields of `Contract`, captured verbatim
98    /// by [`crate::schema::parse_contract_str`].
99    ///
100    /// The schema deliberately tolerates unknown top-level keys — model-family,
101    /// spec and registry YAMLs carry downstream-owned blocks (see
102    /// `parse_contract_with_kind_model_family`), and 1224 of the 1726 contracts
103    /// `pv lint` walks have at least one. `deny_unknown_fields` is therefore not
104    /// an option. Instead the validator uses this list to reject the two shapes
105    /// that are never legitimate: a top-level `kind:` (SCHEMA-018) and a
106    /// near-miss misspelling of a real block name (SCHEMA-019).
107    ///
108    /// Not serialized: it is a parse artifact, not contract content.
109    #[serde(skip)]
110    pub unknown_top_level_keys: Vec<String>,
111    /// Was `metadata.kind:` WRITTEN in the source YAML? (ONT-6b, infra#751)
112    ///
113    /// [`ContractKind`] derives `Default = Kernel` and `metadata.kind` carries
114    /// `#[serde(default)]`, so by the time anything can read [`Contract::kind`]
115    /// the difference between *declared kernel* and *defaulted to kernel* is
116    /// gone. 666 of this corpus's contracts declare no kind at all and are
117    /// judged by the kernel rules because of that default; until ONT-6b nothing
118    /// in `pv validate`'s output said so, and a kind-less file and the same
119    /// bytes with `kind: kernel` printed identically (measured 2026-09-19,
120    /// pv 0.68.1). The validator uses this to decorate the FIRST kernel-only
121    /// error the default caused — see `schema::validator::validate_contract`.
122    ///
123    /// Not serialized: like `unknown_top_level_keys`, it is a parse artifact.
124    /// `false` is the safe default for a `Contract` built in code rather than
125    /// parsed: it only ever adds an explanation to an error that already fired.
126    #[serde(skip)]
127    pub kind_declared: bool,
128    /// The kaizen-record blocks (`contract:`, `kaizen:`, `baseline:`,
129    /// `target:`, …) captured by a second parse pass when — and only when —
130    /// `metadata.kind` is `kaizen`.
131    ///
132    /// Kept OUT of the serde surface of `Contract` on purpose. The corpus
133    /// carries `status:`, `version:`, `invariants:` and `files:` at top level
134    /// on documents of several kinds with incompatible shapes, so promoting
135    /// them to real `Contract` fields would change how all 1726 contracts
136    /// parse in order to validate 46 kaizen records. Scoping the second pass
137    /// to `kind: kaizen` means a type mismatch in some unrelated contract's
138    /// `status:` can never reach this struct.
139    ///
140    /// Not serialized: it is a parse artifact, not contract content.
141    #[serde(skip)]
142    pub kaizen_record: Option<KaizenRecord>,
143    /// The error a strict YAML reader produced on a document this schema
144    /// nonetheless accepted, captured by
145    /// [`crate::schema::parse_contract_str`]. `None` is the healthy case.
146    ///
147    /// The derived deserializer skips unknown subtrees without reading them, so
148    /// a contract can parse cleanly here and be rejected by `yq`, PyYAML, or a
149    /// `serde_yaml::Value` round-trip. SCHEMA-020 turns that divergence into an
150    /// error instead of leaving it to be discovered downstream.
151    ///
152    /// Not serialized: it is a parse artifact, not contract content.
153    #[serde(skip)]
154    pub strict_yaml_error: Option<String>,
155}
156
157/// One row of the CRUX master registry's `stories:` list.
158///
159/// Fields beyond the three domain-checked ones are accepted and ignored — the
160/// registry carries `title`/`contract`/`category` that no rule constrains.
161#[derive(Debug, Clone, Default, Serialize, Deserialize)]
162pub struct CruxStory {
163    /// Story id, e.g. `CRUX-A-01`. Used only to locate a violation.
164    #[serde(default)]
165    pub id: String,
166    /// Which competitor's UX the story was extracted from. Membership-checked
167    /// against `CRUX_COMPETITORS` (rule CRUX-002), the same registry that
168    /// governs `metadata.competitor`, and trimmed on parse for the same reason.
169    #[serde(default, deserialize_with = "deserialize_trimmed_opt_string")]
170    pub competitor: Option<String>,
171    /// Demand, documented `1..=5`. Range-checked by rule CRUX-001 — the same
172    /// `DEMAND_SCORE_RANGE` that governs `metadata.demand_score`.
173    ///
174    /// `i64` for the same reason as [`Metadata::demand_score`]: an out-of-range
175    /// value must REACH the validator and be named, not die in serde.
176    #[serde(default)]
177    pub demand_score: Option<i64>,
178    /// Story status. A closed enum, so an invented value FAILS TO PARSE — the
179    /// registry is held to exactly the vocabulary `IntakeStatus` defines.
180    #[serde(default)]
181    pub status: Option<IntakeStatus>,
182}
183
184/// Every top-level key `Contract` deserializes, in declaration order.
185///
186/// This list is the allow-list SCHEMA-019 checks near-misses against, and it is
187/// pinned to the struct by `contract_fields_match_struct` in `types_tests.rs`:
188/// adding a field to `Contract` without adding it here turns the new block into
189/// a "near-miss of itself" and fails that test.
190pub const CONTRACT_TOP_LEVEL_FIELDS: [&str; 18] = [
191    "metadata",
192    "equations",
193    "proof_obligations",
194    "kernel_structure",
195    "simd_dispatch",
196    "enforcement",
197    "falsification_tests",
198    "kani_harnesses",
199    "qa_gate",
200    "verification_summary",
201    "type_invariants",
202    "requires",
203    "ensures",
204    "coq_spec",
205    "beat",
206    "stories",
207    "falsification",
208    "falsification_conditions",
209];
210
211/// Parameters of a head-to-head BEAT benchmark (`metadata.kind: beat-benchmark`,
212/// PMAT-741): a falsifiable, CI-wired claim that aprender meets-or-beats an
213/// incumbent (scikit-learn / PyTorch / Unsloth / Ollama·llama.cpp) on the
214/// incumbent's own canonical task — the measurement backbone of the four-pillar
215/// "replace AND beat" mission. Required-shape is enforced by
216/// `validate_beat_benchmark` in the validator (BEAT-001..007).
217#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218pub struct Beat {
219    /// Which pillar (1=sklearn, 2=PyTorch, 3=Unsloth, 4=Ollama/llama.cpp).
220    #[serde(default)]
221    pub pillar: Option<u8>,
222    /// The incumbent being beaten — must name one of the four pillars.
223    #[serde(default)]
224    pub incumbent: String,
225    /// How/when the baseline was pinned (free-form provenance).
226    #[serde(default)]
227    pub incumbent_pinned: Option<String>,
228    /// The canonical task on which the beat is measured (apples-to-apples).
229    #[serde(default)]
230    pub canonical_task: Option<String>,
231    /// The measured metric (e.g. `accuracy`, `wall_clock_ms`, `tokens_per_sec`, `mse`).
232    #[serde(default)]
233    pub metric: String,
234    /// `higher_is_better` or `lower_is_better` — fixes the regression direction.
235    #[serde(default)]
236    pub direction: String,
237    /// The incumbent's pinned baseline value.
238    #[serde(default)]
239    pub baseline_value: Option<f64>,
240    /// Optional worst-case incumbent value (e.g. sklearn min over seeds).
241    #[serde(default)]
242    pub baseline_floor: Option<f64>,
243    /// The threshold aprender must meet/beat; CI fails on regression past it.
244    #[serde(default)]
245    pub beat_threshold: Option<f64>,
246    /// When the baseline was sourced (ISO date).
247    #[serde(default)]
248    pub baseline_sourced_date: Option<String>,
249    /// `CPU` or `GPU` — the compute approved for this gate.
250    #[serde(default)]
251    pub approved_compute: Option<String>,
252    /// The CI test/gate name that enforces this beat.
253    #[serde(default)]
254    pub ci_gate_name: String,
255}
256
257/// The outcome of evaluating a measured value against a [`Beat`]'s pinned
258/// threshold — the falsifiable verdict at the heart of `apr beat-run`.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(rename_all = "lowercase")]
261pub enum BeatOutcome {
262    /// aprender meets-or-beats the incumbent: measured is on the winning side of
263    /// `beat_threshold` per `direction`.
264    Won,
265    /// aprender regressed below the pinned threshold — CI must fail.
266    Regressed,
267}
268
269impl Beat {
270    /// Evaluate a measured value against this beat's pinned `beat_threshold`,
271    /// honoring `direction`:
272    /// - `higher_is_better` ⇒ `Won` iff `measured >= beat_threshold`
273    /// - `lower_is_better`  ⇒ `Won` iff `measured <= beat_threshold`
274    ///
275    /// Returns `None` when the contract is too malformed to judge (no
276    /// `beat_threshold`, a non-finite threshold/measurement, or an unknown
277    /// `direction`) — the caller should treat that as a hard error, not a pass.
278    /// The validator's BEAT-004/BEAT-005 rules reject such contracts up front,
279    /// so a well-formed contract always yields `Some`.
280    #[must_use]
281    pub fn evaluate(&self, measured: f64) -> Option<BeatOutcome> {
282        let threshold = self.beat_threshold?;
283        if !threshold.is_finite() || !measured.is_finite() {
284            return None;
285        }
286        match self.direction.trim() {
287            "higher_is_better" => Some(if measured >= threshold {
288                BeatOutcome::Won
289            } else {
290                BeatOutcome::Regressed
291            }),
292            "lower_is_better" => Some(if measured <= threshold {
293                BeatOutcome::Won
294            } else {
295                BeatOutcome::Regressed
296            }),
297            _ => None,
298        }
299    }
300
301    /// Convenience: `true` iff [`evaluate`](Self::evaluate) returns
302    /// [`BeatOutcome::Won`]. A malformed contract (`None`) is **not** a win.
303    #[must_use]
304    pub fn is_won(&self, measured: f64) -> bool {
305        self.evaluate(measured) == Some(BeatOutcome::Won)
306    }
307}
308
309impl Contract {
310    /// Back-compat: `metadata.registry: true` OR `metadata.kind: registry`.
311    pub fn is_registry(&self) -> bool {
312        self.metadata.registry || self.metadata.kind == ContractKind::Registry
313    }
314
315    /// The effective kind, honoring the legacy `registry: true` flag.
316    pub fn kind(&self) -> ContractKind {
317        if self.metadata.registry && self.metadata.kind == ContractKind::Kernel {
318            ContractKind::Registry
319        } else {
320            self.metadata.kind
321        }
322    }
323
324    /// True iff this contract must satisfy PROVABILITY-001 (kernel only).
325    pub fn requires_proofs(&self) -> bool {
326        self.kind() == ContractKind::Kernel
327    }
328
329    /// How many entries sit in the legacy top-level `falsification:` /
330    /// `falsification_conditions:` blocks — content the schema captures but
331    /// does NOT count as `falsification_tests`.
332    ///
333    /// A non-zero result together with an empty `falsification_tests` is the
334    /// inert-contract signature (#2504): the file reads as enforced and
335    /// enforces nothing. `pv status` reports it so the reader is never told
336    /// "Falsification tests: 0" without being told where the entries went.
337    #[must_use]
338    pub fn legacy_falsification_entries(&self) -> usize {
339        fn count(v: Option<&serde_yaml::Value>) -> usize {
340            match v {
341                Some(serde_yaml::Value::Sequence(s)) => s.len(),
342                Some(serde_yaml::Value::Mapping(m)) => m.len(),
343                Some(serde_yaml::Value::Null) | None => 0,
344                Some(_) => 1,
345            }
346        }
347        count(self.falsification.as_ref()) + count(self.falsification_conditions.as_ref())
348    }
349
350    /// Enforce the provability invariant: kernel contracts MUST have
351    /// `proof_obligations`, `falsification_tests`, and `kani_harnesses`.
352    /// Returns a list of violations. Empty list = contract is valid.
353    pub fn provability_violations(&self) -> Vec<String> {
354        if !self.requires_proofs() {
355            return vec![];
356        }
357        let mut violations = Vec::new();
358        if self.proof_obligations.is_empty() {
359            violations.push("Kernel contract has no proof_obligations".into());
360        }
361        if self.falsification_tests.is_empty() {
362            violations.push("Kernel contract has no falsification_tests".into());
363        }
364        if self.kani_harnesses.is_empty() {
365            violations.push("Kernel contract has no kani_harnesses".into());
366        }
367        if self.falsification_tests.len() < self.proof_obligations.len() {
368            violations.push(format!(
369                "falsification_tests ({}) < proof_obligations ({})",
370                self.falsification_tests.len(),
371                self.proof_obligations.len(),
372            ));
373        }
374        violations
375    }
376}
377
378/// Contract metadata block.
379#[derive(Debug, Clone, Default, Serialize, Deserialize)]
380pub struct Metadata {
381    pub version: String,
382    #[serde(default)]
383    pub created: Option<String>,
384    #[serde(default)]
385    pub author: Option<String>,
386    pub description: String,
387    #[serde(default)]
388    pub references: Vec<String>,
389    /// Contract dependencies — other contracts this one composes.
390    /// Values are contract stems (e.g. "silu-kernel-v1").
391    #[serde(default)]
392    pub depends_on: Vec<String>,
393    /// Legacy registry flag — prefer `metadata.kind: registry` for new contracts.
394    #[serde(default)]
395    pub registry: bool,
396    /// Contract kind. Defaults to [`ContractKind::Kernel`].
397    #[serde(default)]
398    pub kind: ContractKind,
399    /// Per-contract enforcement level (Section 17, Gap 1).
400    /// `basic` → schema valid; `standard` → + falsification + kani;
401    /// `strict` → + all bindings implemented; `proven` → + Lean 4 proved.
402    #[serde(default)]
403    pub enforcement_level: Option<EnforcementLevel>,
404    /// Once set, the contract cannot drop below this verification level
405    /// without an explicit `pv unlock` (Section 17, Gap 5).
406    #[serde(default)]
407    pub locked_level: Option<String>,
408    /// CRUX competitive-research story: which competitor's UX the story was
409    /// extracted from. Membership-checked against the `CRUX_COMPETITORS`
410    /// registry in `schema/validator.rs` (rule CRUX-002).
411    ///
412    /// NORMALISED ON PARSE (trimmed). The validator used to `.trim()` before
413    /// comparing, so `competitor: "  ecosystem  "` passed CRUX-002 while the
414    /// stored value kept its padding: the gate laundered a value it never
415    /// fixed, and every consumer reading this field still saw the untrimmed
416    /// string. Trimming here means the checked value and the stored value are
417    /// the same value.
418    #[serde(default, deserialize_with = "deserialize_trimmed_opt_string")]
419    pub competitor: Option<String>,
420    /// CRUX competitive-research story: demand, documented `1..=5` by
421    /// `contracts/crux-competitive-research-ux-v1.yaml` §"demand_score (1..5)".
422    /// Range-checked by rule CRUX-001.
423    ///
424    /// Deliberately `i64`, not `u8`: an out-of-range value must reach the
425    /// validator and be reported as `demand_score 99999 is outside 1..=5`,
426    /// not die in serde as an opaque integer-overflow message.
427    #[serde(default)]
428    pub demand_score: Option<i64>,
429    /// CRUX competitive-research story: intake status. A closed enum, so an
430    /// invented value FAILS TO PARSE (see [`IntakeStatus`]).
431    #[serde(default)]
432    pub intake_status: Option<IntakeStatus>,
433}
434
435/// Deserialize an optional string, trimming surrounding whitespace.
436///
437/// aprender#2555 follow-up: a domain check that trims before comparing accepts
438/// `"  ecosystem  "` and then stores it verbatim. Normalising at the parse
439/// boundary is the fix — it is done once, before any rule runs, so no rule has
440/// to remember to trim and none can disagree about whether it did.
441///
442/// PRESENT-BUT-EMPTY IS NOT ABSENT. A trimmed-to-empty value stays
443/// `Some(String::new())` rather than collapsing to `None`, so `competitor: ''`
444/// and `competitor: '   '` are still REPORTED by CRUX-002 as unregistered.
445/// Collapsing them would have quietly widened the presence gap this field
446/// already has: omission is invisible to the gate, and turning a written-down
447/// blank into another invisible case makes that worse, not better.
448fn deserialize_trimmed_opt_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
449where
450    D: serde::Deserializer<'de>,
451{
452    let raw: Option<String> = Option::deserialize(deserializer)?;
453    Ok(raw.map(|v| v.trim().to_string()))
454}
455
456/// Intake status of a CRUX competitive-research story (`metadata.intake_status`).
457///
458/// The vocabulary is closed and is exactly `STATUS_BADGE` in
459/// `scripts/crux_scaffold_contracts.py`, the generator that emits all 275
460/// `crux-*-v1.yaml` files: `supported`, `partial`, `missing`, `unclear`.
461///
462/// This is an ENUM rather than a `String` on purpose (aprender#2555). A field
463/// serde never parsed cannot be checked by any validator, and a field parsed as
464/// `String` can only be *linted* — a lint is advisory and the caller may ignore
465/// it. Making the type closed pushes the check into deserialization, so an
466/// invented value is not a warning about a contract, it is not a contract.
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(rename_all = "lowercase")]
469pub enum IntakeStatus {
470    /// apr has no surface for this story.
471    Missing,
472    /// apr has a partial surface; parity gaps remain.
473    Partial,
474    /// apr reaches parity with the competitor's canonical verb.
475    Supported,
476    /// The competitor's behaviour has not been pinned down yet.
477    Unclear,
478}
479
480impl std::fmt::Display for IntakeStatus {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        let s = match self {
483            Self::Missing => "missing",
484            Self::Partial => "partial",
485            Self::Supported => "supported",
486            Self::Unclear => "unclear",
487        };
488        write!(f, "{s}")
489    }
490}
491
492/// Per-contract enforcement level (gradual enforcement, Section 17).
493#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
494#[serde(rename_all = "lowercase")]
495pub enum EnforcementLevel {
496    /// Schema valid, has equations.
497    Basic,
498    /// + falsification tests + Kani harnesses.
499    Standard,
500    /// + all bindings implemented + `#[contract]` annotations.
501    Strict,
502    /// + Lean 4 proved (no sorry).
503    Proven,
504}
505
506/// A mathematical equation extracted from a paper (Phase 1 output).
507#[derive(Debug, Clone, Default, Serialize, Deserialize)]
508pub struct Equation {
509    /// Default-empty so diagnostic/methodology contracts that use prose
510    /// requirements instead of a formula (e.g.
511    /// `decode-hot-path-prefix-cache-diagnostic-v1`) still parse.
512    #[serde(default)]
513    pub formula: String,
514    #[serde(default)]
515    pub domain: Option<String>,
516    #[serde(default)]
517    pub codomain: Option<String>,
518    #[serde(default)]
519    pub invariants: Vec<String>,
520    /// Rust preconditions — compiled to `debug_assert!()` by `build.rs`.
521    #[serde(default)]
522    pub preconditions: Vec<String>,
523    /// Rust postconditions — compiled to `debug_assert!()` by `build.rs`.
524    #[serde(default)]
525    pub postconditions: Vec<String>,
526    /// Lean 4 theorem name that proves this equation correct.
527    /// Example: "ProvableContracts.Theorems.Softmax.PartitionOfUnity"
528    #[serde(default)]
529    pub lean_theorem: Option<String>,
530    /// IEEE 754 tolerance: codegen emits `>=` instead of `>` for boundaries (GH-67).
531    #[serde(default)]
532    pub float_tolerance: Option<f64>,
533    /// Compositional verification: what this equation requires from upstream.
534    /// References a guarantees block from another contract/equation.
535    #[serde(default)]
536    pub assumes: Option<ShapeContract>,
537    /// Compositional verification: what this equation provides to downstream.
538    /// Must be satisfiable by any downstream equation that assumes it.
539    #[serde(default)]
540    pub guarantees: Option<ShapeContract>,
541}
542
543/// One or more cited targets (#3347).
544///
545/// The corpus writes an obligation-to-test citation three ways and all three
546/// are authored by hand, so the type accepts all three rather than making one
547/// of them a parse error: a scalar (`FALSIFY-PM-004`), a comma-separated
548/// scalar (`apr-serve-cancellation-v1` names four in one field), and a YAML
549/// sequence (`publish-manifest-v1`, the only one today -- and the one that
550/// proved `Option<String>` was the wrong type by failing the WHOLE corpus
551/// with `invalid type: sequence, expected a string`).
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(untagged)]
554pub enum Citation {
555    /// A single field, possibly holding a comma-separated list.
556    One(String),
557    /// A YAML sequence of targets.
558    Many(Vec<String>),
559}
560
561impl Citation {
562    /// The cited targets, trimmed, with empties dropped. A comma splits a
563    /// scalar because contracts write lists both ways.
564    pub fn targets(&self) -> impl Iterator<Item = &str> {
565        let slice: &[String] = match self {
566            Self::One(s) => std::slice::from_ref(s),
567            Self::Many(v) => v.as_slice(),
568        };
569        slice
570            .iter()
571            .flat_map(|s| s.split(','))
572            .map(str::trim)
573            .filter(|t| !t.is_empty())
574    }
575}
576
577/// A proof obligation derived from an equation.
578///
579/// 26 obligation types: 19 property types plus 7 Design by Contract
580/// types (`precondition`, `postcondition`, `frame`, `loop_invariant`,
581/// `loop_variant`, `old_state`, `subcontract`).
582#[derive(Debug, Clone, Default, Serialize, Deserialize)]
583pub struct ProofObligation {
584    /// Stable identifier, e.g. `GDN-BND-001`. `Option` because 52 contracts
585    /// predate the convention and carry their own (`REG-OB-001`, `PO-HEH-001`,
586    /// `OBLIG-DATA-QUALITY-007-DEAD-OUTPUT-ROW`), and because requiring it is a
587    /// separate, enforcing change.
588    ///
589    /// Until this field existed, `id:` was written to disk and **silently
590    /// dropped on parse** -- the struct had no such field and there is no
591    /// `deny_unknown_fields`, so 3,612 ids generated by
592    /// `scripts/lib/obligation_ids.py` were decoration: no consumer could read
593    /// one. An obligation with no id cannot be cited by a kani harness, a test,
594    /// a receipt or a commit, which is the whole point of having one (#3314).
595    #[serde(default, skip_serializing_if = "Option::is_none")]
596    pub id: Option<String>,
597    /// Obligation category. Defaults to `Invariant` for legacy contracts
598    /// that predate the DbC split (e.g. `eval-harness-humaneval-v1`,
599    /// `publish-manifest-v1`) which ship with just `property:`/`formal:`.
600    #[serde(rename = "type", default)]
601    pub obligation_type: ObligationType,
602    /// Human-readable statement of what must hold. Alias `statement`
603    /// accepted for legacy diagnostic contracts (e.g.
604    /// `decode-hot-path-prefix-cache-diagnostic-v1`) whose POs predate
605    /// the canonical `property:` naming.
606    #[serde(default, alias = "statement")]
607    pub property: String,
608    /// Formal predicate (Rust/Lean syntax). Alias `verification` accepted
609    /// for legacy contracts that ship a shell/pmat-query check instead of
610    /// a formal predicate.
611    #[serde(default, alias = "verification")]
612    pub formal: Option<String>,
613    #[serde(default)]
614    pub tolerance: Option<f64>,
615    #[serde(default)]
616    pub applies_to: Option<AppliesTo>,
617    /// The falsification test(s) that discharge this obligation (#3347) --
618    /// the same link as `FalsificationTest::obligation`, written from the
619    /// obligation's side. 89 obligations in `contracts/` carry it and it is
620    /// the most-used spelling of the link; it too was dropped on parse.
621    ///
622    /// Two resolvable shapes, both measured: `falsification_tests[N]` (62,
623    /// resolves only when `N` is in range) and a test `id` (13). The
624    /// remaining 14 are comma-separated id lists, prose, or a KANI harness id
625    /// -- a kani id is NOT an L2 link and deliberately does not resolve.
626    #[serde(default, skip_serializing_if = "Option::is_none")]
627    pub discharged_by: Option<Citation>,
628    /// Why this obligation is NOT a property of code (PMAT-3091) -- e.g. a
629    /// checkpoint fact, an `O()` with no constant, a throughput claim.
630    ///
631    /// Only meaningful with `applies_to: not_applicable`, where it is REQUIRED
632    /// (SCHEMA-021). Present on any other obligation it is decoration -- a
633    /// justification nothing declares -- and is an error (SCHEMA-023).
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub na_reason: Option<String>,
636    /// Where the claim IS verified, since a unit test cannot (PMAT-3091): a
637    /// bench, a `pv`/CI check, or an evidence command.
638    ///
639    /// Same decoration rule as `na_reason`: required with
640    /// `applies_to: not_applicable` (SCHEMA-022), an error without it
641    /// (SCHEMA-023).
642    #[serde(default, skip_serializing_if = "Option::is_none")]
643    pub na_owner: Option<String>,
644    /// Phase 7: Lean 4 theorem proving metadata.
645    #[serde(default)]
646    pub lean: Option<LeanProof>,
647    /// Postcondition only: links to a precondition obligation ID.
648    #[serde(default)]
649    pub requires: Option<String>,
650    /// Loop invariant/variant only: references a `kernel_structure.phases[]` name.
651    #[serde(default)]
652    pub applies_to_phase: Option<String>,
653    /// Subcontract only: contract stem being refined (must be in `metadata.depends_on`).
654    #[serde(default)]
655    pub parent_contract: Option<String>,
656}
657
658#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "lowercase")]
660pub enum ObligationType {
661    #[default]
662    Invariant,
663    Equivalence,
664    Bound,
665    Monotonicity,
666    Idempotency,
667    Linearity,
668    Symmetry,
669    Associativity,
670    Conservation,
671    Ordering,
672    Completeness,
673    Soundness,
674    Involution,
675    Determinism,
676    Roundtrip,
677    #[serde(rename = "state_machine")]
678    StateMachine,
679    Classification,
680    Independence,
681    Termination,
682    /// Memory/IO safety obligation (bounds checks, non-null, etc.). Legacy
683    /// pre-APR-MONO contracts (e.g. `apr-cli-publish-extra-v1`) used this
684    /// spelling; kept for back-compat alongside the 26 other types.
685    Safety,
686    /// Liveness property (eventually-happens). Same legacy contract
687    /// (`apr-cli-publish-extra-v1`) uses this for progress obligations;
688    /// kept for back-compat.
689    Liveness,
690    // Eiffel DbC types (Meyer 1997)
691    Precondition,
692    Postcondition,
693    Frame,
694    #[serde(rename = "loop_invariant")]
695    LoopInvariant,
696    #[serde(rename = "loop_variant")]
697    LoopVariant,
698    #[serde(rename = "old_state")]
699    OldState,
700    Subcontract,
701}
702
703impl std::fmt::Display for ObligationType {
704    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
705        let s = match self {
706            Self::Invariant => "invariant",
707            Self::Equivalence => "equivalence",
708            Self::Bound => "bound",
709            Self::Monotonicity => "monotonicity",
710            Self::Idempotency => "idempotency",
711            Self::Linearity => "linearity",
712            Self::Symmetry => "symmetry",
713            Self::Associativity => "associativity",
714            Self::Conservation => "conservation",
715            Self::Ordering => "ordering",
716            Self::Completeness => "completeness",
717            Self::Soundness => "soundness",
718            Self::Involution => "involution",
719            Self::Determinism => "determinism",
720            Self::Roundtrip => "roundtrip",
721            Self::StateMachine => "state_machine",
722            Self::Classification => "classification",
723            Self::Independence => "independence",
724            Self::Termination => "termination",
725            Self::Safety => "safety",
726            Self::Liveness => "liveness",
727            Self::Precondition => "precondition",
728            Self::Postcondition => "postcondition",
729            Self::Frame => "frame",
730            Self::LoopInvariant => "loop_invariant",
731            Self::LoopVariant => "loop_variant",
732            Self::OldState => "old_state",
733            Self::Subcontract => "subcontract",
734        };
735        write!(f, "{s}")
736    }
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740#[serde(rename_all = "lowercase")]
741pub enum AppliesTo {
742    All,
743    Scalar,
744    Simd,
745    Converter,
746    /// Not a property of code, so not applicable to unit tests (PMAT-3091).
747    /// Requires `na_reason` and `na_owner` on the obligation. `N/A` is accepted
748    /// as an alias; it is matched as a named variant BEFORE the `#[serde(other)]`
749    /// catch-all, so it can never parse as an algorithm target named "N/A".
750    /// Always serialized as `not_applicable`.
751    #[serde(rename = "not_applicable", alias = "N/A")]
752    NotApplicable,
753    /// Algorithm-specific target (e.g., "degree", "bce", "huber").
754    #[serde(other)]
755    Other,
756}
757
758impl ProofObligation {
759    /// `true` when the obligation is declared `applies_to: not_applicable`.
760    #[must_use]
761    pub fn is_not_applicable(&self) -> bool {
762        self.applies_to == Some(AppliesTo::NotApplicable)
763    }
764}
765
766/// Kernel phase decomposition.
767#[derive(Debug, Clone, Serialize, Deserialize)]
768pub struct KernelStructure {
769    pub phases: Vec<KernelPhase>,
770}
771
772#[derive(Debug, Clone, Serialize, Deserialize)]
773pub struct KernelPhase {
774    pub name: String,
775    pub description: String,
776    #[serde(default)]
777    pub invariant: Option<String>,
778}
779
780/// An enforcement rule from the contract.
781#[derive(Debug, Clone, Serialize, Deserialize)]
782pub struct EnforcementRule {
783    pub description: String,
784    #[serde(default)]
785    pub check: Option<String>,
786    #[serde(default)]
787    pub severity: Option<String>,
788    #[serde(default)]
789    pub reference: Option<String>,
790}
791
792/// A Popperian falsification test.
793///
794/// Each makes a falsifiable prediction about the implementation.
795/// If the prediction is wrong, the test identifies root cause.
796#[derive(Debug, Clone, Default, Serialize, Deserialize)]
797pub struct FalsificationTest {
798    pub id: String,
799    /// What the test asserts. Alias `description` accepted for legacy
800    /// pre-APR-MONO contracts that used the `description:` field name.
801    /// `name:` is NOT aliased because several legacy contracts (e.g.
802    /// `publish-manifest-v1`) ship both `name:` (a slug) and
803    /// `description:` (prose) side-by-side; aliasing both collapses to
804    /// a `duplicate field` error.
805    #[serde(default, alias = "description")]
806    pub rule: String,
807    /// The predicted outcome if the rule holds. Alias `expected` accepted
808    /// for legacy contracts (e.g. `expected: exit 0`, `expected: "PASS"`).
809    /// Defaulted because diagnostic contracts often encode prediction
810    /// inside the rule text alone.
811    #[serde(default, alias = "expected")]
812    pub prediction: String,
813    /// How to run the test. Alias `command` accepted for legacy contracts
814    /// (e.g. shell snippets under `command: |`).
815    #[serde(default, alias = "command")]
816    pub test: Option<String>,
817    /// How to run the test, in the `test_harness:` spelling. 619 entries in
818    /// `contracts/` use this field INSTEAD of `test:` — 94 of them holding a
819    /// real `cargo test` invocation, the rest a shell harness (`grep -q …`,
820    /// `test -f …`, `bash …`).
821    ///
822    /// #2465: this field did not exist on the struct, and `FalsificationTest`
823    /// is not `deny_unknown_fields`, so serde dropped it silently. Every one
824    /// of those 619 entries reached `strict_test_binding` with `test: None`
825    /// and was skipped — the gate reported them as neither bound nor broken.
826    #[serde(default)]
827    pub test_harness: Option<String>,
828    /// The bare test-fn name, when the contract names it here rather than in
829    /// an invocation. Deliberately NOT a serde `alias` of `rule`: several
830    /// legacy contracts (e.g. `publish-manifest-v1`) ship `name:` (a slug)
831    /// and `description:` (prose) side by side, and aliasing both onto one
832    /// field collapses to a `duplicate field` parse error. Consumed as a
833    /// binding source of last resort — see `strict_test_binding`.
834    #[serde(default)]
835    pub name: Option<String>,
836    /// What failure means. Alias `fails_if` accepted for legacy contracts.
837    /// Defaulted because several legacy diagnostic contracts omit it.
838    #[serde(default, alias = "fails_if")]
839    pub if_fails: String,
840    /// The obligation this test discharges — the only machine-readable claim
841    /// that THIS test proves THAT obligation (#3347).
842    ///
843    /// Until this field existed the key was written to disk and silently
844    /// dropped on parse (the same shape as `id` in #3314 and `test_harness`
845    /// in #2465: no `deny_unknown_fields`, so serde discarded it), which is
846    /// why `obligation_matrix` had nothing to read and fell back to comparing
847    /// an INDEX against `falsification_tests.len()`.
848    ///
849    /// Measured over `contracts/` (1,842 files, 4,691 falsification tests):
850    /// 26 entries spell it `obligation:` and 38 spell it `binds_to:`. No entry
851    /// carries BOTH — checked, and it matters, because serde collapses an
852    /// alias pair present on one mapping into a `duplicate field` parse error.
853    ///
854    /// Resolved against the obligation's `id`, then its exact `property` text.
855    /// A comma-separated list cites several obligations.
856    #[serde(default, alias = "binds_to", skip_serializing_if = "Option::is_none")]
857    pub obligation: Option<Citation>,
858}
859
860/// A Kani bounded model checking harness definition.
861///
862/// Corresponds to Phase 6 (Verify) of the pipeline.
863#[derive(Debug, Clone, Default, Serialize, Deserialize)]
864pub struct KaniHarness {
865    pub id: String,
866    pub obligation: String,
867    #[serde(default)]
868    pub property: Option<String>,
869    #[serde(default)]
870    pub bound: Option<u32>,
871    #[serde(default)]
872    pub strategy: Option<KaniStrategy>,
873    #[serde(default)]
874    pub solver: Option<String>,
875    #[serde(default)]
876    pub harness: Option<String>,
877    /// GH-1595: When `true`, the harness has been verified by a green
878    /// `cargo kani` run in CI (e.g. apr-cookbook `kani-gate`). Lifts the
879    /// D3 strategy weight to 1.0 for non-exhaustive strategies because
880    /// the runtime witness supplants the static-readiness signal.
881    #[serde(default)]
882    pub actually_verified: Option<bool>,
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
886#[serde(rename_all = "snake_case")]
887pub enum KaniStrategy {
888    Exhaustive,
889    StubFloat,
890    Compositional,
891    BoundedInt,
892    /// Symbolic IEEE-754 `f32` over a bounded window, each element assumed
893    /// `is_finite()` and within a magnitude bound. Transcendentals are NOT
894    /// stubbed: the proof discharges the real float arithmetic. Contrast
895    /// `stub_float`, which axiomatizes exp/log/sin/cos (#2530).
896    BoundedFloat,
897}
898
899impl std::fmt::Display for KaniStrategy {
900    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
901        let s = match self {
902            Self::Exhaustive => "exhaustive",
903            Self::StubFloat => "stub_float",
904            Self::Compositional => "compositional",
905            Self::BoundedInt => "bounded_int",
906            Self::BoundedFloat => "bounded_float",
907        };
908        write!(f, "{s}")
909    }
910}
911
912/// Phase 7: Lean 4 theorem proving metadata for a proof obligation.
913#[derive(Debug, Clone, Serialize, Deserialize)]
914pub struct LeanProof {
915    /// Lean 4 theorem name (e.g., `Softmax.partition_of_unity`).
916    pub theorem: String,
917    /// Lean 4 module path (e.g., `ProvableContracts.Softmax`).
918    #[serde(default)]
919    pub module: Option<String>,
920    /// Current status of the Lean proof.
921    #[serde(default)]
922    pub status: LeanStatus,
923    /// Lean-level theorem dependencies.
924    #[serde(default)]
925    pub depends_on: Vec<String>,
926    /// Mathlib import paths required.
927    #[serde(default)]
928    pub mathlib_imports: Vec<String>,
929    /// Free-form notes (e.g., "Proof over reals; f32 gap addressed separately").
930    #[serde(default)]
931    pub notes: Option<String>,
932}
933
934/// Status of a Lean 4 proof.
935#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
936#[serde(rename_all = "kebab-case")]
937pub enum LeanStatus {
938    /// Proof is complete and type-checks.
939    Proved,
940    /// Proof uses `sorry` (axiomatized, not yet proved).
941    #[default]
942    Sorry,
943    /// Work in progress.
944    Wip,
945    /// Obligation is not amenable to Lean proof (e.g., performance).
946    NotApplicable,
947}
948
949impl std::fmt::Display for LeanStatus {
950    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
951        let s = match self {
952            Self::Proved => "proved",
953            Self::Sorry => "sorry",
954            Self::Wip => "wip",
955            Self::NotApplicable => "not-applicable",
956        };
957        write!(f, "{s}")
958    }
959}
960
961/// Phase 7: Verification summary across all obligations in a contract.
962#[derive(Debug, Clone, Serialize, Deserialize)]
963pub struct VerificationSummary {
964    pub total_obligations: u32,
965    #[serde(default)]
966    pub l2_property_tested: u32,
967    #[serde(default)]
968    pub l3_kani_proved: u32,
969    #[serde(default)]
970    pub l4_lean_proved: u32,
971    #[serde(default)]
972    pub l4_sorry_count: u32,
973    #[serde(default)]
974    pub l4_not_applicable: u32,
975}
976
977/// QA gate definition for certeza integration.
978///
979/// Legacy diagnostic contracts (e.g.
980/// `decode-hot-path-prefix-cache-diagnostic-v1`) ship a `qa_gate:` block
981/// with only `must_pass:` / `integration:` / `regression_protection:` — no
982/// `id:` or `name:`. All schema fields default so those parse cleanly.
983#[derive(Debug, Clone, Default, Serialize, Deserialize)]
984pub struct QaGate {
985    #[serde(default)]
986    pub id: String,
987    #[serde(default)]
988    pub name: String,
989    #[serde(default)]
990    pub description: Option<String>,
991    #[serde(default)]
992    pub checks: Vec<String>,
993    #[serde(default)]
994    pub pass_criteria: Option<String>,
995    #[serde(default)]
996    pub falsification: Option<String>,
997}
998
999/// A type-level invariant (Meyer's class invariant).
1000///
1001/// Asserts a predicate that must hold for every instance of `type_name`
1002/// at every stable state — after construction and after every public method.
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub struct TypeInvariant {
1005    pub name: String,
1006    /// Rust type name (e.g., `ValidatedTensor`).
1007    #[serde(rename = "type")]
1008    pub type_name: String,
1009    /// Rust boolean expression over `self` (e.g., `!self.dims.is_empty()`).
1010    pub predicate: String,
1011    #[serde(default)]
1012    pub description: Option<String>,
1013}
1014
1015/// Coq verification specification for a contract.
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1017pub struct CoqSpec {
1018    /// Coq module name (e.g., `SoftmaxSpec`).
1019    pub module: String,
1020    /// Coq import statements.
1021    #[serde(default)]
1022    pub imports: Vec<String>,
1023    /// Coq definitions generated from equations.
1024    #[serde(default)]
1025    pub definitions: Vec<CoqDefinition>,
1026    /// Links from proof obligations to Coq lemmas.
1027    #[serde(default)]
1028    pub obligations: Vec<CoqObligation>,
1029}
1030
1031/// A Coq definition derived from a contract equation.
1032#[derive(Debug, Clone, Serialize, Deserialize)]
1033pub struct CoqDefinition {
1034    pub name: String,
1035    pub statement: String,
1036}
1037
1038/// A link between a proof obligation and a Coq lemma.
1039#[derive(Debug, Clone, Serialize, Deserialize)]
1040pub struct CoqObligation {
1041    /// References a proof obligation property or ID.
1042    pub links_to: String,
1043    /// Coq lemma name.
1044    pub coq_lemma: String,
1045    /// Current status of the Coq proof.
1046    #[serde(default = "coq_status_default")]
1047    pub status: String,
1048}
1049
1050fn coq_status_default() -> String {
1051    "stub".to_string()
1052}
1053
1054/// Accepts `equations:` as either a map (canonical) or a list-of-dicts
1055/// with an `id` field (legacy pre-APR-MONO diagnostic contracts like
1056/// `decode-hot-path-prefix-cache-diagnostic-v1`). The list form promotes
1057/// each entry's `id` to the map key; entries without `id` fall back to
1058/// `equation_{N}` so parsing never silently drops data.
1059fn deserialize_equations<'de, D>(d: D) -> Result<BTreeMap<String, Equation>, D::Error>
1060where
1061    D: serde::Deserializer<'de>,
1062{
1063    use serde::de::Error;
1064    use serde_yaml::Value;
1065
1066    let value = Value::deserialize(d)?;
1067    match value {
1068        Value::Null => Ok(BTreeMap::new()),
1069        Value::Mapping(_) => serde_yaml::from_value(value).map_err(D::Error::custom),
1070        Value::Sequence(items) => {
1071            let mut out = BTreeMap::new();
1072            for (i, mut item) in items.into_iter().enumerate() {
1073                let key = match &mut item {
1074                    Value::Mapping(m) => m
1075                        .remove(Value::String("id".into()))
1076                        .and_then(|v| v.as_str().map(ToString::to_string))
1077                        .unwrap_or_else(|| format!("equation_{i}")),
1078                    _ => format!("equation_{i}"),
1079                };
1080                let eq: Equation = serde_yaml::from_value(item).map_err(D::Error::custom)?;
1081                out.insert(key, eq);
1082            }
1083            Ok(out)
1084        }
1085        other => Err(D::Error::custom(format!(
1086            "`equations:` must be a map or a list; got {other:?}"
1087        ))),
1088    }
1089}
1090
1091#[cfg(test)]
1092#[path = "types_tests.rs"]
1093mod tests;