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}