Skip to main content

provable_contracts/schema/
types.rs

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