engenho_diff/junkyo.rs
1//! # junkyo (準拠) — adherence to a standard
2//!
3//! The **xfail axis** the differential harness was missing. [`cotejo`] answers
4//! *"do the two sides agree?"*; junkyo answers the question that actually gates
5//! a release: **"is this row's agreement-or-disagreement the one we declared?"**
6//!
7//! [`cotejo`]: crate::cotejo
8//!
9//! ## Why a bare [`Verdict`] cannot gate
10//!
11//! Before this module, a known divergence had exactly one representation:
12//! **an absent test**. And an absent test is green. So engenho's eight measured
13//! v1.34 divergences (2026-08-28) were invisible to CI not because anyone
14//! decided to waive them, but because there was nowhere to write them down.
15//! A waiver you cannot express becomes a waiver you cannot revoke.
16//!
17//! junkyo gives a divergence a **name, a reason, and a tier** — and then makes
18//! the matrix fail in *both* directions:
19//!
20//! | declared | observed | outcome | gate |
21//! |---|---|---|---|
22//! | [`Expect::Match`] | `Parity` | [`Outcome::Held`] | pass |
23//! | [`Expect::Match`] | `Divergent` | [`Outcome::Regressed`] | **FAIL** — a conformance regression |
24//! | [`Expect::KnownDiverge`] | `Divergent` | [`Outcome::Tracked`] | pass — the debt is known |
25//! | [`Expect::KnownDiverge`] | `Parity` | [`Outcome::Graduated`] | **FAIL** — fixed but undeclared |
26//! | *any* | `ReferenceUnreachable` | [`Outcome::OracleLost`] | **FAIL** — never a silent skip |
27//!
28//! ## The load-bearing rule: graduation is a failure
29//!
30//! [`Outcome::Graduated`] failing is the half that is easy to get wrong and is
31//! the whole point. If a `KnownDiverge` row silently passed once it started
32//! matching, the matrix would rot downward exactly the way a dated coverage
33//! claim does — it would keep reporting "8 known gaps" long after some were
34//! fixed, and nobody would ever be told the debt shrank. Forcing the fixer to
35//! *delete the waiver in the same commit* is what keeps the ledger honest, and
36//! it is why the count of standing waivers is a number you can trust.
37//!
38//! This mirrors sui's `parity_corpus` (`sui/src/parity_corpus.rs`), the fleet's
39//! proven Parity Method: `engenho:kube-apiserver :: sui:CppNix`. The shape is
40//! deliberately the same so a reader who knows one knows the other.
41//!
42//! ## Tier honesty
43//!
44//! This is **only-mitigated (C2 — external-world observation)**, not
45//! unrepresentability, and the ceiling is named: whether engenho matches
46//! upstream is a fact about a *foreign process*, which no Rust type can decide.
47//! The strongest available rung is a differential that cannot silently pass —
48//! which is what this is. Do not describe junkyo as making divergence
49//! unrepresentable; it makes an *undeclared* divergence unrepresentable **as a
50//! green run**, which is a strictly weaker and honest claim.
51
52use core::fmt;
53
54use crate::verdict::Verdict;
55
56/// Why a divergence is being tolerated. A closed vocabulary, so a waiver is
57/// **classified rather than excused** — free-text reasons are how a debt
58/// ledger becomes unreadable.
59///
60/// Adding a variant is deliberately a typed decision: if a new divergence does
61/// not fit one of these, that is a signal worth stopping on, not a reason to
62/// widen the enum reflexively.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum DivergenceClass {
65 /// engenho has not implemented the surface yet. The honest default for a
66 /// milestone gap (e.g. Table conversion, absent workspace-wide).
67 NotYetImplemented,
68 /// engenho deliberately differs and intends to keep differing. Requires a
69 /// stated rationale at the call site — this is the only variant that is
70 /// not automatically a debt to burn down.
71 IntentionalDeviation,
72 /// The oracle and the pinned contract are at different patch levels, so
73 /// the difference is upstream's, not engenho's. Keeps patch skew from
74 /// being laundered through the normalizer.
75 UpstreamPatchSkew,
76 /// The divergence is real but its conformance relevance is not yet
77 /// established. Recorded rather than guessed — promotion should be driven
78 /// off upstream's `[Conformance]` tag list, never from memory.
79 ConformanceRelevanceUnclassified,
80}
81
82impl DivergenceClass {
83 /// Whether this class represents debt that should eventually reach zero.
84 /// [`Self::IntentionalDeviation`] does not; everything else does.
85 #[must_use]
86 pub fn is_burn_down_debt(self) -> bool {
87 !matches!(self, Self::IntentionalDeviation)
88 }
89}
90
91/// What the matrix declares this row *should* do.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum Expect {
94 /// engenho must agree with the oracle. Any divergence fails the gate.
95 Match,
96 /// engenho is known to diverge here. The divergence is tolerated — but
97 /// only while it persists: a row that starts matching fails as
98 /// [`Outcome::Graduated`] until the waiver is removed.
99 KnownDiverge {
100 /// The classified reason. See [`DivergenceClass`].
101 class: DivergenceClass,
102 /// Short human note naming the specific defect, for the failure
103 /// message a future reader will actually see.
104 note: &'static str,
105 },
106}
107
108/// The adjudicated result of one corpus row: `(Expect, Verdict) -> Outcome`.
109///
110/// Closed by construction — every pairing above maps into exactly one variant,
111/// so a future `Verdict` variant is a non-exhaustive-match compile error here
112/// rather than a silently-unhandled case.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum Outcome {
115 /// Declared `Match`, observed parity. The good case.
116 Held,
117 /// Declared `KnownDiverge`, observed divergence. Debt, but *known* debt.
118 Tracked {
119 /// The class carried through from the waiver, so a report can group
120 /// the ledger without re-deriving it.
121 class: DivergenceClass,
122 },
123 /// Declared `Match`, observed divergence. **A conformance regression.**
124 Regressed {
125 /// How many divergences the differ found, for the failure message.
126 divergences: usize,
127 },
128 /// Declared `KnownDiverge`, observed parity. **Fixed but still waived** —
129 /// remove the waiver in the same commit as the fix.
130 Graduated {
131 /// The note from the stale waiver, so the message can name what to delete.
132 stale_note: &'static str,
133 },
134 /// The oracle could not be reached. Never a silent skip.
135 OracleLost,
136}
137
138impl Outcome {
139 /// Whether this outcome lets the gate stay green.
140 ///
141 /// Only [`Self::Held`] and [`Self::Tracked`] pass. In particular
142 /// [`Self::Graduated`] does **not** — see the module docs for why that is
143 /// the load-bearing half.
144 #[must_use]
145 pub fn passes(&self) -> bool {
146 matches!(self, Self::Held | Self::Tracked { .. })
147 }
148
149 /// Whether this outcome fails the gate — the negation of [`Self::passes`],
150 /// named positively so a call site reads as intent.
151 #[must_use]
152 pub fn is_failure(&self) -> bool {
153 !self.passes()
154 }
155}
156
157/// Renders the adjudication verdict. `write!()` inside a `Display` block is
158/// this crate's sanctioned typed emission (★★ TYPED EMISSION) — the reason
159/// text is NEVER `format!()`-composed free text.
160impl fmt::Display for Outcome {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match self {
163 Self::Held => write!(f, "HELD"),
164 Self::Tracked { class } => write!(f, "TRACKED ({class})"),
165 Self::Regressed { divergences } => write!(
166 f,
167 "REGRESSED: declared Match but observed {divergences} divergence(s)"
168 ),
169 Self::Graduated { stale_note } => write!(
170 f,
171 "GRADUATED: declared KnownDiverge ({stale_note}) but observed parity \
172 — delete the waiver in the same commit as the fix"
173 ),
174 Self::OracleLost => write!(f, "ORACLE LOST: the reference could not be reached"),
175 }
176 }
177}
178
179/// A stable label per class, so a report groups the ledger without re-deriving.
180impl fmt::Display for DivergenceClass {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(
183 f,
184 "{}",
185 match self {
186 Self::NotYetImplemented => "not-yet-implemented",
187 Self::IntentionalDeviation => "intentional-deviation",
188 Self::UpstreamPatchSkew => "upstream-patch-skew",
189 Self::ConformanceRelevanceUnclassified => "conformance-relevance-unclassified",
190 }
191 )
192 }
193}
194
195/// Adjudicate one row: what we declared against what we observed.
196///
197/// This is the whole invariant, and it is deliberately total over both enums.
198#[must_use]
199pub fn adjudicate(expect: &Expect, verdict: &Verdict) -> Outcome {
200 match (expect, verdict) {
201 // The oracle arm is checked first: an unreachable oracle tells us
202 // nothing about either side, so it can never be read as agreement.
203 (_, Verdict::ReferenceUnreachable) => Outcome::OracleLost,
204
205 (Expect::Match, Verdict::Parity(_)) => Outcome::Held,
206 (Expect::Match, Verdict::Divergent(d)) => Outcome::Regressed {
207 divergences: d.len(),
208 },
209
210 (Expect::KnownDiverge { class, .. }, Verdict::Divergent(_)) => {
211 Outcome::Tracked { class: *class }
212 }
213 (Expect::KnownDiverge { note, .. }, Verdict::Parity(_)) => {
214 Outcome::Graduated { stale_note: note }
215 }
216 }
217}
218
219/// The verdict over a whole corpus run.
220///
221/// Carries its own **denominator** (`rows`) inside the compared value — the
222/// fleet's anti-vacuity rule. A matrix that stopped discovering rows must fail
223/// loudly rather than report an empty green, which is exactly how a coverage
224/// gate rots without anyone noticing.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct MatrixReport {
227 /// Total rows adjudicated. The denominator.
228 pub rows: usize,
229 /// Rows that held (declared Match, observed parity).
230 pub held: usize,
231 /// Rows tracked as known divergences.
232 pub tracked: usize,
233 /// Failures, each `(row name, the adjudicated outcome)`. The outcome is
234 /// kept typed rather than pre-rendered — it renders through [`Display`]
235 /// at report time, never as a stored string.
236 pub failures: Vec<(String, Outcome)>,
237}
238
239impl MatrixReport {
240 /// Fold adjudicated rows into a report.
241 ///
242 /// `named` is `(row name, outcome)`.
243 #[must_use]
244 pub fn from_rows(named: impl IntoIterator<Item = (String, Outcome)>) -> Self {
245 let mut report = Self {
246 rows: 0,
247 held: 0,
248 tracked: 0,
249 failures: Vec::new(),
250 };
251 for (name, outcome) in named {
252 report.rows += 1;
253 match &outcome {
254 Outcome::Held => report.held += 1,
255 Outcome::Tracked { .. } => report.tracked += 1,
256 _ => {}
257 }
258 if outcome.is_failure() {
259 report.failures.push((name, outcome));
260 }
261 }
262 report
263 }
264
265 /// Whether the gate is green.
266 ///
267 /// **An empty corpus is NOT green.** `rows == 0` means the matrix
268 /// discovered nothing, which is indistinguishable from every row having
269 /// been deleted — the vacuity failure this field exists to catch.
270 #[must_use]
271 pub fn is_green(&self) -> bool {
272 self.rows > 0 && self.failures.is_empty()
273 }
274}
275
276/// One line carrying the denominator, suitable for a gate's output. Always
277/// states `rows`, so a SHRINKING corpus is visible in the log even on a green
278/// run — the anti-vacuity rule made readable.
279impl fmt::Display for MatrixReport {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 write!(
282 f,
283 "junkyo: {} rows — {} held, {} tracked, {} failed",
284 self.rows,
285 self.held,
286 self.tracked,
287 self.failures.len()
288 )
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::path::JsonPath;
296 use crate::verdict::{Divergence, ParityWitness, Side, Verdict};
297
298 /// A `Parity` verdict. `ParityWitness::new()` is `pub(crate)` — proof-by-
299 /// absence — so only an in-crate test can build one, which is the point.
300 fn parity() -> Verdict {
301 Verdict::Parity(ParityWitness::new())
302 }
303
304 /// A `Divergent` verdict carrying one arbitrary divergence.
305 fn divergent() -> Verdict {
306 Verdict::Divergent(vec![Divergence::MissingResource {
307 path: JsonPath::root(),
308 present_on: Side::K3s,
309 }])
310 }
311
312 fn known() -> Expect {
313 Expect::KnownDiverge {
314 class: DivergenceClass::NotYetImplemented,
315 note: "Table conversion absent",
316 }
317 }
318
319 #[test]
320 fn match_plus_parity_holds() {
321 let v = parity();
322 assert_eq!(adjudicate(&Expect::Match, &v), Outcome::Held);
323 assert!(adjudicate(&Expect::Match, &v).passes());
324 }
325
326 #[test]
327 fn match_plus_divergence_is_a_regression() {
328 let out = adjudicate(&Expect::Match, &divergent());
329 assert!(matches!(out, Outcome::Regressed { divergences: 1 }));
330 assert!(!out.passes(), "a regression MUST fail the gate");
331 }
332
333 #[test]
334 fn known_diverge_plus_divergence_is_tracked() {
335 let out = adjudicate(&known(), &divergent());
336 assert!(out.passes(), "known debt does not fail the gate");
337 assert!(matches!(
338 out,
339 Outcome::Tracked {
340 class: DivergenceClass::NotYetImplemented
341 }
342 ));
343 }
344
345 /// The load-bearing rule: a fixed-but-still-waived row FAILS.
346 #[test]
347 fn known_diverge_plus_parity_is_a_failure() {
348 let out = adjudicate(&known(), &parity());
349 assert!(
350 !out.passes(),
351 "a KnownDiverge that starts matching MUST fail — otherwise the \
352 ledger silently over-reports debt forever"
353 );
354 assert!(out.to_string().contains("GRADUATED"));
355 }
356
357 #[test]
358 fn unreachable_oracle_never_reads_as_agreement() {
359 for e in [Expect::Match, known()] {
360 let out = adjudicate(&e, &Verdict::ReferenceUnreachable);
361 assert_eq!(out, Outcome::OracleLost);
362 assert!(
363 !out.passes(),
364 "an unreachable oracle is never a silent skip"
365 );
366 }
367 }
368
369 /// Anti-vacuity: an empty corpus is not green.
370 #[test]
371 fn empty_corpus_is_not_green() {
372 let r = MatrixReport::from_rows(std::iter::empty());
373 assert_eq!(r.rows, 0);
374 assert!(
375 !r.is_green(),
376 "an empty matrix must FAIL — a corpus that discovered nothing is \
377 indistinguishable from one whose rows were all deleted"
378 );
379 }
380
381 #[test]
382 fn report_carries_its_denominator() {
383 let r = MatrixReport::from_rows([
384 ("a".to_string(), Outcome::Held),
385 (
386 "b".to_string(),
387 Outcome::Tracked {
388 class: DivergenceClass::NotYetImplemented,
389 },
390 ),
391 ]);
392 assert!(r.is_green());
393 assert_eq!((r.rows, r.held, r.tracked), (2, 1, 1));
394 assert!(
395 r.to_string().contains("2 rows"),
396 "denominator must be visible"
397 );
398 }
399
400 #[test]
401 fn a_single_failure_reddens_the_whole_matrix() {
402 let r = MatrixReport::from_rows([
403 ("ok".to_string(), Outcome::Held),
404 ("bad".to_string(), Outcome::Regressed { divergences: 3 }),
405 ]);
406 assert!(!r.is_green());
407 assert_eq!(r.failures.len(), 1);
408 assert!(r.failures[0].1.to_string().contains("REGRESSED"));
409 }
410
411 #[test]
412 fn intentional_deviation_is_the_only_non_debt_class() {
413 assert!(!DivergenceClass::IntentionalDeviation.is_burn_down_debt());
414 for c in [
415 DivergenceClass::NotYetImplemented,
416 DivergenceClass::UpstreamPatchSkew,
417 DivergenceClass::ConformanceRelevanceUnclassified,
418 ] {
419 assert!(c.is_burn_down_debt());
420 }
421 }
422}