Skip to main content

provable_contracts/schema/
kaizen.rs

1//! Kaizen improvement records (`metadata.kind: kaizen`).
2//!
3//! # Why this kind exists
4//!
5//! 46 files under `contracts/{entrenar,trueno}/kaizen/` record a specific
6//! removal of waste: a ticket, a status, and — for 17 of them — a `baseline:`
7//! → `target:` pair of measured numbers. None of them carried a `metadata:`
8//! block, so `pv validate` rejected every one with ``missing field
9//! `metadata` `` and the corpus sweep counted 51 unvalidatable files. Bolting
10//! `metadata:` on without a kind would have been worse than the parse error:
11//! the contract kind defaults to [`ContractKind::Kernel`], so each record
12//! would then have been measured against PROVABILITY-001 and told to grow
13//! Kani harnesses it has no business having.
14//!
15//! A kaizen record is not a theorem, it is a MEASUREMENT — and a measurement
16//! is falsifiable in its own way. This module states how.
17//!
18//! # What is enforced, and why each rule survived the corpus
19//!
20//! Every rule below was checked against all 46 records before it shipped; the
21//! two rules that did NOT survive that check are recorded here because their
22//! absence is load-bearing:
23//!
24//! * **"all shared numeric keys must decrease"** — FALSIFIED by
25//!   `gpu-workspace-clip-v1.yaml`, whose whole point is that restoring
26//!   per-block gradient clipping RAISES `d2h_per_block` from 0 to 9. A
27//!   kaizen may legitimately buy a win with a cost.
28//! * **"all shared numeric keys must move in ONE direction"** — FALSIFIED by
29//!   `gradient-accumulation-canary-v1.yaml`, which lowers `batch_size` 4 → 1
30//!   *and* raises `gradient_accumulation_steps` 1 → 4 to hold
31//!   `effective_batch` constant. That is one improvement expressed as two
32//!   opposite movements.
33//!
34//! What IS true of every record, and is therefore enforced: a kaizen record
35//! must claim a movement (KAIZEN-005), and it may not raise every cost it
36//! measures while lowering none (KAIZEN-006) — which is exactly the shape a
37//! record takes when its `baseline:` and `target:` have been written the
38//! wrong way round.
39
40use serde::{Deserialize, Serialize};
41use serde_yaml::{Mapping, Value};
42
43use crate::error::{Severity, Violation};
44
45/// The closed set of `status:` values a kaizen record may declare
46/// (rule KAIZEN-002).
47///
48/// This is the corpus vocabulary, EXACTLY: measured over the 46 records in
49/// `contracts/{entrenar,trueno}/kaizen/` on 2026-09-03 — `implemented` (33),
50/// `pending` (6), `draft` (3), `planned` (3), `implementing` (1). Nothing
51/// speculative is admitted, for the same reason `CRUX_COMPETITORS` admits
52/// nothing speculative: an open domain is what lets a typo validate. Adding a
53/// status is a deliberate one-line edit here plus a case in
54/// `kaizen_status_vocabulary_is_the_measured_corpus`.
55pub const KAIZEN_STATUSES: [&str; 5] =
56    ["draft", "implemented", "implementing", "pending", "planned"];
57
58/// Name fragments that mark a metric as a COST — a quantity a kaizen record
59/// exists to reduce (rule KAIZEN-006).
60///
61/// Matched case-insensitively as substrings of the metric key. Every fragment
62/// is drawn from a key that actually appears in the corpus
63/// (`alloc_size_bytes`, `per_epoch_heap_churn_bytes`, `per_forward_overhead_us`,
64/// `syncs_per_step_36_blocks`, `kernel_launches_per_forward`,
65/// `wasted_alloc_per_step_bytes`, `total_launch_overhead_ms`, `heap_allocs`,
66/// `sync_points`), so none of them is a guess about a future record.
67///
68/// Deliberately NOT included: `d2h`, `batch_size`, `steps`. Those are the
69/// keys the two falsified rules above tripped on — they name a *quantity of
70/// work arranged*, not a *quantity of waste*, and a kaizen may raise either.
71const COST_METRIC_FRAGMENTS: [&str; 9] = [
72    "alloc", "bytes", "churn", "launch", "overhead", "sync", "wasted", "_us", "_ms",
73];
74
75/// The kaizen-specific top-level blocks of a record, read in a second parse
76/// pass by [`crate::schema::parse_contract_str`].
77///
78/// Every field is optional and typed as loosely as the corpus demands
79/// (`kaizen:` is a string in 38 records and a bare integer in 8; `date:` is a
80/// YAML date in 16 and a quoted string in 1). Loose types here are not
81/// laxness — they are what lets the VALIDATOR name the problem instead of
82/// serde killing the parse with an opaque type error, the same reasoning that
83/// makes [`crate::schema::Metadata::demand_score`] an `i64`.
84///
85/// This is a `#[serde(skip)]` field of [`crate::schema::Contract`] rather
86/// than nine new top-level `Contract` fields on purpose: `status`, `version`,
87/// `invariants` and `files` are all keys OTHER contracts in the corpus carry
88/// with incompatible shapes, and widening `Contract` to admit them would
89/// change how 1726 files parse in order to validate 46.
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
91pub struct KaizenRecord {
92    /// The record's own identifier (`contract:`), e.g. `C-BWDSTG-001`.
93    #[serde(default)]
94    pub contract: Option<String>,
95    /// One-line statement of the improvement.
96    #[serde(default)]
97    pub title: Option<String>,
98    /// The kaizen ticket this record discharges (`KAIZEN-061`, `045`, `203`).
99    #[serde(default)]
100    pub kaizen: Option<Value>,
101    /// The contract this record improves upon, if any.
102    #[serde(default)]
103    pub parent: Option<String>,
104    /// Lifecycle state; checked against [`KAIZEN_STATUSES`].
105    #[serde(default)]
106    pub status: Option<String>,
107    /// When the improvement was recorded.
108    #[serde(default)]
109    pub date: Option<Value>,
110    /// Measured state BEFORE the change. Either a flat metric map, or a map
111    /// carrying its own `before:`/`after:` pair (`gpu-l2-norm-reduction-v1`).
112    #[serde(default)]
113    pub baseline: Option<Value>,
114    /// Measured state AFTER the change.
115    #[serde(default)]
116    pub target: Option<Value>,
117    /// Free-form invariants the record asserts survive the change.
118    #[serde(default)]
119    pub invariants: Option<Value>,
120}
121
122impl KaizenRecord {
123    /// The (before, after) metric maps this record pins, if it pins any.
124    ///
125    /// Two shapes are accepted because both exist in the corpus:
126    /// `baseline:`/`target:` (16 records) and a `baseline:` that carries its
127    /// own `before:`/`after:` (1 record). Returning `None` means the record
128    /// makes no numeric claim at all — legitimate for the 29 records that
129    /// assert `invariants:` instead, and handled by KAIZEN-003.
130    #[must_use]
131    pub fn delta_pair(&self) -> Option<(&Mapping, &Mapping)> {
132        let baseline = self.baseline.as_ref()?.as_mapping()?;
133        if let (Some(before), Some(after)) = (
134            baseline.get("before").and_then(Value::as_mapping),
135            baseline.get("after").and_then(Value::as_mapping),
136        ) {
137            return Some((before, after));
138        }
139        let target = self.target.as_ref()?.as_mapping()?;
140        Some((baseline, target))
141    }
142
143    /// Does this record assert anything that a later measurement could
144    /// contradict? Used by KAIZEN-003.
145    fn states_a_claim(&self, contract: &super::types::Contract) -> bool {
146        self.delta_pair().is_some()
147            || !is_empty_block(self.invariants.as_ref())
148            || !contract.proof_obligations.is_empty()
149            || !contract.falsification_tests.is_empty()
150    }
151}
152
153/// Is a captured YAML block absent or empty (`null`, `[]`, `{}`)?
154fn is_empty_block(value: Option<&Value>) -> bool {
155    match value {
156        None | Some(Value::Null) => true,
157        Some(Value::Sequence(s)) => s.is_empty(),
158        Some(Value::Mapping(m)) => m.is_empty(),
159        Some(_) => false,
160    }
161}
162
163/// The units and modifiers a quantity string may carry after its number.
164///
165/// A CLOSED table, not a "strip anything non-numeric" rule. The corpus holds
166/// `'<5'`, `'1000+'` and `'100%'` — all real measurements — next to
167/// `'2026-03-04'` and `'CUBLAS_COMPUTE_32F'`, which are not measurements at
168/// all. A permissive suffix rule reads `2026-03-04` as the number 2026 and
169/// invents a comparison out of a date.
170const QUANTITY_SUFFIXES: [&str; 14] = [
171    "", "%", "+", "x", "B", "KB", "MB", "GB", "KiB", "MiB", "GiB", "ms", "us", "s",
172];
173
174/// The comparison prefixes a quantity string may carry before its number.
175const QUANTITY_PREFIXES: [&str; 5] = ["<=", ">=", "<", ">", "~"];
176
177/// Read a baseline/target scalar as a comparable quantity, or `None` when it
178/// is not one.
179///
180/// `None` is never an error — it means "this pair cannot be compared", and an
181/// uncomparable pair is simply not evidence either way. Booleans are excluded
182/// deliberately: `false` → `true` is a state change, not a movement along an
183/// axis, and calling it "an increase" would let KAIZEN-006 pass judgement on
184/// something it cannot measure.
185#[must_use]
186pub fn parse_quantity(value: &Value) -> Option<f64> {
187    match value {
188        Value::Number(n) => n.as_f64().filter(|v| v.is_finite()),
189        Value::String(s) => parse_quantity_str(s),
190        _ => None,
191    }
192}
193
194/// Read a quantity out of a string: an optional comparison prefix, a number,
195/// and a suffix drawn from [`QUANTITY_SUFFIXES`].
196fn parse_quantity_str(raw: &str) -> Option<f64> {
197    let mut rest = raw.trim();
198    for prefix in QUANTITY_PREFIXES {
199        if let Some(stripped) = rest.strip_prefix(prefix) {
200            rest = stripped.trim_start();
201            break;
202        }
203    }
204    let digits = rest
205        .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '_')
206        .unwrap_or(rest.len());
207    let (number, suffix) = rest.split_at(digits);
208    let number = number.replace('_', "");
209    if !QUANTITY_SUFFIXES.contains(&suffix.trim()) {
210        return None;
211    }
212    number.parse::<f64>().ok().filter(|v| v.is_finite())
213}
214
215/// Every key present in BOTH maps whose values are both quantities, as
216/// `(key, before, after)`.
217fn comparable_metrics(before: &Mapping, after: &Mapping) -> Vec<(String, f64, f64)> {
218    let mut out = Vec::new();
219    for (key, before_value) in before {
220        let Some(name) = key.as_str() else { continue };
221        let Some(after_value) = after.get(key) else {
222            continue;
223        };
224        if let (Some(b), Some(a)) = (parse_quantity(before_value), parse_quantity(after_value)) {
225            out.push((name.to_string(), b, a));
226        }
227    }
228    out
229}
230
231/// Is this metric name one the record exists to REDUCE?
232fn is_cost_metric(name: &str) -> bool {
233    let lowered = name.to_ascii_lowercase();
234    COST_METRIC_FRAGMENTS
235        .iter()
236        .any(|fragment| lowered.contains(fragment))
237}
238
239fn violation(rule: &str, message: String, location: &str) -> Violation {
240    Violation {
241        severity: Severity::Error,
242        rule: rule.to_string(),
243        message,
244        location: Some(location.to_string()),
245    }
246}
247
248/// Validate a `metadata.kind: kaizen` contract (rules KAIZEN-001..006).
249pub(crate) fn validate_kaizen(contract: &super::types::Contract, violations: &mut Vec<Violation>) {
250    let Some(record) = contract.kaizen_record.as_ref() else {
251        violations.push(violation(
252            "KAIZEN-001",
253            "metadata.kind is `kaizen` but the document carries none of the kaizen \
254             record blocks (`contract:`, `status:`, `baseline:`/`target:`) — a kaizen \
255             record that records nothing is not a kaizen record"
256                .to_string(),
257            "contract",
258        ));
259        return;
260    };
261
262    validate_identity(record, violations);
263    validate_non_vacuity(record, contract, violations);
264    validate_delta(record, violations);
265}
266
267/// KAIZEN-001 / KAIZEN-002: the record must name itself and declare a status
268/// from the closed vocabulary.
269fn validate_identity(record: &KaizenRecord, violations: &mut Vec<Violation>) {
270    if record.contract.as_deref().is_none_or(str::is_empty) {
271        violations.push(violation(
272            "KAIZEN-001",
273            "a kaizen record must carry a non-empty `contract:` id — it is how every \
274             other document (parent records, qa_gate, the ledger) refers to this one"
275                .to_string(),
276            "contract",
277        ));
278    }
279
280    match record.status.as_deref().map(str::trim) {
281        None | Some("") => violations.push(violation(
282            "KAIZEN-002",
283            format!(
284                "a kaizen record must declare `status:` — one of: {}",
285                KAIZEN_STATUSES.join(", ")
286            ),
287            "status",
288        )),
289        Some(status) if !KAIZEN_STATUSES.contains(&status) => violations.push(violation(
290            "KAIZEN-002",
291            format!(
292                "kaizen `status: {status}` is not a known lifecycle state — must be one \
293                 of: {}",
294                KAIZEN_STATUSES.join(", ")
295            ),
296            "status",
297        )),
298        Some(_) => {}
299    }
300}
301
302/// KAIZEN-003: the record must assert something a later measurement could
303/// contradict.
304fn validate_non_vacuity(
305    record: &KaizenRecord,
306    contract: &super::types::Contract,
307    violations: &mut Vec<Violation>,
308) {
309    if !record.states_a_claim(contract) {
310        violations.push(violation(
311            "KAIZEN-003",
312            "this kaizen record states nothing that can fail — it has no baseline/target \
313             delta, no `invariants:`, no `proof_obligations:` and no \
314             `falsification_tests:`. A record that cannot be contradicted records an \
315             opinion, not an improvement"
316                .to_string(),
317            "baseline",
318        ));
319    }
320}
321
322/// KAIZEN-004 / 005 / 006: the baseline → target delta must be a real,
323/// comparable, non-contradictory claim.
324fn validate_delta(record: &KaizenRecord, violations: &mut Vec<Violation>) {
325    validate_delta_shape(record, violations);
326    let Some((before, after)) = record.delta_pair() else {
327        return;
328    };
329    let metrics = comparable_metrics(before, after);
330    validate_delta_moves(&metrics, violations);
331    validate_cost_direction(&metrics, violations);
332}
333
334/// KAIZEN-004: a `target:` needs a `baseline:` to be a target OF, both must be
335/// maps, and they must measure at least one metric in common.
336fn validate_delta_shape(record: &KaizenRecord, violations: &mut Vec<Violation>) {
337    let Some(target) = record.target.as_ref() else {
338        return;
339    };
340    let Some(target_map) = target.as_mapping() else {
341        violations.push(violation(
342            "KAIZEN-004",
343            "`target:` must be a map of metric → value so it can be compared to \
344             `baseline:` key by key"
345                .to_string(),
346            "target",
347        ));
348        return;
349    };
350    let Some(baseline_map) = record.baseline.as_ref().and_then(Value::as_mapping) else {
351        violations.push(violation(
352            "KAIZEN-004",
353            "`target:` is declared with no `baseline:` map to improve on — a target \
354             without a before-measurement cannot be shown to be an improvement"
355                .to_string(),
356            "baseline",
357        ));
358        return;
359    };
360    if !baseline_map.keys().any(|k| target_map.contains_key(k)) {
361        violations.push(violation(
362            "KAIZEN-004",
363            "`baseline:` and `target:` share no metric key — the target measures \
364             something the baseline never measured, so nothing in this record can be \
365             compared"
366                .to_string(),
367            "target",
368        ));
369    }
370}
371
372/// KAIZEN-005: at least one shared, comparable metric must actually move.
373fn validate_delta_moves(metrics: &[(String, f64, f64)], violations: &mut Vec<Violation>) {
374    if metrics.is_empty() {
375        violations.push(violation(
376            "KAIZEN-005",
377            "`baseline:` and `target:` share no metric whose values are both \
378             quantities — every shared key holds prose on at least one side, so the \
379             record pins no number and claims nothing measurable"
380                .to_string(),
381            "target",
382        ));
383        return;
384    }
385    if metrics.iter().all(|(_, before, after)| before == after) {
386        let names: Vec<&str> = metrics.iter().map(|(n, _, _)| n.as_str()).collect();
387        violations.push(violation(
388            "KAIZEN-005",
389            format!(
390                "`target:` restates `baseline:` unchanged on every comparable metric \
391                 ({}) — the record claims no movement, so no measurement can falsify it",
392                names.join(", ")
393            ),
394            "target",
395        ));
396    }
397}
398
399/// KAIZEN-006: a record that measures costs must lower at least one of them.
400///
401/// This is the rule that catches a `baseline:`/`target:` pair written the
402/// wrong way round: reversing a real record turns every falling cost into a
403/// rising one, and a kaizen that raises every cost it measures and lowers
404/// none has inverted its own claim. It deliberately does NOT require every
405/// cost to fall — `gpu-workspace-clip-v1` buys stability with PCIe traffic,
406/// and that is a kaizen too.
407fn validate_cost_direction(metrics: &[(String, f64, f64)], violations: &mut Vec<Violation>) {
408    let mut measures_a_cost = false;
409    let mut a_cost_fell = false;
410    let mut risen: Vec<String> = Vec::new();
411    for (name, before, after) in metrics {
412        if !is_cost_metric(name) {
413            continue;
414        }
415        measures_a_cost = true;
416        if after < before {
417            a_cost_fell = true;
418        } else if after > before {
419            risen.push(format!("{name} {before} \u{2192} {after}"));
420        }
421    }
422    if !measures_a_cost || a_cost_fell || risen.is_empty() {
423        return;
424    }
425    violations.push(violation(
426        "KAIZEN-006",
427        format!(
428            "every cost metric this record measures either rises or holds, and none \
429             falls ({}) — a kaizen record removes waste, so this is either a regression \
430             recorded as an improvement or a `baseline:`/`target:` pair written the \
431             wrong way round",
432            risen.join("; ")
433        ),
434        "target",
435    ));
436}
437
438#[cfg(test)]
439mod tests {
440    include!("kaizen_tests.rs");
441}