Skip to main content

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}