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