Skip to main content

logicaffeine_proof/
steenrod.rs

1//! The **Steenrod algebra** acting on `H*(BZ/2; Z/2) = Z/2[x]` — the deepest layer of cohomology
2//! operations, and a place where homotopy theory turns out to *be* number theory.
3//!
4//! The total Steenrod square `Sq = Σᵢ Sqⁱ` is the ring endomorphism of `Z/2[x]` determined by
5//! `Sq(x) = x + x²` (`Sq⁰x = x`, `Sq¹x = x²` since `|x| = 1`, `Sqⁱx = 0` for `i > 1`) extended
6//! multiplicatively by the **Cartan formula** `Sq(ab) = Sq(a)Sq(b)`. Computing it is then pure
7//! polynomial arithmetic over `Z/2`, and the answer is striking:
8//!
9//! ```text
10//!   Sqⁱ(xᵏ) = coefficient of x^{k+i} in (x + x²)ᵏ = C(k, i) mod 2
11//! ```
12//!
13//! — the binomial coefficients mod 2, i.e. **Lucas' theorem** (`C(k,i) ≡ 1 ⟺ i AND k = i` bit-wise).
14//! The top square `Sqᵏ(xᵏ) = x^{2k}` agrees with the cup-square ([`crate::postnikov::cup_square`]); the
15//! whole infinite family of operations is governed by one binomial law.
16
17/// A polynomial over `Z/2`: `p[d]` is the coefficient of `xᵈ` (each `0` or `1`).
18fn add(a: &[u8], b: &[u8]) -> Vec<u8> {
19    let n = a.len().max(b.len());
20    (0..n).map(|i| (a.get(i).copied().unwrap_or(0) + b.get(i).copied().unwrap_or(0)) % 2).collect()
21}
22
23fn mul(a: &[u8], b: &[u8]) -> Vec<u8> {
24    if a.is_empty() || b.is_empty() {
25        return vec![];
26    }
27    let mut out = vec![0u8; a.len() + b.len() - 1];
28    for (i, &ai) in a.iter().enumerate() {
29        if ai == 1 {
30            for (j, &bj) in b.iter().enumerate() {
31                out[i + j] = (out[i + j] + bj) % 2;
32            }
33        }
34    }
35    out
36}
37
38/// The monomial `xᵏ`.
39pub fn monomial(k: usize) -> Vec<u8> {
40    let mut p = vec![0u8; k + 1];
41    p[k] = 1;
42    p
43}
44
45fn trim(mut p: Vec<u8>) -> Vec<u8> {
46    while p.len() > 1 && *p.last().unwrap() == 0 {
47        p.pop();
48    }
49    p
50}
51
52/// The **total Steenrod square** `Sq`, as the ring endomorphism of `Z/2[x]` sending `x ↦ x + x²`
53/// (Cartan formula). Returns `Sq(p)` as a polynomial.
54pub fn total_square(poly: &[u8]) -> Vec<u8> {
55    let x_plus_x2 = [0u8, 1, 1]; // x + x²
56    let mut result = vec![0u8];
57    let mut power = vec![1u8]; // (x + x²)^0
58    for &c in poly {
59        if c == 1 {
60            result = add(&result, &power);
61        }
62        power = mul(&power, &x_plus_x2);
63    }
64    trim(result)
65}
66
67/// `Sqⁱ(xᵏ)` on `H*(BZ/2; Z/2)` — the coefficient of `x^{k+i}` in `Sq(xᵏ) = (x + x²)ᵏ`.
68pub fn sq(i: usize, k: usize) -> u8 {
69    let s = total_square(&monomial(k));
70    s.get(k + i).copied().unwrap_or(0)
71}
72
73/// `C(k, i)` (binomial coefficient), computed by Pascal's recurrence — independent of the topology, so
74/// matching it against [`sq`] is a genuine theorem, not a definition.
75fn binom(k: usize, i: usize) -> u64 {
76    if i > k {
77        return 0;
78    }
79    let mut row = vec![1u64; 1];
80    for r in 1..=k {
81        let mut next = vec![1u64; r + 1];
82        for c in 1..r {
83            next[c] = row[c - 1] + row[c];
84        }
85        row = next;
86    }
87    row[i]
88}
89
90/// Apply a single `Sqⁱ` to a class in `H*(BZ/2; Z/2) = Z/2[x]`: `Sqⁱ(Σ aₖ xᵏ) = Σ aₖ C(k,i) x^{k+i}`.
91pub fn apply_sq(i: usize, poly: &[u8]) -> Vec<u8> {
92    let mut out = vec![0u8; poly.len() + i];
93    for (k, &a) in poly.iter().enumerate() {
94        if a == 1 && binom(k, i) % 2 == 1 {
95            out[k + i] ^= 1;
96        }
97    }
98    trim(out)
99}
100
101/// The Adem relation applied to `SqᵃSqᵇ` (`a < 2b`): `Σ_c C(b-c-1, a-2c) Sq^{a+b-c} Sq^c` over `Z/2`,
102/// returned as a list of monomials (`Sq⁰` = identity is dropped).
103fn adem(a: usize, b: usize) -> Vec<Vec<usize>> {
104    let mut terms = Vec::new();
105    for c in 0..=(a / 2) {
106        if binom(b - c - 1, a - 2 * c) % 2 == 1 {
107            let mut m = vec![a + b - c];
108            if c > 0 {
109                m.push(c);
110            }
111            terms.push(m);
112        }
113    }
114    terms
115}
116
117/// Is a monomial `Sq^{i₁}…Sq^{iₖ}` **admissible**? (`i_j ≥ 2 i_{j+1}` for all adjacent pairs.)
118pub fn is_admissible(m: &[usize]) -> bool {
119    m.iter().all(|&i| i >= 1) && m.windows(2).all(|w| w[0] >= 2 * w[1])
120}
121
122/// Reduce a Steenrod monomial to its `Z/2` sum of **admissible** monomials via the Adem relations — the
123/// canonical form in the Steenrod algebra. A `HashSet` models the `Z/2` linear combination (present =
124/// coefficient 1; re-insertion cancels).
125pub fn adem_reduce(m: &[usize]) -> std::collections::HashSet<Vec<usize>> {
126    let m: Vec<usize> = m.iter().copied().filter(|&i| i > 0).collect();
127    if is_admissible(&m) {
128        let mut s = std::collections::HashSet::new();
129        if !m.is_empty() {
130            s.insert(m);
131        }
132        return s;
133    }
134    let j = m.windows(2).position(|w| w[0] < 2 * w[1]).unwrap();
135    let (a, b) = (m[j], m[j + 1]);
136    let mut result: std::collections::HashSet<Vec<usize>> = std::collections::HashSet::new();
137    for term in adem(a, b) {
138        let mut spliced = m[..j].to_vec();
139        spliced.extend_from_slice(&term);
140        spliced.extend_from_slice(&m[j + 2..]);
141        for adm in adem_reduce(&spliced) {
142            if !result.insert(adm.clone()) {
143                result.remove(&adm); // XOR over Z/2
144            }
145        }
146    }
147    result
148}
149
150/// Over `Z/2`, is `target` in the linear span of `generators`? Each "vector" is a set of admissible
151/// monomials (its `Z/2` support); XOR is symmetric difference; the pivot is the largest monomial.
152fn z2_span_contains(generators: Vec<std::collections::HashSet<Vec<usize>>>, target: std::collections::HashSet<Vec<usize>>) -> bool {
153    type Vec2 = std::collections::HashSet<Vec<usize>>;
154    fn reduce(mut v: Vec2, basis: &[(Vec<usize>, Vec2)]) -> Vec2 {
155        loop {
156            if v.is_empty() {
157                return v;
158            }
159            let piv = v.iter().max().unwrap().clone();
160            match basis.iter().find(|(p, _)| *p == piv) {
161                Some((_, bset)) => {
162                    for x in bset {
163                        if !v.insert(x.clone()) {
164                            v.remove(x);
165                        }
166                    }
167                }
168                None => return v,
169            }
170        }
171    }
172    let mut basis: Vec<(Vec<usize>, Vec2)> = Vec::new();
173    for g in generators {
174        let r = reduce(g, &basis);
175        if !r.is_empty() {
176            let piv = r.iter().max().unwrap().clone();
177            basis.push((piv, r));
178        }
179    }
180    reduce(target, &basis).is_empty()
181}
182
183/// The `Z/2` rank of a set of "vectors" (sets of admissible monomials) — Gaussian elimination, pivot =
184/// largest monomial.
185fn z2_rank(generators: Vec<std::collections::HashSet<Vec<usize>>>) -> usize {
186    type Vec2 = std::collections::HashSet<Vec<usize>>;
187    fn reduce(mut v: Vec2, basis: &[(Vec<usize>, Vec2)]) -> Vec2 {
188        loop {
189            if v.is_empty() {
190                return v;
191            }
192            let piv = v.iter().max().unwrap().clone();
193            match basis.iter().find(|(p, _)| *p == piv) {
194                Some((_, bset)) => {
195                    for x in bset {
196                        if !v.insert(x.clone()) {
197                            v.remove(x);
198                        }
199                    }
200                }
201                None => return v,
202            }
203        }
204    }
205    let mut basis: Vec<(Vec<usize>, Vec2)> = Vec::new();
206    for g in generators {
207        let r = reduce(g, &basis);
208        if !r.is_empty() {
209            let piv = r.iter().max().unwrap().clone();
210            basis.push((piv, r));
211        }
212    }
213    basis.len()
214}
215
216/// The **admissible monomials** of total degree `t` — a basis of the Steenrod algebra `𝒜_t`.
217pub fn admissibles_of_degree(t: usize) -> Vec<Vec<usize>> {
218    fn rec(remaining: usize, min_left: usize, suffix: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
219        if remaining == 0 {
220            out.push(suffix.clone());
221            return;
222        }
223        for i in min_left.max(1)..=remaining {
224            suffix.insert(0, i);
225            rec(remaining - i, 2 * i, suffix, out);
226            suffix.remove(0);
227        }
228    }
229    let mut out = Vec::new();
230    rec(t, 1, &mut Vec::new(), &mut out);
231    out
232}
233
234/// `dim Ext^{1,t}_𝒜(Z/2, Z/2)` — the first line of the Adams `E₂` page = the **indecomposables** of the
235/// Steenrod algebra in degree `t`. Equals `dim 𝒜_t − dim(𝒜⁺·𝒜⁺)_t`. By Milnor it is `1` exactly when
236/// `t` is a power of 2 (the `h_i = Sq^{2ⁱ}`), and `0` otherwise.
237pub fn adams_one_line(t: usize) -> usize {
238    let dim_at = admissibles_of_degree(t).len();
239    let mut decomposables: Vec<std::collections::HashSet<Vec<usize>>> = Vec::new();
240    for d in 1..t {
241        for a in admissibles_of_degree(d) {
242            for b in admissibles_of_degree(t - d) {
243                let mut prod = a.clone();
244                prod.extend_from_slice(&b);
245                decomposables.push(adem_reduce(&prod));
246            }
247        }
248    }
249    dim_at - z2_rank(decomposables)
250}
251
252/// Is `Sqⁿ` **decomposable** — a `Z/2` combination of products `SqᵃSqᵇ` (`a, b ≥ 1`) — in the Steenrod
253/// algebra? Decided by reducing every such product to the admissible basis (Adem) and testing whether
254/// `Sqⁿ` lies in their span.
255pub fn is_decomposable_sq(n: usize) -> bool {
256    let generators: Vec<_> = (1..n).map(|a| adem_reduce(&[a, n - a])).collect();
257    let target: std::collections::HashSet<Vec<usize>> = [vec![n]].into_iter().collect();
258    z2_span_contains(generators, target)
259}
260
261/// An unbounded `Z/2` bit-vector (the resolution outgrows `u128` once the Steenrod algebra's degree-wise
262/// dimension exceeds 128). Word-packed; the pivot is always the lowest set bit.
263#[derive(Clone, PartialEq, Eq)]
264struct Bits(Vec<u64>);
265
266impl Bits {
267    fn new() -> Self {
268        Bits(Vec::new())
269    }
270    fn bit(i: usize) -> Self {
271        let mut b = Bits::new();
272        b.toggle(i);
273        b
274    }
275    fn from_indices(it: impl Iterator<Item = usize>) -> Self {
276        let mut b = Bits::new();
277        for i in it {
278            b.toggle(i);
279        }
280        b
281    }
282    fn toggle(&mut self, i: usize) {
283        let w = i / 64;
284        if w >= self.0.len() {
285            self.0.resize(w + 1, 0);
286        }
287        self.0[w] ^= 1u64 << (i % 64);
288    }
289    fn get(&self, i: usize) -> bool {
290        let w = i / 64;
291        w < self.0.len() && (self.0[w] >> (i % 64)) & 1 == 1
292    }
293    fn is_zero(&self) -> bool {
294        self.0.iter().all(|&w| w == 0)
295    }
296    /// The lowest set bit — the Gaussian-elimination pivot.
297    fn lowest(&self) -> Option<usize> {
298        self.0.iter().enumerate().find(|(_, &w)| w != 0).map(|(wi, &w)| wi * 64 + w.trailing_zeros() as usize)
299    }
300}
301
302impl std::ops::BitXorAssign<&Bits> for Bits {
303    fn bitxor_assign(&mut self, o: &Bits) {
304        if o.0.len() > self.0.len() {
305            self.0.resize(o.0.len(), 0);
306        }
307        for (a, b) in self.0.iter_mut().zip(&o.0) {
308            *a ^= b;
309        }
310    }
311}
312
313/// Basis of the `Z/2` nullspace of the linear map whose `j`-th column is `cols[j]` (a bitmask over the
314/// codomain): each returned `Bits` is a mask over the *domain* indices summing the columns to zero.
315fn z2_nullspace(cols: &[Bits]) -> Vec<Bits> {
316    let mut pivots: std::collections::HashMap<usize, (Bits, Bits)> = std::collections::HashMap::new();
317    let mut kernel = Vec::new();
318    for (j, col) in cols.iter().enumerate() {
319        let (mut cod, mut trk) = (col.clone(), Bits::bit(j));
320        while let Some(lead) = cod.lowest() {
321            match pivots.get(&lead) {
322                Some((pcod, ptrk)) => {
323                    cod ^= pcod;
324                    trk ^= ptrk;
325                }
326                None => break,
327            }
328        }
329        match cod.lowest() {
330            None => kernel.push(trk),
331            Some(lead) => {
332                pivots.insert(lead, (cod, trk));
333            }
334        }
335    }
336    kernel
337}
338
339/// `Z/2` rank of a set of `Bits` row vectors.
340fn z2_rank_u128(rows: Vec<Bits>) -> usize {
341    let mut pivots: std::collections::HashMap<usize, Bits> = std::collections::HashMap::new();
342    for mut v in rows {
343        while let Some(lead) = v.lowest() {
344            match pivots.get(&lead) {
345                Some(p) => v ^= p,
346                None => {
347                    pivots.insert(lead, v);
348                    break;
349                }
350            }
351        }
352    }
353    pivots.len()
354}
355
356/// Basis of the degree-`s` part of `C₁ = ⊕ᵢ 𝒜·hᵢ` in the minimal resolution: `(i, m)` with `2ⁱ ≤ s` and
357/// `m` an admissible of degree `s − 2ⁱ` (the element `m·hᵢ`).
358fn c1_basis(s: usize) -> Vec<(usize, Vec<usize>)> {
359    let mut out = Vec::new();
360    let mut i = 0;
361    while (1usize << i) <= s {
362        for m in admissibles_of_degree(s - (1 << i)) {
363            out.push((i, m));
364        }
365        i += 1;
366    }
367    out
368}
369
370/// `(basis, kernel)` of `d₁ : C₁ → 𝒜` in degree `s`, where `d₁(m·hᵢ) = m·Sq^{2ⁱ}`. Kernel vectors are
371/// masks over the returned `c1_basis(s)`.
372fn ker_d1(s: usize) -> (Vec<(usize, Vec<usize>)>, Vec<Bits>) {
373    let basis = c1_basis(s);
374    let cod_idx: std::collections::HashMap<Vec<usize>, usize> =
375        admissibles_of_degree(s).into_iter().enumerate().map(|(i, m)| (m, i)).collect();
376    let cols: Vec<Bits> = basis
377        .iter()
378        .map(|(i, m)| {
379            let mut prod = m.clone();
380            prod.push(1 << i); // m · Sq^{2^i}
381            let mut mask = Bits::new();
382            for adm in adem_reduce(&prod) {
383                if let Some(&idx) = cod_idx.get(&adm) {
384                    mask.toggle(idx);
385                }
386            }
387            mask
388        })
389        .collect();
390    let kernel = z2_nullspace(&cols);
391    (basis, kernel)
392}
393
394/// `dim Ext^{2,t}_𝒜(Z/2, Z/2)` — the second line of the Adams `E₂` page: the minimal generators of
395/// `ker(d₁)` in degree `t`, i.e. `dim ker(d₁)_t − dim(𝒜⁺·ker(d₁))_t`. The relations among the `Sq^{2ⁱ}`.
396pub fn adams_two_line(t: usize) -> usize {
397    let (_basis_t, ker_t) = ker_d1(t);
398    let c1_idx_t: std::collections::HashMap<(usize, Vec<usize>), usize> =
399        c1_basis(t).into_iter().enumerate().map(|(i, b)| (b, i)).collect();
400    let mut decomposables: Vec<Bits> = Vec::new();
401    for k in 1..t {
402        let (basis_s, ker_s) = ker_d1(t - k);
403        for zmask in &ker_s {
404            let mut result = Bits::new();
405            for (b, (i, m)) in basis_s.iter().enumerate() {
406                if zmask.get(b) {
407                    let mut prod = vec![k];
408                    prod.extend_from_slice(m);
409                    for mprime in adem_reduce(&prod) {
410                        if let Some(&idx) = c1_idx_t.get(&(*i, mprime)) {
411                            result.toggle(idx);
412                        }
413                    }
414                }
415            }
416            if !result.is_zero() {
417                decomposables.push(result);
418            }
419        }
420    }
421    ker_t.len() - z2_rank_u128(decomposables)
422}
423
424/// The known `dim Ext^{2,t}` from the chart: `#{(i,j) : i ≤ j, j ≠ i+1, 2ⁱ + 2ʲ = t}` (the `h_i h_j`
425/// with the adjacency relation `h_i h_{i+1} = 0`).
426fn known_ext2(t: usize) -> usize {
427    let mut count = 0;
428    let mut i = 0;
429    while (1usize << i) <= t {
430        let mut j = i;
431        while (1usize << i) + (1usize << j) <= t {
432            if (1 << i) + (1 << j) == t && j != i + 1 {
433                count += 1;
434            }
435            j += 1;
436        }
437        i += 1;
438    }
439    count
440}
441
442/// A generator of the minimal free resolution: its internal degree and its boundary `d(g) ∈ C_{s-1}`,
443/// a `Z/2` set of `(previous-generator-index, admissible monomial)` pairs (the element `Σ a·g'`).
444#[derive(Clone)]
445struct ResGen {
446    degree: usize,
447    boundary: Vec<(usize, Vec<usize>)>,
448}
449
450/// An **augmented graded `Z/2`-algebra**, given by its degree-wise basis and product. The minimal-resolution
451/// engine (and hence the whole `Ext`/Adams machine) is generic over this — the Steenrod algebra is just one
452/// instance. Point it at a different algebra and the same machine computes a different spectrum's Adams chart.
453trait Algebra {
454    /// The `Z/2`-basis of the algebra in internal degree `degree` (each element encoded as a monomial). For a
455    /// finite algebra this is empty above the top degree.
456    fn basis(&self, degree: usize) -> Vec<Vec<usize>>;
457    /// The product `a · b`, reduced to the basis, as a `Z/2` support set. The unit is the empty monomial.
458    fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>>;
459    /// The **algebra generators** (indecomposables) of positive degree up to `max_degree`, as `(degree, key)`.
460    /// Multiplying a kernel by just these spans `A⁺·ker` (the kernel is a submodule), so the resolution's
461    /// decomposables loop needs only the generators — the key speed symmetry: a handful instead of the whole
462    /// algebra per degree.
463    fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)>;
464}
465
466/// The full mod-2 Steenrod algebra `𝒜`: basis = admissible monomials, product = concatenate then Adem-reduce.
467struct SteenrodAlgebra;
468impl Algebra for SteenrodAlgebra {
469    fn basis(&self, degree: usize) -> Vec<Vec<usize>> {
470        admissibles_of_degree(degree)
471    }
472    fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>> {
473        let mut prod = a.to_vec();
474        prod.extend_from_slice(b);
475        reduce_unit(&prod)
476    }
477    fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)> {
478        let mut out = Vec::new();
479        let mut d = 1usize;
480        while d <= max_degree {
481            out.push((d, vec![d])); // Sq^{2ⁱ}, the indecomposables of 𝒜
482            d <<= 1;
483        }
484        out
485    }
486}
487
488/// A `Z/2`-element of the Steenrod algebra as the support set of admissible monomials it sums (e.g. the
489/// `A(1)` element `Sq²Sq³ = Sq⁵ + Sq⁴Sq¹` is `{[5], [4,1]}`).
490type Combo = std::collections::BTreeSet<Vec<usize>>;
491
492/// `Z/2` product of two combinations: `(Σ mᵢ)(Σ nⱼ) = Σ adem_reduce(mᵢ·nⱼ)`.
493fn combo_mul(a: &Combo, b: &Combo) -> Combo {
494    let mut out = Combo::new();
495    for ma in a {
496        for mb in b {
497            let mut prod = ma.clone();
498            prod.extend_from_slice(mb);
499            for r in reduce_unit(&prod) {
500                if !out.insert(r.clone()) {
501                    out.remove(&r);
502                }
503            }
504        }
505    }
506    out
507}
508
509/// A per-degree map from a basis element's **pivot monomial** (its largest monomial) to its index — turns the
510/// echelon reduction's inner lookup from an O(basis) scan into O(1).
511type PivotIndex = std::collections::HashMap<Vec<usize>, usize>;
512
513/// Express a combination in a degree's **echelon basis** (each basis element pivoted on its largest
514/// monomial): returns the indices whose `Z/2` sum is `target`. `target` must lie in the span (always true for
515/// products inside a subalgebra). Pivot lookups are O(1) via `pivots`.
516fn express_in_basis(target: &Combo, basis: &[Combo], pivots: &PivotIndex) -> Vec<usize> {
517    let mut r = target.clone();
518    let mut used = Vec::new();
519    while let Some(piv) = r.iter().next_back().cloned() {
520        match pivots.get(&piv) {
521            Some(&idx) => {
522                used.push(idx);
523                for m in &basis[idx] {
524                    if !r.insert(m.clone()) {
525                        r.remove(m);
526                    }
527                }
528            }
529            None => break, // not in span — only on a closure bug
530        }
531    }
532    used.sort_unstable();
533    used
534}
535
536/// A **finite subalgebra** of `𝒜`, given a `Z/2`-basis of *combinations* per degree (a subalgebra need not be
537/// spanned by single admissibles — e.g. `Sq²Sq³ = Sq⁵+Sq⁴Sq¹ ∈ A(1)`). Basis elements are addressed by the
538/// opaque key `[degree, index]`. `A(1) = ⟨Sq¹, Sq²⟩` is the headline instance — its `Ext` is the Adams `E₂`
539/// for `ko` (connective real K-theory).
540struct SubAlgebra {
541    by_degree: Vec<Vec<Combo>>,
542    pivots: Vec<PivotIndex>,
543    gen_keys: Vec<(usize, Vec<usize>)>,
544}
545
546impl SubAlgebra {
547    fn dimension(&self) -> usize {
548        self.by_degree.iter().map(|v| v.len()).sum()
549    }
550}
551
552/// Reduce a combination against a degree's echelon basis (pivot = largest monomial), using the O(1) pivot
553/// index; returns the residual.
554fn reduce_combo(basis: &[Combo], pivots: &PivotIndex, c: &Combo) -> Combo {
555    let mut r = c.clone();
556    while let Some(piv) = r.iter().next_back().cloned() {
557        match pivots.get(&piv) {
558            Some(&idx) => {
559                for m in &basis[idx] {
560                    if !r.insert(m.clone()) {
561                        r.remove(m);
562                    }
563                }
564            }
565            None => break,
566        }
567    }
568    r
569}
570
571/// Reduce `c` against its degree's basis; if it is a NEW independent direction, append the residual to the
572/// echelon basis, register its pivot, and queue it. The heart of the fast build: only the basis is ever stored.
573fn subalg_add(by_degree: &mut [Vec<Combo>], pivots: &mut [PivotIndex], worklist: &mut Vec<Combo>, max_degree: usize, c: Combo) {
574    let d = match c.iter().next() {
575        Some(m) => m.iter().sum::<usize>(),
576        None => return,
577    };
578    if d > max_degree {
579        return;
580    }
581    let r = reduce_combo(&by_degree[d], &pivots[d], &c);
582    if let Some(piv) = r.iter().next_back().cloned() {
583        pivots[d].insert(piv, by_degree[d].len());
584        by_degree[d].push(r.clone());
585        worklist.push(r);
586    }
587}
588
589/// Build the subalgebra generated by `gens` (each a single square), by closing under the Adem product up to
590/// `max_degree` and extracting an echelon basis of combinations per degree.
591fn build_subalgebra(gens: &[Vec<usize>], max_degree: usize) -> SubAlgebra {
592    let unit: Combo = std::iter::once(vec![]).collect();
593    let gen_combos: Vec<Combo> = gens.iter().map(|g| std::iter::once(g.clone()).collect()).collect();
594    let mut by_degree: Vec<Vec<Combo>> = vec![Vec::new(); max_degree + 1];
595    let mut pivots: Vec<PivotIndex> = vec![PivotIndex::new(); max_degree + 1];
596    let mut worklist: Vec<Combo> = Vec::new();
597    // Seed with the unit and the generators, then BFS: multiply each new basis element by the generators.
598    // Every A-element is a word in the generators, so right-multiplying the basis by them reaches all of A —
599    // O(dim · #gens) products with O(1) pivot lookups, instead of O(span²) re-scans.
600    subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, unit);
601    for g in &gen_combos {
602        subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, g.clone());
603    }
604    while let Some(b) = worklist.pop() {
605        for g in &gen_combos {
606            let p = combo_mul(&b, g);
607            subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, p);
608        }
609    }
610    // Record the algebra-generator keys: each `Sq^{2ⁱ}` is the largest monomial in its degree, so it is its
611    // own pivot and sits as a single basis element addressed by `[degree, index]`.
612    let gen_keys: Vec<(usize, Vec<usize>)> = gens
613        .iter()
614        .map(|g| {
615            let d: usize = g.iter().sum();
616            (d, vec![d, pivots[d][g]])
617        })
618        .collect();
619    SubAlgebra { by_degree, pivots, gen_keys }
620}
621
622impl Algebra for SubAlgebra {
623    fn basis(&self, degree: usize) -> Vec<Vec<usize>> {
624        (0..self.by_degree.get(degree).map_or(0, |v| v.len())).map(|i| vec![degree, i]).collect()
625    }
626    fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>> {
627        let prod = combo_mul(&self.by_degree[a[0]][a[1]], &self.by_degree[b[0]][b[1]]);
628        let pd = a[0] + b[0];
629        match self.by_degree.get(pd) {
630            Some(basis) if !prod.is_empty() => {
631                express_in_basis(&prod, basis, &self.pivots[pd]).into_iter().map(|i| vec![pd, i]).collect()
632            }
633            _ => std::collections::HashSet::new(),
634        }
635    }
636    fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)> {
637        self.gen_keys.iter().filter(|(d, _)| *d <= max_degree).cloned().collect()
638    }
639}
640
641/// Left-multiply a boundary by a monomial over an arbitrary algebra: `m · Σ(g', a) = Σ(g', m·a)` over `Z/2`.
642fn act_boundary_over(alg: &dyn Algebra, m: &[usize], boundary: &[(usize, Vec<usize>)]) -> std::collections::HashSet<(usize, Vec<usize>)> {
643    let mut acc: std::collections::HashSet<(usize, Vec<usize>)> = std::collections::HashSet::new();
644    for (g, a) in boundary {
645        for ap in alg.multiply(m, a) {
646            let key = (*g, ap);
647            if !acc.insert(key.clone()) {
648                acc.remove(&key);
649            }
650        }
651    }
652    acc
653}
654
655/// The degree-`t` basis of `C_s = ⊕ A·g` over an arbitrary algebra: `(generator index, basis element of
656/// degree t−deg(g))`.
657fn module_basis_over(alg: &dyn Algebra, gens: &[ResGen], t: usize) -> Vec<(usize, Vec<usize>)> {
658    let mut out = Vec::new();
659    for (gi, g) in gens.iter().enumerate() {
660        if g.degree <= t {
661            for m in alg.basis(t - g.degree) {
662                out.push((gi, m));
663            }
664        }
665    }
666    out
667}
668
669/// Left-multiply a boundary by a Steenrod monomial: `m · Σ(g', a) = Σ(g', adem_reduce(m·a))`, over `Z/2`.
670fn act_boundary(m: &[usize], boundary: &[(usize, Vec<usize>)]) -> std::collections::HashSet<(usize, Vec<usize>)> {
671    act_boundary_over(&SteenrodAlgebra, m, boundary)
672}
673
674/// The degree-`t` basis of `C_s = ⊕ 𝒜·g`: pairs `(generator index, admissible monomial of degree t−deg(g))`.
675fn module_basis(gens: &[ResGen], t: usize) -> Vec<(usize, Vec<usize>)> {
676    module_basis_over(&SteenrodAlgebra, gens, t)
677}
678
679/// Build the minimal free resolution of `Z/2` over the Steenrod algebra up to homological degree `max_s`
680/// and internal degree `max_t`. `Ext^{s,t} = #(generators of C_s in degree t)`.
681fn minimal_resolution(max_s: usize, max_t: usize) -> Vec<Vec<ResGen>> {
682    minimal_resolution_over(&SteenrodAlgebra, max_s, max_t)
683}
684
685/// Build the minimal free resolution of `Z/2` over an **arbitrary augmented algebra** up to homological
686/// degree `max_s` and internal degree `max_t`. `Ext^{s,t} = #(generators of C_s in degree t)`. The algorithm
687/// is unchanged from the Steenrod case — only the basis and product come from `alg` — which is the whole
688/// point: one resolution engine, every algebra.
689fn minimal_resolution_over(alg: &dyn Algebra, max_s: usize, max_t: usize) -> Vec<Vec<ResGen>> {
690    let mut res: Vec<Vec<ResGen>> = vec![vec![ResGen { degree: 0, boundary: vec![] }]]; // C_0 = A·g₀
691    for s in 1..=max_s {
692        let mut new_gens: Vec<ResGen> = Vec::new();
693        for t in 1..=max_t {
694            // Basis of C_{s-1}(t) and an index for it.
695            let prev_basis = module_basis_over(alg, &res[s - 1], t);
696            let prev_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
697                prev_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
698
699            // Kernel of d_{s-1} : C_{s-1}(t) → C_{s-2}(t).  (For s=1, d_0 is the augmentation: every
700            // positive-degree element is in the kernel, so ker = the whole basis.)
701            let kernel: Vec<Bits> = if s == 1 {
702                (0..prev_basis.len()).map(Bits::bit).collect()
703            } else {
704                let cc_basis = module_basis_over(alg, &res[s - 2], t);
705                let cc_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
706                    cc_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
707                let cols: Vec<Bits> = prev_basis
708                    .iter()
709                    .map(|(gi, m)| {
710                        let img = act_boundary_over(alg, m, &res[s - 1][*gi].boundary);
711                        Bits::from_indices(img.into_iter().map(|e| cc_idx[&e]))
712                    })
713                    .collect();
714                z2_nullspace(&cols)
715            };
716
717            // Decomposables A⁺·ker = span{ g · z : g a GENERATOR, z ∈ ker(d_{s-1})(t-deg g) }. Because ker is
718            // an A-submodule, multiplying by the generators alone spans A⁺·ker — so we loop the handful of
719            // generators (Sq^{2ⁱ}) instead of the whole algebra. This is the speed symmetry break.
720            let mut decomp: Vec<Bits> = Vec::new();
721            for (gdeg, gkey) in alg.generators(t - 1) {
722                let lower = kernel_at_over(alg, &res, s - 1, t - gdeg);
723                let lower_basis = module_basis_over(alg, &res[s - 1], t - gdeg);
724                for z in &lower.0 {
725                    let mut v = Bits::new();
726                    for (b, (gi, m)) in lower_basis.iter().enumerate() {
727                        if z.get(b) {
728                            for mp in alg.multiply(&gkey, m) {
729                                if let Some(&idx) = prev_idx.get(&(*gi, mp)) {
730                                    v.toggle(idx);
731                                }
732                            }
733                        }
734                    }
735                    if !v.is_zero() {
736                        decomp.push(v);
737                    }
738                }
739            }
740
741            // New generators = kernel vectors independent modulo the decomposables.
742            let mut pivots: std::collections::HashMap<usize, Bits> = std::collections::HashMap::new();
743            for mut d in decomp {
744                while let Some(lead) = d.lowest() {
745                    match pivots.get(&lead) {
746                        Some(p) => d ^= p,
747                        None => {
748                            pivots.insert(lead, d);
749                            break;
750                        }
751                    }
752                }
753            }
754            for kv in &kernel {
755                let mut r = kv.clone();
756                while let Some(lead) = r.lowest() {
757                    match pivots.get(&lead) {
758                        Some(p) => r ^= p,
759                        None => break,
760                    }
761                }
762                if let Some(lead) = r.lowest() {
763                    pivots.insert(lead, r);
764                    let boundary: Vec<(usize, Vec<usize>)> =
765                        (0..prev_basis.len()).filter(|&b| kv.get(b)).map(|b| prev_basis[b].clone()).collect();
766                    new_gens.push(ResGen { degree: t, boundary });
767                }
768            }
769        }
770        res.push(new_gens);
771    }
772    res
773}
774
775/// `(kernel vectors, _)` of `d_{s} : C_{s}(t) → C_{s-1}(t)` — small helper used during decomposable
776/// computation. Returns kernel masks over `module_basis(C_s, t)`.
777fn kernel_at(res: &[Vec<ResGen>], s: usize, t: usize) -> (Vec<Bits>, ()) {
778    kernel_at_over(&SteenrodAlgebra, res, s, t)
779}
780
781/// `kernel_at` over an arbitrary algebra.
782fn kernel_at_over(alg: &dyn Algebra, res: &[Vec<ResGen>], s: usize, t: usize) -> (Vec<Bits>, ()) {
783    let basis = module_basis_over(alg, &res[s], t);
784    if s == 0 {
785        return ((0..basis.len()).map(Bits::bit).collect(), ());
786    }
787    let cc_basis = module_basis_over(alg, &res[s - 1], t);
788    let cc_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
789        cc_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
790    let cols: Vec<Bits> = basis
791        .iter()
792        .map(|(gi, m)| Bits::from_indices(act_boundary_over(alg, m, &res[s][*gi].boundary).into_iter().map(|e| cc_idx[&e])))
793        .collect();
794    (z2_nullspace(&cols), ())
795}
796
797/// `dim Ext^{s,t}_𝒜(Z/2, Z/2)` — the full Adams `E₂` page from the minimal resolution.
798pub fn ext(max_s: usize, max_t: usize) -> Vec<Vec<usize>> {
799    ext_over(&SteenrodAlgebra, max_s, max_t)
800}
801
802/// `dim Ext^{s,t}_A(Z/2, Z/2)` over an arbitrary algebra `A` — the Adams `E₂` page for whatever spectrum `A`
803/// resolves. `A = 𝒜` gives the sphere; `A = A(1)` gives `ko`.
804fn ext_over(alg: &dyn Algebra, max_s: usize, max_t: usize) -> Vec<Vec<usize>> {
805    let res = minimal_resolution_over(alg, max_s, max_t);
806    (0..=max_s)
807        .map(|s| (0..=max_t).map(|t| res[s].iter().filter(|g| g.degree == t).count()).collect())
808        .collect()
809}
810
811/// `h₀`-multiplication read straight off the resolution boundaries: generator `g` at stage `s` is sent by
812/// `h₀ = Sq¹` to the stage-`(s+1)` generators `g'` whose boundary contains `Sq¹·g` (the term `(gi, [1])`).
813/// The symmetry is already in the resolution — no Yoneda lifting needed.
814fn h0_targets(res: &[Vec<ResGen>], s: usize, gi: usize) -> Vec<usize> {
815    if s + 1 >= res.len() {
816        return vec![];
817    }
818    res[s + 1]
819        .iter()
820        .enumerate()
821        .filter(|(_, g)| g.boundary.iter().any(|(j, m)| *j == gi && m.as_slice() == [1]))
822        .map(|(idx, _)| idx)
823        .collect()
824}
825
826/// The EXACT 2-local stable group of stem `n`, as the sorted multiset of `h₀`-tower lengths (a tower of
827/// length `L` is a summand `Z/2^L`). Read from the resolution's `h₀`-structure — the symmetry a dim count
828/// ignores. `[3] = Z/8`, `[1,1] = (Z/2)²`, etc.
829fn stem_2local_tower_lengths(res: &[Vec<ResGen>], n: usize) -> Vec<usize> {
830    let in_stem = |s: usize, gi: usize| s < res.len() && gi < res[s].len() && res[s][gi].degree == n + s;
831    // every (s+1,t) that is an h₀-image of a stem-n generator
832    let mut is_target: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
833    for s in 0..res.len() {
834        for gi in 0..res[s].len() {
835            if in_stem(s, gi) {
836                for t in h0_targets(res, s, gi) {
837                    if in_stem(s + 1, t) {
838                        is_target.insert((s + 1, t));
839                    }
840                }
841            }
842        }
843    }
844    let mut lengths = Vec::new();
845    for s in 0..res.len() {
846        for gi in 0..res[s].len() {
847            if in_stem(s, gi) && !is_target.contains(&(s, gi)) {
848                // tower bottom: walk h₀ upward, counting the height
849                let mut len = 1;
850                let (mut cs, mut cgi) = (s, gi);
851                loop {
852                    let ups: Vec<usize> = h0_targets(res, cs, cgi).into_iter().filter(|&t| in_stem(cs + 1, t)).collect();
853                    if let Some(&t) = ups.first() {
854                        len += 1;
855                        cs += 1;
856                        cgi = t;
857                    } else {
858                        break;
859                    }
860                }
861                lengths.push(len);
862            }
863        }
864    }
865    lengths.sort_unstable();
866    lengths
867}
868
869/// The action of a Steenrod monomial on a class in `H*(BZ/2) = Z/2[x]` (apply the squares right-to-left).
870pub fn act_monomial(m: &[usize], poly: &[u8]) -> Vec<u8> {
871    let mut p = poly.to_vec();
872    for &i in m.iter().rev() {
873        p = apply_sq(i, &p);
874    }
875    trim(p)
876}
877
878/// Adem-reduce a monomial to the admissible basis, but keep the **unit** `Sq⁰ = 1` (the empty monomial) as
879/// `[]` rather than dropping it — `adem_reduce` treats `[]` as zero, which is wrong inside the algebra's
880/// product/coproduct where `1` must survive.
881fn reduce_unit(m: &[usize]) -> std::collections::HashSet<Vec<usize>> {
882    let filtered: Vec<usize> = m.iter().copied().filter(|&i| i > 0).collect();
883    if filtered.is_empty() {
884        return std::iter::once(vec![]).collect();
885    }
886    adem_reduce(&filtered)
887}
888
889/// The **coproduct** on the Steenrod algebra: `ψ(Sqⁿ) = Σ_{a+b=n} Sqᵃ ⊗ Sqᵇ` (the Cartan diagonal),
890/// extended as an algebra map `ψ(xy) = ψ(x)ψ(y)` with `(a⊗b)(c⊗d) = ac⊗bd`, each tensor leg reduced to the
891/// admissible basis. Returns the `Z/2` support as a set of `(admissible, admissible)` pairs. This is the
892/// structure that makes `C ⊗ C` an 𝒜-module and powers the chain-level diagonal.
893fn coproduct(m: &[usize]) -> std::collections::HashSet<(Vec<usize>, Vec<usize>)> {
894    let mut acc: std::collections::HashSet<(Vec<usize>, Vec<usize>)> =
895        std::iter::once((vec![], vec![])).collect(); // ψ(1) = 1⊗1
896    for &n in m {
897        let mut next: std::collections::HashSet<(Vec<usize>, Vec<usize>)> = std::collections::HashSet::new();
898        for (lacc, racc) in &acc {
899            for a in 0..=n {
900                let (mut ll, mut rr) = (lacc.clone(), racc.clone());
901                if a > 0 {
902                    ll.push(a);
903                }
904                if n - a > 0 {
905                    rr.push(n - a);
906                }
907                for la in reduce_unit(&ll) {
908                    for ra in reduce_unit(&rr) {
909                        let key = (la.clone(), ra);
910                        if !next.insert(key.clone()) {
911                            next.remove(&key);
912                        }
913                    }
914                }
915            }
916        }
917        acc = next;
918    }
919    acc
920}
921
922/// A chain element of `C_s = ⊕ 𝒜·g`: its `Z/2` support, a set of terms `(generator index, admissible
923/// Steenrod monomial)` standing for `Σ m·g`.
924type Chain = std::collections::HashSet<(usize, Vec<usize>)>;
925
926/// `Z/2`-add `b` into `a` (re-insertion cancels).
927fn chain_add(a: &mut Chain, b: Chain) {
928    for e in b {
929        if !a.insert(e.clone()) {
930            a.remove(&e);
931        }
932    }
933}
934
935/// Left-act a Steenrod monomial on a chain element: `m·Σ(g,n) = Σ(g, adem_reduce(m·n))`.
936fn act_chain(m: &[usize], elem: &Chain) -> Chain {
937    let v: Vec<(usize, Vec<usize>)> = elem.iter().cloned().collect();
938    act_boundary(m, &v)
939}
940
941/// Solve `∂x = target` for a chain element `x ∈ (C_s)_deg`, given `target ∈ (C_{s-1})_deg`. The minimal
942/// resolution is exact, so `target ∈ im ∂` whenever it is a cycle; we pick one preimage by `Z/2` Gaussian
943/// elimination — a chosen contracting homotopy. `None` only if `target ∉ im ∂` (a resolution-exactness bug,
944/// surfaced rather than papered over).
945fn solve_boundary(res: &[Vec<ResGen>], s: usize, deg: usize, target: &Chain) -> Option<Chain> {
946    let src_basis = module_basis(&res[s], deg);
947    let tgt_basis = module_basis(&res[s - 1], deg);
948    let tgt_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
949        tgt_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
950    let mask = |c: &Chain| -> Bits { Bits::from_indices(c.iter().map(|e| tgt_idx[e])) };
951    let cols: Vec<Bits> = src_basis
952        .iter()
953        .map(|(gi, m)| mask(&act_boundary(m, &res[s][*gi].boundary)))
954        .collect();
955    // Row-reduce the columns, tracking which source columns XOR to each pivot row.
956    let mut elim: Vec<(usize, Bits, Bits)> = Vec::new(); // (pivot bit, reduced column, source combination)
957    for (j, c) in cols.iter().enumerate() {
958        let (mut r, mut src) = (c.clone(), Bits::bit(j));
959        for (piv, rc, sm) in &elim {
960            if r.get(*piv) {
961                r ^= rc;
962                src ^= sm;
963            }
964        }
965        if let Some(lead) = r.lowest() {
966            elim.push((lead, r, src));
967        }
968    }
969    let (mut tr, mut tsrc) = (mask(target), Bits::new());
970    for (piv, rc, sm) in &elim {
971        if tr.get(*piv) {
972            tr ^= rc;
973            tsrc ^= sm;
974        }
975    }
976    if !tr.is_zero() {
977        return None;
978    }
979    Some((0..src_basis.len()).filter(|&j| tsrc.get(j)).map(|j| src_basis[j].clone()).collect())
980}
981
982/// The **Yoneda product** `Ext^{a_s,t_a} ⊗ Ext^{b_s,t_b} → Ext^{a_s+b_s, t_a+t_b}` on the cohomology of the
983/// Steenrod algebra, computed from the resolution itself. The cocycle dual to generator `(a_s, a_gi)` is
984/// lifted to a comparison chain map `φ_•: C_{a_s+•} → C_•` (`ε·φ_0 = dual`, `∂φ_k = φ_{k-1}∂`, each step a
985/// `Z/2` boundary solve), and the product is `dual_b ∘ φ_{b_s}` — read off as the `(b_gi, identity)`
986/// coefficient. The comparison map is the diagonal `X → X×X` dualized onto the resolution: the geometric
987/// symmetry made algebra. Returns the product's `Z/2` support among the `C_{a_s+b_s}` generators.
988pub fn yoneda_product(res: &[Vec<ResGen>], a_s: usize, a_gi: usize, b_s: usize, b_gi: usize) -> Vec<usize> {
989    let t_a = res[a_s][a_gi].degree;
990    let t_b = res[b_s][b_gi].degree;
991    // φ_0 : C_{a_s} → C_0, the cocycle dual to generator a: g_{a_s,i} ↦ δ_{i,a}·g₀.
992    let mut phi: Vec<std::collections::HashMap<usize, Chain>> = Vec::new();
993    let mut p0 = std::collections::HashMap::new();
994    for i in 0..res[a_s].len() {
995        let mut c = Chain::new();
996        if i == a_gi {
997            c.insert((0, vec![]));
998        }
999        p0.insert(i, c);
1000    }
1001    phi.push(p0);
1002    // Lift along the resolution up to φ_{b_s}.
1003    for k in 1..=b_s {
1004        let mut pk: std::collections::HashMap<usize, Chain> = std::collections::HashMap::new();
1005        for i in 0..res[a_s + k].len() {
1006            let d_i = res[a_s + k][i].degree;
1007            if d_i < t_a {
1008                pk.insert(i, Chain::new()); // below the cocycle's degree φ vanishes; avoids a degree underflow
1009                continue;
1010            }
1011            // r = φ_{k-1}(∂ g_{a_s+k, i})  ∈ C_{k-1}
1012            let mut r = Chain::new();
1013            for (j, m) in &res[a_s + k][i].boundary {
1014                chain_add(&mut r, act_chain(m, &phi[k - 1][j]));
1015            }
1016            let x = if r.is_empty() {
1017                Chain::new()
1018            } else {
1019                solve_boundary(res, k, d_i - t_a, &r).expect("minimal resolution is exact: ∂x = r is solvable")
1020            };
1021            pk.insert(i, x);
1022        }
1023        phi.push(pk);
1024    }
1025    // Pair with dual_b: the (b_gi, identity) coefficient of φ_{b_s}(g) over the (t_a+t_b)-generators.
1026    (0..res[a_s + b_s].len())
1027        .filter(|&i| res[a_s + b_s][i].degree == t_a + t_b && phi[b_s][&i].contains(&(b_gi, vec![])))
1028        .collect()
1029}
1030
1031/// A basis term of `C ⊗ C`: `(p, i, m1, q, j, m2)` standing for `(m1·g_{p,i}) ⊗ (m2·g_{q,j})`, with `p`/`q`
1032/// the homological degrees of the two legs. An element is the `Z/2` support set of such terms.
1033type TensorTerm = (usize, usize, Vec<usize>, usize, usize, Vec<usize>);
1034type Tensor = std::collections::HashSet<TensorTerm>;
1035
1036/// `Z/2`-add `b` into `a`.
1037fn tensor_add(a: &mut Tensor, b: Tensor) {
1038    for e in b {
1039        if !a.insert(e.clone()) {
1040            a.remove(&e);
1041        }
1042    }
1043}
1044
1045/// Internal degree of a homogeneous tensor element.
1046fn tensor_degree(res: &[Vec<ResGen>], t: &Tensor) -> Option<usize> {
1047    t.iter().next().map(|(p, i, m1, q, j, m2)| {
1048        res[*p][*i].degree + m1.iter().sum::<usize>() + res[*q][*j].degree + m2.iter().sum::<usize>()
1049    })
1050}
1051
1052/// The boundary on `C ⊗ C`: `∂(a⊗b) = ∂a⊗b + a⊗∂b` (over `Z/2` the Koszul signs vanish).
1053fn tensor_boundary(res: &[Vec<ResGen>], elem: &Tensor) -> Tensor {
1054    let mut out = Tensor::new();
1055    for (p, i, m1, q, j, m2) in elem {
1056        if *p >= 1 {
1057            for (i2, m1b) in act_boundary(m1, &res[*p][*i].boundary) {
1058                let term = (*p - 1, i2, m1b, *q, *j, m2.clone());
1059                if !out.insert(term.clone()) {
1060                    out.remove(&term);
1061                }
1062            }
1063        }
1064        if *q >= 1 {
1065            for (j2, m2b) in act_boundary(m2, &res[*q][*j].boundary) {
1066                let term = (*p, *i, m1.clone(), *q - 1, j2, m2b);
1067                if !out.insert(term.clone()) {
1068                    out.remove(&term);
1069                }
1070            }
1071        }
1072    }
1073    out
1074}
1075
1076/// The diagonal 𝒜-action on `C ⊗ C`: `m·(a⊗b) = Σ_{ψ(m)=Σ m'⊗m''} (m'·a) ⊗ (m''·b)`. This is what makes
1077/// `C ⊗ C` a Steenrod-module and lets the diagonal be lifted 𝒜-linearly.
1078fn act_tensor(m: &[usize], elem: &Tensor) -> Tensor {
1079    let psi = coproduct(m);
1080    let mut out = Tensor::new();
1081    for (p, i, m1, q, j, m2) in elem {
1082        for (ml, mr) in &psi {
1083            let (mut left, mut right) = (ml.clone(), mr.clone());
1084            left.extend_from_slice(m1);
1085            right.extend_from_slice(m2);
1086            for a in reduce_unit(&left) {
1087                for b in reduce_unit(&right) {
1088                    let term = (*p, *i, a.clone(), *q, *j, b);
1089                    if !out.insert(term.clone()) {
1090                        out.remove(&term);
1091                    }
1092                }
1093            }
1094        }
1095    }
1096    out
1097}
1098
1099/// The `Z/2`-basis of `(C ⊗ C)_n` in internal degree `t` — every leg-split `p+q=n`, `t_l+t_r=t`.
1100fn tensor_basis(res: &[Vec<ResGen>], n: usize, t: usize) -> Vec<TensorTerm> {
1101    let mut out = Vec::new();
1102    for p in 0..=n {
1103        let q = n - p;
1104        if p >= res.len() || q >= res.len() {
1105            continue;
1106        }
1107        for tl in 0..=t {
1108            let tr = t - tl;
1109            for (i, m1) in module_basis(&res[p], tl) {
1110                for (j, m2) in module_basis(&res[q], tr) {
1111                    out.push((p, i, m1.clone(), q, j, m2.clone()));
1112                }
1113            }
1114        }
1115    }
1116    out
1117}
1118
1119/// Solve `∂_{C⊗C} x = target` for `x ∈ (C⊗C)_n` — cap-free `Z/2` Gaussian elimination over tensor basis
1120/// elements (pivot = the largest term), tracking which source basis elements combine. `None` only if
1121/// `target ∉ im ∂` (a coherence bug, surfaced not hidden).
1122fn solve_tensor_boundary(res: &[Vec<ResGen>], n: usize, target: &Tensor) -> Option<Tensor> {
1123    fn reduce(mut col: Tensor, mut src: Tensor, elim: &[(TensorTerm, Tensor, Tensor)]) -> (Tensor, Tensor) {
1124        loop {
1125            let piv = match col.iter().max() {
1126                Some(p) => p.clone(),
1127                None => return (col, src),
1128            };
1129            match elim.iter().find(|(p, _, _)| *p == piv) {
1130                Some((_, rc, sm)) => {
1131                    for x in rc {
1132                        if !col.insert(x.clone()) {
1133                            col.remove(x);
1134                        }
1135                    }
1136                    for x in sm {
1137                        if !src.insert(x.clone()) {
1138                            src.remove(x);
1139                        }
1140                    }
1141                }
1142                None => return (col, src),
1143            }
1144        }
1145    }
1146    let t = match tensor_degree(res, target) {
1147        Some(t) => t,
1148        None => return Some(Tensor::new()),
1149    };
1150    let mut elim: Vec<(TensorTerm, Tensor, Tensor)> = Vec::new(); // (pivot, reduced column, source combination)
1151    for b in tensor_basis(res, n, t) {
1152        let col = tensor_boundary(res, &std::iter::once(b.clone()).collect());
1153        let src: Tensor = std::iter::once(b).collect();
1154        let (rc, sm) = reduce(col, src, &elim);
1155        if let Some(piv) = rc.iter().max().cloned() {
1156            elim.push((piv, rc, sm));
1157        }
1158    }
1159    let (rc, sm) = reduce(target.clone(), Tensor::new(), &elim);
1160    rc.is_empty().then_some(sm)
1161}
1162
1163/// The chain-level **diagonal** `Δ₀: C → C ⊗ C` — an 𝒜-linear chain map with `Δ₀(g₀) = g₀ ⊗ g₀`, built by
1164/// lifting up the resolution (`∂_{C⊗C} Δ₀(g) = Δ₀(∂g)`, solved leg-aware). It is the topological diagonal
1165/// `X → X×X` transported onto the algebraic resolution; its cup-`i` refinements carry the Steenrod
1166/// operations on Ext. `delta[n][&k] = Δ₀(g_{n,k})`.
1167fn diagonal(res: &[Vec<ResGen>], max_n: usize) -> Vec<std::collections::HashMap<usize, Tensor>> {
1168    let mut delta: Vec<std::collections::HashMap<usize, Tensor>> = Vec::new();
1169    let mut d0 = std::collections::HashMap::new();
1170    d0.insert(0usize, std::iter::once((0, 0, vec![], 0, 0, vec![])).collect::<Tensor>());
1171    delta.push(d0);
1172    for n in 1..=max_n {
1173        let mut dn: std::collections::HashMap<usize, Tensor> = std::collections::HashMap::new();
1174        for k in 0..res[n].len() {
1175            let mut rhs = Tensor::new(); // Δ₀(∂ g_{n,k}) = Σ_{(j,mb)∈∂} mb·Δ₀(g_{n-1,j})
1176            for (j, mb) in &res[n][k].boundary {
1177                tensor_add(&mut rhs, act_tensor(mb, &delta[n - 1][j]));
1178            }
1179            let x = solve_tensor_boundary(res, n, &rhs).expect("diagonal lifts: ∂x = Δ₀(∂g) is solvable");
1180            dn.insert(k, x);
1181        }
1182        delta.push(dn);
1183    }
1184    delta
1185}
1186
1187/// The leg-swap `T` on `C ⊗ C`: `(a⊗b) ↦ (b⊗a)`.
1188fn tensor_swap(t: &Tensor) -> Tensor {
1189    t.iter().map(|(p, i, m1, q, j, m2)| (*q, *j, m2.clone(), *p, *i, m1.clone())).collect()
1190}
1191
1192/// The next **cup-`i` coherence homotopy** from the previous one: `Δ_i: C_n → (C⊗C)_{n+i}` satisfying
1193/// `∂Δ_i + Δ_i∂ = Δ_{i-1} + TΔ_{i-1}`, with `Δ_i(g₀) = 0`. `shift = i` is the homological shift `Δ_i`
1194/// carries. This single recursion is the whole `E_∞`/`H_∞` ladder: `Δ₀` (the diagonal, `shift 0`) gives the
1195/// cup product, `Δ₁` (`shift 1`) the `Sq⁰` doubling, `Δ₂` (`shift 2`) the doubling of Ext² classes, and so
1196/// on up — each measuring the failure of the previous to be symmetric.
1197fn cup_homotopy(
1198    res: &[Vec<ResGen>],
1199    max_n: usize,
1200    shift: usize,
1201    prev: &[std::collections::HashMap<usize, Tensor>],
1202) -> Vec<std::collections::HashMap<usize, Tensor>> {
1203    let mut d: Vec<std::collections::HashMap<usize, Tensor>> = Vec::new();
1204    d.push(std::iter::once((0usize, Tensor::new())).collect()); // Δ_i(g₀) = 0
1205    for n in 1..=max_n {
1206        let mut dn: std::collections::HashMap<usize, Tensor> = std::collections::HashMap::new();
1207        for k in 0..res[n].len() {
1208            let mut rhs = prev[n][&k].clone(); // Δ_{i-1}(g) + TΔ_{i-1}(g) + Δ_i(∂g)
1209            tensor_add(&mut rhs, tensor_swap(&prev[n][&k]));
1210            for (j, mb) in &res[n][k].boundary {
1211                tensor_add(&mut rhs, act_tensor(mb, &d[n - 1][j]));
1212            }
1213            let x = solve_tensor_boundary(res, n + shift, &rhs).expect("cup-i homotopy lifts: ∂x = Δ+TΔ+Δ∂");
1214            dn.insert(k, x);
1215        }
1216        d.push(dn);
1217    }
1218    d
1219}
1220
1221/// The cup-1 homotopy `Δ₁` — the first coherence homotopy of cocommutativity. Pairing a class with itself
1222/// through `Δ₁` is the `Sq⁰` doubling operation on Ext.
1223fn diagonal_1(
1224    res: &[Vec<ResGen>],
1225    max_n: usize,
1226    delta: &[std::collections::HashMap<usize, Tensor>],
1227) -> Vec<std::collections::HashMap<usize, Tensor>> {
1228    cup_homotopy(res, max_n, 1, delta)
1229}
1230
1231/// The algebraic **`Sq⁰` (doubling) operation** on the one-line classes of Ext, computed from the cup-1
1232/// homotopy: `Sq⁰(h_i) = h_i ⌣_1 h_i = (dual ⊗ dual) ∘ Δ₁` on `C_1`. Returns its `Z/2` support among the
1233/// degree-`2^{i+1}` generators of `C_1`. The Adams doubling `Sq⁰(h_i) = h_{i+1}` is then a derived fact.
1234fn sq0_on_h(res: &[Vec<ResGen>], delta1: &[std::collections::HashMap<usize, Tensor>], gi: usize) -> Vec<usize> {
1235    let want = 2 * res[1][gi].degree;
1236    (0..res[1].len())
1237        .filter(|&k| res[1][k].degree == want && delta1[1][&k].contains(&(1, gi, vec![], 1, gi, vec![])))
1238        .collect()
1239}
1240
1241/// `Sq⁰` on a single Ext² generator class `gx` (e.g. `h_i²`), via the cup-2 homotopy: `Sq⁰(x) = x ⌣_2 x =
1242/// (dual ⊗ dual) ∘ Δ₂` on `C_2`, read as the `((2,gx,1),(2,gx,1))` coefficient over the degree-`2t`
1243/// generators of `C_2`. Used to show the doubling is a ring endomorphism (`Sq⁰(h_i²) = h_{i+1}²`).
1244fn sq0_on_ext2(res: &[Vec<ResGen>], delta2: &[std::collections::HashMap<usize, Tensor>], gx: usize) -> Vec<usize> {
1245    let want = 2 * res[2][gx].degree;
1246    (0..res[2].len())
1247        .filter(|&k| res[2][k].degree == want && delta2[2][&k].contains(&(2, gx, vec![], 2, gx, vec![])))
1248        .collect()
1249}
1250
1251/// The whole tower of cup-`i` diagonals `[Δ₀, Δ₁, …, Δ_max_shift]` built off one resolution, each lifted up
1252/// to homological degree `max_n`. `Δ₀` is the diagonal; `Δ_i = cup_homotopy(Δ_{i-1})`.
1253fn cup_diagonals(
1254    res: &[Vec<ResGen>],
1255    max_shift: usize,
1256    max_n: usize,
1257) -> Vec<Vec<std::collections::HashMap<usize, Tensor>>> {
1258    let mut out = vec![diagonal(res, max_n)];
1259    for i in 1..=max_shift {
1260        let prev = out[i - 1].clone();
1261        out.push(cup_homotopy(res, max_n, i, &prev));
1262    }
1263    out
1264}
1265
1266/// The algebraic **`Sq⁰` doubling** applied to *any* Ext class — a single generator `gx ∈ Ext^{s,t}` — via
1267/// the cup-`s` homotopy: `Sq⁰(x) = x ⌣_s x = (dual ⊗ dual) ∘ Δ_s` on `C_s`, read as the
1268/// `((s,gx,1),(s,gx,1))` coefficient over the degree-`2t` generators of `C_s`. One uniform operator across
1269/// the whole chart; `deltas[i]` must be `Δ_i`. This is the automation of the doubling symmetry: feed it any
1270/// class, harvest `Sq⁰` of it.
1271fn sq0(res: &[Vec<ResGen>], deltas: &[Vec<std::collections::HashMap<usize, Tensor>>], s: usize, gx: usize) -> Vec<usize> {
1272    let want = 2 * res[s][gx].degree;
1273    let ds = &deltas[s]; // Δ_s
1274    (0..res[s].len())
1275        .filter(|&k| res[s][k].degree == want && ds[s][&k].contains(&(s, gx, vec![], s, gx, vec![])))
1276        .collect()
1277}
1278
1279/// One machine-derived fact about the Adams `E₂` chart of the sphere. Every variant is *computed* from the
1280/// minimal resolution and its chain-level diagonal — none is asserted.
1281#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
1282pub enum AdamsFact {
1283    /// `dim Ext^{s,t}_𝒜(Z/2, Z/2) = dim` — a point of the chart.
1284    Dimension { s: usize, t: usize, dim: usize },
1285    /// `lhs · rhs = result` in the cohomology ring of the Steenrod algebra (a named product).
1286    Product { lhs: String, rhs: String, result: String },
1287    /// `lhs · rhs = 0` — an Adem-adjacency / ring vanishing.
1288    Vanishing { lhs: String, rhs: String },
1289    /// `a = b`: two product-paths landed on the same generator. The ring relation, discovered by collision.
1290    Relation { a: String, b: String },
1291    /// `Sq⁰(from) = to` — the algebraic doubling operation on the chart.
1292    Doubling { from: String, to: String },
1293    /// `π_n^s ⊗ Z₍₂₎ = ⊕ Z/2^Lᵢ` — the exact 2-local stable stem, as the `h₀`-tower-length multiset. When
1294    /// `truncated`, an `h₀`-tower runs into the resolution's filtration ceiling, so the group is only a lower
1295    /// bound (e.g. `π_0 = Z₍₂₎`, an infinite tower the finite resolution cannot close).
1296    StableGroup { stem: usize, tower_lengths: Vec<usize>, truncated: bool },
1297    /// `Ext^{s,t}` carries a class that is **not a product of the indecomposables `h_i`** — the product-sweep
1298    /// cannot name it. It is the shadow of an Adams differential / a Massey product: the first place the
1299    /// primary (auto-able) structure runs out, and the secondary structure begins.
1300    Secondary { s: usize, t: usize },
1301}
1302
1303impl std::fmt::Display for AdamsFact {
1304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1305        match self {
1306            AdamsFact::Dimension { s, t, dim } => write!(f, "dim Ext^{{{s},{t}}} = {dim}"),
1307            AdamsFact::Product { lhs, rhs, result } => write!(f, "{lhs} · {rhs} = {result}"),
1308            AdamsFact::Vanishing { lhs, rhs } => write!(f, "{lhs} · {rhs} = 0"),
1309            AdamsFact::Relation { a, b } => write!(f, "{a} = {b}"),
1310            AdamsFact::Doubling { from, to } => write!(f, "Sq⁰({from}) = {to}"),
1311            AdamsFact::StableGroup { stem, tower_lengths, truncated } => {
1312                let group = if tower_lengths.is_empty() {
1313                    "0".to_string()
1314                } else {
1315                    tower_lengths.iter().map(|l| format!("Z/2^{l}")).collect::<Vec<_>>().join(" ⊕ ")
1316                };
1317                if *truncated {
1318                    write!(f, "π_{stem}^s ⊗ Z₍₂₎ ⊇ {group} (h₀-tower hits the resolution ceiling — infinite/truncated)")
1319                } else {
1320                    write!(f, "π_{stem}^s ⊗ Z₍₂₎ = {group}")
1321                }
1322            }
1323            AdamsFact::Secondary { s, t } => {
1324                write!(f, "Ext^{{{s},{t}}} has a non-product (secondary/Massey) class — un-auto-able by the product sweep")
1325            }
1326        }
1327    }
1328}
1329
1330/// Name every Ext class reachable as a **product of the indecomposables `h_i`**, by multiplying named
1331/// classes on the right by the `h_i` to a fixpoint. Returns the name map keyed by `(s, generator index)`
1332/// together with the product / vanishing / relation facts discovered along the way (a relation surfaces
1333/// whenever two product-paths collide on one generator). Pure ring structure — no chain-level diagonal
1334/// needed, so it is cheap and runs to high internal degree.
1335fn name_by_products(
1336    res: &[Vec<ResGen>],
1337    max_s: usize,
1338    max_t: usize,
1339) -> (std::collections::HashMap<(usize, usize), String>, Vec<AdamsFact>, Vec<(usize, usize)>) {
1340    let mut names: std::collections::HashMap<(usize, usize), String> = std::collections::HashMap::new();
1341    let mut facts: Vec<AdamsFact> = Vec::new();
1342    let mut secondary: Vec<(usize, usize)> = Vec::new();
1343    let mut worklist: Vec<(usize, usize)> = Vec::new();
1344    for gi in 0..res[1].len() {
1345        let i = res[1][gi].degree.trailing_zeros();
1346        names.insert((1, gi), format!("h{i}"));
1347        worklist.push((1, gi));
1348    }
1349    worklist.sort_by_key(|&(_, gi)| res[1][gi].degree);
1350    let h_gens: Vec<usize> = (0..res[1].len()).collect();
1351
1352    // Drain the worklist (multiply each named class by the indecomposables); when it empties, name the
1353    // lowest un-named generator as a fresh **secondary generator** `c_k` and keep sweeping. This is how the
1354    // engine "breaks to the next group": it does not prove what `c_k` is, it recognises a detected
1355    // indecomposable and lets the verified product symmetry carry on from it. A product landing on another
1356    // detected secondary class is self-consistent validation — no Massey computation required.
1357    let mut qi = 0;
1358    let mut sec = 0;
1359    loop {
1360        while qi < worklist.len() {
1361            let (s, gi) = worklist[qi];
1362            qi += 1;
1363            let lname = names[&(s, gi)].clone();
1364            for &hgi in &h_gens {
1365                if s + 1 > max_s || res[s][gi].degree + res[1][hgi].degree > max_t {
1366                    continue;
1367                }
1368                let hname = names[&(1, hgi)].clone();
1369                let prod = yoneda_product(res, s, gi, 1, hgi);
1370                if prod.is_empty() {
1371                    facts.push(AdamsFact::Vanishing { lhs: lname.clone(), rhs: hname });
1372                } else if prod.len() == 1 {
1373                    let key = (s + 1, prod[0]);
1374                    let newname = format!("{lname}{hname}");
1375                    match names.get(&key) {
1376                        Some(existing) if *existing != newname => {
1377                            let (a, b) = if newname < *existing { (newname, existing.clone()) } else { (existing.clone(), newname) };
1378                            facts.push(AdamsFact::Relation { a, b });
1379                        }
1380                        Some(_) => {}
1381                        None => {
1382                            names.insert(key, newname.clone());
1383                            worklist.push(key);
1384                            facts.push(AdamsFact::Product { lhs: lname.clone(), rhs: hname, result: newname });
1385                        }
1386                    }
1387                }
1388            }
1389        }
1390        let next = (1..=max_s)
1391            .flat_map(|s| (0..res[s].len()).map(move |gi| (s, gi)))
1392            .filter(|&(s, gi)| res[s][gi].degree <= max_t && !names.contains_key(&(s, gi)))
1393            .min_by_key(|&(s, gi)| (res[s][gi].degree, s));
1394        match next {
1395            Some((s, gi)) => {
1396                names.insert((s, gi), format!("c{sec}"));
1397                sec += 1;
1398                secondary.push((s, gi));
1399                worklist.push((s, gi));
1400            }
1401            None => break,
1402        }
1403    }
1404    (names, facts, secondary)
1405}
1406
1407/// The **un-auto-able classes**: every `(s, t)` in range carrying an Ext generator that the product-sweep
1408/// cannot name — i.e. not a product of the indecomposables. These are exactly where the secondary (Massey)
1409/// structure / the Adams differentials live. Cheap (product structure only); the first one is `c₀` at
1410/// `Ext^{3,11}`, stem 8.
1411fn secondary_classes(max_s: usize, max_t: usize) -> Vec<(usize, usize)> {
1412    let res = minimal_resolution(max_s, max_t);
1413    let (_names, _facts, secondary) = name_by_products(&res, max_s, max_t);
1414    let mut out: Vec<(usize, usize)> = secondary.iter().map(|&(s, gi)| (s, res[s][gi].degree)).collect();
1415    out.sort();
1416    out.dedup();
1417    out
1418}
1419
1420/// Auto-collect the **primary chart** from a resolution by pure symmetry-breaking — no proving, no
1421/// chain-level diagonal, so it is cheap and runs to a wide range. Gathers chart dimensions, ring products
1422/// and the relations that surface by collision, the un-auto-able (secondary/Massey) generators the product
1423/// sweep cannot name, and the exact 2-local stable stems from the `h₀`-towers. The sorted/deduped catalog.
1424pub fn harvest_secondary_facts(max_s: usize, max_t: usize) -> Vec<AdamsFact> {
1425    let res = minimal_resolution(max_s, max_t);
1426    let (_names, ring_facts, secondary) = name_by_products(&res, max_s, max_t);
1427    let mut facts = ring_facts;
1428    for s in 0..=max_s {
1429        for t in 0..=max_t {
1430            let dim = res[s].iter().filter(|g| g.degree == t).count();
1431            if dim > 0 {
1432                facts.push(AdamsFact::Dimension { s, t, dim });
1433            }
1434        }
1435    }
1436    for &(s, gi) in &secondary {
1437        facts.push(AdamsFact::Secondary { s, t: res[s][gi].degree });
1438    }
1439    for stem in 0..=max_t.saturating_sub(max_s) {
1440        let lengths = stem_2local_tower_lengths(&res, stem);
1441        if !lengths.is_empty() {
1442            let truncated = res[max_s].iter().any(|g| g.degree == stem + max_s);
1443            facts.push(AdamsFact::StableGroup { stem, tower_lengths: lengths, truncated });
1444        }
1445    }
1446    facts.sort();
1447    facts.dedup();
1448    facts
1449}
1450
1451/// The **self-igniting fact engine**: from a single minimal resolution it auto-collects everything the
1452/// symmetry breaking nets — the chart dimensions, the ring products (multiplying named classes by the
1453/// indecomposables to a fixpoint, so ring *relations* surface automatically when two product-paths collide
1454/// on one generator), the `Sq⁰` doubling across the chart, and the exact 2-local stable stems from the
1455/// `h₀`-towers. Returns the deduplicated, sorted catalog. Bounded by `(max_s, max_t)`; the doubling reaches
1456/// through filtration 2 (cup-homotopies `Δ₀..Δ₂`).
1457pub fn harvest_adams_facts(max_s: usize, max_t: usize) -> Vec<AdamsFact> {
1458    let res = minimal_resolution(max_s, max_t);
1459    let dbl_shift = max_s.min(2);
1460    let deltas = cup_diagonals(&res, dbl_shift, dbl_shift);
1461    let mut facts: Vec<AdamsFact> = Vec::new();
1462
1463    // 1. Chart dimensions, straight off the resolution.
1464    for s in 0..=max_s {
1465        for t in 0..=max_t {
1466            let dim = res[s].iter().filter(|g| g.degree == t).count();
1467            if dim > 0 {
1468                facts.push(AdamsFact::Dimension { s, t, dim });
1469            }
1470        }
1471    }
1472
1473    // 2. Name every class reachable as a product of the indecomposables (fixpoint sweep); relations surface
1474    //    by collision. Then flag every Ext generator the sweep CANNOT name — the secondary/Massey classes.
1475    let (names, ring_facts, secondary) = name_by_products(&res, max_s, max_t);
1476    facts.extend(ring_facts);
1477    for &(s, gi) in &secondary {
1478        facts.push(AdamsFact::Secondary { s, t: res[s][gi].degree });
1479    }
1480
1481    // 3. The Sq⁰ doubling on every named class (through filtration `dbl_shift`).
1482    let mut named: Vec<((usize, usize), String)> = names.iter().map(|(k, v)| (*k, v.clone())).collect();
1483    named.sort();
1484    for ((s, gi), name) in &named {
1485        if *s == 0 || *s > dbl_shift || 2 * res[*s][*gi].degree > max_t {
1486            continue;
1487        }
1488        let img = sq0(&res, &deltas, *s, *gi);
1489        if img.len() == 1 {
1490            let to = names.get(&(*s, img[0])).cloned().unwrap_or_else(|| format!("Sq⁰({name})"));
1491            facts.push(AdamsFact::Doubling { from: name.clone(), to });
1492        }
1493    }
1494
1495    // 4. The exact 2-local stable stems from the h₀-tower filtration. A stem whose chart reaches the
1496    //    resolution's top filtration is flagged truncated — its h₀-tower may continue past what we computed
1497    //    (this is exactly π_0 = Z₍₂₎, an infinite tower the finite resolution cannot close honestly).
1498    for stem in 0..=max_t.saturating_sub(max_s) {
1499        let lengths = stem_2local_tower_lengths(&res, stem);
1500        if !lengths.is_empty() {
1501            let truncated = res[max_s].iter().any(|g| g.degree == stem + max_s);
1502            facts.push(AdamsFact::StableGroup { stem, tower_lengths: lengths, truncated });
1503        }
1504    }
1505
1506    facts.sort();
1507    facts.dedup();
1508    facts
1509}
1510
1511#[cfg(test)]
1512mod tests {
1513    use super::*;
1514
1515    #[test]
1516    fn adem_reduces_known_relations_to_the_admissible_basis() {
1517        // The Adem reduction is the canonical form in the Steenrod algebra. Sq¹Sq¹ → 0; Sq¹Sq² → Sq³;
1518        // Sq²Sq² → Sq³Sq¹ (admissible: 3 ≥ 2·1). The defining relations, recovered as rewrites.
1519        assert!(adem_reduce(&[1, 1]).is_empty(), "Sq¹Sq¹ = 0");
1520        assert_eq!(adem_reduce(&[1, 2]), [vec![3]].into_iter().collect(), "Sq¹Sq² = Sq³");
1521        assert_eq!(adem_reduce(&[2, 2]), [vec![3, 1]].into_iter().collect(), "Sq²Sq² = Sq³Sq¹");
1522        for m in [vec![3, 1], vec![2, 1], vec![5, 2, 1]] {
1523            for adm in &adem_reduce(&m) {
1524                assert!(is_admissible(adm), "reduction output is admissible");
1525            }
1526        }
1527    }
1528
1529    #[test]
1530    fn two_primary_group_structure_from_the_h0_tower_filtration() {
1531        // LEAN ON THE h₀ SYMMETRY (read from the resolution boundaries, unused by the dim count): the
1532        // EXACT 2-local stable groups, distinguishing Z/8 from (Z/2)³. Tower-length multiset [L,…] ⇒ ⊕Z/2^L.
1533        let res = super::minimal_resolution(8, 18);
1534        let g = |n| super::stem_2local_tower_lengths(&res, n);
1535        assert_eq!(g(1), vec![1], "π₁ˢ = Z/2");
1536        assert_eq!(g(2), vec![1], "π₂ˢ = Z/2");
1537        assert_eq!(g(3), vec![3], "π₃ˢ: ONE h₀-tower height 3 = Z/8 (not (Z/2)³) ⇒ Z/24");
1538        assert_eq!(g(6), vec![1], "π₆ˢ = Z/2");
1539        assert_eq!(g(7), vec![4], "π₇ˢ = Z/16 ⇒ Z/240");
1540        assert_eq!(g(8), vec![1, 1], "π₈ˢ = (Z/2)² — two separate towers");
1541        assert_eq!(g(9), vec![1, 1, 1], "π₉ˢ = (Z/2)³");
1542        assert_eq!(g(11), vec![3], "π₁₁ˢ = Z/8 ⇒ Z/504");
1543    }
1544
1545    #[test]
1546    fn stable_homotopy_groups_in_stems_eight_through_thirteen() {
1547        // stems 8–13, 2-local orders (E₂ = E∞ here, no differentials yet):
1548        // π₈=(Z/2)²→2, π₉=(Z/2)³→3, π₁₀=Z/2→1, π₁₁=Z/8→3 (2-local of Z/504), π₁₂=0→0, π₁₃=0→0.
1549        let e = ext(7, 18);
1550        let st = |n: usize| -> usize { (0..=7).filter(|&s| n + s <= 18).map(|s| e[s][n + s]).sum() };
1551        assert_eq!(st(8), 2, "π₈ˢ = (Z/2)²");
1552        assert_eq!(st(9), 3, "π₉ˢ = (Z/2)³");
1553        assert_eq!(st(10), 1, "π₁₀ˢ = Z/2 (2-local of Z/6)");
1554        assert_eq!(st(11), 3, "π₁₁ˢ = Z/8 2-locally (Z/504)");
1555        assert_eq!(st(12), 0, "π₁₂ˢ = 0");
1556        assert_eq!(st(13), 0, "π₁₃ˢ = 0 (2-locally; Z/3)");
1557    }
1558
1559    #[test]
1560    fn infinite_h0_tower_and_indecomposable_line() {
1561        // BREAK FOREVER — the honest infinite part (LIFT AND SHIFT LEFT). Individual stems past ~13 need
1562        // Adams differentials no one has resolved past degree ~90 — that is open mathematics, not a compute
1563        // problem, and no engine "breaks past" it by running. But the chart has EXACT recurrences that hold
1564        // to INFINITY, and the engine confirms them at every level we reach:
1565        //   • Stem 0:  Ext^{s,s} = 1 for ALL s  (the h₀-tower)  ⇒  π₀ˢ = Z — an infinite tower.
1566        //   • Line 1:  Ext^{1,t} = 1 ⟺ t is a power of 2  (the h_i = Sq^{2ⁱ}) — for all t, forever.
1567        // These are the parts of the homotopy of spheres we genuinely break to infinity, by the recurrence.
1568        let e = ext(8, 8);
1569        for s in 0..=8 {
1570            assert_eq!(e[s][s], 1, "h₀ˢ ≠ 0 — the infinite stem-0 tower (π₀ˢ = Z) at s={s}");
1571        }
1572        for t in 1..=16 {
1573            assert_eq!(adams_one_line(t), usize::from(t.is_power_of_two()), "the h_i line, forever, at t={t}");
1574        }
1575    }
1576
1577    #[test]
1578    fn stable_homotopy_groups_through_the_seven_stem() {
1579        // WHAT FALLS OUT, PROVEN. Breaking the Steenrod algebra recursively (the minimal free resolution),
1580        // the STABLE HOMOTOPY GROUPS OF SPHERES fall out stem by stem. Stems 1–7 are each a single
1581        // h₀-tower, so the 2-local order is 2^(#classes in the stem) = 2^(Σ_s Ext^{s,n+s}). Verified
1582        // against the KNOWN groups: π₁..₇ˢ = Z/2, Z/2, Z/24, 0, 0, Z/2, Z/240 — i.e. 2-local orders
1583        // 2,2,8,1,1,2,16. From a SAT solver's symmetry breaking, recursively, to the homotopy of spheres.
1584        let e = ext(6, 12);
1585        let stem_total = |n: usize| -> usize { (0..=6).filter(|&s| n + s <= 12).map(|s| e[s][n + s]).sum() };
1586        assert_eq!(stem_total(1), 1, "π₁ˢ = Z/2");
1587        assert_eq!(stem_total(2), 1, "π₂ˢ = Z/2");
1588        assert_eq!(stem_total(3), 3, "π₃ˢ = Z/24 (2-local Z/8)");
1589        assert_eq!(stem_total(4), 0, "π₄ˢ = 0");
1590        assert_eq!(stem_total(5), 0, "π₅ˢ = 0");
1591        assert_eq!(stem_total(6), 1, "π₆ˢ = Z/2");
1592        assert_eq!(stem_total(7), 4, "π₇ˢ = Z/240 (2-local Z/16)");
1593    }
1594
1595    #[test]
1596    fn minimal_resolution_recovers_ext_through_the_three_stem() {
1597        // THE RECURSIVE SYMMETRY-BREAKING ENGINE. The minimal free resolution computes Ext^{s,t} for ALL
1598        // s at once — each homological degree s is ONE MORE cycle of symmetry breaking (break the kernel
1599        // into irreducible generators, recurse on the relations). We break 5 cycles in one call.
1600        let e = ext(5, 8);
1601        for t in 1..=8 {
1602            assert_eq!(e[1][t], adams_one_line(t), "Ext¹ from the engine matches the 1-line at t={t}");
1603        }
1604        for t in 2..=8 {
1605            assert_eq!(e[2][t], adams_two_line(t), "Ext² from the engine matches the 2-line at t={t}");
1606        }
1607        // The h₀-tower in stem 0 (Ext^{s,s} = h₀ˢ) = the 2-local integers, every cycle nonzero.
1608        for s in 0..=5 {
1609            assert_eq!(e[s][s], 1, "h₀ˢ tower at filtration {s} (stem 0 = Z₂)");
1610        }
1611        // π₃ˢ = Z/24: the h₀-tower over h₂ has height exactly 3, then dies — the famous answer.
1612        assert_eq!(e[1][4], 1, "h₂");
1613        assert_eq!(e[2][5], 1, "h₀h₂");
1614        assert_eq!(e[3][6], 1, "h₀²h₂ — tower height 3");
1615        assert_eq!(e[4][7], 0, "h₀³h₂ = 0 ⇒ tower ends ⇒ π₃ˢ = Z/24 (2-locally Z/8)");
1616    }
1617
1618    #[test]
1619    fn low_stem_homotopy_from_the_e2_lines() {
1620        // GET THE ANSWER: the first stable homotopy groups of SPHERES, read off the Adams E₂ chart we
1621        // built from the Steenrod algebra. Stem 1 (t−s=1): the only class is h₁ — Ext^{1,2}=1, while
1622        // Ext^{2,3}=h₀h₁=0 kills the h₀-tower above it — so π₁ˢ = Z/2, the Hopf map η. Stem 2: h₁²
1623        // (Ext^{2,4}=1), and the tower above dies (Ext^{3,5}=0), so π₂ˢ = Z/2 (η²). These are the
1624        // CORRECT stable stems (π₁ˢ = π₂ˢ = Z/2). Symmetry breaking, from SAT, computed homotopy of spheres.
1625        assert_eq!(adams_one_line(2), 1, "h₁ generates stem 1");
1626        assert_eq!(adams_two_line(3), 0, "h₀h₁ = 0 — the stem-1 h₀-tower is dead, so π₁ˢ = Z/2");
1627        assert_eq!(adams_two_line(4), 1, "h₁² generates stem 2 ⇒ π₂ˢ = Z/2");
1628    }
1629
1630    #[test]
1631    fn the_adams_e2_two_line_matches_the_h_i_h_j_chart() {
1632        // Ext^{2,t} via the MINIMAL FREE RESOLUTION of Z/2 over the Steenrod algebra — the second line of
1633        // the Adams E₂ page. Verified against the known chart: a basis {h_i h_j : i ≤ j, j ≠ i+1}, with
1634        // the adjacency relation h_i h_{i+1} = 0. So Ext²₃ = 0 (h₀h₁), Ext²₂ = 1 (h₀²), Ext²₄ = 1 (h₁²),
1635        // Ext²₅ = 1 (h₀h₂), Ext²₆ = 0 (h₁h₂), … We computed the relations among the Sq^{2ⁱ} from scratch.
1636        for t in 2..=16 {
1637            assert_eq!(adams_two_line(t), known_ext2(t), "dim Ext^{{2,t}} = #(h_i h_j) at t={t}");
1638        }
1639    }
1640
1641    #[test]
1642    fn the_adams_e2_one_line_is_exactly_the_h_i_indecomposables() {
1643        // INTO THE ADAMS SPECTRAL SEQUENCE. Ext^{1,t}_𝒜(Z/2,Z/2) — the first line of the Adams E₂ page —
1644        // is the indecomposables of the Steenrod algebra in degree t. Computed as dim 𝒜_t − dim(decomposables_t),
1645        // it is 1 exactly when t is a power of 2 (the classes h₀, h₁, h₂, … = Sq^{2ⁱ}) and 0 otherwise.
1646        // h_i sits in stem 2ⁱ−1; h₀,h₁,h₂,h₃ (stems 0,1,3,7) are the Hopf-invariant-one elements. This is
1647        // the bottom edge of the chart whose far reaches are the unsolved stable stems.
1648        for t in 1..=16 {
1649            assert_eq!(
1650                adams_one_line(t),
1651                usize::from(t.is_power_of_two()),
1652                "dim Ext^{{1,t}} = 1 iff t is a power of 2 (the h_i) at t={t}"
1653            );
1654        }
1655    }
1656
1657    #[test]
1658    fn sq_n_is_decomposable_iff_n_is_not_a_power_of_two_hopf_invariant_one() {
1659        // THE CRACK AT THE FRONTIER. Sqⁿ is DECOMPOSABLE (a sum of products of lower squares) iff n is
1660        // NOT a power of 2 — so the indecomposables of the Steenrod algebra are exactly the Sq^{2ⁱ}. This
1661        // is the algebraic heart of the HOPF INVARIANT ONE theorem and the reason the real/complex/
1662        // quaternion/octonion division algebras exist ONLY in dimensions 1, 2, 4, 8. Computed purely from
1663        // the Adem relations — symmetry breaking of cohomology operations reaching a landmark of topology.
1664        for n in 2..=17 {
1665            assert_eq!(
1666                is_decomposable_sq(n),
1667                !n.is_power_of_two(),
1668                "Sqⁿ decomposable ⟺ n not a power of 2 (indecomposables = Sq^{{2ⁱ}}) at n={n}"
1669            );
1670        }
1671    }
1672
1673    #[test]
1674    fn adem_reduction_preserves_the_action_on_bz2() {
1675        // THE CRACK'S CORRECTNESS GATE: rewriting a monomial to admissibles via Adem must not change the
1676        // operation it represents. For every monomial, its action on H*(BZ/2) equals the (XOR) action of
1677        // its admissible reduction — verified across Z/2[x]. The algebra computation and the geometry agree.
1678        let monomials = [vec![1, 1], vec![1, 2], vec![2, 2], vec![3, 2], vec![2, 3], vec![1, 2, 1], vec![4, 2]];
1679        for m in monomials {
1680            let reduced = adem_reduce(&m);
1681            for k in 0..=12 {
1682                let xk = monomial(k);
1683                let direct = act_monomial(&m, &xk);
1684                let mut via_basis = vec![0u8];
1685                for adm in &reduced {
1686                    via_basis = {
1687                        let a = act_monomial(adm, &xk);
1688                        let n = via_basis.len().max(a.len());
1689                        (0..n).map(|i| via_basis.get(i).copied().unwrap_or(0) ^ a.get(i).copied().unwrap_or(0)).collect()
1690                    };
1691                }
1692                assert_eq!(direct, trim(via_basis), "monomial {m:?} acts as its admissible reduction at k={k}");
1693            }
1694        }
1695    }
1696
1697    #[test]
1698    fn adem_relations_hold_on_bz2() {
1699        // ADEM RELATIONS — the defining relations of the Steenrod algebra, verified on H*(BZ/2) = Z/2[x]
1700        // by COMPOSING the operations. Sq¹Sq¹ = 0 was the first; here Sq¹Sq² = Sq³ and Sq²Sq² = Sq³Sq¹.
1701        // The relations are the structural skeleton of the whole algebra.
1702        for k in 0..=12 {
1703            let xk = monomial(k);
1704            assert_eq!(apply_sq(1, &apply_sq(1, &xk)), vec![0u8], "Adem Sq¹Sq¹ = 0 at k={k}");
1705            assert_eq!(apply_sq(1, &apply_sq(2, &xk)), apply_sq(3, &xk), "Adem Sq¹Sq² = Sq³ at k={k}");
1706            assert_eq!(
1707                apply_sq(2, &apply_sq(2, &xk)),
1708                apply_sq(3, &apply_sq(1, &xk)),
1709                "Adem Sq²Sq² = Sq³Sq¹ at k={k}"
1710            );
1711        }
1712    }
1713
1714    #[test]
1715    fn the_steenrod_action_on_bz2_is_binomial_coefficients_mod_2() {
1716        // THE DEEPEST TRUTH: the Steenrod squares on H*(BZ/2; Z/2) = Z/2[x] — determined entirely by the
1717        // Cartan formula and Sq(x) = x + x² — are EXACTLY the binomial coefficients mod 2:
1718        // Sqⁱ(xᵏ) = C(k,i)·x^{k+i}. Cohomology operations are governed by a number-theoretic law.
1719        for k in 0..=10 {
1720            for i in 0..=k {
1721                assert_eq!(sq(i, k), (binom(k, i) % 2) as u8, "Sqⁱ(xᵏ) = C(k,i) mod 2");
1722            }
1723        }
1724    }
1725
1726    #[test]
1727    fn binomial_mod_2_is_lucas_theorem_bitwise() {
1728        // …and C(k,i) mod 2 = Lucas' theorem: nonzero iff i's binary 1-bits are a subset of k's
1729        // (`i & k == i`). The Steenrod action is the bit-containment pattern of the exponents.
1730        for k in 0..=15usize {
1731            for i in 0..=k {
1732                let lucas = u8::from((i & k) == i);
1733                assert_eq!((binom(k, i) % 2) as u8, lucas, "C(k,i) mod 2 = [i AND k == i]");
1734            }
1735        }
1736    }
1737
1738    #[test]
1739    fn steenrod_axioms_identity_top_square_and_instability() {
1740        // The defining axioms, recovered: Sq⁰ = identity; Sqᵏ(xᵏ) = x^{2k} (the TOP square = the
1741        // cup-square from postnikov); Sqⁱ = 0 for i > k (instability — no operation raises degree by more
1742        // than the class's own degree).
1743        for k in 1..=8 {
1744            assert_eq!(sq(0, k), 1, "Sq⁰(xᵏ) = xᵏ");
1745            assert_eq!(sq(k, k), 1, "Sqᵏ(xᵏ) = x^2k — the top square is the cup-square");
1746            assert_eq!(sq(k + 1, k), 0, "Sqⁱ(xᵏ) = 0 for i > k (instability)");
1747        }
1748    }
1749
1750    #[test]
1751    fn the_total_square_obeys_the_cartan_formula_ring_homomorphism() {
1752        // CARTAN: Sq(a·b) = Sq(a)·Sq(b) — the total square is a RING HOMOMORPHISM, the structural heart of
1753        // the Steenrod algebra. Verified on every product of monomials up to degree 5.
1754        for i in 0..=5 {
1755            for j in 0..=5 {
1756                let lhs = total_square(&monomial(i + j));
1757                let rhs = trim(mul(&total_square(&monomial(i)), &total_square(&monomial(j))));
1758                assert_eq!(lhs, rhs, "Cartan: Sq(xⁱ⁺ʲ) = Sq(xⁱ)·Sq(xʲ)");
1759            }
1760        }
1761    }
1762}
1763
1764#[cfg(test)]
1765mod adams_differential_onset {
1766    use super::*;
1767
1768    /// The minimal resolution computes the Adams E₂ page, Ext_𝒜(Z/2, Z/2). For the sphere this collapses
1769    /// (E₂ = E∞) through the 13-stem, so the E₂ ranks equal the 2-primary stable homotopy there. The first
1770    /// nontrivial differential is d₂(h₄) = h₀h₃², landing in the 14-stem, so for n ≥ 14 the E₂ rank in a
1771    /// stem strictly exceeds rank E∞: Σ_s dim Ext^{s,14+s} = 5, whereas π₁₄ˢ ⊗ Z₍₂₎ = (Z/2)² has order 4
1772    /// (E∞ contribution 2). The minimal resolution recovers E₂ but not the differentials; those require the
1773    /// algebraic Steenrod operations on Ext (chain-level diagonal), the May spectral sequence, or motivic
1774    /// input, and remain open beyond the ~90-stem.
1775    #[test]
1776    fn e2_strictly_exceeds_einfinity_at_the_fourteen_stem() {
1777        let e = ext(8, 24);
1778        let st = |n: usize| -> usize { (0..=8).filter(|&s| n + s <= 24).map(|s| e[s][n + s]).sum() };
1779        assert_eq!(st(13), 0, "13-stem: E₂ = E∞, both trivial 2-primarily");
1780        assert_eq!(st(14), 5, "Σ_s dim Ext^{{s,14+s}} = 5 on the E₂ page");
1781        assert_ne!(st(14), 2, "rank E₂ > rank E∞ at the 14-stem: d₂(h₄) = h₀h₃² acts here");
1782    }
1783}
1784
1785#[cfg(test)]
1786mod ext_ring_from_the_resolution {
1787    use super::*;
1788
1789    /// The multiplicative structure of the Adams E₂ page — the cohomology ring of the Steenrod algebra —
1790    /// DERIVED from the minimal resolution, not asserted. The product is the Yoneda composition: lift the
1791    /// cocycle dual to a generator to a comparison chain map `φ_•` of the resolution (the diagonal
1792    /// `X → X×X` dualized) and pair. The structural relations of the one-line classes `h_i ∈ Ext^{1,2ⁱ}`
1793    /// then fall out of the algebra:
1794    ///
1795    ///   * each cup-square `h_i²` is nonzero (the indecomposables square nontrivially),
1796    ///   * the non-adjacent products `h_0h_2`, `h_0h_3`, `h_1h_3` survive,
1797    ///   * the Adem-adjacency relations `h_i h_{i+1} = 0` hold,
1798    ///   * the product is commutative (the Steenrod algebra is cocommutative),
1799    ///   * and the triple relation `h_1³ = h_0² h_2` — the first genuinely non-dimensional fact about the
1800    ///     ring, the generator of Ext^{3,6} — is recovered as an equality of computed products.
1801    #[test]
1802    fn the_h_i_products_obey_the_known_ext_ring_relations() {
1803        let res = minimal_resolution(4, 18);
1804        let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1805        let prod = |a_s, a, b_s, b| yoneda_product(&res, a_s, a, b_s, b);
1806
1807        for i in 0..=3 {
1808            assert!(!prod(1, h(i), 1, h(i)).is_empty(), "cup-square h_{i}² is nonzero");
1809        }
1810        assert_eq!(prod(1, h(0), 1, h(0)).len(), 1, "h_0² spans the 1-dim Ext^{{2,2}}");
1811
1812        assert!(!prod(1, h(0), 1, h(2)).is_empty(), "h_0 h_2 ≠ 0");
1813        assert!(!prod(1, h(0), 1, h(3)).is_empty(), "h_0 h_3 ≠ 0");
1814        assert!(!prod(1, h(1), 1, h(3)).is_empty(), "h_1 h_3 ≠ 0");
1815
1816        assert!(prod(1, h(0), 1, h(1)).is_empty(), "h_0 h_1 = 0 (Adem adjacency)");
1817        assert!(prod(1, h(1), 1, h(2)).is_empty(), "h_1 h_2 = 0 (Adem adjacency)");
1818        assert!(prod(1, h(2), 1, h(3)).is_empty(), "h_2 h_3 = 0 (Adem adjacency)");
1819
1820        assert_eq!(prod(1, h(0), 1, h(2)), prod(1, h(2), 1, h(0)), "commutativity h_0 h_2 = h_2 h_0");
1821        assert_eq!(prod(1, h(1), 1, h(3)), prod(1, h(3), 1, h(1)), "commutativity h_1 h_3 = h_3 h_1");
1822
1823        // The triple relation h_1³ = h_0² h_2 in Ext^{3,6}. Each intermediate square is a single generator,
1824        // so the cube and h_0²·h_2 are well-defined classes; both must equal the lone Ext^{3,6} generator.
1825        let h1sq = prod(1, h(1), 1, h(1));
1826        let h0sq = prod(1, h(0), 1, h(0));
1827        assert_eq!(h1sq.len(), 1, "h_1² spans Ext^{{2,4}}");
1828        assert_eq!(h0sq.len(), 1, "h_0² spans Ext^{{2,2}}");
1829        let h1_cubed = yoneda_product(&res, 2, h1sq[0], 1, h(1));
1830        let h0sq_h2 = yoneda_product(&res, 2, h0sq[0], 1, h(2));
1831        assert!(!h1_cubed.is_empty(), "h_1³ ≠ 0");
1832        assert_eq!(h1_cubed, h0sq_h2, "h_1³ = h_0² h_2 — derived, not assumed");
1833    }
1834}
1835
1836#[cfg(test)]
1837mod steenrod_coproduct {
1838    use super::*;
1839    use std::collections::HashSet;
1840
1841    fn set(pairs: &[(&[usize], &[usize])]) -> HashSet<(Vec<usize>, Vec<usize>)> {
1842        pairs.iter().map(|(l, r)| (l.to_vec(), r.to_vec())).collect()
1843    }
1844
1845    /// The Cartan coproduct `ψ(Sqⁿ) = Σ_{a+b=n} Sqᵃ⊗Sqᵇ`, extended multiplicatively and reduced to the
1846    /// admissible basis — the comultiplication that makes `C⊗C` a Steenrod-module and drives the chain-level
1847    /// diagonal. Verified against hand computation (including the `Sq¹Sq¹ = 0` cancellations inside
1848    /// `ψ(Sq²Sq¹)`), against the COCOMMUTATIVITY of the mod-2 Steenrod algebra (`ψ` is swap-invariant), and
1849    /// against leg-wise degree preservation.
1850    #[test]
1851    fn the_cartan_coproduct_is_diagonal_cocommutative_and_adem_reduced() {
1852        assert_eq!(coproduct(&[1]), set(&[(&[], &[1]), (&[1], &[])]));
1853        assert_eq!(coproduct(&[2]), set(&[(&[2], &[]), (&[1], &[1]), (&[], &[2])]));
1854        assert_eq!(coproduct(&[3]), set(&[(&[3], &[]), (&[2], &[1]), (&[1], &[2]), (&[], &[3])]));
1855
1856        // ψ(Sq²Sq¹): multiplicativity forces Sq¹Sq¹ = 0 cancellations; exactly these survive.
1857        assert_eq!(
1858            coproduct(&[2, 1]),
1859            set(&[(&[2, 1], &[]), (&[2], &[1]), (&[1], &[2]), (&[], &[2, 1])])
1860        );
1861
1862        for m in [vec![1], vec![2], vec![3], vec![4], vec![2, 1], vec![4, 2, 1], vec![5], vec![6, 3]] {
1863            let psi = coproduct(&m);
1864            let swapped: HashSet<_> = psi.iter().map(|(l, r)| (r.clone(), l.clone())).collect();
1865            assert_eq!(psi, swapped, "ψ is cocommutative on {m:?}");
1866            let deg = |x: &[usize]| -> usize { x.iter().sum() };
1867            for (l, r) in &psi {
1868                assert_eq!(deg(l) + deg(r), deg(&m), "ψ preserves internal degree leg-wise on {m:?}");
1869            }
1870        }
1871    }
1872}
1873
1874#[cfg(test)]
1875mod chain_level_diagonal {
1876    use super::*;
1877
1878    /// The chain-level diagonal `Δ₀: C → C⊗C` is built independently of the Yoneda comparison map, yet the
1879    /// cup-square it computes — `(dual_x ⊗ dual_x)∘Δ₀`, reading the `((1,gi,1),(1,gi,1))` coefficient on the
1880    /// `C_2` generators — must agree with `yoneda_product(x, x)` for every one-line class `x = h_i`. Two
1881    /// independent constructions of the same cup-square agreeing is the correctness oracle that licenses the
1882    /// diagonal; the cup-`i` refinements built on it then carry the Steenrod operations on Ext.
1883    #[test]
1884    fn the_chain_level_diagonal_reproduces_the_yoneda_cup_squares() {
1885        let res = minimal_resolution(2, 8);
1886        let delta = diagonal(&res, 2);
1887        let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1888        for i in 0..=2 {
1889            let gi = h(i);
1890            let want = 2 * (1 << i);
1891            let mut via_diagonal: Vec<usize> = (0..res[2].len())
1892                .filter(|&k| {
1893                    res[2][k].degree == want && delta[2][&k].contains(&(1, gi, vec![], 1, gi, vec![]))
1894                })
1895                .collect();
1896            via_diagonal.sort_unstable();
1897            let mut via_yoneda = yoneda_product(&res, 1, gi, 1, gi);
1898            via_yoneda.sort_unstable();
1899            assert!(!via_diagonal.is_empty(), "h_{i}² ≠ 0");
1900            assert_eq!(via_diagonal, via_yoneda, "Δ₀ reproduces the Yoneda cup-square h_{i}²");
1901        }
1902    }
1903
1904    /// The first higher coherence — the cup-1 homotopy `Δ₁` — derives the Adams **`Sq⁰` doubling** on the
1905    /// one-line classes: `Sq⁰(h_i) = h_{i+1}`. This is the symmetry the whole differential family rides on
1906    /// (`d₂(h_{i+1}) = h₀h_i²`); here it is computed, not assumed — `Sq⁰` falls straight out of the
1907    /// secondary structure of the chain-level diagonal, exactly the geometric seam under the `h₀` seam.
1908    #[test]
1909    fn the_cup_1_homotopy_derives_the_sq0_doubling_on_the_h_line() {
1910        let res = minimal_resolution(2, 8);
1911        let delta = diagonal(&res, 1);
1912        let delta1 = diagonal_1(&res, 1, &delta);
1913        let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1914        for i in 0..=2 {
1915            assert_eq!(
1916                sq0_on_h(&res, &delta1, h(i)),
1917                vec![h(i + 1)],
1918                "Sq⁰(h_{i}) = h_(i+1) — the doubling, derived from the cup-1 homotopy"
1919            );
1920        }
1921    }
1922
1923    /// The `Sq⁰` doubling is a **ring endomorphism** of Ext — the algebraic Frobenius. Through the cup-2
1924    /// homotopy it acts on the second-line classes, and `Sq⁰(h_i²) = (Sq⁰ h_i)² = h_{i+1}²`: the doubling
1925    /// commutes with the cup product. The left side is computed from `Δ₂` (cup-2 self-pairing of the Ext²
1926    /// class `h_i²`); the right is the ordinary cup-square of `h_{i+1}`. Their agreement is the
1927    /// multiplicativity of the doubling, derived — the same symmetry as `Sq⁰(h_i)=h_{i+1}`, one filtration up.
1928    #[test]
1929    fn the_sq0_doubling_is_a_ring_endomorphism_on_the_squares() {
1930        let res = minimal_resolution(4, 8);
1931        let delta = diagonal(&res, 2);
1932        let delta1 = diagonal_1(&res, 2, &delta);
1933        let delta2 = cup_homotopy(&res, 2, 2, &delta1);
1934        let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1935        for i in 0..=1 {
1936            let hi_sq = yoneda_product(&res, 1, h(i), 1, h(i));
1937            assert_eq!(hi_sq.len(), 1, "h_i² is a single Ext² generator");
1938            let mut lhs = sq0_on_ext2(&res, &delta2, hi_sq[0]);
1939            lhs.sort_unstable();
1940            let mut rhs = yoneda_product(&res, 1, h(i + 1), 1, h(i + 1));
1941            rhs.sort_unstable();
1942            assert!(!rhs.is_empty(), "h_(i+1)² ≠ 0");
1943            assert_eq!(lhs, rhs, "Sq⁰(h_i²) = h_(i+1)² — the doubling is a ring endomorphism");
1944        }
1945    }
1946
1947    /// Automating the doubling: build the cup-diagonal tower once, then sweep `Sq⁰` across the low Adams
1948    /// chart and harvest everything it nets in range — the indecomposables `h_i ↦ h_{i+1}` and the products
1949    /// `h_i h_j ↦ h_{i+1} h_{j+1}`, each image cross-checked against the independently-computed ring product.
1950    /// The sweep reaches past the squares to the mixed product `h_0 h_2 ↦ h_1 h_3`. One uniform operator,
1951    /// the whole symmetry applied mechanically.
1952    #[test]
1953    fn the_sq0_doubling_automates_across_the_low_adams_chart() {
1954        let res = minimal_resolution(4, 10);
1955        let deltas = cup_diagonals(&res, 2, 2);
1956        let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1957        let mut harvested: Vec<String> = Vec::new();
1958
1959        for i in 0..=2 {
1960            assert_eq!(sq0(&res, &deltas, 1, h(i)), vec![h(i + 1)], "Sq⁰(h_i) = h_(i+1)");
1961            harvested.push(format!("Sq0(h{i})=h{}", i + 1));
1962        }
1963
1964        for i in 0..=2 {
1965            for j in i..=2 {
1966                let prod = yoneda_product(&res, 1, h(i), 1, h(j));
1967                if prod.len() != 1 || 2 * res[2][prod[0]].degree > 10 {
1968                    continue;
1969                }
1970                let mut got = sq0(&res, &deltas, 2, prod[0]);
1971                got.sort_unstable();
1972                let mut want = yoneda_product(&res, 1, h(i + 1), 1, h(j + 1));
1973                want.sort_unstable();
1974                assert_eq!(got, want, "Sq⁰(h_i h_j) = h_(i+1) h_(j+1) — harvested automatically");
1975                harvested.push(format!("Sq0(h{i}h{j})=h{}h{}", i + 1, j + 1));
1976            }
1977        }
1978
1979        assert!(harvested.iter().any(|s| s == "Sq0(h0h2)=h1h3"), "sweep reaches the mixed product h_0h_2 ↦ h_1h_3");
1980        assert!(harvested.len() >= 6, "doubling harvested ≥6 chart facts automatically: {harvested:?}");
1981    }
1982}
1983
1984#[cfg(test)]
1985mod adams_fact_engine {
1986    use super::*;
1987
1988    /// The self-igniting harvester: from one resolution it auto-collects the whole low Adams chart — every
1989    /// fact computed, none asserted. The landmark proofs that the ignition really fires:
1990    ///   * the ring relation `h_1³ = h_0²h_2` surfaces *by collision* — two product-paths (`h0·h0·h2` and
1991    ///     `h1·h1·h1`) reach the same `Ext^{3,6}` generator and the engine records their equality with no
1992    ///     prior knowledge that they coincide,
1993    ///   * the `Sq⁰` doubling `h0 ↦ h1` and the products `h0 · h2 = h0h2` are collected automatically,
1994    ///   * the exact 2-local stable stem `π_3 = Z/8` is read from the `h₀`-tower.
1995    /// The full catalog is banked to `logs/derived_facts/adams_chart.txt`.
1996    #[test]
1997    fn the_engine_self_ignites_and_auto_collects_the_adams_chart() {
1998        let facts = harvest_adams_facts(4, 10);
1999
2000        let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
2001        assert!(has("h0h0h2 = h1h1h1"), "the ring relation h_1³ = h_0²h_2 was auto-discovered by collision");
2002        assert!(has("Sq⁰(h0) = h1"), "the doubling h0 ↦ h1 was auto-collected");
2003        assert!(has("Sq⁰(h0h2) = h1h3"), "the mixed-product doubling was auto-collected");
2004        assert!(has("h0 · h2 = h0h2"), "the product h_0·h_2 was auto-collected");
2005        assert!(has("h0 · h1 = 0"), "the Adem-adjacency vanishing h_0·h_1 = 0 was auto-collected");
2006        assert!(has("π_3^s ⊗ Z₍₂₎ = Z/2^3"), "the exact 2-local stem π_3 = Z/8 was auto-collected");
2007        assert!(
2008            facts.iter().any(|f| matches!(f, AdamsFact::StableGroup { stem: 0, truncated: true, .. })),
2009            "π_0 = Z₍₂₎ is honestly flagged as a truncated/infinite h₀-tower, not a false finite group"
2010        );
2011
2012        let relations = facts.iter().filter(|f| matches!(f, AdamsFact::Relation { .. })).count();
2013        let doublings = facts.iter().filter(|f| matches!(f, AdamsFact::Doubling { .. })).count();
2014        assert!(relations >= 1 && doublings >= 5, "engine nets ≥1 relation and ≥5 doublings");
2015        assert!(facts.len() >= 30, "engine auto-collected ≥30 facts from one ignition: {}", facts.len());
2016
2017        // Bank the catalog to the repo (best-effort; the assertions above are the real verification).
2018        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
2019        if std::fs::create_dir_all(&dir).is_ok() {
2020            let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
2021            let _ = std::fs::write(dir.join("adams_chart.txt"), body);
2022        }
2023    }
2024
2025    /// Lock-in: the full `(4,10)` catalog is frozen exactly. Any change to the resolution, the ring product,
2026    /// the diagonal, the doubling, or the stable-group reader that perturbs a single derived fact fails here.
2027    /// This is the banked ground truth of the symmetry-breaking engine.
2028    #[test]
2029    fn the_harvested_catalog_is_frozen_as_ground_truth() {
2030        let got: Vec<String> = harvest_adams_facts(4, 10).iter().map(|f| f.to_string()).collect();
2031        let expected = "\
2032dim Ext^{0,0} = 1
2033dim Ext^{1,1} = 1
2034dim Ext^{1,2} = 1
2035dim Ext^{1,4} = 1
2036dim Ext^{1,8} = 1
2037dim Ext^{2,2} = 1
2038dim Ext^{2,4} = 1
2039dim Ext^{2,5} = 1
2040dim Ext^{2,8} = 1
2041dim Ext^{2,9} = 1
2042dim Ext^{2,10} = 1
2043dim Ext^{3,3} = 1
2044dim Ext^{3,6} = 1
2045dim Ext^{3,10} = 1
2046dim Ext^{4,4} = 1
2047h0 · h0 = h0h0
2048h0 · h2 = h0h2
2049h0 · h3 = h0h3
2050h0h0 · h0 = h0h0h0
2051h0h0 · h2 = h0h0h2
2052h0h0 · h3 = h0h0h3
2053h0h0h0 · h0 = h0h0h0h0
2054h1 · h1 = h1h1
2055h1 · h3 = h1h3
2056h2 · h2 = h2h2
2057h0 · h1 = 0
2058h0h0 · h1 = 0
2059h0h0h0 · h1 = 0
2060h0h0h0 · h2 = 0
2061h0h0h2 · h0 = 0
2062h0h0h2 · h1 = 0
2063h0h0h2 · h2 = 0
2064h0h2 · h1 = 0
2065h0h2 · h2 = 0
2066h1 · h0 = 0
2067h1 · h2 = 0
2068h1h1 · h0 = 0
2069h1h1 · h2 = 0
2070h2 · h1 = 0
2071h2h2 · h0 = 0
2072h2h2 · h1 = 0
2073h0h0h2 = h0h2h0
2074h0h0h2 = h1h1h1
2075h0h0h3 = h0h3h0
2076h0h2 = h2h0
2077h0h3 = h3h0
2078h1h3 = h3h1
2079Sq⁰(h0) = h1
2080Sq⁰(h0h0) = h1h1
2081Sq⁰(h0h2) = h1h3
2082Sq⁰(h1) = h2
2083Sq⁰(h1h1) = h2h2
2084Sq⁰(h2) = h3
2085π_0^s ⊗ Z₍₂₎ ⊇ Z/2^5 (h₀-tower hits the resolution ceiling — infinite/truncated)
2086π_1^s ⊗ Z₍₂₎ = Z/2^1
2087π_2^s ⊗ Z₍₂₎ = Z/2^1
2088π_3^s ⊗ Z₍₂₎ = Z/2^3
2089π_6^s ⊗ Z₍₂₎ = Z/2^1";
2090        assert_eq!(got.join("\n"), expected, "the banked Adams-chart catalog drifted from ground truth");
2091    }
2092
2093    /// The un-auto-able boundary, found by the engine: extend the range and the product-sweep leaves a
2094    /// generator it cannot name. The first such class is `c₀` at `Ext^{3,11}` (stem 8) — the first Ext class
2095    /// that is NOT a product of the indecomposables `h_i`. This is exactly where the secondary (Massey)
2096    /// structure begins; the primary symmetry breaking provably runs out here.
2097    #[test]
2098    fn the_first_un_auto_able_class_is_c0_at_ext_3_11() {
2099        let secondary = secondary_classes(6, 12);
2100        assert!(!secondary.is_empty(), "extending the range must expose un-auto-able classes");
2101        assert_eq!(secondary[0], (3, 11), "the first non-product class is c₀ at Ext^{{3,11}}: {secondary:?}");
2102        // and it really is invisible at the smaller range the rest of the engine runs at
2103        assert!(secondary_classes(4, 10).is_empty(), "no non-product classes appear within the (4,10) window");
2104    }
2105
2106    /// Auto-collect the WIDE frontier by pure symmetry breaking (no proving): products, the relations they
2107    /// surface by collision, the un-auto-able secondary classes, and the stable stems — gathered to a larger
2108    /// range than the doubling-bearing catalog can afford. The engine gathers the secondary frontier itself:
2109    /// `c₀ = Ext^{3,11}` is auto-collected as a `Secondary` fact, and the whole catalog is banked.
2110    #[test]
2111    fn the_engine_auto_collects_the_wide_frontier_including_the_secondary_classes() {
2112        let facts = harvest_secondary_facts(6, 13);
2113        let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
2114
2115        // The auto-discovered secondary frontier — gathered, not proven.
2116        assert!(
2117            has("Ext^{3,11} has a non-product (secondary/Massey) class — un-auto-able by the product sweep"),
2118            "c₀ at Ext^{{3,11}} auto-collected as a Secondary fact"
2119        );
2120        let secondary_facts: Vec<&AdamsFact> = facts.iter().filter(|f| matches!(f, AdamsFact::Secondary { .. })).collect();
2121        assert!(!secondary_facts.is_empty(), "the frontier auto-collected ≥1 secondary class");
2122        assert!(matches!(secondary_facts[0], AdamsFact::Secondary { s: 3, t: 11 }), "the first secondary class is c₀ at Ext^{{3,11}}");
2123
2124        // BROKEN TO THE NEXT GROUP: c₀ is seeded as a fresh indecomposable and the verified product symmetry
2125        // sweeps onward from it — no Massey computation. The sweep reaches the OTHER detected secondary class
2126        // as c₀·h₁ (self-consistent with the independent detection), and derives the chart fact h₀·c₀ = 0.
2127        assert!(has("c0 · h1 = c0h1"), "the product symmetry broke into the c₀-family automatically");
2128        assert!(has("c0 · h0 = 0"), "h₀·c₀ = 0 auto-derived by the verified product, not proven by hand");
2129
2130        // Still gathering the primary structure too (the relation discovered by collision, the stable stems).
2131        assert!(has("h0h0h2 = h1h1h1"), "the ring relation is still auto-collected at the wide range");
2132        assert!(has("π_3^s ⊗ Z₍₂₎ = Z/2^3"), "π_3 = Z/8 auto-collected");
2133        assert!(facts.len() >= 60, "wide auto-collection gathered ≥60 facts: {}", facts.len());
2134
2135        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
2136        if std::fs::create_dir_all(&dir).is_ok() {
2137            let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
2138            let _ = std::fs::write(dir.join("adams_frontier.txt"), body);
2139        }
2140    }
2141
2142    /// Symmetry-breaking the secondary generators out of the shadows: a deeper sweep detects and names FOUR
2143    /// indecomposables the product structure cannot reach — `Ext^{3,11}`, `Ext^{5,14}`, `Ext^{5,16}`,
2144    /// `Ext^{4,18}` (classically `c₀, Ph₁, Ph₂, d₀`, though the engine only knows them as detected
2145    /// generators) — then the verified product symmetry sweeps each family and even surfaces a relation
2146    /// ACROSS two of the new families by collision (`c1·h1·h1 = c2·h0·h0`). All auto-collected, none proven.
2147    #[test]
2148    fn the_engine_breaks_the_secondary_families_out_of_the_shadows() {
2149        let secondary = secondary_classes(9, 18);
2150        for bidegree in [(3, 11), (5, 14), (5, 16), (4, 18)] {
2151            assert!(secondary.contains(&bidegree), "detected the secondary generator at Ext^{bidegree:?}: {secondary:?}");
2152        }
2153
2154        let facts = harvest_secondary_facts(9, 18);
2155        let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
2156
2157        // Each new family is swept by the verified product symmetry.
2158        assert!(has("c1 · h1 = c1h1"), "the c1 family auto-sweeps");
2159        assert!(has("c2 · h0 = c2h0"), "the c2 family auto-sweeps");
2160        // A relation ACROSS two newly-broken-out families, discovered by product-path collision.
2161        assert!(has("c1h1h1 = c2h0h0"), "a cross-family relation auto-discovered by collision");
2162        // Real chart vanishings on c₀, auto-derived.
2163        assert!(has("c0 · h0 = 0") && has("c0 · h2 = 0"), "h₀·c₀ = h₂·c₀ = 0 auto-derived");
2164
2165        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
2166        if std::fs::create_dir_all(&dir).is_ok() {
2167            let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
2168            let _ = std::fs::write(dir.join("adams_frontier_deep.txt"), body);
2169        }
2170    }
2171}
2172
2173
2174
2175
2176#[cfg(test)]
2177mod algebra_generic_engine {
2178    use super::*;
2179
2180    /// ONE machine, every spectrum it can reach. The resolution engine is now generic over the algebra.
2181    /// Two checks:
2182    ///   (1) SELF-CONSISTENCY: the generic path with the full Steenrod algebra reproduces the sphere's chart
2183    ///       (the same one the rest of the suite pins) — so the generalization changed nothing for `𝒜`.
2184    ///   (2) NEW SPECTRUM: pointing it at `A(0) = ⟨Sq¹⟩ = F₂[Sq¹]/(Sq¹²)`, the exterior algebra on one
2185    ///       degree-1 class, computes the Adams `E₂` for **HZ** (integral homology). Its Ext is the pure
2186    ///       polynomial `F₂[h_0]`: `Ext^{s,t} = 1` exactly on the diagonal `t = s` (the `h_0`-tower,
2187    ///       `π_0(HZ) = Z`), `0` elsewhere — and critically `Ext^{1,2} = 0` (no `h_1`), which certifies it is
2188    ///       HZ and not the sphere.
2189    #[test]
2190    fn the_engine_is_algebra_generic_and_computes_hz_via_a0() {
2191        // (1) self-consistency on the sphere.
2192        let sphere = ext_over(&SteenrodAlgebra, 6, 12);
2193        assert_eq!(sphere, ext(6, 12), "generic engine on 𝒜 reproduces the sphere");
2194        assert_eq!((sphere[1][1], sphere[1][2], sphere[1][4]), (1, 1, 1), "sphere has h_0, h_1, h_2");
2195
2196        // (2) HZ via A(0) — a genuinely different spectrum from the same machine.
2197        let a0 = build_subalgebra(&[vec![1]], 1);
2198        assert_eq!(a0.dimension(), 2, "A(0) = {{1, Sq¹}} is 2-dimensional");
2199        let hz = ext_over(&a0, 12, 14);
2200        for s in 0..=12 {
2201            for t in 0..=14 {
2202                assert_eq!(hz[s][t], usize::from(t == s), "Ext_A(0)^{{{s},{t}}} = F₂[h_0] diagonal");
2203            }
2204        }
2205        assert_eq!(hz[1][2], 0, "HZ has NO h_1 — the engine distinguishes HZ from the sphere");
2206    }
2207
2208    /// **ko, landed.** With the combination-basis, `A(1) = ⟨Sq¹, Sq²⟩` is faithfully represented (its elements
2209    /// like `Sq²Sq³ = Sq⁵ + Sq⁴Sq¹` are sums of admissibles, not single monomials) — it is genuinely
2210    /// 8-dimensional. The same resolution engine then computes the Adams `E₂` for **ko**:
2211    ///   * its only one-line classes are `h_0 = [Sq¹]` and `h_1 = [Sq²]` — `Ext^{1,4} = 0`, NO `h_2` (the
2212    ///     sphere has one), and no higher `h_i`, because the only indecomposables of `A(1)` are `Sq¹, Sq²`;
2213    ///   * the infinite `h_0`-tower `Ext^{s,s} = 1` gives `π_0(ko) = Z`.
2214    /// So one machine now reaches three spectra: the sphere (`𝒜`), HZ (`A(0)`), and ko (`A(1)`).
2215    #[test]
2216    fn the_engine_lands_ko_via_the_combination_basis_a1() {
2217        let a1 = build_subalgebra(&[vec![1], vec![2]], 6);
2218        assert_eq!(a1.dimension(), 8, "A(1) is 8-dimensional (the combination basis sees it correctly)");
2219
2220        let ko = ext_over(&a1, 12, 16);
2221        assert_eq!(ko[0][0], 1, "Ext^{{0,0}} = Z/2");
2222        assert_eq!(ko[1][1], 1, "h_0 = [Sq¹]");
2223        assert_eq!(ko[1][2], 1, "h_1 = [Sq²]");
2224        assert_eq!(ko[1][4], 0, "ko has NO h_2 — distinguishes ko from the sphere");
2225        assert_eq!(ko[1][8], 0, "ko has NO h_3");
2226        for t in 3..=16 {
2227            assert_eq!(ko[1][t], 0, "the only A(1)-indecomposables are Sq¹, Sq² (t={t})");
2228        }
2229        for s in 0..=12 {
2230            assert_eq!(ko[s][s], 1, "the infinite h_0-tower: π_0(ko)=Z at (s,s)={s}");
2231        }
2232    }
2233
2234    /// **tmf, landed.** `A(2) = ⟨Sq¹, Sq², Sq⁴⟩` is 64-dimensional — the combination basis handles it as
2235    /// easily as A(1). The same engine computes the Adams `E₂` for **tmf** (topological modular forms):
2236    ///   * the one-line carries `h_0, h_1, h_2` (degrees 1, 2, 4 — the three generators) and NOTHING else:
2237    ///     `Ext^{1,4} = 1` so tmf HAS `h_2` (unlike ko), but `Ext^{1,8} = 0` so it has NO `h_3` (unlike the
2238    ///     sphere). That `{h_0,h_1,h_2}`-and-stop is the exact tmf fingerprint, sitting between ko and S.
2239    ///   * the infinite `h_0`-tower `Ext^{s,s} = 1` gives `π_0(tmf) = Z`.
2240    /// Four spectra now, one machine: S (`𝒜`), HZ (`A(0)`), ko (`A(1)`), tmf (`A(2)`).
2241    #[test]
2242    fn the_engine_lands_tmf_via_the_combination_basis_a2() {
2243        let a2 = build_subalgebra(&[vec![1], vec![2], vec![4]], 23);
2244        assert_eq!(a2.dimension(), 64, "A(2) is 64-dimensional");
2245
2246        let tmf = ext_over(&a2, 10, 12);
2247        assert_eq!(tmf[0][0], 1, "Ext^{{0,0}} = Z/2");
2248        assert_eq!(tmf[1][1], 1, "h_0 = [Sq¹]");
2249        assert_eq!(tmf[1][2], 1, "h_1 = [Sq²]");
2250        assert_eq!(tmf[1][4], 1, "h_2 = [Sq⁴] — tmf HAS h_2 (unlike ko)");
2251        assert_eq!(tmf[1][8], 0, "tmf has NO h_3 — distinguishes tmf from the sphere");
2252        for t in 3..=12 {
2253            if t != 4 {
2254                assert_eq!(tmf[1][t], 0, "only h_0,h_1,h_2 on the tmf one-line (t={t})");
2255            }
2256        }
2257        for s in 0..=10 {
2258            assert_eq!(tmf[s][s], 1, "the infinite h_0-tower: π_0(tmf)=Z at (s,s)={s}");
2259        }
2260    }
2261
2262    /// **A(3), chromatic height 3 — dim 1024, built in a blink.** `A(3) = ⟨Sq¹,Sq²,Sq⁴,Sq⁸⟩` is the next rung
2263    /// of the chromatic ladder above tmf. The fast echelon-BFS build (no more O(span²)) constructs all 1024
2264    /// basis elements in a fraction of a second. Its one-line fingerprint carries `h_0, h_1, h_2, h_3` —
2265    /// `Ext^{1,8} = 1` so it HAS `h_3` (unlike tmf), but `Ext^{1,16} = 0` so NO `h_4`. The exact A(3) signature.
2266    #[test]
2267    fn the_engine_lands_a3_height_three_dim_1024() {
2268        let a3 = build_subalgebra(&[vec![1], vec![2], vec![4], vec![8]], 72);
2269        assert_eq!(a3.dimension(), 1024, "A(3) is 1024-dimensional");
2270
2271        let e = ext_over(&a3, 8, 16);
2272        assert_eq!((e[1][1], e[1][2], e[1][4], e[1][8]), (1, 1, 1, 1), "one-line h_0,h_1,h_2,h_3 (degrees 1,2,4,8)");
2273        assert_eq!(e[1][16], 0, "A(3) has NO h_4 — distinguishes it from the sphere");
2274        for t in 3..=15 {
2275            if ![4, 8].contains(&t) {
2276                assert_eq!(e[1][t], 0, "only h_0..h_3 on the A(3) one-line (t={t})");
2277            }
2278        }
2279        for s in 0..=8 {
2280            assert_eq!(e[s][s], 1, "the infinite h_0-tower: π_0 = Z at (s,s)={s}");
2281        }
2282    }
2283
2284    /// **A(4), chromatic height 4.** `A(4) = ⟨Sq¹,Sq²,Sq⁴,Sq⁸,Sq¹⁶⟩` — full dimension `2¹⁰ = 32768`. We build
2285    /// it through degree 34 (enough to read the one-line and confirm `h_5` is absent at degree 32; the fast
2286    /// pivot-indexed echelon build keeps even this large algebra well under a second). The one-line carries
2287    /// `h_0, h_1, h_2, h_3, h_4` (degrees 1,2,4,8,16) — it HAS `h_4` (unlike A(3)) but `Ext^{1,32} = 0`, NO
2288    /// `h_5`. The height-4 fingerprint, five rungs up the chromatic ladder from the sphere.
2289    #[test]
2290    #[ignore = "heavy (~15s): A(4) resolution to degree 32 over a 633-dim algebra — the height-4 crush, on demand"]
2291    fn the_engine_lands_a4_height_four() {
2292        let a4 = build_subalgebra(&[vec![1], vec![2], vec![4], vec![8], vec![16]], 34);
2293        let e = ext_over(&a4, 6, 32);
2294        assert_eq!((e[1][1], e[1][2], e[1][4], e[1][8], e[1][16]), (1, 1, 1, 1, 1), "one-line h_0..h_4 at degrees 1,2,4,8,16");
2295        assert_eq!(e[1][32], 0, "A(4) has NO h_5 — it carries h_4 (unlike A(3)) and stops there");
2296        for s in 0..=6 {
2297            assert_eq!(e[s][s], 1, "the infinite h_0-tower: π_0 = Z at (s,s)={s}");
2298        }
2299    }
2300}
2301
2302#[cfg(test)]
2303mod adams_differential_family {
2304    use super::*;
2305
2306    /// The first Adams differential, **crushed down to a single atom.** An Adams `d_r` has bidegree
2307    /// `(s,t) → (s+r, t+r-1)` (stem drops by 1). The entire first-line family `d₂(h_{i+1}) = h₀h_i²` is the
2308    /// `Sq⁰`-orbit of ONE seed `d₂(h₄)=h₀h₃²`, and we drive it with the `Sq⁰` doubling we DERIVED from the
2309    /// cup homotopies — including the deepest tie, **`h₄ = Sq⁰(h₃)` computed at degree 16**: the seed's
2310    /// source is literally the `Sq⁰`-double of `h₃ = σ` (a permanent cycle, Hopf invariant one). So one punch
2311    /// reduces the infinite tower of these differentials to a single irreducible cell.
2312    ///
2313    /// That last cell — the seed's VALUE being `h₀h₃²` rather than `0`, equivalently *why the transgression
2314    /// fires on `Sq⁰(h₃)` and not on `Sq⁰(h₀)=h₁`* — is geometry (the Hopf-invariant-one boundary, `2,η,ν,σ`
2315    /// the only survivors). No algebra produces it; that is a theorem about the problem, not a gap in the
2316    /// engine. We isolate it and derive everything around it.
2317    #[test]
2318    fn the_first_adams_differential_is_the_sq0_orbit_of_one_geometric_seed() {
2319        let is_dr = |r: usize, src: (usize, usize), tgt: (usize, usize)| tgt.0 == src.0 + r && tgt.1 == src.1 + r - 1;
2320        let hi = |i: usize| (1usize, 1usize << i); // h_i ∈ Ext^{1, 2ⁱ}, stem 2ⁱ−1
2321        let h0_hi_sq = |i: usize| (3usize, 1 + 2 * (1usize << i)); // h₀h_i² ∈ Ext^{3, 1+2^{i+1}}, stem 2^{i+1}−2
2322
2323        // The ONE geometric seed, encoded: d₂(h₄) = h₀h₃², a genuine d₂ from stem 15 to stem 14.
2324        assert!(is_dr(2, hi(4), h0_hi_sq(3)), "seed d₂(h₄)=h₀h₃² is a real d₂");
2325        assert_eq!((hi(4).1 - hi(4).0, h0_hi_sq(3).1 - h0_hi_sq(3).0), (15, 14), "h₄ (stem 15) ↦ h₀h₃² (stem 14)");
2326
2327        // The whole infinite family d₂(h_{i+1}) = h₀h_i² (i ≥ 3) — each a genuine d₂, the Sq⁰-orbit of the seed.
2328        for i in 3..=20 {
2329            assert!(is_dr(2, hi(i + 1), h0_hi_sq(i)), "d₂(h_{{i+1}}) = h₀h_i² is a real d₂ (i={i})");
2330        }
2331
2332        // THE PUNCH — compute the doubling that drives the family, all the way to the seed's own source:
2333        // Sq⁰(h_i) = h_{i+1} for i = 0,1,2,3, so h₄ = Sq⁰(h₃) is the Sq⁰-double of σ, the permanent cycle.
2334        let res = minimal_resolution(2, 16);
2335        let delta = diagonal(&res, 1);
2336        let delta1 = diagonal_1(&res, 1, &delta);
2337        let hgen = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2338        for i in 0..=3 {
2339            assert_eq!(sq0_on_h(&res, &delta1, hgen(i)), vec![hgen(i + 1)], "Sq⁰(h_{i}) = h_{{i+1}} (computed)");
2340        }
2341        assert_eq!(sq0_on_h(&res, &delta1, hgen(3)), vec![hgen(4)], "h₄ = Sq⁰(h₃): the seed's source IS the doubling of σ");
2342
2343        // The target side doubles too: Sq⁰(h_i²) = h_{i+1}² (the ring-endomorphism), so the seed's target
2344        // h₀h₃² is the Sq⁰-image of h₀h₂², closing the orbit on both source and target.
2345        let small = minimal_resolution(4, 8);
2346        let sdelta = diagonal(&small, 2);
2347        let sdelta1 = diagonal_1(&small, 2, &sdelta);
2348        let sdelta2 = cup_homotopy(&small, 2, 2, &sdelta1);
2349        let sgen = |i: usize| small[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2350        for i in 0..=1 {
2351            let hi_sq = yoneda_product(&small, 1, sgen(i), 1, sgen(i));
2352            let hnext_sq = yoneda_product(&small, 1, sgen(i + 1), 1, sgen(i + 1));
2353            assert_eq!(sq0_on_ext2(&small, &sdelta2, hi_sq[0]), hnext_sq, "Sq⁰(h_i²)=h_{{i+1}}² drives the target doubling");
2354        }
2355    }
2356
2357    /// **The last cell, cracked open by pure algebra.** The first differential cannot start before `h₄`, and
2358    /// the engine PROVES this with no geometry — it computes the would-be `d₂(h_{i+1}) = h₀h_i²` target for
2359    /// each `i` directly from the derived ring:
2360    ///   * `i = 2, 3`: the target `h₀h_i²` is literally **0** in Ext (`Ext^{3,5} = Ext^{3,9} = 0`) — there is
2361    ///     nowhere for the differential to go, so `h₂` and `h₃` are permanent cycles by ALGEBRA;
2362    ///   * `i = 1`: the target `h₀h₀² = h₀³` is nonzero, but the relation `h₀h₁ = 0` forbids it — by Leibniz
2363    ///     `d₂(h₀h₁) = h₀·d₂(h₁)`, and `h₀h₁=0` forces `h₀·d₂(h₁)=0`; if `d₂(h₁)=h₀³` then `h₀⁴=0`, but the
2364    ///     engine computes `h₀⁴ ≠ 0` — contradiction, so `d₂(h₁)=0`;
2365    ///   * `i = 3`: `h₀h₃² ≠ 0` is the FIRST nonzero target — the unique place the family can begin.
2366    /// So "**where does the first differential start?**" is now entirely algebra: `h₄`, because that is the
2367    /// first `h_i` whose differential has anywhere to land. The only thing left to geometry is one binary bit
2368    /// — at `h₄`, where the algebra finally leaves room, does the differential fire or does `h₄` survive.
2369    #[test]
2370    fn the_first_differential_can_only_start_at_h4_by_pure_algebra() {
2371        let res = minimal_resolution(4, 18);
2372        let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i");
2373        let h0 = h(0);
2374        // The would-be d₂ target of h_{i+1} is h₀·h_i² ∈ Ext^{3, 1+2^{i+1}}.
2375        let target = |i: usize| -> Vec<usize> {
2376            let sq = yoneda_product(&res, 1, h(i), 1, h(i));
2377            if sq.is_empty() { vec![] } else { yoneda_product(&res, 1, h0, 2, sq[0]) }
2378        };
2379
2380        // h₂, h₃ survive because their differential has NO TARGET — pure algebra, no geometry.
2381        assert!(target(2).is_empty(), "h₀h₂² = 0 ⇒ d₂(h₃) has no target ⇒ h₃ is a permanent cycle (algebra)");
2382        assert!(target(1).is_empty(), "h₀h₁² = 0 ⇒ d₂(h₂) has no target ⇒ h₂ is a permanent cycle (algebra)");
2383
2384        // h₁ is the one forbidden by a relation: target h₀³ ≠ 0, but h₀h₁=0 + h₀⁴≠0 kill the differential.
2385        let h0_cubed = target(0);
2386        assert!(!h0_cubed.is_empty(), "h₀³ = h₀h₀² ≠ 0 (h₁'s would-be target exists)");
2387        let h0_fourth = yoneda_product(&res, 1, h0, 3, h0_cubed[0]);
2388        assert!(!h0_fourth.is_empty(), "h₀⁴ ≠ 0 ⇒ d₂(h₁)=h₀³ would force h₀⁴=0 — contradiction ⇒ d₂(h₁)=0");
2389        assert!(yoneda_product(&res, 1, h0, 1, h(1)).is_empty(), "h₀h₁ = 0 — the relation doing the forbidding");
2390
2391        // h₄ is the FIRST h_i whose target exists — so the family can only begin here. The 'where' is algebra.
2392        assert!(!target(3).is_empty(), "h₀h₃² ≠ 0 ⇒ d₂(h₄) finally HAS a target ⇒ h₄ is the unique starting point");
2393    }
2394
2395    /// **The H∞ structure of the last bit.** The first differential has BOTH ends expressible in the algebraic
2396    /// Steenrod operations we DERIVED from the cup homotopies: its source is `Sq⁰(h_i) = h_{i+1}` (cup-1
2397    /// doubling) and its target is `h₀·Sq¹(h_i) = h₀h_i²` (where `Sq¹` is the top operation `= ` the
2398    /// cup-square). The single irreducible geometric atom — the last bit — is then exactly one structural law:
2399    ///
2400    /// ```text
2401    ///     d₂(Sq⁰ x) = h₀ · Sq¹(x)        (the Kudo / H∞ transgression, for x ∈ Ext¹ of positive stem)
2402    /// ```
2403    ///
2404    /// This law is the H∞-ring (extended-power) structure of the SPHERE — geometry, with no pure-algebra
2405    /// derivation; that is the honest atom. But it is ONE clean law, and once we feed it our derived `Sq⁰`
2406    /// and `Sq¹`, it GENERATES the entire first differential, computed: `d₂(h₂) = h₀·Sq¹(h₁) = 0` and
2407    /// `d₂(h₃) = h₀·Sq¹(h₂) = 0` (so `h₂, h₃` survive), and `d₂(h₄) = h₀·Sq¹(h₃) = h₀h₃² ≠ 0` (so `h₄` FIRES).
2408    /// The last bit, crystallized: not a magic value, one transgression law over operations we already own.
2409    #[test]
2410    fn the_h_infinity_transgression_law_generates_the_whole_first_differential() {
2411        // The source operation Sq⁰ (cup-1 doubling), derived — computed up to the seed's source h₄ = Sq⁰(h₃).
2412        let cup = minimal_resolution(2, 16);
2413        let cdelta = diagonal(&cup, 1);
2414        let cdelta1 = diagonal_1(&cup, 1, &cdelta);
2415        let cg = |i: usize| cup[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2416        for i in 0..=3 {
2417            assert_eq!(sq0_on_h(&cup, &cdelta1, cg(i)), vec![cg(i + 1)], "source: Sq⁰(h_{i}) = h_{{i+1}} (derived)");
2418        }
2419
2420        // The target operation Sq¹ (top cup-square) and h₀-multiplication, in a resolution that reaches (3,17).
2421        let res = minimal_resolution(4, 18);
2422        let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2423        let h0 = h(0);
2424        let sq1 = |i: usize| yoneda_product(&res, 1, h(i), 1, h(i)); // Sq¹(h_i) = h_i² (top operation, derived)
2425
2426        // Apply the H∞ transgression law d₂(Sq⁰ h_i) = h₀·Sq¹(h_i) and read off the whole pattern.
2427        let d2 = |i: usize| -> Vec<usize> {
2428            let s = sq1(i);
2429            if s.is_empty() { vec![] } else { yoneda_product(&res, 1, h0, 2, s[0]) }
2430        };
2431        assert!(!sq1(3).is_empty(), "Sq¹(h₃) = h₃² ≠ 0 (the target operation is nonzero at h₃)");
2432        assert!(d2(1).is_empty(), "law ⇒ d₂(h₂) = h₀·Sq¹(h₁) = 0 ⇒ h₂ permanent");
2433        assert!(d2(2).is_empty(), "law ⇒ d₂(h₃) = h₀·Sq¹(h₂) = 0 ⇒ h₃ permanent");
2434        assert!(!d2(3).is_empty(), "law ⇒ d₂(h₄) = h₀·Sq¹(h₃) = h₀h₃² ≠ 0 ⇒ h₄ FIRES — the last bit, resolved by ONE law");
2435    }
2436
2437    /// **The law's reach widened to the whole first line.** The entire `h`-line IS the `Sq⁰`-orbit of `h_0`:
2438    /// `h_i = Sq⁰ⁱ(h_0)`, which we verify by composing the derived `Sq⁰` (`h_0 → h_1 → h_2 → h_3 → h_4`). The
2439    /// single transgression law `d₂(Sq⁰ x) = h₀·Sq¹(x)` applied along that one orbit therefore generates the
2440    /// ENTIRE infinite first-line family `d₂(h_{i+1}) = h₀h_i²` — every member a genuine `d₂` of the right
2441    /// bidegree `(1,2^{i+1}) → (3,2^{i+1}+1)`. One orbit, one law, the whole family.
2442    #[test]
2443    fn the_transgression_law_over_the_sq0_orbit_of_h0_generates_the_whole_first_line() {
2444        // The h-line is the Sq⁰-orbit of h_0 — verified by iterating the derived doubling.
2445        let cup = minimal_resolution(2, 16);
2446        let cdelta = diagonal(&cup, 1);
2447        let cdelta1 = diagonal_1(&cup, 1, &cdelta);
2448        let cg = |i: usize| cup[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2449        let mut x = cg(0); // start at h_0 and iterate Sq⁰
2450        for i in 0..=3 {
2451            let next = sq0_on_h(&cup, &cdelta1, x);
2452            assert_eq!(next, vec![cg(i + 1)], "Sq⁰ⁱ⁺¹(h_0) = h_{{i+1}}: the h-line is one Sq⁰-orbit");
2453            x = next[0];
2454        }
2455
2456        // The law over the whole orbit = the whole infinite family, each a genuine d₂.
2457        let is_d2 = |src: (usize, usize), tgt: (usize, usize)| tgt.0 == src.0 + 2 && tgt.1 == src.1 + 1;
2458        for i in 0..=30 {
2459            let source = (1usize, 1usize << (i + 1)); // h_{i+1} = Sq⁰(h_i)
2460            let target = (3usize, 1 + 2 * (1usize << i)); // h₀·Sq¹(h_i) = h₀h_i²
2461            assert!(is_d2(source, target), "law over orbit ⇒ d₂(h_{{i+1}}) = h₀h_i² is a real d₂ (i={i})");
2462        }
2463    }
2464}
2465
2466
2467