Skip to main content

logicaffeine_proof/
ait.rs

1//! # Algorithmic information theory — certified description-length objects
2//!
3//! The provers already *measure* algorithmic information (`hypercube::symmetry_entropy_bits` =
4//! log₂|Aut(F)|; the clause-orbit quotient as "the computable shadow of Kolmogorov complexity").
5//! This module promotes those measurements into **certified objects**: a description length that
6//! carries a re-checkable witness, so "this object compresses to N bytes" is something a checker
7//! confirms rather than trusts.
8//!
9//! Kolmogorov complexity `K(x)` is uncomputable, so we work with the two honest sides of it:
10//!
11//! - **Upper bounds** `K̄(x)` are computable — the shortest program in a *fixed* description language
12//!   that reproduces `x`. [`DescriptionBound`] carries such a program together with the bytes that,
13//!   when decoded, reproduce `x` (the witness). The description language is layered: [`Descriptor::IntSeq`]
14//!   is the closed-form generator menu of [`logicaffeine_base::describe`] (affine / geometric /
15//!   polynomial / periodic / sparse / …), a computable upper bound with a lossless decode witness.
16//!
17//! Lower bounds (the incompressibility side) and the two-sided structural bound live alongside this,
18//! gated by a budget so we never claim a bound past what the prover can itself re-check (the
19//! operational Chaitin ceiling).
20
21use logicaffeine_base::describe;
22
23use crate::cdcl::{Lit, Var};
24use crate::proof::Perm;
25use std::collections::BTreeSet;
26
27/// The description language. Each variant is a *program* in a fixed language whose length is the
28/// description bound and whose execution (decode) reproduces the object.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum Descriptor {
31    /// Layer 1: the closed-form generator menu of [`logicaffeine_base::describe`] applied to a raw
32    /// integer sequence. `encoded` is the shortest menu encoding; decoding it reproduces the sequence.
33    IntSeq { encoded: Vec<u8> },
34    /// Layer 3: a Boolean function described by its recursive symmetry decomposition ([`structure_tree`]).
35    /// Decoding replays the tree to reproduce the `2ⁿ` truth table.
36    BooleanFunction { num_vars: usize, tree: StructureTree },
37}
38
39/// A **computable upper bound** `K̄(x)` on the Kolmogorov complexity of an object `x`, over the fixed
40/// description language, carrying a re-checkable decode witness.
41///
42/// `bytes` is the program length (the bound). `object_hash` is a stable content hash of `x`.
43/// [`DescriptionBound::verify`] decodes the descriptor and confirms it reproduces `x` — an upper
44/// bound is only trustworthy because *decode(program) = x*.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct DescriptionBound {
47    /// A stable content hash of the described object (order- and value-sensitive).
48    pub object_hash: u64,
49    /// The description length in bytes — the certified upper bound `K̄(x)`.
50    pub bytes: usize,
51    /// The program (in the description language) that reproduces the object.
52    pub descriptor: Descriptor,
53}
54
55impl DescriptionBound {
56    /// Describe an integer sequence by its shortest generator-menu encoding (Layer 1). The result's
57    /// `bytes` is never larger than the plain-varint length of the sequence.
58    pub fn of_int_seq(v: &[i64]) -> DescriptionBound {
59        let encoded = describe::describe_int_seq(v);
60        DescriptionBound { object_hash: hash_ints(v), bytes: encoded.len(), descriptor: Descriptor::IntSeq { encoded } }
61    }
62
63    /// Describe a Boolean function by its recursive symmetry decomposition (Layer 3), the same certified
64    /// upper bound as [`kolmogorov_bound`] but inside the unified `DescriptionBound` framework. `bytes` is
65    /// the tree's total description size rounded up to bytes. `None` if `truth.len()` is not a power of two.
66    pub fn of_boolean(truth: &[bool]) -> Option<DescriptionBound> {
67        let num_vars = truth.len().trailing_zeros() as usize;
68        let tree = structure_tree(truth)?;
69        let bytes = tree.total_description_bits().div_ceil(8).max(1);
70        Some(DescriptionBound {
71            object_hash: hash_bools(truth),
72            bytes,
73            descriptor: Descriptor::BooleanFunction { num_vars, tree },
74        })
75    }
76
77    /// Re-check the decode witness: decode the descriptor and confirm it reproduces the object whose
78    /// hash we recorded. This is the whole point of an upper bound — it is trusted only because the
79    /// program actually reconstructs `x`. A tampered/corrupt descriptor fails here.
80    pub fn verify(&self) -> bool {
81        match &self.descriptor {
82            Descriptor::IntSeq { encoded } => {
83                encoded.len() == self.bytes
84                    && describe::decode_int_seq(encoded).map_or(false, |v| hash_ints(&v) == self.object_hash)
85            }
86            Descriptor::BooleanFunction { num_vars, tree } => {
87                tree.total_description_bits().div_ceil(8).max(1) == self.bytes
88                    && tree.reconstruct().map_or(false, |t| {
89                        t.len() == (1usize << num_vars) && hash_bools(&t) == self.object_hash
90                    })
91            }
92        }
93    }
94}
95
96/// A stable content hash of a Boolean truth table (FNV-1a over the bits, salted by length).
97fn hash_bools(v: &[bool]) -> u64 {
98    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
99    const PRIME: u64 = 0x0000_0100_0000_01b3;
100    let mut h = OFFSET;
101    let mut mix = |b: u8| {
102        h ^= b as u64;
103        h = h.wrapping_mul(PRIME);
104    };
105    for b in (v.len() as u64).to_le_bytes() {
106        mix(b);
107    }
108    for &x in v {
109        mix(x as u8);
110    }
111    h
112}
113
114/// A stable, portable content hash (FNV-1a over the little-endian value bytes, salted by the count so
115/// a trailing zero can't alias a shorter sequence). Deterministic across runs and machines.
116fn hash_ints(v: &[i64]) -> u64 {
117    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
118    const PRIME: u64 = 0x0000_0100_0000_01b3;
119    let mut h = OFFSET;
120    let mut mix = |bytes: [u8; 8]| {
121        for b in bytes {
122            h ^= b as u64;
123            h = h.wrapping_mul(PRIME);
124        }
125    };
126    mix((v.len() as u64).to_le_bytes());
127    for &x in v {
128        mix(x.to_le_bytes());
129    }
130    h
131}
132
133// ---- The two-sided structural bound: K̄(F) ≤ K̄(rep) + K̄(gens) + O(1) -----------------------
134//
135// A formula with a large automorphism group is *compressible*: one representative clause per orbit,
136// plus the generators of the group, reconstructs the whole formula. This is the "symmetry = compression"
137// thesis made into a re-checkable certificate — `symmetry_entropy_bits` (log₂|Aut|) is exactly the bits
138// the group saves, and here that saving is realized as bytes and independently verified.
139
140/// The fixed byte cost of the group-expansion decoder ("take `rep`, apply the group generated by
141/// `gens`, output the union"). A documented constant so the inequality `K̄(F) ≤ K̄(rep) + K̄(gens) + O(1)`
142/// is concrete.
143pub const GROUP_DECODER_OVERHEAD: usize = 8;
144
145/// A certified statement that `F` is described by an orbit-representative set plus automorphism
146/// generators — with all three description lengths, the group-entropy, and a self-contained decode
147/// witness. [`StructuralBound::verify`] re-derives `F`, `rep`, and the generators from the three
148/// descriptors and confirms the generators are automorphisms and that expanding `rep` by the group
149/// they generate reproduces `F` exactly.
150#[derive(Clone, Debug)]
151pub struct StructuralBound {
152    /// The number of Boolean variables `F` is defined over.
153    pub num_vars: usize,
154    /// K̄(F): the flat description of the whole formula.
155    pub whole: DescriptionBound,
156    /// K̄(rep): the description of one clause per orbit.
157    pub rep: DescriptionBound,
158    /// K̄(gens): the description of the automorphism generators.
159    pub gens: DescriptionBound,
160    /// log₂|Aut(F)| — the bits of symmetry the group compresses out.
161    pub group_entropy_bits: f64,
162}
163
164impl StructuralBound {
165    /// The group description length: `K̄(rep) + K̄(gens) + O(1)`.
166    pub fn group_bytes(&self) -> usize {
167        self.rep.bytes + self.gens.bytes + GROUP_DECODER_OVERHEAD
168    }
169
170    /// Whether the group description is strictly shorter than the flat one — a *certified compression*
171    /// by symmetry. (When it is not, we honestly report the flat encoding as the better bound.)
172    pub fn is_compression(&self) -> bool {
173        self.group_bytes() < self.whole.bytes
174    }
175
176    /// The best certified upper bound on `K(F)`: the smaller of the flat and group descriptions.
177    pub fn best_bytes(&self) -> usize {
178        self.whole.bytes.min(self.group_bytes())
179    }
180
181    /// Re-check the whole certificate from scratch, trusting nothing the producer computed:
182    /// (1) all three decode witnesses round-trip; (2) every recovered generator is a genuine
183    /// automorphism of the recovered `F`; (3) expanding `rep` by the group the generators generate
184    /// reproduces `F` exactly (so `rep` + `gens` is a *complete* description).
185    pub fn verify(&self) -> bool {
186        // (1) all three decode witnesses round-trip.
187        if !(self.whole.verify() && self.rep.verify() && self.gens.verify()) {
188            return false;
189        }
190        // Recover F, rep, and the generators from the descriptors alone — trust nothing precomputed.
191        let (nv_f, f_clauses) = match decode_cnf(&self.whole.descriptor) {
192            Some(x) => x,
193            None => return false,
194        };
195        let (_, rep_clauses) = match decode_cnf(&self.rep.descriptor) {
196            Some(x) => x,
197            None => return false,
198        };
199        let (nv_g, gens) = match decode_gens(&self.gens.descriptor) {
200            Some(x) => x,
201            None => return false,
202        };
203        if nv_f != self.num_vars || nv_g != self.num_vars {
204            return false;
205        }
206        // (2) every recovered generator is a genuine automorphism of the recovered F.
207        if !gens.iter().all(|p| crate::symmetry_detect::perm_is_automorphism(&f_clauses, p)) {
208            return false;
209        }
210        // (3) expanding rep by the group the generators generate reproduces F exactly.
211        reconstructs(&f_clauses, &rep_clauses, &gens)
212    }
213}
214
215/// Decode a CNF descriptor back to `(num_vars, clauses)` — `None` on a corrupt/tampered witness.
216fn decode_cnf(d: &Descriptor) -> Option<(usize, Vec<Vec<Lit>>)> {
217    let Descriptor::IntSeq { encoded } = d else { return None };
218    unflatten_cnf(&describe::decode_int_seq(encoded)?)
219}
220
221/// Decode a generator descriptor back to `(num_vars, generators)` — `None` on a corrupt/tampered witness.
222fn decode_gens(d: &Descriptor) -> Option<(usize, Vec<Perm>)> {
223    let Descriptor::IntSeq { encoded } = d else { return None };
224    unflatten_gens(&describe::decode_int_seq(encoded)?)
225}
226
227/// Build the structural bound for `F` under a set of candidate `generators`. Returns `None` if any
228/// generator is not an automorphism, or if `rep` + generators does not reconstruct `F` (so we never
229/// issue a certificate we could not re-check).
230pub fn structural_bound(num_vars: usize, clauses: &[Vec<Lit>], generators: &[Perm]) -> Option<StructuralBound> {
231    if !generators.iter().all(|g| crate::symmetry_detect::perm_is_automorphism(clauses, g)) {
232        return None;
233    }
234    let orbits = crate::hypercube::clause_orbits(clauses, generators);
235    let rep_clauses: Vec<Vec<Lit>> = orbits.iter().filter_map(|o| o.first().map(|&i| clauses[i].clone())).collect();
236    // The representative set plus the group must reconstruct the whole formula.
237    if !reconstructs(clauses, &rep_clauses, generators) {
238        return None;
239    }
240    Some(StructuralBound {
241        num_vars,
242        whole: DescriptionBound::of_int_seq(&flatten_cnf(num_vars, clauses)),
243        rep: DescriptionBound::of_int_seq(&flatten_cnf(num_vars, &rep_clauses)),
244        gens: DescriptionBound::of_int_seq(&flatten_gens(num_vars, generators)),
245        group_entropy_bits: crate::hypercube::symmetry_entropy_bits(num_vars, clauses),
246    })
247}
248
249/// Whether `rep_clauses` plus the group generated by `gens` reconstructs `f_clauses` exactly — checked
250/// WITHOUT enumerating the (astronomically large) group. The caller has verified `f_clauses` is closed
251/// under each generator (`perm_is_automorphism`), so `group·rep ⊆ F` follows from `rep ⊆ F`, and
252/// `F ⊆ group·rep` holds iff every orbit of F under the generators contains a rep clause (any F clause
253/// shares an orbit — hence a group element — with the rep of that orbit).
254fn reconstructs(f_clauses: &[Vec<Lit>], rep_clauses: &[Vec<Lit>], gens: &[Perm]) -> bool {
255    let f_keys = clause_set(f_clauses);
256    let rep_keys = clause_set(rep_clauses);
257    if !rep_keys.is_subset(&f_keys) {
258        return false; // rep must be clauses of F
259    }
260    let orbits = crate::hypercube::clause_orbits(f_clauses, gens);
261    orbits.iter().all(|orbit| {
262        orbit.iter().any(|&i| rep_keys.contains(&crate::symmetry_detect::clause_key(&f_clauses[i])))
263    })
264}
265
266/// The set of canonical clause keys — order- and literal-order-independent, so two formulas compare
267/// equal iff they are the same clause set.
268fn clause_set(clauses: &[Vec<Lit>]) -> BTreeSet<Vec<u32>> {
269    clauses.iter().map(|c| crate::symmetry_detect::clause_key(c)).collect()
270}
271
272/// Encode a literal as `2·var + sign` (0 = positive), the dense non-negative code.
273fn lit_code(l: Lit) -> i64 {
274    (l.var() as i64) * 2 + if l.is_positive() { 0 } else { 1 }
275}
276
277/// The inverse of [`lit_code`]. `None` on a negative/garbage code (a tampered descriptor).
278fn lit_from_code(code: i64) -> Option<Lit> {
279    if code < 0 {
280        return None;
281    }
282    Some(Lit::new((code / 2) as Var, code % 2 == 0))
283}
284
285/// Flatten a CNF to `[num_vars, num_clauses, len₀, lit₀₀, …, len₁, …]` for description.
286fn flatten_cnf(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<i64> {
287    let mut out = vec![num_vars as i64, clauses.len() as i64];
288    for c in clauses {
289        out.push(c.len() as i64);
290        for &l in c {
291            out.push(lit_code(l));
292        }
293    }
294    out
295}
296
297/// The inverse of [`flatten_cnf`]. `None` on a malformed/tampered sequence.
298fn unflatten_cnf(flat: &[i64]) -> Option<(usize, Vec<Vec<Lit>>)> {
299    let mut it = flat.iter().copied();
300    let num_vars = usize::try_from(it.next()?).ok()?;
301    let num_clauses = usize::try_from(it.next()?).ok()?;
302    let mut clauses = Vec::with_capacity(num_clauses.min(4096));
303    for _ in 0..num_clauses {
304        let len = usize::try_from(it.next()?).ok()?;
305        let mut c = Vec::with_capacity(len.min(4096));
306        for _ in 0..len {
307            c.push(lit_from_code(it.next()?)?);
308        }
309        clauses.push(c);
310    }
311    if it.next().is_some() {
312        return None; // trailing garbage
313    }
314    Some((num_vars, clauses))
315}
316
317/// Flatten generators to `[num_gens, num_vars, σ₀(+0), σ₀(+1), …, σ₁(+0), …]` for description.
318fn flatten_gens(num_vars: usize, gens: &[Perm]) -> Vec<i64> {
319    let mut out = vec![gens.len() as i64, num_vars as i64];
320    for g in gens {
321        for v in 0..num_vars {
322            out.push(lit_code(g.apply(Lit::pos(v as Var))));
323        }
324    }
325    out
326}
327
328/// The inverse of [`flatten_gens`]. `None` on a malformed/tampered sequence.
329fn unflatten_gens(flat: &[i64]) -> Option<(usize, Vec<Perm>)> {
330    let mut it = flat.iter().copied();
331    let num_gens = usize::try_from(it.next()?).ok()?;
332    let num_vars = usize::try_from(it.next()?).ok()?;
333    let mut gens = Vec::with_capacity(num_gens.min(4096));
334    for _ in 0..num_gens {
335        let mut images = Vec::with_capacity(num_vars.min(4096));
336        for _ in 0..num_vars {
337            images.push(lit_from_code(it.next()?)?);
338        }
339        gens.push(Perm::from_images(images));
340    }
341    if it.next().is_some() {
342        return None;
343    }
344    Some((num_vars, gens))
345}
346
347// ---- Class-relative incompressibility: "no linear (GF(2)) symmetry shortcut" -----------------
348//
349// A formula's parity (GF(2)) structure is *exactly* characterized by the rank and null space of the
350// XOR system latent in its clauses. When that structure is fully exposed — every parity constraint
351// recovered, the solution space pinned to exactly 2^k over the k-dimensional kernel — there is no
352// further LINEAR collapse to find: any residual hardness is non-linear. This turns the par32-style
353// "linearly-rigid kernel" *measurement* into a re-checkable *certificate*, class-relative to the
354// linear/parity class 𝒟 (an exact claim, no appeal to uncomputable universal K).
355
356/// The maximum variable count for which we ship an explicit GF(2) kernel basis (the strongest
357/// witness): `gf2::solve_gf2` packs each row into a `u64`. Larger systems (par32-scale) are a
358/// documented follow-on requiring incremental kernel-basis extraction from [`crate::xor_engine`].
359const RIGIDITY_MAX_VARS: usize = 64;
360
361/// The prover's own description budget — **the operational Chaitin ceiling.** Kolmogorov complexity
362/// lower bounds are only certifiable up to what the prover can itself re-check; beyond a resource
363/// bound we decline rather than over-claim. Every *lower-bound* (incompressibility) path is gated by
364/// a `Budget` and returns a documented [`Refusal`] instead of a certificate when a bound is exceeded.
365/// (Upper bounds — [`DescriptionBound`] — are always safe to compute: you can only be pleasantly
366/// surprised by a shorter description.)
367#[derive(Clone, Copy, Debug)]
368pub struct Budget {
369    /// The largest GF(2) system (in variables) whose kernel basis we will materialize and re-check.
370    pub max_gaussian_dim: usize,
371}
372
373impl Budget {
374    /// The standard budget: caps the GF(2) kernel-basis materialization at [`RIGIDITY_MAX_VARS`].
375    pub fn standard() -> Budget {
376        Budget { max_gaussian_dim: RIGIDITY_MAX_VARS }
377    }
378}
379
380/// Why a lower-bound certificate was **not** issued — the documented refusal that makes the Chaitin
381/// ceiling operational. A refusal is never a claim that no bound exists; it is an honest "this is
382/// beyond what I can re-check within budget."
383#[derive(Clone, Debug, PartialEq, Eq)]
384pub enum Refusal {
385    /// The GF(2) system exceeds the budget's variable cap (par32-scale needs the incremental engine).
386    OverBudgetGaussian { dim: usize, cap: usize },
387    /// There is no linear (parity) structure in the formula to certify rigid.
388    NoLinearStructure,
389}
390
391/// A re-checkable certificate that `F`'s parity structure admits no linear symmetry shortcut beyond
392/// its exposed kernel: the GF(2) coefficient system has rank `rank`, its solution space is exactly
393/// `2^solution_count_log2` (spanned by `kernel_basis`), and this is the complete linear structure.
394#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct LinearRigidityCert {
396    /// The number of Boolean variables `F` is over.
397    pub num_vars: usize,
398    /// The GF(2) coefficient rows recovered from `F` — bit `v` set iff variable `v` occurs in that
399    /// parity equation (the right-hand side is irrelevant to the linear structure).
400    pub rows: Vec<u64>,
401    /// The rank of the coefficient system — the number of independent parity constraints.
402    pub rank: usize,
403    /// A basis for the null space (the linear symmetry directions). Exactly `num_vars − rank` vectors.
404    pub kernel_basis: Vec<Vec<bool>>,
405    /// `log₂` of the number of GF(2) solutions — `= kernel_basis.len()`.
406    pub solution_count_log2: u32,
407}
408
409/// Certify the linear (GF(2)) rigidity of `F` under the standard budget — `None` on any refusal
410/// (no parity structure, or over budget). See [`certify_linear_rigidity_within`] for the documented
411/// refusal.
412pub fn certify_linear_rigidity(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<LinearRigidityCert> {
413    certify_linear_rigidity_within(num_vars, clauses, &Budget::standard()).ok()
414}
415
416/// Certify the linear (GF(2)) rigidity of `F`, failing **closed** to `budget`: a system that exceeds
417/// the budget's Gaussian cap returns [`Refusal::OverBudgetGaussian`] — the operational Chaitin
418/// ceiling — rather than a certificate we could not re-check.
419pub fn certify_linear_rigidity_within(
420    num_vars: usize,
421    clauses: &[Vec<Lit>],
422    budget: &Budget,
423) -> Result<LinearRigidityCert, Refusal> {
424    if num_vars > budget.max_gaussian_dim {
425        return Err(Refusal::OverBudgetGaussian { dim: num_vars, cap: budget.max_gaussian_dim });
426    }
427    let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
428    if eqs.is_empty() {
429        return Err(Refusal::NoLinearStructure);
430    }
431    let rows = eqs_to_rows(&eqs, num_vars);
432    let sol = crate::gf2::solve_gf2(num_vars, &rows, &vec![false; rows.len()]).ok_or(Refusal::NoLinearStructure)?;
433    let rank = num_vars - sol.kernel_basis.len();
434    Ok(LinearRigidityCert {
435        num_vars,
436        rows,
437        rank,
438        solution_count_log2: sol.kernel_basis.len() as u32,
439        kernel_basis: sol.kernel_basis,
440    })
441}
442
443/// Re-check a [`LinearRigidityCert`] against `F`, trusting nothing the producer computed: re-extract
444/// the parity system, independently recompute its kernel, and confirm the certificate's basis is a
445/// genuine, independent, complete null-space of the recovered rows.
446pub fn check_linear_rigidity(cert: &LinearRigidityCert, clauses: &[Vec<Lit>]) -> bool {
447    if cert.num_vars > RIGIDITY_MAX_VARS {
448        return false;
449    }
450    // Re-extract the parity system straight from the clauses.
451    let eqs = crate::lyapunov::extract_xor(cert.num_vars, clauses);
452    if eqs.is_empty() {
453        return false;
454    }
455    let rows = eqs_to_rows(&eqs, cert.num_vars);
456    // The recovered coefficient rows must match the certificate's.
457    let recovered: BTreeSet<u64> = rows.iter().copied().collect();
458    let claimed: BTreeSet<u64> = cert.rows.iter().copied().collect();
459    if recovered != claimed {
460        return false;
461    }
462    // Independently recompute the true rank / kernel dimension from the recovered rows.
463    let sol = match crate::gf2::solve_gf2(cert.num_vars, &rows, &vec![false; rows.len()]) {
464        Some(s) => s,
465        None => return false,
466    };
467    let true_kernel_dim = sol.kernel_basis.len();
468    if cert.rank != cert.num_vars - true_kernel_dim
469        || cert.solution_count_log2 as usize != true_kernel_dim
470        || cert.kernel_basis.len() != true_kernel_dim
471    {
472        return false;
473    }
474    // Every certificate basis vector must be a genuine homogeneous solution (rows·v = 0)…
475    for v in &cert.kernel_basis {
476        if v.len() != cert.num_vars || rows.iter().any(|&row| gf2_dot(row, v)) {
477            return false;
478        }
479    }
480    // …and the basis must be linearly independent (hence a complete null-space by its dimension).
481    independent_gf2(&cert.kernel_basis)
482}
483
484/// The par32-scale linear-structure certificate: the GF(2) rank and kernel dimension of `F`'s parity
485/// system, valid for **any** number of variables (beyond the `u64` cap of [`LinearRigidityCert`]).
486/// It carries no explicit kernel basis — at this scale the witness is *recomputation*: re-extract the
487/// parity system, re-reduce it, and confirm the same rank and kernel dimension. This is the object
488/// par32's "157-dim linearly-rigid kernel" measurement becomes.
489#[derive(Clone, Debug, PartialEq, Eq)]
490pub struct LinearStructureCert {
491    /// The number of Boolean variables `F` is over.
492    pub num_vars: usize,
493    /// The number of XOR (parity) equations recovered from `F`.
494    pub num_xor_eqs: usize,
495    /// The rank of the GF(2) system (independent parity constraints), from RREF reduction.
496    pub rank: usize,
497    /// The dimension of the linear choice space — the free variables occurring in the reduced matrix.
498    pub kernel_dim: usize,
499}
500
501/// Certify the GF(2) linear structure of `F` at any scale via the incremental engine, or `None` if
502/// there is no parity structure to characterize.
503pub fn certify_linear_structure(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<LinearStructureCert> {
504    let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
505    if eqs.is_empty() {
506        return None;
507    }
508    let inc = crate::xor_engine::IncXor::new(num_vars, &eqs);
509    Some(LinearStructureCert {
510        num_vars,
511        num_xor_eqs: eqs.len(),
512        rank: inc.rank(),
513        kernel_dim: inc.kernel_dim(),
514    })
515}
516
517/// Re-check a [`LinearStructureCert`] by re-extracting `F`'s parity system, re-reducing it, and
518/// confirming the certificate's rank, kernel dimension, and equation count are exactly reproduced.
519pub fn check_linear_structure(cert: &LinearStructureCert, clauses: &[Vec<Lit>]) -> bool {
520    let eqs = crate::lyapunov::extract_xor(cert.num_vars, clauses);
521    if eqs.len() != cert.num_xor_eqs {
522        return false;
523    }
524    let inc = crate::xor_engine::IncXor::new(cert.num_vars, &eqs);
525    inc.rank() == cert.rank && inc.kernel_dim() == cert.kernel_dim
526}
527
528/// The honest verdict on whether `F` admits a **linear (GF(2)) symmetry shortcut** — the certified,
529/// re-checkable "no shortcut of this class" answer the dispatcher and diagnostics can report instead
530/// of silently spinning a symmetry search. Class-relative (linear/parity), with the Chaitin ceiling
531/// as the documented frame: we certify the linear structure exactly, never claim an absolute bound.
532#[derive(Clone, Debug)]
533pub enum LinearShortcut {
534    /// `F`'s parity structure is fully exposed and rigid — no linear shortcut beyond the certified
535    /// `2^kernel_dim` solution space. The strongest available witness is attached: an explicit kernel
536    /// basis ([`LinearRigidityCert`]) when the system fits the `u64` budget, always the incremental
537    /// dimensions ([`LinearStructureCert`]).
538    None { rigidity: Option<LinearRigidityCert>, structure: LinearStructureCert },
539    /// `F` carries no parity structure at all — the linear class offers no leverage (and nothing to
540    /// break), so a linear-symmetry search would find nothing.
541    NoLinearStructure,
542}
543
544/// Decide the linear-shortcut verdict for `F` (fail-closed via [`certify_linear_structure`]).
545pub fn linear_shortcut_verdict(num_vars: usize, clauses: &[Vec<Lit>]) -> LinearShortcut {
546    match certify_linear_structure(num_vars, clauses) {
547        None => LinearShortcut::NoLinearStructure,
548        Some(structure) => {
549            let rigidity = certify_linear_rigidity(num_vars, clauses);
550            LinearShortcut::None { rigidity, structure }
551        }
552    }
553}
554
555/// The variable cap for the exact rigidity check in [`incompressibility_gate`]: the automorphism search
556/// is superpolynomial, so past this size the gate declines (conservative) rather than pay for it.
557const GATE_SYMMETRY_MAX_VARS: usize = 48;
558
559/// The SAT-dispatcher gate: `Some(cert)` iff `F`'s parity structure is fully exposed AND `F` is provably
560/// rigid (`|Aut| = 1`), so the symmetry arsenal is **provably useless** and the solver may go straight to
561/// CDCL with an honest "no shortcut of this class" verdict. The exact rigidity check
562/// (`symmetry_entropy_bits == 0`) is size-gated — past [`GATE_SYMMETRY_MAX_VARS`] the gate declines rather
563/// than run the superpolynomial automorphism search (there is no cheap sound global-asymmetry test; it is
564/// graph-isomorphism-hard). Fail-closed throughout: any doubt returns `None` and the arsenal runs as usual.
565pub fn incompressibility_gate(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<LinearStructureCert> {
566    let structure = certify_linear_structure(num_vars, clauses)?;
567    if num_vars > GATE_SYMMETRY_MAX_VARS {
568        return None; // conservative: the exact rigidity check would be too expensive at this size
569    }
570    if crate::hypercube::symmetry_entropy_bits(num_vars, clauses) == 0.0 {
571        Some(structure) // linear structure + |Aut| = 1 ⇒ genuinely no symmetry shortcut
572    } else {
573        None
574    }
575}
576
577/// Pack each XOR equation's variable set into a `u64` coefficient row.
578fn eqs_to_rows(eqs: &[crate::xorsat::XorEquation], num_vars: usize) -> Vec<u64> {
579    eqs.iter()
580        .map(|e| e.vars.iter().filter(|&&v| v < num_vars.min(64)).fold(0u64, |m, &v| m | (1u64 << v)))
581        .collect()
582}
583
584/// GF(2) dot product of a coefficient row (bitmask) with a solution vector: the parity of the
585/// selected entries.
586fn gf2_dot(row: u64, v: &[bool]) -> bool {
587    let mut acc = false;
588    let mut r = row;
589    while r != 0 {
590        let i = r.trailing_zeros() as usize;
591        if i < v.len() {
592            acc ^= v[i];
593        }
594        r &= r - 1;
595    }
596    acc
597}
598
599// ---- The incompressibility lemma (the lower-bound / "prove" direction) ----------------------
600//
601// The counting core of algorithmic information theory: over a binary alphabet there are `2ⁿ` strings
602// of length `n` but only `Σ_{i<n} 2ⁱ = 2ⁿ − 1` programs *shorter* than `n`. Fewer descriptions than
603// objects ⇒ by pigeonhole at least one length-`n` string has no shorter program: it is incompressible,
604// `K(x) ≥ n`. This is a *lower* bound proved by a shortage of descriptions — the incompressibility
605// method — and it is certified by the very same `O(1)` pigeonhole engine the SAT side already trusts.
606
607/// The **incompressibility lemma** for length `n`, as a re-checkable counting certificate: `2ⁿ`
608/// strings (pigeons) against `2ⁿ − 1` shorter programs (holes) ⇒ an incompressible string exists.
609/// `None` only when `n` is out of the exact-`u128` range (`1 ≤ n ≤ 127`). Re-check with
610/// [`crate::pigeonhole::check_counting_cert`].
611pub fn incompressible_string_exists(n: u32) -> Option<crate::pigeonhole::CountingCert> {
612    if n == 0 || n > 127 {
613        return None; // n=0 is the trivial K(ε) ≥ 0; beyond 127, 2ⁿ overflows u128
614    }
615    let strings: u128 = 1u128 << n;
616    let shorter_programs: u128 = strings - 1; // 2⁰ + … + 2ⁿ⁻¹ = 2ⁿ − 1
617    crate::pigeonhole::certify_pigeonhole_unsat(strings, shorter_programs)
618}
619
620/// A kernel-re-checkable counting certificate that an incompressible Boolean function on `n` variables MUST
621/// exist. A function is its `2ⁿ`-bit truth table, and there are only `2^{2ⁿ} − 1` programs shorter than
622/// `2ⁿ` bits — too few to name all `2^{2ⁿ}` functions — so at least one has no shorter description. This is
623/// the certified REASON the census residue is nonempty: [`boolean_function_census`] measures it, counting
624/// guarantees it. Any such function necessarily sits in the residue (no arsenal compresses the truly
625/// incompressible). `None` for `n = 0` or `n > 6` (a `2⁷`-bit truth table overflows the exact-`u128`
626/// counting range). Re-check with [`crate::pigeonhole::check_counting_cert`].
627pub fn certified_incompressible_function_exists(n: u32) -> Option<crate::pigeonhole::CountingCert> {
628    if n == 0 || n > 6 {
629        return None;
630    }
631    incompressible_string_exists(1u32 << n)
632}
633
634// ---- Incompressibility as a cryptographic diagnostic ----------------------------------------
635//
636// A key or ciphertext must be *incompressible*: if the description engine finds a shorter program
637// for it, THAT program is the attack — a short description means the data is predictable (low
638// Kolmogorov complexity). This is the algorithmic-information view of "looks random". It is honest
639// and one-sided: by the Chaitin ceiling we can certify WEAKNESS (exhibit a compression witness that
640// re-decodes to the data) but never absolute strength — incompressibility here is NECESSARY for a
641// secure key/ciphertext, not sufficient, and only relative to this fixed generator class. (Finding
642// the shortest *linear* recurrence is exactly the classic LFSR / Berlekamp–Massey attack.)
643
644/// The algorithmic-information verdict on key or ciphertext material (a byte string).
645#[derive(Clone, Debug)]
646pub enum CryptoStrength {
647    /// A description shorter than the raw bytes exists — a **certified structural weakness**. `witness`
648    /// decodes back to the data (the concrete attack), and `ratio` = K̄/n < 1 quantifies predictability.
649    Weak { witness: DescriptionBound, ratio: f64 },
650    /// No description beats storing the raw bytes — incompressible relative to the engine's generator
651    /// class. A necessary (not sufficient) condition for cryptographic randomness; no weakness of this
652    /// class. (`ratio ≥ 1`.)
653    IncompressibleInClass { ratio: f64 },
654}
655
656/// The incompressibility ratio `K̄(x)/n` for an `n`-byte string — the shortest menu description
657/// measured against storing the bytes raw (the incompressible size for byte material). ≈ 1.0 ⇒
658/// incompressible (no exploitable structure of this class); well below 1.0 ⇒ compressible (a short,
659/// predictable description exists).
660pub fn incompressibility_ratio(data: &[u8]) -> f64 {
661    if data.is_empty() {
662        return 1.0;
663    }
664    let ints: Vec<i64> = data.iter().map(|&b| b as i64).collect();
665    describe::describe_int_seq(&ints).len() as f64 / data.len() as f64
666}
667
668/// The **GF(2⁸) word linear complexity** of a byte string — the length of the shortest *byte-oriented*
669/// (GF(256)) LFSR that generates it, via Berlekamp–Massey over the AES field. The word analogue of the
670/// bit linear complexity: low relative to `n/2` ⇒ a word-LFSR keystream, a certified weakness. (Runs in
671/// `O(n²)` byte-ops — 64× fewer than the bit-level BM on the same data.)
672pub fn gf256_word_complexity(data: &[u8]) -> usize {
673    let elems: Vec<describe::Gf256> = data.iter().map(|&b| describe::Gf256(b)).collect();
674    describe::berlekamp_massey_field(&elems).0
675}
676
677/// The **2-adic complexity** of a byte string (its LSB-first bit expansion) — the size of the shortest
678/// FCSR (feedback-with-carry / add-with-carry) generating it. Low relative to the bit count ⇒ a
679/// carry-based keystream that **fools every linear-complexity test** (Berlekamp–Massey over any field
680/// sees high complexity; the carry is nonlinear over GF(2)). The certified weakness the linear tools miss.
681pub fn two_adic_complexity_of_bytes(data: &[u8]) -> usize {
682    let bits: Vec<bool> = data.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
683    describe::two_adic_complexity(&bits)
684}
685
686/// The **maximal order complexity** of a byte string (its LSB-first bit expansion) — the length of the
687/// shortest feedback register, LINEAR OR NONLINEAR, generating it. The TOP of the FSR hierarchy: it
688/// catches nonlinear generators (NFSRs, algebraic combiners) that fool every linear-complexity measure.
689/// Low relative to the bit count ⇒ a short-register generator. This is the last cheap rung — a general
690/// nonlinear feedback function is a full truth table (as large as the data), so a low MOC is a certified
691/// STRUCTURAL weakness (a short register exists) even though recovering its *sparse* form is the (hard)
692/// algebraic attack. For a real cipher MOC `≈ n/2`: the incompressible residue, the Chaitin ceiling.
693pub fn maximal_order_complexity_of_bytes(data: &[u8]) -> usize {
694    let bits: Vec<bool> = data.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
695    describe::maximal_order_complexity(&bits)
696}
697
698/// The certified result of an **algebraic-recurrence attack**: a low-degree nonlinear feedback register
699/// that regenerates a byte string, recovered by [`describe::detect_algebraic_recurrence`]. This is the
700/// OPEN rung — where maximal order complexity can only *measure* a nonlinear register (its `2^order`
701/// truth table), the algebraic attack *recovers* it as a sparse ANF and thereby *compresses* it, when
702/// the feedback has low degree. The `anf` is a re-checkable witness (replay it with the seed).
703#[derive(Clone, Debug, PartialEq, Eq)]
704pub struct AlgebraicAttack {
705    /// The register length `L` — how many past bits the feedback reads.
706    pub order: usize,
707    /// The ANF degree bound `d` the recovery used.
708    pub degree: usize,
709    /// The number of active ANF coefficients — the description size, `O(Lᵈ)`.
710    pub anf_terms: usize,
711    /// The recovered ANF over `monomials(order, degree)` — the witness (`algebraic_generate` replays it).
712    pub anf: Vec<bool>,
713    /// `2^order` — the truth-table size maximal order complexity would need for the same register.
714    pub truth_table: usize,
715}
716
717/// Run the algebraic attack on `data` (its LSB-first bit expansion) at maximal ANF degree `max_degree`,
718/// searching register orders `1..=max_order`: return the shortest low-degree nonlinear feedback that
719/// regenerates the whole bit stream, with its sparse ANF. `None` when no degree-≤`max_degree` register
720/// up to `max_order` fits — the genuine high-degree / incompressible residue (a real cipher's keystream,
721/// the Chaitin ceiling). `max_order` bounds the cost: the solve is `O(rows · M²/64)` with `M = O(Lᵈ)`.
722pub fn algebraic_attack_on_bytes(data: &[u8], max_degree: usize, max_order: usize) -> Option<AlgebraicAttack> {
723    let bits: Vec<bool> = data.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
724    let cap = max_order.min(bits.len() / 2);
725    for l in 1..=cap {
726        if let Some(anf) = describe::detect_algebraic_recurrence(&bits, l, max_degree) {
727            return Some(AlgebraicAttack {
728                order: l,
729                degree: max_degree,
730                anf_terms: anf.iter().filter(|&&c| c).count().max(1),
731                anf,
732                truth_table: 1usize.checked_shl(l as u32).unwrap_or(usize::MAX),
733            });
734        }
735    }
736    None
737}
738
739/// A certified **combiner leak**: one candidate LFSR that a keystream measurably correlates with,
740/// recovered by [`describe::correlation_attack`]. The `attack.init_state` is the re-checkable witness.
741#[derive(Clone, Debug, PartialEq)]
742pub struct CombinerLeak {
743    /// Index into the candidate menu of the leaking register.
744    pub candidate_index: usize,
745    /// The recovered register (initial state + correlation edge).
746    pub attack: describe::CorrelationAttack,
747    /// The spurious-bias floor for this register length and sample count.
748    pub floor: f64,
749    /// `bias / floor` — how far above the noise the correlation sits (the significance).
750    pub margin: f64,
751}
752
753/// Scan a byte keystream against a menu of candidate LFSR feedback tap-sets: return every register the
754/// keystream correlates with beyond `significance ×` the spurious floor. Each hit is a certified break of
755/// a nonlinear **combiner** generator — a HIDDEN constituent register recovered *independently*
756/// (Siegenthaler divide-and-conquer), collapsing a `2^(Σ Lⱼ)` search to `Σ 2^Lⱼ`. This reaches the
757/// combiners the algebraic-recurrence rung structurally cannot (their output is a function of the hidden
758/// register outputs, not of the keystream's own past). Empty ⇒ no first-order correlation with any
759/// candidate: correlation-immune, or not this combiner — the ceiling, where higher-order and
760/// fast-correlation attacks take over.
761pub fn scan_for_combiner_leaks(keystream: &[u8], candidates: &[Vec<bool>], significance: f64) -> Vec<CombinerLeak> {
762    let bits: Vec<bool> = keystream.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
763    let mut leaks = Vec::new();
764    for (i, taps) in candidates.iter().enumerate() {
765        let Some(attack) = describe::correlation_attack(&bits, taps) else {
766            continue;
767        };
768        let floor = describe::spurious_bias_floor(taps.len(), attack.samples);
769        if floor > 0.0 && attack.bias > significance * floor {
770            leaks.push(CombinerLeak { candidate_index: i, margin: attack.bias / floor, floor, attack });
771        }
772    }
773    leaks
774}
775
776/// Fast correlation attack (Meier–Staffelbach): recover a leaking LFSR's initial state from a noisy
777/// keystream by DECODING the register-as-linear-code, in time polynomial in `L` — where the exhaustive
778/// correlation attack ([`scan_for_combiner_leaks`], Rung E) needs `O(2^L)`. This is what scales the
779/// correlation break to the register lengths real ciphers use. See [`describe::fast_correlation_attack`].
780pub fn fast_correlation_attack(keystream: &[bool], taps: &[bool], max_iters: usize) -> Option<Vec<bool>> {
781    describe::fast_correlation_attack(keystream, taps, max_iters)
782}
783
784/// Break a shrinking (clock-controlled) generator: recover both register states from the output alone by
785/// guessing the clock register and linear-solving the data register. Reaches a generator whose output is
786/// a data-dependent decimation — not a fixed function of any register's own past — that no feedback,
787/// correlation, algebraic, or linear rung can touch. See [`describe::attack_shrinking_generator`].
788pub fn attack_shrinking_generator(output: &[bool], a_taps: &[bool], s_taps: &[bool]) -> Option<(Vec<bool>, Vec<bool>)> {
789    describe::attack_shrinking_generator(output, a_taps, s_taps)
790}
791
792// ---- The auto-lens-finder: a portfolio meta-dispatcher that forces every lens to declare itself -------
793//
794// The whole campaign is a menu of LENSES, each compressing (and thereby breaking) a different structured
795// family. The covering question is: run them ALL against an object and see which one — if any — fires.
796// The union of the lenses covers the union of their families; whatever no lens compresses is the
797// incompressible residue relative to this arsenal — the operational Chaitin ceiling. No arrangement can
798// cover the WHOLE space (counting says most sequences are incompressible, and Chaitin forbids certifying
799// which), but we can cover the structured families and MEASURE the hole. Lenses are tried cheapest-first.
800
801/// One lens's verdict on a sequence: the number of bits it needs to DESCRIBE the sequence (`usize::MAX`
802/// if the lens finds no exploitable structure). Lower ⇒ the lens compresses it more.
803#[derive(Clone, Debug, PartialEq, Eq)]
804pub struct LensCoverage {
805    pub lens: &'static str,
806    pub description_bits: usize,
807}
808
809/// The result of running the whole lens portfolio against a bit sequence.
810#[derive(Clone, Debug)]
811pub struct LensReport {
812    pub length_bits: usize,
813    /// Every lens's description-length verdict (the covering menu, laid out).
814    pub coverage: Vec<LensCoverage>,
815    /// The lens achieving the shortest description — the one that "covers" the sequence.
816    pub best_lens: &'static str,
817    pub best_description_bits: usize,
818    /// Whether the best lens compresses by at least 2× — i.e. the sequence is genuinely covered, not
819    /// in the incompressible residue.
820    pub covered: bool,
821}
822
823/// The **auto-lens-finder**: run every sequence lens in the arsenal against `bits` and report which one
824/// compresses it (covers it) and by how much — or that none do, placing it in the incompressible residue.
825/// This makes the covering explicit: each structured family is caught by its own lens, and a
826/// cryptographically-random sequence falls through all of them (the ceiling). Lenses are ordered
827/// cheapest-first so a covered sequence is recognized quickly.
828pub fn lens_report(bits: &[bool]) -> LensReport {
829    let n = bits.len();
830    let mut coverage = Vec::new();
831
832    // Linear (LFSR): a length-L register describes it in ~2L bits (taps + seed).
833    let lc = describe::berlekamp_massey_gf2(bits).0;
834    coverage.push(LensCoverage { lens: "linear (LFSR / Berlekamp–Massey)", description_bits: lc.saturating_mul(2) });
835
836    // 2-adic (FCSR): a carry register of complexity c describes it in ~2c bits.
837    let tc = describe::two_adic_complexity(bits);
838    coverage.push(LensCoverage { lens: "2-adic (FCSR)", description_bits: tc.saturating_mul(2) });
839
840    // Maximal-order: a nonlinear register of order L, but its feedback is a 2^L truth table — it only
841    // COMPRESSES when the register is tiny (otherwise it merely measures).
842    let moc = describe::maximal_order_complexity(bits);
843    let moc_bits = if moc < 20 { (1usize << moc).saturating_add(moc) } else { usize::MAX };
844    coverage.push(LensCoverage { lens: "maximal-order (nonlinear FSR)", description_bits: moc_bits });
845
846    // Algebraic recurrence (degree ≤ 2): a low-degree nonlinear feedback, sparse ANF (bounded order search).
847    let alg = (1..=12)
848        .find_map(|l| describe::detect_algebraic_recurrence(bits, l, 2).map(|c| l + c.iter().filter(|&&b| b).count()));
849    coverage.push(LensCoverage { lens: "algebraic-recurrence (deg-2 ANF)", description_bits: alg.unwrap_or(usize::MAX) });
850
851    // MDL codec menu: affine / geometric / polynomial / periodic / sparse / RLE / … over the byte packing.
852    let bytes = describe::bits_to_bytes(bits);
853    let enc = describe::describe_int_seq(&bytes).len().saturating_mul(8);
854    coverage.push(LensCoverage { lens: "MDL codec menu", description_bits: enc });
855
856    let best = coverage.iter().min_by_key(|c| c.description_bits).expect("nonempty");
857    let covered = best.description_bits.saturating_mul(2) < n;
858    LensReport {
859        length_bits: n,
860        best_lens: best.lens,
861        best_description_bits: best.description_bits,
862        covered,
863        coverage,
864    }
865}
866
867/// A coverage census over a corpus of sequences: how many the lens arsenal covers, how many fall in the
868/// uncovered residue, and which lens covered how many. This is the covering problem made countable.
869#[derive(Clone, Debug)]
870pub struct CoverageCensus {
871    pub total: usize,
872    /// Covered by some lens (compressed ≥ 2×).
873    pub covered: usize,
874    /// The incompressible residue — covered by nothing in the arsenal.
875    pub uncovered: usize,
876    /// How many sequences each lens was the best cover for.
877    pub by_lens: Vec<(&'static str, usize)>,
878}
879
880/// Census a corpus of bit sequences through the [`lens_report`] portfolio: tally what each lens covers
881/// and what lands in the uncovered residue.
882pub fn census(corpus: &[Vec<bool>]) -> CoverageCensus {
883    let mut counts: std::collections::BTreeMap<&'static str, usize> = std::collections::BTreeMap::new();
884    let mut covered = 0;
885    for seq in corpus {
886        let r = lens_report(seq);
887        if r.covered {
888            covered += 1;
889            *counts.entry(r.best_lens).or_insert(0) += 1;
890        }
891    }
892    CoverageCensus {
893        total: corpus.len(),
894        covered,
895        uncovered: corpus.len() - covered,
896        by_lens: counts.into_iter().collect(),
897    }
898}
899
900/// Exhaustively census EVERY length-`len` bit sequence (`2^len` of them): what fraction of the WHOLE
901/// space does the lens arsenal cover? The answer is a small sliver — most sequences are incompressible,
902/// the residue — which is the concrete, countable face of the Chaitin ceiling. The structured families
903/// our lenses catch are real and important, but they are a vanishing fraction of the space; you cannot
904/// arrange lenses to cover it all (counting forbids it), and you cannot even certify which points are
905/// uncovered (Chaitin forbids that). `len ≤ ~18` to stay tractable.
906pub fn exhaustive_coverage(len: usize) -> CoverageCensus {
907    let corpus: Vec<Vec<bool>> =
908        (0u64..(1u64 << len)).map(|code| (0..len).map(|i| (code >> i) & 1 == 1).collect()).collect();
909    census(&corpus)
910}
911
912/// The trace of recursively symmetry-breaking an object down to its incompressible core.
913#[derive(Clone, Debug)]
914pub struct RecursiveReduction {
915    /// The size (bytes) at each recursion level — level 0 is the input, the last is the fixed point.
916    pub sizes: Vec<usize>,
917    /// How many symmetry breaks (compressions) before the fixed point.
918    pub depth: usize,
919    /// The size of the irreducible core: what the arsenal cannot reduce further.
920    pub irreducible_bytes: usize,
921    /// Whether any reduction happened at all.
922    pub compressed: bool,
923}
924
925/// **Recursively symmetry-break** an object until nothing reduces it further. Each level applies the
926/// compression lens (the MDL description menu) and recurses on the DESCRIPTION — each compression is a
927/// symmetry break, and its output becomes the next object. The recursion terminates at the FIXED POINT:
928/// the point where no lens shrinks it, the incompressible core.
929///
930/// The terminus is `no lens fires` — which is *"irreducible by this arsenal"*, and is NOT, and can never
931/// be, *"structureless."* Proving no structure exists would be certifying a Kolmogorov lower bound
932/// `K(x) > c`, which the kernel-certified Chaitin theorem in this module says a bounded system can do for
933/// no object past its budget. So this function computes the fixed point; the thing it can never output is
934/// a proof that the fixed point has no structure.
935pub fn recursive_reduce(bytes: &[u8]) -> RecursiveReduction {
936    let mut cur: Vec<i64> = bytes.iter().map(|&b| b as i64).collect();
937    let mut sizes = vec![cur.len()];
938    loop {
939        let enc = describe::describe_int_seq(&cur);
940        if enc.len() >= cur.len() {
941            break; // no lens shrinks it — the incompressible fixed point (relative to the arsenal)
942        }
943        cur = enc.iter().map(|&b| b as i64).collect();
944        sizes.push(cur.len());
945    }
946    RecursiveReduction {
947        depth: sizes.len() - 1,
948        irreducible_bytes: *sizes.last().expect("nonempty"),
949        compressed: sizes.len() > 1,
950        sizes,
951    }
952}
953
954/// The linear-cryptanalytic profile of a Boolean combining/filter function, from its Walsh spectrum: the
955/// best linear approximation (the distinguisher), its nonlinearity, and its correlation-immunity order.
956#[derive(Clone, Debug, PartialEq)]
957pub struct LinearDistinguisher {
958    /// The linear mask `w` of the best approximation `z ≈ ⟨w, inputs⟩`.
959    pub mask: usize,
960    /// The bias `|Pr[C=⟨w,x⟩] − ½|` of that approximation — the distinguishing advantage.
961    pub bias: f64,
962    /// The Hamming weight of `mask`: how many registers the approximation combines (weight 1 = Rung E).
963    pub mask_weight: u32,
964    /// Distance to the nearest affine function; maximal ⇒ bent ⇒ no exploitable linear approximation.
965    pub nonlinearity: u64,
966    /// The correlation-immunity order — how many registers the first-order correlation attack must miss.
967    pub immunity_order: usize,
968}
969
970/// The linear-cryptanalysis profile of a combining/filter function given as its `2ⁿ` truth table: the
971/// whole Walsh spectrum distilled into the best linear approximation, its nonlinearity, and its
972/// correlation-immunity order. Where [`scan_for_combiner_leaks`] (Rung E) reads only weight-1 masks, this
973/// reads them ALL — surfacing the multi-register approximation (`mask_weight ≥ 2`) that E is blind to,
974/// even on a first-order correlation-immune function. `None` for a malformed table.
975pub fn linear_cryptanalysis(truth: &[bool]) -> Option<LinearDistinguisher> {
976    let (mask, bias) = describe::best_linear_approximation(truth, true)?;
977    Some(LinearDistinguisher {
978        mask,
979        bias,
980        mask_weight: mask.count_ones(),
981        nonlinearity: describe::nonlinearity(truth)?,
982        immunity_order: describe::correlation_immunity_order(truth)?,
983    })
984}
985
986/// The algebraic-immunity profile of a filter/combining function: the minimum degree of an annihilator
987/// (the algebraic attack's leverage), the maximum possible `⌈n/2⌉`, and whether it sits at that ceiling.
988#[derive(Clone, Debug, PartialEq, Eq)]
989pub struct AlgebraicImmunityReport {
990    /// `AI(C)` — the minimum degree of a nonzero annihilator of `C` or `C ⊕ 1`.
991    pub immunity: usize,
992    /// `⌈n/2⌉` — the maximum algebraic immunity any `n`-variable function can have.
993    pub max_possible: usize,
994    /// A re-checkable minimum-degree annihilator (the attack's leverage).
995    pub witness: describe::AnnihilatorWitness,
996    /// `AI(C) == ⌈n/2⌉` — maximal algebraic immunity, the algebraic-incompressible residue (the ceiling).
997    pub is_maximal: bool,
998}
999
1000/// The algebraic-immunity profile of a Boolean function given as its `2ⁿ` truth table. Low immunity is a
1001/// certified structural weakness: a degree-`AI` annihilator turns each keystream bit of a filter
1002/// generator using `C` into a degree-`AI` equation in the secret state ([`algebraic_filter_attack`]).
1003/// Maximal immunity (`AI = ⌈n/2⌉`) is the ceiling — no low-degree relation to exploit. `None` for a
1004/// malformed table.
1005pub fn algebraic_immunity_of(truth: &[bool]) -> Option<AlgebraicImmunityReport> {
1006    let (immunity, witness) = describe::algebraic_immunity(truth)?;
1007    let n = truth.len().trailing_zeros() as usize;
1008    let max_possible = n.div_ceil(2);
1009    Some(AlgebraicImmunityReport { immunity, max_possible, witness, is_maximal: immunity == max_possible })
1010}
1011
1012/// Recover the initial state of a filter generator (a length-`L` LFSR with feedback `taps` filtered by
1013/// `filter_truth`) via the algebraic attack — the certified break of a target the correlation and Walsh
1014/// rungs only glimpse statistically. See [`describe::algebraic_filter_attack`]. Returns the secret
1015/// initial state (verified by regeneration) or `None`.
1016pub fn algebraic_filter_attack(keystream: &[bool], taps: &[bool], filter_truth: &[bool]) -> Option<Vec<bool>> {
1017    describe::algebraic_filter_attack(keystream, taps, filter_truth)
1018}
1019
1020// ---- The hypercube structure finder: classify a Boolean function by walking its cube -----------------
1021//
1022// A length-`2ⁿ` truth table IS a labeling of the corners of the `n`-cube. The structure finder walks that
1023// cube through each structural lens — each lens is a genuine traversal of the cube along one axis of
1024// structure — and reports the TIGHTEST class that describes the whole function in far fewer than `2ⁿ`
1025// bits, with a re-checkable decode witness:
1026//
1027//   • Constant       — every corner equal (1 bit).                      walk: read the corners.
1028//   • Junta(k)       — only k of n variables ever change the output.    walk: probe each coordinate edge.
1029//   • Affine         — degree ≤ 1: f(x) = ⊕ aᵢxᵢ ⊕ c (n+1 bits).        walk: the Walsh–Hadamard butterfly.
1030//   • Symmetric      — depends only on Hamming weight (n+1 bits).        walk: the weight shells.
1031//   • LowDegree(d)   — a sparse ANF: few monomials describe it.         walk: the Möbius butterfly.
1032//   • ResistedArsenal— dense high-degree ANF, all vars, high nonlin.    the residue: no lens fires.
1033//
1034// The residue is `irreducible by this arsenal`, NOT `structureless` — the Chaitin ceiling (this module)
1035// forbids certifying the latter. It is exactly the class that indistinguishability obfuscation must live
1036// in: a function whose structure NO adversary lens reads off. The 2001 impossibility of *ideal*
1037// obfuscation is the cryptographic cousin of Chaitin — the code always reveals at least itself — which is
1038// why iO settles for *indistinguishability*, hiding which structured program you hold inside the residue.
1039
1040/// The compressibility class of a Boolean function on the hypercube, tightest description first. Each
1041/// variant carries a re-checkable witness ([`CubeStructure::reconstruct`]) except the residue.
1042#[derive(Clone, Debug, PartialEq, Eq)]
1043pub enum CubeStructure {
1044    /// Every corner carries the same value — the whole cube in one bit.
1045    Constant(bool),
1046    /// A junta: the output depends only on `relevant` (a strict subset of the variables); `subtable` is
1047    /// its `2^{|relevant|}` truth table over those variables (indexed LSB-first in `relevant` order).
1048    Junta { relevant: Vec<usize>, subtable: Vec<bool> },
1049    /// Affine (degree ≤ 1): `f(x) = ⊕ᵢ coeffs[i]·xᵢ ⊕ constant`.
1050    Affine { coeffs: Vec<bool>, constant: bool },
1051    /// Symmetric: the output depends only on the Hamming weight of the input; `by_weight[w]` is its value
1052    /// on every corner of weight `w` (length `n+1`).
1053    Symmetric { by_weight: Vec<bool> },
1054    /// Low algebraic degree with a sparse ANF: `anf` is the `2ⁿ` Möbius-transform witness, `degree` its top.
1055    LowDegree { degree: usize, anf: Vec<bool> },
1056    /// No lens compresses it: dense high-degree ANF, all variables relevant, high nonlinearity. The
1057    /// incompressible residue relative to this arsenal — NOT a proof of structurelessness (Chaitin).
1058    ResistedArsenal { nonlinearity: u64, degree: usize },
1059}
1060
1061impl CubeStructure {
1062    /// Rebuild the `2ⁿ` truth table from this description — the re-checkable decode witness. `None` for the
1063    /// residue (which carries no compressed description) or a malformed witness.
1064    pub fn reconstruct(&self, n: usize) -> Option<Vec<bool>> {
1065        let size = 1usize << n;
1066        match self {
1067            CubeStructure::Constant(b) => Some(vec![*b; size]),
1068            CubeStructure::Junta { relevant, subtable } => {
1069                if subtable.len() != 1usize << relevant.len() {
1070                    return None;
1071                }
1072                Some(
1073                    (0..size)
1074                        .map(|x| {
1075                            let c = relevant.iter().enumerate().fold(0usize, |acc, (bit, &v)| {
1076                                if x & (1 << v) != 0 { acc | (1 << bit) } else { acc }
1077                            });
1078                            subtable[c]
1079                        })
1080                        .collect(),
1081                )
1082            }
1083            CubeStructure::Affine { coeffs, constant } => {
1084                if coeffs.len() != n {
1085                    return None;
1086                }
1087                Some(
1088                    (0..size)
1089                        .map(|x| {
1090                            coeffs.iter().enumerate().fold(*constant, |acc, (i, &a)| acc ^ (a && (x & (1 << i) != 0)))
1091                        })
1092                        .collect(),
1093                )
1094            }
1095            CubeStructure::Symmetric { by_weight } => {
1096                if by_weight.len() != n + 1 {
1097                    return None;
1098                }
1099                Some((0..size).map(|x| by_weight[(x as u64).count_ones() as usize]).collect())
1100            }
1101            // The ANF↔truth Möbius transform is its own inverse over GF(2).
1102            CubeStructure::LowDegree { anf, .. } => {
1103                if anf.len() != size {
1104                    return None;
1105                }
1106                describe::anf(anf)
1107            }
1108            CubeStructure::ResistedArsenal { .. } => None,
1109        }
1110    }
1111}
1112
1113/// The verdict of the hypercube structure finder: the tightest structural class, its description size in
1114/// bits, and how that compares to the raw `2ⁿ`-bit truth table.
1115#[derive(Clone, Debug)]
1116pub struct StructureReport {
1117    pub num_vars: usize,
1118    pub class: CubeStructure,
1119    /// `2ⁿ` — the bits to store the truth table corner-by-corner.
1120    pub raw_bits: usize,
1121    /// The bits of the tightest structural description found.
1122    pub description_bits: usize,
1123    /// Whether any lens beat storing the raw truth table.
1124    pub compressed: bool,
1125}
1126
1127/// Bits needed to name one of `n` variables.
1128fn var_index_bits(n: usize) -> usize {
1129    if n <= 1 { 1 } else { (usize::BITS - (n - 1).leading_zeros()) as usize }
1130}
1131
1132/// `Σ_{k=0}^{d} C(n,k)` — the number of ANF coefficient bits for all monomials of degree `≤ d`, i.e. the
1133/// DENSE encoding size of a degree-`d` function (vs the sparse `weight · n` monomial list).
1134fn partial_binomial_sum(n: usize, d: usize) -> usize {
1135    let mut sum = 0usize;
1136    let mut c = 1usize;
1137    for k in 0..=d.min(n) {
1138        sum = sum.saturating_add(c);
1139        c = c.saturating_mul(n - k) / (k + 1);
1140    }
1141    sum
1142}
1143
1144/// If the function depends only on Hamming weight, its value on each weight shell `0..=n`; else `None`.
1145fn symmetric_profile(truth: &[bool], n: usize) -> Option<Vec<bool>> {
1146    let mut by_weight: Vec<Option<bool>> = vec![None; n + 1];
1147    for (x, &val) in truth.iter().enumerate() {
1148        let w = (x as u64).count_ones() as usize;
1149        match by_weight[w] {
1150            None => by_weight[w] = Some(val),
1151            Some(v) if v != val => return None,
1152            _ => {}
1153        }
1154    }
1155    Some(by_weight.into_iter().map(|o| o.unwrap_or(false)).collect())
1156}
1157
1158/// **Walk the hypercube** of a Boolean function (its `2ⁿ` truth table) and return the tightest structural
1159/// class that describes it, with the achieved description size and a re-checkable witness. Every lens is a
1160/// traversal of the cube along one axis of structure (coordinate edges, Walsh butterfly, weight shells,
1161/// Möbius butterfly); the finder reports whichever yields the shortest description, or the residue
1162/// ([`CubeStructure::ResistedArsenal`]) when none beats storing the table raw. `None` if `truth.len()` is
1163/// not a power of two.
1164pub fn find_structure(truth: &[bool]) -> Option<StructureReport> {
1165    if truth.is_empty() || !truth.len().is_power_of_two() {
1166        return None;
1167    }
1168    let n = truth.len().trailing_zeros() as usize;
1169    let raw = truth.len();
1170    let mut cands: Vec<(CubeStructure, usize)> = Vec::new();
1171
1172    // Constant — the tightest lens: the whole cube in one bit.
1173    if truth.iter().all(|&b| b == truth[0]) {
1174        cands.push((CubeStructure::Constant(truth[0]), 1));
1175    }
1176
1177    // Junta — probe each coordinate edge: a variable is relevant iff flipping it ever changes the output.
1178    let relevant: Vec<usize> =
1179        (0..n).filter(|&i| (0..truth.len()).any(|x| truth[x] != truth[x ^ (1 << i)])).collect();
1180    if !relevant.is_empty() && relevant.len() < n {
1181        let k = relevant.len();
1182        let subtable: Vec<bool> = (0..1usize << k)
1183            .map(|c| {
1184                let x = relevant.iter().enumerate().fold(0usize, |acc, (bit, &v)| {
1185                    if c & (1 << bit) != 0 { acc | (1 << v) } else { acc }
1186                });
1187                truth[x]
1188            })
1189            .collect();
1190        let bits = k * var_index_bits(n) + (1usize << k);
1191        cands.push((CubeStructure::Junta { relevant, subtable }, bits));
1192    }
1193
1194    // Affine — the Walsh butterfly: nonlinearity 0 ⇔ degree ≤ 1. Read coeffs by walking the n axes.
1195    if describe::nonlinearity(truth) == Some(0) {
1196        let constant = truth[0];
1197        let coeffs: Vec<bool> = (0..n).map(|i| truth[1 << i] ^ constant).collect();
1198        cands.push((CubeStructure::Affine { coeffs, constant }, n + 1));
1199    }
1200
1201    // Symmetric — the weight shells: the output depends only on Hamming weight.
1202    if let Some(by_weight) = symmetric_profile(truth, n) {
1203        cands.push((CubeStructure::Symmetric { by_weight }, n + 1));
1204    }
1205
1206    // Low degree — the Möbius butterfly. Account the TIGHTER of the sparse monomial list (`weight · n`) and
1207    // the dense coefficient vector (`Σ_{k≤deg} C(n,k)`), so a DENSE bounded-degree function compresses too.
1208    if let Some(anf) = describe::anf(truth) {
1209        let weight = anf.iter().filter(|&&c| c).count();
1210        let degree =
1211            anf.iter().enumerate().filter(|(_, &c)| c).map(|(m, _)| (m as u64).count_ones() as usize).max().unwrap_or(0);
1212        // The dense coefficient vector only counts for genuinely bounded degree (`≤ n−2`); at degree `n−1`
1213        // it would save a single trivial bit and swallow the honest residue.
1214        let dense = if degree + 2 <= n { partial_binomial_sum(n, degree) } else { usize::MAX };
1215        let bits = weight.saturating_mul(n).min(dense).max(1);
1216        cands.push((CubeStructure::LowDegree { degree, anf }, bits));
1217    }
1218
1219    let (class, description_bits) = cands
1220        .into_iter()
1221        .min_by_key(|(_, b)| *b)
1222        .filter(|(_, b)| *b < raw)
1223        .unwrap_or_else(|| {
1224            let degree = describe::algebraic_degree(truth).unwrap_or(n);
1225            let nonlinearity = describe::nonlinearity(truth).unwrap_or(0);
1226            (CubeStructure::ResistedArsenal { nonlinearity, degree }, raw)
1227        });
1228
1229    Some(StructureReport { num_vars: n, compressed: description_bits < raw, description_bits, raw_bits: raw, class })
1230}
1231
1232/// A **cover of the cube by structural class**: the ANF degree stratification of a Boolean function. Every
1233/// function is a XOR of monomials `∏_{i∈S} xᵢ`, and each monomial is a structural class — its degree `|S|`
1234/// is the order of variable interaction it encodes. Peeling low-degree slices (constant, linear,
1235/// quadratic, …) covers most of the `2ⁿ` corners with a handful of terms; what remains is the residue: the
1236/// high-degree core no lower-order slice explains. This is why the peel is more efficient than walking the
1237/// cube corner by corner — each class-slice accounts for many corners at once, and you reason about the
1238/// small residue directly instead of labeling all `2ⁿ` of them.
1239#[derive(Clone, Debug)]
1240pub struct StructureCover {
1241    pub num_vars: usize,
1242    /// `monomials_by_degree[d]` = ANF monomials of degree exactly `d` — the size of the degree-`d` slice.
1243    pub monomials_by_degree: Vec<usize>,
1244    /// The total ANF monomials across all slices — the whole description in monomials.
1245    pub total_monomials: usize,
1246    /// The highest degree present: the residue's interaction order (0 if the function is a constant).
1247    pub residue_degree: usize,
1248    /// The number of monomials at the residue degree — the irreducible high-degree core.
1249    pub residue_monomials: usize,
1250    /// Bits to store the ANF as monomial indices (`total · n`), against `2ⁿ` raw.
1251    pub description_bits: usize,
1252    pub raw_bits: usize,
1253    /// Whether the peel is a genuine compression — a sparse ANF beats storing the truth table.
1254    pub compressed: bool,
1255}
1256
1257/// **Peel the cube apart by structural class** and examine the residue: return the ANF degree
1258/// stratification of a Boolean function (its `2ⁿ` truth table). Each degree is a slice of structure; the
1259/// top nonempty degree is the residue — the interaction order that survives every lower-order peel, and
1260/// the honest answer to *what remains and why*. `None` if `truth.len()` is not a power of two.
1261pub fn structure_cover(truth: &[bool]) -> Option<StructureCover> {
1262    let anf = describe::anf(truth)?;
1263    let n = truth.len().trailing_zeros() as usize;
1264    let mut by_degree = vec![0usize; n + 1];
1265    for (m, &c) in anf.iter().enumerate() {
1266        if c {
1267            by_degree[(m as u64).count_ones() as usize] += 1;
1268        }
1269    }
1270    let total: usize = by_degree.iter().sum();
1271    let residue_degree = by_degree.iter().rposition(|&c| c > 0).unwrap_or(0);
1272    let residue_monomials = by_degree[residue_degree];
1273    let raw = truth.len();
1274    let description_bits = if total == 0 { 1 } else { total * n };
1275    Some(StructureCover {
1276        num_vars: n,
1277        compressed: description_bits < raw,
1278        monomials_by_degree: by_degree,
1279        total_monomials: total,
1280        residue_degree,
1281        residue_monomials,
1282        description_bits,
1283        raw_bits: raw,
1284    })
1285}
1286
1287// ---- Linear structures: the derivative symmetry the coordinate lenses miss ---------------------------
1288//
1289// The lenses above read structure in the GIVEN coordinates. But a function can be simple in a ROTATED
1290// basis and look dense here: a rotated junta is invariant under some direction `a` that is not an axis, so
1291// the coordinate walk sees every variable as relevant and the finder calls it residue. The autocorrelation
1292// exposes exactly this: `a` is a LINEAR STRUCTURE when `f(x⊕a) ⊕ f(x)` is constant (`|r_f(a)| = 2ⁿ`). The
1293// set of linear structures forms a GF(2) subspace `V(f)`, and `dim V(f) = k > 0` means `f` collapses to an
1294// `(n−k)`-variable function after a linear change of basis. Peeling `V(f)` is the affine-group symmetry
1295// break the whole arsenal was missing — and a genuinely random function still has `V(f) = {0}`, the residue
1296// that survives even this.
1297
1298/// The linear space `V(f)` of a Boolean function: the directions along which the derivative is constant.
1299#[derive(Clone, Debug)]
1300pub struct LinearStructureReport {
1301    pub num_vars: usize,
1302    /// A GF(2) echelon basis of `V(f)`; each element is a nonzero variable bitmask.
1303    pub basis: Vec<usize>,
1304    /// For each basis vector `a`, the constant value of `f(x⊕a) ⊕ f(x)`: `false` = an invariance (period),
1305    /// `true` = a complement (always flips the output).
1306    pub derivative: Vec<bool>,
1307}
1308
1309impl LinearStructureReport {
1310    /// `dim V(f)` — the number of dimensions a linear change of basis can peel off.
1311    pub fn dim(&self) -> usize {
1312        self.basis.len()
1313    }
1314    /// Whether the function carries any linear structure the coordinate lenses miss.
1315    pub fn is_reducible(&self) -> bool {
1316        !self.basis.is_empty()
1317    }
1318    /// Re-check the witness against the truth table: each basis vector is nonzero and independent, and its
1319    /// recorded derivative is genuinely constant across the whole cube.
1320    pub fn verify(&self, truth: &[bool]) -> bool {
1321        if self.basis.len() != self.derivative.len() {
1322            return false;
1323        }
1324        if gf2_echelon_basis(&self.basis).len() != self.basis.len() {
1325            return false; // the recorded basis is not independent
1326        }
1327        for (&a, &c) in self.basis.iter().zip(&self.derivative) {
1328            if a == 0 || a >= truth.len() {
1329                return false;
1330            }
1331            if !(0..truth.len()).all(|x| (truth[x] ^ truth[x ^ a]) == c) {
1332                return false;
1333            }
1334        }
1335        true
1336    }
1337}
1338
1339/// A xor-basis (echelon over GF(2)) of the span of `vectors` — each returned mask has a distinct highest
1340/// set bit, and the count is the GF(2) rank.
1341fn gf2_echelon_basis(vectors: &[usize]) -> Vec<usize> {
1342    let mut basis: Vec<usize> = Vec::new();
1343    for &v in vectors {
1344        let mut x = v;
1345        for &b in &basis {
1346            x = x.min(x ^ b);
1347        }
1348        if x != 0 {
1349            basis.push(x);
1350            basis.sort_unstable_by(|a, b| b.cmp(a));
1351        }
1352    }
1353    basis
1354}
1355
1356/// Reduce `x` modulo an echelon basis, clearing every pivot bit (leaving the coset representative that is
1357/// zero on all pivot positions).
1358fn reduce_by_basis(mut x: usize, basis: &[usize]) -> usize {
1359    for &b in basis {
1360        let hb = 1usize << (usize::BITS - 1 - b.leading_zeros());
1361        if x & hb != 0 {
1362            x ^= b;
1363        }
1364    }
1365    x
1366}
1367
1368/// Detect the linear space `V(f)` of a Boolean function from its autocorrelation. `None` if `truth.len()`
1369/// is not a power of two.
1370pub fn linear_structures(truth: &[bool]) -> Option<LinearStructureReport> {
1371    if truth.is_empty() || !truth.len().is_power_of_two() {
1372        return None;
1373    }
1374    let n = truth.len().trailing_zeros() as usize;
1375    let r = describe::autocorrelation(truth)?;
1376    let full = truth.len() as i64;
1377    let structs: Vec<usize> = (1..truth.len()).filter(|&a| r[a].abs() == full).collect();
1378    let basis = gf2_echelon_basis(&structs);
1379    let derivative = basis.iter().map(|&a| truth[a] ^ truth[0]).collect();
1380    Some(LinearStructureReport { num_vars: n, basis, derivative })
1381}
1382
1383/// The genuine domain compression from the INVARIANCE subspace `V₀(f) = {a : f(x⊕a) = f(x) ∀x}`: an
1384/// `(n − dim V₀)`-variable truth table carrying all the information, over the surviving `free_positions`. A
1385/// rotated junta collapses here even though the coordinate junta lens saw every variable as relevant.
1386#[derive(Clone, Debug)]
1387pub struct InvarianceReduction {
1388    pub original_vars: usize,
1389    pub reduced_vars: usize,
1390    /// The surviving variable positions (a complement of the invariance pivots).
1391    pub free_positions: Vec<usize>,
1392    /// The reduced function over `free_positions`, in the same LSB-first index order.
1393    pub reduced_truth: Vec<bool>,
1394}
1395
1396impl InvarianceReduction {
1397    /// Re-check that the reduction reproduces the original: every corner maps to the reduced value of its
1398    /// coset representative.
1399    pub fn verify(&self, truth: &[bool], invariance_basis: &[usize]) -> bool {
1400        if self.reduced_truth.len() != 1 << self.reduced_vars {
1401            return false;
1402        }
1403        (0..truth.len()).all(|x| {
1404            let rep = reduce_by_basis(x, invariance_basis);
1405            let y = self
1406                .free_positions
1407                .iter()
1408                .enumerate()
1409                .fold(0usize, |acc, (i, &p)| if rep & (1 << p) != 0 { acc | (1 << i) } else { acc });
1410            self.reduced_truth[y] == truth[x]
1411        })
1412    }
1413}
1414
1415/// Reduce an echelon xor-basis to reduced row echelon: each vector keeps its pivot bit, cleared from all
1416/// others.
1417fn gf2_rref(basis: &[usize]) -> Vec<usize> {
1418    let mut b = basis.to_vec();
1419    let pivots: Vec<usize> = b.iter().map(|&v| 1usize << (usize::BITS - 1 - v.leading_zeros())).collect();
1420    for i in 0..b.len() {
1421        for j in 0..b.len() {
1422            if i != j && b[j] & pivots[i] != 0 {
1423                b[j] ^= b[i];
1424            }
1425        }
1426    }
1427    b
1428}
1429
1430/// The affine reduction of a Boolean function: `f(x) = h(reduced) ⊕ ℓ(x)`, where `ℓ` is a linear form and
1431/// `h` collapses onto the free coordinates. This peels the COMPLEMENT linear structures (`D_a f = 1`) that
1432/// [`reduce_by_invariance`] cannot — a residue XOR a linear form is invisible to every other axis but folds
1433/// here.
1434#[derive(Clone, Debug, PartialEq, Eq)]
1435pub struct AffineReduction {
1436    pub num_vars: usize,
1437    /// The linear form `ℓ(x) = ⟨c, x⟩` peeled off (as a coefficient bitmask).
1438    pub linear_form: usize,
1439    pub invariance_basis: Vec<usize>,
1440    pub free_positions: Vec<usize>,
1441    /// The reduced function `h` over `free_positions`.
1442    pub reduced_truth: Vec<bool>,
1443}
1444
1445impl AffineReduction {
1446    /// Rebuild the truth table: `f(x) = h(coset-rep of x) ⊕ ⟨c, x⟩`.
1447    pub fn reconstruct(&self) -> Option<Vec<bool>> {
1448        if self.reduced_truth.len() != 1 << self.free_positions.len() {
1449            return None;
1450        }
1451        Some(
1452            (0..1usize << self.num_vars)
1453                .map(|x| {
1454                    let rep = reduce_by_basis(x, &self.invariance_basis);
1455                    let y = self
1456                        .free_positions
1457                        .iter()
1458                        .enumerate()
1459                        .fold(0usize, |acc, (j, &p)| if rep & (1 << p) != 0 { acc | (1 << j) } else { acc });
1460                    self.reduced_truth[y] ^ ((self.linear_form & x).count_ones() % 2 == 1)
1461                })
1462                .collect(),
1463        )
1464    }
1465}
1466
1467/// Peel a linear form off a Boolean function so its complement linear structures become invariances, then
1468/// reduce. Catches `f = h ⊕ ℓ` where `ℓ` is linear and `h` collapses — the residue-plus-a-linear-form class
1469/// every other axis misses. Fail-closed: returns `Some` only if the reconstruction re-checks exactly.
1470/// `None` when there are no complement structures (pure invariance is [`reduce_by_invariance`]'s job).
1471pub fn affine_reduce(truth: &[bool]) -> Option<AffineReduction> {
1472    if truth.is_empty() || !truth.len().is_power_of_two() {
1473        return None;
1474    }
1475    let n = truth.len().trailing_zeros() as usize;
1476    let ls = linear_structures(truth)?;
1477    if !ls.derivative.iter().any(|&d| d) {
1478        return None; // no complement structure — nothing beyond invariance
1479    }
1480    // The linear form: 1 on the pivots whose RREF basis vector has a complement derivative.
1481    let rref = gf2_rref(&ls.basis);
1482    let mut c = 0usize;
1483    for &b in &rref {
1484        if truth[b] ^ truth[0] {
1485            c |= 1usize << (usize::BITS - 1 - b.leading_zeros());
1486        }
1487    }
1488    let h: Vec<bool> = (0..truth.len()).map(|x| truth[x] ^ ((c & x).count_ones() % 2 == 1)).collect();
1489    let (red, inv_basis) = reduce_by_invariance(&h)?;
1490    let out = AffineReduction {
1491        num_vars: n,
1492        linear_form: c,
1493        invariance_basis: inv_basis,
1494        free_positions: red.free_positions,
1495        reduced_truth: red.reduced_truth,
1496    };
1497    (out.reconstruct().as_deref() == Some(truth)).then_some(out)
1498}
1499
1500/// Peel the invariance subspace off a Boolean function: quotient out every direction `a` with `f(x⊕a) =
1501/// f(x)` and return the smaller function on the surviving coordinates, together with the invariance basis
1502/// that certifies it. `None` when the invariance subspace is trivial (nothing to reduce).
1503pub fn reduce_by_invariance(truth: &[bool]) -> Option<(InvarianceReduction, Vec<usize>)> {
1504    if truth.is_empty() || !truth.len().is_power_of_two() {
1505        return None;
1506    }
1507    let n = truth.len().trailing_zeros() as usize;
1508    let r = describe::autocorrelation(truth)?;
1509    let full = truth.len() as i64;
1510    let invariances: Vec<usize> = (1..truth.len()).filter(|&a| r[a] == full).collect();
1511    let basis = gf2_echelon_basis(&invariances);
1512    if basis.is_empty() {
1513        return None;
1514    }
1515    let pivots: Vec<usize> = basis.iter().map(|&b| (usize::BITS - 1 - b.leading_zeros()) as usize).collect();
1516    let free_positions: Vec<usize> = (0..n).filter(|p| !pivots.contains(p)).collect();
1517    let reduced_vars = free_positions.len();
1518    let reduced_truth: Vec<bool> = (0..1usize << reduced_vars)
1519        .map(|y| {
1520            let x = free_positions
1521                .iter()
1522                .enumerate()
1523                .fold(0usize, |acc, (i, &p)| if y & (1 << i) != 0 { acc | (1 << p) } else { acc });
1524            truth[x]
1525        })
1526        .collect();
1527    Some((InvarianceReduction { original_vars: n, reduced_vars, free_positions, reduced_truth }, basis))
1528}
1529
1530// ---- Affine equivalence: the AGL(n,2)-invariant signature, the rung above linear structures ----------
1531//
1532// Linear structures peel ONE rotation; the rung above asks whether two functions are the SAME up to any
1533// invertible linear change of basis (the affine group AGL(n,2)). The Walsh-spectrum MULTISET is the
1534// invariant: applying a linear map `x → Ax` only PERMUTES the Walsh coefficients, so the multiset of
1535// amplitudes is unchanged — affine-equivalent functions share it. A single nonzero amplitude is the
1536// PLATEAUED class (bent when that amplitude is `2^{n/2}`), and the amplitude `2^{(n+k)/2}` reads off the
1537// linear-space dimension `k` — unifying this rung with the one below it. A generic function has a spread
1538// of amplitudes and belongs to no small affine class: the residue.
1539
1540/// The AGL(n,2)-invariant signature of a Boolean function: its Walsh amplitude distribution and the
1541/// plateaued/bent classification that distribution induces.
1542#[derive(Clone, Debug, PartialEq, Eq)]
1543pub struct AffineSignature {
1544    pub num_vars: usize,
1545    /// The multiset `|Ŵ(w)| → count`, sorted by amplitude — invariant under any affine change of variables.
1546    pub walsh_abs_distribution: Vec<(u64, usize)>,
1547    /// Exactly one nonzero amplitude ⇒ PLATEAUED (maximal nonlinearity for its linear-space dimension).
1548    pub is_plateaued: bool,
1549    /// The plateau amplitude `2^{(n+k)/2}` when plateaued, else `None`.
1550    pub amplitude: Option<u64>,
1551    /// Plateaued with amplitude `2^{n/2}` ⇒ BENT: perfectly nonlinear, trivial linear space (`k = 0`).
1552    pub is_bent: bool,
1553}
1554
1555impl AffineSignature {
1556    /// The linear-space dimension `k` implied by the plateau amplitude `2^{(n+k)/2}` (so `k = 2·log₂amp −
1557    /// n`), matching [`linear_structures`]`.dim()`. `None` when not plateaued.
1558    pub fn implied_linear_dim(&self) -> Option<usize> {
1559        let amp = self.amplitude?;
1560        if !amp.is_power_of_two() {
1561            return None;
1562        }
1563        let log = amp.trailing_zeros() as usize;
1564        (2 * log).checked_sub(self.num_vars)
1565    }
1566}
1567
1568/// Compute the affine-equivalence signature of a Boolean function from its Walsh spectrum. `None` if
1569/// `truth.len()` is not a power of two.
1570pub fn affine_signature(truth: &[bool]) -> Option<AffineSignature> {
1571    let n = truth.len().trailing_zeros() as usize;
1572    let spec = describe::walsh_spectrum(truth)?;
1573    let mut counts: std::collections::BTreeMap<u64, usize> = std::collections::BTreeMap::new();
1574    for &c in &spec {
1575        *counts.entry(c.unsigned_abs()).or_default() += 1;
1576    }
1577    let nonzero: Vec<u64> = counts.keys().copied().filter(|&v| v != 0).collect();
1578    let is_plateaued = nonzero.len() == 1;
1579    let amplitude = is_plateaued.then(|| nonzero[0]);
1580    let bent_amp = (n % 2 == 0).then(|| 1u64 << (n / 2));
1581    let is_bent = amplitude.is_some() && amplitude == bent_amp;
1582    Some(AffineSignature {
1583        num_vars: n,
1584        walsh_abs_distribution: counts.into_iter().collect(),
1585        is_plateaued,
1586        amplitude,
1587        is_bent,
1588    })
1589}
1590
1591// ---- Separability: the direct-sum symmetry — independent blocks solved apart ------------------------
1592//
1593// A different axis than the linear-group rungs: does the function SPLIT into independent pieces? `f(x) =
1594// g(x_A) ⊕ h(x_B)` on disjoint variable sets `A, B` is the cube analogue of a SAT instance whose clause
1595// graph has two components — you solve the pieces apart instead of the whole. The blocks are exactly the
1596// connected components of the ANF variable-interaction graph (two variables are linked when a monomial
1597// contains both), because every monomial lives inside one component. Splitting turns one `2ⁿ` table into
1598// `Σ 2^{|Bᵢ|}` — an exponential collapse — and surfaces the true independent subsystems. A function whose
1599// interaction graph is connected is a single irreducible block: no direct-sum symmetry to break.
1600
1601/// A direct-sum decomposition `f = constant ⊕ ⊕ᵢ fᵢ(x_{Bᵢ})` over independent variable blocks.
1602#[derive(Clone, Debug)]
1603pub struct SeparableDecomposition {
1604    pub num_vars: usize,
1605    /// The XOR of the empty monomial — the standalone constant term.
1606    pub constant: bool,
1607    /// A partition of the RELEVANT variables into independent blocks (each sorted; irrelevant vars omitted).
1608    pub blocks: Vec<Vec<usize>>,
1609    /// Each block's function as a `2^{|Bᵢ|}` truth table, aligned with `blocks` (LSB-first in block order).
1610    pub block_truths: Vec<Vec<bool>>,
1611}
1612
1613impl SeparableDecomposition {
1614    /// Whether the function splits into two or more independent blocks.
1615    pub fn is_separable(&self) -> bool {
1616        self.blocks.len() >= 2
1617    }
1618    /// The total bits of the block tables — `Σ 2^{|Bᵢ|}`, against `2ⁿ` for the whole truth table.
1619    pub fn table_bits(&self) -> usize {
1620        self.block_truths.iter().map(|t| t.len()).sum()
1621    }
1622    /// Rebuild the `2ⁿ` truth table — the re-checkable witness. `None` for a malformed decomposition.
1623    pub fn reconstruct(&self, n: usize) -> Option<Vec<bool>> {
1624        let size = 1usize << n;
1625        let mut out = vec![self.constant; size];
1626        for (b, t) in self.blocks.iter().zip(&self.block_truths) {
1627            if t.len() != 1 << b.len() {
1628                return None;
1629            }
1630            for (x, slot) in out.iter_mut().enumerate() {
1631                let y = b.iter().enumerate().fold(0usize, |acc, (j, &i)| {
1632                    if x & (1 << i) != 0 { acc | (1 << j) } else { acc }
1633                });
1634                *slot ^= t[y];
1635            }
1636        }
1637        Some(out)
1638    }
1639}
1640
1641fn uf_find(parent: &mut [usize], mut x: usize) -> usize {
1642    while parent[x] != x {
1643        parent[x] = parent[parent[x]];
1644        x = parent[x];
1645    }
1646    x
1647}
1648
1649/// Decompose a Boolean function into its independent direct-sum blocks (the connected components of its ANF
1650/// interaction graph). `None` if `truth.len()` is not a power of two.
1651pub fn separable_decomposition(truth: &[bool]) -> Option<SeparableDecomposition> {
1652    if truth.is_empty() || !truth.len().is_power_of_two() {
1653        return None;
1654    }
1655    let n = truth.len().trailing_zeros() as usize;
1656    let anf = describe::anf(truth)?;
1657    let constant = anf[0];
1658    let mut parent: Vec<usize> = (0..n).collect();
1659    let mut relevant = vec![false; n];
1660    for (m, &c) in anf.iter().enumerate() {
1661        if !c || m == 0 {
1662            continue;
1663        }
1664        let bits: Vec<usize> = (0..n).filter(|&i| m & (1 << i) != 0).collect();
1665        for &i in &bits {
1666            relevant[i] = true;
1667        }
1668        for w in bits.windows(2) {
1669            let (ra, rb) = (uf_find(&mut parent, w[0]), uf_find(&mut parent, w[1]));
1670            parent[ra] = rb;
1671        }
1672    }
1673    let mut comp: std::collections::BTreeMap<usize, Vec<usize>> = std::collections::BTreeMap::new();
1674    for i in 0..n {
1675        if relevant[i] {
1676            let r = uf_find(&mut parent, i);
1677            comp.entry(r).or_default().push(i);
1678        }
1679    }
1680    let blocks: Vec<Vec<usize>> = comp.into_values().collect();
1681    let mut block_truths = Vec::with_capacity(blocks.len());
1682    for b in &blocks {
1683        let bmask: usize = b.iter().fold(0, |acc, &i| acc | (1 << i));
1684        let mut sub = vec![false; 1usize << b.len()];
1685        for (m, &c) in anf.iter().enumerate() {
1686            if !c || m == 0 || m & !bmask != 0 {
1687                continue;
1688            }
1689            let sub_m = b.iter().enumerate().fold(0usize, |acc, (j, &i)| {
1690                if m & (1 << i) != 0 { acc | (1 << j) } else { acc }
1691            });
1692            sub[sub_m] = true;
1693        }
1694        block_truths.push(describe::anf(&sub)?); // Möbius is its own inverse: sub-ANF → sub-truth
1695    }
1696    Some(SeparableDecomposition { num_vars: n, constant, blocks, block_truths })
1697}
1698
1699// ---- Variable automorphisms: the permutation-symmetry group Aut(f) ----------------------------------
1700//
1701// The `symmetric` lens is the special case `Aut(f) = Sₙ` — invariance under EVERY variable permutation.
1702// This rung finds the whole permutation group `Aut(f) = {σ : f(σ·x) = f(x)}` and certifies it with a
1703// Schreier–Sims BSGS. It catches partial symmetry the full-symmetric lens declares residue: a
1704// ROTATION-symmetric function (invariant under a cyclic shift of the variables — an entire class in stream
1705// ciphers) is a permutation symmetry, NOT a linear one, so every rung of the linear ladder misses it. The
1706// group compresses the `2ⁿ` table to one value per input-orbit — exactly `n+1` weight classes when the
1707// group is `Sₙ`, recovering the symmetric lens as the top of this ladder.
1708
1709/// The variable-permutation symmetry of a Boolean function: a certified subgroup `G ≤ Aut(f)` and the
1710/// input-orbit compression it induces.
1711#[derive(Clone, Debug, PartialEq, Eq)]
1712pub struct VariableSymmetry {
1713    pub num_vars: usize,
1714    /// Generators of `G` (each a permutation of the `n` variables) — every one a verified automorphism.
1715    pub generators: Vec<Vec<usize>>,
1716    /// `|G|` — the certified group order from Schreier–Sims.
1717    pub order: u128,
1718    /// The number of orbits of inputs `{0,1}ⁿ` under `G`: the compressed table size (one value per orbit).
1719    pub orbit_count: usize,
1720    /// One value per orbit, indexed by the orbit's minimal representative in ascending order.
1721    pub orbit_values: Vec<bool>,
1722}
1723
1724impl VariableSymmetry {
1725    /// Whether the function has any nontrivial variable-permutation symmetry.
1726    pub fn is_nontrivial(&self) -> bool {
1727        self.order > 1
1728    }
1729    /// Rebuild the `2ⁿ` truth table by replaying the group action and painting each orbit its value — the
1730    /// re-checkable witness. `None` for a malformed report.
1731    pub fn reconstruct(&self, n: usize) -> Option<Vec<bool>> {
1732        let size = 1usize << n;
1733        if self.orbit_values.len() != self.orbit_count {
1734            return None;
1735        }
1736        let mut orbs = self.input_orbits(size)?;
1737        orbs.sort_by_key(|o| *o.iter().min().expect("nonempty orbit"));
1738        if orbs.len() != self.orbit_count {
1739            return None;
1740        }
1741        let mut out = vec![false; size];
1742        for (k, o) in orbs.iter().enumerate() {
1743            for &x in o {
1744                out[x] = self.orbit_values[k];
1745            }
1746        }
1747        Some(out)
1748    }
1749    /// Re-check: every generator is a genuine automorphism and the orbit painting reproduces `f`.
1750    pub fn verify(&self, truth: &[bool]) -> bool {
1751        for s in &self.generators {
1752            if s.len() != self.num_vars || !is_variable_automorphism(truth, s) {
1753                return false;
1754            }
1755        }
1756        self.reconstruct(self.num_vars).as_deref() == Some(truth)
1757    }
1758    fn input_orbits(&self, size: usize) -> Option<Vec<Vec<usize>>> {
1759        let lifted: Vec<Vec<usize>> =
1760            self.generators.iter().map(|s| (0..size).map(|x| permute_input_bits(x, s)).collect()).collect();
1761        Some(if lifted.is_empty() { (0..size).map(|x| vec![x]).collect() } else { crate::permgroup::orbits(size, &lifted) })
1762    }
1763}
1764
1765/// Permute the input bits of `x` by the variable permutation `sigma`: bit `i` moves to bit `sigma[i]`.
1766fn permute_input_bits(x: usize, sigma: &[usize]) -> usize {
1767    sigma.iter().enumerate().fold(0usize, |acc, (i, &si)| if x & (1 << i) != 0 { acc | (1 << si) } else { acc })
1768}
1769
1770/// Whether `f(σ·x) = f(x)` for all `x` — i.e. `sigma` is a variable automorphism of the function.
1771fn is_variable_automorphism(truth: &[bool], sigma: &[usize]) -> bool {
1772    (0..truth.len()).all(|x| truth[x] == truth[permute_input_bits(x, sigma)])
1773}
1774
1775/// Find the variable-permutation symmetry group of a Boolean function: test every transposition and the
1776/// cyclic shift, certify the subgroup they generate with Schreier–Sims, and compress by input-orbits.
1777/// `None` if `truth.len()` is not a power of two.
1778pub fn variable_symmetry(truth: &[bool]) -> Option<VariableSymmetry> {
1779    if truth.is_empty() || !truth.len().is_power_of_two() {
1780        return None;
1781    }
1782    let n = truth.len().trailing_zeros() as usize;
1783    let mut generators: Vec<Vec<usize>> = Vec::new();
1784    for i in 0..n {
1785        for j in i + 1..n {
1786            let mut s: Vec<usize> = (0..n).collect();
1787            s.swap(i, j);
1788            if is_variable_automorphism(truth, &s) {
1789                generators.push(s);
1790            }
1791        }
1792    }
1793    // Products of two disjoint transpositions — the symmetries no single transposition realizes (e.g. a
1794    // reflection, or an even permutation fixing f that its transposition factors do not).
1795    for a in 0..n {
1796        for b in a + 1..n {
1797            for c in 0..n {
1798                if c == a || c == b {
1799                    continue;
1800                }
1801                for d in c + 1..n {
1802                    if d == a || d == b {
1803                        continue;
1804                    }
1805                    let mut s: Vec<usize> = (0..n).collect();
1806                    s.swap(a, b);
1807                    s.swap(c, d);
1808                    if is_variable_automorphism(truth, &s) {
1809                        generators.push(s);
1810                    }
1811                }
1812            }
1813        }
1814    }
1815    if n > 1 {
1816        let rho: Vec<usize> = (0..n).map(|i| (i + 1) % n).collect();
1817        if is_variable_automorphism(truth, &rho) {
1818            generators.push(rho);
1819        }
1820    }
1821    let order = crate::permgroup::schreier_sims(n, &generators).order();
1822    let size = 1usize << n;
1823    let sym = VariableSymmetry { num_vars: n, generators, order, orbit_count: 0, orbit_values: Vec::new() };
1824    let mut orbs = sym.input_orbits(size)?;
1825    orbs.sort_by_key(|o| *o.iter().min().expect("nonempty orbit"));
1826    let orbit_values: Vec<bool> = orbs.iter().map(|o| truth[*o.iter().min().expect("nonempty")]).collect();
1827    Some(VariableSymmetry { orbit_count: orbs.len(), orbit_values, ..sym })
1828}
1829
1830// ---- The unified deep finder: the tightest description across every axis ----------------------------
1831//
1832// Each rung is a different symmetry axis, and a function may be simple on one while dense on all the
1833// others: separable-but-high-degree, rotation-symmetric-but-not-linear, a rotated junta the coordinate
1834// walk calls residue. The deep finder runs EVERY axis — coordinate lenses, direct-sum separability, the
1835// Aut(f) permutation group, and linear-invariance reduction — and returns whichever gives the shortest
1836// description. Only when all of them fail to beat the raw truth table is the verdict `residue`: the honest
1837// incompressible core relative to the whole arsenal (never a proof of structurelessness — Chaitin).
1838
1839/// The winning axis and description size of the unified deep structure finder.
1840#[derive(Clone, Debug)]
1841pub struct DeepStructureReport {
1842    pub num_vars: usize,
1843    pub raw_bits: usize,
1844    pub description_bits: usize,
1845    /// Which axis gave the tightest description (`coordinate:*`, `separable`, `permutation`,
1846    /// `linear-invariance`, or `residue`).
1847    pub winner: &'static str,
1848    pub compressed: bool,
1849}
1850
1851/// Run every structural axis and return the globally tightest description of a Boolean function. `None` if
1852/// `truth.len()` is not a power of two.
1853pub fn find_structure_deep(truth: &[bool]) -> Option<DeepStructureReport> {
1854    if truth.is_empty() || !truth.len().is_power_of_two() {
1855        return None;
1856    }
1857    let n = truth.len().trailing_zeros() as usize;
1858    let raw = truth.len();
1859    let mut cands: Vec<(&'static str, usize)> = Vec::new();
1860
1861    if let Some(r) = find_structure(truth) {
1862        if r.compressed {
1863            let name = match r.class {
1864                CubeStructure::Constant(_) => "coordinate:constant",
1865                CubeStructure::Junta { .. } => "coordinate:junta",
1866                CubeStructure::Affine { .. } => "coordinate:affine",
1867                CubeStructure::Symmetric { .. } => "coordinate:symmetric",
1868                CubeStructure::LowDegree { .. } => "coordinate:low-degree",
1869                CubeStructure::ResistedArsenal { .. } => "coordinate:residue",
1870            };
1871            cands.push((name, r.description_bits));
1872        }
1873    }
1874    if let Some(d) = separable_decomposition(truth) {
1875        if d.is_separable() {
1876            cands.push(("separable", d.table_bits()));
1877        }
1878    }
1879    if let Some(s) = variable_symmetry(truth) {
1880        if s.is_nontrivial() {
1881            cands.push(("permutation", s.orbit_count));
1882        }
1883    }
1884    if let Some((red, _)) = reduce_by_invariance(truth) {
1885        cands.push(("linear-invariance", 1usize << red.reduced_vars));
1886    }
1887    if let Some(ar) = affine_reduce(truth) {
1888        cands.push(("affine", ar.reduced_truth.len() + n));
1889    }
1890
1891    let (winner, description_bits) =
1892        cands.into_iter().filter(|(_, b)| *b < raw).min_by_key(|(_, b)| *b).unwrap_or(("residue", raw));
1893    Some(DeepStructureReport { num_vars: n, raw_bits: raw, description_bits, winner, compressed: description_bits < raw })
1894}
1895
1896// ---- The recursive peel: structure all the way down to the fixed point -----------------------------
1897//
1898// The deep finder picks the tightest axis for the WHOLE function. The recursive peel goes further: it
1899// peels that axis and re-runs itself on the smaller function(s) it exposes — each block of a separable
1900// split, the reduced function of a linear-invariance peel — until it bottoms out in a coordinate leaf or
1901// the residue. A function like `((x0⊕x1)∧x2) ⊕ (x3∧x4∧x5)` splits into two blocks, and the first block
1902// then peels its rotated-junta invariance: a two-level tree the single-pass finder cannot express. The
1903// whole tree's `reconstruct` is one re-checkable witness for the entire decomposition. Recursion
1904// terminates because every peel strictly reduces the variable count.
1905
1906/// A recursive structural decomposition of a Boolean function: the fixed point of the deep finder.
1907#[derive(Clone, Debug, PartialEq, Eq)]
1908pub enum StructureTree {
1909    /// A coordinate-lens leaf (constant/junta/affine/symmetric/low-degree), reconstructable from `class`.
1910    Coordinate { class: CubeStructure, num_vars: usize, bits: usize },
1911    /// A permutation-symmetry leaf: the orbit painting reconstructs it.
1912    Permutation { sym: VariableSymmetry, num_vars: usize },
1913    /// The incompressible residue — stored raw (no axis compressed it).
1914    Residue { truth: Vec<bool> },
1915    /// Split into independent blocks, each recursively decomposed.
1916    Separable { num_vars: usize, constant: bool, blocks: Vec<(Vec<usize>, StructureTree)> },
1917    /// Reduced by an invariance subspace; `inner` is the decomposition of the smaller function.
1918    LinearReduced { num_vars: usize, invariance_basis: Vec<usize>, free_positions: Vec<usize>, inner: Box<StructureTree> },
1919    /// Peeled a linear form `⟨c,x⟩`, then reduced; `inner` decomposes the residual `h`.
1920    AffineReduced {
1921        num_vars: usize,
1922        linear_form: usize,
1923        invariance_basis: Vec<usize>,
1924        free_positions: Vec<usize>,
1925        inner: Box<StructureTree>,
1926    },
1927}
1928
1929impl StructureTree {
1930    /// The nesting depth of the decomposition (0 for a leaf).
1931    pub fn depth(&self) -> usize {
1932        match self {
1933            StructureTree::Coordinate { .. } | StructureTree::Permutation { .. } | StructureTree::Residue { .. } => 0,
1934            StructureTree::Separable { blocks, .. } => 1 + blocks.iter().map(|(_, t)| t.depth()).max().unwrap_or(0),
1935            StructureTree::LinearReduced { inner, .. } | StructureTree::AffineReduced { inner, .. } => {
1936                1 + inner.depth()
1937            }
1938        }
1939    }
1940    /// The total description size in bits: the sum over the leaves, with a small charge per structural node.
1941    pub fn total_description_bits(&self) -> usize {
1942        match self {
1943            StructureTree::Coordinate { bits, .. } => *bits,
1944            StructureTree::Permutation { sym, .. } => sym.orbit_count,
1945            StructureTree::Residue { truth } => truth.len(),
1946            StructureTree::Separable { blocks, .. } => {
1947                1 + blocks.iter().map(|(v, t)| v.len() + t.total_description_bits()).sum::<usize>()
1948            }
1949            StructureTree::LinearReduced { invariance_basis, inner, .. } => {
1950                invariance_basis.len() + inner.total_description_bits()
1951            }
1952            StructureTree::AffineReduced { num_vars, inner, .. } => *num_vars + inner.total_description_bits(),
1953        }
1954    }
1955    /// Rebuild the full `2ⁿ` truth table by composing the recursive descriptions — the whole-tree witness.
1956    pub fn reconstruct(&self) -> Option<Vec<bool>> {
1957        match self {
1958            StructureTree::Coordinate { class, num_vars, .. } => class.reconstruct(*num_vars),
1959            StructureTree::Permutation { sym, num_vars } => sym.reconstruct(*num_vars),
1960            StructureTree::Residue { truth } => Some(truth.clone()),
1961            StructureTree::Separable { num_vars, constant, blocks } => {
1962                let size = 1usize << num_vars;
1963                let mut out = vec![*constant; size];
1964                for (vars, sub) in blocks {
1965                    let bt = sub.reconstruct()?;
1966                    for (x, slot) in out.iter_mut().enumerate() {
1967                        let y = vars.iter().enumerate().fold(0usize, |acc, (j, &i)| {
1968                            if x & (1 << i) != 0 { acc | (1 << j) } else { acc }
1969                        });
1970                        *slot ^= bt[y];
1971                    }
1972                }
1973                Some(out)
1974            }
1975            StructureTree::LinearReduced { num_vars, invariance_basis, free_positions, inner } => {
1976                let inner_truth = inner.reconstruct()?;
1977                Some(
1978                    (0..1usize << num_vars)
1979                        .map(|x| {
1980                            let rep = reduce_by_basis(x, invariance_basis);
1981                            let y = free_positions.iter().enumerate().fold(0usize, |acc, (j, &p)| {
1982                                if rep & (1 << p) != 0 { acc | (1 << j) } else { acc }
1983                            });
1984                            inner_truth[y]
1985                        })
1986                        .collect(),
1987                )
1988            }
1989            StructureTree::AffineReduced { num_vars, linear_form, invariance_basis, free_positions, inner } => {
1990                let inner_truth = inner.reconstruct()?;
1991                Some(
1992                    (0..1usize << num_vars)
1993                        .map(|x| {
1994                            let rep = reduce_by_basis(x, invariance_basis);
1995                            let y = free_positions.iter().enumerate().fold(0usize, |acc, (j, &p)| {
1996                                if rep & (1 << p) != 0 { acc | (1 << j) } else { acc }
1997                            });
1998                            inner_truth[y] ^ ((linear_form & x).count_ones() % 2 == 1)
1999                        })
2000                        .collect(),
2001                )
2002            }
2003        }
2004    }
2005}
2006
2007/// Recursively decompose a Boolean function to the fixed point of the deep finder: peel the tightest axis,
2008/// then re-run on the smaller function(s) it exposes, until a coordinate leaf or the residue. `None` if
2009/// `truth.len()` is not a power of two.
2010pub fn structure_tree(truth: &[bool]) -> Option<StructureTree> {
2011    if truth.is_empty() || !truth.len().is_power_of_two() {
2012        return None;
2013    }
2014    let n = truth.len().trailing_zeros() as usize;
2015    let deep = find_structure_deep(truth)?;
2016    let tree = match deep.winner {
2017        "separable" => {
2018            let d = separable_decomposition(truth)?;
2019            let mut blocks = Vec::with_capacity(d.blocks.len());
2020            for (vars, bt) in d.blocks.iter().zip(&d.block_truths) {
2021                blocks.push((vars.clone(), structure_tree(bt)?));
2022            }
2023            StructureTree::Separable { num_vars: n, constant: d.constant, blocks }
2024        }
2025        "linear-invariance" => {
2026            let (red, basis) = reduce_by_invariance(truth)?;
2027            StructureTree::LinearReduced {
2028                num_vars: n,
2029                invariance_basis: basis,
2030                free_positions: red.free_positions.clone(),
2031                inner: Box::new(structure_tree(&red.reduced_truth)?),
2032            }
2033        }
2034        "affine" => {
2035            let ar = affine_reduce(truth)?;
2036            StructureTree::AffineReduced {
2037                num_vars: n,
2038                linear_form: ar.linear_form,
2039                invariance_basis: ar.invariance_basis,
2040                free_positions: ar.free_positions,
2041                inner: Box::new(structure_tree(&ar.reduced_truth)?),
2042            }
2043        }
2044        "permutation" => StructureTree::Permutation { sym: variable_symmetry(truth)?, num_vars: n },
2045        "residue" => StructureTree::Residue { truth: truth.to_vec() },
2046        _ => StructureTree::Coordinate { class: find_structure(truth)?.class, num_vars: n, bits: deep.description_bits },
2047    };
2048    Some(tree)
2049}
2050
2051/// An exhaustive census of the whole Boolean-function space on `n` variables: which structural axis the
2052/// deep finder wins on, for every one of the `2^{2ⁿ}` functions.
2053#[derive(Clone, Debug)]
2054pub struct BooleanCensus {
2055    pub num_vars: usize,
2056    /// `2^{2ⁿ}` — the number of Boolean functions on `n` variables.
2057    pub total: usize,
2058    /// Each winning axis paired with how many functions it is the tightest description for.
2059    pub by_winner: Vec<(&'static str, usize)>,
2060    /// Functions some axis compressed (`total − residue`).
2061    pub compressed: usize,
2062    /// Functions no axis compressed — the incompressible residue relative to the whole arsenal.
2063    pub residue: usize,
2064}
2065
2066/// Exhaustively classify **every** Boolean function on `n` variables by the deep finder's winning axis.
2067/// `None` for `n = 0` or `n > 4` (beyond `2^{2⁴} = 65536` functions the space is astronomically large).
2068/// The residue count is the concrete, countable face of the Chaitin ceiling: it is a growing fraction of
2069/// the space as `n` rises — structured functions are `2^{poly}`, the space is `2^{2ⁿ}`.
2070pub fn boolean_function_census(n: usize) -> Option<BooleanCensus> {
2071    if n == 0 || n > 4 {
2072        return None;
2073    }
2074    let dim = 1usize << n;
2075    let total = 1u64 << dim;
2076    let mut counts: std::collections::BTreeMap<&'static str, usize> = std::collections::BTreeMap::new();
2077    for code in 0..total {
2078        let truth: Vec<bool> = (0..dim).map(|i| (code >> i) & 1 == 1).collect();
2079        *counts.entry(find_structure_deep(&truth)?.winner).or_default() += 1;
2080    }
2081    let residue = counts.get("residue").copied().unwrap_or(0);
2082    Some(BooleanCensus {
2083        num_vars: n,
2084        total: total as usize,
2085        by_winner: counts.into_iter().collect(),
2086        compressed: total as usize - residue,
2087        residue,
2088    })
2089}
2090
2091/// A sampled census of the Boolean-function space on `n` variables, for `n` too large to enumerate.
2092#[derive(Clone, Debug)]
2093pub struct SampledCensus {
2094    pub num_vars: usize,
2095    pub samples: usize,
2096    /// How many sampled functions no axis compressed.
2097    pub residue: usize,
2098    pub by_winner: Vec<(&'static str, usize)>,
2099}
2100
2101impl SampledCensus {
2102    /// The estimated fraction of the space that is the incompressible residue.
2103    pub fn residue_fraction(&self) -> f64 {
2104        self.residue as f64 / self.samples.max(1) as f64
2105    }
2106}
2107
2108/// Estimate the coverage map at `n` variables (where `2^{2ⁿ}` is unenumerable) from a uniform random sample
2109/// of `samples` functions, classified by the deep finder's winning axis. `None` for `n = 0` or `n > 12`.
2110/// The residue fraction climbs toward `1` as `n` grows — the asymptotic form of the exhaustive census, and
2111/// the whole thesis: structured functions vanish and almost every function is incompressible.
2112pub fn sampled_boolean_census(n: usize, samples: usize, seed: u64) -> Option<SampledCensus> {
2113    if n == 0 || n > 12 {
2114        return None;
2115    }
2116    let dim = 1usize << n;
2117    let mut counts: std::collections::BTreeMap<&'static str, usize> = std::collections::BTreeMap::new();
2118    for s in 0..samples {
2119        let truth: Vec<bool> = (0..dim)
2120            .map(|i| {
2121                let mut z =
2122                    seed.wrapping_add((s as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)).wrapping_add(i as u64 + 1);
2123                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2124                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2125                (z ^ (z >> 31)) & 1 == 1
2126            })
2127            .collect();
2128        *counts.entry(find_structure_deep(&truth)?.winner).or_default() += 1;
2129    }
2130    let residue = counts.get("residue").copied().unwrap_or(0);
2131    Some(SampledCensus { num_vars: n, samples, residue, by_winner: counts.into_iter().collect() })
2132}
2133
2134/// A **certified Kolmogorov upper bound** on a Boolean function: the recursive structure decomposition and
2135/// its total description size, with a re-checkable decode witness. This is a `K̄` — an UPPER bound only.
2136/// The kernel-certified Chaitin theorem in this module forbids certifying `K(f) > c` for any function past
2137/// budget, so a bound that equals the raw `2ⁿ` means "irreducible by this arsenal," never "incompressible."
2138#[derive(Clone, Debug)]
2139pub struct KolmogorovBound {
2140    pub num_vars: usize,
2141    /// `K̄(f)` — the total description size of the recursive decomposition (a computable upper bound).
2142    pub bits: usize,
2143    /// `2ⁿ` — the cost of storing the truth table outright.
2144    pub raw_bits: usize,
2145    /// The recursive decomposition itself — the decode witness.
2146    pub tree: StructureTree,
2147}
2148
2149impl KolmogorovBound {
2150    /// `K̄(f) / 2ⁿ` — → 0 for the algorithmically simplest functions, 1 for the residue.
2151    pub fn ratio(&self) -> f64 {
2152        self.bits as f64 / self.raw_bits.max(1) as f64
2153    }
2154    /// Whether the decomposition beats storing the truth table.
2155    pub fn is_compressed(&self) -> bool {
2156        self.bits < self.raw_bits
2157    }
2158    /// Re-check the bound: the decomposition must decode back to the exact function.
2159    pub fn verify(&self, truth: &[bool]) -> bool {
2160        self.tree.reconstruct().as_deref() == Some(truth)
2161    }
2162}
2163
2164/// Compute a certified Kolmogorov upper bound for a Boolean function via its recursive structure
2165/// decomposition. `None` if `truth.len()` is not a power of two.
2166pub fn kolmogorov_bound(truth: &[bool]) -> Option<KolmogorovBound> {
2167    let n = truth.len().trailing_zeros() as usize;
2168    let tree = structure_tree(truth)?;
2169    Some(KolmogorovBound { num_vars: n, bits: tree.total_description_bits(), raw_bits: truth.len(), tree })
2170}
2171
2172// ---- Vectorial Boolean functions (S-boxes): the ladder lifted to n→m bit maps ------------------------
2173//
2174// A block cipher's S-box is a vectorial Boolean function `S : {0,1}ⁿ → {0,1}ᵐ`, and its cryptographic
2175// STRENGTH is the absence of the very structure this whole module hunts. Every nonzero output mask `b`
2176// gives a component function `S_b(x) = ⟨b, S(x)⟩` — a single-output function we feed to the lenses above.
2177// The S-box is weak exactly when those components carry structure: low LINEARITY (a good linear
2178// approximation → linear cryptanalysis), low DIFFERENTIAL UNIFORMITY breached (a biased difference →
2179// differential cryptanalysis), low DEGREE (an algebraic relation), or outright affinity. A strong S-box —
2180// AES's — is the residue: high nonlinearity, differential uniformity 4, degree 7. "We broke AES" is not a
2181// claim these numbers support; they are exactly what certifies its S-box is sound.
2182
2183/// The cryptographic structural profile of an S-box.
2184#[derive(Clone, Debug, PartialEq, Eq)]
2185pub struct SboxProfile {
2186    pub in_bits: usize,
2187    pub out_bits: usize,
2188    /// The maximum count `#{x : S(x⊕a) ⊕ S(x) = b}` over `a≠0, b` — differential-cryptanalysis resistance
2189    /// (lower is stronger; `2` is the optimal APN bound).
2190    pub differential_uniformity: u32,
2191    /// `maxₐ,_{b≠0} |Σₓ (−1)^{⟨b,S(x)⟩ ⊕ ⟨a,x⟩}|` — linear-cryptanalysis resistance (lower is stronger).
2192    pub linearity: u64,
2193    /// The minimum algebraic degree over the nonzero component functions.
2194    pub min_degree: usize,
2195    /// Every component is affine — the trivially-broken S-box.
2196    pub is_affine: bool,
2197    /// A permutation (`n = m` and injective).
2198    pub is_bijective: bool,
2199    /// Almost perfect nonlinear: differential uniformity `2`, the optimal differential resistance.
2200    pub is_apn: bool,
2201}
2202
2203/// Profile an S-box `S : {0,1}ⁿ → {0,1}ᵐ` given as its output table (`sbox[x] = S(x)`, `out_bits = m`):
2204/// differential uniformity, linearity, minimum component degree, and the affine/bijective/APN flags.
2205/// `None` if the table length is not a power of two.
2206pub fn sbox_profile(sbox: &[u32], out_bits: usize) -> Option<SboxProfile> {
2207    if sbox.is_empty() || !sbox.len().is_power_of_two() {
2208        return None;
2209    }
2210    let n = sbox.len().trailing_zeros() as usize;
2211    let m = out_bits;
2212    let size = sbox.len();
2213
2214    let mut differential_uniformity = 0u32;
2215    for a in 1..size {
2216        let mut count = vec![0u32; 1usize << m];
2217        for x in 0..size {
2218            count[(sbox[x] ^ sbox[x ^ a]) as usize] += 1;
2219        }
2220        differential_uniformity = differential_uniformity.max(*count.iter().max().unwrap_or(&0));
2221    }
2222
2223    let mut linearity = 0u64;
2224    let mut min_degree = usize::MAX;
2225    let mut is_affine = true;
2226    for b in 1..(1usize << m) {
2227        let comp: Vec<bool> = sbox.iter().map(|&y| (b as u32 & y).count_ones() % 2 == 1).collect();
2228        let spec = describe::walsh_spectrum(&comp)?;
2229        linearity = linearity.max(spec.iter().map(|&c| c.unsigned_abs()).max().unwrap_or(0));
2230        let deg = describe::algebraic_degree(&comp)?;
2231        min_degree = min_degree.min(deg);
2232        if deg > 1 {
2233            is_affine = false;
2234        }
2235    }
2236
2237    let mut seen = vec![false; 1usize << m];
2238    let is_bijective = n == m
2239        && sbox.iter().all(|&y| {
2240            let u = y as usize;
2241            u < seen.len() && !std::mem::replace(&mut seen[u], true)
2242        });
2243
2244    Some(SboxProfile {
2245        in_bits: n,
2246        out_bits: m,
2247        differential_uniformity,
2248        linearity,
2249        min_degree: if min_degree == usize::MAX { 0 } else { min_degree },
2250        is_affine,
2251        is_bijective,
2252        is_apn: n == m && differential_uniformity == 2,
2253    })
2254}
2255
2256/// The affine-invariant spectra of an S-box: the fingerprints for classification up to affine equivalence.
2257#[derive(Clone, Debug, PartialEq, Eq)]
2258pub struct SboxSpectra {
2259    pub in_bits: usize,
2260    pub out_bits: usize,
2261    /// The differential spectrum: the multiset of DDT entries `#{x : S(x⊕a)⊕S(x)=b}` over `a≠0`, as
2262    /// `(value → count)` sorted. Invariant under any affine change of input or output coordinates.
2263    pub differential_spectrum: Vec<(u32, usize)>,
2264    /// The linear (Walsh) spectrum: the multiset of amplitudes `|Σₓ(−1)^{⟨b,S(x)⟩⊕⟨a,x⟩}|` over `a, b≠0`.
2265    pub walsh_spectrum: Vec<(u64, usize)>,
2266}
2267
2268/// The affine-equivalence fingerprint of an S-box: its differential and linear spectra. Two S-boxes that
2269/// are affine-equivalent (`S′(x) = B·S(A·x⊕a)⊕c` for invertible `A,B`) share both spectra, so a difference
2270/// in either certifies inequivalence — the necessary test at the heart of S-box classification. `None` if
2271/// the table length is not a power of two.
2272pub fn sbox_spectra(sbox: &[u32], out_bits: usize) -> Option<SboxSpectra> {
2273    if sbox.is_empty() || !sbox.len().is_power_of_two() {
2274        return None;
2275    }
2276    let n = sbox.len().trailing_zeros() as usize;
2277    let m = out_bits;
2278    let size = sbox.len();
2279
2280    let mut ddt_hist: std::collections::BTreeMap<u32, usize> = std::collections::BTreeMap::new();
2281    for a in 1..size {
2282        let mut count = vec![0u32; 1usize << m];
2283        for x in 0..size {
2284            count[(sbox[x] ^ sbox[x ^ a]) as usize] += 1;
2285        }
2286        for &c in &count {
2287            *ddt_hist.entry(c).or_default() += 1;
2288        }
2289    }
2290
2291    let mut walsh_hist: std::collections::BTreeMap<u64, usize> = std::collections::BTreeMap::new();
2292    for b in 1..(1usize << m) {
2293        let comp: Vec<bool> = sbox.iter().map(|&y| (b as u32 & y).count_ones() % 2 == 1).collect();
2294        for &w in &describe::walsh_spectrum(&comp)? {
2295            *walsh_hist.entry(w.unsigned_abs()).or_default() += 1;
2296        }
2297    }
2298
2299    Some(SboxSpectra {
2300        in_bits: n,
2301        out_bits: m,
2302        differential_spectrum: ddt_hist.into_iter().collect(),
2303        walsh_spectrum: walsh_hist.into_iter().collect(),
2304    })
2305}
2306
2307/// The **boomerang uniformity** of an S-box permutation — the third cryptanalytic pillar after
2308/// differential and linear. `BCT[a][b] = #{x : S⁻¹(S(x)⊕b) ⊕ S⁻¹(S(x⊕a)⊕b) = a}`; the uniformity is the
2309/// maximum over `a≠0, b≠0` and measures resistance to the boomerang attack (lower is stronger; `2` is
2310/// optimal, attained exactly by APN permutations). `None` if `sbox` is not a permutation.
2311pub fn boomerang_uniformity(sbox: &[u32]) -> Option<u32> {
2312    if sbox.is_empty() || !sbox.len().is_power_of_two() {
2313        return None;
2314    }
2315    let size = sbox.len();
2316    let mut inv = vec![u32::MAX; size];
2317    for (x, &y) in sbox.iter().enumerate() {
2318        let u = y as usize;
2319        if u >= size || inv[u] != u32::MAX {
2320            return None; // not a permutation
2321        }
2322        inv[u] = x as u32;
2323    }
2324    let mut bu = 0u32;
2325    for a in 1..size {
2326        for b in 1..size {
2327            let mut count = 0u32;
2328            for x in 0..size {
2329                let lhs = inv[(sbox[x] as usize) ^ b] ^ inv[(sbox[x ^ a] as usize) ^ b];
2330                if lhs as usize == a {
2331                    count += 1;
2332                }
2333            }
2334            bu = bu.max(count);
2335        }
2336    }
2337    Some(bu)
2338}
2339
2340/// The structural-security verdict on an S-box — the vectorial analogue of [`rsa_full_audit`]. It flags
2341/// only PROVABLE weaknesses (an exact linear relation, a deterministic difference, a quadratic system); a
2342/// clean profile is the honest ceiling, "no structural weakness of these classes," never a proof of
2343/// security (the Chaitin frame in the symmetric world).
2344#[derive(Clone, Debug, PartialEq, Eq)]
2345pub enum SboxVerdict {
2346    /// Every component is affine — trivially broken; linear cryptanalysis is exact.
2347    Affine,
2348    /// Some component `⟨b, S(x)⟩` is affine (`linearity = 2ⁿ`): a linear relation among the outputs holds
2349    /// with probability 1.
2350    LinearComponent,
2351    /// Some input difference forces a single output difference (`differential uniformity = 2ⁿ`).
2352    DeterministicDifference,
2353    /// Algebraic degree 2 — the S-box is a system of quadratic equations, the classic algebraic-attack target.
2354    Quadratic,
2355    /// No structural weakness of the above classes. The profile is the honest ceiling, not a security proof.
2356    NoStructuralWeaknessFound {
2357        differential_uniformity: u32,
2358        linearity: u64,
2359        min_degree: usize,
2360        boomerang_uniformity: Option<u32>,
2361    },
2362}
2363
2364/// Audit an S-box with the full structural arsenal — differential, linear, algebraic, and boomerang — and
2365/// return the first provable weakness, else the honest ceiling with the measured profile. `None` if the
2366/// table length is not a power of two. Threshold-free: it never fabricates a "weak" verdict from an
2367/// arbitrary cutoff, only from a structure that is exact.
2368pub fn sbox_full_audit(sbox: &[u32], out_bits: usize) -> Option<SboxVerdict> {
2369    let p = sbox_profile(sbox, out_bits)?;
2370    let full = 1u64 << p.in_bits;
2371    if p.is_affine {
2372        return Some(SboxVerdict::Affine);
2373    }
2374    if p.linearity == full {
2375        return Some(SboxVerdict::LinearComponent);
2376    }
2377    if p.differential_uniformity as u64 == full {
2378        return Some(SboxVerdict::DeterministicDifference);
2379    }
2380    if p.min_degree == 2 {
2381        return Some(SboxVerdict::Quadratic);
2382    }
2383    Some(SboxVerdict::NoStructuralWeaknessFound {
2384        differential_uniformity: p.differential_uniformity,
2385        linearity: p.linearity,
2386        min_degree: p.min_degree,
2387        boomerang_uniformity: if p.is_bijective { boomerang_uniformity(sbox) } else { None },
2388    })
2389}
2390
2391// ---- RSA: the thesis in the public-key world (structural weakness vs the number-theoretic ceiling) ---
2392//
2393// The compressibility ladder attacks SYMMETRIC keystreams through sequence structure; RSA rests on
2394// integer factorization instead, so none of those rungs touch it. But the same principle governs a
2395// modulus: a WEAK RSA key carries exploitable structure (a small factor, primes too close, a smooth
2396// `p−1`, a shared prime, a small private exponent), and each such structure is a compression — a short
2397// description of the secret — recovered as a certified factorization by `crate::factor`. A
2398// SOUND modulus has none: it is the number-theoretic incompressible residue, the Chaitin ceiling in the
2399// public-key world. Crush every structured form; the sound form stands, and that standing is the proof.
2400
2401/// The structural-security verdict on an RSA modulus.
2402#[derive(Clone, Debug, PartialEq, Eq)]
2403pub enum RsaStrength {
2404    /// A certified structural break: `p·q = N`, found by the named attack (a re-checkable witness).
2405    Factored { p: logicaffeine_base::BigInt, q: logicaffeine_base::BigInt, method: &'static str },
2406    /// The whole structural arsenal declined within budget — no structural shortcut exists. Only the
2407    /// general sub-exponential algorithms remain, which real key sizes push out of reach: the ceiling.
2408    SoundAgainstStructuralAttacks,
2409}
2410
2411/// Audit an RSA modulus with the full structural factoring arsenal (see [`crate::factor`]):
2412/// return a certified factorization if any structural weakness exists, else the soundness verdict — the
2413/// number-theoretic incompressible residue. Uses the default triage budget.
2414pub fn rsa_structural_audit(n: &logicaffeine_base::BigInt) -> RsaStrength {
2415    match crate::factor::structural_factor(n, Default::default()) {
2416        Some(w) => RsaStrength::Factored { p: w.p, q: w.q, method: w.method },
2417        None => RsaStrength::SoundAgainstStructuralAttacks,
2418    }
2419}
2420
2421/// The verdict of throwing the ENTIRE arsenal at an RSA public key.
2422#[derive(Clone, Debug, PartialEq, Eq)]
2423pub enum RsaAuditVerdict {
2424    /// The structural factoring suite split the modulus (the method names the structure it exploited).
2425    Factored { method: &'static str, p: logicaffeine_base::BigInt, q: logicaffeine_base::BigInt },
2426    /// Wiener's attack found a small private exponent.
2427    WeakExponent,
2428    /// The compressibility classifier found exploitable structure in the modulus bytes.
2429    CompressibleModulus(CompressibilityClass),
2430    /// No lens in the arsenal compresses or factors it. This is NOT a proof of security — by the
2431    /// kernel-certified Chaitin bound (this module) no such proof exists — it is "resists everything we
2432    /// have built," the honest ceiling.
2433    ResistsFullArsenal,
2434}
2435
2436/// Throw the **entire arsenal** at an RSA public key `(N, e)`: the structural factoring suite (small
2437/// factor / close primes / smooth `p−1` / Pollard rho), Wiener's small-exponent attack, and the
2438/// compressibility classifier on the modulus bytes. This is the pre-release safety gate — if any of our
2439/// own mathematics broke RSA, this is where it would surface. `ResistsFullArsenal` is the honest ceiling,
2440/// not a security proof: we certify weakness whenever structure exists and can never certify its absence.
2441pub fn rsa_full_audit(n: &logicaffeine_base::BigInt, e: &logicaffeine_base::BigInt) -> RsaAuditVerdict {
2442    use crate::factor;
2443    if let Some(w) = factor::structural_factor(n, Default::default()) {
2444        return RsaAuditVerdict::Factored { method: w.method, p: w.p, q: w.q };
2445    }
2446    if factor::wiener(e, n).is_some() {
2447        return RsaAuditVerdict::WeakExponent;
2448    }
2449    let (_, bytes) = n.to_le_bytes();
2450    let report = classify_bytes(&bytes);
2451    if report.class != CompressibilityClass::Incompressible {
2452        return RsaAuditVerdict::CompressibleModulus(report.class);
2453    }
2454    RsaAuditVerdict::ResistsFullArsenal
2455}
2456
2457/// Assess key/ciphertext bytes: `Weak` (with a re-checkable compression witness — the concrete attack)
2458/// when the engine describes the data in fewer bytes than storing it raw, else `IncompressibleInClass`.
2459pub fn assess_key_material(data: &[u8]) -> CryptoStrength {
2460    let ints: Vec<i64> = data.iter().map(|&b| b as i64).collect();
2461    let witness = DescriptionBound::of_int_seq(&ints);
2462    let ratio = if data.is_empty() { 1.0 } else { witness.bytes as f64 / data.len() as f64 };
2463    // A description shorter than the raw bytes is a certified weakness — the witness IS the attack.
2464    if witness.bytes < data.len() {
2465        CryptoStrength::Weak { witness, ratio }
2466    } else {
2467        CryptoStrength::IncompressibleInClass { ratio }
2468    }
2469}
2470
2471// ---- Compressibility classes: where an input sits on the ordered → random spectrum --------------
2472//
2473// The description engine tries a whole menu of generators and keeps the shortest. The SHAPE of the
2474// winner is the input's *compressibility class* — a computable, certified shadow of Kolmogorov
2475// complexity — and how far it beats storing the raw bytes is the *degree*. From most to least
2476// structured: Generated (a tiny closed-form program) → Periodic → LowEntropy → Smooth → Incompressible
2477// (algorithmically random relative to this class).
2478
2479/// The compressibility class of an input, read off the winning description-menu generator.
2480#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2481pub enum CompressibilityClass {
2482    /// A closed-form program reproduces it (affine / geometric / polynomial / general generator) —
2483    /// algorithmically simplest: K̄ = O(1) regardless of length.
2484    Generated,
2485    /// A repeating block (cyclic / periodic).
2486    Periodic,
2487    /// Low per-symbol entropy: a dominant value, long runs, or a tiny alphabet (sparse / RLE / dict).
2488    LowEntropy,
2489    /// Smoothly varying: small successive differences or a narrow value range (delta / DoD / frame-of-reference).
2490    Smooth,
2491    /// Incompressible: nothing beats storing it raw — algorithmically random relative to this class.
2492    Incompressible,
2493}
2494
2495/// A full compressibility report: the class, the degree (`K̄/n`), and the raw vs described sizes.
2496#[derive(Clone, Debug)]
2497pub struct CompressibilityReport {
2498    pub class: CompressibilityClass,
2499    /// `K̄ / n` — described bytes over raw bytes. ≈ 1 incompressible; → 0 algorithmically simple.
2500    pub ratio: f64,
2501    pub described_bytes: usize,
2502    pub raw_bytes: usize,
2503}
2504
2505/// Classify a byte string by its compressibility class and degree.
2506pub fn classify_bytes(data: &[u8]) -> CompressibilityReport {
2507    let ints: Vec<i64> = data.iter().map(|&b| b as i64).collect();
2508    let described = describe::describe_int_seq(&ints);
2509    let raw = data.len();
2510    let ratio = if raw == 0 { 1.0 } else { described.len() as f64 / raw as f64 };
2511    let class = if described.len() >= raw.max(1) {
2512        // Nothing beat storing the bytes raw ⇒ incompressible, whatever the nominal winning tag.
2513        CompressibilityClass::Incompressible
2514    } else {
2515        class_of_tag(described.first().copied().unwrap_or(describe::T_INTS))
2516    };
2517    CompressibilityReport { class, ratio, described_bytes: described.len(), raw_bytes: raw }
2518}
2519
2520/// Classify a text string by the compressibility class of its UTF-8 bytes.
2521pub fn classify_text(text: &str) -> CompressibilityReport {
2522    classify_bytes(text.as_bytes())
2523}
2524
2525/// Classify an integer sequence (numeric data, not bytes) by its compressibility class, measured
2526/// against the plain varint encoding — so a linear recurrence (Fibonacci-class) is recognized as a
2527/// closed-form `Generated` program even though it grows past byte range.
2528pub fn classify_int_seq(data: &[i64]) -> CompressibilityReport {
2529    let described = describe::describe_int_seq(data);
2530    let mut baseline = vec![describe::T_INTS];
2531    describe::leb128_encode(&mut baseline, data.iter().copied(), data.len());
2532    let raw = baseline.len();
2533    let ratio = if raw == 0 { 1.0 } else { described.len() as f64 / raw as f64 };
2534    let class = if described.len() >= raw {
2535        CompressibilityClass::Incompressible
2536    } else {
2537        class_of_tag(described.first().copied().unwrap_or(describe::T_INTS))
2538    };
2539    CompressibilityReport { class, ratio, described_bytes: described.len(), raw_bytes: raw }
2540}
2541
2542/// Map a winning description-menu tag to its compressibility class.
2543fn class_of_tag(tag: u8) -> CompressibilityClass {
2544    use describe::*;
2545    match tag {
2546        t if t == T_INTS_AFFINE
2547            || t == T_INTS_GEOMETRIC
2548            || t == T_INTS_POLY
2549            || t == T_GEN
2550            || t == T_INTS_LRECUR =>
2551        {
2552            CompressibilityClass::Generated
2553        }
2554        t if t == T_INTS_PERIODIC => CompressibilityClass::Periodic,
2555        t if t == T_INTS_SPARSE || t == T_INTS_RLE || t == T_INTS_DICT => CompressibilityClass::LowEntropy,
2556        t if t == T_INTS_DELTA || t == T_INTS_DOD || t == T_INTS_FOR => CompressibilityClass::Smooth,
2557        _ => CompressibilityClass::Incompressible, // T_INTS / T_BYTES: no exploitable structure of this class
2558    }
2559}
2560
2561/// Whether the given GF(2) vectors (each ≤ 64-wide) are linearly independent — Gaussian elimination
2562/// over GF(2), full rank iff rank equals the count.
2563fn independent_gf2(vectors: &[Vec<bool>]) -> bool {
2564    let mut rows: Vec<u64> = vectors
2565        .iter()
2566        .map(|v| v.iter().enumerate().fold(0u64, |m, (i, &b)| if b && i < 64 { m | (1u64 << i) } else { m }))
2567        .collect();
2568    let mut rank = 0usize;
2569    for col in 0..64u32 {
2570        if let Some(piv) = (rank..rows.len()).find(|&r| rows[r] & (1u64 << col) != 0) {
2571            rows.swap(rank, piv);
2572            let pr = rows[rank];
2573            for r in 0..rows.len() {
2574                if r != rank && rows[r] & (1u64 << col) != 0 {
2575                    rows[r] ^= pr;
2576                }
2577            }
2578            rank += 1;
2579        }
2580    }
2581    rank == vectors.len()
2582}
2583
2584#[cfg(test)]
2585mod tests {
2586    use super::*;
2587
2588    #[test]
2589    fn description_bound_decode_witness_round_trips() {
2590        // An affine sequence compresses to a closed-form generator; the witness must decode back to
2591        // it, and the bound must be strictly smaller than shipping the raw varint column.
2592        let v: Vec<i64> = (0..200).map(|i| 10 + 7 * i).collect();
2593        let db = DescriptionBound::of_int_seq(&v);
2594        assert!(db.verify(), "the decode witness must reproduce the described sequence");
2595        let baseline = describe::describe_int_seq(&v); // the menu already picked the smallest form…
2596        let mut plain = vec![19u8]; // T_INTS baseline, for a direct "never worse than varint" check
2597        describe::leb128_encode(&mut plain, v.iter().copied(), v.len());
2598        assert!(db.bytes <= plain.len(), "K̄ is never larger than the varint baseline");
2599        assert!(db.bytes < 12, "an affine column ships as a generator (a handful of bytes)");
2600        assert_eq!(db.bytes, baseline.len());
2601    }
2602
2603    #[test]
2604    fn description_bound_rejects_tampered_witness() {
2605        let v: Vec<i64> = (0..50).map(|i| i * i - 3 * i + 5).collect();
2606        let mut db = DescriptionBound::of_int_seq(&v);
2607        assert!(db.verify(), "the honest witness verifies");
2608        // Corrupt the program: decoding now yields a different sequence (or fails), so the recorded
2609        // object hash no longer matches — verification must reject.
2610        if let Descriptor::IntSeq { encoded } = &mut db.descriptor {
2611            let last = encoded.len() - 1;
2612            encoded[last] ^= 0xff;
2613        }
2614        assert!(!db.verify(), "a tampered decode witness must be rejected");
2615    }
2616
2617    #[test]
2618    fn compression_is_never_over_claimed() {
2619        // A pseudo-random column has no closed-form generator, but the menu can still legitimately
2620        // shrink it below plain varint (here frame-of-reference bit-packing of wide ~63-bit values).
2621        // "Not over-claimed" is exactly: K̄ is never LARGER than the varint baseline, and whatever
2622        // smaller bound it reports is a REAL, decodable description (the witness round-trips) — never
2623        // a lossy shortcut.
2624        let mut s = 0x9e37_79b9_7f4a_7c15u64;
2625        let v: Vec<i64> = (0..300)
2626            .map(|_| {
2627                s ^= s << 13;
2628                s ^= s >> 7;
2629                s ^= s << 17;
2630                (s >> 1) as i64
2631            })
2632            .collect();
2633        let db = DescriptionBound::of_int_seq(&v);
2634        assert!(db.verify(), "any reported bound must decode back to the exact sequence");
2635        let mut plain = vec![19u8];
2636        describe::leb128_encode(&mut plain, v.iter().copied(), v.len());
2637        assert!(db.bytes <= plain.len(), "K̄ is never larger than the varint baseline");
2638    }
2639
2640    /// A genuinely NON-linear byte source (splitmix64's finalizer). A linear generator like xorshift is
2641    /// deliberately avoided here: Berlekamp–Massey now BREAKS linear generators (they are exactly the
2642    /// weakness the LFSR detector catches), so only a nonlinear source is incompressible.
2643    fn splitmix_bytes(n: usize, mut s: u64) -> Vec<u8> {
2644        (0..n)
2645            .map(|_| {
2646                s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
2647                let mut z = s;
2648                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2649                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2650                z ^= z >> 31;
2651                (z >> 24) as u8
2652            })
2653            .collect()
2654    }
2655
2656    #[test]
2657    fn structured_key_material_is_certified_weak() {
2658        // A "key" with a repeating byte pattern is predictable: the engine finds a short description,
2659        // and that description IS the attack — it re-decodes to the exact key.
2660        let weak_key: Vec<u8> = (0..300).map(|i| (i % 7) as u8).collect();
2661        match assess_key_material(&weak_key) {
2662            CryptoStrength::Weak { witness, ratio } => {
2663                assert!(witness.verify(), "the compression witness reproduces the key (the attack)");
2664                assert!(ratio < 0.5, "a predictable key is far shorter than its raw bytes, got {ratio}");
2665            }
2666            CryptoStrength::IncompressibleInClass { .. } => panic!("a periodic key must be flagged weak"),
2667        }
2668    }
2669
2670    #[test]
2671    fn lfsr_keystream_key_is_certified_weak() {
2672        // An LFSR keystream "key" looks statistically random but is generated by a short register.
2673        // Berlekamp–Massey recovers it, so the description engine flags the key as WEAK and the witness
2674        // (the register) reproduces the whole keystream — the classic stream-cipher attack, certified.
2675        let taps = [false, false, true, false, false, false, true]; // x⁷+x³+1, period 127
2676        let seed = [true, false, true, true, false, false, true];
2677        let bits = describe::lfsr_generate(&taps, &seed, 200 * 8);
2678        let key: Vec<u8> = describe::bits_to_bytes(&bits).iter().map(|&x| x as u8).collect();
2679        match assess_key_material(&key) {
2680            CryptoStrength::Weak { witness, ratio } => {
2681                assert!(witness.verify(), "the recovered register reproduces the keystream (the attack)");
2682                assert!(ratio < 0.2, "the key collapses to its register, ratio {ratio}");
2683            }
2684            CryptoStrength::IncompressibleInClass { .. } => panic!("an LFSR keystream key must be weak"),
2685        }
2686    }
2687
2688    #[test]
2689    fn word_and_two_adic_complexity_detect_their_keystreams() {
2690        // A GF(256) word-LFSR keystream → low word complexity.
2691        let taps = [describe::Gf256(0x02), describe::Gf256(0x8d), describe::Gf256(0x1f)];
2692        let seed = [describe::Gf256(0x41), describe::Gf256(0x9c), describe::Gf256(0x07)];
2693        let word_key: Vec<u8> = describe::lfsr_generate_field(&taps, &seed, 120).iter().map(|g| g.0).collect();
2694        assert_eq!(gf256_word_complexity(&word_key), 3, "word-LFSR keystream has word complexity 3");
2695
2696        // An FCSR keystream → low 2-adic complexity even though its LINEAR complexity is high.
2697        let bits = describe::fcsr_generate(
2698            &logicaffeine_base::BigInt::from_i64(7),
2699            &logicaffeine_base::BigInt::from_i64(19),
2700            8 * 60,
2701        );
2702        let fcsr_key: Vec<u8> = describe::bits_to_bytes(&bits).iter().map(|&x| x as u8).collect();
2703        assert!(two_adic_complexity_of_bytes(&fcsr_key) < 12, "FCSR key has low 2-adic complexity");
2704
2705        // Nonlinear random bytes are high complexity by BOTH measures.
2706        let random = splitmix_bytes(120, 0x1357_9bdf_2468_ace0);
2707        assert!(gf256_word_complexity(&random) > 30, "random has high word complexity");
2708        assert!(two_adic_complexity_of_bytes(&random) > 200, "random ≈ n/2 2-adic complexity");
2709        // Maximal order complexity is the TOP of the hierarchy: ≤ the bit linear complexity for any data.
2710        let bits: Vec<bool> = random.iter().flat_map(|&b| (0..8).map(move |j| (b >> j) & 1 == 1)).collect();
2711        assert!(
2712            maximal_order_complexity_of_bytes(&random) <= describe::berlekamp_massey_gf2(&bits).0,
2713            "MOC ≤ linear complexity for everything (it is the shorter, more general register)"
2714        );
2715    }
2716
2717    #[test]
2718    fn algebraic_attack_breaks_a_nonlinear_keystream_that_maximal_order_only_measures() {
2719        // A QUADRATIC order-8 nonlinear feedback keystream:
2720        //   s[i] = s[i-1] ⊕ s[i-5] ⊕ s[i-6] ⊕ s[i-8] ⊕ (s[i-1] AND s[i-6]).
2721        // Its bit linear complexity is 246 and its 2-adic complexity is high — every LINEAR rung is
2722        // fooled. Maximal order complexity sees the true register (order 8) but only as a 2⁸=256-entry
2723        // truth table. The ALGEBRAIC attack recovers the sparse degree-2 ANF and REGENERATES it.
2724        let seed: Vec<bool> = (0..8).map(|k| (0x9E37u64 >> k) & 1 == 1).collect();
2725        let mut bits = seed.clone();
2726        while bits.len() < 8 * 80 {
2727            let i = bits.len();
2728            let s = |k: usize| bits[i - k];
2729            bits.push(s(1) ^ s(5) ^ s(6) ^ s(8) ^ (s(1) & s(6)));
2730        }
2731        let key: Vec<u8> = describe::bits_to_bytes(&bits).iter().map(|&x| x as u8).collect();
2732
2733        // Every LINEAR rung is fooled; the maximal-order rung only MEASURES the register.
2734        assert!(gf256_word_complexity(&key) > 30, "word-LFSR rung fooled by the nonlinear feedback");
2735        assert!(two_adic_complexity_of_bytes(&key) > 100, "2-adic rung fooled too");
2736        assert!(maximal_order_complexity_of_bytes(&key) <= 8, "maximal order complexity sees the order-8 register");
2737
2738        // The algebraic attack RECOVERS it — a sparse ANF, far below the truth table.
2739        let attack = algebraic_attack_on_bytes(&key, 2, 16).expect("degree-2 attack recovers the register");
2740        assert!(attack.order <= 8, "the shortest register found is order ≤ 8, got {}", attack.order);
2741        assert_eq!(attack.truth_table, 1 << attack.order, "truth-table size is 2^order");
2742        assert!(
2743            attack.anf_terms < attack.truth_table,
2744            "the ANF ({} terms) is smaller than the truth table ({})",
2745            attack.anf_terms,
2746            attack.truth_table
2747        );
2748
2749        // The ANF is a re-checkable witness: replay it from the seed and it reproduces the keystream.
2750        let regen = describe::algebraic_generate(attack.order, attack.degree, &attack.anf, &bits[..attack.order], bits.len());
2751        assert_eq!(regen, bits, "the recovered ANF regenerates the whole nonlinear keystream — the attack");
2752    }
2753
2754    #[test]
2755    fn combiner_scan_breaks_geffe_but_not_a_cryptographic_keystream() {
2756        // Build a Geffe combiner keystream (z = x2 ? x1 : x3) as bytes.
2757        let n = 8 * 250; // exactly 250 bytes, no bit padding
2758        let taps1 = [false, false, true, false, false, false, true]; // L=7
2759        let taps2 = [false, false, true, false, true]; // L=5 (protected middle)
2760        let taps3 = [false, false, false, false, true, false, false, false, true]; // L=9
2761        let seed1 = [true, false, true, true, false, false, true];
2762        let seed2 = [true, true, false, false, true];
2763        let seed3 = [true, false, false, true, false, true, true, false, true];
2764        let x1 = describe::lfsr_generate(&taps1, &seed1, n);
2765        let x2 = describe::lfsr_generate(&taps2, &seed2, n);
2766        let x3 = describe::lfsr_generate(&taps3, &seed3, n);
2767        let z: Vec<bool> = (0..n).map(|i| if x2[i] { x1[i] } else { x3[i] }).collect();
2768        let key: Vec<u8> = describe::bits_to_bytes(&z).iter().map(|&x| x as u8).collect();
2769
2770        let candidates = vec![taps1.to_vec(), taps2.to_vec(), taps3.to_vec()];
2771        let leaks = scan_for_combiner_leaks(&key, &candidates, 3.0);
2772
2773        // The two correlated registers leak (indices 0 and 2); the correlation-immune middle (1) does not.
2774        let leaking: Vec<usize> = leaks.iter().map(|l| l.candidate_index).collect();
2775        assert_eq!(leaking, vec![0, 2], "Geffe leaks its outer registers, not the protected middle");
2776        // Each leak recovers the exact hidden register — a re-checkable witness (regenerate and compare).
2777        for leak in &leaks {
2778            let taps = &candidates[leak.candidate_index];
2779            let regen = describe::lfsr_generate(taps, &leak.attack.init_state, n);
2780            let planted = if leak.candidate_index == 0 { &x1 } else { &x3 };
2781            assert_eq!(&regen, planted, "the recovered initial state regenerates the hidden register");
2782            assert!(leak.margin > 3.0, "the leak clears the significance threshold, margin {}", leak.margin);
2783        }
2784        // Divide-and-conquer: two independent 2⁷ and 2⁹ searches replace a 2^(7+5+9)=2²¹ joint search.
2785
2786        // A cryptographic (splitmix) keystream correlates with NONE of the candidates — the ceiling.
2787        let random = splitmix_bytes(250, 0xfeed_face_0bad_c0de);
2788        assert!(
2789            scan_for_combiner_leaks(&random, &candidates, 3.0).is_empty(),
2790            "a strong keystream leaks no register to first-order correlation — the ceiling"
2791        );
2792    }
2793
2794    #[test]
2795    fn linear_cryptanalysis_reads_the_whole_spectrum_where_correlation_reads_one_bit() {
2796        // The CI(1) combiner f = a1 ⊕ a2 ⊕ (a3 ∧ a4): correlation-immune to first order (E blind), yet a
2797        // weight-2 linear approximation leaks with bias ¼. linear_cryptanalysis surfaces exactly that.
2798        let f: Vec<bool> = (0..16)
2799            .map(|x| {
2800                let b = |i: usize| (x >> i) & 1 == 1;
2801                b(0) ^ b(1) ^ (b(2) & b(3))
2802            })
2803            .collect();
2804        let d = linear_cryptanalysis(&f).expect("well-formed");
2805        assert_eq!(d.immunity_order, 1, "first-order correlation-immune — E finds nothing");
2806        assert_eq!(d.bias, 0.25, "but a linear approximation leaks with bias ¼");
2807        assert!(d.mask_weight >= 2, "at weight ≥ 2 — the multi-register leak beyond E's reach");
2808
2809        // The bent ceiling: g = a1a2 ⊕ a3a4 has maximal nonlinearity and no approximation beating 1/8.
2810        let g: Vec<bool> = (0..16)
2811            .map(|x| {
2812                let b = |i: usize| (x >> i) & 1 == 1;
2813                (b(0) & b(1)) ^ (b(2) & b(3))
2814            })
2815            .collect();
2816        let dg = linear_cryptanalysis(&g).expect("well-formed");
2817        assert_eq!(dg.nonlinearity, 6, "bent ⇒ maximal nonlinearity 6 for n=4");
2818        assert_eq!(dg.bias, 0.125, "no linear approximation beats the flat-spectrum floor 1/8 — the ceiling");
2819    }
2820
2821    #[test]
2822    fn algebraic_immunity_report_grades_filters_and_the_attack_recovers_state() {
2823        // A low-immunity filter is weak; a maximal-immunity one is the ceiling.
2824        let weak: Vec<bool> = (0..16).map(|x| (x >> 0) & 1 == 1).collect(); // C = x1, AI 1
2825        let wr = algebraic_immunity_of(&weak).expect("well-formed");
2826        assert_eq!(wr.immunity, 1, "an affine filter has AI 1");
2827        assert!(!wr.is_maximal, "AI 1 < ⌈4/2⌉ = 2 — exploitable");
2828        assert!(describe::verify_annihilator(&weak, &wr.witness), "the annihilator re-checks");
2829
2830        let bent: Vec<bool> = (0..16)
2831            .map(|x| {
2832                let b = |i: usize| (x >> i) & 1 == 1;
2833                (b(0) & b(1)) ^ (b(2) & b(3))
2834            })
2835            .collect();
2836        let br = algebraic_immunity_of(&bent).expect("well-formed");
2837        assert_eq!(br.immunity, 2, "the bent filter has AI 2");
2838        assert!(br.is_maximal, "AI 2 = ⌈4/2⌉ — maximal algebraic immunity, the ceiling");
2839
2840        // The full break: recover a filter generator's secret state from its keystream alone.
2841        let taps = [false, false, false, false, false, false, true, false, false, true]; // x¹⁰+x³+1
2842        let s0 = [true, true, false, true, false, true, true, false, false, true];
2843        let filter: Vec<bool> = (0..8).map(|x| (x as u32).count_ones() >= 2).collect(); // maj3, AI 2
2844        let (m, n) = (3usize, 400usize);
2845        let seq = describe::lfsr_generate(&taps, &s0, n + m);
2846        let keystream: Vec<bool> = (0..n)
2847            .map(|t| {
2848                let idx = (0..m).fold(0usize, |a, i| a | (usize::from(seq[t + i]) << i));
2849                filter[idx]
2850            })
2851            .collect();
2852        let recovered = algebraic_filter_attack(&keystream, &taps, &filter).expect("attack succeeds");
2853        assert_eq!(recovered, s0.to_vec(), "the algebraic attack recovers the exact secret state");
2854    }
2855
2856    #[test]
2857    fn fast_correlation_scales_the_correlation_break_past_exhaustive_search() {
2858        // A length-17 LFSR (x¹⁷+x³+1) leaking through a 12%-noise channel: the exhaustive correlation
2859        // attack would try 2¹⁷ states, but the decoder recovers the register in polynomial time.
2860        let mut taps = vec![false; 17];
2861        taps[13] = true;
2862        taps[16] = true;
2863        let seed: Vec<bool> = (0..17).map(|k| (0x5A5Au64 >> k) & 1 == 1).collect();
2864        let n = 4000;
2865        let a = describe::lfsr_generate(&taps, &seed, n);
2866        let mut st = 0x0f1e_2d3c_4b5a_6978u64;
2867        let z: Vec<bool> = a
2868            .iter()
2869            .map(|&bit| {
2870                st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
2871                bit ^ ((st >> 40) % 100 < 12)
2872            })
2873            .collect();
2874        let state = fast_correlation_attack(&z, &taps, 400).expect("decodes the leaking register");
2875        assert_eq!(state, seed, "the recovered state IS the register key — no 2¹⁷ search");
2876    }
2877
2878    #[test]
2879    fn shrinking_generator_falls_to_clock_control_inversion() {
2880        // A clock register (L=7) shrinks a data register (L=9): the output is a data-dependent decimation
2881        // with high linear complexity that no lower rung touches. Guess the clock, linear-solve the data.
2882        let a_taps = [false, false, true, false, false, false, true];
2883        let s_taps = [false, false, false, false, true, false, false, false, true];
2884        let a_seed = [true, true, false, false, true, false, true];
2885        let s_seed = [false, true, true, false, true, true, false, false, true];
2886        let m = 300;
2887        let output = describe::shrinking_generator(&a_taps, &a_seed, &s_taps, &s_seed, m);
2888        let (a_rec, s_rec) = attack_shrinking_generator(&output, &a_taps, &s_taps).expect("the generator falls");
2889        assert_eq!(
2890            describe::shrinking_generator(&a_taps, &a_rec, &s_taps, &s_rec, 800),
2891            describe::shrinking_generator(&a_taps, &a_seed, &s_taps, &s_seed, 800),
2892            "the recovered registers reproduce the keystream well past the attacked length — a full break"
2893        );
2894    }
2895
2896    #[test]
2897    fn full_arsenal_audit_flags_weak_keys_and_clears_a_sound_one() {
2898        use crate::factor;
2899        use logicaffeine_base::BigInt;
2900        let big = |s: &str| BigInt::parse_decimal(s).unwrap();
2901        let one = BigInt::from_i64(1);
2902        let e = BigInt::from_i64(65537);
2903
2904        // Weak by close primes → factored by the structural suite.
2905        let p = factor::next_prime(&big("1000000000000000000000000000057"));
2906        let q = factor::next_prime(&p.add(&BigInt::from_i64(100)));
2907        let n = p.mul(&q);
2908        assert!(matches!(rsa_full_audit(&n, &e), RsaAuditVerdict::Factored { .. }), "close primes caught");
2909
2910        // Weak by a small private exponent → caught by Wiener (primes well separated, so factoring misses).
2911        let p = factor::next_prime(&big("1000000000000000"));
2912        let q = factor::next_prime(&big("3000000000000000"));
2913        let n = p.mul(&q);
2914        let phi = p.sub(&one).mul(&q.sub(&one));
2915        let small_d = BigInt::from_i64(7919);
2916        let big_e = factor::mod_inverse(&small_d, &phi).expect("d coprime to φ");
2917        assert_eq!(rsa_full_audit(&n, &big_e), RsaAuditVerdict::WeakExponent, "small d caught by Wiener");
2918
2919        // A sound key → resists every lens we have. THE RELEASE-SAFETY RESULT: our own mathematics does
2920        // not break a soundly-generated RSA key.
2921        let p = factor::next_prime(&big("1000000000000000000000000000057"));
2922        let q = factor::next_prime(&big("9000000000000000000000000000000"));
2923        let n = p.mul(&q);
2924        assert_eq!(
2925            rsa_full_audit(&n, &e),
2926            RsaAuditVerdict::ResistsFullArsenal,
2927            "a sound RSA key resists the entire arsenal — we are not breaking RSA"
2928        );
2929    }
2930
2931    #[test]
2932    fn recursive_reduction_reaches_the_incompressible_fixed_point() {
2933        // A cleanly periodic sequence is broken down, then bottoms out at a small irreducible core.
2934        let structured: Vec<u8> = (0..300).map(|i| [3u8, 1, 4, 1, 5, 9, 2, 6][i % 8]).collect();
2935        let r = recursive_reduce(&structured);
2936        assert!(r.compressed, "the periodic sequence is symmetry-broken down");
2937        assert!(r.irreducible_bytes < structured.len() / 2, "and reaches a small fixed point, {:?}", r.sizes);
2938
2939        // A cryptographic-random sequence: no lens fires, so the fixed point IS the object itself — depth
2940        // 0. This is "resists our arsenal," NOT "proven structureless" (the terminus Chaitin forbids).
2941        let random: Vec<u8> = (0..300u32)
2942            .map(|i| {
2943                let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i as u64 + 1);
2944                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2945                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2946                ((z ^ (z >> 31)) & 0xFF) as u8
2947            })
2948            .collect();
2949        let r = recursive_reduce(&random);
2950        assert_eq!(r.depth, 0, "random has no structure any lens breaks — the fixed point is the object");
2951        assert!(!r.compressed, "no lens fires — the residue, which we cannot prove is truly structureless");
2952    }
2953
2954    // A deterministic pseudorandom labeling of the n-cube's corners: dense high-degree ANF, all variables
2955    // relevant, high nonlinearity — the residue class the structure finder cannot compress.
2956    fn pseudorandom_truth(n: usize) -> Vec<bool> {
2957        (0..1u64 << n)
2958            .map(|i| {
2959                let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i + 1);
2960                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2961                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2962                (z ^ (z >> 31)) & 1 == 1
2963            })
2964            .collect()
2965    }
2966
2967    #[test]
2968    fn find_structure_reads_a_constant_off_the_cube() {
2969        let truth = vec![true; 8]; // n=3, f ≡ 1
2970        let r = find_structure(&truth).expect("a well-formed cube");
2971        assert_eq!(r.num_vars, 3);
2972        assert!(matches!(r.class, CubeStructure::Constant(true)));
2973        assert!(r.compressed && r.description_bits == 1, "the whole cube in one bit");
2974        assert_eq!(r.class.reconstruct(3).as_deref(), Some(&truth[..]));
2975    }
2976
2977    #[test]
2978    fn find_structure_reads_a_junta_off_the_coordinate_walk() {
2979        // MAJ(x0,x1,x2) embedded in an 8-variable cube: only 3 of 8 variables are relevant, and its ANF is
2980        // dense enough (weight 3) that the junta description (17 bits) beats the low-degree one (24 bits).
2981        let n = 8;
2982        let truth: Vec<bool> = (0..1usize << n).map(|x| (x & 0b111).count_ones() >= 2).collect();
2983        let r = find_structure(&truth).unwrap();
2984        match &r.class {
2985            CubeStructure::Junta { relevant, .. } => assert_eq!(relevant, &vec![0, 1, 2]),
2986            other => panic!("expected a junta, got {other:?}"),
2987        }
2988        assert!(r.compressed);
2989        assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
2990    }
2991
2992    #[test]
2993    fn find_structure_reads_affine_parity_off_the_walsh_walk() {
2994        // Parity of all four variables: every variable relevant (no junta), degree 1 (nonlinearity 0).
2995        let n = 4;
2996        let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() % 2 == 1).collect();
2997        let r = find_structure(&truth).unwrap();
2998        match &r.class {
2999            CubeStructure::Affine { coeffs, constant } => {
3000                assert_eq!(coeffs, &vec![true; 4]);
3001                assert!(!constant);
3002            }
3003            other => panic!("expected affine, got {other:?}"),
3004        }
3005        assert!(r.compressed && r.description_bits == n + 1);
3006        assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
3007    }
3008
3009    #[test]
3010    fn find_structure_reads_a_symmetric_function_off_the_weight_shells() {
3011        // MAJ3 over all three variables: nonlinear, every variable relevant, depends only on Hamming weight.
3012        let n = 3;
3013        let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() >= 2).collect();
3014        let r = find_structure(&truth).unwrap();
3015        assert!(matches!(r.class, CubeStructure::Symmetric { .. }), "got {:?}", r.class);
3016        assert!(r.compressed);
3017        assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
3018    }
3019
3020    #[test]
3021    fn find_structure_reads_low_degree_off_the_mobius_walk() {
3022        // The inner-product bent function x0x1 ⊕ x2x3 ⊕ x4x5: high nonlinearity (so the affine lens fails)
3023        // but a sparse degree-2 ANF the Möbius walk reads off in three monomials.
3024        let n = 6;
3025        let truth: Vec<bool> = (0..1usize << n)
3026            .map(|x| {
3027                let b = |i: usize| x & (1usize << i) != 0;
3028                (b(0) && b(1)) ^ (b(2) && b(3)) ^ (b(4) && b(5))
3029            })
3030            .collect();
3031        let r = find_structure(&truth).unwrap();
3032        match &r.class {
3033            CubeStructure::LowDegree { degree, .. } => assert_eq!(*degree, 2),
3034            other => panic!("expected low-degree, got {other:?}"),
3035        }
3036        assert!(r.compressed);
3037        assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]));
3038    }
3039
3040    #[test]
3041    fn find_structure_finds_no_lens_for_a_pseudorandom_function() {
3042        // The residue: no lens fires. "Resisted our arsenal," NOT "structureless" (the Chaitin terminus).
3043        let n = 6;
3044        let truth = pseudorandom_truth(n);
3045        let r = find_structure(&truth).unwrap();
3046        assert!(matches!(r.class, CubeStructure::ResistedArsenal { .. }), "got {:?}", r.class);
3047        assert!(!r.compressed, "the residue does not beat storing the truth table");
3048        assert!(r.class.reconstruct(n).is_none(), "the residue carries no compressed witness");
3049    }
3050
3051    #[test]
3052    fn structure_witness_is_re_checkable_and_rejects_tampering() {
3053        let n = 4;
3054        let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() % 2 == 1).collect();
3055        let r = find_structure(&truth).unwrap();
3056        assert_eq!(r.class.reconstruct(n).as_deref(), Some(&truth[..]), "the witness reproduces the function");
3057        let tampered = match r.class {
3058            CubeStructure::Affine { mut coeffs, constant } => {
3059                coeffs[0] = !coeffs[0];
3060                CubeStructure::Affine { coeffs, constant }
3061            }
3062            other => panic!("expected affine, got {other:?}"),
3063        };
3064        assert_ne!(tampered.reconstruct(n).as_deref(), Some(&truth[..]), "a tampered witness does not");
3065    }
3066
3067    #[test]
3068    fn structure_cover_peels_a_sparse_function_to_a_small_residue() {
3069        // x0x1 ⊕ x2x3 ⊕ x4x5: three degree-2 slices cover the whole cube; nothing higher remains.
3070        let n = 6;
3071        let truth: Vec<bool> = (0..1usize << n)
3072            .map(|x| {
3073                let b = |i: usize| x & (1usize << i) != 0;
3074                (b(0) && b(1)) ^ (b(2) && b(3)) ^ (b(4) && b(5))
3075            })
3076            .collect();
3077        let c = structure_cover(&truth).unwrap();
3078        assert_eq!(c.monomials_by_degree, vec![0, 0, 3, 0, 0, 0, 0], "only the degree-2 slice is populated");
3079        assert_eq!(c.total_monomials, 3);
3080        assert_eq!((c.residue_degree, c.residue_monomials), (2, 3), "the residue is three quadratic terms");
3081        assert_eq!(c.monomials_by_degree.iter().sum::<usize>(), c.total_monomials, "the slices sum to the whole");
3082        assert!(c.compressed, "a sparse ANF covers all 2^n corners cheaply");
3083    }
3084
3085    #[test]
3086    fn structure_cover_of_a_constant_is_the_degree_zero_slice() {
3087        let c = structure_cover(&vec![true; 8]).unwrap(); // n=3, f ≡ 1
3088        assert_eq!(c.monomials_by_degree, vec![1, 0, 0, 0], "just the constant term");
3089        assert_eq!((c.total_monomials, c.residue_degree), (1, 0));
3090        assert!(c.compressed);
3091    }
3092
3093    #[test]
3094    fn structure_cover_leaves_a_dense_high_degree_residue_for_a_pseudorandom_function() {
3095        // No low-degree peel covers it: the monomials spread up to the top degree and the ANF stays dense —
3096        // the residue we "missed," and WHY (genuine high-order interaction, not low-degree structure).
3097        let n = 6;
3098        let truth = pseudorandom_truth(n);
3099        let c = structure_cover(&truth).unwrap();
3100        assert_eq!(c.monomials_by_degree.iter().sum::<usize>(), c.total_monomials);
3101        assert!(c.residue_degree >= 4, "structure reaches high degree, got {}", c.residue_degree);
3102        assert!(c.total_monomials > n, "a dense ANF, not a sparse low-degree cover");
3103        assert!(!c.compressed, "the peel does not beat storing the truth table — the incompressible core");
3104    }
3105
3106    // f(x) = h(x0⊕x1, x2, x3, x4, x5) — a ROTATED junta: invariant under a = e0⊕e1, so it collapses to a
3107    // 5-variable function, yet every one of the six coordinates is individually relevant.
3108    fn rotated_junta(n_free: usize) -> Vec<bool> {
3109        let h = pseudorandom_truth(n_free); // dense on its 5 inputs
3110        (0..1usize << (n_free + 1))
3111            .map(|x| {
3112                let u0 = ((x & 1) ^ ((x >> 1) & 1)) & 1; // x0 ⊕ x1
3113                let rest = x >> 2; // x2..x5 → h inputs 1..(n_free-1)
3114                h[u0 | (rest << 1)]
3115            })
3116            .collect()
3117    }
3118
3119    #[test]
3120    fn linear_structures_finds_none_in_a_pseudorandom_function() {
3121        // The residue survives even the affine-group lens: no direction has a constant derivative.
3122        let truth = pseudorandom_truth(6);
3123        let r = linear_structures(&truth).unwrap();
3124        assert_eq!(r.dim(), 0, "a random function has trivial linear space, got basis {:?}", r.basis);
3125        assert!(!r.is_reducible());
3126        assert!(reduce_by_invariance(&truth).is_none(), "nothing to peel");
3127        assert!(r.verify(&truth), "the empty witness trivially re-checks");
3128    }
3129
3130    #[test]
3131    fn linear_structures_peel_a_rotated_junta_the_coordinate_lenses_miss() {
3132        let n = 6;
3133        let truth = rotated_junta(5);
3134        // The base arsenal sees dense, all-variables-relevant structure → residue.
3135        let base = find_structure(&truth).unwrap();
3136        assert!(matches!(base.class, CubeStructure::ResistedArsenal { .. }), "base arsenal: {:?}", base.class);
3137        // But the derivative symmetry finds the rotated invariance a = e0 ⊕ e1 = 3.
3138        let ls = linear_structures(&truth).unwrap();
3139        assert!(ls.is_reducible(), "the rotated junta has a nontrivial linear space");
3140        assert_eq!(reduce_by_basis(0b11, &ls.basis), 0, "a = e0 ⊕ e1 lies in the linear space");
3141        assert!(ls.verify(&truth), "the linear-structure witness re-checks");
3142        // And the invariance peel actually collapses a whole dimension.
3143        let (red, basis) = reduce_by_invariance(&truth).expect("a rotated junta reduces");
3144        assert!(red.reduced_vars < n, "peeled at least one dimension: {} → {}", n, red.reduced_vars);
3145        assert!(red.verify(&truth, &basis), "the reduced function reproduces the original on every corner");
3146    }
3147
3148    #[test]
3149    fn affine_reduce_peels_a_residue_plus_a_linear_form() {
3150        // f(x) = g(x1..x5) ⊕ x0, with g a dense residue on 5 variables. Only a complement structure (e0),
3151        // no invariance and no other axis — the deep finder currently calls it residue.
3152        let n = 6;
3153        let g = pseudorandom_truth(5);
3154        let truth: Vec<bool> = (0..1usize << n).map(|x| g[x >> 1] ^ (x & 1 != 0)).collect();
3155        assert!(matches!(find_structure(&truth).unwrap().class, CubeStructure::ResistedArsenal { .. }));
3156        assert!(reduce_by_invariance(&truth).is_none(), "no pure invariance to peel");
3157        let ar = affine_reduce(&truth).expect("the linear form peels off");
3158        assert!(ar.reduced_truth.len() < truth.len(), "reduced onto fewer coordinates");
3159        assert_eq!(ar.reconstruct().as_deref(), Some(&truth[..]), "h ⊕ ℓ reconstructs f exactly");
3160    }
3161
3162    #[test]
3163    fn linear_structure_witness_rejects_tampering() {
3164        let truth = rotated_junta(5);
3165        let r = linear_structures(&truth).unwrap();
3166        assert!(r.verify(&truth));
3167        // Flip a recorded derivative: the constancy re-check must fail.
3168        let mut bad = r.clone();
3169        bad.derivative[0] = !bad.derivative[0];
3170        assert!(!bad.verify(&truth), "a tampered derivative is caught");
3171        // Corrupt a basis vector to a non-structure direction: also caught.
3172        let mut bad2 = r.clone();
3173        bad2.basis[0] ^= 0b100; // add x2 — no longer a constant derivative
3174        assert!(!bad2.verify(&truth), "a tampered basis vector is caught");
3175    }
3176
3177    // Apply an invertible GF(2) linear change of variables g(x) = f(Ax); `rows[i]` is the linear form for
3178    // output coordinate i. The Walsh multiset — and hence the affine signature — is invariant under this.
3179    fn apply_linear_input_map(truth: &[bool], rows: &[usize]) -> Vec<bool> {
3180        (0..truth.len())
3181            .map(|x| {
3182                let ax = rows
3183                    .iter()
3184                    .enumerate()
3185                    .fold(0usize, |acc, (i, &r)| if (r & x).count_ones() % 2 == 1 { acc | (1 << i) } else { acc });
3186                truth[ax]
3187            })
3188            .collect()
3189    }
3190
3191    #[test]
3192    fn affine_signature_is_invariant_under_a_linear_change_of_basis() {
3193        // A lower-triangular unit-diagonal matrix — invertible, and it genuinely mixes coordinates.
3194        let rows = vec![0b0001usize, 0b0011, 0b0111, 0b1111];
3195        assert_eq!(gf2_echelon_basis(&rows).len(), 4, "the map must be invertible");
3196        let bent: Vec<bool> = (0..16)
3197            .map(|x| {
3198                let b = |i: usize| x & (1 << i) != 0;
3199                (b(0) && b(1)) ^ (b(2) && b(3))
3200            })
3201            .collect();
3202        for f in [bent, pseudorandom_truth(4), (0..16).map(|x: usize| (x as u32).count_ones() % 2 == 1).collect()] {
3203            let g = apply_linear_input_map(&f, &rows);
3204            assert_eq!(
3205                affine_signature(&f).unwrap().walsh_abs_distribution,
3206                affine_signature(&g).unwrap().walsh_abs_distribution,
3207                "the Walsh multiset is an affine invariant — a rotation cannot change the class"
3208            );
3209        }
3210    }
3211
3212    #[test]
3213    fn affine_signature_recognizes_bent_and_plateaued_and_reads_the_linear_dimension() {
3214        // Bent x0x1 ⊕ x2x3: one amplitude 2^{n/2}=4, trivial linear space (k=0).
3215        let bent: Vec<bool> = (0..16)
3216            .map(|x| {
3217                let b = |i: usize| x & (1 << i) != 0;
3218                (b(0) && b(1)) ^ (b(2) && b(3))
3219            })
3220            .collect();
3221        let sb = affine_signature(&bent).unwrap();
3222        assert!(sb.is_plateaued && sb.is_bent && sb.amplitude == Some(4));
3223        assert_eq!(sb.implied_linear_dim(), Some(0));
3224        assert_eq!(sb.implied_linear_dim(), Some(linear_structures(&bent).unwrap().dim()));
3225
3226        // Partially-bent x0x1 ⊕ x2 (n=3): plateaued amplitude 2^{(3+1)/2}=4, NOT bent (odd n), linear dim 1.
3227        let pb: Vec<bool> = (0..8)
3228            .map(|x| {
3229                let b = |i: usize| x & (1 << i) != 0;
3230                (b(0) && b(1)) ^ b(2)
3231            })
3232            .collect();
3233        let sp = affine_signature(&pb).unwrap();
3234        assert!(sp.is_plateaued && !sp.is_bent && sp.amplitude == Some(4));
3235        assert_eq!(sp.implied_linear_dim(), Some(1), "amplitude 2^{{(n+k)/2}} reads off k=1");
3236        assert_eq!(sp.implied_linear_dim(), Some(linear_structures(&pb).unwrap().dim()), "the two rungs agree");
3237
3238        // A pseudorandom function is not plateaued: a spread of amplitudes, no small affine class.
3239        let rnd = pseudorandom_truth(6);
3240        let sr = affine_signature(&rnd).unwrap();
3241        assert!(!sr.is_plateaued && sr.amplitude.is_none(), "the residue has many Walsh amplitudes");
3242        assert!(sr.walsh_abs_distribution.len() >= 3, "a genuine spread of amplitudes");
3243    }
3244
3245    // Two dense pseudorandom functions on 3 variables each, joined by XOR — the block content is arbitrary
3246    // but the halves {0,1,2} and {3,4,5} never share a monomial, so they are independent blocks.
3247    fn split_dense(seed_lo: u64, seed_hi: u64) -> Vec<bool> {
3248        let bit = |i: u64, s: u64| {
3249            let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i + s);
3250            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3251            (z ^ (z >> 27)) & 1 == 1
3252        };
3253        (0..64usize).map(|x| bit((x & 0b111) as u64, seed_lo) ^ bit(((x >> 3) & 0b111) as u64, seed_hi)).collect()
3254    }
3255
3256    #[test]
3257    fn separable_decomposition_splits_independent_blocks() {
3258        // x0x1 ⊕ x2x3 ⊕ x4x5: three independent pairs.
3259        let n = 6;
3260        let truth: Vec<bool> = (0..1usize << n)
3261            .map(|x| {
3262                let b = |i: usize| x & (1usize << i) != 0;
3263                (b(0) && b(1)) ^ (b(2) && b(3)) ^ (b(4) && b(5))
3264            })
3265            .collect();
3266        let d = separable_decomposition(&truth).unwrap();
3267        assert_eq!(d.blocks, vec![vec![0, 1], vec![2, 3], vec![4, 5]], "the three pairs split apart");
3268        assert!(d.is_separable());
3269        assert!(d.table_bits() < truth.len(), "Σ 2^|B| = 12 beats 2ⁿ = 64");
3270        assert_eq!(d.reconstruct(n).as_deref(), Some(&truth[..]), "the blocks XOR back to the whole");
3271    }
3272
3273    #[test]
3274    fn separable_decomposition_peels_dense_blocks_apart() {
3275        // g(x0,x1,x2) ⊕ h(x3,x4,x5) with g, h dense: no block spans both halves, whatever the content.
3276        let n = 6;
3277        let truth = split_dense(1, 999);
3278        let d = separable_decomposition(&truth).unwrap();
3279        assert!(d.is_separable(), "the two dense halves are independent blocks");
3280        for b in &d.blocks {
3281            let lo = b.iter().any(|&i| i < 3);
3282            let hi = b.iter().any(|&i| i >= 3);
3283            assert!(!(lo && hi), "no block bridges the two independent halves: {b:?}");
3284        }
3285        assert_eq!(d.reconstruct(n).as_deref(), Some(&truth[..]));
3286    }
3287
3288    #[test]
3289    fn a_connected_function_is_one_irreducible_block() {
3290        // A path x0x1 ⊕ x1x2 ⊕ x2x3 ⊕ x3x4 links all five variables: no direct-sum split.
3291        let n = 5;
3292        let truth: Vec<bool> = (0..1usize << n)
3293            .map(|x| {
3294                let b = |i: usize| x & (1usize << i) != 0;
3295                (b(0) && b(1)) ^ (b(1) && b(2)) ^ (b(2) && b(3)) ^ (b(3) && b(4))
3296            })
3297            .collect();
3298        let d = separable_decomposition(&truth).unwrap();
3299        assert_eq!(d.blocks, vec![vec![0, 1, 2, 3, 4]], "one connected block");
3300        assert!(!d.is_separable());
3301        assert_eq!(d.reconstruct(n).as_deref(), Some(&truth[..]));
3302    }
3303
3304    #[test]
3305    fn separable_reconstruct_rejects_tampering() {
3306        let truth = split_dense(7, 42);
3307        let mut d = separable_decomposition(&truth).unwrap();
3308        assert_eq!(d.reconstruct(6).as_deref(), Some(&truth[..]));
3309        d.block_truths[0][0] = !d.block_truths[0][0]; // corrupt one block-table entry
3310        assert_ne!(d.reconstruct(6).as_deref(), Some(&truth[..]), "a tampered block is caught");
3311    }
3312
3313    // A dense ROTATION-symmetric function: one value per rotation necklace of the variables, so f(ρ·x) =
3314    // f(x) for the cyclic shift ρ, but no transposition and no linear structure captures it.
3315    fn rotation_symmetric(n: usize, seed: u64) -> Vec<bool> {
3316        let mask = (1usize << n) - 1;
3317        let rot = |x: usize| ((x << 1) | (x >> (n - 1))) & mask;
3318        (0..1usize << n)
3319            .map(|x| {
3320                let (mut m, mut y) = (x, x);
3321                for _ in 0..n {
3322                    y = rot(y);
3323                    m = m.min(y);
3324                }
3325                let mut z = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(m as u64 + seed);
3326                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3327                (z ^ (z >> 27)) & 1 == 1
3328            })
3329            .collect()
3330    }
3331
3332    #[test]
3333    fn variable_symmetry_generalizes_the_symmetric_class() {
3334        // MAJ on 4 variables: every transposition fixes it → the whole S_4 (order 24), and the input orbits
3335        // are exactly the n+1 Hamming-weight classes — the symmetric lens recovered as Aut(f) = S_n.
3336        let n = 4;
3337        let truth: Vec<bool> = (0..1usize << n).map(|x| (x as u32).count_ones() >= 2).collect();
3338        let s = variable_symmetry(&truth).unwrap();
3339        assert_eq!(s.order, 24, "Aut(MAJ4) = S_4");
3340        assert_eq!(s.orbit_count, n + 1, "the orbits are the weight classes");
3341        assert!(s.verify(&truth));
3342        assert_eq!(s.reconstruct(n).as_deref(), Some(&truth[..]));
3343    }
3344
3345    #[test]
3346    fn variable_symmetry_finds_rotation_symmetry_the_linear_ladder_misses() {
3347        // A dense rotation-symmetric function on 5 variables.
3348        let n = 5;
3349        let truth = rotation_symmetric(n, 12345);
3350        let s = variable_symmetry(&truth).unwrap();
3351        assert!(s.is_nontrivial() && s.order % 5 == 0, "C_5 ≤ Aut(f), order {}", s.order);
3352        // A PROPER, PARTIAL symmetry: more than trivial, less than the full S_5 the symmetric lens needs —
3353        // and more orbits than the n+1 weight classes, so the symmetric lens would have declared it residue.
3354        assert!(s.order < 120 && s.orbit_count > n + 1, "partial symmetry: order {} orbits {}", s.order, s.orbit_count);
3355        assert!(s.orbit_count < 1 << n, "yet still compressed: {} < {}", s.orbit_count, 1 << n);
3356        assert!(s.verify(&truth));
3357        assert_eq!(s.reconstruct(n).as_deref(), Some(&truth[..]), "the orbit painting reproduces f");
3358    }
3359
3360    #[test]
3361    fn variable_symmetry_is_trivial_for_a_pseudorandom_function() {
3362        // At n=6 a spurious transposition needs 2^{n-2}=16 coincidences (~2^-16), so the group is trivial.
3363        let truth = pseudorandom_truth(6);
3364        let s = variable_symmetry(&truth).unwrap();
3365        assert_eq!(s.order, 1, "a random function has no variable symmetry");
3366        assert!(!s.is_nontrivial());
3367        assert_eq!(s.orbit_count, 1 << 6, "every input is its own orbit — the residue");
3368        assert!(s.verify(&truth), "the trivial group trivially reproduces f");
3369    }
3370
3371    #[test]
3372    fn variable_symmetry_witness_rejects_tampering() {
3373        let truth = rotation_symmetric(5, 999);
3374        let mut s = variable_symmetry(&truth).unwrap();
3375        assert!(s.verify(&truth));
3376        s.orbit_values[0] = !s.orbit_values[0]; // repaint one orbit
3377        assert!(!s.verify(&truth), "a tampered orbit value is caught");
3378        // Corrupt a generator to a non-automorphism: also caught.
3379        let mut s2 = variable_symmetry(&truth).unwrap();
3380        if let Some(g) = s2.generators.first_mut() {
3381            g.swap(0, 1); // no longer necessarily an automorphism
3382        }
3383        // Either the generator check fails, or (if the swap happens to still fix f) reconstruct still holds;
3384        // force the check by also breaking an orbit value so verify must reject.
3385        s2.orbit_values[1] = !s2.orbit_values[1];
3386        assert!(!s2.verify(&truth));
3387    }
3388
3389    #[test]
3390    fn deep_finder_routes_each_function_to_its_tightest_axis() {
3391        // Separable wins on HETEROGENEOUS independent blocks: g(x0,x1,x2) ⊕ h(x3,x4,x5) with g ≠ h dense —
3392        // distinct blocks, so no block-swap permutation symmetry competes with the direct-sum split.
3393        let sep = split_dense(1, 999);
3394        assert_eq!(find_structure_deep(&sep).unwrap().winner, "separable");
3395
3396        // Rotation symmetry: no coordinate/linear lens sees it, the permutation group does.
3397        let rot = rotation_symmetric(5, 12345);
3398        assert_eq!(find_structure_deep(&rot).unwrap().winner, "permutation");
3399
3400        // A rotated junta the coordinate walk calls residue: linear-invariance peels a dimension.
3401        let rj = rotated_junta(5);
3402        assert_eq!(find_structure_deep(&rj).unwrap().winner, "linear-invariance");
3403
3404        // A single monomial x0x1x2 is a strict coordinate (low-degree) win — one term beats every axis.
3405        let mono: Vec<bool> = (0..16usize).map(|x| (x & 1 != 0) && (x & 2 != 0) && (x & 4 != 0)).collect();
3406        assert_eq!(find_structure_deep(&mono).unwrap().winner, "coordinate:low-degree");
3407
3408        // Parity is MAXIMALLY linear-reducible: quotienting the even-weight subspace collapses it to one
3409        // bit — tighter than the affine coordinate description, so the deep finder takes the tighter axis.
3410        let parity: Vec<bool> = (0..16usize).map(|x| (x as u32).count_ones() % 2 == 1).collect();
3411        assert_eq!(find_structure_deep(&parity).unwrap().winner, "linear-invariance");
3412
3413        // The residue: every axis fails, honestly reported (not "structureless").
3414        let rnd = pseudorandom_truth(6);
3415        let d = find_structure_deep(&rnd).unwrap();
3416        assert_eq!(d.winner, "residue");
3417        assert!(!d.compressed && d.description_bits == d.raw_bits);
3418    }
3419
3420    #[test]
3421    fn structure_tree_recurses_to_a_nested_fixed_point() {
3422        // f = ((x0⊕x1)∧x2) ⊕ (x3∧x4∧x5): the two halves are independent (separable), and the first half is
3423        // a rotated junta that then peels its x0⊕x1 invariance — a two-level decomposition.
3424        let n = 6;
3425        let truth: Vec<bool> = (0..1usize << n)
3426            .map(|x| {
3427                let b = |i: usize| x & (1usize << i) != 0;
3428                ((b(0) ^ b(1)) && b(2)) ^ (b(3) && b(4) && b(5))
3429            })
3430            .collect();
3431        let tree = structure_tree(&truth).unwrap();
3432        // Top level splits into the two independent halves.
3433        match &tree {
3434            StructureTree::Separable { blocks, .. } => {
3435                assert_eq!(blocks.len(), 2, "the two halves split apart");
3436                // At least one block recurses further (the rotated-junta half peels a linear invariance).
3437                assert!(
3438                    blocks.iter().any(|(_, t)| matches!(t, StructureTree::LinearReduced { .. })),
3439                    "a block peels deeper: {:?}",
3440                    blocks.iter().map(|(_, t)| t.depth()).collect::<Vec<_>>()
3441                );
3442            }
3443            other => panic!("expected a separable top level, got {other:?}"),
3444        }
3445        assert!(tree.depth() >= 2, "a genuinely nested decomposition, depth {}", tree.depth());
3446        assert_eq!(tree.reconstruct().as_deref(), Some(&truth[..]), "the whole tree reconstructs f exactly");
3447        assert!(tree.total_description_bits() < truth.len(), "and it is a real compression of the cube");
3448    }
3449
3450    #[test]
3451    fn structure_tree_bottoms_out_at_the_residue() {
3452        let truth = pseudorandom_truth(6);
3453        let tree = structure_tree(&truth).unwrap();
3454        assert!(matches!(tree, StructureTree::Residue { .. }), "no axis peels a random function");
3455        assert_eq!(tree.depth(), 0);
3456        assert_eq!(tree.reconstruct().as_deref(), Some(&truth[..]), "the residue is stored and reproduced raw");
3457    }
3458
3459    #[test]
3460    fn census_residue_is_certified_nonempty_by_counting() {
3461        // The two halves of the campaign agree: the census MEASURES a nonempty residue, and counting
3462        // CERTIFIES (kernel-re-checkably) that an incompressible function must exist to fill it.
3463        for nv in 2..=4u32 {
3464            let census = boolean_function_census(nv as usize).unwrap();
3465            assert!(census.residue > 0, "the arsenal empirically leaves a residue at n={nv}");
3466            let cert = certified_incompressible_function_exists(nv).expect("a counting certificate exists");
3467            assert!(crate::pigeonhole::check_counting_cert(&cert), "the counting certificate re-checks");
3468            // The certificate counts the 2^{2ⁿ} functions against the 2^{2ⁿ}−1 shorter programs.
3469            assert_eq!(cert.pigeons, 1u128 << (1u128 << nv), "one pigeon per Boolean function");
3470            assert_eq!(cert.holes, (1u128 << (1u128 << nv)) - 1, "one hole per shorter program");
3471        }
3472        // A function on 6 vars is a 64-bit truth table (in range); 7 vars is 128 bits (overflows u128).
3473        assert!(certified_incompressible_function_exists(6).is_some());
3474        assert!(certified_incompressible_function_exists(7).is_none());
3475    }
3476
3477    #[test]
3478    fn sampled_census_shows_the_residue_approaching_one() {
3479        // The asymptotic thesis, sampled where 2^{2ⁿ} is unenumerable: structured functions vanish and
3480        // almost every function is the incompressible residue.
3481        let fracs: Vec<f64> =
3482            (4..=8).map(|n| sampled_boolean_census(n, 1200, 12345).unwrap().residue_fraction()).collect();
3483        // The n=4 sample matches the exhaustive census (~0.62), validating the estimator.
3484        assert!((fracs[0] - 0.62).abs() < 0.08, "n=4 sample ≈ exhaustive census: got {}", fracs[0]);
3485        // Monotonically climbing toward 1 (a small tolerance near the top where sampling noise dominates).
3486        for w in fracs.windows(2) {
3487            assert!(w[1] >= w[0] - 0.01, "the residue fraction climbs toward 1 with n: {fracs:?}");
3488        }
3489        assert!(*fracs.last().unwrap() > 0.99, "by n=8 almost every function is incompressible: {fracs:?}");
3490    }
3491
3492    #[test]
3493    fn boolean_census_maps_the_space_and_the_residue_grows() {
3494        // Exhaustively classify every Boolean function on n=2,3,4 variables by the deep finder's winner.
3495        let (c2, c3, c4) = (
3496            boolean_function_census(2).unwrap(),
3497            boolean_function_census(3).unwrap(),
3498            boolean_function_census(4).unwrap(),
3499        );
3500        for c in [&c2, &c3, &c4] {
3501            assert_eq!(c.total, 1usize << (1usize << c.num_vars), "2^{{2ⁿ}} functions");
3502            assert_eq!(c.by_winner.iter().map(|(_, k)| *k).sum::<usize>(), c.total, "every function is classified");
3503            assert_eq!(c.compressed + c.residue, c.total);
3504        }
3505        // THE THESIS, measured in the representative regime (n=2's 16 functions are a boundary artifact):
3506        // the incompressible residue is a growing fraction of the space as n rises.
3507        let (f3, f4) = (c3.residue as f64 / c3.total as f64, c4.residue as f64 / c4.total as f64);
3508        assert!(f4 > f3 + 0.3, "the residue fraction jumps with n (n=3 → {f3:.3}, n=4 → {f4:.3})");
3509        assert!(c4.residue * 2 > c4.total, "by n=4 the incompressible residue is already the majority");
3510        // Every structural axis the ladder built — including the new affine peel — is the tightest
3511        // description for some function.
3512        for axis in
3513            ["affine", "coordinate:constant", "coordinate:low-degree", "linear-invariance", "permutation", "separable"]
3514        {
3515            assert!(c4.by_winner.iter().any(|(w, k)| *w == axis && *k > 0), "axis {axis} wins somewhere");
3516        }
3517    }
3518
3519    #[test]
3520    fn description_bound_covers_boolean_functions_in_the_unified_framework() {
3521        // A structured Boolean function fits the SAME certified-K̄ framework as integer sequences.
3522        let structured: Vec<bool> = (0..64usize)
3523            .map(|x| {
3524                let b = |i: usize| x & (1usize << i) != 0;
3525                ((b(0) ^ b(1)) && b(2)) ^ (b(3) && b(4) && b(5))
3526            })
3527            .collect();
3528        let db = DescriptionBound::of_boolean(&structured).unwrap();
3529        assert!(matches!(db.descriptor, Descriptor::BooleanFunction { .. }));
3530        assert!(db.verify(), "the tree decode witness re-checks inside DescriptionBound");
3531        assert!(db.bytes < structured.len() / 8 + 1, "K̄ beats the packed truth table, {} bytes", db.bytes);
3532
3533        // Tampering the recorded bound breaks verification (the certificate is only as good as its decode).
3534        let mut bad = DescriptionBound::of_boolean(&structured).unwrap();
3535        bad.bytes += 1;
3536        assert!(!bad.verify(), "an inconsistent bound is rejected");
3537
3538        // The int-sequence descriptor still verifies — the two layers coexist.
3539        let seq = DescriptionBound::of_int_seq(&(0..100).map(|i| 3 * i + 1).collect::<Vec<_>>());
3540        assert!(seq.verify());
3541    }
3542
3543    #[test]
3544    fn kolmogorov_bound_certifies_compression_and_the_honest_residue() {
3545        // A structured function compresses: K̄ ≪ 2ⁿ, and the decomposition decodes back to it exactly.
3546        let structured: Vec<bool> = (0..64usize)
3547            .map(|x| {
3548                let b = |i: usize| x & (1usize << i) != 0;
3549                ((b(0) ^ b(1)) && b(2)) ^ (b(3) && b(4) && b(5))
3550            })
3551            .collect();
3552        let kb = kolmogorov_bound(&structured).unwrap();
3553        assert!(kb.is_compressed() && kb.ratio() < 1.0, "structure gives K̄ < 2ⁿ, ratio {}", kb.ratio());
3554        assert!(kb.verify(&structured), "the decode witness re-checks the bound");
3555
3556        // A random function: the bound equals raw — "irreducible by this arsenal," NOT proven incompressible.
3557        let rnd = pseudorandom_truth(6);
3558        let kr = kolmogorov_bound(&rnd).unwrap();
3559        assert_eq!(kr.bits, kr.raw_bits, "no axis beats the truth table — the residue, K̄ = 2ⁿ");
3560        assert!(!kr.is_compressed() && kr.verify(&rnd));
3561
3562        // Tampering the witness breaks the certificate.
3563        let mut bad = kolmogorov_bound(&structured).unwrap();
3564        bad.tree = StructureTree::Residue { truth: rnd.clone() };
3565        assert!(!bad.verify(&structured), "a witness for a different function does not verify");
3566    }
3567
3568    #[test]
3569    fn sbox_profile_flags_a_linear_sbox_as_weak() {
3570        // The identity S-box S(x)=x on 4 bits: every component is linear, differences are constant.
3571        let id: Vec<u32> = (0..16u32).collect();
3572        let p = sbox_profile(&id, 4).unwrap();
3573        assert!(p.is_affine && p.min_degree == 1, "a linear S-box has affine components");
3574        assert_eq!(p.differential_uniformity, 16, "S(x⊕a)⊕S(x)=a is constant — maximally weak");
3575        assert_eq!(p.linearity, 16, "a perfect linear approximation exists");
3576        assert!(p.is_bijective && !p.is_apn);
3577    }
3578
3579    #[test]
3580    fn sbox_profile_certifies_the_aes_sbox_is_strong() {
3581        #[rustfmt::skip]
3582        const AES: [u8; 256] = [
3583            0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
3584            0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
3585            0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
3586            0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
3587            0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
3588            0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
3589            0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
3590            0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
3591            0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
3592            0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
3593            0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
3594            0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
3595            0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
3596            0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
3597            0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
3598            0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
3599        ];
3600        let sbox: Vec<u32> = AES.iter().map(|&b| b as u32).collect();
3601        let p = sbox_profile(&sbox, 8).unwrap();
3602        // Our tools reproduce AES's published strength exactly.
3603        assert_eq!(p.differential_uniformity, 4, "AES S-box differential uniformity");
3604        assert_eq!(p.linearity, 32, "AES S-box linearity (nonlinearity 128 − 16 = 112)");
3605        assert_eq!(p.min_degree, 7, "AES S-box algebraic degree");
3606        assert!(p.is_bijective && !p.is_affine && !p.is_apn, "strong, invertible, not linear, not APN");
3607    }
3608
3609    // Apply an invertible GF(2) linear map to the INPUT: S'(x) = S(Ax); rows[i] is coordinate i's form.
3610    fn apply_input_linear(sbox: &[u32], rows: &[usize]) -> Vec<u32> {
3611        (0..sbox.len())
3612            .map(|x| {
3613                let ax = rows
3614                    .iter()
3615                    .enumerate()
3616                    .fold(0usize, |acc, (i, &r)| if (r & x).count_ones() % 2 == 1 { acc | (1 << i) } else { acc });
3617                sbox[ax]
3618            })
3619            .collect()
3620    }
3621    // Apply an invertible GF(2) linear map to the OUTPUT: S'(x) = B·S(x).
3622    fn apply_output_linear(sbox: &[u32], rows: &[usize]) -> Vec<u32> {
3623        sbox.iter()
3624            .map(|&y| {
3625                rows.iter().enumerate().fold(0u32, |acc, (i, &r)| {
3626                    if (r & y as usize).count_ones() % 2 == 1 { acc | (1 << i) } else { acc }
3627                })
3628            })
3629            .collect()
3630    }
3631
3632    #[test]
3633    fn sbox_spectra_are_affine_invariants() {
3634        // The PRESENT S-box — a nonlinear 4-bit permutation.
3635        let present: Vec<u32> = vec![0xC, 5, 6, 0xB, 9, 0, 0xA, 0xD, 3, 0xE, 0xF, 8, 4, 7, 1, 2];
3636        let rows = vec![0b0001usize, 0b0011, 0b0111, 0b1111]; // an invertible GL(4,2) map
3637        assert_eq!(gf2_echelon_basis(&rows).len(), 4);
3638        let base = sbox_spectra(&present, 4).unwrap();
3639        let g = apply_input_linear(&present, &rows);
3640        assert_eq!(sbox_spectra(&g, 4).unwrap(), base, "spectra invariant under an input linear change");
3641        let h = apply_output_linear(&present, &rows);
3642        assert_eq!(sbox_spectra(&h, 4).unwrap(), base, "spectra invariant under an output linear change");
3643    }
3644
3645    #[test]
3646    fn sbox_spectra_distinguish_inequivalent_boxes() {
3647        let id: Vec<u32> = (0..16u32).collect();
3648        let present: Vec<u32> = vec![0xC, 5, 6, 0xB, 9, 0, 0xA, 0xD, 3, 0xE, 0xF, 8, 4, 7, 1, 2];
3649        assert_ne!(
3650            sbox_spectra(&id, 4).unwrap().differential_spectrum,
3651            sbox_spectra(&present, 4).unwrap().differential_spectrum,
3652            "a linear box and a nonlinear box cannot be affine-equivalent"
3653        );
3654    }
3655
3656    #[test]
3657    fn sbox_profile_recognizes_an_apn_gold_function() {
3658        // x ↦ x³ over GF(2³) (poly x³+x+1) is a Gold APN permutation: differential uniformity 2.
3659        fn gf8_mul(mut a: u32, mut b: u32) -> u32 {
3660            let mut p = 0;
3661            for _ in 0..3 {
3662                if b & 1 == 1 {
3663                    p ^= a;
3664                }
3665                b >>= 1;
3666                let hi = a & 0b100;
3667                a = (a << 1) & 0b111;
3668                if hi != 0 {
3669                    a ^= 0b011; // reduce x³ ≡ x + 1
3670                }
3671            }
3672            p
3673        }
3674        let sbox: Vec<u32> = (0..8u32).map(|x| gf8_mul(gf8_mul(x, x), x)).collect();
3675        let p = sbox_profile(&sbox, 3).unwrap();
3676        assert_eq!(p.differential_uniformity, 2, "the Gold function is APN");
3677        assert!(p.is_apn && p.is_bijective, "an APN permutation on an odd number of bits");
3678        // An APN permutation has the optimal boomerang uniformity 2.
3679        assert_eq!(boomerang_uniformity(&sbox), Some(2), "APN ⟹ boomerang uniformity 2");
3680        // The Gold function is APN (optimal differential) but degree 2 — the audit flags the ALGEBRAIC
3681        // weakness the differential measure alone misses.
3682        assert_eq!(sbox_full_audit(&sbox, 3), Some(SboxVerdict::Quadratic), "optimal differential, weak algebra");
3683    }
3684
3685    #[test]
3686    fn sbox_audit_flags_weakness_and_clears_aes() {
3687        // A linear S-box: every component affine → trivially broken.
3688        let id: Vec<u32> = (0..16u32).collect();
3689        assert_eq!(sbox_full_audit(&id, 4), Some(SboxVerdict::Affine));
3690
3691        #[rustfmt::skip]
3692        const AES: [u8; 256] = [
3693            0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
3694            0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
3695            0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
3696            0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
3697            0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
3698            0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
3699            0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
3700            0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
3701            0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
3702            0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
3703            0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
3704            0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
3705            0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
3706            0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
3707            0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
3708            0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
3709        ];
3710        let aes: Vec<u32> = AES.iter().map(|&b| b as u32).collect();
3711        // AES resists every structural test; the audit reports its (strong) profile as the honest ceiling.
3712        assert_eq!(
3713            sbox_full_audit(&aes, 8),
3714            Some(SboxVerdict::NoStructuralWeaknessFound {
3715                differential_uniformity: 4,
3716                linearity: 32,
3717                min_degree: 7,
3718                boomerang_uniformity: Some(6),
3719            }),
3720            "AES resists the structural arsenal — not broken"
3721        );
3722    }
3723
3724    #[test]
3725    fn boomerang_uniformity_matches_the_aes_published_value() {
3726        #[rustfmt::skip]
3727        const AES: [u8; 256] = [
3728            0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
3729            0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
3730            0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
3731            0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
3732            0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
3733            0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
3734            0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
3735            0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
3736            0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
3737            0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
3738            0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
3739            0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
3740            0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
3741            0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
3742            0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
3743            0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
3744        ];
3745        let sbox: Vec<u32> = AES.iter().map(|&b| b as u32).collect();
3746        assert_eq!(boomerang_uniformity(&sbox), Some(6), "AES S-box boomerang uniformity (Cid et al. 2018)");
3747    }
3748
3749    #[test]
3750    fn coverage_census_shows_covered_families_and_the_uncovered_residue() {
3751        use logicaffeine_base::BigInt;
3752        let n = 300;
3753
3754        // Structured corpus: each generator's output is covered by SOME lens in the arsenal.
3755        let mut structured: Vec<Vec<bool>> = Vec::new();
3756        structured.push(describe::lfsr_generate(
3757            &[false, false, true, false, false, false, true],
3758            &[true, false, true, true, false, false, true],
3759            n,
3760        )); // LFSR → linear lens
3761        structured.push(describe::fcsr_generate(&BigInt::from_i64(7), &BigInt::from_i64(19), n)); // FCSR → 2-adic lens
3762        structured.push(describe::fcsr_generate(&BigInt::from_i64(11), &BigInt::from_i64(23), n));
3763        structured.push((0..n).map(|i| [true, false, false, true, true, false][i % 6]).collect()); // periodic → MDL
3764        let sc = census(&structured);
3765        assert_eq!(sc.uncovered, 0, "every structured sequence is covered; map = {:?}", sc.by_lens);
3766
3767        // Random corpus: cryptographic (splitmix) sequences — the uncovered residue.
3768        let random: Vec<Vec<bool>> = (0..40u64)
3769            .map(|i| {
3770                let mut st = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul(i.wrapping_add(1));
3771                (0..n)
3772                    .map(|_| {
3773                        st = st.wrapping_add(0x9E37_79B9_7F4A_7C15);
3774                        let mut z = st;
3775                        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3776                        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
3777                        z ^= z >> 31;
3778                        z & 1 == 1
3779                    })
3780                    .collect()
3781            })
3782            .collect();
3783        assert_eq!(census(&random).covered, 0, "cryptographic-random sequences are the uncovered residue");
3784
3785        // Exhaustive: what fraction of the WHOLE length-14 space does the arsenal cover?
3786        let ex = exhaustive_coverage(14);
3787        eprintln!(
3788            "exhaustive len=14: covered {}/{} = {:.2}%, residue {} = {:.2}%",
3789            ex.covered,
3790            ex.total,
3791            100.0 * ex.covered as f64 / ex.total as f64,
3792            ex.uncovered,
3793            100.0 * ex.uncovered as f64 / ex.total as f64,
3794        );
3795        assert!(ex.covered * 2 < ex.total, "the covered families are a minority sliver — most of the space is residue");
3796    }
3797
3798    #[test]
3799    fn rsa_structural_audit_breaks_weak_moduli_and_certifies_the_ceiling() {
3800        use crate::factor;
3801        use logicaffeine_base::BigInt;
3802        let big = |s: &str| BigInt::parse_decimal(s).unwrap();
3803
3804        // Weak: two adjacent primes fall to Fermat — a certified break.
3805        let p = factor::next_prime(&big("1000000000000000000"));
3806        let q = factor::next_prime(&p.add(&BigInt::from_i64(2)));
3807        let n = p.mul(&q);
3808        match rsa_structural_audit(&n) {
3809            RsaStrength::Factored { p: a, q: b, method } => {
3810                assert!(factor::verify_factorization(&n, &a, &b), "the witness re-multiplies to N");
3811                assert!(method.contains("Fermat"), "close primes are caught by Fermat, got {method}");
3812            }
3813            RsaStrength::SoundAgainstStructuralAttacks => panic!("close primes must be broken"),
3814        }
3815
3816        // Sound: two large, well-separated primes resist the entire arsenal — the ceiling.
3817        let p = factor::next_prime(&big("1000000000000000000"));
3818        let q = factor::next_prime(&big("9000000000000000000"));
3819        let n = p.mul(&q);
3820        assert_eq!(
3821            rsa_structural_audit(&n),
3822            RsaStrength::SoundAgainstStructuralAttacks,
3823            "a sound modulus is the number-theoretic incompressible residue"
3824        );
3825    }
3826
3827    #[test]
3828    fn algebraic_attack_stops_at_the_ceiling_on_random_bytes() {
3829        // A cryptographic (splitmix) keystream has no low-degree feedback at any small order: the
3830        // algebraic attack finds nothing — the genuine high-degree / incompressible residue, the ceiling.
3831        let random = splitmix_bytes(120, 0xdead_beef_cafe_babe);
3832        assert!(
3833            algebraic_attack_on_bytes(&random, 2, 20).is_none(),
3834            "no degree-2 register regenerates a cryptographic keystream — the Chaitin ceiling"
3835        );
3836    }
3837
3838    #[test]
3839    fn random_key_material_is_incompressible_in_class() {
3840        // Pseudo-random bytes carry no generator structure: nothing beats storing them raw.
3841        let key = splitmix_bytes(400, 0x1234_5678_9abc_def0);
3842        match assess_key_material(&key) {
3843            CryptoStrength::IncompressibleInClass { ratio } => {
3844                assert!(ratio >= 0.95, "random bytes are ~incompressible, got ratio {ratio}");
3845            }
3846            CryptoStrength::Weak { ratio, .. } => panic!("random key wrongly flagged weak, ratio {ratio}"),
3847        }
3848    }
3849
3850    #[test]
3851    fn incompressibility_ratio_separates_structure_from_randomness() {
3852        let structured: Vec<u8> = (0..300).map(|i| (i % 8) as u8).collect(); // periodic, period 8
3853        let random = splitmix_bytes(300, 0x9e37_79b9_7f4a_7c15);
3854        assert!(
3855            incompressibility_ratio(&structured) < 0.3 * incompressibility_ratio(&random),
3856            "structured key ({}) must be far more compressible than random ({})",
3857            incompressibility_ratio(&structured),
3858            incompressibility_ratio(&random),
3859        );
3860    }
3861
3862    #[test]
3863    fn classify_recognizes_the_ordered_to_random_spectrum() {
3864        // Generated: a plain counter is an affine program.
3865        let counter: Vec<u8> = (0..200u16).map(|i| i as u8).collect();
3866        assert_eq!(classify_bytes(&counter).class, CompressibilityClass::Generated);
3867        // Periodic: a short repeating block.
3868        let periodic: Vec<u8> = (0..300).map(|i| (i % 8) as u8).collect();
3869        assert_eq!(classify_bytes(&periodic).class, CompressibilityClass::Periodic);
3870        // Incompressible: full-byte-entropy pseudo-random bytes.
3871        let random = splitmix_bytes(400, 0xDEAD_BEEF_CAFE_1234);
3872        assert_eq!(classify_bytes(&random).class, CompressibilityClass::Incompressible);
3873        // Structured inputs sit far below the incompressible baseline; random sits at it.
3874        assert!(classify_bytes(&counter).ratio < 0.2, "a counter is nearly free to describe");
3875        assert!(classify_bytes(&periodic).ratio < 0.2, "a short period is nearly free to describe");
3876        assert!(classify_bytes(&random).ratio >= 0.95, "random bytes cost ~their full length");
3877    }
3878
3879    #[test]
3880    fn classify_recognizes_fibonacci_as_a_generator() {
3881        // Fibonacci is a linear recurrence — a closed-form GENERATOR — even though it is neither affine
3882        // nor polynomial. This is exactly the LFSR / Berlekamp–Massey structure: a "random-looking"
3883        // sequence that is in fact fully predictable, now caught and collapsed to a few numbers.
3884        let mut fib = vec![0i64, 1];
3885        while fib.len() < 60 {
3886            let n = fib.len();
3887            fib.push(fib[n - 1].wrapping_add(fib[n - 2]));
3888        }
3889        let report = classify_int_seq(&fib);
3890        assert_eq!(report.class, CompressibilityClass::Generated, "Fibonacci is a generator, not random");
3891        assert!(report.ratio < 0.1, "60 Fibonacci terms collapse to a handful, ratio {}", report.ratio);
3892    }
3893
3894    #[test]
3895    fn classify_text_places_inputs_on_the_compressibility_spectrum() {
3896        // Repetitive text is highly structured — far below the incompressible baseline.
3897        let repeated = "the quick brown fox jumps over the lazy dog. ".repeat(30);
3898        let rep = classify_text(&repeated);
3899        assert_ne!(rep.class, CompressibilityClass::Incompressible, "repeated text has structure");
3900        assert!(rep.ratio < 0.5, "repeated text compresses well, ratio {}", rep.ratio);
3901        // Full-byte-entropy random bytes are incompressible relative to the menu.
3902        let random_bytes = splitmix_bytes(400, 0x0102_0304_0506_0708);
3903        let rand_ratio = classify_bytes(&random_bytes).ratio;
3904        assert_eq!(classify_bytes(&random_bytes).class, CompressibilityClass::Incompressible);
3905        // Narrow-alphabet random text sits BETWEEN: a 94-symbol alphabet is only ~6.5 bits/symbol, so
3906        // the classifier honestly detects that limited-alphabet structure — more compressible than full
3907        // randomness, far less than true repetition. The whole spectrum in one ordering:
3908        let ascii: String =
3909            splitmix_bytes(400, 0x0a0b_0c0d_0e0f_1011).iter().map(|&b| char::from(33 + b % 94)).collect();
3910        let ascii_ratio = classify_text(&ascii).ratio;
3911        assert!(
3912            rep.ratio < ascii_ratio && ascii_ratio < rand_ratio,
3913            "spectrum: repetition ({}) < narrow-alphabet text ({ascii_ratio}) < full randomness ({rand_ratio})",
3914            rep.ratio,
3915        );
3916    }
3917
3918    #[test]
3919    fn structural_bound_certifies_symmetry_and_realizes_compression() {
3920        // The pigeonhole formula has a large automorphism group (Sₚ × Sₕ). One clause per orbit plus
3921        // the generators reconstructs it. The certificate re-checks (every generator an automorphism,
3922        // every orbit covered by a representative), the symmetry-entropy is positive, the certificate
3923        // never worsens the bound, and at this scale the group description is strictly shorter than the
3924        // flat one — the "symmetry = compression" thesis realized as certified bytes.
3925        let (cnf, _) = crate::families::php(6);
3926        let gens = crate::hypercube::php_perm_symmetries(6);
3927        let sb = structural_bound(cnf.num_vars, &cnf.clauses, &gens).expect("php is symmetric");
3928        assert!(sb.verify(), "the group description must re-check: automorphisms + full reconstruction");
3929        assert!(sb.group_entropy_bits > 0.0, "a symmetric formula has positive symmetry-entropy");
3930        assert!(sb.best_bytes() <= sb.whole.bytes, "the certificate never worsens the upper bound");
3931        assert!(sb.is_compression(), "at scale, PHP compresses via its symmetry group (group < flat)");
3932        assert_eq!(sb.best_bytes(), sb.group_bytes(), "the group description is the better bound here");
3933    }
3934
3935    #[test]
3936    fn structural_bound_declines_a_non_automorphism() {
3937        // A permutation that is not an automorphism must never yield a certificate.
3938        let (cnf, _) = crate::families::php(3);
3939        let mut imgs: Vec<Lit> = (0..cnf.num_vars).map(|v| Lit::pos(v as Var)).collect();
3940        imgs[0] = Lit::pos(1); // collide two variables — breaks the clause structure
3941        let bogus = Perm::from_images(imgs);
3942        assert!(structural_bound(cnf.num_vars, &cnf.clauses, &[bogus]).is_none());
3943    }
3944
3945    /// Build the CNF gadget for a parity equation `⊕vars = rhs`: exactly the `2^(k-1)` clauses over
3946    /// `vars` whose negated-literal count has the parity `extract_xor` expects (`rhs = 1 − neg%2`).
3947    fn xor_gadget(vars: &[usize], rhs: bool) -> Vec<Vec<Lit>> {
3948        let k = vars.len();
3949        let target = if rhs { 0 } else { 1 }; // neg-count parity that decodes to this rhs
3950        let mut clauses = Vec::new();
3951        for mask in 0u32..(1u32 << k) {
3952            if (mask.count_ones() % 2) as u32 == target {
3953                let clause = vars
3954                    .iter()
3955                    .enumerate()
3956                    .map(|(i, &v)| Lit::new(v as Var, mask & (1 << i) == 0))
3957                    .collect();
3958                clauses.push(clause);
3959            }
3960        }
3961        clauses
3962    }
3963
3964    #[test]
3965    fn linear_rigidity_kernel_is_rechecked() {
3966        // Two independent parity constraints over 4 variables — x0⊕x1⊕x2 and x1⊕x2⊕x3 — pin the
3967        // system to rank 2 with a 2-dimensional kernel (2² linear solutions). The certificate exposes
3968        // exactly that structure and re-checks independently.
3969        let mut clauses = xor_gadget(&[0, 1, 2], true);
3970        clauses.extend(xor_gadget(&[1, 2, 3], true));
3971        let cert = certify_linear_rigidity(4, &clauses).expect("has parity structure");
3972        assert_eq!(cert.rank, 2, "two independent parity rows");
3973        assert_eq!(cert.solution_count_log2, 2, "2-dimensional kernel ⇒ 2² solutions");
3974        assert_eq!(cert.kernel_basis.len(), 2);
3975        assert_eq!(cert.kernel_basis.len(), cert.num_vars - cert.rank, "rank–nullity");
3976        assert!(check_linear_rigidity(&cert, &clauses), "the exposed linear structure re-checks");
3977    }
3978
3979    #[test]
3980    fn linear_rigidity_rejects_tampered_kernel() {
3981        let mut clauses = xor_gadget(&[0, 1, 2], true);
3982        clauses.extend(xor_gadget(&[1, 2, 3], true));
3983        let mut cert = certify_linear_rigidity(4, &clauses).expect("has parity structure");
3984        assert!(check_linear_rigidity(&cert, &clauses));
3985        // Corrupt a null-space vector: it is no longer a solution of the parity system.
3986        cert.kernel_basis[0][0] ^= true;
3987        assert!(!check_linear_rigidity(&cert, &clauses), "a non-solution kernel vector is rejected");
3988    }
3989
3990    #[test]
3991    fn certify_linear_rigidity_declines_without_parity() {
3992        // A plain 2-SAT clause is no XOR gadget — there is no linear structure to certify.
3993        let clauses = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::pos(2)]];
3994        assert!(certify_linear_rigidity(3, &clauses).is_none());
3995    }
3996
3997    #[test]
3998    fn linear_structure_certified_at_par32_scale() {
3999        // A Tseitin expander on a 3-regular graph — the canonical "hard parity" instance — over more
4000        // than 64 variables, past the u64 cap of the explicit-basis certificate.
4001        let (_eqs, cnf, _) = crate::families::tseitin_expander(60, 0x51A7);
4002        assert!(cnf.num_vars > 64, "the par32-scale regime, beyond gf2's u64 rows");
4003        // The explicit-basis certificate honestly declines past 64 variables…
4004        assert!(certify_linear_rigidity(cnf.num_vars, &cnf.clauses).is_none());
4005        // …but the incremental structure certificate scales to it.
4006        let cert = certify_linear_structure(cnf.num_vars, &cnf.clauses).expect("has parity structure");
4007        assert!(cert.rank > 0 && cert.num_xor_eqs > 0, "a non-trivial recovered parity system");
4008        assert!(check_linear_structure(&cert, &cnf.clauses), "rank + kernel dimension re-check");
4009        // Tamper: a wrong rank must be rejected.
4010        let mut bad = cert.clone();
4011        bad.rank += 1;
4012        assert!(!check_linear_structure(&bad, &cnf.clauses), "an inflated rank is rejected");
4013    }
4014
4015    #[test]
4016    fn certify_linear_structure_declines_without_parity() {
4017        let clauses = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::pos(2)]];
4018        assert!(certify_linear_structure(3, &clauses).is_none());
4019    }
4020
4021    #[test]
4022    fn linear_shortcut_verdict_reports_rigidity_with_a_rechecking_certificate() {
4023        // Small parity system (≤ 64 vars): the verdict is "no linear shortcut", carrying BOTH the
4024        // incremental structure and the strongest explicit-basis certificate, each re-checkable.
4025        let mut small = xor_gadget(&[0, 1, 2], true);
4026        small.extend(xor_gadget(&[1, 2, 3], true));
4027        match linear_shortcut_verdict(4, &small) {
4028            LinearShortcut::None { rigidity, structure } => {
4029                assert!(check_linear_structure(&structure, &small));
4030                let rig = rigidity.expect("≤64 vars ⇒ an explicit kernel basis is available");
4031                assert!(check_linear_rigidity(&rig, &small));
4032            }
4033            LinearShortcut::NoLinearStructure => panic!("the parity system has linear structure"),
4034        }
4035
4036        // par32-scale (> 64 vars): still "no linear shortcut", but only the incremental cert (the
4037        // explicit basis is beyond the u64 budget) — reported honestly, never over-claimed.
4038        let (_e, cnf, _) = crate::families::tseitin_expander(60, 0x51A7);
4039        assert!(cnf.num_vars > 64);
4040        match linear_shortcut_verdict(cnf.num_vars, &cnf.clauses) {
4041            LinearShortcut::None { rigidity, structure } => {
4042                assert!(rigidity.is_none(), "past the u64 budget the explicit basis is withheld");
4043                assert!(check_linear_structure(&structure, &cnf.clauses));
4044            }
4045            LinearShortcut::NoLinearStructure => panic!("the Tseitin expander has linear structure"),
4046        }
4047
4048        // No parity structure ⇒ the linear class is empty.
4049        let plain = vec![vec![Lit::pos(0), Lit::pos(1)]];
4050        assert!(matches!(linear_shortcut_verdict(2, &plain), LinearShortcut::NoLinearStructure));
4051    }
4052
4053    #[test]
4054    fn incompressibility_gate_fires_on_rigid_linear_and_declines_on_symmetric() {
4055        // Declines on a symmetric formula (PHP): the symmetry arsenal is NOT useless there, so the gate
4056        // must fall through and let it run.
4057        let (php, _) = crate::families::php(4);
4058        assert!(incompressibility_gate(php.num_vars, &php.clauses).is_none(), "PHP is symmetric → decline");
4059        // Declines without any linear structure (nothing to certify rigid).
4060        assert!(incompressibility_gate(3, &[vec![Lit::pos(0), Lit::pos(1)]]).is_none());
4061        // A pure Tseitin formula is NOT rigid — parity phase-symmetries (flips along cycles) survive —
4062        // so the gate correctly declines even though it has linear structure.
4063        let (_e, tseitin, _) = crate::families::tseitin_expander(20, 0x51A7);
4064        assert!(certify_linear_structure(tseitin.num_vars, &tseitin.clauses).is_some(), "has parity structure");
4065        assert!(incompressibility_gate(tseitin.num_vars, &tseitin.clauses).is_none(), "but not rigid → decline");
4066
4067        // Fires on a genuinely rigid linear instance: a rigid random-3SAT (which distinguishes every
4068        // variable, |Aut| = 1) with one XOR gadget grafted over already-used variables — the rigid part
4069        // breaks the gadget's symmetry, so the whole formula has parity structure AND no symmetry
4070        // shortcut. The gate certifies it and the attached cert re-checks.
4071        let mut fired = false;
4072        for seed in [0x51A7u64, 0xBEEF, 0x1234, 0xF00D, 0xCAFE, 0xABCD, 0x9E37, 0x2718] {
4073            let base = crate::families::random_3sat(12, 40, seed);
4074            let mut clauses = base.clauses.clone();
4075            clauses.extend(xor_gadget(&[0, 1, 2], true));
4076            if let Some(cert) = incompressibility_gate(base.num_vars, &clauses) {
4077                assert!(check_linear_structure(&cert, &clauses), "the attached structure cert re-checks");
4078                fired = true;
4079                break;
4080            }
4081        }
4082        assert!(fired, "a rigid random-3SAT + an XOR gadget is linear AND rigid — the gate must fire");
4083    }
4084
4085    #[test]
4086    fn incompressibility_lemma_is_certified_by_counting() {
4087        // For every length there are strictly more strings than shorter programs, so an incompressible
4088        // string exists — and the counting refutation re-checks from scratch.
4089        for n in 1..=100u32 {
4090            let cert = incompressible_string_exists(n).expect("2ⁿ strings > 2ⁿ − 1 shorter programs");
4091            assert_eq!(cert.pigeons, 1u128 << n, "2ⁿ strings of length n");
4092            assert_eq!(cert.holes, (1u128 << n) - 1, "2ⁿ − 1 programs shorter than n");
4093            assert!(crate::pigeonhole::check_counting_cert(&cert), "the counting refutation re-checks");
4094        }
4095        // The certificate is exactly PHP(2ⁿ → 2ⁿ − 1).
4096        let c = incompressible_string_exists(10).unwrap();
4097        assert_eq!((c.pigeons, c.holes), (1024, 1023));
4098        // Out of exact range ⇒ no certificate (never a false one).
4099        assert!(incompressible_string_exists(0).is_none());
4100        assert!(incompressible_string_exists(200).is_none());
4101    }
4102
4103    #[test]
4104    fn budget_refuses_oversized_gaussian_fail_closed() {
4105        // The operational Chaitin ceiling: a budget too small to materialize this 4-variable kernel
4106        // fails CLOSED with a documented refusal — never a certificate it could not re-check.
4107        let mut clauses = xor_gadget(&[0, 1, 2], true);
4108        clauses.extend(xor_gadget(&[1, 2, 3], true));
4109        let tiny = Budget { max_gaussian_dim: 2 };
4110        assert_eq!(
4111            certify_linear_rigidity_within(4, &clauses, &tiny),
4112            Err(Refusal::OverBudgetGaussian { dim: 4, cap: 2 })
4113        );
4114        // Within budget, the same system certifies and re-checks.
4115        let cert = certify_linear_rigidity_within(4, &clauses, &Budget::standard()).expect("within budget");
4116        assert!(check_linear_rigidity(&cert, &clauses));
4117        // No parity structure ⇒ a NoLinearStructure refusal, not a false certificate.
4118        let plain = vec![vec![Lit::pos(0), Lit::pos(1)]];
4119        assert_eq!(
4120            certify_linear_rigidity_within(2, &plain, &Budget::standard()),
4121            Err(Refusal::NoLinearStructure)
4122        );
4123    }
4124
4125    #[test]
4126    fn structural_bound_rejects_tampered_generators() {
4127        let (cnf, _) = crate::families::php(3);
4128        let gens = crate::hypercube::php_perm_symmetries(3);
4129        let mut sb = structural_bound(cnf.num_vars, &cnf.clauses, &gens).expect("php is symmetric");
4130        assert!(sb.verify());
4131        // Corrupt the generator description: the recovered generators no longer reconstruct F.
4132        if let Descriptor::IntSeq { encoded } = &mut sb.gens.descriptor {
4133            let mid = encoded.len() / 2;
4134            encoded[mid] ^= 0xff;
4135        }
4136        assert!(!sb.verify(), "a tampered generator description must be rejected");
4137    }
4138}