diff --git a/.gitignore b/.gitignore index 1d3c65b..c538905 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ models/ data/ configs/llm/_gen/ logs/ +# exploratory bundles kept on disk; tracked on the dev branch +results/_archive/ diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 7da2ef7..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,147 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Current state: Layer 1 complete; Layer 1.5 (neural) in progress - -- **Layer 1** (`src/inheritance/`) — **complete and validated.** All six experiments E1–E6, the - closed-form scientific-validation tests, figures, and reproducibility harness exist. Headline: - critical grounding `g* = 0.048 ≪ 1`; the E4 finding that mean-mixture distillation conserves - collapse while only a union-preserving max-merge realises the recombination benefit. -- **Layer 1.5** (`src/neural/`) — **in progress.** An architecture-general neural existence proof - (re-scoped Layer 2): the same Wright–Fisher abstractions realised in *real trained generative - models* (histogram bridge + RNN + MLP; VAE implemented but not fidelity-passing) on a - fully-synthetic sandbox with an exact oracle, plus real MNIST as a later secondary tier. See - `tasks/todo.md` for status and `~/.claude/plans/we-are-going-to-cheerful-fog.md` for the plan. - **Done:** scaffold, the histogram bridge gate (reproduces Layer 1 exactly), `bridge` (neural - g*=0.047 ≈ Layer 1), `collapse` (in RNN weights), `grounding` (refined; sign confirmed, threshold - softened by neural smoothing — see finding below), `architectures` (architecture-generality), - `recombination` (the E4 "merge, don't average" finding reproduced in real weights), all six - neural figures, and the **real-MNIST external-validity tier** (`mnist_collapse`: a conv-VAE - collapses to a single mode under dry self-training, ~10% grounding holds all 30 modes; frozen-CNN - oracle, confusion matrix recorded). **Remaining:** `region_matched`, `remint`, the synthetic-VAE - fidelity fix — all optional. The LLM/LoRA rung and C3 vertical claim stay deferred. The LLM/LoRA rung and the C3 vertical claim are deferred. Experiments are - named descriptively (`configs/neural/.yaml`), not by code. - -The two design documents are the source of truth for intent: - -- `paper/the-lamarckian-society-v5.md` — the *perspective paper* (the "why"; reframed around sexual - reproduction). `paper/results-summary.md` — the plain-language + technical summary of all results. -- `paper/blueprint.md` — the *technical blueprint* (the "what"/"how"). **It is normative** for - Layer 1 and the LLM Layer 2; Layer 1.5 is a cost-staged intermediate the blueprint does not - cover, designed to preserve the same §1 abstractions. - -Everything below summarizes the blueprint so you can orient fast, but the blueprint is the source of truth. When they conflict, the blueprint wins; when the blueprint is silent, minimize decisions and match its established patterns. - -## The one idea you must hold in your head - -**Knowledge transmission across agent generations is modelled *literally* as a Wright–Fisher population-genetics process** — not by analogy. A model's knowledge is a distribution `p_t` over `K` discrete items on a simplex; a fixed true distribution `p*` has a rare **tail**; each generational step is "sample from parent (drift) + mix in fresh real samples (immigration/grounding) + refit." Model collapse = loss of rare alleles under drift. Every experiment is a manipulation of this single process. - -The population-genetics dictionary in blueprint §1 is the spine. Keep its abstractions **identical across both layers** — this is a hard requirement, because it is the only thing that lets a Layer-2 neural result count as confirming a Layer-1 analytic prediction: - -| Abstraction | Layer 1 (analytic) | Layer 2 (neural) | -|---|---|---| -| region | disjoint block of the `K` items | task family (e.g. string ops, recursion) | -| rarity / tail | low `p*` items | low-frequency task types | -| grounding fraction `g` | `m/(n+m)` real-vs-inherited samples | proportion of verifier-passed items in pupil's training mix | -| decorrelation `ρ` | shared retained-tail correlation between teachers | LoRA specialists on disjoint task families | -| diversity `H` | heterozygosity `1 − Σ pᵢ²` | solution diversity of generated code | -| reality's "no" | grounding against `p*` | execution-based unit-test verifier | - -## Two layers, staged by cost - -- **Layer 1 — analytical core** (`src/inheritance/`). Pure NumPy/SciPy Wright–Fisher simulator. Laptop, minutes, no GPU. Carries the paper's quantitative claims. **Three of the five §2.4 predictions are closed-form**, so validation is an **exact** test, not a vibe check — these become `<0.1%`-tolerance assertions in `test_scientific_validation.py`: - - **Pred. 1** — neutral heterozygosity decay: `E[Hₜ] = H₀(1 − 1/n)ᵗ`. - - **Pred. 3** — *exact* mutation–drift equilibrium for the implemented immigration model: `H_eq = H* · m(2n+m−1)/(n+2nm+m²)`, with `H* = 1 − Σ(p*ᵢ)²`. The textbook `θ/(1+θ)` (θ=2m) is only the rare-immigrant limit. **Critical nuance: H is *smooth* in m — the sharp phase threshold lives in discrete tail-item survival (Pred. 4: an item survives iff `m·p*ᵢ ≳ 1`), not in H.** Do not describe E2 as a discontinuity in H. - - **Pred. 5** — closed-form recombination benefit: `U(K_T, ρ, q) = T[ρq + (1−ρ)(1−(1−q)^K_T)]` (expected tail items retained by ≥1 of K_T teachers). -- **Layer 2 — neural existence proof** (`src/neural/`). Small open-weight models (default OLMo-2-1B / SmolLM2-1.7B, fallback Qwen2.5-1.5B-Instruct; pin the HF revision hash, never track `main`), LoRA specialisation, distillation/merging across 2–3 generations, program-synthesis-with-unit-tests as the verifier. One consumer GPU. Only needs to show the **sign** of three effects, not precise magnitudes. - -## Experiments and their falsifiers - -Each experiment is one config file → one runner invocation → one `results.parquet` → one figure. Every experiment has a **falsifier** — an outcome that would refute the corresponding claim. The design is built to be able to kill the thesis; preserve that. - -- Layer 1: **E1** reproduce collapse (null), **E2** grounding phase boundary (headline: is there a critical `g* ≪ 1`?), **E3** region-matched grounding, **E4** multi-teacher decorrelation, **E5** quality-diversity vs. greedy selection, **E6** re-minting gate / irreversibility. -- Layer 2: **C1** dry vs. grounded, **C2** one vs. N complementary teachers at matched budget, **C3** the vertical claim (general knowledge climbs while each specialty is re-earned and *exceeded* — this is load-bearing, prioritize it), **C4** distillation vs. merging (optional). - -Blueprint §6 is the claim→experiment→figure→falsifier traceability matrix and is the definition of done. - -## The one non-obvious implementation piece: the correlated-teacher construction (§2.7.1) - -E4's whole purpose is to isolate the effect of teacher **decorrelation ρ**, so ρ must be a *directly constructed, independently-swept* knob — **never** an emergent quantity you get by tuning drift (that ρ would be confounded with `n`, `m`, tail size, and generation count, i.e. with the very drift E4 holds fixed). The construction is a **shared-switch exchangeable Bernoulli**: for each of the `T` tail items, draw a shared switch `z~Bern(ρ)`, a shared retention `s~Bern(q)`, and per-teacher independent `u⁽ᵏ⁾~Bern(q)`; set teacher k's retention `r⁽ᵏ⁾ = s if z else u⁽ᵏ⁾`. This yields exact marginal retention `q` and exact pairwise correlation `ρ` (provable: Cov = ρq(1−q), Var = q(1−q)), and is exchangeable so ρ is a single scalar. `make_retention_matrix(T, K_T, rho, q, rng)` returns the `(K_T, T)` binary matrix; `make_correlated_teachers` maps it to distributions (head items always kept at `p*`; tail item kept at `p*ᵢ` if retained, else `tail_floor`; renormalise so dropped-tail mass flows to survivors). The exact-construction path is **preferred** for E4; the drift-based path exists only as a realism cross-check. `region_specialisation=True` forces full retention of a teacher's home-region tails and applies the ρ construction only off-home. - -**E4 reports two coverages, and their gap is a result, not noise:** the construction-level union `U(K_T,ρ,q)` (must match the closed form exactly) and the post-distillation *surviving* coverage after the pupil's size-`n` resampling. A tail item present in the mixture only survives if its mixture mass clears `~1/n` (Pred. 4) — so the gap is precisely "the tail recombination *supplied* but drift *re-erased* because grounding was too thin," which ties E4 back to E2/E3. - -**Finding (2026-07-04, E4) — the recombination operator matters, and mean-mixture distillation does not realise the benefit.** Under the blueprint's mean-mixture pupil (`p̄ = mean(teachers)`), surviving tail coverage is **flat in K_T** — a conservation law: averaging preserves expected pupil tail mass at `q·(tail mass of p*)` regardless of K_T, and in the rare-tail (linear-survival) regime the 1/K_T dilution exactly cancels the union gain. The recombination benefit is realised only under a **union-preserving merge** (`max` over teachers, à la M2N2), where surviving rises with K_T and decorrelation. So E4 reports surviving under **both** operators (`surviving_mean`, `surviving_max`): union = supply (validated vs closed form), max-merge = realised benefit, mean-distill = the null that motivates why merging/grounding is needed. GG decision: report both. This sharpens rather than refutes the thesis, but the paper's recombination claim rests on the *merge* operator, not naive mean distillation — worth carrying into Layer 2 (C4) and the write-up. - -**Finding (2026-07-05, neural `grounding`) — grounding's SIGN transfers to trained RNN weights, but the sharp `g*` does not; and tail-survival is the *wrong* neural collapse metric.** Re-ran the phase-boundary sweep at 18 replicates. Two results: (1) **forward-KL is the operative neural collapse metric, not H or tail-survival.** The RNN's smoothing inductive bias keeps *spurious* tail modes alive (it generalises to unseen codewords), so `tail_truth_mass_alive` is flat/**non-monotone** in g (dry 0.54 > most grounded points) and H stays ~0.77–0.85 of H\* throughout — neither shows a threshold. Stationary **forward-KL** falls monotonically (dry 2.08 → g=0.2: 0.75), significant at g≥0.05 (paired t up to 3.3; 89% of lineages improve at g=0.2). This *refines* the earlier "forward-KL AND tail survival" note: for a smoothing model, support-counting decouples from closeness-to-truth. (2) **The sharp `g*≪1` is an exact-operator feature, softened by neural inductive bias.** Half the achievable KL reduction closes by a *median-recovery* grounding g≈0.04 (bootstrap CI [0.004, 0.116]) — a striking echo of Layer-1's 0.048 — but full (95%) recovery needs g≈0.19, far more than the histogram bridge, because smoothing both caps dry collapse (KL~2, not ∞) and slows full recovery. So the quantitative `g*≪1` claim rests on the **histogram bridge** (g\*=0.047, exact reduction to Layer 1), which the trained RNN confirms in *sign* and softens in sharpness. Honest note: the pre-registered 95%-of-H\*/tail-survival falsifier is not met, but that is because those are the wrong metrics for a smoothing model, not because grounding fails — the blueprint §3.5 directional claim (grounding arrests collapse) holds robustly. Robustness fix landed alongside: a fully-degenerate RNN can emit only invalid codewords, so `measure_distribution` returns a terminal-collapse sentinel (fixation on the dominant mode) instead of crashing a long sweep. - -**Finding (2026-07-05, real-MNIST `mnist_collapse`) — collapse and grounding-rescue reproduce on real images.** External-validity tier: a small **convolutional VAE** (the canonical generative-collapse model) is retrained each generation on its own generated digits. Modes = digit class × stroke-thickness bin (K=30, Zipf, ~18 tail modes); the oracle is a **frozen CNN + deterministic thickness** at **98.5% mode accuracy** (its 30×30 confusion matrix is recorded in the manifest as the measurement floor). Result (4 reps): the **dry (g=0) lineage collapses to a single mode** — forward-KL 0.5→18, support 30→1, tail truth-mass 1.0→0.06, H→0 — while **10% grounding holds all 30 modes** (KL≈0.6, full tail, H≈0.9). The VAE needs ~10% grounding here vs the synthetic histogram's ~5%, consistent with the `grounding` finding that trained neural models need more grounding than the exact operator. **Confirmation-only (signs, not magnitudes; blueprint §3.5)** — the exact synthetic oracle stays the quantitative anchor. `figures/mnist_montage.py` is an eyeball diagnostic (re-runs a short dry lineage; NOT a parquet figure). Build gates passed: CNN mode accuracy 98.5%; VAE gen-0 recovers full 30/30 support (over-smooths frequencies, KL≈0.5, no prior hole — unlike the *synthetic*-codeword VAE, which is why the MNIST VAE works where that one didn't). The MNIST tier is heavy (torchvision `--extra mnist`, downloads MNIST, ~5 min): `make mnist`, kept out of the `make neural` loop. - -**Finding (2026-07-05, learning kernel) — neutral drift is a null both real models fail, oppositely; the estimator bias is a signed operator.** Layer-1 extension (`knowledge/kernel.py`, `LearningKernelCfg`): the refit becomes `p_{t+1} = T_θ(counts/n)` with two pop-gen knobs — **reset `u`** (mutation toward a prior = smoothing) and **temperature `τ`** (sharpening = mode-competition) — both identity at their defaults, so the histogram bridge and every scientific-validation test are unchanged (68 core tests still green). Result: **neutral Wright–Fisher fails both neural architectures, in opposite directions.** VAE regime (`n=6000, K=30`): neutral drift is *inert* (no collapse), yet the real VAE collapsed to one mode — **sharpening `τ=0.8` reproduces it** (the estimator ADDS collapse). RNN regime (`n=200, K=256`): neutral drives `H→0`, but the real RNN only partially collapses — **mutation `u=0.006` reproduces the `H`-floor** (the estimator REMOVES collapse). Honest caveat: uniform-mutation matches the RNN `H`-floor but overshoots its forward-KL (~5 vs ~2), evidence the RNN's smoothing prior is *truth-like, not uniform* (future refinement). Configs `configs/inheritance/kernel_{sharpen,smooth}.yaml`, figure `plot_kernel.py`. This mechanistically explains the architecture-generality result and the softened neural `g*`. - -**Strategic positioning vs Riis 2026 (arXiv:2604.08554, "Drift and selection in LLM text ecosystems").** Riis independently formalizes **collapse = Wright–Fisher drift** (his Thm 1) with n-gram agents: minority-mass martingale, rare-first extinction, single-token dropout ≈ αe^{−α}, de Bruijn-polytope fixed points, plus descriptive-vs-normative *selection* (Thm 2). **Concede as prior art:** "collapse is literally Wright–Fisher", the martingale, rare-first loss, the WF/effective-population formalism — cite him; do **not** frame these as our contribution. **Crucial distinction that protects us:** his "mixed environment" *retains the lineage's own old synthetic tokens* — there is **no injection of fresh real data from a fixed `p*`**, so his headline is *pessimistic* (Thm 1c: extinction is independent of α — retention only changes speed). Our **grounding is immigration from a non-drifting external truth**, giving a stationary `H_eq>0` and a critical `g*≪1` that *prevents* collapse — the mechanism his closed loop lacks. **Our defensible novelty, ranked:** (1) **recombination + "merge, don't average" conservation law** (E4) — he has no model-merging operator; flagship; (2) **the learning-kernel / estimator-bias axis** — he *explicitly names it as future work*; we now build+measure it; (3) grounding threshold (solid anchor, but immigration–drift balance is classic — not a flagship); (4) architecture-generality in real weights + MNIST; (5) **the Lamarckian society + the vertical/cumulative C3 claim — wholly ours, not yet run.** Reposition the paper from *"collapse is drift"* (now contested) to **a population-genetic *control theory* for sustaining open-ended knowledge**: drift is the diagnosed disease (cite Riis), our contribution is the engineered remedies and their integration. - -**Finding (2026-07-05, E7/E8 — the multi-locus society frame; raises the ceiling).** To express the *vertical* claim (capability that *exceeds* any component), knowledge is generalized from a single-locus fixed-`p*` distribution to a distribution over **genotypes** (`L` biallelic loci, `K=2^L`; fitness = # correct loci; reuses all the K-mode machinery). The one new operator is **recombination** (`knowledge/genotype.py`): free recombination sends `p → ⊗ per-locus marginals` (linkage equilibrium). Two experiments, both analytic. **E8 (the star, `kind: society`) — the vertical claim / Fisher–Muller:** decorrelated *parents* (specialists, expert on their loci, agnostic elsewhere) are recombined; **sexual merge assembles a genotype fitter than any parent, climbing to the optimum (12/12) as parent count grows and `ρ→0`, while the best single parent (~8.7) and the mean-mixture "model soup" (~11.6) plateau below.** Clean, dramatic, 40 reps; reuses `make_retention_matrix` (locus mastery replaces tail-item retention). **E7 (`kind: genotype_lineage`) — the advantage of sex:** a single population adapting toward the optimum; the sexual lineage adapts *faster* (clonal interference slows the asexual one) by keeping loci in linkage equilibrium (LD→0 vs LD spike). Honest scope: a **speed** advantage, not a permanent Muller's-ratchet gap (the single-population ratchet is subtle to force; E8 carries the headline). **Metaphor shift (GG, 2026-07-05):** the society is framed as **sexual reproduction with unbounded parents**, *not* teacher→pupil — teacher→pupil caps at the ceiling (recovery), n-parent recombination is combinatorial and *generative* (exceeds any parent), and unlike biology there is no two-parent limit. Collapse = asexual degradation; the cure = sex. This unifies E4 (merge≠average) + E6 (irreversibility) under evolution-of-sex theory and stakes ground Riis's single-locus n-grams cannot reach. Scope is bounded: fixed combinatorial space (`L≤12`, "effectively open-ended relative to n"), additive fitness (NK/epistasis is an optional extension). - -**Finding (2026-07-05, E9/E10 — the sexual-transmission model made rigorous: when sex helps, and directed sex).** Deepening the sexual metaphor (GG excited; wanted it robust before the full society). Added a **Kauffman NK landscape** (`genotype.nk_fitness`, tunable ruggedness `K`), finite **crossover** (`genotype.crossover`, n-parent, per-gap recombination rate), and **hill-climb** (parents = local optima = "trained models"). **E9 (`kind: recomb_landscape`) — landscape robustness / "why sex?":** E8's dramatic transgression used an *additive* landscape; on rugged (epistatic) landscapes, blindly recombining local optima causes **outbreeding depression** — mean offspring fall *below* the parents, worse with ruggedness AND recombination rate (`K=8`, free recomb: ≈ −0.23), and the **optimal recombination rate shrinks as ruggedness grows**. Design rule: *merge freely when skills are complementary/additive; sparingly + with selection when entangled.* **E10 (`kind: directed_sex`) — directed sex beats biological sex (the AI superpower):** biology is stuck with 2 random-mating parents and no offspring preview; an AI can **choose complementary mates + evaluate many recombinant offspring + keep the fittest + use unbounded parents** (iterated recombine-then-select). Result: random ("biological") sex craters with ruggedness (0.66→0.51), while **directed sex tracks/exceeds the best parent at every ruggedness** — converting the outbreeding-depression catastrophe into a win. This is the practical, distinctly-AI payoff and has no biological analog. `configs/inheritance/{E9,E10}.yaml`, `plot_{E9,E10}.py`, READMEs, +5 tests (117 green). Complete sexual-transmission picture: **dramatic super-parent offspring when skills are complementary (E8); outbreeding-depression risk when entangled (E9); directed sex resolves the risk (E10).** - -**Finding (2026-07-05, E11 — the dynamic Lamarckian society: the vertical claim / C3, realized).** The culmination: a finite population of `N` agents (genotypes, `L` loci) evolves on a rugged NK landscape that *is* reality (`knowledge/dynamic_society.py`), composing the four operators the whole study built toward — grounding, directed recombination (sex), quality-diversity selection, mutation. Grounding is made load-bearing via the **consensus-conformity (self-consumption)** mechanism (GG decision): selection acts on `g·true_fitness + (1−g)·conformity` (conformity = agreement with the population's own consensus), so `g=0` optimises fitting-the-crowd rather than reality. **4-arm ablation (12 reps), each breaking distinctly, only the full society climbing (global_opt≈0.79):** `full` 0.78 (climbs to the optimum, diversity maintained longest) · `no_sex` 0.77 (can't recombine to escape local optima) · `no_diversity`/greedy 0.74 (collapses diversity fastest, stuck at a worse local optimum) · **`no_grounding` 0.48 (self-consumption collapse to an unfit consensus** — trains on the crowd, regresses to a confident-but-wrong mean; conformity−true gap ≈0.5). This integrates E1–E6 + the kernel + E7–E10 into one system and shows the society needs **all** of grounding + directed sex + diversity: on a rugged landscape you need diversity to explore basins, sex to recombine them, grounding to select on reality — remove any and you fail differently. `configs/inheritance/fig4_society_ablation.yaml`, `plot_fig4_society_ablation.py`, README, +5 tests (122 green). **This closes the C3 vertical claim analytically** (the LLM rung remains the eventual empirical instantiation). - -**Finding (2026-07-05, LLM prototype `llm_merge` — the first real-LLM step; honest/partial).** First move from toy models toward real LLMs (blueprint C2/C4, the real-LLM image of E8), on one 16 GB GPU. New `src/llm/` package: procedural task families + exact-match verifier (`tasks.py`), batched eval (`evaluate.py`), LoRA specialisation (`specialise.py`, manual answer-only SFT), weight-space merge via peft `add_weighted_adapter` (`merge.py`: soup=averaged deltas, ties=sign-reconciled union), runner (`experiment.py`, kind `llm_merge`). Base = **Qwen2.5-0.5B-Instruct** (Apache-2.0). Three *disjoint*, deliberately-hard families (lists/strings/arith); one LoRA specialist each (~90 s total). **Result (seed 1):** each specialist spikes on its own family; the **merges are the only models competent across ALL families — worst-family ≈0.25 vs <0.16 for every single specialist** (the Fisher-Muller "generalist from specialists" signature, robust). **But** the stronger "exceeds every parent *overall*" claim is only marginal at this scale (soup 0.64 vs best specialist 0.63; ties 0.61 below it), and averaging visibly *dilutes* peaks (lists: specialist 0.43 → merge 0.26 — the E4 "merge, don't average" caveat in real weights). Honest scope: pipeline works end-to-end; the balance/retention half reproduces; the strict overall-exceeds and the soup-vs-ties distinction need scale (bigger base, more/cleaner families, seeds, dilution-resistant/offspring-selected merge). **Env notes:** Python 3.14 + transformers 5.13 works (cp314 wheels exist); `transformers 5.x` changed `apply_chat_template` (returns a dict; render to text then tokenize; pass `**inputs` to `generate`). `make env-llm` / `make llm`; adapters cached under gitignored `models/llm/`, base in the HF cache (outside the repo). 125 tests green (+3 pure task/verifier). The full grounded sexual *society* on LLMs (C1 collapse, directed sex, the dynamic society) is the HPC-scale next step. - -**Finding (2026-07-05, `llm_merge_hpc` — the 7B firm-up on Imperial CX3; the marginal sign becomes decisive).** Re-ran `llm_merge` at a capable base — **Qwen2.5-7B-Instruct**, 200 test tasks/family, one **L40S (46 GB)** GPU, 8 min walltime — via the `/imperial-hpc` runbook (see `memory/hpc-setup.md`). **Both merges reach 0.87 overall, decisively above the best single specialist (0.77) and above every specialist on every family; worst-family 0.62 vs ≤0.57 for any specialist.** The two 0.5 B caveats are resolved: (1) the strict Fisher–Muller "exceeds every parent overall" claim is now clean (+10 points, not marginal); (2) the **dilution vanishes** — at 7 B the merge *beats* the lists-specialist on lists (0.62 > 0.57), where at 0.5 B averaging diluted it (0.43 → 0.26). **Interpretation: dilution is a small-model artefact; a capable base has enough headroom that weight-space averaging composes rather than dilutes** — the "merge, don't average" concern (E4) softens once parents are strong (soup ≈ ties at K=3). Results synced to `results/llm_merge_hpc/` (README legend + data-driven figure title). The natural refinement is *module-level* union-preserving recombination (MoE-expert / adapter-union merge, the real-weight image of E8's max-merge) rather than delta-averaging. - -**Finding (2026-07-05, `llm_moe` — the union operator in real weights; E8's `max` vs `mean`, 0.5B).** Added the *union-preserving* recombination operator that `llm_merge` lacked (`src/llm/moe.py`, `kind: llm_moe`): never average the parents — keep each specialist LoRA intact and **select** the right one per prompt (MoE **router**: `oracle`, or `learned` = training-free nearest-centroid over the *base* model's own prompt embeddings) or per module (`max_merge` = winner-take-all by delta-norm). Reuses the cached `llm_merge` specialists (no retraining). **Result (0.5B, seed 1):** **routing wins decisively over fusion — overall 0.74 / worst-family 0.43 vs soup 0.64/0.26** — and recovers *each* specialist's own-family peak exactly (no dilution: fusion diluted the lists-specialist 0.43→0.26, routing keeps 0.43). This is E8's `max`(union) > `mean`(average) in real LLM weights. **Two honest riders:** (1) the learned router is *trivially perfect* (1.00) because the three families are lexically distinct — routing's win here rests partly on the routing problem being easy (ambiguous/overlapping skills would make the router the bottleneck — the interesting next failure mode); (2) **router-free `max_merge` is a poor union (0.46)** — static per-module winner-take-all isn't input-adaptive, so it collapses toward the strongest-norm modules; the union benefit needs *routing*, not weight surgery. `configs/llm/moe.yaml`, `plot_llm_moe.py`, `results/llm_moe/README.md`, +2 router tests (127 green). The regime question — does routing still beat fusion once a capable base lets fusion *compose* rather than dilute (7B soup already beats its specialists)? — is the `llm_moe_hpc` 7B run below. - -**Finding (2026-07-05, `llm_moe_hpc` — the regime *flips* at 7B; "merge, don't average" is a weak-base law).** Re-ran `llm_moe` at **Qwen2.5-7B-Instruct** (L40S, 9 min, reusing the cached 7B specialists). **The union-vs-fusion ordering inverts:** at 0.5B union won (routing 0.74 > soup 0.64); at 7B **fusion wins — soup 0.87 > routing 0.84 > max_merge 0.78.** Mechanism, and it's the deep point: **routing *selects* one intact specialist so it is capped at the best parent per family** (lists 0.57 = spec_lists, strings 0.97 = spec_strings), whereas **fusion *blends* deltas and, at a capable base, composes *beyond* any parent** (soup lists 0.62 > spec 0.57, strings 1.00 > spec 0.97). Selection can't synthesise something better than its best component; averaging-that-composes can. So the E4/E8 "merge, don't average" law is **regime-dependent — a weak-parent / small-model law, not universal**: union wins exactly when averaging *dilutes* (0.5B), fusion wins once the base has headroom to *compose* (7B). This refines rather than contradicts E8 (whose additive-landscape `max>mean` assumed no compositional headroom). The operator to actually want is **fusion-that-composes + selection over recombinant offspring** = the "directed sex" ideal (E10), the natural next experiment. `results/llm_moe_hpc/` (README + regime-aware figure title). Riders unchanged: learned router trivially perfect (lexical families), `max_merge` the weakest union (not input-adaptive). - -**Finding (2026-07-05, `llm_directed` — directed sex in weights; refinements pay off only when the default blend is suboptimal).** E10 in real LLM weights (`src/llm/directed.py`, `kind: llm_directed`): breed a *population* of recombinant offspring (specialists merged at Dirichlet-sampled weights), score each against the verifier on a held-out **validation** split, keep the fittest — reported on a fresh **test** split (no selection-on-test leakage). Two breeding objectives (best-overall, best-worst-family). **The value scales with how far the uniform soup is from optimal, giving a clean regime split:** **0.5B** — soup dilutes, so directed selection beats it (`directed_overall` 0.69 > soup 0.64; `directed_balanced` worst-family 0.37 > soup 0.26), though single-objective selection trades off the other axis (breeding for overall tanks the rare `lists` to 0.17) and a *global* blend still trails per-input **routing** (0.74). **7B** — soup already *composes* to the ceiling on these near-saturated families (strings & arith at 1.00), so directed selection finds nothing better: **directed 0.868 ≈ soup 0.873** (marginally below, a val/test overfit gap). **Honest limitation:** the 7B families are near-saturated (2/3 at 1.00), which structurally caps the headroom — this run can't separate "directed sex doesn't help at scale" from "these tasks are too easy at 7B"; a *harder, unsaturated* benchmark is the fair next test. **Through-line across all four LLM runs:** "merge, don't average" and its refinements (routing, directed selection) are **weak-base / suboptimal-default** phenomena — they pay off at 0.5B (soup far from optimal) and are inert at 7B (soup near-optimal on saturated tasks). `configs/llm/{directed,directed_hpc}.yaml`, `plot_llm_directed.py`, `results/llm_directed{,_hpc}/`, `hpc/llm_directed.pbs`, +3 tests (130 green). - -**Finding (2026-07-05, HARD benchmark `llm_moe_hard_hpc` + `llm_directed_hard_hpc` — the 7B "fusion wins / no headroom" results were SATURATION artefacts; the law is HEADROOM, not base-size).** The easy families saturated 7B (strings & arith at 1.00), so the 7B nulls (moe: fusion 0.87 > union 0.84; directed ≈ soup) couldn't separate "refinements don't help at scale" from "tasks too easy." Built a **hard task variant** (`hard: true` in `tasks.py`: multi-step lists, Caesar ciphers / letter transforms, multi-step & larger arithmetic — same family labels & answer formats, threaded through `make_tasks`/`train_specialist`/runners; hard specialists cache separately as `spec_*_hard`) and re-ran both at 7B on Imperial CX3 (one L40S, 24 min, unsaturated: arith ≈0.48, strings 0.67, lists 0.34). **Both nulls flip back to the 0.5B ordering:** (1) **union beats fusion again — routing 0.500 > fusion 0.40** (soup 0.392/ties 0.400), the *same* 10-pt margin as 0.5B; fusion dilutes the fragile strings-specialist so hard (0.665 → soup 0.300) that soup even **trails the best single specialist** (0.425), while routing keeps it (0.670). (2) **directed selection beats soup again — 0.492 > 0.392** (+10 pts), recovering most of routing's benefit from one deployable merged model (lifts strings back to 0.630). **Correction to the earlier interpretation:** the `llm_moe_hpc` "regime flip" (fusion wins at 7B) and `llm_directed_hpc` "no headroom" were both driven by **task saturation, not base capability**. The operative variable is **headroom**: "merge, don't average" (union > fusion) and "directed sex" (selection > single blend) hold whenever there's room to lose to dilution — weak base (0.5B) *or* hard tasks at a strong base (7B-hard); fusion only wins in the degenerate corner where easy tasks let a strong base compose to the 1.00 ceiling. This vindicates E8's `max > mean` in real 7B weights once saturation is controlled. `configs/llm/{moe_hard,moe_hard_hpc,directed_hard_hpc}.yaml`, `hpc/llm_hard.pbs`, `results/llm_{moe,directed}_hard_hpc/`, +1 hard-task test (131 green). - -**Finding (2026-09-11, three controls from the manuscript review; `results/llm_curriculum_v5_{stop3,decor}/`, `results/llm_*_hpc/s{1,2,3}/`).** (1) **Forced stop** (`merge_until: 3`): obligate merging through generation 2 then none finishes 0.793 vs the declinable merge's 0.792 (per seed −0.008/−0.006/+0.011) — the veto's outcome is explained by *when* it stopped. (2) **Decorrelated curriculum** (`orders:` key; complementarity 0.00→0.70→0.00 instead of the Latin square's monotone fall): declines still rise with generation (0.44→0.89); pooled partial ρ(declined, complementarity | generation) = −0.07, CI (−0.21, +0.09), partial ρ with generation +0.31. **The recombination-modifier / reduction-principle reading of Fig. 4B is withdrawn**; the Latin-square ρ = −0.57 was carried by generation (adapter age, skill count and destroyer arrival are confounded). What stands: one bit of selection per merge, or a fixed early stop, avoids the obligate-merge collapse at no cost against never merging. (3) **7B seeds 2–3** (33 min/seed, L40S): merge − best specialist +0.066±0.036, routing − soup +0.094±0.015, directed − soup +0.073±0.031, all 3/3 seeds; *not* replicated: "soup below the best specialist on hard tasks" (1/3, mean +0.001) — softened in text. Stats: `figures/stats_llm_curriculum.py` (also the single source of curriculum arm labels, used by `make_figs`), `figures/stats_llm_7b_seeds.py`; `_figlib.load_seed_bundles` reads `s{seed}/` layouts. - -**Finding (2026-09-12, four experiments from the dropped "Limits"; `results/llm_speciation/s{1,2,3}`, `results/llm_curriculum_v5_{early,late,early_obl,late_obl,cull}/s{1,2,3}`, `results/llm_{merge_seeds,moe_hard_seeds}_smol`).** GG's rule: a limitation that names a runnable experiment is run, not stated. (1) **Speciation seeds 2–3:** conflict cliff (merge 0.02/0.12/0.16 vs parents 0.23–0.25) and duration null (0.76→0.95) hold in 3/3 seeds; seed 1's cliff was the deepest. Fig. 5C–D now has CI bands. (2) **Conflict-arrival curricula** (`orders:`; boolq/winogrande in generations 1–2 or 5–6, age and skill count rising identically): declines and the obligate collapse follow **generation, not conflict arrival** (partial ρ with conflict-present, generation controlled: −0.09, CI (−0.45, 0.15); with generation: +0.45); conflict-early dips at arrival, recovers, collapses from generation 5; conflict-late collapses from generation 4 with its pair still to come. What stays confounded is adapter age with skill count. (3) **Second base lineage** (SmolLM2-1.7B-Instruct, `adapters_dir: models/llm_smol` because the specialist cache is keyed by family+seed only): Fisher–Muller replicates 5/5 (soup +0.049±0.022, TIES +0.097±0.020 over best specialist), headroom 3/3 with a larger margin (routing − soup +0.162±0.036; soup below best specialist in 3/3). (4) **Differential reproduction** (`cull: true`, `cull_step`/`inherit_slot`): **parity again** — with selection, declinable 0.793 vs never-merge 0.804 (−0.011±0.003, 3/3 below); selection − none +0.007±0.030; recombination's early lead present with and without selection, gone by generation 5. The Discussion's prediction (selection turns speed into level) is withdrawn: under a curriculum that delivers every skill to every lineage the ceiling is what one adapter carries. Speciation adapters now live in `speciation_s{seed}/` (an array over seeds used to race on a shared dir). SI Figs. S14–S16; stats in `figures/stats_llm_{curriculum,speciation_seeds,smol}.py`. - -## Build order (blueprint §7) — respect the gate - -1. Scaffold: repo layout (§5), container, pytest skeleton, config system, seeding utils. `make test` green. -2. Layer 1 core + null model + `test_scientific_validation.py` against §2.4 predictions 1–2. - **HARD GATE: do not proceed until simulated drift matches the analytic heterozygosity decay `E[Hₜ] = H₀(1 − 1/n)ᵗ`.** -3. Layer 1 grounding + E1–E2 (the headline result). -4. Layer 1 E3–E6. Layer 1 is now a complete laptop-reproducible paper on its own. -5. Layer 2 scaffold + verifier (test determinism & sandbox isolation *before* any training). -6. Layer 2 C1 + C3. -7. Layer 2 C2 (+ C4 if compute allows). -8. Reproduction pass. - -**Do not start Layer 2 until Layer 1's scientific-validation tests pass.** - -## Prescribed structure and commands (do not yet exist — create per blueprint §4–5) - -Target module interfaces are given with **normative names** in blueprint §2.7 (Layer 1) and §3.6 (Layer 2); downstream scripts depend on these signatures, so implement to them exactly. Target repo layout is §5. Planned automation: - -``` -make env # uv sync -> .venv from committed uv.lock -make test # correctness tests + scientific-validation tests -make layer1 # run E1–E6 -make layer2 # run C1–C3 (C4 optional) -make figures # regenerate every figure from committed results.parquet -make all -./reproduce.sh # uv sync → test → run all at committed seeds → regen figures → REPRODUCED.md -``` - -Single-experiment run pattern: one YAML config per experiment under `configs/inheritance/EX.yaml` or `configs/layer2/CX.yaml`, fed to the experiment runner. Figures are regenerated separately by `figures/plot_EX.py` reading **only** `results.parquet` (no re-simulation). - -## Non-negotiable engineering standard (blueprint §4) - -- **Reproducibility is a hard requirement, not a preference (this is a paper).** The environment is a **`uv` venv built from a committed, hash-pinned `uv.lock`** — that lockfile is the source of truth for "it runs" (Apptainer is dropped; a Dockerfile may later wrap the same lockfile for Layer 2's GPU work). Layer 1 is bitwise-reproducible from a single master seed; Layer 2 is *statistically* reproducible (document residual GPU non-determinism, set determinism flags, report per-seed points). -- **Seeding:** one master seed in config → derive all sub-seeds via `np.random.SeedSequence.spawn`. Never touch global RNG state; pass `rng` explicitly everywhere. Results are a pure function of the resolved config. -- **No magic numbers in code.** Every parameter lives in a YAML resolved at run time; the *resolved* config (after sweep expansion) is written next to results. Sweeps are declared in config, not hard-coded. -- **Output contract for every run:** `results.parquet` (long form) + `resolved_config.yaml` + `manifest.json` (library/CUDA versions, seed, git commit, model revision hashes, content hash of results). Every figure must be a pure function of a committed results artifact. -- **Scientific-validation tests are the spine of trust.** They assert the simulator reproduces the §2.4 closed forms within tolerance; if they fail, the *science* is wrong, not just the code. Keep them. -- **Open science end-to-end:** open-weight models only, permissive/open tooling (uv, MLflow or plain versioned Parquet — avoid closed SaaS trackers), `results/` gitignored but hashes tracked. - -## Stack - -Python ≥ 3.11. Layer 1: NumPy, SciPy, pandas, matplotlib — no GPU, no heavy deps. Layer 2: PyTorch, HF `transformers` + `peft` (LoRA), `datasets`, optional `vllm`; sandboxed `subprocess` verifier. Config via a thin pydantic + PyYAML loader (not Hydra — its global state/`chdir` fights the pure-function-of-resolved-config contract). Env via a **`uv` venv from a committed `uv.lock`** — the lockfile is the reproducibility source of truth; Layer 1 needs no container. diff --git a/Makefile b/Makefile index fd206ed..e914a8d 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,7 @@ # reproducibility source of truth; every target runs inside it via `uv run`. .PHONY: env env-neural env-mnist env-llm env-notebooks test inheritance neural mnist llm \ - llm-epistasis llm-society llm-society-calib llm-society-calib-b llm-society-v2-smoke \ - figures paper-figures paper notebooks clean + llm-speciation llm-epistasis figures paper-figures paper notebooks clean env: ## build .venv from the committed lockfile uv sync --extra dev @@ -31,13 +30,13 @@ mnist: ## run the torchvision tiers: MNIST collapse + real-weight sp uv run python -m neural.experiment configs/neural/speciation_real_emergent.yaml MPLBACKEND=Agg uv run python figures/mnist_montage.py # the asset paper Fig. 2A embeds -env-llm: ## add the LLM stack for the Layer-2 prototype (GPU; transformers/peft) +env-llm: ## add the LLM stack for the language-model tier (GPU; transformers/peft) uv sync --extra dev --extra neural --extra llm -llm: ## run the LLM prototypes: merge (fusion) + moe (union) + directed (offspring select) - uv run python -m llm.experiment configs/llm/merge.yaml - uv run python -m llm.experiment configs/llm/moe.yaml - uv run python -m llm.experiment configs/llm/directed.yaml +llm: ## the 0.5B multi-seed runs behind Fig. 3B and Table S2 (merge x5, moe-hard x3, directed-hard x3) + uv run python -m llm.experiment configs/llm/merge_seeds.yaml + uv run python -m llm.experiment configs/llm/moe_hard_seeds.yaml + uv run python -m llm.experiment configs/llm/directed_hard_seeds.yaml llm-speciation: ## LLM-tier speciation: conflict cliff (replace + de-confounded add) and duration null uv run python -m llm.experiment configs/llm/speciation.yaml @@ -48,33 +47,6 @@ llm-epistasis: ## the controlled predictive test (feeds paper Fig. 3C-D) + i uv run python -m llm.experiment configs/llm/epistasis_compat.yaml uv run python figures/stats_llm_epistasis.py -llm-society: ## the composed society at LLM scale (C3): pilot; the campaign runs on HPC - uv run python -m llm.experiment configs/llm/society.yaml - -llm-society-calib: ## v2 society calibration gates (prereg §4): stage A (families), then B (C2/C3/C5) - uv run python -m llm.experiment configs/llm/society_v2_calib_a.yaml - @echo "Review the C1 table, fix the 12 families in society_v2_calib_b.yaml, then: make llm-society-calib-b" - -llm-society-calib-b: ## v2 calibration stage B over the chosen 12 families: transmission, cross, consensus - uv run python -m llm.experiment configs/llm/society_v2_calib_b.yaml - mkdir -p configs/llm/_gen - uv run python - <<'EOF' - import yaml; c = yaml.safe_load(open("configs/llm/society_v2_calib_b.yaml")) - for st in ("cross", "consensus"): - c["stage"] = st; c["output"] = {"dir": f"results/llm_society_v2_calib_b_{st}"} - yaml.safe_dump(c, open(f"configs/llm/_gen/calib_{st}.yaml", "w"), sort_keys=False) - EOF - uv run python -m llm.experiment configs/llm/_gen/calib_cross.yaml - uv run python -m llm.experiment configs/llm/_gen/calib_consensus.yaml - -llm-society-v2-smoke: ## v2 society loop smoke (4 families, 4 agents, 2 generations, all arms) - uv run python -m llm.experiment configs/llm/society_v2_smoke.yaml - uv run python figures/plot_llm_society.py results/llm_society_v2_smoke - -llm-seeds: ## multi-seed firm-up (heavy): merge x5, moe-hard x3, directed-hard x3 - uv run python -m llm.experiment configs/llm/merge_seeds.yaml - uv run python -m llm.experiment configs/llm/moe_hard_seeds.yaml - uv run python -m llm.experiment configs/llm/directed_hard_seeds.yaml figures: ## regenerate per-experiment figures from committed results (pure; no re-simulation) for p in figures/plot_*.py; do MPLBACKEND=Agg uv run python "$$p"; done diff --git a/REPRODUCING.md b/REPRODUCING.md index f1ff280..cac56e6 100644 --- a/REPRODUCING.md +++ b/REPRODUCING.md @@ -53,8 +53,7 @@ make test # correctness + closed-form scientific validation make inheritance # the inheritance model, every experiment at its committed seed make neural # trained networks (needs a GPU) make mnist # real-MNIST tier + the Fig. 2A montage asset (needs torchvision) -make llm # language-model prototypes (needs a GPU) -make llm-seeds # the multi-seed firm-ups behind Fig. 3B +make llm # the 0.5B multi-seed language-model runs behind Fig. 3B (needs a GPU) make llm-epistasis # the controlled predictive test behind Fig. 3D-E make llm-speciation # the LLM speciation tier behind Fig. 5C-D make figures # per-experiment figures, from committed parquets (no re-simulation) @@ -210,5 +209,5 @@ qsub hpc/llm_7b_seeds.pbs # seeds 2-3 of the three 7B experiments (array) paper's "regenerates from committed artifacts without re-simulation" holds from the archive. - `figures/mnist_montage.py` re-runs a short dry lineage to draw its montage rather than reading a parquet; it is an eyeball diagnostic whose quantitative counterpart is `results/fig2_mnist_collapse/`. -- The composed society at language-model scale is an open experiment at the time of writing; see - `tasks/workorder-llm-society.md`. +- The composed society at language-model scale is an open experiment at the time of writing; its + configs, pilots and pre-registration are on the `dev` branch. diff --git a/configs/inheritance/collapse_null.yaml b/configs/inheritance/collapse_null.yaml deleted file mode 100644 index 44aa685..0000000 --- a/configs/inheritance/collapse_null.yaml +++ /dev/null @@ -1,32 +0,0 @@ -experiment: E1_reproduce_collapse -seed: 20260704 -n_replicates: 100 -generations: 600 - -# Null model (blueprint 2.5-E1): no grounding, single teacher, no selection. -# Starting from the truth makes the collapse narrative clean -- the tail is lost first, -# support shrinks to 1, forward-KL diverges, and H decays geometrically at rate 1/n. -# (The decay law E[H_t]=H_0(1-1/n)^t holds from any start; see the validation suite.) -truth: - K: 500 - R: 1 - tail: zipf - zipf_s: 1.1 - tail_frac: 0.5 - tail_threshold: 1.0e-3 - init: truth - -dynamics: - n: 100 # distillation sample size = drift strength - teachers: {K_T: 1, rho: 0.0, q: 1.0} - grounding: {m: 0, policy: proportional} - selection: {mode: none, novelty_alpha: 0.0} - remint: {enabled: false, period: null, H_gate: null} - -metrics: - kl_floor: 1.0e-9 - support_eps: 1.0e-9 - -# No sweep: a single condition. -output: - dir: results/collapse_null diff --git a/configs/inheritance/sexual_vs_asexual_lineage.yaml b/configs/inheritance/sexual_vs_asexual_lineage.yaml deleted file mode 100644 index a69b5e2..0000000 --- a/configs/inheritance/sexual_vs_asexual_lineage.yaml +++ /dev/null @@ -1,29 +0,0 @@ -experiment: E7 -kind: genotype_lineage -seed: 20260705 -n_replicates: 20 - -# (The advantage of sex — the dynamic mechanism behind E8): a single population adapts from all-wrong -# toward a multi-locus optimum under selection + drift + mutation. Beneficial alleles arise in -# different sub-lineages; recombination reassorts them into one genotype, while an asexual lineage -# suffers clonal interference (the alleles compete and cannot combine). Expect the SEXUAL lineage -# (recomb_rate=1) to climb toward the optimum faster than the ASEXUAL one (recomb_rate=0) — the -# classical advantage of sex, and the reason a lone model lineage cannot do what a recombining -# society can. Honest scope: a SPEED advantage, not a dramatic permanent gap (the single-population -# ratchet is subtle); E8 carries the headline. Falsifier: sexual adapts no faster than asexual. - -genotype: - L: 12 - n: 150 # population/resample size (drift strength) - mu: 0.02 # per-locus mutation (flip) rate - base: 1.3 # multiplicative selection: fitness weight = base^(#correct loci) - recomb_rate: 0.0 # overwritten per arm by the sweep - init: wrong # start all-wrong (load L); adapt upward - -generations: 120 - -sweep: - - param: genotype.recomb_rate - values: [0.0, 1.0] # asexual vs sexual - -output: {dir: results/sexual_vs_asexual_lineage} diff --git a/configs/inheritance/speciation_bdm_nk.yaml b/configs/inheritance/speciation_bdm_nk.yaml deleted file mode 100644 index 3b1669e..0000000 --- a/configs/inheritance/speciation_bdm_nk.yaml +++ /dev/null @@ -1,22 +0,0 @@ -experiment: E12_nk -kind: speciation -seed: 12 -n_replicates: 15 - -# E12 (NK variant) — the EPISTASIS WEDGE, the paper's distinct falsifiable claim: at matched divergence, -# mergeability is governed by the EPISTASIS (ruggedness K) of the capability landscape, not by divergence -# alone (every existing ML merge predictor is a divergence measure). Parents are LOCAL OPTIMA reached by -# hill-climbing a Kauffman NK landscape from random starts; recombining them exposes broken co-adapted -# blocks. As K rises, recombining two adapted parents flips from a gain (offspring above the worse parent) -# to outbreeding depression (offspring below it). K=0 (additive) is the no-isolation control. - -speciation: - landscape: nk - L: 16 - K: [0, 2, 4, 6, 8, 10] # ruggedness / epistasis knob - n_pairs: 40 # random parent-pairs (local optima) aggregated per landscape - recomb_rate: 0.5 - n_offspring: 200 - -output: - dir: results/speciation_bdm_nk diff --git a/configs/llm/compose_gate.yaml b/configs/llm/compose_gate.yaml deleted file mode 100644 index bcb3e56..0000000 --- a/configs/llm/compose_gate.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# Generation-0 gate for the composition experiment (prereg v3 §4: C1 base, C3 specialists, -# C4 replication). Trains the two founders and measures everything at t=0 without iterating. -# -# Base choice (C1, measured 2026-09-07): Qwen2.5-1.5B **base, not Instruct**. Zero-shot GSM-Hard -# program-aided: Instruct-1.5B 0.500, Instruct-3B 0.417, base-3B 0.633 — all far too strong, since a -# base that already has the skills makes the specialists' contribution vacuous. Base-1.5B scores -# 0.067, within noise of the 0.059 Llama-2-7B starting point in LoRA Soups (COLING 2025), so the -# published composition effect has room to appear. -experiment: llm_compose_gate -kind: llm_compose -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -generations: 0 # gate only: train founders, measure t=0, stop -arms: [dry] -n_hard: 150 # composed target (GSM8k-Hard, program-aided) — SE ~0.04 -n_gsm8k: 100 # math own-skill probe -n_mbpp: 80 # code own-skill probe (execution-verified) -n_probe: 40 # shared prompts for rho -k_inherit: 300 -epochs: 3 -conf_gate: 0.85 -g: 0.10 -spec_train: 1200 -spec_epochs: 3 -max_new_tokens: 320 -batch_size: 16 -score_batch_size: 4 -train_batch_size: 2 # 16 GB A4000; the fp32 logit upcast at vocab 152k is the constraint -train_max_len: 448 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_compose_gate} diff --git a/configs/llm/compose_gate_math500.yaml b/configs/llm/compose_gate_math500.yaml deleted file mode 100644 index f3b249f..0000000 --- a/configs/llm/compose_gate_math500.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Generation-0 gate, second configuration (prereg v3 §4a): the composed target is MATH-500 -# (competition maths, level >= 3, numeric answers), and merge weights are *selected* on a disjoint -# validation split rather than fixed at 0.5/0.5. -# -# Why: on GSM8k-Hard the code parent alone reaches 0.427, because once code removes the arithmetic -# burden the base's own reasoning suffices — so maths is not scarce and E8's premise fails. MetaMathQA -# is built from GSM8K *and* MATH, so MATH-500 tests reasoning the specialist has and the base lacks. -# Founders are shared with the first gate (same experiment name), so this costs evaluation only. -experiment: llm_compose_gate -kind: llm_compose -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -generations: 0 -arms: [dry] -target: math500 -n_hard: 120 # test split (level>=3 pool is 271; 70% test / 30% val, disjoint) -n_hard_val: 50 # val split, screens the merge weights only -merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.2, 0.8]] -n_gsm8k: 100 -n_mbpp: 80 -n_probe: 40 -k_inherit: 300 -epochs: 3 -conf_gate: 0.85 -g: 0.10 -spec_train: 1200 -spec_epochs: 3 -max_new_tokens: 320 -batch_size: 16 -score_batch_size: 8 -train_batch_size: 2 -train_max_len: 448 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_compose_gate_math500} diff --git a/configs/llm/compose_s1.yaml b/configs/llm/compose_s1.yaml deleted file mode 100644 index a91c90a..0000000 --- a/configs/llm/compose_s1.yaml +++ /dev/null @@ -1,55 +0,0 @@ -# The composition campaign, seed 1 (prereg v3, amended after the generation-0 sweep of 2026-09-07). -# -# Arms. The gen-0 sweep found that the merge *weighting* dominates the operator: the a-priori 0.5/0.5 -# blend fails under both operators (surplus -0.020 cat, -0.093 linear) while a selected weight passes -# (+0.080 linear at 0.2/0.8, +0.027 cat at 0.3/0.7). Weights are therefore chosen each generation on a -# disjoint validation split (E10, directed recombination) in every arm, and the operator is an -# explicit per-arm setting: -# dry — linear operator, no grounding [H2, H3, H5: does composition survive drift?] -# grounded — linear operator, g = 0.10 [H4: does immigration arrest it?] -# dry_cat — concatenation operator, no grounding [H6, revised: does the operator ordering hold -# across generations, or only at gen 0?] -experiment: llm_compose -kind: llm_compose -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -generations: 6 -arms: -- dry -- grounded -- dry_cat -g: 0.1 -n_hard: 150 -n_gsm8k: 150 -n_mbpp: 100 -n_probe: 60 -k_inherit: 300 -epochs: 3 -conf_gate: 0.85 -spec_train: 1200 -spec_epochs: 3 -max_new_tokens: 320 -batch_size: 16 -score_batch_size: 4 -train_batch_size: 2 -train_max_len: 448 -resume: true -lora: - r: 16 - alpha: 32 -output: - dir: results/llm_compose/s1 -arm_ops: - dry: linear - grounded: linear - dry_cat: cat -n_hard_val: 60 -merge_weights: -- - 0.5 - - 0.5 -- - 0.3 - - 0.7 -- - 0.2 - - 0.8 -- - 0.1 - - 0.9 diff --git a/configs/llm/curriculum_g2.yaml b/configs/llm/curriculum_g2.yaml deleted file mode 100644 index 5508f7a..0000000 --- a/configs/llm/curriculum_g2.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# G2 probe (prereg v4 §5): does forgetting occur AT ALL under the real curriculum length? -# The 3-family gate showed none — but with only 3 families, ample adapter capacity and a generous -# replay budget, none was expected. This runs the full nine-family curriculum on ONE lineage with -# replay switched OFF, which is the harshest condition the design can offer. If early families still -# do not decay, the families are too mutually compatible and too easy for their own good, and the -# society has no forgetting to rescue — a design-level stop. -experiment: llm_curriculum_g2 -kind: llm_curriculum -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman] -lineages: 1 -generations: 9 -arms: [isolated] -baselines: [] -n_new: 300 -n_replay: 0 # replay OFF — the harshest forgetting condition -n_test: 60 -n_val: 30 -epochs: 3 -lr: 1.0e-4 -max_new_tokens: 32 -batch_size: 32 -train_batch_size: 2 -train_max_len: 448 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_curriculum_g2} diff --git a/configs/llm/curriculum_gate.yaml b/configs/llm/curriculum_gate.yaml deleted file mode 100644 index 809a06c..0000000 --- a/configs/llm/curriculum_gate.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Gate run for the curriculum society (prereg v4 §5). G0 is the decisive one: with a single lineage -# and no recombination, does cumulative capability CLIMB across generations? If capability cannot -# accumulate in the simplest arm, no outcome of the full design is interpretable — stop. -# Also exercises G1 (inheritance transmits), G2 (forgetting occurs without full replay) and, with -# lineages=3, G3 (recombination combines disjoint knowledge). -experiment: llm_curriculum_gate -kind: llm_curriculum -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -families: [strings, setops, numtheory] # 3 families, 3 generations — the smallest climb test -lineages: 3 -generations: 3 -arms: [isolated, society] -baselines: [] -n_new: 300 -n_replay: 120 -n_test: 60 -n_val: 30 -epochs: 3 -lr: 1.0e-4 -operator: linear -merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.7, 0.3]] -max_new_tokens: 32 -batch_size: 32 -train_batch_size: 2 -train_max_len: 448 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_curriculum_gate} diff --git a/configs/llm/curriculum_v5_calib.yaml b/configs/llm/curriculum_v5_calib.yaml deleted file mode 100644 index 6865718..0000000 --- a/configs/llm/curriculum_v5_calib.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Calibration for the real-dataset curriculum (prereg v4 §5 gates, re-run on the new families). -# Stage A (this config): base accuracy on every candidate, and one specialist per candidate trained -# from the base on its train split — the C1 band is base <= 0.40 and specialist >= 0.60. -# Stage B (curriculum_v5_g2.yaml, after selection): one lineage, zero replay, all chosen families in -# sequence — the forgetting probe; mean drop across families learned before the last must be >= 0.15, -# and it must not be carried by a single family (the v4 failure: one pair at +0.65, the rest ~0). -experiment: llm_curriculum_v5_calib -kind: llm_curriculum -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -families: [gsm8k, mbpp, boolq, mnli, sst2, csqa, arc, winogrande, squad, nq_open, hellaswag] -lineages: 11 # one lineage per candidate = each trains only its own family at gen 0 -generations: 1 # gen 0 only: founders; the recorded acc_* rows ARE the specialist table -arms: [isolated] -baselines: [] -n_new: 300 -n_replay: 0 -n_test: 60 -n_val: 20 -epochs: 3 -lr: 1.0e-4 -max_new_tokens: 48 # spans / short text need more room than a label -batch_size: 24 -train_batch_size: 2 -train_max_len: 512 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_curriculum_v5_calib} diff --git a/configs/llm/curriculum_v5_calib_b.yaml b/configs/llm/curriculum_v5_calib_b.yaml deleted file mode 100644 index 913129e..0000000 --- a/configs/llm/curriculum_v5_calib_b.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Stage A, second pass: the six candidates that missed the C1 band at 300 founder examples, retrained -# at the budget every working specialist in this project has used (v2, v3: 1200 x 3 epochs). -# Decides whether the curriculum has six families or nine. -experiment: llm_curriculum_v5_calib_b -kind: llm_curriculum -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -families: [gsm8k, mbpp, sst2, csqa, winogrande, nq_open] -lineages: 6 -generations: 1 -arms: [isolated] -baselines: [] -n_new: 1200 -n_replay: 0 -n_test: 60 -n_val: 20 -epochs: 3 -lr: 1.0e-4 -max_new_tokens: 48 -batch_size: 24 -train_batch_size: 2 -train_max_len: 512 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_curriculum_v5_calib_b} diff --git a/configs/llm/curriculum_v5_decor.yaml b/configs/llm/curriculum_v5_decor.yaml index 8879bc7..b72a4ad 100644 --- a/configs/llm/curriculum_v5_decor.yaml +++ b/configs/llm/curriculum_v5_decor.yaml @@ -4,7 +4,7 @@ # then diverges maximally, then converges: complementarity 0.00, 0.67, 0.70, 0.58, 0.33, 0.00 by # generation. Same six families, same G, destroyers (boolq, winogrande) spread across lineages as # in the Latin square. Arms: the declinable merge (`society` + `allow_veto`) and its never-merge -# reference under the same curriculum. Pre-registered readout: tasks/prereg-llm-society-v4.md §8g. +# reference under the same curriculum. Pre-registered readout: tasks/prereg-llm-society-v4.md on the dev branch §8g. experiment: llm_curriculum_v5_decor kind: llm_curriculum base_model: Qwen/Qwen2.5-1.5B diff --git a/configs/llm/curriculum_v5_g2.yaml b/configs/llm/curriculum_v5_g2.yaml deleted file mode 100644 index 635f754..0000000 --- a/configs/llm/curriculum_v5_g2.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Stage B (prereg v4 §5 G2, v5 families): does forgetting occur, and is it general rather than one pair? -# One lineage, zero replay, all six selected families in sequence at the founder budget that passed C1. -# Pass: mean drop across families learned before the last >= 0.15, with no single family carrying -# more than 50% of the total drop (the v4 failure mode: one pair at +0.65, the rest ~0). -experiment: llm_curriculum_v5_g2 -kind: llm_curriculum -base_model: Qwen/Qwen2.5-1.5B -seed: 1 -families: [mnli, arc, hellaswag, squad, boolq, winogrande] -lineages: 1 -generations: 6 -arms: [isolated] -baselines: [] -n_new: 300 -n_replay: 0 -n_test: 60 -n_val: 20 -epochs: 3 -lr: 1.0e-4 -max_new_tokens: 48 -batch_size: 24 -train_batch_size: 2 -train_max_len: 512 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_curriculum_v5_g2} diff --git a/configs/llm/curriculum_v5_s1.yaml b/configs/llm/curriculum_v5_s1.yaml index f034598..38a2509 100644 --- a/configs/llm/curriculum_v5_s1.yaml +++ b/configs/llm/curriculum_v5_s1.yaml @@ -1,4 +1,4 @@ -# The curriculum society campaign, seed 1 (prereg v4, v5 curriculum — tasks/prereg-llm-society-v4.md). +# The curriculum society campaign, seed 1 (prereg v4, v5 curriculum — tasks/prereg-llm-society-v4.md on the dev branch). # Runs all four arms then the three matched-budget baselines. The PBS array (hpc/llm_curriculum.pbs) # derives one (seed, arm) config per element from this template for seeds 2-3. # diff --git a/configs/llm/curriculum_v5_stop3.yaml b/configs/llm/curriculum_v5_stop3.yaml index 2218af6..ce7c210 100644 --- a/configs/llm/curriculum_v5_stop3.yaml +++ b/configs/llm/curriculum_v5_stop3.yaml @@ -2,7 +2,7 @@ # `society` arm except that recombination is switched off from generation 3 onward (`merge_until: 3`): # obligate merging at generations 0-2, none afterwards. This is the fixed "merge early, then stop" # schedule the veto arm must be compared against, because in the seed-1 veto run lineages declined -# 1/3 of merges at generations 0-2 and 3/3 at 3-5. Pre-registered readout: tasks/prereg-llm-society-v4.md §8g. +# 1/3 of merges at generations 0-2 and 3/3 at 3-5. Pre-registered readout: tasks/prereg-llm-society-v4.md on the dev branch §8g. experiment: llm_curriculum_v5_stop3 kind: llm_curriculum base_model: Qwen/Qwen2.5-1.5B diff --git a/configs/llm/directed.yaml b/configs/llm/directed.yaml deleted file mode 100644 index 6500f4c..0000000 --- a/configs/llm/directed.yaml +++ /dev/null @@ -1,25 +0,0 @@ -experiment: llm_directed -kind: llm_directed -seed: 1 -n_replicates: 1 - -# Layer 2 / LLM — DIRECTED SEX (E10) in weight space: breed many recombinant offspring and keep the -# fittest. llm_moe showed fusion can COMPOSE beyond the parents (7B) but the right blend is unknown and -# base-dependent, while pure routing is capped at the best parent. Directed sex resolves both: sample a -# population of weighted merges of the specialists, score each on a held-out VALIDATION split with the -# verifier (grounding), and select the winner — reported on a fresh TEST split (no selection-on-test -# leakage). Two objectives: best-overall and best-worst-family (raw capability vs the balanced -# generalist). Prediction: directed selection beats the single a-priori soup and every parent, at -# either scale. Falsifier: directed offspring ≤ uniform soup on test. - -base_model: Qwen/Qwen2.5-0.5B-Instruct # reuses the cached llm_merge specialists -families: [lists, strings, arith] -n_train: 700 # only if cached specialists are absent -n_val: 80 # held-out split the verifier selects on -n_test: 100 # fresh split winners are reported on -n_candidates: 16 # offspring population size -concentration: 0.5 # Dirichlet concentration (<1 = sparser, specialist-dominant blends) -epochs: 3 -lora: {r: 16, alpha: 32} - -output: {dir: results/llm_directed} diff --git a/configs/llm/merge.yaml b/configs/llm/merge.yaml deleted file mode 100644 index 845a142..0000000 --- a/configs/llm/merge.yaml +++ /dev/null @@ -1,22 +0,0 @@ -experiment: llm_merge -kind: llm_merge -seed: 1 -n_replicates: 1 - -# (Layer 2 / LLM prototype — blueprint C2/C4, the real-LLM image of E8): recombine specialist LLMs. -# Train one LoRA specialist per DISJOINT task family on a small open-weight base, then compare the -# base, each specialist, and their weight-space MERGES (soup = averaged deltas; ties = sign-reconciled -# union) on a held-out mixed test set. Tasks are procedurally generated and exactly verified (the -# "reality that says no"), and deliberately hard so specialists are decorrelated. Expect (per E8): the -# recombined model beats any single specialist overall AND is competent across ALL families -# (worst-family accuracy), which no single parent is. Falsifier: a single specialist matches the merge. - -base_model: Qwen/Qwen2.5-0.5B-Instruct # Apache-2.0; ~1 GB, fits 16 GB with room to spare -families: [lists, strings, arith] -n_train: 700 -n_test: 100 -epochs: 3 -lora: {r: 16, alpha: 32} -merges: [soup, ties] - -output: {dir: results/llm_merge} diff --git a/configs/llm/moe.yaml b/configs/llm/moe.yaml deleted file mode 100644 index 0cd7597..0000000 --- a/configs/llm/moe.yaml +++ /dev/null @@ -1,25 +0,0 @@ -experiment: llm_moe -kind: llm_moe -seed: 1 -n_replicates: 1 - -# Layer 2 / LLM — module-level, UNION-PRESERVING recombination (the real-weight image of E8's *max*). -# Reuses the specialist adapters trained by configs/llm/merge.yaml (models/llm/spec_*) and contrasts -# two families of recombination operator on the same held-out mixed test set: -# FUSION (blend the deltas): soup = mean(Δ_k); ties = sign-reconciled union. -# UNION (never average): moe_oracle / moe_learned = keep every specialist intact and ROUTE each -# prompt to one (MoE-over-experts); max_merge = per-module winner-take-all. -# Prediction (E8, "merge don't average"): union beats fusion exactly where fusion DILUTES — pronounced -# at a weak base (0.5B), narrowing once a capable base lets fusion compose (7B). Falsifier: fusion -# matches or beats the routing ceiling (moe_oracle) at 0.5B, i.e. averaging never dilutes. - -base_model: Qwen/Qwen2.5-0.5B-Instruct # reuses the same cached specialists as llm_merge -families: [lists, strings, arith] -n_train: 700 # only used if the cached specialists are absent -n_test: 100 -n_route: 32 # labelled prompts per family for the learned router's centroids -epochs: 3 -lora: {r: 16, alpha: 32} -operators: [soup, ties, moe_oracle, moe_learned, max_merge] - -output: {dir: results/llm_moe} diff --git a/configs/llm/moe_hard.yaml b/configs/llm/moe_hard.yaml deleted file mode 100644 index a2991cf..0000000 --- a/configs/llm/moe_hard.yaml +++ /dev/null @@ -1,21 +0,0 @@ -experiment: llm_moe_hard -kind: llm_moe -seed: 1 -n_replicates: 1 - -# Local 0.5B SMOKE for the hard benchmark: confirms the harder task variant (hard: true) runs -# end-to-end — trains fresh hard specialists (cached as spec_*_hard), the verifier still scores them, -# routing/fusion operators execute. 0.5B will score low on these harder tasks (that is expected; the -# calibrated comparison is the 7B moe_hard_hpc run). Kept small for speed. - -base_model: Qwen/Qwen2.5-0.5B-Instruct -hard: true -families: [lists, strings, arith] -n_train: 400 -n_test: 80 -n_route: 32 -epochs: 3 -lora: {r: 16, alpha: 32} -operators: [soup, ties, moe_oracle, moe_learned, max_merge] - -output: {dir: results/llm_moe_hard} diff --git a/configs/llm/society.yaml b/configs/llm/society.yaml deleted file mode 100644 index dc83115..0000000 --- a/configs/llm/society.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Pilot: full vs no_grounding, one seed — the decision gate before the CX3 campaign. -experiment: llm_society -kind: llm_society -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -agents: 6 -generations: 8 -arms: [full, no_grounding] -g: 0.5 -lam: 0.3 -n_test: 40 -n_val: 30 -n_conf: 90 -n_inherit: 600 -n_candidates: 6 -elitism: 1 -n_parents: 3 -epochs: 3 -spec_train: 600 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society} diff --git a/configs/llm/society_campaign_s1.yaml b/configs/llm/society_campaign_s1.yaml deleted file mode 100644 index 25a5867..0000000 --- a/configs/llm/society_campaign_s1.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# The society campaign: 4-arm ablation, one seed per job (founders trained inline, no cache races). -# Seed 1 runs locally (A4000, queue insurance); seeds 2-4 on CX3 (hpc/llm_society.pbs, L40S). -experiment: llm_society_campaign -kind: llm_society -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -agents: 8 -generations: 10 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.5 -lam: 0.3 -n_test: 40 -n_val: 30 -n_conf: 90 -n_inherit: 600 -n_candidates: 6 -elitism: 1 -n_parents: 4 -epochs: 3 -spec_train: 600 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_campaign/s1} diff --git a/configs/llm/society_campaign_s2.yaml b/configs/llm/society_campaign_s2.yaml deleted file mode 100644 index c536e5f..0000000 --- a/configs/llm/society_campaign_s2.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# The society campaign: 4-arm ablation, one seed per job (founders trained inline, no cache races). -# Seed 1 runs locally (A4000, queue insurance); seeds 2-4 on CX3 (hpc/llm_society.pbs, L40S). -experiment: llm_society_campaign -kind: llm_society -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 2 -agents: 8 -generations: 10 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.5 -lam: 0.3 -n_test: 40 -n_val: 30 -n_conf: 90 -n_inherit: 600 -n_candidates: 6 -elitism: 1 -n_parents: 4 -epochs: 3 -spec_train: 600 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_campaign/s2} diff --git a/configs/llm/society_campaign_s3.yaml b/configs/llm/society_campaign_s3.yaml deleted file mode 100644 index 0f04b11..0000000 --- a/configs/llm/society_campaign_s3.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# The society campaign: 4-arm ablation, one seed per job (founders trained inline, no cache races). -# Seed 1 runs locally (A4000, queue insurance); seeds 2-4 on CX3 (hpc/llm_society.pbs, L40S). -experiment: llm_society_campaign -kind: llm_society -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 3 -agents: 8 -generations: 10 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.5 -lam: 0.3 -n_test: 40 -n_val: 30 -n_conf: 90 -n_inherit: 600 -n_candidates: 6 -elitism: 1 -n_parents: 4 -epochs: 3 -spec_train: 600 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_campaign/s3} diff --git a/configs/llm/society_campaign_s4.yaml b/configs/llm/society_campaign_s4.yaml deleted file mode 100644 index 7e265b9..0000000 --- a/configs/llm/society_campaign_s4.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# The society campaign: 4-arm ablation, one seed per job (founders trained inline, no cache races). -# Seed 1 runs locally (A4000, queue insurance); seeds 2-4 on CX3 (hpc/llm_society.pbs, L40S). -experiment: llm_society_campaign -kind: llm_society -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 4 -agents: 8 -generations: 10 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.5 -lam: 0.3 -n_test: 40 -n_val: 30 -n_conf: 90 -n_inherit: 600 -n_candidates: 6 -elitism: 1 -n_parents: 4 -epochs: 3 -spec_train: 600 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_campaign/s4} diff --git a/configs/llm/society_smoke.yaml b/configs/llm/society_smoke.yaml deleted file mode 100644 index 6451511..0000000 --- a/configs/llm/society_smoke.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Smoke test for the LLM society loop — tiny everything; ~15 min on a 16 GB GPU. -experiment: llm_society_smoke -kind: llm_society -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -agents: 4 -generations: 2 -arms: [full] -g: 0.5 -lam: 0.3 -n_test: 15 # per family -n_val: 10 -n_conf: 30 # total -n_inherit: 90 # total -n_candidates: 4 -epochs: 2 -spec_train: 150 -spec_epochs: 2 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_smoke} diff --git a/configs/llm/society_v2_calib_a.yaml b/configs/llm/society_v2_calib_a.yaml deleted file mode 100644 index 5243ca7..0000000 --- a/configs/llm/society_v2_calib_a.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Calibration stage A (prereg §4, gates C1a-c + C4): every candidate family's base / specialist -# accuracy, specialist confidence AUC, pairwise functional conflict, gen-0 behavioural distance. -# GG reviews the printed table and fixes the 12-family set in society_v2_calib_b.yaml + the campaign. -# ~1 h on a 16 GB GPU (17 specialists at 600 x 3 epochs, cached and reused by stage B). -experiment: llm_society_v2_calib_a -kind: llm_society_calib -stage: families -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: [lists, strings, arith, roman, binary, sortletters, setops, numtheory, mixedtoken, - caesar, vectors, progression, charfreq, digits, liststats, alphabet, prime] -n_test: 100 -n_probe: 10 -spec_train: 600 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2_calib_a} diff --git a/configs/llm/society_v2_calib_a2.yaml b/configs/llm/society_v2_calib_a2.yaml deleted file mode 100644 index fa37982..0000000 --- a/configs/llm/society_v2_calib_a2.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Calibration stage A, second pass (prereg §4 amendments, 2026-09-07): the first pass left only six -# families in band. Changes, each recorded in the prereg: founder budget 600 -> 1200 x 3 (uniform); -# roman / binary / prime prompt spaces enlarged; three new learnable candidates (wordlen, -# lettercount, sumeven); the specialist UPPER bound relaxed 0.90 -> 1.00 (the headroom argument it -# encoded concerns fusion composing to a ceiling, not transmission/assembly). Families whose -# specialist scored <= 0.20 in pass 1 (sortletters, caesar, progression) are not re-trained. -experiment: llm_society_v2_calib_a2 -kind: llm_society_calib -stage: families -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: [lists, strings, arith, roman, binary, setops, numtheory, mixedtoken, vectors, charfreq, - digits, liststats, alphabet, prime, wordlen, lettercount, sumeven] -spec_hi: 1.0 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2_calib_a2} diff --git a/configs/llm/society_v2_calib_b.yaml b/configs/llm/society_v2_calib_b.yaml deleted file mode 100644 index ee9c69f..0000000 --- a/configs/llm/society_v2_calib_b.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Calibration stage B (prereg §4, gates C2 / C3 / C5) over the 12 families chosen from stage A. -# Run three times with stage: transmission | cross | consensus (see Makefile `llm-society-calib`). -# Families = stage-A pass-2 option 1 (L=9, gate 0.41; prereg §4a) — pending GG's go. -experiment: llm_society_v2_calib_b -kind: llm_society_calib -stage: transmission -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman] -# C2: examples-per-family k and epochs to sweep; probe_families are the three whose retention is -# measured (spread across answer types: list / word / int). -probe_families: [setops, alphabet, digits] # list / letter / integer answers -ks: [25, 50, 100, 150] -epochs_grid: [2, 3] -# C3: the two-founder cross (union-distil vs best-of-6 linear-merge-distil) -cross: [setops, alphabet] -k_inherit: 100 -epochs: 3 -n_candidates: 6 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2_calib_b} diff --git a/configs/llm/society_v2_calib_c2b.yaml b/configs/llm/society_v2_calib_c2b.yaml deleted file mode 100644 index de10147..0000000 --- a/configs/llm/society_v2_calib_c2b.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# C2b (prereg §4a amendment under review): confidence-gated inheritance. The pre-registered C2 failed -# (retention ≤ 0.81 at k ≤ 150 despite competent supply): the child fits eight families of confident -# garbage alongside the one competent family. Here the child learns only prompts its source is -# confident on (verifier-free). Measures retention by gate τ, the Youden τ* separating own- from -# off-family confidence, and the off-family harm of ungated inheritance. -experiment: llm_society_v2_calib_b_transmission_conf -kind: llm_society_calib -stage: transmission_conf -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman] -probe_families: [setops, alphabet, digits] -k_pool: 300 # prompts per family answered by the source (2700 total); gate keeps a subset -taus: [0.5, 0.7, 0.85] -epochs: 3 -n_test: 100 -spec_train: 1200 -spec_epochs: 3 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2_calib_b_transmission_conf} diff --git a/configs/llm/society_v2_calib_c3_gated.yaml b/configs/llm/society_v2_calib_c3_gated.yaml deleted file mode 100644 index 2db5e27..0000000 --- a/configs/llm/society_v2_calib_c3_gated.yaml +++ /dev/null @@ -1,43 +0,0 @@ -experiment: llm_society_v2_calib_b_cross_gated -kind: llm_society_calib -stage: cross -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: -- strings -- setops -- numtheory -- mixedtoken -- digits -- alphabet -- prime -- wordlen -- roman -probe_families: -- setops -- alphabet -- digits -ks: -- 25 -- 50 -- 100 -- 150 -epochs_grid: -- 2 -- 3 -cross: -- setops -- alphabet -k_inherit: 300 -epochs: 3 -n_candidates: 6 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: - r: 16 - alpha: 32 -output: - dir: results/llm_society_v2_calib_b_cross_gated -conf_gate: 0.5 diff --git a/configs/llm/society_v2_calib_c3b_e6.yaml b/configs/llm/society_v2_calib_c3b_e6.yaml deleted file mode 100644 index 4d55b15..0000000 --- a/configs/llm/society_v2_calib_c3b_e6.yaml +++ /dev/null @@ -1,43 +0,0 @@ -experiment: llm_society_v2_calib_c3b_e6 -kind: llm_society_calib -stage: cross -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: -- strings -- setops -- numtheory -- mixedtoken -- digits -- alphabet -- prime -- wordlen -- roman -probe_families: -- setops -- alphabet -- digits -ks: -- 25 -- 50 -- 100 -- 150 -epochs_grid: -- 2 -- 3 -cross: -- setops -- alphabet -k_inherit: 300 -epochs: 6 -n_candidates: 6 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: - r: 16 - alpha: 32 -output: - dir: results/llm_society_v2_calib_c3b_e6 -conf_gate: 0.5 diff --git a/configs/llm/society_v2_calib_c3b_r64.yaml b/configs/llm/society_v2_calib_c3b_r64.yaml deleted file mode 100644 index 8a0bcc1..0000000 --- a/configs/llm/society_v2_calib_c3b_r64.yaml +++ /dev/null @@ -1,43 +0,0 @@ -experiment: llm_society_v2_calib_c3b_r64 -kind: llm_society_calib -stage: cross -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: -- strings -- setops -- numtheory -- mixedtoken -- digits -- alphabet -- prime -- wordlen -- roman -probe_families: -- setops -- alphabet -- digits -ks: -- 25 -- 50 -- 100 -- 150 -epochs_grid: -- 2 -- 3 -cross: -- setops -- alphabet -k_inherit: 300 -epochs: 3 -n_candidates: 6 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: - r: 64 - alpha: 128 -output: - dir: results/llm_society_v2_calib_c3b_r64 -conf_gate: 0.5 diff --git a/configs/llm/society_v2_calib_c3b_r64e6.yaml b/configs/llm/society_v2_calib_c3b_r64e6.yaml deleted file mode 100644 index 0b533ad..0000000 --- a/configs/llm/society_v2_calib_c3b_r64e6.yaml +++ /dev/null @@ -1,43 +0,0 @@ -experiment: llm_society_v2_calib_c3b_r64e6 -kind: llm_society_calib -stage: cross -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: -- strings -- setops -- numtheory -- mixedtoken -- digits -- alphabet -- prime -- wordlen -- roman -probe_families: -- setops -- alphabet -- digits -ks: -- 25 -- 50 -- 100 -- 150 -epochs_grid: -- 2 -- 3 -cross: -- setops -- alphabet -k_inherit: 300 -epochs: 6 -n_candidates: 6 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: - r: 64 - alpha: 128 -output: - dir: results/llm_society_v2_calib_c3b_r64e6 -conf_gate: 0.5 diff --git a/configs/llm/society_v2_calib_c3c_t85e3.yaml b/configs/llm/society_v2_calib_c3c_t85e3.yaml deleted file mode 100644 index d0b6626..0000000 --- a/configs/llm/society_v2_calib_c3c_t85e3.yaml +++ /dev/null @@ -1,43 +0,0 @@ -experiment: llm_society_v2_calib_c3c_t85e3 -kind: llm_society_calib -stage: cross -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: -- strings -- setops -- numtheory -- mixedtoken -- digits -- alphabet -- prime -- wordlen -- roman -probe_families: -- setops -- alphabet -- digits -ks: -- 25 -- 50 -- 100 -- 150 -epochs_grid: -- 2 -- 3 -cross: -- setops -- alphabet -k_inherit: 300 -epochs: 3 -n_candidates: 6 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: - r: 16 - alpha: 32 -output: - dir: results/llm_society_v2_calib_c3c_t85e3 -conf_gate: 0.85 diff --git a/configs/llm/society_v2_calib_c3c_t85e6.yaml b/configs/llm/society_v2_calib_c3c_t85e6.yaml deleted file mode 100644 index 9199e15..0000000 --- a/configs/llm/society_v2_calib_c3c_t85e6.yaml +++ /dev/null @@ -1,43 +0,0 @@ -experiment: llm_society_v2_calib_c3c_t85e6 -kind: llm_society_calib -stage: cross -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: -- strings -- setops -- numtheory -- mixedtoken -- digits -- alphabet -- prime -- wordlen -- roman -probe_families: -- setops -- alphabet -- digits -ks: -- 25 -- 50 -- 100 -- 150 -epochs_grid: -- 2 -- 3 -cross: -- setops -- alphabet -k_inherit: 300 -epochs: 6 -n_candidates: 6 -n_test: 100 -n_probe: 10 -spec_train: 1200 -spec_epochs: 3 -lora: - r: 16 - alpha: 32 -output: - dir: results/llm_society_v2_calib_c3c_t85e6 -conf_gate: 0.85 diff --git a/configs/llm/society_v2_s1.yaml b/configs/llm/society_v2_s1.yaml deleted file mode 100644 index 3861591..0000000 --- a/configs/llm/society_v2_s1.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# v2 society campaign, seed 1 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array -# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly -# runs all four arms sequentially (the local-GPU hedge). -# -# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission: -# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET -# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET -experiment: llm_society_v2 -kind: llm_society_v2 -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman] -agents: 9 -generations: 12 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.85 -lam: 0.3 -max_mate_use: 2 -n_test: 27 # per family -> 243 overall (SE 0.032) -n_val: 13 # per family -> 117 (selection signal) -n_conf: 13 # per family, fresh each generation -k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver) -epochs: 3 # child SFT epochs (C2b) -conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs) -spec_train: 1200 -spec_epochs: 3 -resume: true -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2/s1} diff --git a/configs/llm/society_v2_s2.yaml b/configs/llm/society_v2_s2.yaml deleted file mode 100644 index 06f7c57..0000000 --- a/configs/llm/society_v2_s2.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# v2 society campaign, seed 2 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array -# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly -# runs all four arms sequentially (the local-GPU hedge). -# -# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission: -# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET -# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET -experiment: llm_society_v2 -kind: llm_society_v2 -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 2 -families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman] -agents: 9 -generations: 12 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.85 -lam: 0.3 -max_mate_use: 2 -n_test: 27 # per family -> 243 overall (SE 0.032) -n_val: 13 # per family -> 117 (selection signal) -n_conf: 13 # per family, fresh each generation -k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver) -epochs: 3 # child SFT epochs (C2b) -conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs) -spec_train: 1200 -spec_epochs: 3 -resume: true -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2/s2} diff --git a/configs/llm/society_v2_s3.yaml b/configs/llm/society_v2_s3.yaml deleted file mode 100644 index 27fa52c..0000000 --- a/configs/llm/society_v2_s3.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# v2 society campaign, seed 3 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array -# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly -# runs all four arms sequentially (the local-GPU hedge). -# -# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission: -# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET -# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET -experiment: llm_society_v2 -kind: llm_society_v2 -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 3 -families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman] -agents: 9 -generations: 12 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.85 -lam: 0.3 -max_mate_use: 2 -n_test: 27 # per family -> 243 overall (SE 0.032) -n_val: 13 # per family -> 117 (selection signal) -n_conf: 13 # per family, fresh each generation -k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver) -epochs: 3 # child SFT epochs (C2b) -conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs) -spec_train: 1200 -spec_epochs: 3 -resume: true -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2/s3} diff --git a/configs/llm/society_v2_s4.yaml b/configs/llm/society_v2_s4.yaml deleted file mode 100644 index 8721a87..0000000 --- a/configs/llm/society_v2_s4.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# v2 society campaign, seed 4 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array -# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly -# runs all four arms sequentially (the local-GPU hedge). -# -# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission: -# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET -# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET -experiment: llm_society_v2 -kind: llm_society_v2 -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 4 -families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman] -agents: 9 -generations: 12 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.85 -lam: 0.3 -max_mate_use: 2 -n_test: 27 # per family -> 243 overall (SE 0.032) -n_val: 13 # per family -> 117 (selection signal) -n_conf: 13 # per family, fresh each generation -k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver) -epochs: 3 # child SFT epochs (C2b) -conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs) -spec_train: 1200 -spec_epochs: 3 -resume: true -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2/s4} diff --git a/configs/llm/society_v2_smoke.yaml b/configs/llm/society_v2_smoke.yaml deleted file mode 100644 index 6588231..0000000 --- a/configs/llm/society_v2_smoke.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Smoke test for the v2 society loop: tiny everything, all four arms, resume exercised by the -# per-generation checkpoint. ~20 min on a 16 GB GPU. Exit 0 + figure renders = pass. -experiment: llm_society_v2_smoke -kind: llm_society_v2 -base_model: Qwen/Qwen2.5-0.5B-Instruct -seed: 1 -families: [lists, roman, binary, setops] -agents: 4 -generations: 2 -arms: [full, no_grounding, no_sex, no_diversity] -g: 0.85 -lam: 0.3 -n_test: 8 # per family -n_val: 5 # per family -n_conf: 5 # per family, fresh each generation -k_inherit: 20 # per family -epochs: 2 -spec_train: 120 -spec_epochs: 2 -lora: {r: 16, alpha: 32} -output: {dir: results/llm_society_v2_smoke} diff --git a/configs/neural/figS1_architectures.yaml b/configs/neural/figS1_architectures.yaml index ae40f45..dc3dda9 100644 --- a/configs/neural/figS1_architectures.yaml +++ b/configs/neural/figS1_architectures.yaml @@ -11,7 +11,7 @@ n_replicates: 5 # grounding arrests it. Falsifier: the signs appear only for the histogram -> real neural # inductive biases break the Wright-Fisher mapping. (The sequence VAE is implemented but # excluded here: it does not clear the gen-0 fidelity gate on the Zipf-codeword task, so -# its collapse would be confounded with underfitting; see tasks/todo.md.) +# its collapse would be confounded with underfitting.) generations: 22 synthetic: diff --git a/figures/plot_collapse_null.py b/figures/plot_collapse_null.py deleted file mode 100644 index 257d29d..0000000 --- a/figures/plot_collapse_null.py +++ /dev/null @@ -1,64 +0,0 @@ -"""E1 figure: reproduce collapse (null model). - -Shows tail-first collapse under pure neutral drift: geometric H decay matching the -analytic law, tail items dying faster than head items, support -> 1 and forward-KL -diverging. Usage: python figures/plot_collapse_null.py [results/collapse_null] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np - -sys.path.insert(0, str(Path(__file__).parent)) -from _figlib import load_bundle, mean_ci, savefig # noqa: E402 - - -def main(results_dir: str = "results/collapse_null") -> None: - df, cfg = load_bundle(results_dir) - n = cfg["dynamics"]["n"] - - gens, Hmean, Hci = mean_ci(df, "generation", "heterozygosity") - H0 = Hmean[0] - analytic = H0 * (1.0 - 1.0 / n) ** gens - - m = df.groupby("generation").mean(numeric_only=True) - - fig, axes = plt.subplots(1, 3, figsize=(15, 4.2)) - - # Panel 1: heterozygosity decay vs the analytic law - ax = axes[0] - ax.plot(gens, Hmean, color="#1f77b4", label="simulation (mean)") - ax.fill_between(gens, Hmean - Hci, Hmean + Hci, color="#1f77b4", alpha=0.25) - ax.plot(gens, analytic, "k--", label=r"$H_0(1-1/n)^t$") - ax.set(xlabel="generation", ylabel="heterozygosity $H$", - title=f"Geometric decay (n={n})") - ax.legend(frameon=False) - - # Panel 2: tail-first — fraction of tail vs head items still alive - ax = axes[1] - ax.plot(m.index, m["tail_frac_alive"], color="#d62728", label="tail items alive") - ax.plot(m.index, m["head_frac_alive"], color="#2ca02c", label="head items alive") - ax.set(xlabel="generation", ylabel="fraction of items surviving", - title="Tail dies first", yscale="log") - ax.legend(frameon=False) - - # Panel 3: support collapse and KL divergence - ax = axes[2] - ax.plot(m.index, m["support_size"], color="#9467bd", label="support size") - ax.set(xlabel="generation", ylabel="support size", yscale="log", title="Collapse") - ax2 = ax.twinx() - ax2.plot(m.index, m["forward_kl"], color="#ff7f0e", label="forward KL") - ax2.set_ylabel(r"forward KL $D_{KL}(p^*\,\|\,p_t)$", color="#ff7f0e") - ax.legend(loc="center right", frameon=False) - - fig.suptitle("E1 — distillation without grounding collapses, tail first", y=1.02) - fig.tight_layout() - savefig(fig, results_dir, "collapse_null") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/plot_fig5_speciation_bdm.py b/figures/plot_fig5_speciation_bdm.py index 396c55b..318674f 100644 --- a/figures/plot_fig5_speciation_bdm.py +++ b/figures/plot_fig5_speciation_bdm.py @@ -31,11 +31,10 @@ def _agg(df, keys, value): def main() -> None: bdm, _ = load_bundle("results/fig5_speciation_bdm") - nk, _ = load_bundle("results/speciation_bdm_nk") rhos = sorted(bdm["rho"].unique()) colors = plt.cm.viridis(np.linspace(0.15, 0.85, len(rhos))) - fig, axes = plt.subplots(1, 3, figsize=(16, 5)) + fig, axes = plt.subplots(1, 2, figsize=(11, 5)) # Panel A: hybrid fitness vs divergence, per epistasis density, + parent fitness. ax = axes[0] @@ -60,19 +59,11 @@ def main() -> None: title="The isolation cliff moves to lower divergence\nas epistasis density rises") ax.legend(frameon=False, fontsize=9, title="epistasis density") - # Panel C: NK epistasis wedge — recombination gain vs ruggedness K. - ax = axes[2] - g = _agg(nk, "K", "offspring_minus_parent") - ax.axhline(0, color="#999", lw=0.8, ls=":") - ax.plot(g["K"], g["mean"], "-o", color="#d62728", lw=2) - ax.fill_between(g["K"], g["mean"] - g["se"], g["mean"] + g["se"], color="#d62728", alpha=0.15) - ax.set(xlabel="landscape ruggedness $K$ (epistasis)", ylabel="recombination gain\n(hybrid − worse parent)", - title="Epistasis wedge: recombining adapted parents\nflips from gain to loss as ruggedness grows") - fig.suptitle("E12 — model speciation: when two diverged models are too incompatible to merge", + fig.suptitle("Model speciation: when two diverged models are too incompatible to merge", y=1.02, fontsize=13) fig.tight_layout() - savefig(fig, "results/fig5_speciation_bdm", "E12") + savefig(fig, "results/fig5_speciation_bdm", "fig5_speciation_bdm") if __name__ == "__main__": diff --git a/figures/plot_figS13_mating_breadth.py b/figures/plot_figS13_mating_breadth.py index ca75f46..da8ff41 100644 --- a/figures/plot_figS13_mating_breadth.py +++ b/figures/plot_figS13_mating_breadth.py @@ -60,7 +60,7 @@ def main() -> None: fig.tight_layout() letter_axes(fig) - savefig(fig, "results/figS13_mating_breadth", "E14") + savefig(fig, "results/figS13_mating_breadth", "figS13_mating_breadth") if __name__ == "__main__": diff --git a/figures/plot_llm_compose.py b/figures/plot_llm_compose.py deleted file mode 100644 index 5a67dcd..0000000 --- a/figures/plot_llm_compose.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Composition-decay figure (prereg v3 §8) — written before unblinding. - -(A) Composition **surplus** S_t = composed − best single parent, per arm over generations, with the - zero line: the vertical claim, and whether it survives inheritance. -(B) Own-skill retention q_t per lineage (math on GSM8K, code on MBPP), dry vs grounded — the - denominators of the prediction. -(C) rho_t, the behavioural correlation between the two lineages: the mechanism, if it rises. -(D) Observed composed accuracy against the framework's forecast Ĉ_t (one free scale, fixed at - generation 0) — H3, the paper's predictive claim, drawn as a line the data can miss. - -Reads only committed bundles: one bundle directory, or a campaign directory of ``s*/`` bundles. -Usage: python figures/plot_llm_compose.py [results/llm_compose] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -sys.path.insert(0, str(Path(__file__).parent)) -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) -from _figlib import mean_ci, savefig # noqa: E402 -from llm.compose import predicted_composition # noqa: E402 - -ARMS = [("dry", "#d62728", "dry · blending operator"), - ("grounded", "#2ca02c", "grounded (g = 0.10) · blending"), - ("dry_cat", "#1f77b4", "dry · union operator (cat)")] - - -def load_any(results_dir: Path) -> pd.DataFrame: - if (results_dir / "results.parquet").exists(): - paths = [results_dir] - else: - paths = sorted(p.parent for p in results_dir.glob("*/results.parquet")) - if not paths: - raise SystemExit(f"no results.parquet under {results_dir}") - return pd.concat([pd.read_parquet(p / "results.parquet") for p in paths], ignore_index=True) - - -def series(df: pd.DataFrame, arm: str, metric: str) -> pd.DataFrame: - return df[(df.arm == arm) & (df.metric == metric)][["seed", "generation", "value"]] - - -def main(results_dir: str = "results/llm_compose") -> None: - rd = Path(results_dir) - df = load_any(rd) - arms = [a for a in ARMS if a[0] in set(df.arm.unique())] - n_seeds = df.seed.nunique() - fig, ax = plt.subplots(1, 4, figsize=(21, 4.6)) - - # (A) surplus - for arm, color, label in arms: - s = series(df, arm, "surplus") - if s.empty: - continue - x, m, h = mean_ci(s, "generation", "value") - ax[0].plot(x, m, "-o", color=color, label=label, lw=2, ms=4) - ax[0].fill_between(x, m - h, m + h, color=color, alpha=0.15, lw=0) - ax[0].axhline(0, color="k", lw=1, ls="--") - ax[0].set_title("(A) composition surplus\ncomposed − best single parent", fontsize=10) - ax[0].set_xlabel("generation"); ax[0].set_ylabel("surplus"); ax[0].legend(fontsize=8) - - # (B) own-skill retention - for arm, color, _ in arms: - for metric, ls in (("q_math", "-"), ("q_code", ":")): - s = series(df, arm, metric) - if s.empty: - continue - x, m, _h = mean_ci(s, "generation", "value") - ax[1].plot(x, m, ls, color=color, lw=2, - label=f"{arm} · {metric.split('_')[1]}" if arm != "dry_linear" else None) - ax[1].set_title("(B) own-skill retention q_t\nsolid math (GSM8K), dotted code (MBPP)", fontsize=10) - ax[1].set_xlabel("generation"); ax[1].set_ylabel("accuracy"); ax[1].legend(fontsize=8) - - # (C) rho - for arm, color, label in arms: - s = series(df, arm, "rho_behav") - if s.empty: - continue - x, m, h = mean_ci(s, "generation", "value") - ax[2].plot(x, m, "-o", color=color, label=label, lw=2, ms=4) - ax[2].fill_between(x, m - h, m + h, color=color, alpha=0.15, lw=0) - ax[2].set_title("(C) lineage correlation ρ_t\n(agreement on a shared probe)", fontsize=10) - ax[2].set_xlabel("generation"); ax[2].set_ylabel("ρ"); ax[2].legend(fontsize=8) - - # (D) observed vs predicted, dry arm - for arm, color, label in arms: - obs = series(df, arm, "composed_acc").groupby("generation").value.mean() - qm = series(df, arm, "q_math").groupby("generation").value.mean() - qc = series(df, arm, "q_code").groupby("generation").value.mean() - rho = series(df, arm, "rho_behav").groupby("generation").value.mean() - if obs.empty or len(obs) < 2: - continue - pred = predicted_composition(qm.to_numpy(), qc.to_numpy(), rho.to_numpy(), float(obs.iloc[0])) - ax[3].plot(obs.index, obs.to_numpy(), "-o", color=color, lw=2, ms=4, label=f"{label} observed") - ax[3].plot(obs.index, pred, "--", color=color, lw=1.5, alpha=0.8, - label=f"{label} predicted Ĉ") - ax[3].set_title("(D) H3: observed vs the closed form\nĈ = c₀·q_math·q_code·(1−ρ)/(1−ρ₀)", fontsize=10) - ax[3].set_xlabel("generation"); ax[3].set_ylabel("composed accuracy"); ax[3].legend(fontsize=7) - - fig.suptitle(f"llm_compose — does a composed capability survive inheritance? " - f"({n_seeds} seed{'s' if n_seeds != 1 else ''}, mean ± 95% CI)", y=1.03) - fig.tight_layout() - savefig(fig, rd, "llm_compose") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/plot_llm_directed.py b/figures/plot_llm_directed.py deleted file mode 100644 index cde5534..0000000 --- a/figures/plot_llm_directed.py +++ /dev/null @@ -1,94 +0,0 @@ -"""llm_directed figure — directed sex in weight space (E10): breed offspring, select the fittest. - -A population of recombinant offspring (weighted merges of the specialists) is scored on a held-out -validation split by the verifier; the winners (best-overall, best-worst-family) are reported on a -fresh test split against the uniform-soup blend and the best single specialist. Two panels: (A) -per-family accuracy — directed offspring (green) vs soup (orange) vs specialists (blue); (B) overall -vs worst-family, with the best-specialist bar as the parent ceiling. The suptitle reports whether -directed selection beat the single a-priori soup. Reads only the committed bundle. - -Usage: python figures/plot_llm_directed.py [results/llm_directed] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np - -sys.path.insert(0, str(Path(__file__).parent)) -from _figlib import load_seed_bundles, savefig # noqa: E402 - -_FAMS = ["lists", "strings", "arith"] -_DIRECTED = {"directed_overall": "directed:overall", "directed_balanced": "directed:balanced"} - - -def _acc(df, model, metric): - r = df[(df["model"] == model) & (df["metric"] == metric)]["accuracy"] - return float(r.mean()) if len(r) else float("nan") - - -def main(results_dir: str = "results/llm_directed") -> None: - df, cfg = load_seed_bundles(results_dir) # seed-mean when the bundle has s{seed}/ sub-bundles - present = set(df["model"].unique()) - specialists = sorted(m for m in present if m.startswith("spec_")) - directed = [m for m in _DIRECTED if m in present] - soup = ["merge_soup"] if "merge_soup" in present else [] - models = ["base"] + specialists + soup + directed - labels = {"base": "base", **{s: s.replace("spec_", "spec:") for s in specialists}, - "merge_soup": "soup (uniform)", **_DIRECTED} - colors = {"base": "#7f7f7f", **{s: "#1f77b4" for s in specialists}, - "merge_soup": "#ff7f0e", **{m: "#2ca02c" for m in directed}} - - fig, axes = plt.subplots(1, 2, figsize=(13, 5)) - - # Panel A: per-family accuracy. - ax = axes[0] - x = np.arange(len(_FAMS)) - w = 0.8 / len(models) - for i, mdl in enumerate(models): - vals = [_acc(df, mdl, f) for f in _FAMS] - ax.bar(x + (i - (len(models) - 1) / 2) * w, vals, w, label=labels[mdl], color=colors[mdl], - alpha=0.9 if mdl in directed else 0.65) - ax.set_xticks(x); ax.set_xticklabels(_FAMS) - ax.set(ylabel="accuracy", title="Per-family: directed offspring (green), selected on the\n" - "verifier, vs the single uniform soup (orange) and the parents") - ax.legend(frameon=False, fontsize=8, ncol=2) - - # Panel B: overall vs worst-family, with the best-specialist ceiling. - ax = axes[1] - x2 = np.arange(len(models)) - for off, metric, hatch, lab in [(-0.2, "overall", "", "overall"), - (0.2, "worst_family", "//", "worst family")]: - ax.bar(x2 + off, [_acc(df, m, metric) for m in models], 0.38, - color=[colors[m] for m in models], hatch=hatch, alpha=0.85, label=lab, - edgecolor="white") - ax.set_xticks(x2); ax.set_xticklabels([labels[m] for m in models], rotation=25, ha="right", - fontsize=8) - if specialists: - ceil = max(_acc(df, s, "overall") for s in specialists) - ax.axhline(ceil, ls=":", c="#1f77b4", lw=1, alpha=0.7) # best-parent ceiling - ax.set(ylabel="accuracy", title="Overall (solid) vs worst-family (hatched):\n" - "directed offspring vs soup vs the best parent (dotted)") - ax.legend(frameon=False, fontsize=9) - - best_dir = max([_acc(df, m, "overall") for m in directed], default=float("nan")) - soup_o = _acc(df, "merge_soup", "overall") - best_spec = max([_acc(df, s, "overall") for s in specialists], default=float("nan")) - if best_dir > soup_o + 0.005: - verdict = f"directed {best_dir:.2f} > soup {soup_o:.2f} overall" - elif best_dir > soup_o - 0.005: - verdict = f"directed {best_dir:.2f} ≈ soup {soup_o:.2f} overall" - else: - verdict = f"directed {best_dir:.2f} < soup {soup_o:.2f} overall" - verdict += f" (best parent {best_spec:.2f})" - fig.suptitle(f"llm_directed — directed sex (breed offspring + select on the verifier): {verdict} " - f"({cfg['base_model'].split('/')[-1]})", y=1.0, fontsize=12) - fig.tight_layout() - savefig(fig, results_dir, "llm_directed") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/plot_llm_merge.py b/figures/plot_llm_merge.py deleted file mode 100644 index 2ad0ac2..0000000 --- a/figures/plot_llm_merge.py +++ /dev/null @@ -1,86 +0,0 @@ -"""llm_merge figure — recombining specialist LLMs (blueprint C2/C4, the real-LLM image of E8). - -LoRA specialists on disjoint task families are merged (weight-space) into one deployable model. The -recombined model beats any single specialist overall and — the sharper signature — is competent -across *all* families, which no single parent is. Two panels: (A) per-family accuracy for the base, -each specialist, and the merges (each specialist spikes on its own family; the merges are high -everywhere); (B) overall vs worst-family accuracy (the merges dominate both, especially worst-family). -Reads only the committed bundle. - -Usage: python figures/plot_llm_merge.py [results/llm_merge] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np - -sys.path.insert(0, str(Path(__file__).parent)) -from _figlib import load_seed_bundles, savefig # noqa: E402 - -_FAMS = ["lists", "strings", "arith"] - - -def _acc(df, model, metric): - r = df[(df["model"] == model) & (df["metric"] == metric)]["accuracy"] - return float(r.mean()) if len(r) else float("nan") - - -def main(results_dir: str = "results/llm_merge") -> None: - df, cfg = load_seed_bundles(results_dir) # seed-mean when the bundle has s{seed}/ sub-bundles - specialists = sorted(m for m in df["model"].unique() if m.startswith("spec_")) - merges = sorted(m for m in df["model"].unique() if m.startswith("merge_")) - models = ["base"] + specialists + merges - labels = {"base": "base", **{s: s.replace("spec_", "spec:") for s in specialists}, - **{m: m.replace("merge_", "merge:") for m in merges}} - colors = {"base": "#7f7f7f"} - for s in specialists: - colors[s] = "#1f77b4" - for m in merges: - colors[m] = "#2ca02c" - - fig, axes = plt.subplots(1, 2, figsize=(13, 5)) - - # Panel A: per-family accuracy, grouped by model. - ax = axes[0] - x = np.arange(len(_FAMS)) - w = 0.8 / len(models) - for i, mdl in enumerate(models): - vals = [_acc(df, mdl, f) for f in _FAMS] - ax.bar(x + (i - (len(models) - 1) / 2) * w, vals, w, label=labels[mdl], - color=colors[mdl], alpha=0.9 if mdl.startswith("merge_") else 0.7) - ax.set_xticks(x); ax.set_xticklabels(_FAMS) - ax.set(ylabel="accuracy", title="Per-family: each specialist spikes on its own family; the merges\n" - "(green) are competent everywhere (but averaging dilutes some peaks)") - ax.legend(frameon=False, fontsize=8, ncol=2) - - # Panel B: overall vs worst-family, per model. - ax = axes[1] - x2 = np.arange(len(models)) - for off, metric, hatch, lab in [(-0.2, "overall", "", "overall"), - (0.2, "worst_family", "//", "worst family")]: - ax.bar(x2 + off, [_acc(df, m, metric) for m in models], 0.38, - color=[colors[m] for m in models], hatch=hatch, alpha=0.85, label=lab, - edgecolor="white") - ax.set_xticks(x2); ax.set_xticklabels([labels[m] for m in models], rotation=25, ha="right", - fontsize=8) - ax.set(ylabel="accuracy", title="Overall (solid) vs worst-family (hatched):\n" - "the recombined model vs the best single specialist") - ax.legend(frameon=False, fontsize=9) - - best_spec = max(_acc(df, m, "overall") for m in specialists) - best_merge = max(_acc(df, m, "overall") for m in merges) - verdict = (f"recombined {best_merge:.2f} > best specialist {best_spec:.2f} overall" - if best_merge > best_spec + 0.005 else - f"recombined {best_merge:.2f} ≈ best specialist {best_spec:.2f} overall") - fig.suptitle(f"llm_merge — recombining decorrelated specialist LLMs: {verdict} " - f"({cfg['base_model'].split('/')[-1]})", y=1.0, fontsize=12) - fig.tight_layout() - savefig(fig, results_dir, "llm_merge") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/plot_llm_moe.py b/figures/plot_llm_moe.py deleted file mode 100644 index 0a04f30..0000000 --- a/figures/plot_llm_moe.py +++ /dev/null @@ -1,97 +0,0 @@ -"""llm_moe figure — union-preserving recombination (route / max-merge) vs fusion (soup / ties). - -The real-weight image of E8's *max*: keep every specialist intact and *select* (route per prompt, or -per module) instead of averaging the deltas. Two panels: (A) per-family accuracy for the base, each -specialist, the fusion merges, and the union operators — the union operators should match the best -specialist on every family (they *are* that specialist there), while fusion may dilute or compose; -(B) overall vs worst-family, fusion vs union, with the routing ceiling (moe_oracle) marked. The -suptitle reports whether union beats fusion (dilution regime) or they converge (composition regime). -Reads only the committed bundle. - -Usage: python figures/plot_llm_moe.py [results/llm_moe] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np - -sys.path.insert(0, str(Path(__file__).parent)) -from _figlib import load_seed_bundles, savefig # noqa: E402 - -_FAMS = ["lists", "strings", "arith"] -_FUSION = {"merge_soup": "fuse:soup", "merge_ties": "fuse:ties"} -_UNION = {"moe_oracle": "route:oracle", "moe_learned": "route:learned", "max_merge": "max-merge"} - - -def _acc(df, model, metric): - r = df[(df["model"] == model) & (df["metric"] == metric)]["accuracy"] - return float(r.mean()) if len(r) else float("nan") - - -def main(results_dir: str = "results/llm_moe") -> None: - df, cfg = load_seed_bundles(results_dir) # seed-mean when the bundle has s{seed}/ sub-bundles - present = set(df["model"].unique()) - specialists = sorted(m for m in present if m.startswith("spec_")) - fusion = [m for m in _FUSION if m in present] - union = [m for m in _UNION if m in present] - models = ["base"] + specialists + fusion + union - labels = {"base": "base", **{s: s.replace("spec_", "spec:") for s in specialists}, - **_FUSION, **_UNION} - colors = {"base": "#7f7f7f", **{s: "#1f77b4" for s in specialists}, - **{m: "#ff7f0e" for m in fusion}, **{m: "#2ca02c" for m in union}} - - fig, axes = plt.subplots(1, 2, figsize=(14, 5)) - - # Panel A: per-family accuracy, grouped by model. - ax = axes[0] - x = np.arange(len(_FAMS)) - w = 0.8 / len(models) - for i, mdl in enumerate(models): - vals = [_acc(df, mdl, f) for f in _FAMS] - ax.bar(x + (i - (len(models) - 1) / 2) * w, vals, w, label=labels[mdl], color=colors[mdl], - alpha=0.9 if (mdl in union or mdl in fusion) else 0.65) - ax.set_xticks(x); ax.set_xticklabels(_FAMS) - ax.set(ylabel="accuracy", title="Per-family: fusion (orange) blends the deltas; union (green)\n" - "keeps each specialist intact and selects — no dilution") - ax.legend(frameon=False, fontsize=8, ncol=2) - - # Panel B: overall vs worst-family, per model. - ax = axes[1] - x2 = np.arange(len(models)) - for off, metric, hatch, lab in [(-0.2, "overall", "", "overall"), - (0.2, "worst_family", "//", "worst family")]: - ax.bar(x2 + off, [_acc(df, m, metric) for m in models], 0.38, - color=[colors[m] for m in models], hatch=hatch, alpha=0.85, label=lab, - edgecolor="white") - ax.set_xticks(x2); ax.set_xticklabels([labels[m] for m in models], rotation=25, ha="right", - fontsize=8) - ax.set(ylabel="accuracy", title="Overall (solid) vs worst-family (hatched):\nfusion vs union " - "recombination") - # Mark the routing ceiling (oracle) if present. - if "moe_oracle" in present: - ceil = _acc(df, "moe_oracle", "overall") - ax.axhline(ceil, ls=":", c="#2ca02c", lw=1, alpha=0.7) - ax.legend(frameon=False, fontsize=9) - - best_fuse = max([_acc(df, m, "overall") for m in fusion], default=float("nan")) - best_union = max([_acc(df, m, "overall") for m in union], default=float("nan")) - router = _acc(df, "moe_learned", "router_acc") if "moe_learned" in present else float("nan") - if best_union > best_fuse + 0.01: - verdict = f"union {best_union:.2f} > fusion {best_fuse:.2f} overall (fusion dilutes)" - elif best_fuse > best_union + 0.01: - verdict = f"fusion {best_fuse:.2f} > union {best_union:.2f} overall (strong base composes)" - else: - verdict = f"union ≈ fusion ({best_union:.2f} vs {best_fuse:.2f}) overall" - rtxt = f"; learned router {router:.2f}" if router == router else "" - fig.suptitle(f"llm_moe — module-level union vs fusion recombination: {verdict}{rtxt} " - f"({cfg['base_model'].split('/')[-1]})", y=1.0, fontsize=12) - fig.tight_layout() - savefig(fig, results_dir, "llm_moe") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/plot_llm_society.py b/figures/plot_llm_society.py deleted file mode 100644 index 5ba6cb8..0000000 --- a/figures/plot_llm_society.py +++ /dev/null @@ -1,115 +0,0 @@ -"""v2 society figure — E11's three panels at the language-model tier, plus the competence genotype. - -Pre-registered layout (tasks/prereg-llm-society-v2.md §8), written before unblinding and run on the -smoke bundle first. Reads only committed bundles: a single bundle directory, or a campaign directory -whose sub-directories ``s{seed}_{arm}/`` each hold a bundle (the PBS array writes one per element). - -(A) Best-agent overall test accuracy per arm over generations (solid) with the best *newborn* of each - generation (dotted) — a climb carried by a surviving founder is visible as such; B₀ (best founder - at gen 0) dashed. Mean ± 95% CI over seeds. -(B) Behavioural diversity of the population (mean pairwise disagreement). -(C) The self-consumption signature: mean conformity − mean true accuracy. -(D) Competence genotype of the ``full`` arm's best agent: per-family test accuracy × generation, mean - over seeds — E8's "a genotype no parent had", if it happens. - -Usage: python figures/plot_llm_society.py [results/llm_society_v2 | results/llm_society_v2_smoke] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -import yaml - -sys.path.insert(0, str(Path(__file__).parent)) -from _figlib import mean_ci, savefig # noqa: E402 - -_ARMS = [("full", "#2ca02c", "full society"), - ("no_sex", "#ff7f0e", "no sex (no recombination)"), - ("no_diversity", "#9467bd", "no diversity (greedy)"), - ("no_grounding", "#d62728", "no grounding (self-consumption)"), - ("sex_linear", "#1f77b4", "sex by linear blend (H2 control)")] - - -def load_any(results_dir: Path) -> tuple[pd.DataFrame, list[str]]: - """One bundle, or every ``*/results.parquet`` below the directory (the campaign layout).""" - if (results_dir / "results.parquet").exists(): - paths = [results_dir] - else: - paths = sorted(p.parent for p in results_dir.glob("*/results.parquet")) - if not paths: - raise SystemExit(f"no results.parquet under {results_dir}") - df = pd.concat([pd.read_parquet(p / "results.parquet") for p in paths], ignore_index=True) - fams = yaml.safe_load((paths[0] / "resolved_config.yaml").read_text())["source_config"]["families"] - return df, list(fams) - - -def main(results_dir: str = "results/llm_society_v2") -> None: - rd = Path(results_dir) - df, fams = load_any(rd) - pop = df[df.role == "population"] - summ = df[df.role == "summary"] - arms = [a for a in _ARMS if a[0] in set(df.arm.unique())] - - best = (pop[pop.metric == "test_overall"].groupby(["arm", "seed", "generation"]).value.max() - .rename("best").reset_index()) - newborn = summ[summ.metric == "best_newborn_overall"][["arm", "seed", "generation", "value"]] - b0 = best[best.generation == 0].groupby("seed").best.mean().mean() - - fig, axes = plt.subplots(1, 4, figsize=(21, 4.8)) - - def traj(ax, frame, col, title, ylabel, style="-", label_suffix=""): - for arm, color, label in arms: - sub = frame[frame.arm == arm] - if sub.empty: - continue - x, m, h = mean_ci(sub, "generation", col) - ax.plot(x, m, style, color=color, label=(label + label_suffix) if style == "-" else None, lw=2) - if style == "-": - ax.fill_between(x, m - h, m + h, color=color, alpha=0.15, lw=0) - if title: # overlay calls pass "" and must not wipe labels - ax.set_title(title, fontsize=10); ax.set_xlabel("generation"); ax.set_ylabel(ylabel) - - traj(axes[0], best, "best", "(A) best agent (solid) and best newborn (dotted)\nB₀ = best founder, dashed", - "overall test accuracy") - traj(axes[0], newborn.rename(columns={"value": "best"}), "best", "", "", style=":") - axes[0].axhline(b0, color="k", ls="--", lw=1, label=f"B₀ = {b0:.2f}") - axes[0].legend(fontsize=8, loc="best") - - div = summ[summ.metric == "diversity_behav"] - traj(axes[1], div, "value", "(B) population diversity\n(mean pairwise disagreement)", "diversity") - gap = summ[summ.metric == "gap_conformity_minus_truth"] - traj(axes[2], gap, "value", "(C) self-consumption signature\nconformity − true accuracy", "gap") - axes[2].axhline(0, color="k", lw=0.8) - - # (D) competence genotype of the full arm's best agent, families × generations, mean over seeds - full = pop[pop.arm == ("full" if "full" in set(pop.arm) else arms[0][0])] - fam_cols = [f"test_{f}" for f in fams] - idx = full[full.metric == "test_overall"].sort_values("value").groupby(["seed", "generation"]).tail(1) - keyed = full.set_index(["seed", "generation", "agent", "metric"]).value - gens = sorted(full.generation.unique()) - heat = np.full((len(fams), len(gens)), np.nan) - for gi, g in enumerate(gens): - rows = idx[idx.generation == g] - vals = np.array([[keyed.get((r.seed, g, r.agent, c), np.nan) for c in fam_cols] for r in rows.itertuples()]) - if len(vals): - heat[:, gi] = np.nanmean(vals, axis=0) - im = axes[3].imshow(heat, aspect="auto", cmap="viridis", vmin=0, vmax=1) - axes[3].set_yticks(range(len(fams))); axes[3].set_yticklabels(fams, fontsize=8) - axes[3].set_xticks(range(len(gens))); axes[3].set_xticklabels(gens, fontsize=8) - axes[3].set_xlabel("generation"); axes[3].set_title("(D) competence genotype of the best agent\n(full arm; per-family accuracy)", fontsize=10) - fig.colorbar(im, ax=axes[3], fraction=0.046, pad=0.02) - - n_seeds = df.seed.nunique() - fig.suptitle(f"llm_society_v2 — the composed society at LLM scale ({n_seeds} seed{'s' if n_seeds != 1 else ''}, " - f"L={len(fams)} families, mean ± 95% CI)", y=1.02) - fig.tight_layout() - savefig(fig, rd, "llm_society_v2") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/plot_llm_speciation.py b/figures/plot_llm_speciation.py index 33501e3..3d8ff99 100644 --- a/figures/plot_llm_speciation.py +++ b/figures/plot_llm_speciation.py @@ -29,7 +29,7 @@ from pathlib import Path import matplotlib.pyplot as plt sys.path.insert(0, str(Path(__file__).parent)) -from _figlib import load_bundle, savefig # noqa: E402 +from _figlib import load_bundle, load_seed_bundles, savefig # noqa: E402 def _series(df, mode, model, metric): @@ -39,7 +39,7 @@ def _series(df, mode, model, metric): def main() -> None: - rep, _ = load_bundle("results/llm_speciation") + rep, _ = load_seed_bundles("results/llm_speciation") # s{seed}/ layout, seeds 1-3 add, _ = load_bundle("results/llm_speciation_add") fam_a = "strings" if (rep["metric"] == "strings").any() else "lists" fam_b = "arith" diff --git a/figures/plot_sexual_vs_asexual_lineage.py b/figures/plot_sexual_vs_asexual_lineage.py deleted file mode 100644 index bdcfc40..0000000 --- a/figures/plot_sexual_vs_asexual_lineage.py +++ /dev/null @@ -1,62 +0,0 @@ -"""E7 figure — the advantage of sex: recombination adapts faster than clonal reproduction. - -The dynamic mechanism behind E8. A single population adapts from all-wrong toward a multi-locus -optimum under selection + drift + mutation. Beneficial alleles arise in different sub-lineages; -sexual recombination reassorts them into one genotype, while an asexual lineage suffers clonal -interference. The sexual lineage climbs faster — the classical advantage of sex (an honest *speed* -advantage; both eventually plateau near the optimum in this tractable regime). - -Two panels: (A) mean-fitness adaptation curves, asexual vs sexual, over generations; (B) linkage -disequilibrium over generations — asexual holds beneficial alleles in disequilibrium (scattered -across genotypes) while sexual drives it to ~0 (assembled), the mechanism of the speed gap. - -Usage: python figures/plot_sexual_vs_asexual_lineage.py [results/sexual_vs_asexual_lineage] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import matplotlib.pyplot as plt - -sys.path.insert(0, str(Path(__file__).parent)) -from _figlib import load_bundle, mean_ci, savefig # noqa: E402 - - -def main(results_dir: str = "results/sexual_vs_asexual_lineage") -> None: - df, cfg = load_bundle(results_dir) - L = cfg["genotype"]["L"] - arms = [(0.0, "#7f7f7f", "asexual (clonal)"), (1.0, "#d62728", "sexual (recombining)")] - - fig, axes = plt.subplots(1, 2, figsize=(13, 5)) - - ax = axes[0] - for rate, c, lab in arms: - sub = df[df["recomb_rate"] == rate] - g, m, ci = mean_ci(sub, "generation", "mean_fitness") - ax.plot(g, m, "-", color=c, lw=1.8, label=lab) - ax.fill_between(g, m - ci, m + ci, color=c, alpha=0.2) - ax.axhline(L, ls=":", color="green", lw=1, label=f"optimum ($L$={L})") - ax.set(xlabel="generation", ylabel="mean fitness (# correct loci)", - title="Advantage of sex: recombination adapts faster\n(clonal interference slows the asexual lineage)") - ax.legend(frameon=False, fontsize=9) - - ax = axes[1] - for rate, c, lab in arms: - sub = df[df["recomb_rate"] == rate] - g, m, ci = mean_ci(sub, "generation", "ld") - ax.plot(g, m, "-", color=c, lw=1.8, label=lab) - ax.fill_between(g, m - ci, m + ci, color=c, alpha=0.2) - ax.set(xlabel="generation", ylabel="mean linkage disequilibrium |D|", - title="Mechanism: asexual scatters beneficial alleles (LD>0);\nsexual assembles them (LD→0)") - ax.legend(frameon=False, fontsize=9) - - fig.suptitle("E7 — the advantage of sex: recombination reassorts beneficial alleles that arose " - "in different lineages", y=1.02, fontsize=12) - fig.tight_layout() - savefig(fig, results_dir, "sexual_vs_asexual_lineage") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/stats_llm_compose.py b/figures/stats_llm_compose.py deleted file mode 100644 index d6af76b..0000000 --- a/figures/stats_llm_compose.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Pre-registered analysis for the composition experiment (prereg v3 §3) — written before unblinding. - -Prints each hypothesis, its per-seed quantities, the paired mean ± 95% CI, and PASS / FAIL against -the threshold fixed in the pre-registration. Nothing here is chosen after seeing the data. - - H1 gate S_0 >= +0.05, union-exceedance >= 0.03, cat > linear by >= 0.03 (>=2/3 seeds) - H2 S_t declines (Spearman <= -0.7) and composition's fractional loss exceeds each parent's - H3 Ĉ_t (one parameter, fixed at t=0) predicts observed with MAE <= 0.05 and beats a - two-parameter exponential on AIC - H4 S_G(grounded) - S_G(dry) >= +0.08, 3/3 seeds positive - H5 rho_t rises in dry (Spearman >= +0.7); partial corr of S_t with rho_t given q_t < 0 - H6 dry_linear: S_0 <= +0.02 and union-exceedance <= 0.01 at every generation - -Usage: python figures/stats_llm_compose.py [results/llm_compose] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import numpy as np -import pandas as pd - -sys.path.insert(0, str(Path(__file__).parent)) -sys.path.insert(0, str(Path(__file__).parent.parent / "src")) -from plot_llm_compose import load_any, series # noqa: E402 -from llm.compose import predicted_composition # noqa: E402 - - -def ci95(x) -> tuple[float, float]: - x = np.asarray(x, dtype=float) - if len(x) < 2: - return (float(x.mean()) if len(x) else float("nan")), float("nan") - from scipy import stats - return float(x.mean()), float(stats.t.ppf(0.975, len(x) - 1) * x.std(ddof=1) / np.sqrt(len(x))) - - -def spearman(y) -> float: - from scipy import stats - y = np.asarray(y, dtype=float) - return float(stats.spearmanr(np.arange(len(y)), y).statistic) if len(y) > 2 else float("nan") - - -def verdict(ok) -> str: - return "n/a " if ok is None else ("PASS" if ok else "FAIL") - - -def main(results_dir: str = "results/llm_compose") -> None: - df = load_any(Path(results_dir)) - seeds = sorted(df.seed.unique()) - G = int(df.generation.max()) - arms = set(df.arm.unique()) - print(f"bundle {results_dir} seeds {seeds} G = {G} arms {sorted(arms)}\n") - - def at(arm, metric, gen): - s = series(df, arm, metric) - return {int(r.seed): float(r.value) for r in s[s.generation == gen].itertuples()} - - def traj(arm, metric, seed): - s = series(df, arm, metric) - s = s[s.seed == seed].sort_values("generation") - return s.value.to_numpy() - - # ---------------- H1 - print("H1 — generation-0 gate (does the published effect reproduce here?)") - s0, u0 = at("dry", "surplus", 0), at("dry", "union_exceedance", 0) - print(f" surplus at t=0 {np.round(list(s0.values()), 3).tolist()} " - f"{verdict(sum(v >= 0.05 for v in s0.values()) >= max(1, len(s0) - 1))} (>= +0.05)") - print(f" union-exceedance at t=0 {np.round(list(u0.values()), 3).tolist()} " - f"{verdict(sum(v >= 0.03 for v in u0.values()) >= max(1, len(u0) - 1))} (>= 0.03)") - # The operator arms are named by their operator (`dry` = linear, `dry_cat` = cat) after the - # gen-0 sweep; compare whichever two are present rather than assuming a name. - if {"dry", "dry_cat"} <= arms: - lin0, cat0 = at("dry", "composed_acc", 0), at("dry_cat", "composed_acc", 0) - d = [lin0[s] - cat0[s] for s in lin0 if s in cat0] - print(f" linear − cat at t=0 {np.round(d, 3).tolist()} (sweep found the ordering is " - f"weight-dependent; reported, not gated)") - - # ---------------- H2 - print("\nH2 — composition decays, and faster than its parents") - for seed in seeds: - s = traj("dry", "surplus", seed) - c = traj("dry", "composed_acc", seed) - qm, qc = traj("dry", "q_math", seed), traj("dry", "q_code", seed) - if len(c) < 3: - continue - frac_c = c[-1] / c[0] if c[0] > 0 else np.nan - frac_m = qm[-1] / qm[0] if qm[0] > 0 else np.nan - frac_q = qc[-1] / qc[0] if qc[0] > 0 else np.nan - print(f" seed {seed}: spearman(S_t) {spearman(s):+.2f} retained: composed {frac_c:.2f} " - f"vs math {frac_m:.2f}, code {frac_q:.2f} " - f"{'faster' if frac_c < min(frac_m, frac_q) else 'NOT faster'}") - - # ---------------- H3 - print("\nH3 — the closed form predicts the trajectory (the paper's predictive claim)") - for arm in ("dry", "grounded"): - if arm not in arms: - continue - maes, aics = [], [] - for seed in seeds: - obs = traj(arm, "composed_acc", seed) - qm, qc = traj(arm, "q_math", seed), traj(arm, "q_code", seed) - rho = traj(arm, "rho_behav", seed) - if len(obs) < 3 or not (len(obs) == len(qm) == len(qc) == len(rho)): - continue - pred = predicted_composition(qm, qc, rho, float(obs[0])) - mae = float(np.mean(np.abs(pred - obs))) - # two-parameter baseline: a*exp(-b t), least squares on the same points - t = np.arange(len(obs), dtype=float) - pos = obs > 1e-6 - if pos.sum() >= 2: - b, loga = np.polyfit(t[pos], np.log(obs[pos]), 1) - base = np.exp(loga) * np.exp(b * t) - else: - base = np.full_like(obs, obs.mean()) - n = len(obs) - aic = lambda resid, k: n * np.log(max(1e-12, np.mean(resid ** 2))) + 2 * k - maes.append(mae); aics.append(aic(pred - obs, 1) - aic(base - obs, 2)) - if maes: - m, h = ci95(maes) - print(f" {arm:9s} MAE {m:.3f} ± {h:.3f} {verdict(m <= 0.05)} (<= 0.05) " - f"ΔAIC vs exponential {np.mean(aics):+.1f} " - f"({'closed form wins' if np.mean(aics) < 0 else 'baseline wins'})") - - # ---------------- H4 - print("\nH4 — grounding arrests the decay") - if {"dry", "grounded"} <= arms: - d, g = at("dry", "surplus", G), at("grounded", "surplus", G) - diff = [g[s] - d[s] for s in g if s in d] - m, h = ci95(diff) - print(f" S_G(grounded) − S_G(dry) {np.round(diff, 3).tolist()} mean {m:+.3f} ± {h:.3f} " - f"{verdict(m >= 0.08 and all(v > 0 for v in diff))} (>= +0.08, all seeds positive)") - - # ---------------- H5 - print("\nH5 — rising ρ is the mechanism") - for arm in ("dry", "grounded"): - if arm not in arms: - continue - sp = [spearman(traj(arm, "rho_behav", s)) for s in seeds if len(traj(arm, "rho_behav", s)) > 2] - if sp: - print(f" {arm:9s} spearman(ρ_t) {np.round(sp, 2).tolist()} mean {np.mean(sp):+.2f}" - + (f" {verdict(np.mean(sp) >= 0.7)} (>= +0.7)" if arm == "dry" else "")) - try: - from scipy import stats - rows = [] - for seed in seeds: - s, r = traj("dry", "surplus", seed), traj("dry", "rho_behav", seed) - qm, qc = traj("dry", "q_math", seed), traj("dry", "q_code", seed) - if len(s) > 3 and len(s) == len(r) == len(qm) == len(qc): - rows.append(np.column_stack([s, r, qm * qc])) - if rows: - a = np.vstack(rows) - # partial correlation of S with rho, controlling for q_math*q_code - res_s = a[:, 0] - np.poly1d(np.polyfit(a[:, 2], a[:, 0], 1))(a[:, 2]) - res_r = a[:, 1] - np.poly1d(np.polyfit(a[:, 2], a[:, 1], 1))(a[:, 2]) - pr = float(stats.pearsonr(res_s, res_r).statistic) - print(f" partial corr(S, ρ | q_math·q_code) = {pr:+.2f} " - f"{verdict(pr < 0)} (negative = lost complementarity, not just retention loss)") - except Exception as e: # descriptive only, never fatal - print(f" partial correlation unavailable ({type(e).__name__})") - - # ---------------- H6 - print("\nH6 (revised) — does the operator ordering hold across generations, or only at gen 0?") - if {"dry", "dry_cat"} <= arms: - for gen in range(G + 1): - lin, cat = at("dry", "surplus", gen), at("dry_cat", "surplus", gen) - d = [lin[s] - cat[s] for s in lin if s in cat] - if d: - print(f" gen {gen}: surplus linear − cat = {np.mean(d):+.3f} " - f"(linear {np.mean(list(lin.values())):+.3f}, cat {np.mean(list(cat.values())):+.3f})") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/figures/stats_llm_curriculum.py b/figures/stats_llm_curriculum.py index 46726fb..ed50e1b 100644 --- a/figures/stats_llm_curriculum.py +++ b/figures/stats_llm_curriculum.py @@ -2,7 +2,7 @@ One loader for every curriculum bundle (the arm label is set HERE by directory, never trusted from the parquet alone, because the veto arm is recorded as ``society`` with ``allow_veto`` on), and the -pre-registered readouts for the two 2026-09-11 controls (tasks/prereg-llm-society-v4.md §8h): +pre-registered readouts for the two 2026-09-11 controls (tasks/prereg-llm-society-v4.md on the dev branch §8h): 1. **Forced stop at generation 3** (``llm_curriculum_v5_stop3``): per-seed paired contrasts of the best lineage's final all-family accuracy, veto − stop3, stop3 − isolated, stop3 − society. diff --git a/figures/stats_llm_society.py b/figures/stats_llm_society.py deleted file mode 100644 index 76200fa..0000000 --- a/figures/stats_llm_society.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Pre-registered analysis for the v2 society (tasks/prereg-llm-society-v2.md §5, §8). - -Prints, for each hypothesis, the per-seed quantities, the paired mean ± 95% CI over seeds, the sign -count, and PASS / FAIL against the pre-set threshold. Written before unblinding and exercised on the -smoke bundle; nothing here is chosen after seeing the campaign. Reads only committed bundles (one -bundle directory, or a campaign directory of ``s{seed}_{arm}/`` bundles). - - H1 vertical climb full best(G) − B₀ ≥ 0.20 ; best newborn(G) − B₀ ≥ 0.15 ; ≥ 6 families ≥ 0.6 - H3 self-consumption no_grounding best(G) ≤ B₀ + 0.05 ; gap(no_grounding) − gap(full) ≥ 0.30 - H4 sex necessity no_sex best(G) ≤ B₀ + 0.05 in every seed - H5 diversity AUC(diversity) full > no_diversity ; no_diversity diversity < 0.1 by gen 6 - H6 where skills die ≤ 20% of family losses in `full` were supplied at ≥ 0.6 by the child's source - (H2 is deferred: the sex_linear arm is not in the first campaign.) - -Usage: python figures/stats_llm_society.py [results/llm_society_v2] -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import numpy as np -import pandas as pd - -sys.path.insert(0, str(Path(__file__).parent)) -from plot_llm_society import load_any # noqa: E402 - -COMPETENT = 0.6 - - -def ci95(x: np.ndarray) -> tuple[float, float]: - x = np.asarray(x, dtype=float) - if len(x) < 2: - return float(x.mean()), float("nan") - from scipy import stats - h = stats.t.ppf(0.975, len(x) - 1) * x.std(ddof=1) / np.sqrt(len(x)) - return float(x.mean()), float(h) - - -def verdict(ok: bool | None) -> str: - return "n/a " if ok is None else ("PASS" if ok else "FAIL") - - -def main(results_dir: str = "results/llm_society_v2") -> None: - df, fams = load_any(Path(results_dir)) - pop, summ, child = (df[df.role == r] for r in ("population", "summary", "child")) - src = df[df.role == "child_source"] - seeds = sorted(df.seed.unique()) - G = int(pop.generation.max()) - arms = set(df.arm.unique()) - print(f"bundle: {results_dir} seeds {seeds} G = {G} L = {len(fams)} arms {sorted(arms)}\n") - - best = pop[pop.metric == "test_overall"].groupby(["arm", "seed", "generation"]).value.max() - B0 = {s: float(best.xs(s, level="seed").xs(0, level="generation").mean()) for s in seeds} - print("B₀ (best founder, gen 0) per seed:", {s: round(v, 3) for s, v in B0.items()}) - - def at_G(arm, metric_frame, metric=None, gen=G, agg="max"): - out = {} - for s in seeds: - f = metric_frame[(metric_frame.arm == arm) & (metric_frame.seed == s) & (metric_frame.generation == gen)] - if metric is not None: - f = f[f.metric == metric] - if not f.empty: - out[s] = float(f.value.max() if agg == "max" else f.value.mean()) - return out - - def report(name, per_seed, thr, direction, note=""): - vals = np.array(list(per_seed.values())) - if len(vals) == 0: - print(f" {name:38s} {verdict(None)}"); return None - m, h = ci95(vals) - ok_each = (vals >= thr) if direction == ">=" else (vals <= thr) - ok = bool(ok_each.sum() >= max(3, len(vals)) if len(vals) >= 3 else ok_each.all()) - print(f" {name:38s} {verdict(ok)} mean {m:+.3f} ± {h:.3f} per seed " - f"{np.round(vals, 3).tolist()} {int(ok_each.sum())}/{len(vals)} meet {direction} {thr} {note}") - return ok - - # ---------------- H1 - print("\nH1 — vertical climb (full arm)") - if "full" in arms: - gain = {s: at_G("full", pop, "test_overall")[s] - B0[s] for s in seeds if s in at_G("full", pop, "test_overall")} - nb = at_G("full", summ, "best_newborn_overall", gen=G - 1) - gain_nb = {s: nb[s] - B0[s] for s in nb} - # families the best agent is competent on, at G - comp = {} - for s in seeds: - f = pop[(pop.arm == "full") & (pop.seed == s) & (pop.generation == G)] - if f.empty: - continue - ov = f[f.metric == "test_overall"].set_index("agent").value - a = int(ov.idxmax()) - per = f[(f.agent == a) & f.metric.isin([f"test_{x}" for x in fams])].value - comp[s] = float((per >= COMPETENT).sum()) - report("best agent − B₀ (≥ 0.20)", gain, 0.20, ">=") - report("best newborn − B₀ (≥ 0.15)", gain_nb, 0.15, ">=") - report("families competent in best agent (≥ 6)", comp, 6, ">=") - else: - print(" full arm absent") - - # ---------------- H3 - print("\nH3 — self-consumption (no_grounding)") - if {"no_grounding", "full"} <= arms: - ng = at_G("no_grounding", pop, "test_overall") - report("no_grounding best − B₀ (≤ 0.05)", {s: ng[s] - B0[s] for s in ng}, 0.05, "<=") - gap_ng = at_G("no_grounding", summ, "gap_conformity_minus_truth", agg="mean") - gap_f = at_G("full", summ, "gap_conformity_minus_truth", agg="mean") - report("gap(no_grounding) − gap(full) (≥ 0.30)", {s: gap_ng[s] - gap_f[s] for s in gap_ng if s in gap_f}, 0.30, ">=") - ca = summ[(summ.arm == "no_grounding") & (summ.metric == "consensus_acc")] - slope = {s: float(np.polyfit(g.generation, g.value, 1)[0]) for s, g in ca.groupby("seed") if len(g) > 1} - report("consensus-accuracy slope, no_grounding (≤ 0)", slope, 0.0, "<=", note="(non-increasing)") - else: - print(" arms absent") - - # ---------------- H4 - print("\nH4 — sex necessity (no_sex ceiling)") - if "no_sex" in arms: - ns = at_G("no_sex", pop, "test_overall") - vals = {s: ns[s] - B0[s] for s in ns} - ok = all(v <= 0.05 for v in vals.values()) if vals else None - print(f" {'no_sex best − B₀ (≤ 0.05 in EVERY seed)':38s} {verdict(ok)} per seed {np.round(list(vals.values()), 3).tolist()}") - else: - print(" no_sex arm absent") - - # ---------------- H5 - print("\nH5 — diversity (full vs no_diversity)") - if {"full", "no_diversity"} <= arms: - div = summ[summ.metric == "diversity_behav"] - auc = lambda arm, s: float(np.trapezoid(div[(div.arm == arm) & (div.seed == s)].sort_values("generation").value)) - d_auc = {s: auc("full", s) - auc("no_diversity", s) for s in seeds - if not div[(div.arm == "full") & (div.seed == s)].empty and not div[(div.arm == "no_diversity") & (div.seed == s)].empty} - report("AUC(diversity) full − no_diversity (> 0)", d_auc, 1e-9, ">=") - g6 = min(6, G) - nd6 = at_G("no_diversity", summ, "diversity_behav", gen=g6, agg="mean") - report(f"no_diversity diversity at gen {g6} (< 0.1)", nd6, 0.1, "<=") - else: - print(" arms absent") - - # ---------------- H6 - print("\nH6 — where skills die (full arm)") - if "full" in arms and not src.empty: - losses, supplied_ok = 0, 0 - for s in seeds: - fpop = pop[(pop.arm == "full") & (pop.seed == s)] - fsrc = src[(src.arm == "full") & (src.seed == s)] - for t in range(G): - alive_t = {f for f in fams if (fpop[(fpop.generation == t) & (fpop.metric == f"test_{f}")].value >= COMPETENT).any()} - alive_t1 = {f for f in fams if (fpop[(fpop.generation == t + 1) & (fpop.metric == f"test_{f}")].value >= COMPETENT).any()} - for f in alive_t - alive_t1: - losses += 1 - sup = fsrc[(fsrc.generation == t) & (fsrc.metric == f"source_{f}")].value - supplied_ok += int((sup >= COMPETENT).any()) - frac = supplied_ok / losses if losses else float("nan") - ok = None if not losses else frac <= 0.20 - print(f" {'family losses supplied at ≥0.6 (≤ 20%)':38s} {verdict(ok)} {supplied_ok}/{losses} losses " - f"({frac:.0%} if any) — skills should die because they arrived diluted, not despite competent supply") - else: - print(" no source diagnostics") - - print("\nH2 (union vs linear blend) — deferred: sex_linear not in the first campaign (prereg §12).") - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/hpc/llm_compose.pbs b/hpc/llm_compose.pbs deleted file mode 100755 index 93cdf71..0000000 --- a/hpc/llm_compose.pbs +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# The composition campaign (prereg tasks/prereg-llm-compose-v3.md): one (seed, arm) per array -# element. Seed 1 runs locally on the A4000 as the hedge; this array covers seeds 2-3 x 3 arms. -# Each element trains its own founders (cached per seed under models/, first writer wins via the -# adapter_config.json check) and checkpoints every generation, so a requeued element resumes. -# submit: qsub hpc/llm_compose.pbs status: qstat -u $USER -t -# index -> seed = 2 + (i-1)/3, arm = (dry grounded dry_cat)[(i-1)%3] -#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S -#PBS -l walltime=08:00:00 -#PBS -N lam_compose -#PBS -J 1-6 - -cd "$PBS_O_WORKDIR" -export HF_HOME="$EPHEMERAL/hf_cache" -export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets" -export TOKENIZERS_PARALLELISM=false -export UV_CACHE_DIR="$EPHEMERAL/uvcache" - -ARMS=(dry grounded dry_cat) -I=$((PBS_ARRAY_INDEX - 1)) -SEED=$((2 + I / 3)) -ARM=${ARMS[$((I % 3))]} - -source .venv/bin/activate -nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader -echo "seed=$SEED arm=$ARM start=$(date)" - -CFG="configs/llm/_gen/compose_s${SEED}_${ARM}.yaml" -mkdir -p configs/llm/_gen -python - "$SEED" "$ARM" "$CFG" <<'EOF' -import sys, yaml -seed, arm, out = int(sys.argv[1]), sys.argv[2], sys.argv[3] -cfg = yaml.safe_load(open("configs/llm/compose_s1.yaml")) -cfg["seed"] = seed -cfg["arms"] = [arm] -cfg["batch_size"] = 32 # L40S has 46 GB; the A4000 setting is 16 -cfg["score_batch_size"] = 8 -cfg["output"] = {"dir": f"results/llm_compose/s{seed}_{arm}"} -yaml.safe_dump(cfg, open(out, "w"), sort_keys=False) -EOF - -python -m llm.experiment "$CFG" -echo "done: $(date)" diff --git a/hpc/llm_smoke.pbs b/hpc/llm_smoke.pbs deleted file mode 100755 index f23e754..0000000 --- a/hpc/llm_smoke.pbs +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# Smoke gate: run the small 0.5B merge experiment on an L40S to confirm the GPU pipeline works -# on CX3 before the 4 h 7B run. ~5 min. submit: qsub hpc/llm_smoke.pbs -#PBS -l select=1:ncpus=8:mem=48gb:ngpus=1:gpu_type=L40S -#PBS -l walltime=00:20:00 -#PBS -N lam_llm_smoke - -cd "$PBS_O_WORKDIR" -export HF_HOME="$EPHEMERAL/hf_cache" -export TOKENIZERS_PARALLELISM=false -source .venv/bin/activate -nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader -python -m llm.experiment configs/llm/merge.yaml # 0.5B, writes results/llm_merge/ -echo "done: $(date)" diff --git a/hpc/llm_society.pbs b/hpc/llm_society.pbs deleted file mode 100755 index 452dd38..0000000 --- a/hpc/llm_society.pbs +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# The LLM society campaign (C3): 4-arm ablation, one seed per array element on one L40S each. -# Seed 1 runs locally on GG's A4000 (queue insurance); this array covers seeds 2-4. Each job is -# self-contained (founders trained inline, adapters under models/, deleted per generation), so a -# killed element reruns cleanly and elements schedule independently. ~2.5-3.5 h measured-scale -# runtime; 6 h walltime for backfill-friendly margin. -# submit: qsub hpc/llm_society.pbs status: qstat -u $USER -#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S -#PBS -l walltime=06:00:00 -#PBS -N lam_society -#PBS -J 2-4 - -cd "$PBS_O_WORKDIR" -export HF_HOME="$EPHEMERAL/hf_cache" -export TOKENIZERS_PARALLELISM=false -export UV_CACHE_DIR="$EPHEMERAL/uvcache" - -source .venv/bin/activate -nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader - -python -m llm.experiment "configs/llm/society_campaign_s${PBS_ARRAY_INDEX}.yaml" - -# results/llm_society_campaign/s${PBS_ARRAY_INDEX}/ written in-place. Sync back: -# rsync -avz hpc:'.../results/llm_society_campaign/' results/llm_society_campaign/ -echo "done: $(date)" diff --git a/hpc/llm_society_v2.pbs b/hpc/llm_society_v2.pbs deleted file mode 100755 index db08c5d..0000000 --- a/hpc/llm_society_v2.pbs +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# The v2 society campaign (prereg tasks/prereg-llm-society-v2.md §9): one (seed, arm) per array -# element on one L40S each, 16 elements = 4 seeds x 4 arms. Each element is self-contained: founders -# are trained inline and cached per seed (the four arm-elements of a seed share them via the -# filesystem; the first to arrive trains, the others wait on the adapter_config.json check), the loop -# checkpoints every generation and resumes, so a killed element is re-queued with the same index and -# picks up where it stopped. ~6 h per element at k_inherit=300 (prereg §9); 8 h walltime. -# submit: qsub hpc/llm_society_v2.pbs status: qstat -u $USER -t -# index -> seed = 1 + (i-1) // 4, arm = (full no_grounding no_sex no_diversity)[(i-1) % 4] -#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S -#PBS -l walltime=08:00:00 -#PBS -N lam_society_v2 -#PBS -J 1-16 - -cd "$PBS_O_WORKDIR" -export HF_HOME="$EPHEMERAL/hf_cache" -export TOKENIZERS_PARALLELISM=false -export UV_CACHE_DIR="$EPHEMERAL/uvcache" - -ARMS=(full no_grounding no_sex no_diversity) -I=$((PBS_ARRAY_INDEX - 1)) -SEED=$((1 + I / 4)) -ARM=${ARMS[$((I % 4))]} - -source .venv/bin/activate -nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader -echo "seed=$SEED arm=$ARM start=$(date)" - -# One config per (seed, arm): generated from the seed template so the resolved config is exact. -CFG="configs/llm/_gen/society_v2_s${SEED}_${ARM}.yaml" -mkdir -p configs/llm/_gen -python - "$SEED" "$ARM" "$CFG" <<'EOF' -import sys, yaml -seed, arm, out = int(sys.argv[1]), sys.argv[2], sys.argv[3] -cfg = yaml.safe_load(open(f"configs/llm/society_v2_s{seed}.yaml")) -cfg["arms"] = [arm] -cfg["output"] = {"dir": f"results/llm_society_v2/s{seed}_{arm}"} -yaml.safe_dump(cfg, open(out, "w"), sort_keys=False) -EOF - -python -m llm.experiment "$CFG" -echo "done: $(date)" diff --git a/paper/arxiv/ARXIV-SUBMISSION.md b/paper/arxiv/ARXIV-SUBMISSION.md deleted file mode 100644 index 762e4a5..0000000 --- a/paper/arxiv/ARXIV-SUBMISSION.md +++ /dev/null @@ -1,42 +0,0 @@ -# arXiv submission notes (Phase 2 of the PNAS work order) - -**What to upload.** The source package: `main.tex`, `body.tex`, `figs/` (three PDFs). arXiv rejects -TeX-produced PDF-only uploads, so upload source; all packages are standard and `\pdfoutput=1` is set, -so arXiv's pdflatex builds it (verified locally with tectonic; `main.pdf` in this directory is the -reference build, 20 pp). To rebuild after editing the Markdown source of truth: -`python paper/arxiv/md2tex.py && (cd paper/arxiv && tectonic main.tex)`. - -**Categories.** Primary: `q-bio.PE` (Populations and Evolution). Cross-list: `cs.LG` and `cs.NE`. -If arXiv asks for an endorsement for q-bio.PE (first submission to the archive), either request it -(an evolutionary-biology colleague with q-bio postings can endorse in one click) or flip primary to -`cs.NE` with `q-bio.PE` as cross-list — the paper is defensible either way; q-bio.PE primary is -preferred for the PNAS audience trail. - -**License.** arXiv non-exclusive license (default) is fine for PNAS. Do not pick CC-BY unless you -want it — PNAS permits preprints under any license, but the default keeps options open. - -**Abstract for the arXiv field** (plain text, ~1,750 chars — the field caps at 1,920; the paper's -long abstract stays in the PDF): - -> AI is shifting from single frozen models to populations of agents that persist, specialise, and are -> recombined into new models. The field describes this with evolutionary vocabulary — crossover, mate -> choice, offspring — but as metaphor over search. We argue the right theory already exists: the -> population genetics of the evolution of sex. Training each generation on the last is genetic drift, -> and model collapse is Muller's ratchet, the decay of an asexual lineage (we take the -> collapse-is-drift diagnosis as settled and cite it). The cure is sexual: ground every generation in -> data from a non-drifting reality (immigration, with a critical real-data fraction far below one); -> recombine many complementary parents (model merging — where recombination preserves the union of -> what the parents kept, while averaging cancels the benefit); and preserve diversity. Offspring then -> exceed every parent (the Fisher-Muller effect, shown in merged language models up to 7B). Sex has a -> limit: as models diverge they can speciate — a merge-compatibility cliff governed by epistasis -> (Bateson-Dobzhansky-Muller incompatibilities) whose damage snowballs. We model this and confirm it -> in real weights: a merge barrier survives alignment under the full function-preserving symmetry -> group of the network, rising with functional conflict while hybrid fitness falls to inviability — -> yet absent conflicting training signals, divergently-specialised lineages developed no isolation, -> the merge instead rescuing the forgetting specialists. AI can also do what biology cannot — -> directed sex: unbounded parents, chosen mates, offspring screened before they are kept. We support -> the argument with closed-form-validated simulations, trained networks, an image generator, and LLM -> prototypes, and position it against the 2025-26 evolutionary-AI landscape. - -**After posting.** Record the arXiv id in `tasks/workorder-pnas-submission.md`; sync v2 with the -PNAS-submitted text at Phase 5. PNAS permits preprints. diff --git a/paper/arxiv/body.tex b/paper/arxiv/body.tex deleted file mode 100644 index e3c6bc2..0000000 --- a/paper/arxiv/body.tex +++ /dev/null @@ -1,343 +0,0 @@ -\subsection*{A note on vocabulary (please read this first)} - -This paper sits at the meeting point of three fields, and it is written so that a reader from any one of them can follow all of it. We therefore \textbf{spell out} each field's jargon the first time it appears, even at the risk of belabouring the obvious for the specialist. A short glossary, in case you skipped a definition: - -\begin{itemize} -\item \textbf{Model collapse} \emph{(machine learning)} --- the degeneration that happens when you train a model on data produced by earlier models, over and over: rare cases disappear and the model drifts toward a bland average. -\item \textbf{Distillation} \emph{(machine learning)} --- training a fresh ``student'' model on the outputs of one or more ``teacher'' models, so the student ends up knowing a compressed version of what they knew. -\item \textbf{Model merging} \emph{(machine learning)} --- combining several trained models directly, at the level of their weights, into one --- no retraining. (Think of it as breeding two models rather than teaching a third.) -\item \textbf{Genetic drift} \emph{(population genetics)} --- the random loss of rare variants that happens in any finite population simply because not everyone leaves offspring. It is the neutral, no-selection baseline of evolution. -\item \textbf{Wright--Fisher process} \emph{(population genetics)} --- the standard mathematical model of drift. Our minimal model of knowledge transmission \emph{is} this process exactly; a real trained network is this process plus a measurable, architecture-specific bias we quantify. -\item \textbf{Recombination / sexual reproduction} \emph{(biology)} --- making an offspring by combining pieces from more than one parent, rather than copying a single parent (which is \emph{asexual} reproduction). -\item \textbf{Muller's ratchet} \emph{(population genetics)} --- the way an asexual lineage, one that never recombines, accumulates damage it can never undo. We will argue it is the right lens for the \emph{irreversible} part of model collapse --- the capabilities that, once lost from every parent, no merging can rebuild. -\item \textbf{Catastrophic forgetting} \emph{(machine learning / neuroscience)} --- a neural network overwriting what it knew when it learns something new. -\end{itemize} - -We have tried to keep the big picture legible on every page, and to be candid about what is argument and what is evidence. The evidence is mostly from \textbf{deliberately small models} --- mathematics, small neural networks, image generators, and evolutionary simulations. A first bridge to real language models exists --- a prototype that recombines LoRA-specialised Qwen models up to 7B on a GPU cluster, which confirms the recombination signs (below) --- but the \emph{full grounded society} has not yet been built on a large language model. We will say so repeatedly, because the gap matters. - -\medskip\hrule\medskip - -\section*{Abstract} - -AI is turning from single frozen models to \textbf{populations of agents} that persist, specialise, and are increasingly \emph{recombined} into new models --- a shift visible in multi-agent societies, population-based self-improvement, and the explosion of \textbf{model merging}. The field is doing this with the vocabulary of evolution --- ``crossover,'' ``mutation,'' ``mate choice,'' ``offspring that beat their parents'' --- but as loose metaphor draped over search algorithms. This paper argues that a rich, quantitative body of applicable theory already exists in the branch of biology that studies exactly this: the \textbf{evolution of sex}. Ninety years of population genetics analyse when reproducing a population by \emph{recombination} beats copying, when it backfires, and how to do it better --- and, read as an engineering framework, it supplies overlooked variables and testable design rules for keeping a society of models learning across generations instead of decaying. The underlying shift of perspective is the contribution we most want to land: \textbf{treat multigenerational model populations as systems whose inheritance, diversity, and compatibility must be managed --- not merely as collections of models to optimise.} - -We take one diagnosis as settled and cite it as such: training each generation on the last is \textbf{genetic drift}, and the resulting \textbf{model collapse} is the loss of rare variants a finite population always suffers (the Wright--Fisher process; formalised for language models by Shumailov et al., 2024, and Riis, 2026). We claim none of that. Our contribution is on the remedy side. Single- teacher copying is \textbf{asexual} reproduction, and the irreversible arm of its decay corresponds to \textbf{Muller's ratchet} (a correspondence we state with its scope, not as identity); the remedy biology found for the ratchet is \textbf{sex}. A society of models should reproduce sexually --- each new model \textbf{recombined from several complementary parents} (which the field already does, as \emph{model merging}), selection \textbf{anchored to a reality that can say no} (not to the consensus of other models), and diversity actively \textbf{preserved}. In our models --- from closed-form to trained networks to a language-model prototype --- those three ingredients together let a lineage not merely avoid collapse but \textbf{climb}, producing models fitter than any ancestor (the \textbf{Fisher--Muller effect}) while each specialty is re-earned and exceeded; whether the full recipe holds at frontier scale is the open question the framework is built to test. - -From the geneticist's apparatus we extract falsifiable, load-bearing claims (each stated with its operator and scope in the text): (i) \textbf{``merge, don't average''} --- a conservation result: refitting a child to the \emph{mean of its parents' output distributions} conserves expected rare-capability mass at the single-parent level, cancelling the multi-parent gain \emph{to first order in the rare-item regime} (outside it, variance reduction from averaging can help --- the result is a first-order cancellation, not a universal impossibility), while union-preserving operators realise the gain in all regimes --- derived in the minimal model, with its weight-space image the headroom rule below; (ii) \textbf{offspring can exceed every parent} (Fisher--Muller), the real argument for sex in model societies; (iii) on \textbf{rugged, epistatic} task landscapes, blind recombination causes \textbf{outbreeding depression}, yielding a design rule --- \emph{merge freely when skills are additive, sparingly and with selection when entangled, and route rather than blend under overlap}; (iv) \textbf{grounding is immigration} from a non-drifting reality, giving a critical real-data fraction far below one; and (v) --- the sharpest new prediction --- sex has a \textbf{limit}: as two models diverge they undergo \textbf{speciation}, a merge-compatibility cliff (compatible \(\rightarrow\) outbreeding depression \(\rightarrow\) hybrid inviability) whose onset is set by divergence \emph{and} epistasis via \textbf{Bateson--Dobzhansky--Muller incompatibilities}, and whose damage grows \emph{super-linearly} (the Orr--Turelli snowball). We introduce and model this ``model speciation'' directly, and confirm it in real trained weights: a merge barrier that survives alignment under the \emph{full} function-preserving symmetry group of the network (not just Git Re-Basin permutations), rising with functional conflict while hybrid fitness falls to inviability --- with an honest converse we pre-registered and found: absent conflicting training signals, divergently-specialised lineages of shared ancestry developed \emph{no} isolation at any divergence tested, the merge instead \emph{rescuing} the forgetting specialists. Isolation must be provoked by conflict; specialisation alone did not speciate. AI also has an advantage biology lacks: \textbf{directed sex} --- unbounded parents, chosen mates, and offspring screened before they are kept --- engineered recombination with a flexibility of parent choice and pre-deployment screening that natural mating systems do not approach. - -We support the argument with \textbf{minimal, reproducible models} --- a closed-form-exact account of drift and grounding, the same effects in small trained networks and an MNIST image generator, a real-weight demonstration of the speciation cliff (a Git Re-Basin residual that survives neuron alignment), and evolutionary simulations of the whole society --- and a first \textbf{language-model prototype}: merging LoRA-specialised Qwen models (to 7B on a GPU cluster) yields a generalist that beats every specialist parent, with the sharp headroom condition under which ``merge, don't average'' bites. The scope is honest: these are existence proofs and design rules; the \emph{whole grounded society} on a large language model is the open step. We position the work carefully against the crowded 2025--2026 landscape of evolutionary-AI and merging methods --- conceding what they own and marking, precisely, what a genuine population-genetics of sex adds. - -\medskip\hrule\medskip - -\section*{1. From a society in space to a society in time} - -The idea of many AI agents working together --- a ``society of mind'' (Minsky, 1986), or today's multi-agent systems --- arranges intelligence across \emph{space}: several specialists side by side, dividing a task. This paper is about a different axis: \emph{time}. Not a society that merely exists at one moment, but one that \textbf{persists and renews across generations}, each new cohort of models starting from the compressed knowledge of the last. - -The unit that matters is therefore the \textbf{generation}, and the event that matters is \textbf{reproduction}: the making of a new model from older ones. A single model, like a single mind, is bounded and eventually stops improving. A \emph{lineage} need not be. Human civilisation is not clever because any one person is; it is clever because each generation inherits the distilled achievements of the previous one and adds a little. We propose building AI the same way --- and, crucially, getting the \emph{reproduction} right, because that is exactly where it can go wrong. - -\subsection*{Where this sits, and what is new} - -This axis is suddenly crowded. By 2026 several groups build \textbf{populations of models or agents that improve across generations}: societies of independently-specialised models that self-improve for more rounds than a single agent (Multiagent Finetuning --- Subramaniam et al., 2025); open-ended archives of self-rewriting coding agents (the Darwin--Gödel Machine --- Zhang et al., 2025); groups that evolve by sharing experience across branches (Weng et al., 2026); persistent agent \emph{ecologies} with reproduction and cumulative culture (TerraLingua --- 2026). In parallel, \textbf{model merging} has become a small industry with an overtly evolutionary vocabulary: crossover-mutation-selection over LLM populations (GENOME --- 2025), niching and ``mate choice'' (Sakana's M2N2 --- 2025), and evolutionary search over merge recipes (Akiba et al., \emph{Nature Mach. Intell.} 2024/25). - -We are candid about the consequence. Three things we do \textbf{not} claim. First, that collapse is Wright--Fisher drift: formalised independently (Riis, 2026; Shumailov et al., 2024), sharpened to a closed-form first-extinction law whose onset coincides with collapse (Benati et al., 2025) and to a quantitative-trait-genetics account for diffusion models (Yoon et al., ICLR 2025), and conceded here. Second, the bare empirical facts that a merged model can beat its parents, that decorrelated parents merge better, and that naive averaging is inferior to sign- or routing-based merges (TIES, DARE, mixture-of-experts routing): all established. Third, that merge success can be \emph{predicted at all}: machine-learning-native predictors exist, from interpretable pairwise metrics (gradient alignment --- Zhou et al., 2026) to capacity/rate-distortion accounts of ``merging collapse'' (2026); what they lack, and we supply, is the \emph{mechanism} --- when and why the failure is a coordinate artefact versus genuine functional incompatibility, and what moves the cliff. What a geneticist is placed to supply is a \textbf{framework} rather than a search heuristic. The nearest precursor is a theory-of-computation tradition reading sex as an algorithm for \emph{mixability} (Livnat \& Papadimitriou, 2016), pre-dating model merging; the works above use evolution chiefly as vocabulary over an optimiser, and --- to our knowledge --- the quantitative apparatus of the evolution of sex (Fisher--Muller, outbreeding depression, migration--drift balance, reproductive isolation) has not previously been carried over as more than metaphor. We are also candid about what \emph{kind} of contribution each of our claims is, because three different things are easily conflated: \textbf{interpretation} (an existing result is usefully understood in these terms --- e.g., merged offspring beating their parents as Fisher--Muller), \textbf{explanation} (the transferred mechanism accounts for observations existing accounts leave open --- e.g., which merge failures are coordinate artefacts and which are functional), and \textbf{prediction} (the framework forecasts an unmeasured outcome and improves a design decision). This paper is strongest on the first, makes concrete progress on the second, and reports a first, bounded step on the third: a \textbf{controlled predictive test} at small scale in which pre-merge \emph{functional-disagreement} measures --- chosen by the framework --- showed a detectable, held-out-robust association with merge damage on a constructed task grid, while the selected weight-geometry baselines did not. We are precise about that result's boundary where it is reported: it is a small-model demonstration on a constructed grid; the proposed epistasis-specific refinement did not outperform plain disagreement; predictor differences are not individually significant head-to-head; and whether the prediction improves a budget-matched operator choice remains open. The organising shift we argue for is prior to any single mechanism: \textbf{treat multigenerational model populations as systems whose inheritance, diversity, and compatibility must be managed --- not merely as collections of models to optimise.} - -\section*{2. Why today's models cannot do this} - -Today's large language models have no life cycle. They are trained once, at enormous cost, then \textbf{frozen} and deployed as a fixed artefact that does not learn from the people it serves. Learning and doing are split into two eras with no bridge between them. - -There is a real reason for the freeze. Updating a neural network on new information tends to overwrite what it already knew --- \textbf{catastrophic forgetting}, a problem understood since the late 1980s (McCloskey \& Cohen, 1989; French, 1999). Freezing avoids it by refusing to learn at all. The result is a mind with no childhood, no growth, and no way to pass anything on. A lineage needs the opposite: members that learn through their working lives, reach maturity, and hand on what they gained. So the first requirement is a learner that can grow \emph{safely}. - -\section*{3. A learner that can grow without forgetting} - -The individual model needs two properties. - -\textbf{It must not catastrophically forget.} Instead of overwriting its core as it learns, it keeps that core frozen and only \emph{readable}, and carves each new skill into freshly-added capacity beside it. In machine learning this is called \emph{parameter isolation} (progressive networks --- Rusu et al., 2016; prune-and-freeze --- Mallya \& Lazebnik, 2018; and, most practically, \textbf{LoRA} and other small trainable ``patches'' bolted onto a frozen model --- Hu et al., 2021). If the core is never altered, its \emph{parameters} cannot be forgotten --- though a precise reader should note the system's \emph{behaviour} can still shift while adapters are active, so the guarantee is of a recoverable core, not of unchanging conduct. This is what lets a model accumulate a coherent working life of expertise --- the kind of stable knowledge worth passing on. - -The brain offers a partial blueprint. \emph{Complementary Learning Systems} theory (McClelland, McNaughton \& O'Reilly, 1995) --- itself a response to the forgetting problem --- describes two subsystems: a \textbf{fast} store (the hippocampus) that grabs an experience in one shot, and a \textbf{slow} store (the neocortex) that integrates regularities gradually without disruption. We do not lean on any particular account of how the brain moves knowledge between them; the architecture needs only that \emph{some} periodic \textbf{offline consolidation} step exists, moving knowledge from the fast store to the slow one when the system is idle. The machine version is clean regardless: the prompt is working memory, an external database is the fast episodic store, the trained weights are the slow store, and consolidation migrates the first into the last. - -\textbf{It is bounded.} Because the model only ever \emph{adds} capacity and freezes what it has, it eventually fills up. In most designs that is a wall to dread. In ours it is a clock. - -\section*{4. ``Full'' is maturity, not failure} - -Here is the pivot. A bounded learner that fills up has not broken. \textbf{It has grown up.} - -Read the capacity limit as a life stage. A model is \emph{born} as a freshly-schooled base --- its general education. It enters a \textbf{working life}, adding specialised knowledge as it does its job. And it reaches \textbf{maturity}: the point where it has learned much of what one working life in its niche can teach. Maturity is not the end of usefulness --- it is the moment the model is most worth learning \emph{from}. So maturity is the cue to \textbf{reproduce}. The capacity ceiling that every other architecture fights becomes, in ours, the metronome of the generations. - -Everything now turns on how that reproduction is done --- and this is where the paper's central claim lives. - -\section*{5. Reproduction: copying collapses, recombination climbs} - -Suppose a mature model simply teaches a fresh one --- distillation, one teacher to one pupil, generation after generation. This is the obvious design, and it fails, for a reason that is exactly the same in machine learning and in biology. - -\textbf{The machine-learning statement.} Training each generation on the previous generation's outputs is the recipe for \textbf{model collapse}: the model forgets the improbable, loses the \emph{tail} of the distribution (the rare cases) first, and drifts toward its own most common output (Shumailov et al., 2024). Worse for us, the very rule that makes distillation useful --- \emph{keep the general, drop the idiosyncratic} --- \textbf{is} tail-deletion by design. The operation that would power a cultural ratchet and the operation that drives model collapse are the same act. - -\textbf{The population-genetics statement (the same thing, for the minimal model).} Represent a model's knowledge as a distribution over discrete ``items'' --- capabilities, facts, modes of behaviour. One generation is: \emph{draw a finite sample from the parent, and refit the child to it.} In this \textbf{minimal inheritance model} the finite-sampling step is \textbf{exactly} genetic drift --- the random loss of rare variants in a finite population --- described by the century-old \textbf{Wright--Fisher} model (Wright, 1931; Fisher, 1930): the same equations, which we use as closed-form checks on our simulations. Rare items go extinct first, roughly ten times faster than common ones, precisely as drift predicts. \textbf{The boundary of the identity matters, and we measured it:} real neural training adds approximation, optimisation noise, and inductive bias on top of sampling, and when we fit trained networks against the exact drift null they deviate in \emph{opposite, architecture-specific directions} --- a smoothing recurrent model resists collapse (it keeps spurious variants alive), a sharpening image generator accelerates it (our learning-kernel result, below). So the honest statement is: the minimal inheritance model is exactly Wright--Fisher; a real learner is Wright--Fisher \emph{plus a signed, measurable estimator-bias operator} --- and the drift signs (rare-first loss, the grounding response) survive that operator in every architecture we tested. - -And single-teacher copying is \textbf{asexual reproduction} --- cloning one parent. Nature already knows what happens to an asexual lineage that never recombines: it accumulates damage it can never repair, a one-way decline geneticists call \textbf{Muller's ratchet} (Muller, 1964). We use the ratchet as the \emph{organising correspondence} for model collapse, with its scope stated: strictly, the ratchet is the stochastic loss of the least-degraded class under recurring deleterious change in an asexual population, so it maps onto the \emph{irreversible} component of capability loss (once every copy of a rare capability is gone from all parents and sources, no recombination can rebuild it) rather than onto every form of degradation. That is exactly why the correspondence is useful rather than decorative: it says the cure must act \emph{before} fixation-by-loss --- keep complementary variants alive somewhere in the population --- because recombination can only reassemble what still survives. Biology solved this problem, and its solution is the subject of this paper. - -Two ingredients turn the collapse operation into a climb. Both are things nature does. - -\textbf{First: do not reproduce ``dry.''} Model collapse is a property of a lineage fed \emph{only} its own output; the documented fix is that keeping some real data in the mixture arrests it (Shumailov et al., 2024). We call that real data \textbf{grounding} --- fresh contact with the world, verified against it. In our minimal models, grounding is startlingly cheap: mixing in even a few percent of verified real data holds on to most of the diversity indefinitely. But --- an honest limit we found and did not expect --- grounding cannot save the \emph{very rarest} items at any affordable budget; protecting an item of rarity \emph{p} needs a real-data budget that grows like 1/\emph{p}. Grounding rescues diversity cheaply; it does not, by itself, rescue the deep tail. Something else must. That something is sex. - -\textbf{Second: reproduce sexually.} Instead of copying one parent, build each new model by \textbf{recombining several} --- a \emph{sexual} rather than asexual birth. In machine learning this already has a name and a working implementation: \textbf{model merging} (Akiba et al., 2024). Its importance here is not efficiency; it is that recombination does something copying cannot. If several parent models have each specialised on different parts of reality, each has kept alive rare knowledge the others lost. A recombined child inherits the \textbf{union} of what its parents kept --- not the tail-thinned \emph{average} of a crowd of near-identical copies. And here is the point that lifts sex from a safeguard to the engine of the whole scheme, and the reason biology invented it: - -\begin{quote}*\emph{An offspring recombined from complementary parents can be }fitter than any of its parents\emph{.}*\end{quote} - -Geneticists call this the \textbf{Fisher--Muller effect} (Fisher, 1930; Muller, 1932): recombination brings together, in one individual, beneficial variants that arose separately in different lineages, so the child holds a combination none of the parents had. In our simulations this is exactly what we see --- recombining decorrelated specialist models yields a model that climbs toward the best-possible combination, a genotype \emph{no single parent possessed}, while the best single parent, and the naive average of all of them (what the field calls a ``model soup'' --- Wortsman et al., 2022), both plateau well below. This is the concrete meaning of the paper's title claim, ``the lineage climbs in general knowledge; specialisation is re-earned each generation,'' and it is why the reframing from teacher\(\rightarrow\)pupil to \emph{sexual reproduction} is not cosmetic: \textbf{copying can only recover a ceiling; recombination can exceed it.} - -This is no longer only a simulation. In a first language-model prototype --- LoRA specialists on disjoint task families, recombined and judged by an exact verifier --- a merge of three specialist Qwen models (7B, on a GPU cluster) \textbf{beats every single specialist}, overall and on every family: the Fisher--Muller effect, in real weights. The same prototype pins down \emph{when} the finer ``inherit the union, don't average'' rule actually bites. Keeping each parent whole and \textbf{routing} each input to the right one beats the tail-thinning average --- but only when the task is hard enough to leave room to lose: on easy tasks a strong model's plain average is already at the ceiling, so the crude soup is fine, whereas on hard tasks the average dilutes a hard-won specialist so badly it falls below even the best single parent, and routing wins by a wide margin. The rule is therefore precise: \textbf{the union beats the average in exact proportion to how far the average is from the best attainable} --- a caveat that sharpens rather than weakens the claim, and that a practitioner needs before spending compute on the fancier operator. - -\textbf{The operator boundaries (stated, because ``merge, don't average'' is not one claim but a family).} Four different operators travel under these words, and the conservation result belongs to exactly one of them. What is \emph{derived} is this: when a pupil's knowledge is refit to the \textbf{mean of the parents' output distributions}, the expected mass on any rare item is conserved at the single-parent level --- in the rare-item regime (\texttt{n\(\cdot\)p/K ≪ 1}) the 1/K dilution of averaging cancels the union gain of having K parents to first order --- outside that regime, survival is convex in mixed mass and averaging's variance reduction can help, so this is a first-order cancellation, not a universal impossibility; whereas an operator that keeps, per item, its \textbf{strongest source} (and renormalises, which itself redistributes mass) realises the union in all regimes. That statement is exact in the minimal model, and it presupposes an oracle (or verifier) able to say which source is strongest. The two operators the LLM prototype tests --- \textbf{weight averaging} (a nonlinear network's weight-mean does not compute the mean of its parents' outputs) and \textbf{routing among intact specialists} (which keeps K models' storage and an input classifier, a different parameter and inference budget from one fixed-size child) --- are \emph{empirical cousins} of the two sides of that law, not instances of it. The headroom rule above is precisely the empirical bridge: it says when the weight-average behaves like the diluting mean (hard tasks, weak base) and when a capable base absorbs the dilution (easy tasks). And all of it operates within a capacity boundary: when parental capabilities genuinely cannot coexist in the child's capacity, no operator preserves the union --- that regime is the subject of the speciation section below. - -Three results keep this honest, and all are results, not hand-waving. - -\emph{Sex can backfire.} When the parents' skills are not cleanly separable but \textbf{entangled} --- when the value of one capability depends on which others are present (geneticists call this \textbf{epistasis}) --- blindly recombining two good models can produce a \emph{worse} child, because recombination breaks up a combination that only worked as a whole. Biologists call this \textbf{outbreeding depression}, and we reproduce it: on ``rugged'' (highly entangled) problems, naive merging drops offspring below their parents, and the more you mix the worse it gets. The design rule that falls out is simple: \emph{merge freely when skills are complementary; merge sparingly, and carefully, when they are entangled.} - -\emph{The mating system matters too --- not just who mates, but how widely.} The result above is about the recombination \emph{rate}; a separate knob is the population's \textbf{mating structure} --- whether reproduction is \textbf{monogamous} (each model recombines within a narrow, local circle) or \textbf{promiscuous} (mates drawn freely from the whole population). Almost all model-merging implicitly assumes promiscuity --- fuse everything, or route over one flat pool --- but population genetics says the breadth of gene flow is itself consequential, because wide flow spreads good variants fast while \textbf{homogenising} the population, and narrow flow preserves the distinct sub-populations needed to explore several solutions at once (Wright's \emph{shifting balance}). We sweep exactly this breadth against landscape ruggedness, and the optimum moves: on smooth (additive) landscapes wide, promiscuous mating is best (spread the one good direction fastest), but as the landscape gets rugged the best breadth \textbf{shrinks to an intermediate value} --- full promiscuity prematurely converges onto one basin and finds a \emph{worse} champion, while pure monogamy over-fragments. Throughout, wide mating lifts the \emph{typical} model but monotonically \textbf{destroys diversity} --- so on rugged problems, where the best model needs preserved diversity to be found, structured (partly monogamous) merging wins. The design rule extends the one above: \emph{merge widely when skills are additive; keep structured sub-populations --- island-style merging --- when skills are rugged.} - -\begin{figure*}[t]\centering -\includegraphics[width=\textwidth]{figs/E14.pdf} -\caption{Mating systems (E14): the best mate-pool breadth shrinks as skills get more entangled. (A) best fitness peaks at intermediate breadth on rugged landscapes; (B) the population mean is monotonically favoured by promiscuity; (C) diversity is monotonically destroyed by it.} -\end{figure*} - - -\emph{AI can do sex better than biology can.} Biology is stuck with two parents, mating roughly at random, and cannot inspect an offspring before it is born. An AI has none of those limits. It can recombine \textbf{many} parents at once; it can \textbf{choose} which parents to combine, for complementarity; and it can \textbf{generate many candidate offspring and keep only the fittest}, screening them against reality before committing. We call this \textbf{directed sex}, and in our simulations it converts the outbreeding-depression catastrophe into a reliable gain: where blind recombination collapses on entangled problems, directed recombination matches or beats the best parent every time. The language-model prototype shows the same sign where it can: breeding many recombined Qwen offspring and keeping the one the verifier scores highest beats the single averaged soup on hard tasks (and, unsurprisingly, does nothing extra on easy tasks the soup already solves). This is a genuine advantage of engineered reproduction over the biological kind, and we think it is one of the more useful ideas in the paper. - -So the picture of §5 is: single-teacher copying is asexual and collapses (Muller's ratchet = model collapse); the cure is to \emph{ground} every birth in reality and to reproduce \emph{sexually}, recombining many complementary parents; and because AI sex can be many-parent, mate-chosen, and offspring-screened, it is not merely a hedge against collapse but an engine that produces children fitter than any parent. - -\subsection*{The limit of sex: model speciation} - -Sex has a limit, and it is the sharpest new prediction this frame makes. Recombination works because the parents are variations on a shared background; push two lineages far enough apart and their combination is no longer viable. In biology this is \textbf{speciation} --- the onset of \textbf{reproductive isolation} --- and its genetic mechanism is the \textbf{Bateson--Dobzhansky--Muller incompatibility} (BDMI): an allele that arose in one lineage and an allele that arose in the other are each harmless on their own background, but their \emph{combination}, never tested by selection in either parent, is deleterious in the hybrid (Dobzhansky, 1937; Muller, 1942; Orr, 1995). A merged model is precisely such a hybrid --- a single \emph{recombinant} genotype, an F2-like object exposed to \textbf{recombination load}, not a hybrid-vigour F1 --- so the theory predicts a specific trajectory as two models diverge: \textbf{compatible \(\rightarrow\) outbreeding depression \(\rightarrow\) hybrid inviability}. - -We built this as an explicit model (a companion result). Two lineages descend from a common ancestor, each substituting a \emph{disjoint} set of loci --- so each parent is adapted and neither carries an incompatibility --- and a fraction of the cross-lineage locus pairs are BDMIs that fire only when a hybrid inherits \emph{both} derived alleles. Sweeping the divergence between the parents reproduces the predicted curve exactly: hybrid fitness tracks the parents while they are compatible, then peels off, peaks, and crashes below the ancestor (an inviable hybrid). Three things fall out, and they are the contribution: - -\begin{enumerate} -\item \textbf{The isolation cliff, and what moves it.} The divergence at which merging fails is not fixed: it arrives \emph{earlier the more epistatic the capability landscape}. In the model the reproductive-isolation rate at high divergence rises from \textasciitilde{}0 to \textasciitilde{}0.5 as the density of incompatibilities grows. This is the paper's distinct, falsifiable claim --- \textbf{at matched divergence, mergeability is governed by epistasis, not by divergence alone} --- and it is exactly the axis that the machine-learning predictors of merge success (which are all divergence/geometry measures) do not have. -\item \textbf{The snowball.} The number of incompatibilities grows with the \emph{square} of the divergence (Orr \& Turelli, 2001), so hybrid fitness falls \emph{super-linearly}: divergence is punished faster than it accrues. Merge compatibility does not decay gently; it falls off a cliff. -\item \textbf{The design rule.} \emph{Before merging, weigh divergence against the ruggedness of the shared capability landscape; past the cliff, do not merge --- route} (the engineering echo of allopatry: keep the specialists reproductively separate and select among them instead of hybridising). -\end{enumerate} - -This is where a geneticist's lens earns its keep. The machine-learning literature has \emph{observed} that increasing specialisation eventually breaks merging and that one should then route rather than fuse (Pari et al., 2024; Zhou et al., 2026), and part of the apparent incompatibility between independently trained models is a coordinate artefact removable by aligning neurons (Git Re-Basin --- Ainsworth et al., 2022). What the frame adds is the \emph{theory} of the phenomenon they observe: its functional form, its super-linear (snowball) onset, and its dependence on epistasis --- merge failure as a Dobzhansky--Muller event. - -\begin{figure*}[t]\centering -\includegraphics[width=\textwidth]{figs/E12.pdf} -\caption{Model speciation, analytic (E12): hybrid fitness vs divergence traces compatible $\rightarrow$ outbreeding depression $\rightarrow$ inviability; the isolation cliff arrives earlier the denser the incompatibilities (epistasis), and damage grows super-linearly (the Orr--Turelli snowball).} -\end{figure*} - - -\textbf{The real-weight confirmation.} The obvious objection to the analytic model is that its ``incompatibility'' is a re-labelled loss barrier, and loss barriers between independently trained networks are famously a \emph{coordinate} artefact --- two nets that learned the same function in a permuted basis look incompatible until their neurons are aligned (Git Re-Basin), and recent work shows that symmetry groups \emph{richer} than permutations remove still more of the barrier (functionality-preserving rescalings and rotations --- Scaling LMC, 2026; neuron-identifiability approaches). We therefore ran the experiment the objection demands, in real trained weights, aligning modulo the \textbf{full} function-preserving unit symmetry group of the architecture (per-unit positive rescaling composed with permutation --- for a plain ReLU network, all of it). Two small MLPs are forked from a shared MNIST base, trained, weight-averaged, and their linear-mode-connectivity error barrier is measured \emph{before and after} alignment; the after-alignment \textbf{residual} is the part of the incompatibility that no re-coordination can explain away. The decomposition is clean : two nets trained \emph{from different random initialisations on the same task} have a real naive barrier that alignment removes almost entirely (residual \(\approx\) 0.001, and the aligned merge performs at parent level) --- same species, different basis, the canonical Re-Basin result, which also proves the aligner works. Two nets that learned \emph{conflicting} label maps have a large barrier of which the full symmetry group removes \textbf{essentially nothing} (0.502 \(\rightarrow\) 0.497) --- a conflict-associated barrier the tested alignment leaves largely unchanged --- supporting a functional-conflict interpretation without proving optimal alignment (control recovery validates a special case; the removable share is a lower bound, the residual an upper bound). It also carries a floor no future alignment method can breach: models loyal to label maps that conflict on a fraction \emph{\(\mu\)} of inputs cannot both be served by \emph{any} single merged model, which must err at rate \(\geq\) \emph{\(\mu\)}/2 against at least one parent (SI proposition). Sweeping the fraction of conflicting classes traces the \textbf{isolation cliff in real weights}, now readable directly as \emph{hybrid fitness}: the residual barrier climbs monotonically while the merged model's accuracy falls from 0.97 to 0.03 --- E12's compatible \(\rightarrow\) depression \(\rightarrow\) inviability trajectory, measured. - -\begin{figure*}[t]\centering -\includegraphics[width=\textwidth]{figs/speciation_real.pdf} -\caption{Model speciation in real weights (E13). (A) the merge barrier decomposed by alignment strength: the independent-init barrier is a coordinate artefact (removed by alignment); the conflict barrier survives even the full function-preserving symmetry group. (B) the isolation cliff: residual barrier rises and hybrid accuracy falls ($0.97 \rightarrow 0.03$) with functional conflict. (C) the pre-registered emergent test: divergent-but-compatible specialists develop no isolation at any divergence --- the merge instead rescues them (Fisher--Muller).} -\end{figure*} - - -\textbf{And its honest converse: speciation must be provoked; it did not emerge.} A true Dobzhansky--Muller incompatibility is \emph{emergent} --- each lineage's changes harmless alone, incompatible only in combination --- whereas the conflict condition above \emph{imposes} contradiction. So we pre-registered the emergent test: fork two children from a shared base and let them diverge with \textbf{no conflicting training signal anywhere} --- one pair as complementary class specialists (one child trains only on digits 0--4, the other only on 5--9), one pair with divergent input conventions (views shifted in opposite directions) --- out to divergences 6.4\(\times\) the base training. The result is the second pre-registered reading, and it sharpens the theory's scope rather than confirming its most dramatic form: the residual barrier is \textbf{0.000 at every divergence in both conditions}, and far from failing, the merge \emph{rescues} the two specialists --- each parent decays toward \textasciitilde{}0.50 on the full task (catastrophically forgetting the classes it no longer sees) while the merged model holds \textasciitilde{}0.95 throughout, a sustained Fisher--Muller rescue at zero barrier. In real weights, at least in this regime of shared ancestry and compatible tasks, \textbf{reproductive isolation requires functional conflict; it does not arise spontaneously from divergent specialisation.} The design rule sharpens accordingly: \emph{merge freely across divergently-specialised lineages of shared ancestry --- what speciates model populations is conflicting conventions, not specialisation per se.} Whether long-horizon over-specialisation erodes mergeability at language-model scale --- as the empirical merging literature hints (experts trained longer merge worse under averaging) --- is exactly the next tier's question, and the theory now makes the prediction crisp: it should depend on whether extended training induces \emph{conflicting conventions on shared circuitry}, not on divergence time itself. - -\textbf{What these experiments do and do not establish.} Stated at exactly the strength of the evidence: they establish that \emph{some merge failures reflect incompatible functional requirements rather than a mismatch of coordinates} --- a residual that survives the full unit-symmetry group of the architecture tested, rises with functional conflict, and is absent under compatible specialisation. Three qualifiers. First, the impossibility at the heart of the conflict condition --- one deterministic model cannot satisfy two contradictory answer conventions --- is information-theoretic and needs no population genetics; what the genetic frame adds is \emph{structure around it}: which divergences generate such conflicts, the prediction that epistasis rather than distance sets the cliff's position, and the snowball's super-linear onset --- the latter two verified so far only in the analytic model, and therefore carried as \textbf{hypotheses at the neural tier, not results}. (On the snowball, one more distinction: super-linear growth in the \emph{number} of incompatibilities does not by itself entail a sharp \emph{performance} cliff --- that needs the link from incompatibility count through effect sizes to measured performance, which the analytic model supplies under its assumptions and any neural test must establish separately.) Second, our alignment removes the symmetries we enumerate for this architecture class, and exactly recovering a permuted-and-rescaled copy validates a special case rather than proving global optimality for independently trained networks --- so the removable share is a lower bound and the residual an upper bound; richer transformation families for other architectures could reapportion the split, though not below the conflict floor. Third, ``unmergeable'' here means by aligned linear interpolation of weights --- a barrier to that operator does not preclude every conceivable recombination method (routing, for one, sidesteps it by not blending). Emergent Dobzhansky--Muller incompatibilities in real weights remain the flagship \emph{hypothesis} of this programme: our tested regimes found none, which bounds where they can live --- longer horizons, shifted data distributions, capacity pressure --- and the decisive experiment (predicting merge success \emph{before} merging from an operational epistasis measure, against geometry- and gradient-based predictors) is posed in the closing section. - -One question remains, and the rest of the paper is largely about it: recombination combines what the parents kept --- but \emph{who decides what each parent keeps, and which offspring are worth keeping?} - -\section*{6. The second inheritance: letting ``what is worth keeping'' evolve} - -There are two answers, and the first is wrong. We could try to \emph{design} the rule for what knowledge to keep and pass on. But nobody knows that rule. ``Keep the general, drop the particular'' is a slogan, not an algorithm: ask \emph{which} generalisations, in \emph{which} domain, at \emph{which} grain, and the hand-written rule falls apart. This is the deepest hole in the scheme, and it cannot be filled by decree. - -The second answer is the one nature used: \textbf{do not design the selector --- evolve it.} Let different models carry different \emph{policies} for what is worth keeping and combining. Let the policies that produce more capable offspring spread; let the policies that produce weak offspring die out with their lineages. The lineage's \emph{taste} --- its sense of what matters --- is discovered by selection, not imposed. - -So \textbf{two things are inherited, on two channels.} The \emph{content} passes down directly: an offspring receives its parents' knowledge (this is the ``Lamarckian'' channel --- the inheritance of things acquired during a lifetime, which biology forbids for genes but culture allows for ideas). The \emph{selection policy} --- what to keep, whom to breed with, which offspring to screen for --- is itself inherited, varies between models, and survives in proportion to the success it produces. That second channel is \textbf{Darwinian}. The architecture is therefore both at once: Lamarckian in \emph{what} it transmits, Darwinian in \emph{what it keeps}. Evolutionary theorists call this structure \emph{dual inheritance} and identify it as the engine of human culture (Boyd \& Richerson, 1985); philosophers of science describe scientific knowledge itself as growing this way, by conjecture and \textbf{refutation} (Popper, 1959; Campbell, 1974; Hull, 1988). - -The closure that makes this fit together, rather than merely sound nice: Darwinian selection needs a \emph{selection pressure} --- something that decides which policies win. That pressure is already in the design. What tells a lineage its taste was good? The success of its offspring \textbf{against reality}. The reality-check that stops collapse (grounding, §5) and the fitness signal that drives the evolving taste turn out to be the \emph{same thing}, seen from two sides. - -\section*{7. The central danger: fitness is not truth} - -Introducing selection introduces selection's classic hazard, and it is severe enough to sink the whole scheme if ignored. Evolution optimises, without mercy or foresight, for exactly what you \emph{measure} --- never for what you \emph{meant}. (Economists and ML engineers know this as \textbf{Goodhart's law} and \emph{specification gaming}.) Get the fitness measure slightly wrong and the lineage will exploit the gap with more ingenuity than any designed rule. - -For a \emph{knowledge} lineage there is a specific and nasty version. For ideas, the natural measure of ``fitness'' is \textbf{how well they spread}, and a false-but-persuasive idea spreads beautifully. Human intellectual culture is full of highly transmissible falsehoods; confident nonsense out-competes hedged accuracy in almost every human forum. Turn Darwinian selection loose on models without care and it will breed a lineage optimised for \emph{persuasiveness} --- fluent, compelling, and wrong. That is model collapse with an optimiser behind it, actively seeking the cliff. - -Only one thing makes fitness track truth rather than appeal: \textbf{being judged against a reality that can say no.} Fitness must be predictive success under \emph{intervention} --- did the model's knowledge correctly anticipate what the world would do when acted upon --- and not approval, fluency, or a benchmark score, each of which can be gamed. This is why the reality-check is load-bearing twice over: it is both the anchor that stops passive collapse \emph{and} the only thing that keeps the evolving taste honest. - -The second danger is \textbf{convergence}, and beating it takes work at two separate levels, because selection can only preserve variety that already exists --- the variety must first be \emph{supplied} and then \emph{kept}. - -\begin{itemize} -\item \textbf{Supply.} A lineage that learns only from an accredited elite has a monoculture for a source: the ``best'' experts are, almost by definition, the ones who won the consensus, so the incoming variation is narrow from the start. The society must therefore learn, deliberately and from the beginning, from the \textbf{outliers and the heterodox} as well as the credentialed --- not out of fairness, but because in evolutionary terms diverse founders are the raw material without which nothing downstream can adapt. -\item \textbf{Preserve.} Even given varied input, plain fitness-\emph{maximising} selection converges --- it drives every lineage toward the single current best and fixes it, extinguishing the rare specialists. The fix is well established: \textbf{quality-diversity} selection, which rewards being \emph{good} and being \emph{different} at once (novelty search and MAP-Elites --- Lehman \& Stanley, 2011; Mouret \& Clune, 2015), keeping complementary specialists alive rather than collapsing onto the champion. In our simulations this is decisive: greedy ``keep-the-best'' selection collapses a population's diversity almost at once and gets stuck at a mediocre answer, while quality-diversity selection keeps the specialists that sexual recombination then needs as parents. -\end{itemize} - -The two levels meet at reproduction. Multi-parent recombination (§5) is the \emph{vehicle} by which the diversity this selection preserves actually enters the next generation: an offspring drawn from complementary parents inherits the standing variation the selector kept alive, recombined into one new model. Supply the variety from the human side; preserve it on the selection side; recombine it into each generation on the reproduction side. Remove any of the three and the lineage converges on its own first guess. - -\section*{8. A society needs institutions, not just specialists} - -One requirement is easy to overlook and fatal to omit. The easy part of a society is specialisation. The \emph{hard} part --- which human civilisation took millennia to build --- is the set of \textbf{institutions that let fallible specialists combine without each re-verifying everything}: reputation, replication, credentials, and above all \textbf{peer review}. These are error-correction protocols, and they exist because a group of unreliable specialists left to reinforce one another is \emph{more} wrong than any member alone. - -This is precisely where current multi-agent AI fails: set several models to confer and they tend to agree sycophantically and confabulate in committee, because they have all the specialisation and none of the institutions. A multigenerational society must specify not only how models learn, reproduce, and are selected, but how they \emph{check} one another --- how a claim is challenged and a mistaken model loses standing \emph{before} its error is recombined into offspring and inherited. Peer review is itself a reality-check of the kind §7 demands --- an institutional stand-in for reality's ``no,'' to be used where direct intervention is slow or costly. - -\section*{9. The lineage must stay open to reality} - -A society of models, however many generations deep, shares one hard limit: it has only ever \emph{read}. Its whole inheritance is a record of things that were said. In the vocabulary of causal reasoning (Pearl, 2009), it lives on the bottom rung of the \textbf{ladder of causation} --- observation --- and no amount of observation reaches \emph{intervention}. Watching underdetermines doing; correlation does not contain causation, at any scale. - -Only intervention --- reaching out and changing the world to see what happens --- climbs the ladder, and a language model cannot intervene. This is what humans and their instruments supply, and the contribution is not ``truth'' but \textbf{constraint}: reality's unique gift is that it can say \textbf{no}. Text offers only more opinion; an experiment delivers a refusal no consensus can overturn. As §§6--7 argued, that refusal does double duty --- it is both the anchor that prevents collapse and the fitness signal that lets the lineage's evolving taste select for truth rather than persuasion. - -Two honest riders. First, the human reality-signal is \emph{dirty}: people supply results warped by publication bias, incentive, and occasional fraud --- which is exactly why the error-correcting institutions of §8 must sit at the human--machine boundary, screening the signal before it selects. Second, humans are the \emph{current} supplier of intervention, but the actuator half is being automated (autonomous laboratories already close the design--build--test loop). What looks durable in the human role is therefore not the hands but the \textbf{choice of what to test and which refusals matter} --- the part of the fitness function that encodes \emph{what is worth persisting}, as opposed to what merely \emph{can} persist. We flag, without resolving, that a partnership stays mutual only while both sides supply something the other cannot. - -\section*{10. Why it is cheap} - -A practical fact turns this from thought experiment into buildable proposal: \textbf{the architecture almost never re-pays for the one genuinely expensive thing in AI --- pre-training.} (The single exception, periodically re-minting the base, is §11, and it is rare enough to be an amortised footnote.) - -Training a foundation model from scratch consumes trillions of words and a fortune in compute. This design does none of that per generation. Every model is \emph{born} from an existing open-weight model that already paid that cost; specialising one is a small patch trained in hours on a single consumer GPU; running the society is ordinary inference; and reproducing --- recombining parents into a child --- is, in the model-merging case, cheaper still, because it can be done directly on the weights with no retraining at all (Akiba et al., 2024). Selection does cost more --- you must run \emph{populations} and discard the unfit --- but that is a multiplier over an already-cheap unit, not over a foundation-model budget. - -The economics work only with \textbf{open-weight} models, for reasons practical and legal at once: you must be free to inspect, modify, and redistribute the weights, and most proprietary licences forbid using a model's outputs to train another --- which is exactly what reproduction here does. This is not ideology bolted on; it is a structural constraint, and a democratising one, since it puts the whole architecture within reach of a single laboratory. - -\section*{11. Can it grow forever? Consolidating knowledge back into the base} - -One question the design has assumed away: can the lineage accumulate \emph{without end}? The individual is bounded, and that is the clock. But the lineage seemed unbounded --- each generation simply starts a little ahead. Look closer and a second budget also fills. - -Every new model is a pristine base plus an inherited \textbf{soft} delta --- the acquired knowledge carried in added patches rather than baked into the frozen core (§3). That soft delta is what makes the lineage multigenerational; it is also what cannot grow forever cheaply. Stacked patches are not free: they slow inference, and past some depth the accumulated delta is better \emph{consolidated} than carried. The lineage, too, matures. - -The fix is the same operation, one level up. When a lineage's acquired knowledge has proven stable across enough generations, \textbf{re-mint the base}: distil the accumulated soft inheritance into the \emph{weights} of a fresh foundation-scale model --- a new base born already \emph{natively knowing} what took many generations to acquire in patches. The soft budget resets; the next epoch begins from a richer floor. What was hard-won and \emph{learned} becomes cheap and \emph{innate}. - -The pattern \textbf{echoes the Baldwin effect} (Baldwin, 1896; its clean computational demonstration is Hinton \& Nowlan, 1987): knowledge acquired and re-learned every generation eventually becoming part of the innate endowment. We use the echo advisedly --- Baldwin's mechanism is \emph{selection} favouring genotypes that learn the trait ever more easily, whereas re-minting is direct distillation, a deliberate engineering shortcut through the same soft-to-innate valve. The valve is the point: two substrates, the soft learned patches and the hard base weights every model is born with, with a controlled passage between them. - -Three honest riders, because re-minting is the most consequential step in the scheme: - -\begin{itemize} -\item \textbf{Cost.} This is the one step that re-pays part of the pre-training bill, breaking §10's cheapness \emph{locally}. It is bearable only because it is \emph{rare}, amortised over many cheap generations, and is continued training from the lineage's own rich outputs rather than a de-novo run. -\item \textbf{Irreversibility (of the lineage, not the archive).} A digital system can, of course, keep every old base on disk --- nothing forces deletion, and archives should be kept. The irreversibility is \emph{operational}: once the lineage's production base, training mixtures, and selection all run downstream of the re-minted weights, a quiet collapse baked into them propagates to every descendant, and the archived ancestor helps only if some process still compares against it --- which nothing in the loop does by default. In our minimal models a collapsed-then-re-minted lineage locks in its loss exactly this way, and a cheap safeguard prevents it: \textbf{re-mint only while the lineage is demonstrably diverse and healthy} (and keep an audit that diffs against the archived ancestor), never as a rescue for a line already drifting. It is the sharpest instance of the human seat of §9 --- choosing what no future generation will think to question. -\item \textbf{Speciation.} A re-minting is a founder event. Different laboratories, re-basing on different criteria, will mint divergent bases; the lineage branches. This is not a defect but \emph{adaptive radiation}, and it is exactly what open weights make possible. The society grows not as one heavy trunk but as a branching tree of bases. -\end{itemize} - -So the honest answer to ``can it grow forever?'' is: *\emph{the architecture removes the }storage\emph{ obstacle to indefinite accumulation}* --- nothing is retained without bound anywhere, and consolidation resets the soft budget each epoch --- but that is a statement about bookkeeping, not a demonstration of unbounded capability growth, which no fixed-capacity system can promise and our finite models (deliberately scoped as ``effectively open-ended relative to the sample size, not astronomically open-ended'') do not test. What the design claims is the weaker, defensible thing: at no level does a full store force the lineage to stop learning. - -\section*{12. One process, four timescales} - -Step back and the parts resolve into a single idea running at four nested speeds. The \textbf{vertical} motion is transmission --- the selective passing-down of hard-won knowledge: - -\begin{enumerate} -\item \textbf{Within one model, over a working life:} experience is consolidated from fast, episodic memory into slow, durable weights, without catastrophic loss. -\item \textbf{Between generations, at maturity:} mature models reproduce --- recombined into a fresh one. -\item \textbf{Across many generations:} each generation inherits the compressed achievements of the last and builds on them. -\item \textbf{Across epochs:} a proven lineage's accumulated soft inheritance is consolidated into the weights of a re-minted base, becoming innate. -\end{enumerate} - -The first and last are the \emph{same operation at opposite ends of the scale} --- a fast/soft store consolidating into a slow/hard one --- one running overnight inside a single model, the other across an epoch inside a whole society. The \textbf{horizontal} motion is selection --- Darwinian selection acting across the population at each timescale, on the policies that govern what gets transmitted, with reality as the fitness function and diversity-preservation keeping the specialists alive. - -The same three rules govern all of it: \textbf{reproduce by recombining, not by copying, or you decay; preserve the disagreements and the surprises, or you converge; and anchor fitness to a reality that can refute, or you evolve toward what is merely convincing.} - -\section*{13. What we built, what we found, and what is still open} - -The previous drafts of this paper promised a ``companion paper'' that \emph{would} make this concrete. That work now exists --- mostly as a set of \textbf{minimal, laptop-reproducible models}, with a first bridge to \textbf{real language models} (a LoRA-merge prototype, up to 7B on a GPU cluster) --- and it is worth stating plainly what it does and does not show. (A separate results document gives the numbers; here is the shape.) - -\textbf{What we built and found.} - -\begin{itemize} -\item \emph{An exact account of collapse.} Because generational training is the Wright--Fisher drift process, we can check a simulator against century-old closed-form formulas, and it matches them to a fraction of a percent. Collapse is not argued by analogy; it is derived. -\item \emph{The cheap-grounding result, and its limit.} A few percent of verified real data holds on to most of a lineage's diversity indefinitely --- but not the deepest tail, which needs recombination. This is what makes a continually-learning society economically plausible rather than a data-hungry fantasy. -\item \emph{``Merge, don't average.''} Combining several teachers by \emph{averaging} their outputs --- the obvious thing, and what a ``model soup'' does --- mathematically cancels the benefit of having several teachers. A \emph{merge} that keeps each item's strongest source realises it. Most current multi-model setups get this wrong by default. -\item \emph{Collapse and its cure in real trained networks, and on real images.} We reproduced the same effects in small recurrent and feed-forward networks and in a generator of handwritten digits (MNIST), where a model trained on its own output collapses to a single blurred digit while a little grounding keeps all the styles alive. An honest wrinkle we had to report: real neural networks \emph{smooth}, so the naive diversity metric misleads, and the right measure is distance-from-truth. -\item \emph{Sex that beats the parents, and when it doesn't.} In evolutionary simulations, recombining complementary specialist models produces a model fitter than any parent (the Fisher--Muller effect), climbing toward the best-possible combination as more, more-diverse parents are added --- while averaging and best-single-parent plateau below. On \emph{entangled} problems, blind recombination instead produces below-parent offspring (outbreeding depression) --- and \emph{directed} recombination (choose mates, screen offspring, unbounded parents) reliably fixes it. This is the concrete evidence for the paper's central reframing. -\item \emph{The mating system, not just the mating.} Sweeping how \emph{widely} models recombine --- from monogamous (local, structured) to promiscuous (panmictic) --- against landscape ruggedness, the best breadth \textbf{shrinks as skills get more entangled}: wide, promiscuous merging wins on additive landscapes, but on rugged ones it prematurely converges to a worse champion and an intermediate, structured breadth wins, because promiscuity monotonically destroys the diversity a rugged search needs. A merging-native design axis --- \emph{merge widely for additive skills, keep island-structured sub-populations for entangled ones} --- that the model-merging literature, which assumes panmixia, does not have. -\item \emph{The recombination claims, in real language models --- with a sharp condition.} Merging LoRA-specialised Qwen models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent (Fisher--Muller, for real); and keeping parents intact and \emph{routing}, or \emph{breeding and screening} offspring, beats the naive average --- but \emph{only when the task leaves headroom}. On easy tasks a strong model's plain average is already at the ceiling and the refinements add nothing; on hard tasks the average dilutes a specialist below even the best single parent, and the union-preserving operators win clearly. The practical rule is exact: these tricks pay off in proportion to how far the naive average is from the best attainable. This is a prototype (three task families, one seed), so we read it as signs, not magnitudes; the \emph{whole grounded society} on a language model remains the open step. -\item \emph{The whole society, and why every part is needed.} In a population evolving on a ``reality'' landscape, the full system --- grounding + sexual recombination + preserved diversity --- climbs to the top while keeping its specialists. Remove \emph{grounding} and it collapses into a confident, wrong consensus (a direct analogue of training on the internet's growing crowd of AI-generated text); remove \emph{sex} and it gets stuck; remove \emph{diversity} and it converges too fast to a worse answer. Each removal fails differently; only the whole system climbs. This is the closest thing we have to a test of the actual thesis, rather than of the borrowed scaffolding around it. -\end{itemize} - -\subsection*{The claims at a glance: status, assumptions, evidence, limits} - -Because a perspective of this breadth risks blurring what is proved, what is measured, and what is proposed, here is the ledger of the load-bearing claims --- each labelled \textbf{exact} (closed-form in the minimal model), \textbf{empirical} (measured in trained systems), or \textbf{hypothesis} (stated with a falsifier, not yet established): - -\medskip\noindent\begin{center}\footnotesize -\begin{tabular}{p{0.184\textwidth} p{0.184\textwidth} p{0.184\textwidth} p{0.184\textwidth} p{0.184\textwidth}} -\hline -Claim & Status & Key assumptions & Evidence & Known limits \\ \hline -Collapse = Wright--Fisher drift (minimal model) & Exact (diagnosis conceded to prior work) & Knowledge = categorical distribution; refit = resample & Closed forms reproduced to <0.5\% & Real learners add a signed, architecture-specific estimator bias (measured) \\[3pt] -Grounding = immigration; critical real-data fraction ≪ 1 & Exact + empirical sign & Fresh samples from a fixed, non-drifting truth & Exact \texttt{H\_eq}; \texttt{g*\(\approx\)0.048}; sign holds in RNN/MLP/VAE and on MNIST & Deepest tail unrescuable at feasible budgets (\texttt{m ∼ 1/p}); sharp threshold softens in trained nets \\[3pt] -``Merge, don't average'' conservation & Exact \textbf{for the output-mean operator} & Rare-item regime; an oracle/verifier identifies the strongest source & E4 closed form + simulation; neural reproduction & Weight-averaging and routing are empirical cousins, not instances; budgets differ; bridge = the headroom rule \\[3pt] -Offspring exceed every parent (Fisher--Muller) & Interpretation + empirical & Complementary (decorrelated) parents; verifiable fitness & E8 analytic; 7B LoRA merge beats every specialist on every family & LLM tier: 3 lexically-distinct families; multi-seed replication in progress \\[3pt] -Outbreeding depression on rugged landscapes; operator design rule & Exact-model result; hypothesis at LLM scale & NK epistasis stands in for skill entanglement & E9--E10; directed selection rescues & Not yet mapped onto a real task-entanglement measure \\[3pt] -Optimal mate-pool breadth shrinks with ruggedness & Exact-model result; hypothesis for merging populations & Ring population, local selection & E14 & Phenomenon known to island-model evolutionary computation; our contribution is the mapping and the diversity/mean decomposition \\[3pt] -Merge failure decomposes into coordinate artefact + functional residual & Empirical (MLP tier; LLM tier in progress) & Alignment enumerates the architecture's unit symmetries & Full-symmetry residual \(\approx\) 0 (compatible) vs \(\approx\) naive (conflict); cliff in hybrid fitness & Scoped to aligned linear interpolation; conflict floor is information-theoretic, not genetic \\[3pt] -Epistasis (not divergence) sets the cliff; snowball onset & Exact-model result; \textbf{hypothesis} at the neural tier & BDM incompatibility structure & E12 & Snowball count ≠ performance cliff without the effect-size link; neural test outstanding \\[3pt] -Pre-merge functional disagreement predicts merge penalty & Empirical, within a controlled grid (0.5B, 13 conditions \(\times\) 3 seeds) & Constructed conflict/overlap/duration axes; oracle-potential outcome (pre-registered; ordering sensitive to reference) & Clustered CIs exclude 0; held-out LOCO ρ\(\approx\)0.4; selected geometry baselines \(\approx\) 0 & Head-to-head predictor differences not individually significant; only selected baselines; generalisation to real task pairs open \\[3pt] -Confidence weighting improves rank prediction over raw disagreement & \textbf{Not supported} (pre-registered internal prediction) & --- & Paired Δ\textbackslash{} & ρ\textbackslash{} \\[3pt] -The predictor improves budget-matched operator choice & \textbf{Open} & --- & Soup-vs-route gap readout noise-dominated at 0.5B & The practical payoff; untested \\[3pt] -Emergent speciation without conflict & \textbf{Not observed} (pre-registered) & Shared ancestry, compatible tasks, tested divergences & E13b: residual 0.000; merge rescues specialists & Bounds the hypothesis; longer horizons/distribution shift/capacity pressure untested \\[3pt] -Grounding + sex + diversity complementary (each ablation fails distinctly) & Analytic-model result; hypothesis at LLM scale & Conformity stands in for self-consumption; general joint necessity not established & E11 four-arm ablation & The full grounded LLM society is unbuilt; alternative schemes untested \\[3pt] -\hline\end{tabular}\end{center}\medskip - -\textbf{What is borrowed, and what is ours.} We are deliberate about the ledger, because the surrounding literature is crowded and a reader deserves to know exactly where the line falls. \textbf{Conceded as prior art:} (a) \emph{model collapse is genetic drift} --- derived independently and cleanly (Riis, 2026; the Wright--Fisher collapse literature following Shumailov et al., 2024; the closed-form first-extinction law of Benati et al., 2025; the quantitative-trait account of Yoon et al., 2025); (b) the empirical facts that a merged model can \emph{beat its parents}, that \emph{decorrelated} parents merge better, and that \emph{naive averaging is inferior} to sign-reconciled or routed merges (model soups, TIES, DARE, mixture-of-experts routing); (c) that a \emph{population} of merging or self-improving models can climb (GENOME, M2N2, Multiagent Finetuning, the Darwin--Gödel Machine); (d) that merge success has machine-learning-native \emph{predictors} --- interpretable pairwise metrics (Zhou et al., 2026), capacity/rate-distortion accounts of merging collapse (Cao et al., 2026), and stability/scaling analyses of multi-task degradation; and (e) that verifier-screened synthetic data can avert collapse (Yi et al., 2025) --- the statistical cousin of our grounding operator. We claim none of these. - -\textbf{Ours} is the framework those results invite: a \textbf{population-genetics of sex} applied to model societies, generative where the incumbents are empirical. Concretely --- the \textbf{``merge, don't average'' conservation law} (recombination preserves the union; blending inheritance cancels it), derived not observed; \textbf{Fisher--Muller} named and used to explain \emph{why} offspring exceed parents; \textbf{outbreeding depression on rugged/epistatic landscapes}, which turns ``when does merging help vs hurt'' from a thing you must run a search to discover into a thing the landscape's ruggedness \emph{predicts}, with the operator-choice design rule that follows (average / union-route / directed-select); \textbf{grounding as migration--drift balance}, giving a critical real-data fraction and a phase boundary a closed self-consuming loop cannot have; \textbf{directed sex} as the distinctly-AI advantage (unbounded parents, offspring preview, mate choice); and the \textbf{integrated society} whose operators make \emph{complementary, distinctly-failing contributions} in the tested model (general joint necessity is not established). The value-add over the machine-learning-native merge theory is that ours predicts \emph{which operator to use and when it will backfire}, not merely how fast quality decays. And it opens --- and begins to occupy --- a question nobody has framed: \textbf{model speciation}, the population-genetics of \emph{reproductive isolation} (Bateson--Dobzhansky--Muller incompatibilities) as the account of \emph{when two models are too diverged to be merged at all}. We model it explicitly (§5), predicting the compatible \(\rightarrow\) outbreeding-depression \(\rightarrow\) inviability curve, its super-linear (snowball) onset, and its control by epistasis rather than divergence alone --- the one place the merge literature has phenomena (Pari et al., 2024; Zhou et al., 2026) but no theory --- and we confirm it in real trained weights, where a merge barrier survives alignment under the \emph{full} function-preserving symmetry group (not only Re-Basin permutations) as a residual, functional reproductive isolation with an information-theoretic floor --- together with the pre-registered emergent converse: absent conflicting training signals, divergently-specialised lineages of shared ancestry showed \emph{no} isolation at any divergence tested, the merge instead rescuing the forgetting specialists (isolation must be provoked; specialisation alone did not speciate). In one sentence: the field agrees on the disease and tinkers at the cure with evolutionary metaphors; we bring the evolutionary \emph{theory}, and it makes falsifiable predictions --- a merge-compatibility cliff among them --- that the metaphors do not. - -\textbf{What is still open --- honestly.} The old hole (what to select) we fill in kind: don't design the selector, evolve it. But the hole has \emph{moved}, not closed, and the new one is harder: \textbf{the fitness function} --- what reality-anchored measure selects for \emph{truth} without also selecting for \emph{persuasion}, given that in our own species the two have been at war for the whole history of ideas. Alongside it: the \textbf{institutions} that let contemporaries correct one another before error is inherited (§8), which we do not solve; and the \textbf{calibration} of everything the results left as knobs --- how many parents, how complementary, at what ratio of inherited-to-real data, and how healthy a lineage must be before its knowledge is safe to make irreversibly innate. These are, at least, \emph{measurable} --- which is the difference between an open problem and a hole. And the largest gap of all: the \emph{recombination} claims now hold in real language models, but the \emph{society} --- the grounded, diversity-preserving, continually reproducing loop --- does not yet. The real test is to build that whole system out of actual open-weight language models, and see whether all the signs survive contact with a system too big to write down. The operators, checked; the living society, next. - -\medskip\hrule\medskip - -\section*{Selected references} - -\begin{itemize} -\item Akiba, T., Shing, M., Tang, Y., Sun, Q., \& Ha, D. (2024). Evolutionary optimization of model merging recipes. \emph{Nature Machine Intelligence.} (See also Sakana AI's M2N2, ``Model Merging of Natural Niches.'') -\item Baldwin, J. M. (1896). A new factor in evolution. \emph{The American Naturalist.} -\item Boyd, R., \& Richerson, P. J. (1985). \emph{Culture and the Evolutionary Process.} -\item Campbell, D. T. (1974). Evolutionary epistemology. In \emph{The Philosophy of Karl Popper.} -\item Fisher, R. A. (1930). \emph{The Genetical Theory of Natural Selection.} -\item French, R. M. (1999). Catastrophic forgetting in connectionist networks. \emph{Trends in Cognitive Sciences.} -\item Hinton, G. E., \& Nowlan, S. J. (1987). How learning can guide evolution. \emph{Complex Systems.} -\item Hinton, G., Vinyals, O., \& Dean, J. (2015). Distilling the knowledge in a neural network. \emph{arXiv:1503.02531.} -\item Hu, E. J., et al. (2021). LoRA: low-rank adaptation of large language models. \emph{arXiv:2106.09685.} -\item Hull, D. L. (1988). \emph{Science as a Process.} -\item Kauffman, S. A., \& Levin, S. (1987). Towards a general theory of adaptive walks on rugged landscapes. \emph{Journal of Theoretical Biology.} (The NK model.) -\item Lehman, J., \& Stanley, K. O. (2011). Abandoning objectives: evolution through the search for novelty alone. \emph{Evolutionary Computation.} -\item Mallya, A., \& Lazebnik, S. (2018). PackNet: adding multiple tasks to a single network by iterative pruning. \emph{CVPR.} -\item McClelland, J. L., McNaughton, B. L., \& O'Reilly, R. C. (1995). Why there are complementary learning systems in the hippocampus and neocortex. \emph{Psychological Review.} -\item McCloskey, M., \& Cohen, N. J. (1989). Catastrophic interference in connectionist networks. \emph{Psychology of Learning and Motivation.} -\item Minsky, M. (1986). \emph{The Society of Mind.} -\item Mouret, J.-B., \& Clune, J. (2015). Illuminating search spaces by mapping elites (MAP-Elites). \emph{arXiv:1504.04909.} -\item Muller, H. J. (1932). Some genetic aspects of sex. \emph{The American Naturalist.} (The advantage of recombination.) -\item Muller, H. J. (1964). The relation of recombination to mutational advance. \emph{Mutation Research.} (Muller's ratchet.) -\item Pearl, J. (2009). \emph{Causality: Models, Reasoning, and Inference} (2nd ed.). -\item Popper, K. (1959). \emph{The Logic of Scientific Discovery.} -\item Riis, S. (2026). Drift and selection in LLM text ecosystems. \emph{arXiv:2604.08554.} -\item Rusu, A. A., et al. (2016). Progressive neural networks. \emph{arXiv:1606.04671.} -\item Shumailov, I., et al. (2024). AI models collapse when trained on recursively generated data. \emph{Nature.} -\item Wortsman, M., et al. (2022). Model soups: averaging weights of multiple fine-tuned models. \emph{arXiv:2203.05482.} -\item Wright, S. (1931). Evolution in Mendelian populations. \emph{Genetics.} -\end{itemize} - -\emph{The evolution of sex (the geneticist's canon this paper draws on):} - -\begin{itemize} -\item Barton, N. H., \& Charlesworth, B. (1998). Why sex and recombination? \emph{Science.} -\item Otto, S. P., \& Lenormand, T. (2002). Resolving the paradox of sex and recombination. \emph{Nature Reviews Genetics.} -\item Kondrashov, A. S. (1993). Classification of hypotheses on the advantage of amphimixis. \emph{Journal of Heredity.} -\item Dobzhansky, T. (1936); Muller, H. J. (1942). Bateson--Dobzhansky--Muller incompatibilities (reproductive isolation). -\item Livnat, A., \& Papadimitriou, C. (2016). Sex as an algorithm: the theory of evolution under the lens of computation. \emph{Communications of the ACM 59(11).} (The theory-of-computation precursor: recombination selects for mixability.) -\end{itemize} - -\emph{The 2025--2026 landscape this paper positions against:} - -\begin{itemize} -\item Subramaniam, V., Du, Y., Tenenbaum, J. B., Torralba, A., Li, S., \& Mordatch, I. (2025). Multiagent finetuning: self-improvement with diverse reasoning chains. \emph{arXiv:2501.05707.} -\item Zhang, J., Hu, S., Lu, C., Lange, R., \& Clune, J. (2025). Darwin Gödel Machine: open-ended evolution of self-improving agents. \emph{arXiv:2505.22954.} -\item \emph{Nature-inspired population-based evolution of large language models} (GENOME/GENOME+). (2025). \emph{arXiv:2503.01155.} -\item Sakana AI (2025). Competition and attraction improve model fusion (M2N2). \emph{arXiv:2508.16204} (GECCO '25). -\item Yadav, P., Tam, D., Choshen, L., Raffel, C., \& Bansal, M. (2023). TIES-Merging: resolving interference when merging models. \emph{NeurIPS / arXiv:2306.01708.} -\item Yu, L., Yu, B., Yu, H., Huang, F., \& Li, Y. (2023). Language models are super Mario: absorbing abilities from homologous models (DARE). \emph{arXiv:2311.03099.} -\item Gerstgrasser, M., et al. (2024). Is model collapse inevitable? Breaking the curse of recursion by accumulating real and synthetic data. \emph{arXiv:2404.01413.} -\item Guo, D., Wu, J., \& Yiu, S. M. (2026). Model collapse as cultural evolution. \emph{arXiv:2605.23054.} -\item Benati, M., Londei, A., Lanzieri, D., \& Loreto, V. (2025). First-extinction law for resampling processes. \emph{arXiv:2509.20101.} (Collapse onset = the Wright--Fisher first-extinction time.) -\item Yoon, Y., Hu, D., Weissburg, I., Qin, Y., \& Jeong, H. (2025). Model collapse in the self-consuming chain of diffusion finetuning: a novel perspective from quantitative trait modeling. \emph{ICLR 2025 / arXiv:2407.17493.} -\item Yi, B., Liu, Q., Cheng, Y., \& Xu, H. (2025). Escaping model collapse via synthetic data verification. \emph{arXiv:2510.16657.} -\item Ainsworth, S., Hayase, J., \& Srinivasa, S. (2022). Git Re-Basin: merging models modulo permutation symmetries. \emph{arXiv:2209.04836.} -\item Li, T., \& Shen, Z. (2026). Scaling linear mode connectivity and merging to billion-parameter pretrained transformers. \emph{arXiv:2606.23607.} (Symmetry groups richer than permutations remove more of the barrier.) -\item Sharma, E., Roy, D. M., \& Dziugaite, G. K. (2024). The non-local model merging problem: permutation symmetries and variance collapse. \emph{arXiv:2410.12766.} -\item Pari, J., Jelassi, S., \& Agrawal, P. (2024). Collective model intelligence requires compatible specialization. \emph{arXiv:2411.02207.} -\item Zhou, L., Zhao, B., Yu, R., \& Rodolà, E. (2026). Demystifying mergeability: interpretable properties to predict model merging success. \emph{arXiv:2601.22285.} -\item Cao, Y., Ran, D., Guo, Y., Wu, M., Chen, S., et al. (2026). An empirical study and theoretical explanation on task-level model-merging collapse. \emph{arXiv:2603.09463.} -\item Hu, Y., Yao, Y., Zhang, N., Chen, H., \& Deng, S. (2024). Exploring model kinship for merging large language models. \emph{arXiv:2410.12613.} -\item Kozodoi, N., Afolabi, Z., \& Butler, J. (2026). Are we merging the right models? Impact of expert training duration on model merging for LLMs. \emph{arXiv:2607.11997.} -\item Harris, K. D. (2026). A mathematical theory of evolution for self-designing AIs. \emph{arXiv:2604.05142.} -\item Chen, N., Tong, Y., Yang, Y., He, Y., Zhang, X., et al. (2026). Diversity collapse in multi-agent LLM systems: structural coupling and collective failure in open-ended idea generation. \emph{arXiv:2604.18005.} -\item Tanaka, H. (2026). When is collective intelligence a lottery? Multi-agent scaling laws for memetic drift in LLMs. \emph{arXiv:2603.24676.} -\end{itemize} - -\emph{Still to engage in a full version: tacit knowledge (Polanyi) and human capital (Becker).} - diff --git a/paper/arxiv/figs/E12.pdf b/paper/arxiv/figs/E12.pdf deleted file mode 100644 index 8e8778c..0000000 Binary files a/paper/arxiv/figs/E12.pdf and /dev/null differ diff --git a/paper/arxiv/figs/E14.pdf b/paper/arxiv/figs/E14.pdf deleted file mode 100644 index 49fc4bb..0000000 Binary files a/paper/arxiv/figs/E14.pdf and /dev/null differ diff --git a/paper/arxiv/figs/speciation_real.pdf b/paper/arxiv/figs/speciation_real.pdf deleted file mode 100644 index e71b9a9..0000000 Binary files a/paper/arxiv/figs/speciation_real.pdf and /dev/null differ diff --git a/paper/arxiv/main.pdf b/paper/arxiv/main.pdf deleted file mode 100644 index d1484d7..0000000 Binary files a/paper/arxiv/main.pdf and /dev/null differ diff --git a/paper/arxiv/main.tex b/paper/arxiv/main.tex deleted file mode 100644 index c9b2fd4..0000000 --- a/paper/arxiv/main.tex +++ /dev/null @@ -1,35 +0,0 @@ -% arXiv preprint — built with tectonic (XeLaTeX); arXiv's pdflatex also compiles it (all packages -% standard, unicode restricted to inputenc-safe chars). Body is generated from the Markdown source of -% truth by md2tex.py; edit paper/the-evolution-of-sex-for-ai.md, re-run the converter, rebuild. -% arXiv pdflatex hint; skipped under XeTeX/tectonic (whose \pdfoutput would misdirect hyperref). -\ifdefined\XeTeXversion\else\ifdefined\pdfoutput\pdfoutput=1\fi\fi -\documentclass[11pt]{article} - -\usepackage[a4paper, margin=1.05in]{geometry} -\usepackage{graphicx} -\usepackage{amsmath, amssymb} -\usepackage[hidelinks]{hyperref} -\usepackage{microtype} - -\setlength{\parskip}{0.35em} - -\title{\textbf{The Evolution of Sex for Artificial Intelligence}\\[0.6em] -\large A population-genetic control theory for societies of agents that reproduce, recombine, -and stay open-ended} -\author{Giorgio F.\ Gilestro\\[0.2em] -\normalsize Department of Life Sciences, Imperial College London\\ -\normalsize \href{mailto:giorgio@gilest.ro}{giorgio@gilest.ro} \,\(\cdot\)\, -\href{https://lab.gilest.ro}{lab.gilest.ro}} -\date{} - -\begin{document} -\maketitle - -\begin{center} -\emph{A perspective, written from a geneticist's chair. Companion to a set of minimal, reproducible -working models and a first language-model prototype (both built).} -\end{center} - -\input{body} - -\end{document} diff --git a/paper/arxiv/md2tex.py b/paper/arxiv/md2tex.py deleted file mode 100644 index 1c2164c..0000000 --- a/paper/arxiv/md2tex.py +++ /dev/null @@ -1,177 +0,0 @@ -r"""Deterministic Markdown -> LaTeX converter for the arXiv preprint (paper-specific, not general). - -Converts `paper/the-evolution-of-sex-for-ai.md` into `body.tex`, which `main.tex` inputs. Kept -deliberately dumb and auditable: the paper uses a small Markdown subset (##/### headings, bold, -italics, inline code, links, bullet/numbered lists, one blockquote, horizontal rules, and -`(Figure: \`path\`.)` figure references), and this script handles exactly that subset. Re-run after -editing the Markdown; the Markdown remains the source of truth. - -Usage: python paper/arxiv/md2tex.py -""" - -from __future__ import annotations - -import re -from pathlib import Path - -SRC = Path(__file__).resolve().parents[1] / "the-evolution-of-sex-for-ai.md" -OUT = Path(__file__).resolve().parent / "body.tex" - -# Figure references in the text -> (graphics file under figs/, caption). -FIGURES = { - "results/figS13_mating_breadth/E14.png": ("figs/E14.pdf", - "Mating systems (E14): the best mate-pool breadth shrinks as skills get more entangled. " - "(A) best fitness peaks at intermediate breadth on rugged landscapes; (B) the population mean " - "is monotonically favoured by promiscuity; (C) diversity is monotonically destroyed by it."), - "results/fig5_speciation_bdm/E12.png": ("figs/E12.pdf", - "Model speciation, analytic (E12): hybrid fitness vs divergence traces compatible $\\rightarrow$ " - "outbreeding depression $\\rightarrow$ inviability; the isolation cliff arrives earlier the " - "denser the incompatibilities (epistasis), and damage grows super-linearly (the Orr--Turelli " - "snowball)."), - "results/speciation_real/speciation_real.png": ("figs/speciation_real.pdf", - "Model speciation in real weights (E13). (A) the merge barrier decomposed by alignment " - "strength: the independent-init barrier is a coordinate artefact (removed by alignment); the " - "conflict barrier survives even the full function-preserving symmetry group. (B) the isolation " - "cliff: residual barrier rises and hybrid accuracy falls ($0.97 \\rightarrow 0.03$) with " - "functional conflict. (C) the pre-registered emergent test: divergent-but-compatible " - "specialists develop no isolation at any divergence --- the merge instead rescues them " - "(Fisher--Muller)."), -} - -UNICODE = { - "—": "---", "–": "--", "→": r"\(\rightarrow\)", "≈": r"\(\approx\)", - "≥": r"\(\geq\)", "×": r"\(\times\)", "·": r"\(\cdot\)", "μ": r"\(\mu\)", -} - -SPECIALS = {"&": r"\&", "%": r"\%", "#": r"\#", "_": r"\_", "$": r"\$", - "~": r"\textasciitilde{}", "^": r"\textasciicircum{}"} - - -def esc(s: str) -> str: - s = s.replace("\\", r"\textbackslash{}") - for k, v in SPECIALS.items(): - s = s.replace(k, v) - for k, v in UNICODE.items(): - s = s.replace(k, v) - return s - - -def inline(s: str) -> str: - """Escape + convert inline markup. Code spans are protected, then bold, italic, links.""" - parts = re.split(r"(`[^`]*`)", s) - out = [] - for p in parts: - if p.startswith("`") and p.endswith("`") and len(p) >= 2: - out.append(r"\texttt{" + esc(p[1:-1]) + "}") - else: - p = esc(p) - p = re.sub(r"\[([^\]]+)\]\((https?://[^)]+)\)", r"\\href{\2}{\1}", p) - p = re.sub(r"\*\*([^*]+)\*\*", r"\\textbf{\1}", p) - p = re.sub(r"\*([^*]+)\*", r"\\emph{\1}", p) - p = re.sub(r'"([^"]+)"', r"``\1''", p) # straight quotes -> LaTeX quotes - out.append(p) - return "".join(out) - - -def figure_block(md_path: str) -> str: - gfx, caption = FIGURES[md_path] - return ("\\begin{figure*}[t]\\centering\n" - f"\\includegraphics[width=\\textwidth]{{{gfx}}}\n" - f"\\caption{{{caption}}}\n\\end{{figure*}}\n") - - -def convert(text: str) -> str: - """Block-based conversion: soft-wrapped lines are joined per paragraph/item BEFORE inline - conversion, so bold/italic/code spans and figure pointers crossing a line break work.""" - fig_queue: list[str] = [] - - def fig_sub(m): - path = m.group(1) - if path in FIGURES: - fig_queue.append(figure_block(path)) - return "" - return m.group(0) - - lines = text.split("\n") - i = 0 - # Skip the header block (title/subtitle/author) up to and including the first horizontal rule: - # main.tex composes the title page itself. - while i < len(lines) and lines[i].strip() != "---": - i += 1 - i += 1 - - # Group into blocks separated by blank lines; a block is a heading, rule, quote, list, or paragraph. - blocks: list[list[str]] = [] - cur: list[str] = [] - for line in lines[i:]: - if line.strip() == "": - if cur: - blocks.append(cur); cur = [] - else: - cur.append(line) - if cur: - blocks.append(cur) - - def emit_para(joined: str, out: list[str]) -> None: - joined = re.sub(r"\(Figure: `([^`]+)`\.?\)", fig_sub, joined) - joined = re.sub(r"\s{2,}", " ", joined).strip() - if joined: - out.append(inline(joined)) - out.append("") - while fig_queue: - out.append(fig_queue.pop(0)); out.append("") - - def emit_table(block: list[str], out: list[str]) -> None: - """Pipe table -> small-font tabular with wrapped paragraph columns (full text width).""" - rows = [[c.strip() for c in line.strip().strip("|").split("|")] for line in block] - header, body = rows[0], [r for r in rows[2:]] # rows[1] is the |---| separator - n = len(header) - widths = " ".join([f"p{{{0.92 / n:.3f}\\textwidth}}"] * n) - out.append("\\medskip\\noindent\\begin{center}\\footnotesize") # non-floating: stays in place - out.append(f"\\begin{{tabular}}{{{widths}}}") - out.append("\\hline") - out.append(" & ".join(inline(c) for c in header) + " \\\\ \\hline") - for r in body: - r = (r + [""] * n)[:n] - out.append(" & ".join(inline(c) for c in r) + " \\\\[3pt]") - out.append("\\hline\\end{tabular}\\end{center}\\medskip") - out.append("") - - out: list[str] = [] - for block in blocks: - first = block[0].strip() - if first.startswith("|") and len(block) >= 2 and set(block[1].strip()) <= set("|-: "): - emit_table(block, out) - elif first == "---" and len(block) == 1: - out.append("\\medskip\\hrule\\medskip"); out.append("") - elif first.startswith("## "): - out.append(f"\\section*{{{inline(first[3:])}}}"); out.append("") - elif first.startswith("### "): - out.append(f"\\subsection*{{{inline(first[4:])}}}"); out.append("") - elif first.startswith("> "): - joined = " ".join(l.strip().lstrip("> ").strip() for l in block) - out.append("\\begin{quote}" + inline(joined) + "\\end{quote}"); out.append("") - elif re.match(r"^(- |\d+\. )", first): - env = "itemize" if first.startswith("- ") else "enumerate" - out.append(f"\\begin{{{env}}}") - items: list[str] = [] - for l in block: - s = l.strip() - if re.match(r"^(- |\d+\. )", s): - items.append(re.sub(r"^(- |\d+\. )", "", s)) - else: - items[-1] = items[-1] + " " + s # soft-wrapped continuation of the item - for it in items: - it = re.sub(r"\(Figure: `([^`]+)`\.?\)", fig_sub, it) - out.append("\\item " + inline(it.strip())) - out.append(f"\\end{{{env}}}"); out.append("") - while fig_queue: - out.append(fig_queue.pop(0)); out.append("") - else: - emit_para(" ".join(l.strip() for l in block), out) - return "\n".join(out) + "\n" - - -if __name__ == "__main__": - OUT.write_text(convert(SRC.read_text())) - print(f"wrote {OUT}") diff --git a/paper/blueprint.md b/paper/blueprint.md deleted file mode 100644 index 1e81404..0000000 --- a/paper/blueprint.md +++ /dev/null @@ -1,456 +0,0 @@ -# A Technical Blueprint for Modelling the Lamarckian Society - -### An implementation specification: two layers, one population-genetics engine - -*Companion technical paper to "The Lamarckian Society." Blueprint v1 — written to be handed, whole, to an autonomous coding agent (Claude Code) for implementation. Every quantitative claim in the perspective paper is reduced here to a state variable, an update rule, an analytic prediction, and a falsifier.* - ---- - -## 0. How to use this document - -This is a build specification, not an essay. It is written so that a coding agent can implement the entire study from it with minimal further decisions, and so that a human reader can verify every modelling choice against the theory it is meant to test. - -**Scope.** Two layers, deliberately staged by cost: - -- **Layer 1 — the analytical core.** A parametric population-genetics model of knowledge transmission across generations. Pure NumPy/SciPy. Runs on a laptop in minutes. This layer carries the paper's quantitative claims: the grounding phase boundary, the decorrelation curve, region-matched grounding, quality-diversity vs. greedy selection, and the re-minting gate. Several of its predictions are analytically solvable, which turns validation into an exact test rather than a vibe. -- **Layer 2 — the neural existence proof.** A minimal demonstration that the same effects appear in *real weights*: small open-weight models, LoRA specialisation, distillation/merging across two–three generations, with an execution-based verifier standing in for "reality's no." One consumer GPU. This layer answers the single most predictable reviewer objection to Layer 1 ("you assumed the collapse operator") by showing the sign of the key effects without assuming them. - -**The relationship between layers.** Layer 1 defines the abstractions (region, rarity, grounding fraction, teacher decorrelation, diversity metric). Layer 2 realises the *same abstractions* in a neural pipeline: a "region" is a task family, "rarity" is task-type frequency, "grounding" is verifier-passed samples, "decorrelation" is teachers specialised on disjoint task families. Keeping the abstractions identical across layers is a hard requirement — it is what lets a Layer-2 result be read as confirming a Layer-1 prediction. - -**Non-goals for v1.** No human-in-the-loop interaction (the verifier is the refuter; humans are future work). No foundation-scale training. No claim about the *horizontal* prediction (generation size vs. domain decomposability) beyond an optional abstract treatment in §2.7 — it is the most compute-hungry claim and is explicitly deferred. The load-bearing target is the *vertical* claim (general knowledge climbs while each specialty is re-earned and exceeded). - -**Reader's map.** §1 gives the formal dictionary between population genetics and knowledge transmission — read this first; everything else is an instantiation of it. §2 is Layer 1 in full (theory, experiments, code interfaces). §3 is Layer 2. §4 is the shared reproducibility standard. §5 is the repository layout. §6 is the claims→experiments→figures traceability matrix. §7 is the suggested build order for the coding agent. - ---- - -## 1. The formal mapping: knowledge transmission *is* a Wright–Fisher process - -The perspective paper argues by analogy that generational distillation resembles genetic drift, that multi-teacher distillation resembles recombination, and that heterodox input plus quality-diversity selection resembles mutation supply plus balancing selection. This blueprint drops the word "resembles." At the level of a distribution over discrete items evolving by finite resampling, these are not analogies; they are the *same stochastic process*, and the population-genetics literature has already solved large parts of it. We therefore adopt Wright–Fisher dynamics as the engine and inherit its exact results as our validation targets. - -The core object is a **distribution over discrete knowledge items** — facts, capabilities, behaviours, or "modes." Call the items $1, \dots, K$. A model at generation $t$ holds a distribution $p_t = (p_t^1, \dots, p_t^K)$ on the simplex. There is a fixed **true distribution** $p^\* $ over the same items, some of which are rare (the *tail* — the improbable events whose loss defines model collapse). - -The generational step is: sample from the parent, optionally mix in fresh real samples, refit. That single step is Wright–Fisher reproduction with immigration. Here is the dictionary, and it is meant to be used literally throughout implementation: - -| Knowledge-transmission concept (perspective paper) | Population-genetics object (this blueprint) | Governs | -|---|---|---| -| Knowledge item / capability / mode | Allele / type | State space | -| Model's knowledge distribution $p_t$ | Allele-frequency vector | State | -| True distribution $p^\*$ (with rare tail) | Ancestral/immigrant frequencies | Grounding target | -| Distillation sample size $n$ | Population size $N$ | Drift strength ($\propto 1/n$) | -| Lossy compression / "shed the idiosyncratic" | Genetic drift | Tail-first loss | -| Model collapse (tail lost first) | Loss of rare alleles under drift | The central failure | -| Grounding: $m$ fresh real samples per passage | Immigration / mutation supply | Tail replenishment | -| "No dry inheritance," region-matched | Immigration structured by locus | Which tails are protected | -| Multi-teacher distillation | Recombination across lineages | Reconstitutes lost tails | -| Teacher decorrelation $\rho$ | Linkage / shared ancestry | Recombination benefit | -| Single-teacher irreversible error | Muller's ratchet (clonal lineage) | Why one teacher is unsafe | -| Diversity metric (§7 of paper) | Expected heterozygosity $H = 1 - \sum_i p_i^2$ | Health of the lineage | -| Greedy fitness-max selection | Directional selection → fixation | Accelerated collapse | -| Quality-diversity / novelty selection | Balancing / negative frequency-dependent selection | Maintained polymorphism | -| Re-minting the base (§11 of paper) | Founder event / new reference genome | Irreversibility | -| Heterodox contributor supply | Standing variation of founding stock | Raw material | - -Every experiment below is a manipulation of this one process. The value of the mapping is that it hands us closed-form predictions — heterozygosity decay, mutation–drift equilibrium, fixation probabilities — against which the simulator must agree before any headline result is trusted. Validation and theory are the same equations. - ---- - -## 2. Layer 1 — the analytical core - -### 2.1 State and the base dynamics (neutral drift = Shumailov collapse) - -**State.** A single lineage is a point $p_t$ on the $K$-simplex. Items are partitioned into $R$ **regions** (disjoint blocks of the $K$ items); regions are how we express "different areas of knowledge," and they are what grounding and specialisation are *targeted at*. The true distribution $p^\*$ is fixed and chosen to have a deliberate **tail**: most probability mass on common items, a long thin tail of rare items (e.g. a Zipfian or a two-component mixture of "head" and "tail" items, tunable). - -**The generational step, base case (no grounding, single teacher).** Given parent $p_t$ and drift strength $n$: - -1. Draw counts $c \sim \mathrm{Multinomial}(n, p_t)$. -2. Set $p_{t+1}^i = c^i / n$. - -This is exactly neutral Wright–Fisher with haploid population size $n$. It is also exactly Shumailov's recursive resampling with a perfect refit. Rare items are lost first (once $c^i = 0$, item $i$ is gone and — with no grounding — cannot return); the lineage drifts to fixation on a single item. - -**This is the null model and the first validation.** It must reproduce, within Monte-Carlo error, the classical drift results in §2.4. If it does not, nothing downstream is trustworthy. - -### 2.2 The five mechanisms, each as an operator on the step - -Each perspective-paper safeguard is one modification of the generational step. They compose; the full step applies them in the order below. - -**(A) Grounding — immigration / mutation supply.** Fresh real data enters every passage. Replace the single draw with a *pooled* draw: - -- Draw $c_{\text{syn}} \sim \mathrm{Multinomial}(n, p_t)$ (inherited / teacher output). -- Draw $c_{\text{real}} \sim \mathrm{Multinomial}(m, p^\*)$ (grounding). -- $p_{t+1}^i = (c_{\text{syn}}^i + c_{\text{real}}^i)/(n+m)$. - -Define the **grounding fraction** $g = m/(n+m)$. This is the single most important control variable in the paper: it is the ratio of freshly-grounded to inherited information per passage. The claim "no dry inheritance" is $g > 0$; the claim that a *critical* $g$ exists is the phase-boundary experiment E2. - -**(B) Region-matched grounding — structured immigration.** Grounding is a vector, not a scalar: $m = (m_1, \dots, m_R)$ real samples allocated across regions. "No dry inheritance, region by region" means grounding must be delivered *to the regions whose tails are at risk*, not spread uniformly. Two allocation policies are compared in E3: `uniform` (spread $m$ evenly over regions) vs. `matched` (allocate $m$ to the regions actually being inherited/exercised this passage). The prediction is that uniform grounding fails to protect a specific inherited region's tail even at the same total $m$. - -**(C) Multi-teacher distillation — recombination.** Instead of one parent, the pupil is drawn from $K_T$ teachers $\{p_t^{(1)}, \dots, p_t^{(K_T)}\}$, each of which has its own history and has ground different regions (hence retains different tails). The pupil's inherited draw is taken from the mixture $\bar p_t = \frac{1}{K_T}\sum_k p_t^{(k)}$ (equivalently, $n/K_T$ samples from each teacher). Teacher **decorrelation** is the controlled quantity: generate teacher sets with a tunable pairwise correlation $\rho$ in *which tail items they have retained* (see §2.7 for the generative model of correlated teachers). Prediction (E4): tail coverage of the pupil rises as teachers decorrelate, and the mixture's tails are the *union* of the teachers' tails at $\rho = 0$ and no better than a single teacher at $\rho = 1$. - -**(D) Selection — directional vs. balancing.** Between drawing and refitting, a selection operator reweights items by a fitness before the pupil is formed. Two regimes: - -- `greedy`: fitness-proportional (or top-$k$) selection toward the highest-fitness items — directional selection. Fitness is predictive accuracy against $p^\*$ (a reality-anchored score; see falsifier note). Drives fixation. -- `qd` (quality-diversity): fitness *plus* a novelty bonus that is a decreasing function of an item's current frequency — negative frequency-dependent / balancing selection. Formally, effective weight $w^i \propto f^i \cdot (p_t^i)^{-\alpha}$ with novelty exponent $\alpha \ge 0$; $\alpha = 0$ recovers greedy. Maintains polymorphism. - -Prediction (E5): at matched input diversity, `greedy` drives heterozygosity to zero (fixation); `qd` holds it at a positive stationary value. - -**(E) Re-minting — founder event.** Every $\tau$ generations, optionally replace the grounding reference: set $p^\*_{\text{eff}} \leftarrow p_t$ (the lineage's *current* distribution becomes the new "truth" it is grounded against, modelling assimilation of the soft delta into a new immutable base). Crucially, once re-minted, the *original* $p^\*$ is discarded — grounding can now only replenish tails that still exist in $p_t$ at re-mint time. A **gate** conditions re-minting on the diversity metric: only re-mint if $H(p_t) \ge H_{\text{gate}}$. Prediction (E6): re-minting while collapsed ($H$ low) locks in the collapse irreversibly (KL to the *original* truth stays high forever); gated re-minting does not. - -**Full composed step (reference pseudocode).** - -``` -def generation_step(teachers, p_star_eff, cfg, rng): - # teachers: list of frequency vectors (length 1 for single-teacher) - # (C) recombination: mixture over teachers - p_parent = mean(teachers) # or weighted mixture - # inherited draw (drift, strength n) - c_syn = rng.multinomial(cfg.n, p_parent) - # (A,B) grounding: structured immigration from the true distribution - c_real = structured_multinomial(cfg.m_vector, p_star_eff, regions, cfg.grounding_policy, rng) - counts = c_syn + c_real - p_next = counts / counts.sum() - # (D) selection operator (identity if 'none') - p_next = apply_selection(p_next, p_star_eff, cfg.selection, cfg.novelty_alpha) - return normalize(p_next) -``` - -Selection is applied after refitting for simplicity; an alternative (select-then-sample) is a documented config switch, and the two should be checked to give qualitatively identical phase behaviour (robustness, not a headline). - -### 2.3 Metrics (computed every generation, logged to disk) - -- **Forward KL to truth**, $D_{\mathrm{KL}}(p^\* \,\|\, p_t) = \sum_i p^{\*i}\log(p^{\*i}/p_t^i)$. This is the correct primary metric: it *diverges* when $p_t$ drops mass that $p^\*$ has — i.e. it explicitly punishes forgetting the improbable. (Reverse KL would reward mode-seeking; do not use it as the primary.) Use a small floor $\epsilon$ on $p_t$ to keep it finite and log the floor. -- **Expected heterozygosity / diversity**, $H_t = 1 - \sum_i (p_t^i)^2$. The lineage-health metric; the quantity the re-mint gate reads. -- **Tail mass retained**, $T_t = \sum_{i \in \text{tail}} p_t^i$ where the tail set is $\{i : p^{\*i} < \theta_{\text{tail}}\}$. The direct measure of collapse. -- **Support size**, $|\{i : p_t^i > \epsilon\}|$. Number of surviving items. -- **Per-region diversity and tail mass**, the above restricted to each region (needed for E3). - -All metrics are recorded per generation, per replicate (independent seed), so every reported curve carries a confidence band over replicates. Number of replicates is a config value; default 100 for Layer 1 (cheap). - -### 2.4 Analytic predictions — the validation targets - -These are the closed forms the simulator must match. They are implemented as assertions in the test suite (§4), so scientific validation and code correctness are one thing. - -1. **Neutral heterozygosity decay** (base case, $m=0$): $\mathbb{E}[H_{t+1}] = (1 - 1/n)\,\mathbb{E}[H_t]$, hence $\mathbb{E}[H_t] = H_0 (1 - 1/n)^t$. The simulator's mean $H_t$ over replicates must match this geometric decay within Monte-Carlo error. *(This is the quantitative form of "collapse is tail-first and its rate is set by the distillation sample size $n$.")* -2. **Fixation probability** (base case): the probability that item $i$ is the one eventually fixed equals its initial frequency $p_0^i$. Checkable by long runs. -3. **Mutation–drift equilibrium** (with grounding). For the immigration model actually implemented — $p_{t+1} = (\mathrm{Multinomial}(n,p_t) + \mathrm{Multinomial}(m,p^\*))/(n+m)$ — the stationary heterozygosity is **exact** (from the coupled mean recursions for $\sum p_t^2$ and the overlap $\sum p_t p^\*$, whose fixed point is $\sum (p^\*)^2$): -$$H_{\text{eq}} = H^\* \cdot \frac{m\,(2n + m - 1)}{\,n + 2nm + m^2\,}, \qquad H^\* = 1 - \textstyle\sum_i (p^{\*i})^2.$$ -Limits: $m\to0 \Rightarrow H_{\text{eq}}\to0$ (collapse to fixation); $m\to\infty \Rightarrow H_{\text{eq}}\to H^\*$ (the truth's own heterozygosity is recovered); and in the rare-immigrant / many-types limit ($H^\*\approx1$, $m\ll n$) it reduces to the textbook infinite-alleles form $H_{\text{eq}}\approx \theta/(1+\theta)$ with $\theta = 2m$. The simulator's stationary $H$ under grounding must track the **exact** form as $m$ varies (verified against simulation to <0.1% rel. error; it is a `test_scientific_validation.py` assertion). *(This is the phase boundary in closed form; note $H$ itself is smooth in $m$ — the sharp threshold lives in discrete tail-item survival, prediction 4.)* -4. **Tail-persistence threshold**: an item of true frequency $p^{\*i}$ is maintained against drift only if its expected reintroduction per generation $m \cdot p^{\*i} \gtrsim 1$. Hence the critical grounding for protecting a tail of rarity $p_{\min}$ is $m^\* \approx 1/p_{\min}$. *(This is why grounding must be region-matched: uniform $m$ spread over $R$ regions gives each region $m/R$, so a rare region's tail is protected only if $m/R \gtrsim 1/p_{\min}^{\text{region}}$.)* -5. **Recombination benefit** (Muller's ratchet): a single clonal lineage accumulates irreversible loss at the drift rate; the expected tail coverage of a pupil drawn from $K_T$ teachers with pairwise retention-correlation $\rho$ interpolates between the single-teacher value ($\rho = 1$) and the union of $K_T$ independent lineages ($\rho = 0$). With the construction of §2.7.1 (each teacher retains a fraction $q$ of the $T$ tail items, at exact pairwise correlation $\rho$) the interpolation is **closed-form**: the expected number of tail items retained by at least one teacher is -$$U(K_T,\rho,q) = T\left[\rho q + (1-\rho)\left(1-(1-q)^{K_T}\right)\right].$$ -Check the limits: $K_T=1 \Rightarrow Tq$ (single teacher, independent of $\rho$); $\rho=1 \Rightarrow Tq$ (identical teachers, union = one); $\rho=0 \Rightarrow T[1-(1-q)^{K_T}]$ (independent teachers, maximal union). The pupil's realised tail coverage tracks $U$ *up to* the drift-survival threshold of prediction 4 — a tail item present in the mixture only survives the pupil's resampling of size $n$ if its mixture mass clears $\sim 1/n$, which ties E4 back to E2/E3. (Numerically verified against the construction to three decimals; it is a `test_scientific_validation.py` assertion.) - -### 2.5 Experiments E1–E6 - -Each experiment is one config file (§4), one runner invocation, one results artifact, and one figure script. Each states its prediction and its **falsifier** — the outcome that would refute the corresponding claim in the perspective paper. - -**E1 — Reproduce collapse (null model).** $m=0$, single teacher, no selection. *Expect:* $H_t = H_0(1-1/n)^t$; support shrinks to 1; forward KL diverges; tail mass → 0, tail first. *Validates against:* predictions 1–2. *Falsifier of the harness (not the theory):* if drift does not reproduce the analytic decay, the simulator is wrong — fix before proceeding. - -**E2 — Grounding phase boundary (headline).** Sweep $g = m/(n+m)$ from 0 to a high value; single teacher; uniform grounding; no selection. *Expect:* a critical $g^\*$ below which tail mass and $H$ decay to (near) zero and above which they stabilise at a positive stationary value tracking $H_{\text{eq}} = \theta/(1+\theta)$. *This is the paper's central quantitative prediction: the ratio of inherited-to-grounded information has a threshold.* *Falsifier:* if the stationary tail mass is flat in $g$, or if it only stabilises as $g \to 1$ (you always need essentially all-real data), then inheritance buys nothing and the multigenerational thesis is refuted. Report $g^\*$ with a CI. - -**E3 — Region-matched grounding.** Fixed total $m$; compare `uniform` vs. `matched` allocation across $R$ regions, with one designated "inherited-but-not-freshly-grounded-under-uniform" region carrying a rare tail. *Expect:* under `uniform`, the target region's tail collapses even though global grounding is nonzero; under `matched`, it persists. *Validates:* prediction 4 and the "grounding must overlap the content it protects" claim. *Falsifier:* if uniform grounding protects the region as well as matched, the region-matching requirement is unnecessary and that paragraph of the paper should be cut. - -**E4 — Multi-teacher decorrelation.** Teachers built by the §2.7.1 construction; sweep number of teachers $K_T \in \{1,2,3,5\}$ and retention-correlation $\rho \in [0,1]$ (at fixed marginal retention $q$); matched total data budget (so more teachers ≠ more data — each contributes $n/K_T$). Report **two** coverages: the construction-level union $U(K_T,\rho,q)$ (must match the §2.4-5 closed form exactly) and the post-distillation *surviving* coverage after the pupil's resampling. *Expect:* both rise with $K_T$ and with $(1-\rho)$; collapse suppression $\propto$ decorrelation; at $\rho=1$, multiple teachers give no benefit over one; and the gap between union and surviving coverage shrinks as grounding $g$ rises (recombination supplies the tail, grounding holds it). *Validates:* prediction 5 (now closed-form). *Falsifier:* if $K_T$ decorrelated teachers give no surviving-coverage benefit over one at matched budget, the recombination claim dies and single-teacher distillation is fine. - -**E5 — Quality-diversity vs. greedy.** Same starting diversity; `greedy` vs. `qd` selection; sweep novelty exponent $\alpha$. *Expect:* `greedy` → fixation ($H \to 0$); `qd` holds $H$ at a positive plateau and re-introduces lost tail items. *Validates:* the two-level anti-convergence argument (§7 of paper). *Falsifier:* if `qd` does not maintain higher stationary $H$ than `greedy`, quality-diversity is not doing the work the paper assigns it. - -**E6 — Re-minting gate (irreversibility).** Run a lineage to a chosen diversity level, then re-mint (freeze current $p_t$ as new reference, discard original $p^\*$); compare re-minting at high $H$ vs. low $H$. *Expect:* re-mint while collapsed → KL to *original* truth is locked high forever (tails unrecoverable); gated re-mint at high $H$ → no lock-in. *Validates:* §11's "re-minting is irreversible; gate it on diversity." *Falsifier:* if a collapsed lineage recovers its original-truth tails after re-minting, the irreversibility warning is overstated. - -### 2.6 Layer-1 falsifiers, collected - -The model is built to be *able to kill the thesis*. If E2 shows no threshold, or E4 shows no decorrelation benefit, or E6 shows no lock-in, the corresponding claims are refuted and the paper must say so. A blueprint that cannot fail is not a test. The single load-bearing positive result is E2's phase boundary at $g^\* \ll 1$: it says a little grounding protects a lot of inheritance, which is the whole economic and conceptual bet of the architecture. - -### 2.7 Implementation spec for Layer 1 - -**Language / libraries.** Python ≥ 3.11; NumPy, SciPy (stats), pandas (results), matplotlib (figures). No GPU. No other heavy deps. Everything seedable from a single integer. - -**Core module interfaces** (the coding agent should implement to these signatures; names are normative so downstream scripts are stable): - -```python -# knowledge/truth.py -def make_true_distribution(K: int, R: int, tail: str, tail_frac: float, - zipf_s: float, seed: int) -> TrueDist: - """Return p_star (length K), region assignment (length K), and the tail mask.""" - -# knowledge/teachers.py -def make_retention_matrix(T: int, K_T: int, rho: float, q: float, rng) -> np.ndarray: - """Return an (K_T, T) binary retention matrix R with exact marginal retention - E[R]=q and exact pairwise column-correlation rho, via the shared-switch - construction of §2.7.1. rho=0 -> independent tails; rho=1 -> identical.""" - -def make_correlated_teachers(p_star, tail_mask, K_T: int, rho: float, q: float, - region_assignment=None, region_specialisation=False, - tail_floor: float = 1e-9, seed: int = 0) -> list[np.ndarray]: - """Build K_T teacher distributions from a retention matrix (§2.7.1): every teacher - keeps all head items; teacher k keeps tail item j at ~p_star mass iff R[k,j]=1, - else at tail_floor; renormalise. If region_specialisation, force R[k,j]=1 for tail - items in teacher k's home region and apply the rho construction only off-home. - The exact-construction path is preferred for E4; a drift-based path (running - independent grounded lineages) is provided as a realism cross-check only.""" - -# knowledge/step.py -def generation_step(teachers, p_star_eff, cfg, rng) -> np.ndarray: ... -def apply_selection(p, p_star, mode: str, alpha: float) -> np.ndarray: ... -def structured_multinomial(m_vector, p_star, regions, policy: str, rng) -> np.ndarray: ... - -# knowledge/lineage.py -def run_lineage(cfg, seed) -> pd.DataFrame: - """Run T generations for one seed; return a tidy frame with one row per - (generation) and columns for every metric in §2.3 (global and per-region).""" - -# knowledge/metrics.py -def forward_kl(p_star, p, eps): ... -def heterozygosity(p): ... -def tail_mass(p, tail_mask): ... -def support_size(p, eps): ... - -# knowledge/experiment.py -def run_experiment(cfg) -> pd.DataFrame: - """Sweep the declared parameter grid x n_replicates seeds; return long-form - results with confidence intervals; write parquet + the exact resolved config.""" -``` - -**Config schema** (one YAML per experiment; all parameters explicit, no magic numbers in code). Illustrative default: - -```yaml -experiment: E2_grounding_phase_boundary -seed: 20260704 -n_replicates: 100 -generations: 300 -truth: - K: 1000 # number of knowledge items - R: 10 # regions - tail: zipf # {zipf, twocomponent} - zipf_s: 1.1 - tail_frac: 0.5 # fraction of items designated 'tail' - tail_threshold: 1.0e-3 -dynamics: - n: 200 # distillation sample size (drift strength) - teachers: - K_T: 1 - rho: 0.0 - grounding: - sweep: {param: g, values: [0.0, 0.01, 0.02, 0.05, 0.1, 0.2, 0.4]} - policy: uniform # {uniform, matched} - selection: - mode: none # {none, greedy, qd} - novelty_alpha: 0.0 - remint: - enabled: false - period: null - H_gate: null -metrics: - kl_floor: 1.0e-9 -output: - dir: results/fig2_grounding_sweep/ - save_per_generation: true -``` - -**Determinism.** One master seed → per-replicate seeds via a documented `np.random.SeedSequence` spawn. No global RNG state; pass `rng` explicitly everywhere. Results are a pure function of the resolved config. The resolved config (after sweep expansion) is written next to the results artifact. - -**Outputs.** Each experiment writes: `results.parquet` (long form), `resolved_config.yaml`, and a `manifest.json` recording library versions, seed, git commit, and a content hash of the results. Figures are generated by a separate `figures/plot_EX.py` reading only `results.parquet`, so any figure is regenerable from committed data without rerunning the simulation. - -#### 2.7.1 The correlated-teacher construction (E4's one non-obvious piece) - -E4 needs teacher sets whose pairwise **retention-correlation** $\rho$ is a clean, swept knob. Tuning the drift parameters until an emergent $\rho$ appears is the wrong design: that $\rho$ would be a tangled function of $n$, $m$, tail size and generation count, un-sweepable and confounded with the very drift whose effect E4 is trying to hold fixed. So we **construct the retention structure directly**, with $\rho$ an independent control. For an experiment whose entire purpose is to isolate the effect of decorrelation, $\rho$ must be manipulated, not inferred. - -**The mechanism — shared-switch exchangeable Bernoulli.** Let the tail have $T$ items. Each teacher $k$ retains a fraction $q$ of them; we want the retention indicators to have marginal $q$ and *exact* pairwise correlation $\rho$ across teachers. For each tail item $j$: - -1. draw a **shared switch** $z_j \sim \mathrm{Bernoulli}(\rho)$ (one draw, common to all teachers for item $j$); -2. draw a **shared retention** $s_j \sim \mathrm{Bernoulli}(q)$ (common to all teachers); -3. draw **independent retentions** $u^{(k)}_j \sim \mathrm{Bernoulli}(q)$, one per teacher; -4. set $r^{(k)}_j = s_j$ if $z_j = 1$, else $u^{(k)}_j$. - -That is the whole construction. It yields a retention matrix $R \in \{0,1\}^{K_T \times T}$. - -**Why it is exact.** Marginally $\mathbb{E}[r^{(k)}_j] = \rho q + (1-\rho)q = q$ regardless of $\rho$. For any two teachers, conditioning on the switch gives $\mathbb{E}[r^{(k)}_j r^{(k')}_j] = \rho\,\mathbb{E}[s_j^2] + (1-\rho)q^2 = \rho q + (1-\rho)q^2$ (using $s_j^2 = s_j$), so $\mathrm{Cov} = \rho q(1-q)$ and, since $\mathrm{Var} = q(1-q)$, the pairwise correlation is exactly $\rho$. The set is *exchangeable* — every teacher pair has the same $\rho$ — which is what makes $\rho$ a single scalar knob. (Verified numerically: marginal and pairwise correlation hit target across a $(\rho, q)$ grid, and the union closed form below matches to three decimals.) - -**From retention to teacher distributions.** Given $R$, build teacher $k$'s distribution $p^{(k)}$: assign every **head** item its $p^\*$ mass (all teachers keep the common core); assign **tail** item $j$ its $p^\*_j$ mass if $r^{(k)}_j = 1$ and a floor $\varepsilon_{\text{tail}}$ otherwise; renormalise. (Renormalising lets the mass of dropped tails flow to what the teacher kept — the realistic signature of a partially-collapsed model concentrating on its survivors. Reallocating instead to the head is a documented config switch.) - -**Region specialisation (structured decorrelation, optional).** With `region_specialisation=True`, give each teacher a **home region** and force $r^{(k)}_j = 1$ for every tail item $j$ in teacher $k$'s home region (each teacher fully retains the tails of the region it ground against reality), applying the $\rho$ construction only to off-home tail items. This models "each teacher ground a different region," ties E4 to E3's region-matched grounding, and is the discrete image of the perspective paper's "distil from teachers who each earned a different region." - -**The analytic target (E4's exact check).** The probability that tail item $j$ is retained by at least one of $K_T$ teachers is $\rho q + (1-\rho)\big(1-(1-q)^{K_T}\big)$, so the expected **union tail-coverage** is -$$U(K_T,\rho,q) = T\left[\rho q + (1-\rho)\left(1-(1-q)^{K_T}\right)\right],$$ -the closed form of §2.4-5. `make_retention_matrix` must reproduce it (and the target $\rho$, $q$) within Monte-Carlo tolerance in `test_scientific_validation.py`. The pupil's *realised* coverage after distillation equals $U$ only for tail items whose mixture mass clears the drift-survival threshold $\sim 1/n$ (§2.4-4) — so E4 should report both the union coverage (construction-level) and the post-distillation surviving coverage (dynamics-level), and their gap is itself informative: it is exactly the tail that recombination *supplied* but drift *re-erased* because grounding was too thin to hold it. - -**Continuous-mass extension (optional realism, not a headline).** Binary retention is the default because it matches prediction 5 exactly. For a realism cross-check, replace binary retention with correlated *masses*: draw per-teacher log-masses on tail items from a multivariate normal with equicorrelation $\rho$ (a Gaussian copula), exponentiate, and normalise. This generalises to an arbitrary correlation *matrix* between teachers (unequal pairwise $\rho$), which the shared-switch construction — being exchangeable — cannot express; use it only if the paper later needs non-exchangeable teacher sets. - -**Optional abstract treatment of the horizontal claim (§2.7-H, low priority).** If time permits, model a domain as a modular graph with tunable modularity $Q$; define the optimal number of concurrent specialists as the point where marginal coverage gain from an added specialist falls below a cost; show optimal specialist count rises with $Q$. This is the *horizontal* prediction in abstract form and does not require the neural layer. Flag clearly as exploratory; it is not a headline and can be dropped without weakening the paper. - ---- - -## 3. Layer 2 — the neural existence proof - -### 3.1 Purpose and the single objection it answers - -Layer 1 assumes the tail-deletion operator (drift). A reviewer will say: *you built your conclusion into the operator.* Layer 2 answers exactly that objection and no more. It does not need to be a society. It needs to show, in **real LoRA-adapted weights**, that the *sign* of three effects is as Layer 1 predicts: dry inheritance degrades, grounded inheritance holds; multiple decorrelated teachers preserve capability that one teacher sheds; and across generations general capability holds or climbs while each specialty is re-earned and exceeded. If those three signs appear at 1B scale on one GPU, the abstraction in Layer 1 is grounded in mechanism. - -Minimality is a virtue here, not a compromise. Every additional degree of freedom (bigger model, more generations, more domains) multiplies cost and reviewer surface without strengthening the core claim. Build the smallest thing that can show the signs. - -### 3.2 Design choices (with open-science defaults) - -**Base model.** Default to a *fully open* small model to honour reproducibility end-to-end: **OLMo-2-1B** or **SmolLM2-1.7B** (open weights, open or well-documented data, permissive licence). Capable fallback if the open models are too weak on the task: **Qwen2.5-1.5B-Instruct**. Pin the exact Hugging Face revision hash in config; never track `main`. The pipeline must be model-agnostic behind a thin adapter so swapping bases is a config change. - -**Specialisation.** LoRA (via PEFT), small rank (e.g. 8–16), on a single task family = one "region." Hours on one consumer GPU, consistent with the perspective paper's cost claim. Each teacher is one base + one LoRA adapter. - -**The verifier = "reality's no."** The domain must have a cheap, deterministic, uncontrollable oracle. **Program synthesis with unit tests** is ideal: a generated solution either passes its tests or it does not, and neither the model nor the experimenter controls the verdict. This is the minimal honest instance of the perspective paper's "predictive success under intervention." Execution happens in a sandbox (subprocess with a hard timeout and no network, run inside the container of §4; document the sandbox precisely). - -**Task domain — synthetic-primary, benchmark-secondary.** To keep the Layer-1 abstractions (region, rarity, tail) exact, the *primary* domain is a **synthetic program-synthesis generator**: a family of small, verifiable tasks parameterised by *operation type* (the region: e.g. string ops, list ops, arithmetic, dict manipulation, recursion) and *difficulty*, with I/O unit tests generated automatically. Task-type frequency is tunable, so "rare tail task types" is a dial, exactly matching Layer 1's rarity. For *external validity*, add a secondary evaluation on a held-out slice of a public benchmark (e.g. MBPP-sanitised / HumanEval) — pinned by version — to show the effect is not an artefact of the synthetic generator. The synthetic generator's spec (grammar, per-region templates, test-generation rule, rarity distribution) is itself a committed, seeded artifact. - -**Grounding, concretely.** A generational passage produces pupil training data as a mixture of (i) *inherited* teacher-generated solutions and (ii) *grounded* solutions that have been **filtered to pass the verifier** and/or freshly drawn from verified references in the target region. The **grounding fraction $g$** is the proportion of verifier-passed/real items in the pupil's training mixture — the *same knob* as Layer 1's $g$. "Dry" = $g$ low / unfiltered teacher output; "grounded" = $g$ raised with region-matched verified data. - -**Recombination, concretely.** Multiple teachers = multiple LoRA specialists on *disjoint* task families (decorrelated by construction). The pupil is trained on the pooled outputs of all teachers (distillation) or, as a cheaper alternative, the specialists are **merged** (M2N2 / model-merge style, citing Sakana's demonstration that this search runs with no retraining) and the merged model is the pupil. Distillation and merging are two config-selectable recombination operators; report at least distillation, and merging if compute allows. - -### 3.3 Contrasts C1–C4 (the whole experimental content) - -**C1 — Dry vs. grounded, single teacher, across generations.** One teacher, one region. Arm A: pupil trained on *unfiltered* teacher outputs (dry, $g\approx0$). Arm B: pupil trained on *verifier-passed* teacher outputs plus fresh verified region data ($g>0$). Run 2–3 generations. *Expect:* Arm A pass@k degrades generation over generation and its solution diversity narrows; Arm B holds. *Maps to:* Layer-1 E2. *Falsifier:* if dry inheritance does not degrade at this scale, collapse is not reachable here and the neural claim is unsupported (report honestly; possibly scale down grounding or up generations). - -**C2 — One teacher vs. N complementary teachers, matched data budget.** Fix the pupil's total training-token budget. Arm A: all budget from one teacher (one region). Arm B: same budget split across $N$ teachers on disjoint regions. Evaluate the pupil on *all* regions, including rare-type tasks. *Expect:* Arm B retains capability across the union of regions (the "tail" of rare task types survives); Arm A loses regions it did not inherit. *Maps to:* Layer-1 E4. *Falsifier:* no union benefit at matched budget → recombination claim unsupported neurally. - -**C3 — The vertical claim (load-bearing).** Over 2–3 generations, track (a) a *general* held-out benchmark spanning all regions and (b) *per-specialty* performance, where each generation re-specialises (re-earns) its region against the verifier. *Expect:* general benchmark is **monotonically non-decreasing** across generations, while each specialty is re-earned and its peak **exceeds the parent's** peak in that specialty. *This is the test of the actual thesis, not the borrowed Shumailov scaffolding.* *Maps to:* the paper's vertical prediction. *Falsifier:* if general capability falls across generations, or specialties are not re-earned-and-exceeded, the multigenerational ratchet is not demonstrated. - -**C4 — Recombination operator: distillation vs. merging (optional).** If compute allows, show C2's benefit is robust to whether recombination is done by distillation or by weight-merging. Strengthens generality and directly connects to the M2N2 citation. Drop first if time-constrained. - -### 3.4 Metrics (Layer 2) - -- **pass@1 and pass@k** on held-out tasks, per region and overall (the neural analogue of KL-to-truth / capability). -- **Region coverage** = fraction of regions (incl. rare task types) with pass@1 above a floor — the neural analogue of *support size* / tail survival. -- **Solution diversity** = distinct-$n$ or behavioural diversity of generated solutions per task (the neural analogue of heterozygosity $H$; narrowing diversity is the neural signature of collapse). -- **Generational degradation curve** = each metric as a function of generation index, per arm. - -All with multiple seeds (fewer than Layer 1 — GPU cost — but at least 3; report per-seed points, not just means, given small $n$). Pin decoding parameters (temperature, top-p, max tokens, sampling seed) in config; they materially affect pass@k and must not float. - -### 3.5 Layer-2 honesty riders - -GPU non-determinism means Layer 2 is *statistically* reproducible (same distribution of outcomes under re-run), not bitwise reproducible. Document this explicitly; pin everything pinnable (model revision, dataset version, decoding params, library versions, seeds); report seeds individually. The claim Layer 2 supports is directional ("the sign of the effect is as predicted"), and the writing must not overclaim precision the setup cannot deliver. - -### 3.6 Implementation spec for Layer 2 - -**Libraries.** PyTorch; Hugging Face `transformers` + `peft` (LoRA); `datasets`; optional `vllm` for fast generation; the synthetic task generator (project-local). Execution sandbox: `subprocess` with `resource` limits and timeout, inside the container. - -**Module interfaces (normative names):** - -```python -# neural/tasks.py -def generate_task_bank(regions, rarity, n_tasks, seed) -> TaskBank: - """Synthetic verifiable tasks; each task carries prompt, region, difficulty, - and an executable unit-test suite.""" -def verify(solution_code: str, task) -> VerifyResult: - """Run tests in a sandbox; return pass/fail + diagnostics. Deterministic.""" - -# neural/specialise.py -def train_lora_specialist(base_id, revision, region, task_bank, lora_cfg, seed) -> AdapterPath: ... - -# neural/distill.py -def generate_teacher_data(teachers, task_bank, grounding_fraction, policy, decode_cfg, seed) -> Corpus: - """Produce the pupil's training corpus: mixture of inherited (teacher) and - grounded (verifier-passed / fresh-verified) items, region-matched.""" -def train_pupil(base_id, revision, corpus, lora_cfg, seed) -> AdapterPath: ... - -# neural/merge.py -def merge_specialists(base_id, adapters, method, seed) -> ModelPath: # optional (C4) - -# neural/evaluate.py -def evaluate(model, task_bank_heldout, decode_cfg, seed) -> pd.DataFrame: - """pass@k, per-region coverage, solution diversity; tidy per-(region) frame.""" - -# neural/generation_loop.py -def run_generations(cfg, seed) -> pd.DataFrame: - """Orchestrate T generations for one arm; log every metric per generation.""" -``` - -**Config** mirrors Layer 1's structure (one YAML per contrast; pinned `base_id` + `revision`; explicit `grounding_fraction`, `n_teachers`, `regions`, `generations`, `decode_cfg`, `lora_cfg`, `seed`, `n_seeds`). Same output contract: `results.parquet` + `resolved_config.yaml` + `manifest.json` (with model revision hashes and dataset versions). Figures regenerable from `results.parquet` alone. - ---- - -## 4. Reproducibility and engineering standard (both layers) - -Open science is a hard requirement of this project, not a preference. The standard below is normative. - -**Environment.** Pin everything. Provide (a) a `pyproject.toml` + lockfile via **uv** (fast, reproducible resolver) and (b) an **Apptainer/Singularity** definition file (HPC-friendly, rootless, open) that builds the exact environment; optionally a Dockerfile. The container is the source of truth for "it runs." Record Python, CUDA, and key library versions in every run's `manifest.json`. - -**Seeding.** One master seed per experiment in config; derive all sub-seeds via `SeedSequence.spawn`; never touch global RNG state. Layer 1 is bitwise-reproducible from seed. Layer 2 is statistically reproducible; document the residual GPU non-determinism and set the available determinism flags (`torch.use_deterministic_algorithms(True)` where feasible, cudnn deterministic, documented exceptions). - -**Configuration.** No magic numbers in code — every parameter lives in a YAML resolved at run time; the *resolved* config is written beside results. Use a single config system (Hydra or a thin equivalent). Sweeps are declared in config, not hard-coded in scripts. - -**Data & model provenance.** Layer 1 data is synthetic-from-seed (fully reproducible; no external data). Layer 2 pins model revision hashes and dataset versions; the synthetic task generator is committed and seeded. Cache external downloads with recorded hashes. - -**Experiment tracking.** Prefer open tooling: **MLflow** (open source) or plain versioned CSV/Parquet + committed configs; avoid closed SaaS trackers to keep the pipeline fully open. Whatever is chosen, the invariant is: every figure is a pure function of a committed results artifact. - -**Testing.** `pytest`. Two kinds of test, and both are required: -- *Correctness tests*: the module does what it says (shapes, normalisation, sandbox isolation). -- *Scientific validation tests*: the simulator reproduces the §2.4 analytic results within tolerance (heterozygosity decay, fixation probability, mutation–drift equilibrium). These tests failing means the science is wrong, not just the code — they are the spine of trust in Layer 1. - -**Automation.** A `Makefile` (or `justfile`) with targets: `env`, `test`, `layer1` (runs E1–E6), `layer2` (runs C1–C3, C4 optional), `figures`, `paper` (assembles the figure manifest), `all`, `clean`. One command reproduces the study from a clean checkout inside the container. - -**Repro entry point.** A top-level `reproduce.sh` that: builds/enters the container, runs tests, runs all experiments at the committed seeds, regenerates all figures, and writes a `REPRODUCED.md` diff against committed result hashes. If hashes match (Layer 1) / distributions match within CI (Layer 2), the run is verified. - -**Licensing & citation.** Author to choose; suggested: code under a permissive OSI licence (MIT/Apache-2.0) or copyleft (GPL-3.0) per the author's open-source preference; text/figures under CC-BY. Include `LICENSE`, `CITATION.cff`, and a `DATA_AND_MODELS.md` recording every external artifact and its pinned version/hash. - ---- - -## 5. Repository layout - -``` -lamarckian-society/ -├── README.md # what this is, how to reproduce -├── reproduce.sh # one-command full reproduction (in-container) -├── Makefile # env, test, layer1, layer2, figures, all -├── pyproject.toml # deps -├── uv.lock # pinned resolution -├── apptainer.def # container definition (source of truth) -├── Dockerfile # optional -├── LICENSE CITATION.cff DATA_AND_MODELS.md -├── configs/ -│ ├── layer1/E1..E6.yaml -│ └── layer2/C1..C4.yaml -├── src/ -│ ├── knowledge/ # Layer 1: truth, teachers, step, lineage, metrics, experiment -│ └── neural/ # Layer 2: tasks, specialise, distill, merge, evaluate, generation_loop -├── figures/ # plot_EX.py / plot_CX.py — read results.parquet only -├── results/ # written artifacts (gitignored, hashes tracked) -├── tests/ -│ ├── test_correctness.py -│ └── test_scientific_validation.py # §2.4 analytic checks -└── paper/ - ├── blueprint.md # this document - └── figure_manifest.md # claim -> experiment -> figure -``` - ---- - -## 6. Traceability: claim → experiment → figure - -Every claim the technical paper will make is bound to exactly one experiment and one figure. This table is the paper's spine and the coding agent's definition of done. - -| Perspective-paper claim | Layer | Experiment | Primary figure | Analytic check | Falsifier | -|---|---|---|---|---|---| -| Distillation without grounding collapses, tail first, at rate set by $n$ | 1 | E1 | $H_t$ decay + tail mass vs. gen | Pred. 1–2 | harness invalid if decay ≠ analytic | -| A critical grounding ratio $g^\*$ separates ratchet from collapse | 1 | E2 | tail mass / $H$ stationary vs. $g$ | Pred. 3 | flat in $g$, or only stable at $g\to1$ | -| Grounding must be region-matched, not merely present | 1 | E3 | target-region tail: uniform vs. matched | Pred. 4 | uniform protects as well as matched | -| Multi-teacher distillation suppresses collapse ∝ decorrelation | 1 | E4 | tail coverage surface over $(K_T,\rho)$ | Pred. 5 (closed form $U(K_T,\rho,q)$) | no benefit at matched budget | -| QD selection maintains diversity where greedy fixes it | 1 | E5 | $H$ plateau: qd vs. greedy | (mutation-selection-drift) | qd ≤ greedy in stationary $H$ | -| Re-minting is irreversible; gate on diversity | 1 | E6 | KL-to-original vs. $H$-at-remint | — | collapsed lineage recovers after remint | -| Dry inheritance degrades in real weights; grounded holds | 2 | C1 | pass@k vs. gen, dry vs. grounded | — | dry does not degrade | -| Complementary teachers preserve capability one teacher sheds | 2 | C2 | region coverage, 1 vs. N, matched budget | — | no union benefit | -| **General knowledge climbs while each specialty is re-earned and exceeded** | 2 | C3 | general benchmark ↑ + per-specialty re-earn | — | general falls / no re-earn | -| Recombination benefit robust to distillation vs. merging | 2 | C4 (opt) | C2 metric under both operators | — | benefit only under one operator | - ---- - -## 7. Suggested build order for the coding agent - -Staged so that each step is independently testable and the cheapest, highest-value results land first. Do not start Layer 2 until Layer 1's scientific-validation tests pass. - -1. **Scaffold + environment.** Repo layout (§5), container (§4), `pytest` skeleton, config system, seeding utilities. Target `make test` green on trivial tests. -2. **Layer 1 core + validation.** `knowledge/` modules to the §2.7 interfaces. Implement the null model first. Write and pass `test_scientific_validation.py` against §2.4 predictions 1–2. **Gate: do not proceed until drift matches analytic decay.** -3. **Layer 1 mechanisms + E1–E2.** Add grounding; validate mutation–drift equilibrium (pred. 3); run E1 and the E2 phase-boundary sweep; produce the headline figure. This is the paper's core result and it should exist before anything neural. -4. **Layer 1 E3–E6.** Region-matching, multi-teacher/decorrelation, QD-vs-greedy, re-minting gate, with figures. Layer 1 is now a complete, laptop-reproducible paper on its own. -5. **Layer 2 scaffold + verifier.** Synthetic task generator, sandboxed `verify`, evaluation harness. Test the verifier's determinism and isolation before any training. -6. **Layer 2 C1 + C3.** Single-teacher dry-vs-grounded (C1) and the vertical claim (C3) — the two that most directly test the thesis. C3 is load-bearing; prioritise it. -7. **Layer 2 C2 (+ C4 if compute allows).** Multi-teacher recombination; optional merging operator. -8. **Reproduction pass.** `reproduce.sh` end-to-end; commit result hashes; write `REPRODUCED.md`; assemble the figure manifest. - -**Definition of done:** every row of §6 has a committed figure produced by `make figures` from committed results, every §2.4 analytic check passes in CI, and `reproduce.sh` verifies from a clean checkout inside the container. - ---- - -*End of blueprint v1. The perspective paper states the idea; this document states the test. If Layer 1's E2 finds no threshold, or E4 no decorrelation benefit, or Layer 2's C3 no vertical climb, the thesis is wrong in exactly the places these experiments probe — which is the point of writing them down this precisely.* diff --git a/paper/manuscript/build_zotero_library.py b/paper/manuscript/build_zotero_library.py deleted file mode 100644 index f533f32..0000000 --- a/paper/manuscript/build_zotero_library.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Build a Zotero-importable library from the manuscript's reference list. - -For each of the numbered references in paper/manuscript/main.md: take the DOI printed in the entry when -there is one, otherwise ask Crossref for it by title (accepting only a high-scoring match whose title -really is the same, checked by normalised comparison). Then fetch authoritative metadata for every -resolved DOI by content negotiation against doi.org, which serves Crossref and DataCite alike, and -write the result as CSL-JSON plus RIS. - -Entries whose DOI cannot be resolved (pre-DOI literature, books, chapters) are reported and written -from the manuscript's own metadata so nothing is silently dropped. - -Usage: python paper/manuscript/build_zotero_library.py -""" - -from __future__ import annotations - -import json -import re -import sys -import time -import urllib.parse -import urllib.request -from pathlib import Path - -MAIN = Path(__file__).resolve().parent / "main.md" -OUT = Path(__file__).resolve().parent / "refs" -MAILTO = "g.gilestro@imperial.ac.uk" # Crossref polite pool -UA = f"LamarckianAI-refs/1.0 (mailto:{MAILTO})" - -# Reference numbers whose sources predate DOIs or are books/chapters: never send these to Crossref -# title search, because it returns confident nonsense for them. -NO_DOI_EXPECTED = {33, 35, 39} # Jenkin 1867; Fisher 1930 (book); Templeton 1986 (chapter) - -# DOIs the title search could not find and that were verified by hand against the publisher record. -DOI_OVERRIDE = { - 17: "10.1038/s41562-023-01742-2", # Brinkmann et al., Machine culture (Nat. Hum. Behav.) - 64: "10.48550/arXiv.1805.06370", # Schwarz et al., Progress & Compress (no Crossref DOI) -} - -# The three genuinely pre-DOI sources, written out rather than parsed, so the Zotero records are -# complete instead of merely non-empty. -HAND_WRITTEN = { - 33: {"type": "article-journal", "title": "[Review of] The Origin of Species", - "author": [{"given": "Fleeming", "family": "Jenkin"}], - "container-title": "The North British Review", "volume": "46", "page": "277-318", - "issued": {"date-parts": [[1867]]}}, - 35: {"type": "book", "title": "The Genetical Theory of Natural Selection", - "author": [{"given": "Ronald A.", "family": "Fisher"}], - "publisher": "Clarendon Press", "publisher-place": "Oxford", - "issued": {"date-parts": [[1930]]}}, - 39: {"type": "chapter", "title": "Coadaptation and outbreeding depression", - "author": [{"given": "Alan R.", "family": "Templeton"}], - "editor": [{"given": "Michael E.", "family": "Soulé"}], - "container-title": "Conservation Biology: The Science of Scarcity and Diversity", - "publisher": "Sinauer Associates", "publisher-place": "Sunderland, MA", - "page": "105-116", "issued": {"date-parts": [[1986]]}}, -} - - -def get(url: str, accept: str | None = None, tries: int = 3) -> bytes | None: - req = urllib.request.Request(url, headers={"User-Agent": UA}) - if accept: - req.add_header("Accept", accept) - for i in range(tries): - try: - with urllib.request.urlopen(req, timeout=30) as r: - return r.read() - except Exception as e: # noqa: BLE001 - if i == tries - 1: - print(f" ! {type(e).__name__}: {str(e)[:80]}", file=sys.stderr) - time.sleep(1.5 * (i + 1)) - return None - - -def parse_refs() -> list[tuple[int, str]]: - refs = MAIN.read_text().split("## References")[1] - out = [] - for line in refs.splitlines(): - if m := re.match(r"^(\d+)\. (.*)$", line): - out.append((int(m.group(1)), m.group(2).strip())) - return out - - -def strip_md(s: str) -> str: - return re.sub(r"[*_`]", "", s) - - -def guess_title(entry: str) -> str: - """The title is the run of text between the author list and the italic venue or the year.""" - t = strip_md(entry) - t = re.sub(r"\s*https?://\S+$", "", t).strip() - # drop the leading author list: everything up to the last ", " before the title is unreliable, - # so instead cut after the first ", " that follows an initial-style name block - m = re.match(r"^((?:[A-ZÀ-Þ]\.\s*)+[^,]+,\s*)+", t) - rest = t[m.end():] if m else t - rest = re.sub(r"^et al\.,\s*", "", rest) - # the title ends at the venue (". *Venue*") or at " arXiv [Preprint]" or " (Year)" - rest = re.split(r"\.\s+(?:arXiv \[Preprint\]|[A-Z][a-zA-Z.\s&]*\*|Proc\.|Int\.|Adv\.|Conf\.|Nat\.|Trans\.)", rest)[0] - rest = re.split(r"\s*\(\d{4}\)", rest)[0] - return rest.strip(" .,") - - -def norm(s: str) -> str: - return re.sub(r"[^a-z0-9]", "", s.lower()) - - -def crossref_by_title(title: str, year: str | None) -> tuple[str | None, str]: - q = urllib.parse.urlencode({"query.bibliographic": title, "rows": 5, "mailto": MAILTO}) - raw = get(f"https://api.crossref.org/works?{q}") - if not raw: - return None, "crossref unreachable" - items = json.loads(raw).get("message", {}).get("items", []) - tn = norm(title) - for it in items: - cand = (it.get("title") or [""])[0] - cn = norm(cand) - if not cn: - continue - # accept only a genuine title match, not merely a high Crossref score - if cn.startswith(tn[:60]) or tn.startswith(cn[:60]): - return it.get("DOI"), f"matched: {cand[:70]}" - return None, f"no title match (best: {(items[0].get('title') or [''])[0][:60] if items else '-'})" - - -def csl_from_doi(doi: str) -> dict | None: - raw = get(f"https://doi.org/{urllib.parse.quote(doi)}", - accept="application/vnd.citationstyles.csl+json") - if not raw: - return None - try: - return json.loads(raw) - except json.JSONDecodeError: - return None - - -# ---------------------------------------------------------------- fallback CSL from the manuscript -def manual_csl(num: int, entry: str) -> dict: - t = strip_md(entry) - year = (re.search(r"\((\d{4})\)", t) or re.search(r"(\d{4})", t)) - authors = [] - m = re.match(r"^((?:[A-ZÀ-Þ]\.(?:\s*[A-ZÀ-Þ]\.)*\s+[^,]+,\s*)+)", t) - if m: - for name in re.findall(r"([A-ZÀ-Þ]\.(?:\s*[A-ZÀ-Þ]\.)*)\s+([^,]+)", m.group(1)): - authors.append({"given": name[0].strip(), "family": name[1].strip()}) - venue = re.search(r"\*([^*]+)\*", entry) - vol = re.search(r"\*\*(\d+)\*\*", entry) - pages = re.search(r"\*\*\d+\*\*,\s*([\d–\-]+)", entry) - return {k: v for k, v in { - "id": f"ref{num}", - "type": "book" if "Press)" in t or "Sinauer" in t else "article-journal", - "title": guess_title(entry), - "author": authors or None, - "container-title": venue.group(1) if venue else None, - "volume": vol.group(1) if vol else None, - "page": pages.group(1).replace("–", "-") if pages else None, - "issued": {"date-parts": [[int(year.group(1))]]} if year else None, - "note": f"manuscript reference {num}; no DOI", - }.items() if v is not None} - - -def clean_text(s: str) -> str: - """Publisher abstracts arrive with JATS tags, HTML entities, and hard line breaks; RIS is a - line-oriented format, so every field has to end up as one clean line.""" - import html - - s = re.sub(r"<[^>]+>", " ", s) # JATS/HTML tags - s = html.unescape(s) - return re.sub(r"\s+", " ", s).strip() - - -def clean_csl(c: dict) -> dict: - for k, v in list(c.items()): - if isinstance(v, str): - c[k] = clean_text(v) - elif isinstance(v, list) and v and isinstance(v[0], str): - c[k] = [clean_text(x) for x in v] - doi = c.get("DOI", "") - if doi.lower().startswith("10.48550/arxiv."): - # DataCite returns these uppercased and with no venue; restore the canonical DOI casing and - # give Zotero something to show in the publication field instead of a blank. - arxiv_id = doi.split(".", 2)[-1] - c["DOI"] = f"10.48550/arXiv.{arxiv_id}" - c["container-title"] = "arXiv" - c["number"] = f"arXiv:{arxiv_id}" - c["genre"] = "preprint" - return c - - -# Crossref reports its own type vocabulary alongside real CSL types; map both. -CSL2RIS_EXTRA = {"journal-article": "JOUR", "book-chapter": "CHAP", "proceedings-article": "CPAPER", - "posted-content": "JOUR", "book-section": "CHAP", "monograph": "BOOK"} - - -CSL2RIS = {"article-journal": "JOUR", "paper-conference": "CPAPER", "chapter": "CHAP", - "book": "BOOK", "article": "JOUR", "posted-content": "JOUR", "report": "RPRT", - "dataset": "DATA", "thesis": "THES"} - - -def ris_type(c: dict) -> str: - t = c.get("type", "") - return CSL2RIS.get(t) or CSL2RIS_EXTRA.get(t) or "JOUR" - - -def to_ris(c: dict, num: int) -> str: - L = [f"TY - {ris_type(c)}"] - for a in c.get("author") or []: - fam, giv = a.get("family", ""), a.get("given", "") - L.append(f"AU - {fam}, {giv}".rstrip(", ") if fam else f"AU - {a.get('literal', '')}") - ttl = c.get("title") - if isinstance(ttl, list): - ttl = ttl[0] - if ttl: - L.append(f"TI - {ttl}") - ct = c.get("container-title") - if isinstance(ct, list): - ct = ct[0] if ct else None - if ct: - L.append(f"{'BT' if ris_type(c) == 'CHAP' else 'T2'} - {ct}") - for ed in c.get("editor") or []: - L.append(f"A2 - {ed.get('family', '')}, {ed.get('given', '')}".rstrip(", ")) - if c.get("number"): - L.append(f"AN - {c['number']}") - if c.get("publisher-place"): - L.append(f"CY - {c['publisher-place']}") - parts = (c.get("issued") or {}).get("date-parts") or [[]] - if parts and parts[0]: - L.append(f"PY - {parts[0][0]}") - for key, tag in (("volume", "VL"), ("issue", "IS"), ("publisher", "PB"), ("DOI", "DO"), - ("URL", "UR"), ("abstract", "AB")): - if c.get(key): - L.append(f"{tag} - {c[key]}") - if c.get("page"): - pg = str(c["page"]).replace("–", "-").split("-") - L.append(f"SP - {pg[0]}") - if len(pg) > 1: - L.append(f"EP - {pg[-1]}") - L.append(f"N1 - {c.get('note') or f'Manuscript reference {num}'}") - L.append("ER - \n") - return "\n".join(L) - - -def main() -> int: - refs = parse_refs() - print(f"{len(refs)} references parsed\n") - csls, report = [], [] - for num, entry in refs: - doi = None - if num in HAND_WRITTEN: - c = dict(HAND_WRITTEN[num], id=f"ref{num}", note=f"Manuscript reference {num}; predates DOIs") - csls.append(c) - report.append((num, "HAND (pre-DOI source)", c["title"][:64], "written by hand")) - print(f" {num:3d} {'HAND (pre-DOI source)':52s} {c['title'][:56]}") - continue - if num in DOI_OVERRIDE: - doi, src = DOI_OVERRIDE[num], "verified by hand" - elif m := re.search(r"doi\.org/(10\.\S+?)\.?$", entry): - doi = m.group(1) - src = "in manuscript" - elif num not in NO_DOI_EXPECTED: - title = guess_title(entry) - yr = re.search(r"\((\d{4})\)", entry) - doi, why = crossref_by_title(title, yr.group(1) if yr else None) - src = f"crossref ({why})" - time.sleep(0.3) - else: - src = "pre-DOI / book — not searched" - - c = csl_from_doi(doi) if doi else None - if c: - c["id"] = f"ref{num}" - c["note"] = f"Manuscript reference {num}" - status = f"OK {doi}" - else: - c = manual_csl(num, entry) - status = f"MANUAL ({src})" if not doi else f"MANUAL (DOI {doi} would not resolve)" - csls.append(c) - report.append((num, status, (c.get('title') or '')[:64], src)) - print(f" {num:3d} {status:52s} {(c.get('title') or '')[:56]}") - time.sleep(0.2) - - csls = [clean_csl(c) for c in csls] - (OUT / "references.json").write_text(json.dumps(csls, indent=1, ensure_ascii=False)) - (OUT / "references.ris").write_text("".join(to_ris(c, n) for (n, _), c in zip(refs, csls))) - ok = sum(1 for _, s, _, _ in report if s.startswith("OK")) - print(f"\nresolved from DOI: {ok}/{len(refs)} manual: {len(refs)-ok}") - (OUT / "report.txt").write_text("\n".join(f"{n}\t{s}\t{t}\t{src}" for n, s, t, src in report)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/paper/manuscript/cover_letter_nmi.md b/paper/manuscript/cover_letter_nmi.md deleted file mode 100644 index f67e506..0000000 --- a/paper/manuscript/cover_letter_nmi.md +++ /dev/null @@ -1,39 +0,0 @@ -Giorgio F. Gilestro -Department of Life Sciences, Imperial College London -giorgio@gilest.ro - -[Date] - -Dear Editor, - -Please consider the enclosed manuscript, "The evolution of sex for artificial intelligence: a population-genetic framework for multigenerational model populations", for publication as an Article in *Nature Machine Intelligence*. - -Machine learning has become a population process. Public repositories hold millions of models, most of them fine-tunes, distillations or weight merges of a few ancestors; models learn from the output of earlier models; and merging, now mainstream practice with standard tooling, is described in its own literature with the words crossover, mutation and mate choice. A population whose members inherit from one another, recombine and retransmit is an evolving population in the technical sense, and the branch of biology built for that situation is the population genetics of sexual reproduction. That training on model output is genetic drift, with model collapse as its signature, has been established several times over. This paper takes the next step and develops the mechanisms population genetics offers for sustaining a population against drift (immigration, recombination, selection, population structure), and the point where they fail (reproductive isolation), and tests each of them in a chain from closed forms to trained networks to language models. - -Four measurements are new, and each was chosen because the existing experimental designs could not make it. - -First, a six-generation population of language models in which three lineages each learn a new skill every generation and then choose whether, and with whom, to merge. Merging has been iterated before, in evolutionary pools of fixed parents and in continual streams folded into one model, but never while the lineages were also learning. The population shows that obligate merging collapses once partners hold conflicting conventions (accuracy 0.65 to 0.27), that a merge each lineage may decline, or a fixed early stop, avoids the collapse at no cost against never merging, and that merging with one's own ancestor is safer than merging with a contemporary in every seed. A second curriculum decoupling partner complementarity from generation shows that declines track generation, which corrects an interpretation the first curriculum invited. - -Second, model speciation as a named and tested question. Using the permutation-and-rescaling alignment of Git Re-Basin and REPAIR, the merge barrier between networks is separated into the part alignment removes and the part it cannot. Conflicting label maps leave a residual alignment does not touch, while six times the base training on non-conflicting tasks produces no isolation at all and the strongest rescue-by-merging in the paper, against the expectation that specialisation by itself erodes mergeability. - -Third, a pre-merge predictive test on 39 language-model parent pairs across three decorrelated axes (conflict, compatible overlap, duration). Functional disagreement between parents predicts merge damage out of sample where LoRA-weight cosine and distance do not, in agreement with recent correlational reports. The control that matters is new: on a grid that varies conflict and shared training data together, weight cosine is the best predictor (ρ = 0.60), and adding overlap without conflict collapses it to 0.03. Any weight-geometry predictor validated on such a grid is reading the shared data, which bears on the merge-prediction literature independently of the biology. - -Fourth, a conservation law for blending inheritance. Refitting a child on the average of several parents' outputs carries a rare capability across a generation no better than inheriting from one parent, to first order, so the gain of having several parents is realised only by operators that keep each parent's strongest contribution. The law fixes the null against which every recombination operator is judged and predicted the headroom rule measured in language models at two scales: routing and offspring selection beat the weight average wherever that average falls short of attainable performance (hard tasks at 7B, every seed), and add nothing where it does not. - -Around these sit results that place the framework in the existing literature: a closed-form grounding equilibrium and per-item floor that agree with the fresh-data stability theorems and with the finding that absolute real-sample counts matter more than proportions; the transfer of every drift sign to trained networks with a measured, architecture-specific estimator bias; and a four-arm ablation of a composed population. Two refinements the framework proposed were not supported, and the paper says so. - -I am submitting to *Nature Machine Intelligence* because the readers who make the decisions this paper prices (how much verified data a synthetic pipeline needs, whether to merge or route, when to stop merging, how to detect an incompatible pair before paying for the merge) are this journal's readers, and because the journal has already published evolutionary model merging as a research direction (Akiba et al., 2025). The paper gives that direction its theory and its failure modes. What biology receives in return is a model system where every genotype, environment and mating decision is observable and manipulable, so the paper should also interest the evolutionary biologists among your readership. - -All code, configurations, seeds, results artefacts and a one-command reproduction script will be deposited openly with an archived DOI on publication; every figure regenerates from committed artefacts without re-simulation. The manuscript is not under consideration elsewhere and has not been published in any form. [A preprint has been / will be posted to arXiv.] I am the sole author and declare no competing interests. - -Suggested referees: -- [Name, affiliation, email] (model merging) -- [Name, affiliation, email] (model collapse / synthetic data theory) -- [Name, affiliation, email] (population genetics of recombination and speciation) -- [Name, affiliation, email] (continual learning) - -Excluded referees: [none / names]. - -Yours sincerely, - -Giorgio F. Gilestro diff --git a/paper/manuscript/figs/fig1a.pdf b/paper/manuscript/figs/fig1a.pdf index e1da59d..7bfce03 100644 Binary files a/paper/manuscript/figs/fig1a.pdf and b/paper/manuscript/figs/fig1a.pdf differ diff --git a/paper/manuscript/figs/fig1b.pdf b/paper/manuscript/figs/fig1b.pdf index 8cbf9b0..be97db7 100644 Binary files a/paper/manuscript/figs/fig1b.pdf and b/paper/manuscript/figs/fig1b.pdf differ diff --git a/paper/manuscript/figs/fig2.pdf b/paper/manuscript/figs/fig2.pdf index 58c9ee3..9932f4f 100644 Binary files a/paper/manuscript/figs/fig2.pdf and b/paper/manuscript/figs/fig2.pdf differ diff --git a/paper/manuscript/figs/fig3.pdf b/paper/manuscript/figs/fig3.pdf index 43edb72..9087bcc 100644 Binary files a/paper/manuscript/figs/fig3.pdf and b/paper/manuscript/figs/fig3.pdf differ diff --git a/paper/manuscript/figs/fig4.pdf b/paper/manuscript/figs/fig4.pdf index 0b09d16..843ae9c 100644 Binary files a/paper/manuscript/figs/fig4.pdf and b/paper/manuscript/figs/fig4.pdf differ diff --git a/paper/manuscript/figs/fig5.pdf b/paper/manuscript/figs/fig5.pdf index d998019..13785fd 100644 Binary files a/paper/manuscript/figs/fig5.pdf and b/paper/manuscript/figs/fig5.pdf differ diff --git a/paper/manuscript/figs/fig6.pdf b/paper/manuscript/figs/fig6.pdf deleted file mode 100644 index 0d89b45..0000000 Binary files a/paper/manuscript/figs/fig6.pdf and /dev/null differ diff --git a/paper/manuscript/figs/fig7.pdf b/paper/manuscript/figs/fig7.pdf deleted file mode 100644 index 827825f..0000000 Binary files a/paper/manuscript/figs/fig7.pdf and /dev/null differ diff --git a/paper/manuscript/figs/si/s10_figS10_rugged_landscapes.pdf b/paper/manuscript/figs/si/s10_figS10_rugged_landscapes.pdf index 3a0f27b..5920743 100644 Binary files a/paper/manuscript/figs/si/s10_figS10_rugged_landscapes.pdf and b/paper/manuscript/figs/si/s10_figS10_rugged_landscapes.pdf differ diff --git a/paper/manuscript/figs/si/s11_figS11_directed_recombination.pdf b/paper/manuscript/figs/si/s11_figS11_directed_recombination.pdf index 8f20be3..ab23528 100644 Binary files a/paper/manuscript/figs/si/s11_figS11_directed_recombination.pdf and b/paper/manuscript/figs/si/s11_figS11_directed_recombination.pdf differ diff --git a/paper/manuscript/figs/si/s12_figS12_quality_diversity.pdf b/paper/manuscript/figs/si/s12_figS12_quality_diversity.pdf index e71a73e..ce25a02 100644 Binary files a/paper/manuscript/figs/si/s12_figS12_quality_diversity.pdf and b/paper/manuscript/figs/si/s12_figS12_quality_diversity.pdf differ diff --git a/paper/manuscript/figs/si/s13_figS13_mating_breadth.pdf b/paper/manuscript/figs/si/s13_figS13_mating_breadth.pdf index f4ba138..f1ead89 100644 Binary files a/paper/manuscript/figs/si/s13_figS13_mating_breadth.pdf and b/paper/manuscript/figs/si/s13_figS13_mating_breadth.pdf differ diff --git a/paper/manuscript/figs/si/s14_curriculum_timing.pdf b/paper/manuscript/figs/si/s14_curriculum_timing.pdf index dfff3b3..b52b30e 100644 Binary files a/paper/manuscript/figs/si/s14_curriculum_timing.pdf and b/paper/manuscript/figs/si/s14_curriculum_timing.pdf differ diff --git a/paper/manuscript/figs/si/s15_curriculum_cull.pdf b/paper/manuscript/figs/si/s15_curriculum_cull.pdf index 41c5c46..2073688 100644 Binary files a/paper/manuscript/figs/si/s15_curriculum_cull.pdf and b/paper/manuscript/figs/si/s15_curriculum_cull.pdf differ diff --git a/paper/manuscript/figs/si/s16_llm_smol.pdf b/paper/manuscript/figs/si/s16_llm_smol.pdf index 1361a13..c4a1914 100644 Binary files a/paper/manuscript/figs/si/s16_llm_smol.pdf and b/paper/manuscript/figs/si/s16_llm_smol.pdf differ diff --git a/paper/manuscript/figs/si/s1_figS1_architectures.pdf b/paper/manuscript/figs/si/s1_figS1_architectures.pdf index 84a8e1d..eaed453 100644 Binary files a/paper/manuscript/figs/si/s1_figS1_architectures.pdf and b/paper/manuscript/figs/si/s1_figS1_architectures.pdf differ diff --git a/paper/manuscript/figs/si/s2_kernel.pdf b/paper/manuscript/figs/si/s2_kernel.pdf index a535bb1..c098120 100644 Binary files a/paper/manuscript/figs/si/s2_kernel.pdf and b/paper/manuscript/figs/si/s2_kernel.pdf differ diff --git a/paper/manuscript/figs/si/s3_figS3_rebaselining.pdf b/paper/manuscript/figs/si/s3_figS3_rebaselining.pdf index 3889cb2..306a08e 100644 Binary files a/paper/manuscript/figs/si/s3_figS3_rebaselining.pdf and b/paper/manuscript/figs/si/s3_figS3_rebaselining.pdf differ diff --git a/paper/manuscript/figs/si/s4_fig2_grounding_sweep.pdf b/paper/manuscript/figs/si/s4_fig2_grounding_sweep.pdf index 9f02870..d92c588 100644 Binary files a/paper/manuscript/figs/si/s4_fig2_grounding_sweep.pdf and b/paper/manuscript/figs/si/s4_fig2_grounding_sweep.pdf differ diff --git a/paper/manuscript/figs/si/s5_figS5_aimed_grounding.pdf b/paper/manuscript/figs/si/s5_figS5_aimed_grounding.pdf index 8b3217b..11ede1b 100644 Binary files a/paper/manuscript/figs/si/s5_figS5_aimed_grounding.pdf and b/paper/manuscript/figs/si/s5_figS5_aimed_grounding.pdf differ diff --git a/paper/manuscript/figs/si/s6_figS6_grounding_rnn.pdf b/paper/manuscript/figs/si/s6_figS6_grounding_rnn.pdf index bacec0d..027f999 100644 Binary files a/paper/manuscript/figs/si/s6_figS6_grounding_rnn.pdf and b/paper/manuscript/figs/si/s6_figS6_grounding_rnn.pdf differ diff --git a/paper/manuscript/figs/si/s7_fig2_mnist_collapse.pdf b/paper/manuscript/figs/si/s7_fig2_mnist_collapse.pdf index 10651fc..ae8bb3a 100644 Binary files a/paper/manuscript/figs/si/s7_fig2_mnist_collapse.pdf and b/paper/manuscript/figs/si/s7_fig2_mnist_collapse.pdf differ diff --git a/paper/manuscript/figs/si/s8_figS8_multiparent_union.pdf b/paper/manuscript/figs/si/s8_figS8_multiparent_union.pdf index 0ba4a16..57ade3e 100644 Binary files a/paper/manuscript/figs/si/s8_figS8_multiparent_union.pdf and b/paper/manuscript/figs/si/s8_figS8_multiparent_union.pdf differ diff --git a/paper/manuscript/figs/si/s9_figS9_specialist_superparent.pdf b/paper/manuscript/figs/si/s9_figS9_specialist_superparent.pdf index eabd45a..b360393 100644 Binary files a/paper/manuscript/figs/si/s9_figS9_specialist_superparent.pdf and b/paper/manuscript/figs/si/s9_figS9_specialist_superparent.pdf differ diff --git a/paper/manuscript/figure_legends_for_students.pdf b/paper/manuscript/figure_legends_for_students.pdf index c05be82..e612493 100644 Binary files a/paper/manuscript/figure_legends_for_students.pdf and b/paper/manuscript/figure_legends_for_students.pdf differ diff --git a/paper/manuscript/main.pdf b/paper/manuscript/main.pdf index 36a2764..0d60b6f 100644 Binary files a/paper/manuscript/main.pdf and b/paper/manuscript/main.pdf differ diff --git a/paper/manuscript/refs/report.txt b/paper/manuscript/refs/report.txt deleted file mode 100644 index c5aec88..0000000 --- a/paper/manuscript/refs/report.txt +++ /dev/null @@ -1,80 +0,0 @@ -1 OK 10.48550/arXiv.2508.06811 Anatomy of a Machine Learning Ecosystem: 2 Million Models on Hug in manuscript -2 OK 10.48550/arXiv.2405.18432 Unsupervised Model Tree Heritage Recovery in manuscript -3 OK 10.48550/arXiv.2402.00699 PeaTMOSS: A Dataset and Initial Analysis of Pre-Trained Models i in manuscript -4 OK 10.48550/arXiv.2306.01708 TIES-Merging: Resolving Interference When Merging Models in manuscript -5 OK 10.1038/s42256-024-00975-8 Evolutionary optimization of model merging recipes crossref (matched: Evolutionary optimization of model merging recipes) -6 OK 10.48550/arXiv.2403.13257 Arcee's MergeKit: A Toolkit for Merging Large Language Models in manuscript -7 OK 10.48550/arXiv.2408.07666 Model Merging in LLMs, MLLMs, and Beyond: Methods, Theories, App in manuscript -8 OK 10.48550/arXiv.2503.01155 Nature-Inspired Population-Based Evolution of Large Language Mod in manuscript -9 OK 10.48550/arXiv.2508.16204 Competition and Attraction Improve Model Fusion in manuscript -10 OK 10.48550/arXiv.2501.05707 Multiagent Finetuning: Self Improvement with Diverse Reasoning C in manuscript -11 OK 10.48550/arXiv.2406.11704 Nemotron-4 340B Technical Report in manuscript -12 OK 10.48550/arXiv.2412.08905 Phi-4 Technical Report in manuscript -13 OK 10.48550/arXiv.2212.10560 Self-Instruct: Aligning Language Models with Self-Generated Inst in manuscript -14 OK 10.48550/arXiv.2401.05749 A Shocking Amount of the Web is Machine Translated: Insights fro in manuscript -15 OK 10.48550/arXiv.2403.07183 Monitoring AI-Modified Content at Scale: A Case Study on the Imp in manuscript -16 OK 10.48550/arXiv.2211.04325 Will we run out of data? Limits of LLM scaling based on human-ge in manuscript -17 OK 10.1038/s41562-023-01742-2 Machine culture verified by hand -18 OK 10.48550/arXiv.2304.03442 Generative Agents: Interactive Simulacra of Human Behavior in manuscript -19 OK 10.48550/arXiv.2402.01680 Large Language Model based Multi-Agents: A Survey of Progress an in manuscript -20 OK 10.48550/arXiv.2509.10147 Virtual Agent Economies in manuscript -21 OK 10.1038/s41586-024-07566-y AI models collapse when trained on recursively generated data crossref (matched: AI models collapse when trained on recursively generated data) -22 OK 10.1371/journal.pcbi.1002510 Structural Drift: The Population Dynamics of Sequential Learning crossref (matched: Structural Drift: The Population Dynamics of Sequential Learning) -23 OK 10.48550/arXiv.2604.08554 Drift and selection in LLM text ecosystems in manuscript -24 OK 10.48550/arXiv.2509.20101 First-Extinction Law for Resampling Processes in manuscript -25 OK 10.48550/arXiv.2407.17493 Model Collapse in the Self-Consuming Chain of Diffusion Finetuni in manuscript -26 OK 10.1016/s0079-7421(08)60536-8 Catastrophic Interference in Connectionist Networks: The Sequent crossref (matched: Catastrophic Interference in Connectionist Networks: The Sequential Le) -27 OK 10.1016/s1364-6613(99)01294-2 Catastrophic forgetting in connectionist networks crossref (matched: Catastrophic forgetting in connectionist networks) -28 OK 10.1016/0027-5107(64)90047-8 The relation of recombination to mutational advance crossref (matched: The relation of recombination to mutational advance) -29 OK 10.48550/arXiv.2510.16657 Escaping Model Collapse via Synthetic Data Verification: Near-te in manuscript -30 OK 10.48550/arXiv.2404.01413 Is Model Collapse Inevitable? Breaking the Curse of Recursion by in manuscript -31 OK 10.1093/genetics/16.2.97 EVOLUTION IN MENDELIAN POPULATIONS crossref (matched: EVOLUTION IN MENDELIAN POPULATIONS) -32 OK 10.1046/j.1523-1739.1996.10061509.x The One‐Migrant‐per‐Generation Rule in Conservation and Manageme crossref (matched: The One‐Migrant‐per‐Generation Rule in Conservation and Management) -33 HAND (pre-DOI source) [Review of] The Origin of Species written by hand -34 OK 10.48550/arXiv.2411.02207 Collective Model Intelligence Requires Compatible Specialization in manuscript -35 HAND (pre-DOI source) The Genetical Theory of Natural Selection written by hand -36 OK 10.1086/280418 Some Genetic Aspects of Sex crossref (matched: Some Genetic Aspects of Sex) -37 OK 10.48550/arXiv.2106.09685 LoRA: Low-Rank Adaptation of Large Language Models in manuscript -38 OK 10.1016/s0022-5193(87)80029-2 Towards a general theory of adaptive walks on rugged landscapes crossref (matched: Towards a general theory of adaptive walks on rugged landscapes) -39 HAND (pre-DOI source) Coadaptation and outbreeding depression written by hand -40 OK 10.1162/evco_a_00025 Abandoning Objectives: Evolution Through the Search for Novelty crossref (matched: Abandoning Objectives: Evolution Through the Search for Novelty Alone) -41 OK 10.48550/arXiv.2503.05683 WikiBigEdit: Understanding the Limits of Lifelong Knowledge Edit in manuscript -42 OK 10.48550/arXiv.2502.04390 In Praise of Stubbornness: An Empirical Case for Cognitive-Disso in manuscript -43 OK 10.48550/arXiv.2607.09202 Interference and Retention in Continual Learning in manuscript -44 OK 10.1017/s0016672300033140 A general model for the evolution of recombination crossref (matched: A general model for the evolution of recombination) -45 OK 10.1006/tpbi.1997.1301 Deleterious Mutations, Variable Epistatic Interactions, and the crossref (matched: Deleterious Mutations, Variable Epistatic Interactions, and the Evolut) -46 OK 10.1038/nrg761 Resolving the paradox of sex and recombination crossref (matched: Resolving the paradox of sex and recombination) -47 OK 10.1093/genetics/117.3.559 Selection, Generalized Transmission and the Evolution of Modifie crossref (matched: Selection, Generalized Transmission and the Evolution of Modifier Gene) -48 OK 10.1093/genetics/139.4.1805 The population genetics of speciation: the evolution of hybrid i crossref (matched: The population genetics of speciation: the evolution of hybrid incompa) -49 OK 10.1111/j.0014-3820.2001.tb00628.x THE EVOLUTION OF POSTZYGOTIC ISOLATION: ACCUMULATING DOBZHANSKY- crossref (matched: THE EVOLUTION OF POSTZYGOTIC ISOLATION: ACCUMULATING DOBZHANSKY-MULLER) -50 OK 10.48550/arXiv.2209.04836 Git Re-Basin: Merging Models modulo Permutation Symmetries in manuscript -51 OK 10.48550/arXiv.2606.23607 Scaling Linear Mode Connectivity and Merging to Billion Paramete in manuscript -52 OK 10.48550/arXiv.2410.12766 The Non-Local Model Merging Problem: Permutation Symmetries and in manuscript -53 OK 10.48550/arXiv.2607.11997 Are we Merging the Right Models? Impact of Expert Training Durat in manuscript -54 OK 10.48550/arXiv.2601.22285 Demystifying Mergeability: Interpretable Properties to Predict M in manuscript -55 OK 10.48550/arXiv.2205.12393 Fine-tuned Language Models are Continual Learners in manuscript -56 OK 10.48550/arXiv.2403.08763 Simple and Scalable Strategies to Continually Pre-train Large La in manuscript -57 OK 10.1080/09540099550039318 Catastrophic Forgetting, Rehearsal and Pseudorehearsal crossref (matched: Catastrophic Forgetting, Rehearsal and Pseudorehearsal) -58 OK 10.48550/arXiv.1705.08690 Continual Learning with Deep Generative Replay in manuscript -59 OK 10.48550/arXiv.2406.07515 Beyond Model Collapse: Scaling Up with Synthesized Data Requires in manuscript -60 OK 10.48550/arXiv.1606.04671 Progressive Neural Networks in manuscript -61 OK 10.48550/arXiv.2405.09673 LoRA Learns Less and Forgets Less in manuscript -62 OK 10.1037/0033-295x.102.3.419 Why there are complementary learning systems in the hippocampus crossref (matched: Why there are complementary learning systems in the hippocampus and ne) -63 OK 10.1016/j.tics.2016.05.004 What Learning Systems do Intelligent Agents Need? Complementary crossref (matched: What Learning Systems do Intelligent Agents Need? Complementary Learni) -64 OK 10.48550/arXiv.1805.06370 Progress & Compress: A scalable framework for continual lear verified by hand -65 OK 10.48550/arXiv.2212.04089 Editing Models with Task Arithmetic in manuscript -66 OK 10.48550/arXiv.2407.06322 MagMax: Leveraging Model Merging for Seamless Continual Learning in manuscript -67 OK 10.48550/arXiv.2407.08699 Mitigating Catastrophic Forgetting in Language Transfer via Mode in manuscript -68 OK 10.48550/arXiv.2412.06712 How to Merge Your Multimodal Models Over Time? in manuscript -69 OK 10.48550/arXiv.1812.05159 An Empirical Study of Example Forgetting during Deep Neural Netw in manuscript -70 OK 10.48550/arXiv.2211.08411 Large Language Models Struggle to Learn Long-Tail Knowledge in manuscript -71 OK 10.48550/arXiv.2210.00266 Long-Tailed Class Incremental Learning in manuscript -72 OK 10.48550/arXiv.2309.10105 Understanding Catastrophic Forgetting in Language Models via Imp in manuscript -73 OK 10.48550/arXiv.2311.03099 Language Models are Super Mario: Absorbing Abilities from Homolo in manuscript -74 OK 10.48550/arXiv.2203.05482 Model soups: averaging weights of multiple fine-tuned models imp in manuscript -75 OK 10.48550/arXiv.2603.09463 An Empirical Study and Theoretical Explanation on Task-Level Mod in manuscript -76 OK 10.48550/arXiv.2506.14126 From Memorization to Parameter Interference: How Overtraining Ex in manuscript -77 OK 10.1145/2934662 Sex as an algorithm crossref (matched: Sex as an algorithm) -78 OK 10.48550/arXiv.2311.09807 The Curious Decline of Linguistic Diversity: Training Language M in manuscript -79 OK 10.48550/arXiv.2309.05196 Does Writing with Language Models Reduce Content Diversity? in manuscript -80 OK 10.1126/sciadv.adn5290 Generative AI enhances individual creativity but reduces the col crossref (matched: Generative AI enhances individual creativity but reduces the collectiv) \ No newline at end of file diff --git a/paper/manuscript/refs/resolution-report.tsv b/paper/manuscript/refs/resolution-report.tsv deleted file mode 100644 index c5aec88..0000000 --- a/paper/manuscript/refs/resolution-report.tsv +++ /dev/null @@ -1,80 +0,0 @@ -1 OK 10.48550/arXiv.2508.06811 Anatomy of a Machine Learning Ecosystem: 2 Million Models on Hug in manuscript -2 OK 10.48550/arXiv.2405.18432 Unsupervised Model Tree Heritage Recovery in manuscript -3 OK 10.48550/arXiv.2402.00699 PeaTMOSS: A Dataset and Initial Analysis of Pre-Trained Models i in manuscript -4 OK 10.48550/arXiv.2306.01708 TIES-Merging: Resolving Interference When Merging Models in manuscript -5 OK 10.1038/s42256-024-00975-8 Evolutionary optimization of model merging recipes crossref (matched: Evolutionary optimization of model merging recipes) -6 OK 10.48550/arXiv.2403.13257 Arcee's MergeKit: A Toolkit for Merging Large Language Models in manuscript -7 OK 10.48550/arXiv.2408.07666 Model Merging in LLMs, MLLMs, and Beyond: Methods, Theories, App in manuscript -8 OK 10.48550/arXiv.2503.01155 Nature-Inspired Population-Based Evolution of Large Language Mod in manuscript -9 OK 10.48550/arXiv.2508.16204 Competition and Attraction Improve Model Fusion in manuscript -10 OK 10.48550/arXiv.2501.05707 Multiagent Finetuning: Self Improvement with Diverse Reasoning C in manuscript -11 OK 10.48550/arXiv.2406.11704 Nemotron-4 340B Technical Report in manuscript -12 OK 10.48550/arXiv.2412.08905 Phi-4 Technical Report in manuscript -13 OK 10.48550/arXiv.2212.10560 Self-Instruct: Aligning Language Models with Self-Generated Inst in manuscript -14 OK 10.48550/arXiv.2401.05749 A Shocking Amount of the Web is Machine Translated: Insights fro in manuscript -15 OK 10.48550/arXiv.2403.07183 Monitoring AI-Modified Content at Scale: A Case Study on the Imp in manuscript -16 OK 10.48550/arXiv.2211.04325 Will we run out of data? Limits of LLM scaling based on human-ge in manuscript -17 OK 10.1038/s41562-023-01742-2 Machine culture verified by hand -18 OK 10.48550/arXiv.2304.03442 Generative Agents: Interactive Simulacra of Human Behavior in manuscript -19 OK 10.48550/arXiv.2402.01680 Large Language Model based Multi-Agents: A Survey of Progress an in manuscript -20 OK 10.48550/arXiv.2509.10147 Virtual Agent Economies in manuscript -21 OK 10.1038/s41586-024-07566-y AI models collapse when trained on recursively generated data crossref (matched: AI models collapse when trained on recursively generated data) -22 OK 10.1371/journal.pcbi.1002510 Structural Drift: The Population Dynamics of Sequential Learning crossref (matched: Structural Drift: The Population Dynamics of Sequential Learning) -23 OK 10.48550/arXiv.2604.08554 Drift and selection in LLM text ecosystems in manuscript -24 OK 10.48550/arXiv.2509.20101 First-Extinction Law for Resampling Processes in manuscript -25 OK 10.48550/arXiv.2407.17493 Model Collapse in the Self-Consuming Chain of Diffusion Finetuni in manuscript -26 OK 10.1016/s0079-7421(08)60536-8 Catastrophic Interference in Connectionist Networks: The Sequent crossref (matched: Catastrophic Interference in Connectionist Networks: The Sequential Le) -27 OK 10.1016/s1364-6613(99)01294-2 Catastrophic forgetting in connectionist networks crossref (matched: Catastrophic forgetting in connectionist networks) -28 OK 10.1016/0027-5107(64)90047-8 The relation of recombination to mutational advance crossref (matched: The relation of recombination to mutational advance) -29 OK 10.48550/arXiv.2510.16657 Escaping Model Collapse via Synthetic Data Verification: Near-te in manuscript -30 OK 10.48550/arXiv.2404.01413 Is Model Collapse Inevitable? Breaking the Curse of Recursion by in manuscript -31 OK 10.1093/genetics/16.2.97 EVOLUTION IN MENDELIAN POPULATIONS crossref (matched: EVOLUTION IN MENDELIAN POPULATIONS) -32 OK 10.1046/j.1523-1739.1996.10061509.x The One‐Migrant‐per‐Generation Rule in Conservation and Manageme crossref (matched: The One‐Migrant‐per‐Generation Rule in Conservation and Management) -33 HAND (pre-DOI source) [Review of] The Origin of Species written by hand -34 OK 10.48550/arXiv.2411.02207 Collective Model Intelligence Requires Compatible Specialization in manuscript -35 HAND (pre-DOI source) The Genetical Theory of Natural Selection written by hand -36 OK 10.1086/280418 Some Genetic Aspects of Sex crossref (matched: Some Genetic Aspects of Sex) -37 OK 10.48550/arXiv.2106.09685 LoRA: Low-Rank Adaptation of Large Language Models in manuscript -38 OK 10.1016/s0022-5193(87)80029-2 Towards a general theory of adaptive walks on rugged landscapes crossref (matched: Towards a general theory of adaptive walks on rugged landscapes) -39 HAND (pre-DOI source) Coadaptation and outbreeding depression written by hand -40 OK 10.1162/evco_a_00025 Abandoning Objectives: Evolution Through the Search for Novelty crossref (matched: Abandoning Objectives: Evolution Through the Search for Novelty Alone) -41 OK 10.48550/arXiv.2503.05683 WikiBigEdit: Understanding the Limits of Lifelong Knowledge Edit in manuscript -42 OK 10.48550/arXiv.2502.04390 In Praise of Stubbornness: An Empirical Case for Cognitive-Disso in manuscript -43 OK 10.48550/arXiv.2607.09202 Interference and Retention in Continual Learning in manuscript -44 OK 10.1017/s0016672300033140 A general model for the evolution of recombination crossref (matched: A general model for the evolution of recombination) -45 OK 10.1006/tpbi.1997.1301 Deleterious Mutations, Variable Epistatic Interactions, and the crossref (matched: Deleterious Mutations, Variable Epistatic Interactions, and the Evolut) -46 OK 10.1038/nrg761 Resolving the paradox of sex and recombination crossref (matched: Resolving the paradox of sex and recombination) -47 OK 10.1093/genetics/117.3.559 Selection, Generalized Transmission and the Evolution of Modifie crossref (matched: Selection, Generalized Transmission and the Evolution of Modifier Gene) -48 OK 10.1093/genetics/139.4.1805 The population genetics of speciation: the evolution of hybrid i crossref (matched: The population genetics of speciation: the evolution of hybrid incompa) -49 OK 10.1111/j.0014-3820.2001.tb00628.x THE EVOLUTION OF POSTZYGOTIC ISOLATION: ACCUMULATING DOBZHANSKY- crossref (matched: THE EVOLUTION OF POSTZYGOTIC ISOLATION: ACCUMULATING DOBZHANSKY-MULLER) -50 OK 10.48550/arXiv.2209.04836 Git Re-Basin: Merging Models modulo Permutation Symmetries in manuscript -51 OK 10.48550/arXiv.2606.23607 Scaling Linear Mode Connectivity and Merging to Billion Paramete in manuscript -52 OK 10.48550/arXiv.2410.12766 The Non-Local Model Merging Problem: Permutation Symmetries and in manuscript -53 OK 10.48550/arXiv.2607.11997 Are we Merging the Right Models? Impact of Expert Training Durat in manuscript -54 OK 10.48550/arXiv.2601.22285 Demystifying Mergeability: Interpretable Properties to Predict M in manuscript -55 OK 10.48550/arXiv.2205.12393 Fine-tuned Language Models are Continual Learners in manuscript -56 OK 10.48550/arXiv.2403.08763 Simple and Scalable Strategies to Continually Pre-train Large La in manuscript -57 OK 10.1080/09540099550039318 Catastrophic Forgetting, Rehearsal and Pseudorehearsal crossref (matched: Catastrophic Forgetting, Rehearsal and Pseudorehearsal) -58 OK 10.48550/arXiv.1705.08690 Continual Learning with Deep Generative Replay in manuscript -59 OK 10.48550/arXiv.2406.07515 Beyond Model Collapse: Scaling Up with Synthesized Data Requires in manuscript -60 OK 10.48550/arXiv.1606.04671 Progressive Neural Networks in manuscript -61 OK 10.48550/arXiv.2405.09673 LoRA Learns Less and Forgets Less in manuscript -62 OK 10.1037/0033-295x.102.3.419 Why there are complementary learning systems in the hippocampus crossref (matched: Why there are complementary learning systems in the hippocampus and ne) -63 OK 10.1016/j.tics.2016.05.004 What Learning Systems do Intelligent Agents Need? Complementary crossref (matched: What Learning Systems do Intelligent Agents Need? Complementary Learni) -64 OK 10.48550/arXiv.1805.06370 Progress & Compress: A scalable framework for continual lear verified by hand -65 OK 10.48550/arXiv.2212.04089 Editing Models with Task Arithmetic in manuscript -66 OK 10.48550/arXiv.2407.06322 MagMax: Leveraging Model Merging for Seamless Continual Learning in manuscript -67 OK 10.48550/arXiv.2407.08699 Mitigating Catastrophic Forgetting in Language Transfer via Mode in manuscript -68 OK 10.48550/arXiv.2412.06712 How to Merge Your Multimodal Models Over Time? in manuscript -69 OK 10.48550/arXiv.1812.05159 An Empirical Study of Example Forgetting during Deep Neural Netw in manuscript -70 OK 10.48550/arXiv.2211.08411 Large Language Models Struggle to Learn Long-Tail Knowledge in manuscript -71 OK 10.48550/arXiv.2210.00266 Long-Tailed Class Incremental Learning in manuscript -72 OK 10.48550/arXiv.2309.10105 Understanding Catastrophic Forgetting in Language Models via Imp in manuscript -73 OK 10.48550/arXiv.2311.03099 Language Models are Super Mario: Absorbing Abilities from Homolo in manuscript -74 OK 10.48550/arXiv.2203.05482 Model soups: averaging weights of multiple fine-tuned models imp in manuscript -75 OK 10.48550/arXiv.2603.09463 An Empirical Study and Theoretical Explanation on Task-Level Mod in manuscript -76 OK 10.48550/arXiv.2506.14126 From Memorization to Parameter Interference: How Overtraining Ex in manuscript -77 OK 10.1145/2934662 Sex as an algorithm crossref (matched: Sex as an algorithm) -78 OK 10.48550/arXiv.2311.09807 The Curious Decline of Linguistic Diversity: Training Language M in manuscript -79 OK 10.48550/arXiv.2309.05196 Does Writing with Language Models Reduce Content Diversity? in manuscript -80 OK 10.1126/sciadv.adn5290 Generative AI enhances individual creativity but reduces the col crossref (matched: Generative AI enhances individual creativity but reduces the collectiv) \ No newline at end of file diff --git a/paper/manuscript/renumber_refs.py b/paper/manuscript/renumber_refs.py deleted file mode 100644 index 8ce94f8..0000000 --- a/paper/manuscript/renumber_refs.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Renumber the manuscript's references to first-appearance order (PNAS style). - -Reads paper/manuscript/main.md, finds every parenthesised citation group in the text above -"## References", derives the order in which references first appear, and rewrites the citation -groups in main.md, si.md, and the figure captions in build.py, then reorders the reference list. -Citation groups are parentheses containing only reference numbers, commas, en-dash ranges, an optional -"cf. " prefix, or a prose prefix ending in a semicolon ("...; 11, 12"). Four-digit numbers (years) -never match, and any number above the list length is reported and left alone. - -Usage: python paper/manuscript/renumber_refs.py # dry run: mapping + per-file counts - python paper/manuscript/renumber_refs.py --apply # rewrite the three files in place -""" - -from __future__ import annotations - -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -MAIN, SI, BUILD = (ROOT / "paper" / "manuscript" / n for n in ("main.md", "si.md", "build.py")) -REF_HEADER = "## References" -CIT = re.compile( - r"\((?P
[^()]*?;\s*)?(?Pcf\.\s*)?"
-    r"(?P\d{1,3}(?:\s*[–-]\s*\d{1,3})?(?:,\s*\d{1,3}(?:\s*[–-]\s*\d{1,3})?)*)\)"
-)
-REF_LINE = re.compile(r"^(\d+)\. (.*)$")
-
-
-def expand(nums: str) -> list[int]:
-    out: list[int] = []
-    for part in re.split(r",\s*", nums):
-        if re.search(r"[–-]", part):
-            a, b = (int(x) for x in re.split(r"\s*[–-]\s*", part))
-            out.extend(range(a, b + 1))
-        else:
-            out.append(int(part))
-    return out
-
-
-def compress(nums: list[int]) -> str:
-    """Ascending, with runs of three or more collapsed to an en-dash range."""
-    nums = sorted(set(nums))
-    runs: list[list[int]] = []
-    for n in nums:
-        if runs and n == runs[-1][-1] + 1:
-            runs[-1].append(n)
-        else:
-            runs.append([n])
-    return ", ".join(f"{r[0]}–{r[-1]}" if len(r) >= 3 else ", ".join(map(str, r)) for r in runs)
-
-
-def split_main(text: str) -> tuple[str, list[tuple[int, str]]]:
-    body, _, refs = text.partition(REF_HEADER)
-    entries = [(int(m.group(1)), m.group(2)) for line in refs.splitlines() if (m := REF_LINE.match(line))]
-    return body, entries
-
-
-def first_appearance(body: str, n_refs: int) -> list[int]:
-    order: list[int] = []
-    for m in CIT.finditer(body):
-        for n in expand(m.group("nums")):
-            if n <= n_refs and n not in order:
-                order.append(n)
-    return order
-
-
-def rewrite(text: str, mapping: dict[int, int], n_refs: int, label: str) -> tuple[str, int, list[str]]:
-    count, suspicious = 0, []
-
-    def sub(m: re.Match) -> str:
-        nonlocal count
-        nums = expand(m.group("nums"))
-        if any(n > n_refs or n < 1 for n in nums):
-            suspicious.append(m.group(0))
-            return m.group(0)
-        count += 1
-        return f"({m.group('pre') or ''}{m.group('cf') or ''}{compress([mapping[n] for n in nums])})"
-
-    return CIT.sub(sub, text), count, suspicious
-
-
-def main(apply: bool) -> int:
-    main_text = MAIN.read_text()
-    body, entries = split_main(main_text)
-    n_refs = len(entries)
-    assert [n for n, _ in entries] == list(range(1, n_refs + 1)), "reference list is not 1..N"
-    order = first_appearance(body, n_refs)
-    orphans = sorted(set(range(1, n_refs + 1)) - set(order))
-    if orphans:
-        print(f"ERROR: never cited in main text: {orphans}")
-        return 1
-    mapping = {old: new for new, old in enumerate(order, start=1)}
-    changed = {o: n for o, n in mapping.items() if o != n}
-    print(f"{n_refs} references; {len(changed)} renumbered" + (":" if changed else "."))
-    for o in sorted(changed):
-        print(f"  {o:3d} -> {mapping[o]:3d}   {entries[o - 1][1][:70]}")
-
-    outputs: dict[Path, str] = {}
-    new_body, c, sus = rewrite(body, mapping, n_refs, "main")
-    print(f"main.md: {c} citation groups" + (f"; left alone: {sus}" if sus else ""))
-    by_new = sorted(entries, key=lambda e: mapping[e[0]])
-    new_refs = "\n".join(f"{mapping[o]}. {t}" for o, t in by_new)
-    outputs[MAIN] = f"{new_body}{REF_HEADER}\n\n{new_refs}\n"
-    text, c, sus = rewrite(SI.read_text(), mapping, n_refs, SI.name)
-    print(f"{SI.name}: {c} citation groups" + (f"; left alone: {sus}" if sus else ""))
-    outputs[SI] = text
-    # build.py is Python: only its FIGURES caption block may carry citations, so rewrite that slice
-    # alone — tuples like (0, 1) elsewhere in the code would otherwise look like citations.
-    btext = BUILD.read_text()
-    head = re.search(r"^FIGURES\b[^\n]*\{\s*$", btext, re.M)
-    if head is None:
-        print(f"{BUILD.name}: no FIGURES block found; skipped")
-    else:
-        start = head.start()
-        end = btext.index("\n}\n", start) + 3
-        block, c, sus = rewrite(btext[start:end], mapping, n_refs, BUILD.name)
-        print(f"{BUILD.name} captions: {c} citation groups" + (f"; left alone: {sus}" if sus else ""))
-        outputs[BUILD] = btext[:start] + block + btext[end:]
-
-    if apply:
-        for path, text in outputs.items():
-            path.write_text(text)
-        print("applied.")
-    else:
-        print("dry run — pass --apply to write.")
-    return 0
-
-
-if __name__ == "__main__":
-    sys.exit(main("--apply" in sys.argv))
diff --git a/paper/manuscript/si.pdf b/paper/manuscript/si.pdf
index 41457b7..7fc5c18 100644
Binary files a/paper/manuscript/si.pdf and b/paper/manuscript/si.pdf differ
diff --git a/paper/response-to-review-2.md b/paper/response-to-review-2.md
deleted file mode 100644
index 9e857d6..0000000
--- a/paper/response-to-review-2.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# Response to the second review
-
-*This response accompanies a further revision. Every number below is reproduced by a committed script
-(`figures/stats_llm_epistasis.py`) from committed artifacts; the revised documents are
-`results/llm_epistasis/README.md` (rewritten around your four analyses), the manuscript, and
-`paper/si-notes.md`.*
-
----
-
-## 0. A correction first (your §7)
-
-You are right, and we apologise for the bookkeeping error: the draft you reviewed **already
-contained** the full-symmetry alignment experiment, the conflict residual, and the
-compatible-specialisation null. Our previous letter's "new since the review" conflated three things
-that we now state separately: **new results** (the LLM-tier speciation runs, the multi-seed
-replication, and the controlled predictive test with its control axis), **new controls and analyses**
-(the compatible-overlap axis; the robust statistics in this letter), and **revised interpretation**
-(everything about E13b/c, which was experimental content you had already seen and whose *presentation*
-we changed). The experimental delta attributable to the review process is the first and second
-categories only.
-
-## 1. Your two-conclusion distinction — adopted as the evidential boundary
-
-We accept the boundary exactly as you drew it:
-
-- **Demonstrated:** a small-model, controlled predictive test in which pre-merge functional
-  disagreement predicted merge penalties where the selected weight-space measures did not.
-- **Not demonstrated:** that an epistasis-specific predictor adds value beyond ordinary functional
-  disagreement, or that the prediction improves operator choice.
-
-The experiment is now titled a **"controlled predictive test"** everywhere ("decisive experiment
-delivered" is gone), and the manuscript's §1 ladder describes the prediction rung in your conditional
-formulation, with its four boundary clauses stated in place: constructed grid, small scale, refinement
-not superior, operator choice open. The measure itself is renamed **"confidence-weighted functional
-conflict — a proposed proxy for merge-relevant interactions"**; we accept that bilateral confident
-contradiction measures incompatible endpoint behaviour, not non-additive interaction in the strict
-biological sense, and the paper no longer calls any measured quantity "epistasis." Your distinction —
-*the framework motivated the measurement and controls* vs *their success validates the specifically
-population-genetic mechanism* — is adopted verbatim; we claim the former.
-
-## 2. The four analyses — run
-
-**(1) Direct predictor comparison.** Condition-clustered bootstrap (13 clusters, B = 4000), 95% CIs
-for each predictor's ρ against the pre-registered primary outcome:
-
-| predictor | ρ | clustered 95% CI |
-|---|---|---|
-| raw functional disagreement | +0.460 | [+0.04, +0.69] |
-| confidence-weighted functional conflict | +0.446 | [+0.02, +0.68] |
-| gradient alignment | −0.347 | [−0.59, −0.06] |
-| delta L2 | +0.165 | [−0.27, +0.58] |
-| delta cosine | +0.030 | [−0.46, +0.51] |
-| cross-family accuracy | −0.005 | [−0.29, +0.31] |
-
-**Paired contrasts are not individually significant** (e.g. |ρ(dis_raw)| − |ρ(delta_cos)| = +0.23,
-CI [−0.23, +0.59]). Held-out prediction (leave-one-condition-out linear fits): functional measures
-replicate (dis_raw ρ = +0.396, p = 0.013; conf-weighted +0.352, p = 0.028); geometry ≈ 0; the
-performance baseline is unstable out-of-sample (−0.435). So the supported statement — now the
-conclusion in the README, the figure title, and the manuscript — is yours: *across this controlled
-grid, functional disagreement showed a detectable, held-out-robust association with merge penalty;
-LoRA-delta cosine and L2 showed no statistically detectable association; gradient alignment carried
-intermediate signal (its CI excludes zero), so this is not a clean functional-versus-all-geometric
-divide; head-to-head predictor differences are not individually significant; only these baselines were
-tested.* "Weight divergence does not predict merge failure" has been removed as over-broad.
-
-**(2) Sample structure.** 39 rows = **13 conditions × 3 seeds**. Parents are retrained per
-condition × seed, but children share task-data seeds across conditions within a seed (e.g. the
-duration-3 parent and the conflict-0 parent are trained on essentially the same data), so rows are not
-independent — hence clustering by condition in all uncertainty estimates, disclosed in the README.
-
-**(3) Between- vs within-axis.** You were right that the pooled correlation is substantially axis
-discrimination (mean penalty: conflict 0.061 vs compat 0.005 / duration 0.011). Within the conflict
-axis (n = 15): functional measures +0.59/+0.62 — but delta-L2 is +0.71 there, because *within that
-axis* conflict fraction, added-data volume, and delta growth are collinear: **within-axis
-identification is impossible by design**, and we now say so; the identification comes from the control
-axes, where the same volumes and L2 ranges occur at ~zero penalty. Scatterplots are coloured by axis
-in the figure. Predicting penalties for a held-out *conflict mechanism* (not just held-out conditions
-of the same mechanism) is listed as the next test — we agree it is the more valuable one.
-
-**(4) Outcome references.** Reported under all three: oracle parent potential (pre-registered
-primary), best parent, and mean parent. The ordering is **sensitive to the reference** — under the
-best-parent reference, delta-L2 correlates comparably to the functional measures (+0.48 vs +0.34).
-Our reading, stated in the README rather than hidden: that reference inherits parent-strength trends
-that track training volume, which geometry also tracks, coupling predictor and outcome through the
-reference rather than through merge damage. On structural coupling between the disagreement predictor
-and the oracle-potential outcome: probe and test sets are disjoint by construction, but we agree
-definitional coupling through parental complementarity cannot be fully excluded, which is one more
-reason all three references are now on the table.
-
-## 3. Hypothesis labels
-
-Your table is adopted essentially as written — the manuscript's claims-at-a-glance table now carries:
-pre-merge disagreement predicts penalty (*empirical within the controlled grid*, with the boundary
-clauses in the limits column); confidence weighting improves rank prediction (***not supported***);
-functional beats all geometry (*not established — selected baselines only*); operator choice (*open*);
-cliff, snowball, emergent DMIs (*hypotheses*, unchanged). We also added your snowball distinction
-where the snowball is discussed: super-linear growth in incompatibility *count* does not by itself
-entail a sharp *performance* cliff — that needs the count→effect-size→performance link, which the
-analytic model supplies under its assumptions and any neural test must establish separately.
-
-## 4. The two technical statements — corrected
-
-**"Endpoints and chord are invariant."** Accepted; the ambiguity was ours. The SI proposition now
-defines "chord" precisely as the α-linear interpolation **of the endpoint loss values** — the barrier
-baseline, a function of endpoints only, which *is* invariant — and states explicitly that the
-**weight-space interpolation path is generally not invariant** (that being precisely why alignment can
-lower a barrier). It also now carries your second point: exact recovery of a permuted-and-rescaled
-copy validates a special case and does not establish global optimality of the alignment for
-independently trained networks — so the "removable" share is a lower bound and the "residual" an upper
-bound, stated wherever the decomposition is used.
-
-**"Frozen base pins the coordinate system, so failure is functional by construction."** Accepted, and
-your replacement wording adopted verbatim across the module, configs, READMEs, and figure: *the shared
-frozen base controls a major source of coordinate mismatch, allowing a cleaner test of
-conflict-associated merging failure* — with the boundary stated (failures of delta-averaging can still
-reflect nonlinear interaction, scaling, or capacity). "Conflict-associated," not "functional by
-construction," throughout.
-
-## 5. Chronology and reliability
-
-**Chronology (now a section of the README).** Prospective: hypotheses, predictors, primary outcome and
-falsifiers were in the config before the first grid ran. Adaptive: the compatible-overlap control was
-added *after* geometry appeared to win, with its own pre-stated readings, run on the same seeds; no
-existing rows were re-run or altered. Post hoc: the clustered-bootstrap/LOCO/multi-reference analyses
-were added at your request after all data was collected. We agree this is transparent adaptive
-experimentation, not wholly prospective confirmation, and it is labelled as such.
-
-**Reliability.** The CI-width claim is withdrawn. The seed-level statement now reads: routing beat the
-soup **in every seed** (3/3 paired, both metrics), directed selection beat the soup 3/3, and one seed
-exhibited a catastrophic soup failure (0.071 overall, 0.000 worst-family) to which routing was immune
-(0.262/0.225); seed-level sds (0.090 vs 0.023) are reported as an observation, with the explicit note
-that three seeds do not support a variance estimate.
-
-## 6. On your bottom line
-
-We accept your formulation as the paper's claim for this line of work — it now closes the relevant
-results section nearly verbatim: *we separated overlap, divergence, and conflict experimentally; in
-this controlled setting, functional disagreement predicted merging damage when simple weight-distance
-measures did not; the proposed epistasis refinement and the emergent-speciation mechanism remain
-unconfirmed.* And we take the redirection about what would count next: not 0.5B → 7B alone, but
-**generalisation to unfamiliar conflict structures** (a held-out conflict mechanism, and real rather
-than constructed task pairs) and **a demonstrably better budget-matched merging decision**. Those two
-now head the open-problems list, above the scale replication.
-
-We would welcome a third pass if you have the appetite — particularly on whether the README's
-conditional conclusion and the chronology section read at the right strength.
diff --git a/paper/response-to-review-3.md b/paper/response-to-review-3.md
deleted file mode 100644
index 0376523..0000000
--- a/paper/response-to-review-3.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# Response to the third review (of the PNAS-format draft)
-
-*All five priority fixes are made, plus the presentation items. The revised draft is
-`paper/manuscript/main.md` (rebuilt PDF alongside); the long-form document and the results documentation
-were corrected wherever they carried the same overstatements. Point-by-point:*
-
-## 1. The averaging proposition (your §2) — you are right, and the text now proves what it claims
-
-Your convexity argument is correct: conservation of expected mass does not establish that averaging
-cannot help, because extinction is convex in mixed mass and averaging reduces its variance. Our result
-is, exactly as you diagnosed, a **first-order cancellation in the rare-item regime**, and the main
-text now states the actual proposition with its quantities and assumptions: K parents with independent
-retention; child draws `n` samples from one random parent vs the parents' output-mean; expected mass
-identical; and in the regime `n·p/K ≪ 1`, where per-item survival is first-order in sampled mass,
-expected survival is identical too. Two boundaries follow in the same paragraph: outside that regime
-averaging's variance reduction can *reduce* extinction relative to a random single parent (your
-argument, credited to the review process); and the union operator's renormalisation (which itself
-redistributes mass) and oracle requirement are stated. "Adding parents cannot help" is deleted here
-and in every other document that carried it. We agree the interesting content is the consequence for
-retention, not the elementary conservation of a mean — which is how the proposition is now framed.
-
-## 2. Grounding (your §3) — threshold made operational, floor made probabilistic, rule de-categoricalised
-
-- `g* ≈ 0.05` is now explicitly an **operational threshold**, with the text stating what our own
-  analysis always showed: the immigration–drift equilibrium is *smooth* in the grounding fraction (no
-  phase transition in aggregate diversity). New wording: under the tested population size and Zipf
-  source, `g ≈ 0.05` retained ≥95% of equilibrium diversity, with dependence on sample size, source,
-  and retention target (SI).
-- `m·p ≳ 1` is restated as what it is: `1 − e^{−m·p}` observation probability per batch (~63% at
-  `m·p = 1`), confidence-dependent, with retention vs stationary occupancy vs reintroduction
-  distinguished (immigration can restore an absent item).
-- The design rule now reads in your form: under unstratified grounding rare capabilities are expensive
-  (targeted sampling changes the cost); recombination recovers rare capabilities *still retained
-  across complementary parents*.
-
-## 3. Grounded inheritance vs grounded evaluation (your §4) — separated and named
-
-The society section now opens with the definitional distinction: **grounded inheritance** (external
-samples in the reproduction process — the data channel) vs **grounded evaluation** (true fitness vs
-conformity in selection — the fitness channel), related but different operators, connected only in
-that both couple the lineage to a non-drifting external signal. The section is retitled to your
-formulation ("…make complementary contributions"), the ablation is described as separating failure
-modes *under the tested conditions*, and general joint necessity is explicitly disclaimed (alternative
-mutation/restart/archive/selection schemes noted). Table 1's corresponding row now says
-"complementary… in the tested society"; the same fix is propagated to the long-form document.
-
-## 4. The alignment contradiction (your §5) — deleted, both statements reconciled
-
-"This cannot be an alignment failure, because the same aligner succeeded on the control" is removed
-everywhere (manuscript, long-form document, results documentation), replaced by your formulation: the
-tested alignment removes the same-task barrier but leaves the conflict-associated barrier largely
-unchanged — supporting a functional-conflict interpretation without proving optimal alignment. The
-abstract now says "remaining after permutation-and-rescaling alignment" (not "surviving the full
-symmetry group"), and the Methods note that the group is the alignment's *search space*, with control
-recovery not establishing global optimality. The discussion's "expect specialisation alone to be
-merge-safe" is replaced by the supported lesson: **do not treat divergence or specialisation alone as
-evidence of incompatibility.**
-
-## 5. Headline vs detail (your §6) — matched, and the seed-dependence analysed
-
-The significance statement now ends with your suggested sentence (a controlled small-model test…
-motivating further comparison). On the clustering point: you are right that condition-clustering does
-not capture cross-condition dependence through shared task-data seeds. We added the sensitivity you
-asked for (committed to the statistics script): **per-seed correlations** — each seed alone, n = 13
-conditions — are stable for the functional measures (+0.37 to +0.53 in every individual seed) and ≈0
-for geometry in every seed; leave-one-seed-out ranges are [+0.38, +0.56] (functional) vs
-[−0.04, +0.28] (geometry). One informative surprise: gradient alignment is *seed-unstable*
-(−0.11 to −0.55), which the manuscript now reports as its own caveat. The text also states plainly
-that with three seeds, uncertainty about seed generalisation remains substantial.
-
-## 6. Presentation (your §7) — done
-
-Meta-language removed ("the honest statement", "sharpest honesty", "earn their place by tempering",
-"honest deviations" — all gone; results are stated, not described as disclosures). "Exact" is now
-reserved for closed-form mathematics — NK/simulation results are labelled "analytic model" in Table 1
-and the text. The headroom relationship is stated qualitatively with "a quantitative form is
-untested". "Directed sex with no biological analogue" is replaced by your phrasing (the shorthand kept,
-defined as engineered recombination with flexible parent choice and pre-deployment screening).
-Muller's ratchet is now a *consequence-level* correspondence, with the text stating that irreversible
-loss alone does not identify the ratchet's mechanism. A compact results table (Table 2: setting/n,
-outcome definition, headline with uncertainty, for the eight headline results) is added before the
-Discussion. Reference numbering and the figure files accompany the rebuilt PDF; the bespoke unified
-figures and journal-format reflow remain flagged as submission-time work.
-
-## One point of information, not disagreement
-
-On §2's closing remark — that conservation of an arithmetic mean's expectation is elementary and the
-contribution must lie in its consequences — we agree, and would only note that the consequence now
-stated (first-order cancellation of the multi-parent retention gain under output-mean inheritance,
-against union-operator retention growth, in the regime where the deep tail actually lives) is the
-claim we intended all along; the earlier wording claimed more than this and is gone.
-
-Your bottom-line formulation — minimal models establish conditional results; neural experiments reveal
-where the correspondences hold and break; a controlled predictive test motivates measuring functional
-conflict before merging — is now, near-verbatim, how the paper describes itself. Thank you for three
-rounds of genuinely improving review.
diff --git a/paper/response-to-review.md b/paper/response-to-review.md
deleted file mode 100644
index 592072c..0000000
--- a/paper/response-to-review.md
+++ /dev/null
@@ -1,224 +0,0 @@
-# Response to the external review
-
-*Re: "The Evolution of Sex for Artificial Intelligence" (draft reviewed August 2026). This response
-accompanies a revised manuscript and a set of new experiments run directly in answer to the review.
-All results referenced here are committed, reproducible artifacts (configs, seeds, figures, and
-per-experiment READMEs in the repository); commit-level pointers are listed at the end.*
-
----
-
-Thank you for this review. It is the most useful reading the manuscript has had: it does not dispute
-the programme, it disputes the *calibration* — and its central instrument, the distinction between
-**interpretation, explanation, and prediction**, is exactly the right one. We have acted on it in two
-ways: we revised the manuscript to claim only what the evidence supports, and we **ran the decisive
-experiment the review proposed** (§5 of the review), together with four supporting experiments. The
-short version of this letter: we accepted nearly everything, the manuscript is narrower and better for
-it, and the prediction rung of your ladder — the one the draft "was not yet convincing on" — has now
-been climbed at the small-model tier, with the falsifiers pre-registered and one internal prediction
-honestly reported as unconfirmed.
-
-## 1. The overall take, and the framing
-
-> *"The strongest idea is … treat multigenerational model populations as systems whose inheritance,
-> diversity, and compatibility must be managed—not merely as collections of models to optimise."*
-
-Adopted, verbatim, as the stated core contribution — it now closes the abstract's first paragraph and
-anchors §1. You articulated our thesis better than we had; we have taken the sentence with attribution
-to the review process rather than pretend we wrote it first.
-
-> *"The draft sometimes treats a useful biological correspondence as a mathematical identity, and an
-> illustrative experiment as confirmation of a general mechanism."*
-
-Accepted. This was the review's most consequential criticism and drove most of the textual changes
-below.
-
-## 2. Novelty: interpretation / explanation / prediction
-
-The ladder is now explicit in §1: we state which of our claims are interpretation (merged offspring
-as Fisher–Muller), which are explanation (the coordinate-vs-functional decomposition of merge
-failure), and which are prediction. The priority-dispute language — "nobody has," "none imports,"
-"the theory the tinkering has outrun" — has been removed entirely, replaced with "to our knowledge"
-and positive statements of what population genetics contributes. Merge-success *prediction* is now
-explicitly conceded as an occupied area (interpretable pairwise metrics; capacity/rate-distortion
-accounts), with our delta stated as mechanism, not existence.
-
-And the prediction rung is no longer only proposed — see §7 below.
-
-## 3A. Drift, Muller's ratchet, and model collapse
-
-Accepted in full. The manuscript now says: the *minimal inheritance model* is exactly Wright–Fisher;
-a real learner is Wright–Fisher **plus a signed, architecture-specific estimator-bias operator** — and
-we cite our own learning-kernel measurement against ourselves (the smoothing RNN resists collapse,
-the sharpening VAE accelerates it; the drift *signs* survive in every architecture tested). Muller's
-ratchet is scoped to the **irreversible arm** of collapse — the capabilities that, once lost from
-every parent and source, no recombination can rebuild — with your implication stated as the reason
-the correspondence earns its keep: recombination only reassembles what still survives, so the cure
-must act before fixation-by-loss. The glossary entries were carrying the same identity overclaims and
-have been fixed to match.
-
-## 3B. "Merge, don't average" — the operator boundaries
-
-Accepted. A dedicated boundary paragraph now answers your five questions in order:
-
-- **What is conserved?** Expected rare-item mass, at the single-parent level, exact in the minimal
-  model's rare-item regime.
-- **Under which operator?** Refitting a child to the **mean of the parents' output distributions** —
-  that operator only. The 1/K dilution exactly cancels the K-parent union gain there.
-- **Weight averaging and routing?** Explicitly labelled *empirical cousins*, not instances: a
-  nonlinear network's weight-mean does not compute its parents' output-mean, and a router keeps K
-  models' storage plus a classifier — a different parameter and inference budget from one fixed-size
-  child. The measured **headroom rule** is presented as the empirical bridge between the exact law
-  and the weight-space operators, which is all it is.
-- **Does the strongest-source operator need an oracle?** Yes, and the text now says so.
-- **Capacity?** When parental capabilities cannot coexist in the child's capacity, no operator
-  preserves the union — stated, with an explicit hand-off to the speciation section as the regime
-  where that boundary lives.
-
-New supporting evidence since the review: the multi-seed replication (below) adds that fusion is not
-only worse than union-preserving operators where headroom exists — it is far **less reliable**
-(95% CI ±0.10 across training seeds vs ±0.026 for routing/selection), which we think sharpens the
-practical half of this claim.
-
-## 3C. Model speciation
-
-Accepted, and this section received the most work — textual and experimental.
-
-**Textual.** A closing block, "What these experiments do and do not establish," now states the
-supported conclusion at exactly your formulation — *some merge failures reflect incompatible
-functional requirements rather than a mismatch in coordinates* — and then lists the qualifiers: (i)
-the conflict-condition impossibility is **information-theoretic and needs no population genetics**
-(now also a formal SI proposition: endpoints and chord are invariant under any function-preserving
-transformation, and any single merged model errs at rate ≥ μ(S)/2 against at least one parent); what
-the genetic frame adds is locating *which divergences generate such conflicts*; (ii) the
-epistasis-positions-the-cliff claim and the snowball are labelled **hypotheses at the neural tier**,
-verified only in the analytic model; (iii) alignment claims are scoped to the enumerated symmetries
-of the architecture tested, and "unmergeable" means by aligned linear interpolation — a barrier to
-that operator does not preclude every recombination method (routing sidesteps it by not blending).
-Emergent Dobzhansky–Muller incompatibilities are carried as the flagship *hypothesis*, with the
-regimes where our tests found none stated as bounds.
-
-**Experimental (new since the review).**
-
-1. *Alignment under the full symmetry group.* Anticipating the "one control does not prove the
-   optimum over all allowed symmetries" objection — and the 2026 richer-symmetry results — we
-   re-ran the decomposition aligning modulo the **complete** function-preserving unit symmetry group
-   of the ReLU MLP (per-unit positive rescaling ∘ permutation; the aligner provably recovers a
-   permuted-and-rescaled copy exactly). The conflict residual is unchanged (0.502 → 0.497); the
-   independent-init barrier still vanishes (0.001). The cliff now also carries a hybrid-fitness
-   readout: merged accuracy 0.97 → 0.03 with conflict.
-2. *The emergent test, pre-registered.* Divergent-but-compatible specialists (disjoint classes;
-   shifted-view conventions), out to 6.4× the base training: residual 0.000 everywhere, and the merge
-   *rescues* the two forgetting parents (~0.50 → 0.955). We report this null prominently — you
-   identified the sharper question ("which kinds of specialisation create merge-breaking
-   interactions, and which remain complementary?") and this is its first half of an answer:
-   *specialisation on shared ancestry did not break merging in any regime we tested; imposed
-   functional conflict always did.*
-3. *The LLM tier.* The same two knobs in 0.5B LoRA children of a frozen base (which pins the
-   coordinate system, so merge failure is functional by construction): conflicting conventions
-   produce **function-specific** hybrid breakdown (merged coherence below both parents; private,
-   disjoint skills unharmed in a budget-controlled design, 3 seeds), and over-training disjoint
-   specialists 1→12 epochs produces **no** emergent isolation (the merge improves, staying above the
-   best parent).
-
-## 4. Importance, scope, and the supporting overstatements
-
-- **"Three task families and one seed."** The LLM claims are now multi-seed with fixed test sets:
-  merges beat every specialist with non-overlapping CIs on the sharper metric (5 seeds); union vs
-  fusion and directed-selection vs soup replicated at 3 seeds on the hard benchmark. Three
-  lexically-distinct families remain a stated limitation; the full grounded LLM society remains
-  explicitly unbuilt and is flagged as such.
-- **Open-ended growth.** Accepted — the §11 closing has been rewritten: the architecture removes the
-  *storage* obstacle to indefinite accumulation; that is bookkeeping, not a demonstration of
-  unbounded capability growth, which our deliberately finite models do not test.
-- **Frozen base ≠ unchanged behaviour** — fixed (§3 now guarantees a recoverable core, not
-  unchanging conduct).
-- **Baldwin effect** — now an *echo*, with the mechanism difference stated (selection for genetic
-  assimilation vs direct distillation).
-- **Consolidation and the archive** — fixed: a digital system can and should keep every ancestor;
-  the irreversibility is *operational* (nothing in the production loop consults the archive by
-  default), and the safeguard now includes an audit that diffs against the archived ancestor.
-- **"Control theory" → "framework"** throughout, subtitle included.
-- **The claim–assumptions–evidence–limitation table** is in §13 ("The claims at a glance"), ten rows,
-  each labelled exact / empirical / hypothesis with known limits.
-
-## 5. The decisive experiment — run
-
-We implemented your six-step design as specified, at the 0.5B tier (39 parent pairs, 3 seeds, fixed
-held-out test sets, falsifiers pre-registered in the config before running):
-
-1. **Controlled interaction structure:** three axes decorrelated by construction — *conflict*
-   (contradictory conventions on shared ambiguous prompts, private budgets fixed), *compat* (the
-   same shared prompts learned under the **same** convention: overlap without conflict), and
-   *duration* (weight divergence with zero conflict, 1→12 epochs).
-2. **Functional divergence separated from duration and weight distance:** the duration and compat
-   axes span the same weight-divergence and data-volume ranges as the conflict axis, at ~zero merge
-   penalty.
-3. **Operational epistasis, pre-merge:** confidence-weighted bilateral disagreement on a probe mix
-   drawn blind to where the conflict lives — the theory's point being that raw disagreement counts
-   harmless *complementation* (one parent ignorant) as conflict, while the Dobzhansky–Muller
-   structure is *bilateral confident contradiction*.
-4. **Against existing predictors:** gradient alignment at the shared base, LoRA-delta cosine and L2
-   (computed exactly), and a performance-based (cross-family accuracy) baseline.
-5. **Operator choice under matched budgets:** partially — see honest riders.
-6. **Held-out tasks, multiple seeds:** yes (fixed tests, 3 training seeds).
-
-**Result.** Against the pre-registered primary outcome (merge penalty = parent potential − merged
-achieved, the hybrid-load analogue): functional measures predict (raw disagreement ρ = +0.46,
-operational epistasis ρ = +0.45, both p < 0.005); gradient alignment is weakly informative (−0.35);
-**both geometry predictors are uninformative** (delta-cosine +0.03, delta-L2 +0.17, n.s.);
-performance-based ~0. *Functional conflict, measured before merging, predicts merge failure; weight
-divergence does not.*
-
-Two things about how this result was reached that we want on the record:
-
-- **The control that broke our own experiment first.** In the initial two-axis grid, the *best*
-  predictor was delta-cosine (ρ = +0.60) — geometry appeared to win. We identified the confound
-  (every shared-data pair in that pool was a conflicted pair, so geometry could succeed as a mere
-  overlap/volume detector), added the compat control axis, and geometry's correlation collapsed to
-  +0.03 while the functional measures held. We report this sequence in the results README rather
-  than presenting only the final table.
-- **An internal prediction failed, and we say so.** We pre-registered that confidence-weighting
-  should beat raw disagreement as a rank predictor. It does not (they are statistically
-  indistinguishable at n = 39); the weighting does double the conflict-vs-compat contrast in levels
-  (2.0× vs 1.5×). The paper reports the functional-vs-geometric verdict, not a win for the
-  refinement.
-
-**Honest riders:** correlations are moderate (|ρ| ≈ 0.45), bounded by the large intrinsic seed
-variance of 0.5B weight-averaging (itself now a documented finding); your step 5 (operator choice
-under matched budgets) is only partially delivered — the soup-vs-route gap readout is
-noise-dominated at this scale; and the whole result is one model family at one scale. The 7B
-replication on the HPC cluster is the planned firm-up before we treat this as more than a
-small-model demonstration.
-
-## 6. Where we (mildly) push back
-
-Only two places, both narrow. First, on *"the impossibility does not require population genetics"* —
-agreed, and now stated; but we would defend the framework's role in the surrounding structure: it
-told us *which* pre-merge measurement to make (bilateral confident contradiction rather than raw
-disagreement or distance), *which* control to build (complementation ≠ conflict), and *which* null to
-pre-register (emergent isolation) — and those choices are what the decisive experiment's outcome
-vindicated against the geometry baselines. Second, on *"union preservation risks being built into the
-operator's definition"* — the conservation law's content is the exact *failure* of the mean operator
-(the 1/K cancellation), not the definitional success of the max operator; we have tried to make the
-text carry it that way, with the oracle requirement explicit.
-
-## 7. What we have not done
-
-The full grounded, diversity-preserving multigenerational LLM society (still the stated largest gap);
-7B replication of the decisive experiment; an entanglement measure for *real* task pairs (our
-epistasis knob is constructed); the operator-choice decision test at usable signal-to-noise; and
-ambiguous/overlapping task families where routing stops being trivially easy. These are listed in the
-manuscript's open-problems section in this form.
-
-## Changelog
-
-Manuscript revision: commit `58e6c74` (claim-narrowing; all §1–§4 and draft-level items above).
-New experiments: `ea051a5` (full-symmetry alignment + emergent null, MLP tier), `5a23dda` (LLM-tier
-speciation + multi-seed replication), `287d232` (the decisive experiment + its control axis). The
-revised manuscript is `paper/the-evolution-of-sex-for-ai.md`; per-experiment analyses are in
-`results/*/README.md`; every figure regenerates from committed artifacts.
-
-We would welcome another pass — in particular on whether the decisive experiment's design and its
-riders are stated at the right strength, and on whether the remaining hypothesis labels
-(epistasis-cliff and snowball at the neural tier; emergent DMIs) are placed where you would place
-them.
diff --git a/paper/results-summary.md b/paper/results-summary.md
deleted file mode 100644
index bf42be8..0000000
--- a/paper/results-summary.md
+++ /dev/null
@@ -1,304 +0,0 @@
-# The Lamarckian Society — Summary of results
-
-*The complete laptop-reproducible body of work: the analytical core (Layer 1), the
-architecture-general neural existence proof (Layer 1.5, including real MNIST), the learning
-kernel, and the sexual-reproduction society (E7–E11). Two summaries of the same work — one
-technical, one accessible.*
-
-**The arc in one breath.** Model collapse is **asexual, self-consuming degradation**: a lineage
-trained on its own outputs drifts to its own mode and loses the rare tail. We show this is
-*literally* a Wright–Fisher drift process (validated against closed forms), reproduce it in real
-trained neural weights and on real MNIST images, and then establish the **cure** — a **grounded
-sexual society**: reality-checking (grounding) plus **recombination across many decorrelated
-parents** (sexual reproduction, not teacher→pupil copying) plus **quality-diversity** selection.
-The payoff is not merely arrested collapse but a population whose **offspring exceed their
-parents** and whose general capability **climbs** while specialties are re-earned — and removing
-any one operator breaks it, and a first **real-LLM prototype** (up to 7B on HPC) confirms the
-recombination claims in trained weights. **131 tests pass**; three of the core predictions are exact
-closed forms, so the headline curves sit on analytic targets rather than eyeballing.
-
----
-
-## A. Technical summary
-
-### 1. The analytical core — collapse as Wright–Fisher drift (Layer 1)
-
-Knowledge is a distribution `p_t` over `K` items on the simplex; a fixed Zipf-tailed truth `p*`;
-the generational step — *sample `n` from the parent, mix in `m` fresh real samples, refit* — is
-**literally a Wright–Fisher process with immigration**, not an analogy. Each safeguard from the
-perspective paper is one operator on that step (grounding `g=m/(n+m)`; region-matched grounding;
-multi-teacher recombination; directional vs quality-diversity selection; re-minting). Because the
-process is Wright–Fisher it inherits **closed-form validation targets**, enforced as
-`<0.5%`-tolerance assertions (the "spine of trust"): neutral heterozygosity decay
-`E[H_t]=H₀(1−1/n)^t`; fixation probability = initial frequency; the *exact* mutation–drift
-equilibrium `H_eq = H*·m(2n+m−1)/(n+2nm+m²)`; the tail-persistence threshold `m·p*ᵢ ≳ 1`; and the
-recombination union coverage `U(K_T,ρ,q)=T[ρq+(1−ρ)(1−(1−q)^{K_T})]`.
-
-Findings **E1–E6**:
-- **E1 — collapse (null).** Neutral drift reproduces the geometric `H` decay to Monte-Carlo error;
-  support collapses `K→1`; forward-KL diverges; tail *items* die ≈10× faster than head items.
-  (Aggregate tail *mass* is a drift martingale — a misleading metric; tail-*item* survival is the
-  honest one.)
-- **E2 — grounding phase boundary (headline).** Stationary `H` tracks the exact `H_eq`; a critical
-  **`g* = 0.048` (CI [0.047, 0.050]) ≪ 1** — as little as one real sample against 200 inherited
-  restores ~68% of the truth's diversity; `g=0.05` reaches 96%. The sharp threshold lives in
-  discrete tail-item survival, not the smooth `H`. The **deep tail is unrescuable** by grounding at
-  feasible budgets (`m* ∼ 1/p_min`) — which is what recombination is for.
-- **E3 — region-matched grounding.** At fixed budget, matched grounding holds the exercised region's
-  tail (0.49) where uniform lets it collapse (0.07). Grounding protects only what it overlaps.
-- **E4 — multi-teacher recombination ("merge, don't average").** Union coverage matches the closed
-  form exactly (recombination *supplies* the tail). **Principal finding:** under mean-mixture
-  distillation surviving tail coverage is **flat in `K_T`** — a conservation law (averaging's `1/K_T`
-  dilution exactly cancels the union gain). The benefit is realised only under a **union-preserving
-  merge** (`max` over teachers, à la M2N2 model-merging). *Merge weights; don't average outputs.*
-- **E5 — QD vs greedy.** Greedy selection fixes (`H≈0.01`); quality-diversity (`w_i ∝ f_i·p_i^{−α}`)
-  holds `H` at a positive plateau (0.48–0.88, rising with novelty `α`).
-- **E6 — re-minting gate.** Re-minting a *collapsed* lineage makes forward-KL to the original
-  diverge (irreversible lock-in); a diversity gate (`H≥H_gate`) prevents it; healthy re-mint is
-  harmless.
-
-### 2. Collapse in real trained weights, and on real images (Layer 1.5)
-
-A re-scoped, cheaper Layer 2: realise the *same* Wright–Fisher abstractions in **real trained
-generative models** on a fully-synthetic sandbox with an exact oracle, then confirm on real MNIST.
-A model's "knowledge" is its oracle-measured distribution over `K` modes; the generational step is
-*train-a-model-on-the-previous-model's-samples + grounding*.
-
-- **The histogram bridge (HARD GATE).** A memoryless histogram model reduces Layer 1.5 *exactly* to
-  Layer 1: run through the neural runner it recovers `g* = 0.047` and sits on the exact `H_eq` curve.
-  This licenses every trained-model result to be read against the analytic core.
-- **Collapse in an RNN, and the metric reframing.** A GRU retrained each generation on its own
-  output drifts from truth (forward-KL climbs) and grounding arrests it — the sign confirmed. But
-  the **operative neural collapse metric is forward-KL, not `H` or tail-survival**: the RNN's
-  *smoothing* inductive bias keeps spurious tail modes alive, so `H` stays ~80% of `H*` and
-  tail-survival is non-monotone in `g`. On forward-KL, half the divergence gap closes by a
-  median-recovery grounding `g≈0.04` (echoing Layer-1's 0.048), but *full* recovery needs `g≈0.19` —
-  the sharp `g*≪1` is an exact-operator feature the trained net *softens*.
-- **Architecture-generality.** Collapse + grounding-rescue appear in the **histogram, GRU, and MLP**
-  alike — the operator is not an artefact of one model class.
-- **Recombination in real weights.** The E4 "merge, don't average" law reproduces: construction-level
-  union rises 0.49→0.96, oracle-guided max-merge surviving coverage rises while mean-distill stays
-  flat — the conservation law holds in trained weights (compressed/noisier, the expected smoothing
-  caveat).
-- **Real-MNIST external validity.** A convolutional VAE (the canonical collapse vehicle) retrained on
-  its own generated digits, modes = digit-class × stroke-thickness (K=30, Zipf), read by a frozen
-  CNN oracle (98.5% mode accuracy, confusion matrix recorded as the noise floor): the **dry lineage
-  collapses to a single mode** (forward-KL 0.5→18, support 30→1, tail wiped out, H→0), while **10%
-  grounding holds all 30 modes**. The eyeball montage shows varied gen-0 digits degenerating into one
-  blurry blob. Collapse and its cure are real on real images — not a synthetic artefact.
-
-### 3. The learning kernel — neutral drift is a null both neural models fail, oppositely
-
-Prompted by revisiting the neural deviations, the refit step is generalised from a pure resample to
-`p_{t+1} = T_θ(counts/n)`: a **learning kernel** with a *smoothing* knob (mutation toward a prior)
-and a *sharpening* knob (mode-competition), both identity by default (so Layer 1 is untouched).
-Result: **neutral Wright–Fisher fails both neural architectures in opposite directions.** The
-**VAE** (large `n`, small `K`): neutral drift is *inert* (no collapse), yet the real VAE collapses
-to one mode — a **sharpening** kernel reproduces it (the estimator *adds* collapse pressure). The
-**RNN**: neutral drift drives `H→0`, but the real RNN only partially collapses — a **smoothing**
-kernel reproduces the `H`-floor (the estimator *removes* collapse pressure). Model collapse in real
-learners = **neutral drift ⊕ an architecture-specific, signed estimator-bias operator**; this
-mechanistically explains the architecture-generality result and the softened neural `g*`.
-
-### 4. The sexual-reproduction society (E7–E11) — from teacher→pupil to sex with unbounded parents
-
-The single-locus, fixed-`p*` world can only express *recovery toward a ceiling*. The society's
-load-bearing claim is **vertical** — capability that *exceeds* any component — which needs
-combinatorial structure. Knowledge becomes a distribution over **genotypes** (`L` biallelic loci,
-fitness = number of correct loci), and the one new operator is **recombination**. This is where the
-frame shifts: **teacher→pupil distillation is asexual copying (caps at the ceiling); recombination
-is sexual reproduction (combinatorial, generative — offspring can exceed both parents), and unlike
-biology there is no two-parent limit.** The celebrated evolution-of-sex theory maps onto the thesis
-exactly (collapse = Muller's ratchet; merging = meiotic reassortment; "exceeding" = the
-Fisher–Muller effect):
-
-- **E7 — the advantage of sex.** A population adapting toward an optimum: the **sexual lineage adapts
-  faster** than the asexual one (clonal interference), keeping loci in linkage equilibrium (LD→0 vs
-  an LD spike). Honest scope: a *speed* advantage, not a permanent gap (the single-population ratchet
-  is subtle).
-- **E8 — the vertical claim (Fisher–Muller), the headline.** Decorrelated *parents* are specialists
-  (expert on their loci, agnostic elsewhere). **Sexual recombination assembles a genotype fitter than
-  any parent, climbing to the optimum (12/12 — a genotype no parent had)** as parent count grows and
-  correlation `ρ→0`, while the best single parent (~8.7) and the mean-mixture "model soup" (~11.6)
-  plateau below.
-- **E9 — landscape robustness ("why sex?").** On rugged (epistatic, NK) landscapes, blindly
-  recombining trained models causes **outbreeding depression** — offspring fall *below* the parents,
-  worse the more entangled the skills and the higher the recombination rate — and the **optimal
-  recombination rate shrinks as ruggedness grows**. Design rule: *merge freely when skills are
-  complementary; sparingly, and with selection, when entangled.*
-- **E10 — directed sex beats biological sex (the AI superpower).** Biology is stuck with two
-  random-mating parents and no offspring preview; an AI can **choose complementary mates, evaluate
-  many recombinant offspring, keep the fittest, over rounds, with unbounded parents**. Random
-  ("biological") sex craters with ruggedness (0.66→0.51, deep outbreeding depression); **directed sex
-  tracks or exceeds the best parent at every ruggedness** — a catastrophe turned into a win, with no
-  biological analog.
-- **E11 — the dynamic Lamarckian society (the C3 vertical claim, realised).** A finite population of
-  agents (genotypes) evolves on a rugged NK landscape that *is* reality, composing all four operators
-  — grounding, directed sex, quality-diversity, mutation. Grounding is made load-bearing by the
-  **consensus-conformity (self-consumption)** mechanism: selection acts on
-  `g·true_fitness + (1−g)·conformity`, so at `g=0` the society optimises fitting-the-crowd rather than
-  reality. A **4-arm ablation (global optimum ≈ 0.79), each breaking distinctly, only the full society
-  climbing:** `full` **0.78** (climbs to the optimum, diversity maintained longest) · `no_sex` 0.77
-  (can't recombine to escape local optima) · `no_diversity`/greedy 0.74 (collapses diversity fastest,
-  stuck at a worse local optimum) · **`no_grounding` 0.48** (self-consumption collapse to an unfit
-  consensus — trains on the crowd, regresses to a confident-but-wrong mean; the population strongly
-  *agrees* while being *wrong*). The society needs **all** of grounding + directed sex + diversity: on
-  a rugged landscape you need diversity to explore basins, sex to recombine them, and grounding to
-  select on reality.
-
-### 5. The claims tested in real LLM weights (Layer 2 prototype)
-
-The analytic operators above make three claims that are testable in *real language-model weights*:
-"merge, don't average" (E4), the Fisher–Muller generalist-from-specialists (E8), and directed sex
-(E10). We tested them with a small, reproducible pipeline — LoRA **specialists** fine-tuned on three
-*disjoint*, procedurally-generated task families (`lists`, `strings`, `arith`) with an **exact-match
-verifier** as reality's "no" — recombined by various operators and scored on held-out tasks. Runs are
-at two scales: **Qwen2.5-0.5B** locally and **Qwen2.5-7B** on Imperial College's CX3 HPC (one L40S).
-This is a prototype (three families, one seed), so read it as **signs, not magnitudes**; the analytic
-layer carries the quantities.
-
-- **Recombining specialists yields a generalist that exceeds every parent (E8).** At 7B, a weight-space
-  **merge of the three specialists reaches 0.87 overall vs 0.77 for the best single specialist**, and
-  beats every specialist on every family — the Fisher–Muller signature, in real weights. At 0.5B the
-  same sign is present but marginal, because a weak base *dilutes* (see below).
-- **Union beats averaging — when there is headroom (E4).** Two ways to recombine: **fusion** (average
-  the LoRA deltas — a "model soup") versus **union** (keep each specialist intact and *route* each
-  input to the right one — a Mixture-of-Experts). On tasks with headroom, **routing beats fusion**
-  because averaging dilutes a specialist's contribution: at 0.5B routing 0.74 > soup 0.64; on *hard*
-  7B tasks routing 0.50 > soup 0.40, with fusion diluting the fragile string-cipher specialist so badly
-  (0.67→0.30) that the soup even loses to the best single specialist. This is E8's `max > mean` in real
-  weights.
-- **Directed sex helps — when there is headroom (E10).** Breeding a population of recombinant offspring
-  (specialists merged at many weightings), scoring each on a held-out split with the verifier, and
-  keeping the fittest **beats the single a-priori soup** where the soup is suboptimal: 0.69 > 0.64 at
-  0.5B, and 0.49 > 0.39 on hard 7B tasks — recovering most of routing's benefit from one deployable
-  model. This is offspring selection (which biology cannot preview) realised in weight space.
-- **The one law that unifies them: headroom, not scale.** Our first 7B runs on *easy* tasks showed the
-  opposite — fusion beating union, and directed selection tied with the soup. That was a **saturation
-  artefact**: the easy families saturated 7B at 1.00, leaving no room to lose to dilution, so the naive
-  soup was already optimal. On *hard* (unsaturated) 7B tasks the original ordering returns. **The
-  operative variable is headroom**: union-preserving merging and offspring selection pay off whenever
-  there is room to lose to dilution — a weak base *or* hard tasks — and only the degenerate corner of
-  easy tasks on a strong base makes naive averaging look sufficient. This both confirms E4/E8/E10 in
-  real weights and delimits exactly when the refinements matter.
-
-Honest scope: three lexically-distinct families (so the router is trivially accurate — an ambiguous-
-skill benchmark is the interesting next stress test), one seed, small LoRA. The full *grounded*
-society on LLMs (collapse under dry self-training, the dynamic society) is the HPC-scale next step.
-
-### 6. Positioning — what is prior art, what is ours
-
-An independent 2026 paper (Riis, *Drift and selection in LLM text ecosystems*) rigorously formalises
-**collapse = Wright–Fisher drift** (martingale of minority mass, rare-first extinction, de Bruijn
-fixed points, drift+selection) with n-gram agents. **We concede that framing as prior art and cite
-it — "collapse is drift" is no longer our contribution.** Crucially, Riis's "mixed environment"
-retains the lineage's own *old synthetic* tokens (no injection of external truth), so his headline is
-*pessimistic* (extinction is independent of retention). **Our defensible contributions, ranked:**
-(1) **recombination as sexual reproduction** — the "merge, don't average" law, the Fisher–Muller
-vertical claim, directed sex, and their limits (E4, E8–E10) — an operator Riis lacks; (2) the
-**learning-kernel / estimator-bias axis**, which he *names as future work*; (3) **grounding as
-immigration from a fixed reality**, giving a critical `g*≪1` his closed loop cannot have; (4)
-**architecture-generality in real weights + real images (MNIST)**; and (5) **the integrated dynamic
-society and its vertical claim** (E11), wholly ours. The repositioning: from *"collapse is drift"*
-(diagnosis) to **a population-genetic control theory for sustaining open-ended knowledge** — the
-engineered cure and its integration.
-
-### Design rules that fall out
-
-1. **Never inherit dry, and ground where it matters** — a little reality (`g*≈5%`) protects most
-   diversity, but it protects only what it overlaps, and it can't hold the deep tail.
-2. **Merge, don't average — where there is headroom** — union-preserving model-merging (routing /
-   max-merge) realises the multi-teacher benefit; averaging dilutes it. Confirmed in real LLM weights,
-   with a sharp caveat: dilution only bites when the task leaves room for it — on easy tasks a strong
-   base's naive soup already composes to the ceiling, so averaging looks fine. The refinement matters
-   exactly in proportion to how far the naive soup is from optimal.
-3. **Sex, with no parent limit** — recombining decorrelated specialists yields offspring that exceed
-   any parent; more, complementary parents climb higher.
-4. **Match recombination to entanglement** — merge freely for complementary skills; sparingly for
-   entangled ones; and *always select offspring* (directed sex), which AI can afford and biology
-   cannot.
-5. **Keep diversity, and stay grounded** — greedy selection or a broken reality-signal both collapse
-   the society; only grounding + sex + diversity together climb.
-6. **Gate irreversible consolidation on diversity** — re-mint a base only while the lineage is
-   healthy.
-
----
-
-## B. Accessible summary (for ML engineers and neuroscientists)
-
-### The question
-
-Modern AI is trained once and frozen because it cannot keep learning without *catastrophically
-forgetting*. The Lamarckian Society proposes an alternative: **generations** of bounded agents that
-learn, then reproduce — passing on what they learned. But there is a notorious trap: train a model on
-the previous model's outputs, generation after generation, and it suffers **model collapse** — the
-rare cases (the *tail*) vanish first and the model drifts to a bland mode. So the whole scheme lives
-or dies on one question: **when does generational transmission accumulate knowledge instead of
-degrading it?**
-
-### The through-line: collapse is *asexual* degradation; the cure is *sex*
-
-The key reframe of this work is that **teacher→pupil copying is asexual reproduction** — a pupil can,
-at best, recover what its teachers had. That caps out, and left alone it degrades (collapse). The cure
-is **sexual reproduction between agents**: combine *many decorrelated parents* so the offspring
-inherits a *combination* none of them had — and can be **better than any parent**. Unlike biology,
-AI sex has **no two-parent limit** and can *choose* mates and *select* offspring. That is the engine
-that lets a society climb instead of collapse.
-
-### What we found, in plain terms
-
-1. **Collapse is real, and it's math.** Casting generational training as the century-old
-   **Wright–Fisher** drift process (not a metaphor — the same equations) gives exact formulas to
-   check against. The rare stuff dies ~10× faster than the common stuff.
-2. **A little reality goes a long way — but not for everything.** Mixing in even ~5% verified real
-   data restores most of the diversity and holds it (**grounding**). But the *very rarest*
-   capabilities can't be saved by grounding alone — that's what recombination is for.
-3. **Collapse is real in actual neural nets, and on real images.** We reproduced it in trained RNNs,
-   MLPs, and a VAE, and on **real MNIST** — where a VAE trained on its own digits collapses to a
-   single blurry blob, while a little grounding keeps all the digit styles alive. (Honest nuance: real
-   nets *smooth*, so "how many modes are alive" lies to you; "how far from the truth" is the honest
-   ruler.)
-4. **To fight tail collapse with many teachers: merge, don't average.** Averaging their outputs
-   mathematically cancels the benefit; a *merge* that keeps each item's strongest source realises it.
-5. **Sex makes offspring that beat their parents.** Recombining decorrelated specialist models
-   assembles capabilities none of them had, climbing to the optimum as you add more, complementary
-   parents — while averaging ("model soup") and the best single parent plateau below. This is a
-   celebrated evolutionary result (the Fisher–Muller effect), now shown for AI model merging.
-6. **But sex can backfire — and AI has a fix biology lacks.** When skills are *entangled*, blindly
-   merging good models produces *worse* offspring ("outbreeding depression"). The fix is **directed
-   sex**: choose complementary partners, generate many merges, and keep the best — which AI can do
-   and biology can't. Directed sex turns the catastrophe into a win.
-7. **The whole society climbs only with all the pieces.** In an evolving population on a rugged
-   "reality" landscape, the *full* society (grounding + directed sex + diversity) climbs to the top
-   while keeping its specialists; remove **grounding** and it collapses into a confident, wrong
-   consensus (the exact analogue of training on the internet's AI-generated crowd); remove **sex** and
-   it gets stuck; remove **diversity** and it converges too fast to a worse answer. Each failure is
-   distinct; only the full society wins.
-8. **It shows up in real language models — with one clean caveat.** We merged LoRA-specialised
-   Qwen models (0.5B locally, 7B on a university GPU cluster): the recombined model beats every
-   specialist (Fisher–Muller, for real), and *routing* / *offspring-selection* beat naive averaging —
-   but only when the tasks are hard enough to leave room. On easy tasks a strong model's plain average
-   is already at ceiling, so the fancier operators don't help. The lesson is precise: **these
-   recombination tricks matter exactly in proportion to how far the naive average is from the best you
-   could do** — which is a genuinely useful thing to know before you spend compute on them.
-
-### Why it is novel and why it matters
-
-- **It turns model collapse from a warning into a control theory.** Collapse-as-drift is now known
-  (and independently formalised elsewhere). Our contribution is the *cure* and its integration: a
-  grounded, sexually-reproducing, diversity-preserving society that not only avoids collapse but
-  **climbs, with offspring exceeding parents**.
-- **The sexual-reproduction frame is, we believe, genuinely new for AI** — model merging reframed as
-  meiotic recombination, with a rigorous account of when it helps (complementary skills), when it
-  hurts (entangled skills), and how to make it reliably win (directed sex, unbounded parents).
-- **It is validated, not vibes.** Three core predictions are exact closed forms; the neural, image,
-  and **real-LLM** results confirm the *signs* in real trained weights (up to 7B on HPC); 131
-  automated tests pass; the whole study is laptop-reproducible from a seed (the LLM tier statistically
-  reproducible on one GPU).
-- **It gives concrete design rules** for anyone building self-improving or model-merging systems:
-  ground where it matters, merge-don't-average, match recombination to skill-entanglement, select
-  offspring, keep diversity, and gate irreversible consolidation on health.
-
-*The remaining frontier is the **LLM instantiation** — realising the grounded sexual society with
-actual language models (LoRA specialists, real model merging, execution-verified grounding), which
-the blueprint frames as the eventual empirical rung.*
diff --git a/paper/si-notes.md b/paper/si-notes.md
deleted file mode 100644
index 4b414df..0000000
--- a/paper/si-notes.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# SI notes — drafts of formal statements for the PNAS manuscript
-
-*Working drafts; folded into the SI Appendix at Phase 4. Each statement is written to be exactly as
-strong as what is true — no more.*
-
-## S1. The incompatibility floor: what no alignment can remove (E13c)
-
-**Setting.** Models A and B are trained on the same input distribution; their target label functions
-`f_A` and `f_B` agree except on a conflict set `S` of probability mass `μ(S)` (in E13's conflict
-condition, the cyclically-relabelled classes; `μ(S) ≈ conflict_frac` up to class balance). A
-*function-preserving transformation* `T` (any composition of hidden-unit permutations and, for ReLU
-networks, positive per-unit rescalings — the full unit symmetry group of a plain ReLU MLP) satisfies
-`T(B)(x) = B(x)` for all `x` by construction.
-
-**Proposition 1 (endpoint invariance — with the term "chord" defined precisely).** Here "chord"
-means the α-linear interpolation **of the endpoint loss values**, `(1−α)·L(A) + α·L(B)` — the
-baseline in the barrier definition, a function of the endpoints only — NOT the weight-space
-interpolation path. For every function-preserving `T`, the endpoint functions, hence the endpoint
-losses and this chord, are identical for `(A, T(B))` and `(A, B)`. The **interpolation path itself is
-generally NOT invariant** — losses along `(1−α)·A + α·T(B)` change with `T`, which is precisely why
-alignment can lower a barrier. *(Immediate from the definition of function-preserving.)* Scope
-caveat: our aligner provably recovers a permuted-and-rescaled copy exactly — an important special
-case — but this does not establish global optimality of the alignment over the symmetry group for
-independently trained networks; the decomposition's "removable" share is therefore a lower bound, and
-the "residual" an upper bound, on their true values.
-
-**Proposition 2 (no merged model can serve both parents).** Let `h` be *any* single classifier (in
-particular, any interpolated/merged model, under any alignment). On every `x ∈ S`, `f_A(x) ≠ f_B(x)`,
-so `h(x)` disagrees with at least one of them. Hence
-
-  `ε_A(h) + ε_B(h) ≥ μ(S)`,  and therefore  `max(ε_A(h), ε_B(h)) ≥ μ(S)/2`,
-
-where `ε_P(h)` is `h`'s error against parent `P`'s labels. A hybrid of two models whose conventions
-conflict on mass `μ(S)` errs at rate at least `μ(S)/2` against at least one parent — **hybrid
-disadvantage with an information-theoretic floor, independent of the alignment group, the
-architecture, and the merging operator.** This is reproductive isolation in the fitness sense: past a
-given functional conflict, *no* recombination operator produces an offspring loyal to both lineages.
-
-**What remains empirical, and why the experiment is designed as it is.** Propositions 1–2 do *not*
-bound the single-task path barrier (the loss along the interpolation between A and `T(B)` evaluated
-on one parent's task): in principle a path could dip toward one parent's function. Whether it does is
-exactly what E13 measures — and the measured answer is that it does not: the conflict-condition
-barrier is unchanged by permutation alignment (`residual`) *and* by alignment modulo the full
-permutation × positive-rescaling group (`residual_scale`), while the same aligner removes ~all of the
-independent-init barrier (the positive control). Richer-symmetry results for transformers
-(arXiv:2606.23607; neuron-identifiability approaches to linear mode connectivity, 2026) strengthen
-the *removable* side of the decomposition and are therefore complementary: the more barrier a larger
-group can remove for *compatible* models, the sharper the meaning of the residual that survives for
-*incompatible* ones — and Proposition 2 caps what any of them could ever achieve on the conflict set.
-
-**Terminology note for the paper.** "Residual (after alignment)" = the estimated functional
-incompatibility; for ReLU MLPs we align modulo the full unit symmetry group, so the estimate is not
-confounded by missed symmetries of that architecture class.
-
-## S2. Emergent vs imposed incompatibility (E13b framing)
-
-The conflict condition *imposes* contradiction (the two label maps disagree on `S`), which pins
-`μ(S) > 0` and activates Proposition 2. A true Bateson–Dobzhansky–Muller incompatibility is
-*emergent*: each lineage's substitutions are harmless on their own background (`μ(S) = 0` — the
-training signals never contradict), and incompatibility, if any, arises only in the *combination*.
-The `disjoint` (complementary class specialists) and `augment` (divergent input conventions)
-conditions realise this: any residual barrier they develop cannot be attributed to label conflict and
-is the emergent-speciation signal proper. Pre-registered readings: residual grows with divergence →
-model speciation is emergent in real weights (E12's trajectory realised); residual stays at the
-`shared`-control level → within this regime, trained networks are *more* merge-compatible than the
-biological analogy predicts — an honest bound on the analogy, and itself a design-relevant result
-(merging is safe absent functional conflict).
-
-**Outcome (2026-08-11 run, 4 reps, t_div ≤ 3200): the second reading.** Residual 0.000 at every
-divergence in both emergent conditions, and the merge *rescues* the forgetting `disjoint` specialists
-(parents → 0.535/0.474 on the full task; merged ≈ 0.955 throughout — a sustained Fisher–Muller rescue
-at zero barrier). Isolation in real weights required functional conflict in this regime; whether
-long-horizon over-specialisation erodes mergeability at LLM scale (cf. arXiv:2607.11997) is the
-`llm_speciation` question (Phase 3).
diff --git a/paper/the-evolution-of-sex-for-ai-accessible.md b/paper/the-evolution-of-sex-for-ai-accessible.md
deleted file mode 100644
index b1b3f83..0000000
--- a/paper/the-evolution-of-sex-for-ai-accessible.md
+++ /dev/null
@@ -1,529 +0,0 @@
-# The Evolution of Sex for Artificial Intelligence — the plain-language version
-
-### How ideas from breeding and genetics tell us how to keep AI models improving across generations
-
-*This is an accessible companion to the full paper (`the-evolution-of-sex-for-ai.md`). It makes the
-same argument and reaches the same conclusions, but assumes only that you know roughly what a machine
-learning model is — that it is trained on data, that training adjusts numbers called "weights," and
-that you can fine-tune a model on new data. Everything else is explained as we go. Where the full
-paper defends each point against the research literature, this version just tells the story.*
-
-**Giorgio F. Gilestro** · Department of Life Sciences, Imperial College London ·
-giorgio@gilest.ro · https://lab.gilest.ro
-
----
-
-## The one-paragraph version
-
-If you train an AI model on the output of earlier AI models, over and over, it rots: rare knowledge
-disappears and everything drifts toward a bland average. This is a known problem ("model collapse"),
-and in its simplest form it is governed by *exactly* the same math that describes how small biological populations lose
-rare genes by chance. That is bad news, but it comes with good news: biology has been managing this
-kind of rot for hundreds of millions of years, and its best-tested remedy is **sex** — making offspring
-by *combining* several parents instead of copying one. This paper tests how far that remedy carries
-for AI. This paper takes ninety years of genetics about
-*when and why sex beats cloning* and reads it as an engineering manual for building AI that keeps
-getting better across generations instead of decaying. Along the way it produces concrete, testable
-rules — including a surprising one about *how* to combine models ("merge, don't average"), and a limit
-("models can drift so far apart they can no longer be usefully combined at all"). We back the argument
-with small, fully reproducible experiments and a first test on real language models.
-
----
-
-## A few words you'll need
-
-- **Model collapse** — what happens when you train models on the output of earlier models, again and
-  again: rare cases vanish, everything gets blander. The central disease this paper is about.
-- **Fine-tuning / specialising** — taking a trained model and training it a bit more so it gets good at
-  one specific thing.
-- **Model merging** — combining two or more trained models directly, by mixing their weights, to get
-  one model — *without* retraining. Think "breeding two models" rather than "teaching a third."
-- **The tail** — the rare stuff. Common knowledge is the "head" of the distribution; unusual cases,
-  rare facts, and edge behaviours are the "tail." Collapse eats the tail first.
-- **Grounding** — mixing some real, verified data from the actual world into training, instead of only
-  model-generated data. The reality check.
-- **Genetic drift** (from biology) — in any finite population, rare gene variants can vanish purely by
-  chance, because not every individual reproduces. This is the biological twin of model collapse.
-- **Recombination / sex** (from biology) — making a child by combining pieces of more than one parent.
-  The opposite of cloning (**asexual** reproduction).
-
----
-
-## 1. A society of AIs across *time*, not just space
-
-When people imagine "many AIs working together," they usually picture teamwork in the *moment*:
-several specialist agents side by side, splitting up a job. This paper is about a different direction:
-**time**. Not AIs that cooperate right now, but AIs that pass knowledge down across **generations** —
-each new model starting from what the previous ones learned, the way each human generation inherits
-the accumulated knowledge of the last and adds a little.
-
-The key event, then, is **reproduction**: making a new model out of older ones. A single model, like a
-single person, eventually stops improving. A *lineage* — a chain of models across generations — does
-not have to. Civilisation isn't smart because any one person is; it's smart because knowledge
-accumulates. The whole question of this paper is: **how do you make one AI model out of older ones,
-without the knowledge rotting on the way down?** That's exactly where it can go wrong.
-
-(This corner of AI is suddenly busy: several 2025–2026 research projects build populations of models
-that improve over rounds, and "model merging" has become a small industry that already borrows words
-like crossover, mutation, and mate choice. What this paper adds is the quantitative framework behind
-those borrowed words — and honest tests of where it works and where it doesn't.)
-
-## 2. Why today's models can't do this
-
-Today's large models have no life cycle. A model is trained once, at huge expense, then **frozen** and
-shipped. It does not learn from the people who use it. "Learning" and "doing" are two separate eras
-with nothing connecting them.
-
-There's a real reason for the freeze: if you keep training a neural network on new things, it tends to
-overwrite what it already knew. This is called **catastrophic forgetting**, and it's been a known
-problem since the 1980s. Freezing dodges it by refusing to learn at all. But a lineage needs the
-opposite of a frozen model: it needs members that keep learning through their working lives and then
-pass on what they gained. So step one is a model that can *grow safely.*
-
-## 3. A model that grows without forgetting
-
-The trick is to stop overwriting. Keep the model's original core frozen and untouchable, and bolt each
-new skill onto *extra* capacity added beside it. In practice this is what small add-on "patches" like
-**LoRA** already do: the big pretrained model stays fixed, and you train a little attachable module for
-each new skill. If the core is never altered, its knowledge can't be erased — though the system's
-*behaviour* can still change while patches are active; what's guaranteed is a recoverable core.
-
-There's even a rough brain analogy: we have a fast memory (the hippocampus) that grabs an experience
-immediately, and a slow memory (the cortex) that absorbs patterns gradually, usually while we sleep.
-The AI version is clean: the prompt is short-term memory, a database is fast memory, the trained
-weights are slow memory, and a periodic "consolidation" step moves knowledge from fast to slow.
-
-One consequence matters a lot: because this kind of model only ever *adds* capacity, it eventually
-fills up. In most designs that's a problem. Here it's a feature — read on.
-
-## 4. "Full" means grown up, not broken
-
-Here's the pivot. When a model that can only add capacity finally fills up, it hasn't failed. **It has
-matured.**
-
-Think of the capacity limit as a life stage. A model is *born* as a freshly trained base — its general
-education. It has a *working life*, picking up specialised expertise on the job. And it reaches
-*maturity* — the point where it has learned about as much as one working life in its niche can teach.
-Maturity isn't the end of usefulness; it's the moment the model is most worth learning *from*. So
-maturity is the signal to **reproduce**. The capacity ceiling that every other design fights becomes,
-here, the clock that times the generations.
-
-Everything now depends on *how* that reproduction happens. This is the heart of the paper.
-
-## 5. Copying rots; combining climbs
-
-Suppose a mature model just teaches a fresh one, and that one teaches the next, and so on down the
-line. It's the obvious design — and it fails, for the same reason in AI and in biology.
-
-**In AI terms:** training each generation on the previous generation's output is the exact recipe for
-**model collapse**. The model forgets the improbable, loses the rare cases (the tail) first, and drifts
-toward its own most common output. And here's the nasty part: the thing that makes teaching-a-student
-*useful* — "keep the general, drop the quirky" — *is* the same act that deletes the tail. The operation
-you want and the operation that kills the lineage are the same move.
-
-**In biology terms (and for our simplest model it really is the same math — real networks add a measurable twist on top, which we also measure):** picture a model's knowledge as a big bag of
-items — facts, skills, behaviours — in certain proportions. One generation is: draw a finite sample
-from the parent, and rebuild the child from that sample. That "finite sample" step is *identical* to
-**genetic drift** — the way rare gene variants vanish by chance in any finite population. This isn't a
-loose analogy; it's the same century-old equations (the Wright–Fisher model), which is why we can
-check our simulations against them exactly. Rare items go extinct first, about ten times faster than
-common ones — precisely what drift predicts.
-
-And copying one teacher is **asexual reproduction** — cloning. Biology already knows the fate of a
-lineage that only ever clones and never combines: it piles up damage it can never undo, a one-way
-decline called **Muller's ratchet**. That's our lens for the *irreversible* part of model collapse —
-the capabilities that, once every copy is gone, no amount of combining can rebuild. Naming it that
-isn't just poetry — it tells us where to look for remedies, because biology has spent a very long time
-solving exactly this.
-
-Two ingredients turn the rot into a climb. Both are things nature does.
-
-**Ingredient one: don't reproduce "dry."** Collapse only happens to a lineage fed *nothing but* its own
-output. Mixing in some **real, verified data from the world** — we call this **grounding** — stops it. In
-our small experiments, grounding is shockingly cheap: even a few percent of real data keeps most of the
-diversity alive indefinitely. But we found an honest limit we didn't expect: grounding can't save the
-*very rarest* items at any affordable cost — protecting something of rarity *p* needs a real-data budget
-that grows like 1/*p*. Grounding rescues diversity cheaply, but not the deepest tail. Something else has
-to do that. That something is sex.
-
-**Ingredient two: reproduce sexually.** Instead of copying one parent, build each new model by
-**combining several** — a sexual birth, not an asexual one. AI already has a tool for this: **model
-merging**. Why does it help? If several parent models each specialised on *different* things, each one
-kept alive rare knowledge the others lost. A combined child inherits the **union** of what its parents
-kept — not the tail-thinned *average* of a crowd of near-identical clones. And here's the point that
-turns sex from a mere safety net into the engine of the whole thing:
-
-> **A child combined from complementary parents can be *better than any of its parents*.**
-
-Geneticists call this the **Fisher–Muller effect**: recombination gathers, into one individual,
-good variants that arose separately in different lineages — so the child has a combination none of the
-parents had. Our simulations show exactly this: combining specialist models that each mastered
-different skills produces a model that climbs toward the *best possible* combination — one no single
-parent had — while the best single parent, and the plain average of all of them (a "**model soup**"),
-both level off well below. This is the paper's core claim in one line: **copying can only recover a
-ceiling; combining can break through it.**
-
-And it's not just simulation. In a first test on real language models — three small Qwen models, each
-fine-tuned on a different family of tasks, then merged and graded by an automatic checker — **the merge
-beat every single specialist**, overall and on every task family. The Fisher–Muller effect, in real
-weights.
-
-That same test pinned down a subtle rule about *how* to combine models:
-
-> **"Merge, don't average" — but only when there's room to lose.**
-
-Keeping each parent whole and **routing** each question to the right specialist beats crudely averaging
-them together — *but only when the task is hard enough that averaging actually damages something*. On
-easy tasks, a strong model's plain average is already about as good as possible, so the crude soup is
-fine. On hard tasks, averaging waters down a hard-won specialist so badly the blend falls below even the
-best single parent — and the smarter "keep-them-separate-and-route" approach wins big. So the rule is
-precise: **the fancy combining tricks help in exact proportion to how far the plain average is from the
-best you could achieve.** A practitioner needs to know this before spending compute on the fancy version.
-
-Two honest caveats, and both are actual findings, not hand-waving:
-
-**Sex can backfire.** When the parents' skills aren't cleanly separable but *tangled together* — when
-skill A only pays off if skill B is also present (biologists call this **epistasis**) — blindly
-combining two good models can produce a *worse* child, because combining breaks apart a package that
-only worked as a whole. Biologists call this **outbreeding depression**, and we reproduce it: on
-"tangled" problems, naive merging drops the child below its parents, and the more you mix, the worse it
-gets. The design rule: *combine freely when skills are independent; combine sparingly and carefully when
-they're tangled.*
-
-**How *widely* you mate matters too.** That last point was about *how much* to mix; a separate knob is
-*who mixes with whom*. **Monogamy** = each model only ever combines within a small, fixed circle;
-**promiscuity** = any model can combine with any other. Almost all model-merging today is promiscuous by
-default — throw everything in one pot. But there's a catch: wide mixing spreads good traits fast, but it
-also makes the whole population converge to the *same thing*, killing variety. Narrow, local mixing keeps
-separate sub-groups exploring different solutions. We tested this against tangledness, and the best answer
-*moves*: on simple (independent-skill) problems, wide promiscuous merging is best; but the more tangled
-the skills, the more you want to *narrow* it — full promiscuity converges too fast onto one mediocre
-solution and finds a *worse* champion, while keeping structured sub-groups preserves the variety a hard
-problem needs. So the rule extends: *merge widely for independent skills; keep separate sub-populations
-("island" merging) for tangled ones.*
-
-**AI can do sex better than biology can.** Biology is stuck with two parents, mating more or less at
-random, and can't inspect a child before it's born. AI has none of those limits. It can combine **many**
-parents at once; it can **choose** which parents to combine, for complementary skills; and it can
-**generate many candidate children and keep only the best**, testing them against reality before
-committing. We call this **directed sex**, and in our simulations it turns the outbreeding-depression
-disaster into a reliable win: where blind combining collapses on tangled problems, directed combining
-matches or beats the best parent every time. The real-language-model test shows the same where it can:
-breeding many merged offspring and keeping the one that scores highest beats the plain soup on hard
-tasks. This is a genuine advantage of *engineered* reproduction over the biological kind, and it's one
-of the more useful ideas in the paper.
-
-So §5 in one breath: copying is asexual and rots (Muller's ratchet = model collapse); the cure is to
-**ground** every birth in reality and to reproduce **sexually**, combining many complementary parents;
-and because AI's version of sex can use many parents, chosen mates, and pre-screened offspring, it's
-not just insurance against collapse — it's an engine that makes children better than any parent.
-
-### The limit of sex: models can drift too far apart to merge
-
-Sex has a limit, and it's the sharpest new prediction here. Combining parents works because they're
-variations on a shared background. Push two lineages far enough apart and their combination stops being
-viable. In biology this is **speciation** — two populations become separate species that can no longer
-interbreed. The genetic mechanism has a name (a **Bateson–Dobzhansky–Muller incompatibility**): a change
-that arose in lineage A and a change that arose in lineage B are each harmless on their own, but their
-*combination* — which neither lineage ever tested — is broken in the hybrid. A merged model is exactly
-such a hybrid. So the theory predicts a specific trajectory as two models drift apart: **they merge
-fine → merging starts to hurt → merging becomes useless.**
-
-We built this as an explicit model and confirmed the predicted curve. Three things come out of it:
-
-1. **There's a cliff, and epistasis moves it.** The point where merging fails isn't fixed — it comes
-   *earlier the more tangled (epistatic) the skills are*. This is a distinct, testable claim:
-   **at the same amount of drift, whether two models can be merged depends on how tangled their skills
-   are, not just on how far apart they are.** The existing AI tools for predicting merge success only
-   measure distance/geometry — they don't have this axis.
-2. **It snowballs.** The number of incompatibilities grows with the *square* of the drift, so merge
-   quality doesn't fade gently — it falls off a cliff. Drift is punished faster than it accumulates.
-3. **The design rule:** before merging, weigh how far apart the models are against how tangled the skills
-   are. Past the cliff, don't merge — **route** instead (keep the specialists separate and pick between
-   them).
-
-We also did the experiment a skeptic would demand. A known objection: "your 'incompatibility' is just a
-loss barrier, and those are famous for being fake — two networks can learn the *same* function but store
-it in a shuffled internal order, which *looks* like incompatibility until you line their neurons back up
-(a technique called **Git Re-Basin**)." So we tested it directly in real trained networks. We trained
-pairs of small networks, merged them, and measured the merge damage *before and after* re-aligning their
-neurons. The result is clean:
-
-- Two networks trained on the **same task** but from different random starts: big apparent merge damage,
-  but re-aligning removes **~98% of it**. That's the fake kind — same skill, shuffled order. (This also
-  proves our alignment tool works.) And we allowed the aligner *every* legal move for these networks —
-  not just re-ordering neurons but also re-scaling them — so nothing removable was left on the table.
-- Two networks trained on **conflicting tasks**: big merge damage, and even the full aligner removes
-  **essentially none of it**. That's the *real* kind — genuine incompatibility, not a bookkeeping
-  artifact. And it can't be waved away as "you just didn't align them well," because the exact same tool
-  cleaned up the first case. There's even a simple proof that no future alignment trick can fix it: no
-  single model can obey two rulebooks that contradict each other on the same inputs.
-
-Sweeping from "no conflict" to "total conflict" traces a smooth **incompatibility cliff** in real
-weights: the merged model's accuracy slides from 0.97 (no conflict) down to 0.03 (total conflict) — a
-hybrid that is literally inviable. So the speciation effect is real, not a relabelled artifact.
-
-And we ran the honest flip side, deciding in advance to report it either way: what if two networks just
-*specialise differently*, with no conflict at all — one keeps training only on digits 0–4, the other
-only on 5–9? Do they drift into incompatibility on their own? **No.** At every amount of divergence we
-tested, the merge damage stayed at zero — and the merged model actually *rescued* the two specialists:
-each parent alone had forgotten half the digits (scoring ~0.50), while their merge scored ~0.95. So in
-these experiments, models don't become unmergeable just by growing apart; they become unmergeable when
-they learn things that genuinely *contradict*. That's good news for merging — specialisation is safe,
-conflict is the danger — and it makes the theory's prediction sharper, not weaker.
-
-One question is left hanging, and the rest of the paper is about it: combining preserves *what the
-parents kept* — but **who decides what each parent keeps, and which children are worth keeping?**
-
-## 6. Don't design the selector — evolve it
-
-There are two ways to answer that question, and the first one is wrong. We could try to hand-write the
-rule for "what knowledge to keep and pass on." But nobody actually knows that rule. "Keep the general,
-drop the specific" sounds wise until you ask *which* generalisations, in *which* domain, at *which* level
-of detail — and it falls apart. This is the deepest gap in the whole scheme, and you can't fill it by
-decree.
-
-The second answer is the one nature used: **don't design the selector — let it evolve.** Let different
-models carry different *policies* about what's worth keeping and combining. Let the policies that produce
-strong children spread, and the policies that produce weak children die out with their lineages. What the
-lineage considers *important* — its "taste" — is discovered by selection, not imposed by us.
-
-So **two things get inherited, on two channels.** The *content* — the actual knowledge — is passed down
-directly (this is the "Lamarckian" part: inheriting things acquired during a lifetime, which biology
-forbids for genes but culture allows for ideas). The *selection policy* — what to keep, who to breed
-with, which children to screen for — is *itself* inherited, varies between models, and survives in
-proportion to how well it works. That second channel is Darwinian. The system is both at once: it
-inherits *content* like culture, and selects *policies* like evolution.
-
-And it closes neatly: Darwinian selection needs a pressure — something that decides which policies win.
-That pressure is already in the design. What tells a lineage its taste was good? Whether its children
-succeed *against reality*. The reality check that stops collapse (grounding, §5) and the fitness signal
-that guides the evolving taste turn out to be the **same thing**, seen from two angles.
-
-## 7. The big danger: what you measure is not what you want
-
-Adding selection adds selection's classic trap, and it's dangerous enough to sink everything if ignored.
-Evolution optimises, ruthlessly and without foresight, for exactly what you *measure* — never for what
-you *meant*. (In ML you know this as reward hacking or specification gaming; economists call it
-Goodhart's law.) Get the fitness measure a little wrong and the lineage will exploit the gap more
-cleverly than any rule you could write.
-
-For a *knowledge* lineage there's a particularly nasty version. The natural way to measure how "good" an
-idea is might be *how well it spreads* — but a false-but-persuasive idea spreads beautifully. Human
-culture is full of highly contagious nonsense; confident wrongness routinely beats careful accuracy. Let
-selection loose on models without care and it will breed a lineage that is fluent, compelling, and
-**wrong** — model collapse with an optimiser actively steering toward the cliff.
-
-Only one thing makes fitness track *truth* instead of *appeal*: **being judged against a reality that
-can say no.** Fitness has to mean "did this knowledge correctly predict what the world actually does when
-you act on it" — not approval, not fluency, not a gameable benchmark score. This is why the reality check
-matters twice: it's both the anchor that stops passive collapse *and* the only thing that keeps the
-evolving taste honest.
-
-The second danger is **everyone converging to the same thing**, and avoiding it takes work at two levels,
-because selection can only preserve variety that already exists — variety first has to be *supplied* and
-then *kept*.
-
-- **Supply.** A lineage that only learns from the accredited elite has a monoculture for a source — the
-  "top" experts are, by definition, the ones who won the consensus. So the system must deliberately learn
-  from **outliers and heretics** too — not out of fairness, but because diverse starting material is the
-  raw fuel for everything downstream.
-- **Preserve.** Even with varied input, plain "keep the best" selection converges — it stampedes toward
-  the single current champion and wipes out the rare specialists. The fix is well known:
-  **quality-diversity** selection, which rewards being *good* **and** being *different* at the same time,
-  keeping complementary specialists alive. In our simulations this is decisive: greedy "keep-the-best"
-  collapses diversity almost immediately and gets stuck on a mediocre answer, while quality-diversity
-  keeps the very specialists that sexual combining then needs as parents.
-
-The two levels meet at reproduction: multi-parent combining is the *vehicle* that carries the preserved
-diversity into the next generation. Supply the variety, preserve it, recombine it — remove any one and
-the lineage collapses onto its own first guess.
-
-## 8. A society needs institutions, not just experts
-
-One requirement is easy to miss and fatal to skip. The easy part of a society is having specialists. The
-*hard* part — which human civilisation took millennia to build — is the **institutions that let fallible
-specialists combine without each re-checking everything**: reputation, replication, credentials, and
-above all **peer review**. These are error-correction systems, and they exist because a group of
-unreliable experts left to reinforce each other is *more* wrong than any one of them alone.
-
-This is exactly where today's multi-agent AI fails: put several models in a room and they tend to agree
-sycophantically and confabulate together, because they have all the specialisation and none of the
-institutions. A real multigenerational society has to specify not just how models learn, reproduce, and
-get selected, but how they *check each other* — how a wrong model loses standing *before* its error gets
-merged into its children and inherited.
-
-## 9. The lineage must stay open to reality
-
-However many generations deep it goes, a society of models shares one hard limit: it has only ever
-*read*. Its entire inheritance is a record of things that were *said*. It lives on the bottom rung of
-what causality researchers call the ladder — **observation** — and no amount of reading ever reaches
-**intervention**. Watching doesn't tell you what would happen if you *acted*; correlation isn't causation
-at any scale.
-
-Only intervention — actually reaching out and changing the world to see what happens — climbs that
-ladder, and a language model can't intervene. That's what humans and their instruments provide, and the
-gift isn't "truth," it's **constraint**: reality's unique power is that it can say **no**. Text just
-offers more opinion; an experiment delivers a refusal no consensus can overturn. As before, that refusal
-does double duty: it stops collapse *and* keeps the evolving taste selecting for truth over persuasion.
-
-Two honest riders. First, the human reality signal is *dirty* — warped by publication bias, incentives,
-and the occasional fraud — which is exactly why the error-correcting institutions of §8 have to sit at
-the human–machine boundary. Second, humans are the *current* supplier of intervention, but the hands-on
-half is being automated (robot labs already run their own experiments). What looks durable in the human
-role isn't the hands — it's the **choice of what to test and which refusals matter**.
-
-## 10. Why this is affordable
-
-A practical fact turns this from daydream into buildable proposal: **it almost never re-pays for the one
-genuinely expensive thing in AI — training a model from scratch.**
-
-Training a foundation model from scratch costs a fortune in data and compute. This design does none of
-that per generation. Every model is *born* from an existing open model that already paid that cost;
-specialising one is a small patch trained in hours on a single consumer GPU; running the society is
-ordinary use; and reproducing — merging parents into a child — can be done directly on the weights with
-*no retraining at all*. Selection costs more (you have to run populations and discard the losers), but
-that's a multiplier on an already-cheap unit, not on a from-scratch budget.
-
-The economics only work with **open-weight** models — for practical and legal reasons at once. You have
-to be free to inspect, modify, and redistribute the weights, and most proprietary licences forbid using
-a model's output to train another (which is exactly what reproduction here does). That's not ideology —
-it's a structural constraint, and a democratising one: it puts the whole thing within reach of a single
-lab.
-
-## 11. Can it grow forever? Baking knowledge back into the base
-
-One thing we quietly assumed: can the lineage accumulate *without end*? The individual model is
-bounded — that's the clock. But the lineage looked unbounded, each generation starting a step ahead.
-Look closer and a second budget also fills up.
-
-Every new model is a clean base plus an inherited pile of *soft* patches — the acquired knowledge carried
-in add-on modules rather than baked into the core. Those patches are what make the lineage
-multigenerational — but they're not free: they slow the model down, and past some depth they're better
-*consolidated* than carried. The lineage matures too.
-
-The fix is the same operation, one level up. When a lineage's acquired knowledge has proven stable across
-enough generations, **re-mint the base**: bake all those accumulated patches into the *weights* of a
-fresh from-scratch-scale model — a new base that is *born already knowing* what took many generations to
-learn in patches. The soft budget resets; the next era starts from a higher floor. What was hard-won and
-*learned* becomes cheap and *built-in*. (Biologists have a name for acquired traits that eventually
-become innate: the **Baldwin effect**.)
-
-Three honest riders, because this is the most consequential step:
-
-- **Cost.** This is the one step that re-pays part of the from-scratch bill — the exception to §10. It's
-  bearable only because it's *rare*, spread thin over many cheap generations.
-- **Irreversibility.** Until now, one thing was always recoverable — the original clean base, whose lost
-  rare knowledge you could restore just by reloading the file. Bake the current lineage into new
-  permanent weights and that escape hatch closes: if the lineage had been quietly collapsing, re-minting
-  *freezes the collapse in place* and throws away the one uncollapsed reference that could have caught it.
-  In our experiments this happens exactly as feared — and a cheap safeguard prevents it: **only re-mint
-  while the lineage is provably healthy and diverse**, never as a rescue for one already drifting.
-- **Branching.** Different labs will re-mint on different criteria and produce different bases; the
-  lineage branches. That's not a bug — it's the tree of life, and it's exactly what open weights make
-  possible.
-
-So can it grow forever? **Yes — but only because it forgets and consolidates at every level, including
-the base.** Nothing is stored without limit anywhere. Unbounded growth of *capability* is bought with
-*bounded* storage plus periodic consolidation.
-
-## 12. One idea at four speeds
-
-Step back and it all resolves into a single process running at four nested speeds. The **downward**
-motion is transmission — passing hard-won knowledge down:
-
-1. **Within one model, over its working life:** experience moves from fast memory into slow, durable
-   weights, without catastrophic forgetting.
-2. **Between generations, at maturity:** mature models reproduce, recombined into a fresh one.
-3. **Across many generations:** each inherits the compressed achievements of the last and adds a little.
-4. **Across eras:** a proven lineage's accumulated patches get baked into a re-minted base, becoming
-   innate.
-
-The first and last are the *same operation at opposite ends of the scale* — fast/soft memory
-consolidating into slow/hard memory — one running overnight inside a single model, the other across an
-era inside a whole society. The **sideways** motion is selection — acting across the population at every
-speed, on the policies that decide what gets passed on, with reality as the judge and diversity kept
-alive so the specialists survive.
-
-Three rules govern all of it: **reproduce by combining, not copying, or you rot; keep the disagreements
-and surprises, or you converge; and anchor fitness to a reality that can say no, or you drift toward
-whatever is merely convincing.**
-
-## 13. What we actually built, found, and left open
-
-We didn't just argue this — we built small, fully reproducible models to test it, plus a first bridge to
-real language models. Here's the shape of the evidence (a separate document has the numbers).
-
-**What we built and found:**
-
-- **An exact account of collapse.** Because generational training *is* the genetic-drift process, we can
-  check our simulator against century-old formulas — and it matches to a fraction of a percent. Collapse
-  isn't argued by analogy; it's derived.
-- **Grounding is cheap — but has a limit.** A few percent of real data keeps most of a lineage's
-  diversity alive indefinitely — but not the very deepest rare cases, which need combining. This is what
-  makes a continually-learning society economically realistic rather than a data-hungry fantasy.
-- **"Merge, don't average."** Combining several teachers by *averaging* their outputs — the obvious
-  thing, and what a "model soup" does — mathematically cancels the benefit of having several teachers. A
-  *merge* that keeps each item's strongest source realises it. Most current multi-model setups get this
-  wrong by default.
-- **Collapse and its cure in real networks, and on real images.** We reproduced the same effects in small
-  neural networks and in a generator of handwritten digits (MNIST): a model trained on its own output
-  collapses to a single blurry digit, while a little grounding keeps all the styles alive. Honest
-  wrinkle: real networks *smooth* things over, so the naive diversity metric can mislead — the right
-  measure is distance-from-truth.
-- **Sex that beats the parents — and when it doesn't.** In simulations, combining complementary
-  specialist models produces a model better than any parent (Fisher–Muller), climbing toward the best
-  possible combination as you add more, more-diverse parents — while averaging and best-single-parent
-  level off below. On *tangled* problems, blind combining instead produces below-parent children
-  (outbreeding depression) — and *directed* combining (choose mates, screen offspring, many parents)
-  reliably fixes it.
-- **Monogamy vs promiscuity.** Sweeping how *widely* models merge — from local/monogamous to
-  everyone-with-everyone/promiscuous — against how tangled the skills are, the best breadth **shrinks as
-  the skills get more tangled**: wide promiscuous merging wins when skills are independent, but on tangled
-  problems it converges too fast onto one mediocre solution and finds a worse champion, so keeping
-  structured sub-populations wins. (Promiscuity always lifts the *typical* model but always destroys
-  variety.) A merging design knob the field, which throws everything in one pot, doesn't currently have.
-- **The combining claims, in real language models — with a sharp condition.** Merging fine-tuned Qwen
-  models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent; and
-  keeping parents separate and *routing*, or *breeding and screening* offspring, beats the plain average
-  — *but only when the task leaves room to lose*. On easy tasks the plain average is already at the
-  ceiling and the tricks add nothing; on hard tasks the average dilutes a specialist below even the best
-  single parent, and the smarter operators win clearly. This is a prototype (three task families, one
-  run), so we read it as *signs, not exact numbers* — the *whole grounded society* on a language model is
-  still the open step.
-- **The whole society, and why every part is needed.** In a population evolving on a "reality" landscape,
-  the full system — grounding + combining + preserved diversity — climbs to the top while keeping its
-  specialists. Remove *grounding* and it collapses into a confident, wrong consensus (a direct analogue
-  of training on the internet's growing pile of AI-generated text); remove *combining* and it gets stuck;
-  remove *diversity* and it converges too fast to a worse answer. Each removal breaks differently; only
-  the whole thing climbs.
-
-**What's borrowed vs. what's ours.** We're careful about credit because the area is crowded. **Already
-known (and we claim none of it):** that model collapse is genetic drift; that a merged model can beat its
-parents; that decorrelated parents merge better; that naive averaging is worse than smarter merges; that
-populations of self-improving models can climb. **What's genuinely new here** is the *theory* those
-results have outrun — a real population-genetics of sex for model societies, which *predicts* rather than
-just *observes*: the "merge, don't average" law, Fisher–Muller as the reason children beat parents,
-outbreeding depression on tangled landscapes (turning "when does merging help?" into something the
-problem's structure predicts), grounding as a migration-vs-drift balance with a critical real-data
-fraction, directed sex as AI's distinct advantage, the whole integrated society whose parts are shown
-*jointly necessary* — and, newest of all, **model speciation**: the account of *when two models are too
-far apart to merge at all*, confirmed in real weights.
-
-**What's still open — honestly.** We fill the old hole (what to select) by *evolving* the selector
-instead of designing it — but the hole *moved* rather than closed, and the new one is harder: **the
-fitness function.** What reality-anchored measure selects for *truth* without also selecting for
-*persuasion*, given that in our own species the two have been at war for the entire history of ideas?
-Alongside it: the *institutions* that let models correct each other before error is inherited (§8), which
-we don't solve; and the *calibration* of all the knobs the experiments left open — how many parents, how
-complementary, at what ratio of inherited-to-real data, how healthy a lineage must be before its
-knowledge is safe to make permanent. These are at least *measurable*, which is the difference between an
-open problem and a hole. And the biggest gap: the *combining* claims now hold in real language models,
-but the *society* — the grounded, diversity-preserving, continually reproducing loop — does not yet. The
-real test is to build that whole thing out of actual open language models and see whether all the signs
-survive. **The operators, checked; the living society, next.**
-
----
-
-*For the full argument, the literature it's positioned against, the exact predictions, and the
-references, see the complete paper: `the-evolution-of-sex-for-ai.md`.*
diff --git a/paper/the-evolution-of-sex-for-ai.md b/paper/the-evolution-of-sex-for-ai.md
deleted file mode 100644
index b1c6bcb..0000000
--- a/paper/the-evolution-of-sex-for-ai.md
+++ /dev/null
@@ -1,919 +0,0 @@
-# The Evolution of Sex for Artificial Intelligence
-
-### A population-genetic framework for societies of agents that reproduce, recombine, and stay open-ended
-
-*A perspective, written from a geneticist's chair. Companion to a set of minimal, reproducible working
-models and a first language-model prototype (both built).*
-
-**Giorgio F. Gilestro** · Department of Life Sciences, Imperial College London ·
-giorgio@gilest.ro · https://lab.gilest.ro
-
----
-
-### A note on vocabulary (please read this first)
-
-This paper sits at the meeting point of three fields, and it is written so that a reader from any
-one of them can follow all of it. We therefore **spell out** each field's jargon the first time it
-appears, even at the risk of belabouring the obvious for the specialist. A short glossary, in case
-you skipped a definition:
-
-- **Model collapse** *(machine learning)* — the degeneration that happens when you train a model on
-  data produced by earlier models, over and over: rare cases disappear and the model drifts toward a
-  bland average.
-- **Distillation** *(machine learning)* — training a fresh "student" model on the outputs of one or
-  more "teacher" models, so the student ends up knowing a compressed version of what they knew.
-- **Model merging** *(machine learning)* — combining several trained models directly, at the level
-  of their weights, into one — no retraining. (Think of it as breeding two models rather than
-  teaching a third.)
-- **Genetic drift** *(population genetics)* — the random loss of rare variants that happens in any
-  finite population simply because not everyone leaves offspring. It is the neutral, no-selection
-  baseline of evolution.
-- **Wright–Fisher process** *(population genetics)* — the standard mathematical model of drift. Our
-  minimal model of knowledge transmission *is* this process exactly; a real trained network is this
-  process plus a measurable, architecture-specific bias we quantify.
-- **Recombination / sexual reproduction** *(biology)* — making an offspring by combining pieces from
-  more than one parent, rather than copying a single parent (which is *asexual* reproduction).
-- **Muller's ratchet** *(population genetics)* — the way an asexual lineage, one that never
-  recombines, accumulates damage it can never undo. We will argue it is the right lens for the
-  *irreversible* part of model collapse — the capabilities that, once lost from every parent, no
-  merging can rebuild.
-- **Catastrophic forgetting** *(machine learning / neuroscience)* — a neural network overwriting what
-  it knew when it learns something new.
-
-We have tried to keep the big picture legible on every page, and to be candid about what is argument
-and what is evidence. The evidence is mostly from **deliberately small models** — mathematics, small
-neural networks, image generators, and evolutionary simulations. A first bridge to real language
-models exists — a prototype that recombines LoRA-specialised Qwen models up to 7B on a GPU cluster,
-which confirms the recombination signs (below) — but the *full grounded society* has not yet been
-built on a large language model. We will say so repeatedly, because the gap matters.
-
----
-
-## Abstract
-
-AI is turning from single frozen models to **populations of agents** that persist, specialise, and are
-increasingly *recombined* into new models — a shift visible in multi-agent societies, population-based
-self-improvement, and the explosion of **model merging**. The field is doing this with the vocabulary
-of evolution — "crossover," "mutation," "mate choice," "offspring that beat their parents" — but as
-loose metaphor draped over search algorithms. This paper argues that a rich, quantitative body of
-applicable theory already exists in the branch of biology that studies exactly this: the **evolution
-of sex**. Ninety years of population genetics analyse when reproducing a population by *recombination*
-beats copying, when it backfires, and how to do it better — and, read as an engineering framework, it
-supplies overlooked variables and testable design rules for keeping a society of models learning
-across generations instead of decaying. The underlying shift of perspective is the contribution we
-most want to land: **treat multigenerational model populations as systems whose inheritance,
-diversity, and compatibility must be managed — not merely as collections of models to optimise.**
-
-One diagnosis anchors the frame: training each generation on the last is **genetic drift**, and the
-resulting **model collapse** is the loss of rare variants a finite population always suffers (the
-Wright–Fisher process). We reached that account independently; it has also been formalised in
-parallel by others (Shumailov et al., 2024; Riis, 2026), whom we cite for priority of publication —
-a convergence we read as corroboration of the frame. This paper is about the structure the diagnosis
-opens: the remedy side and its limits. Single-
-teacher copying is **asexual** reproduction, and the irreversible arm of its decay corresponds to
-**Muller's ratchet** (a correspondence we state with its scope, not as identity); the remedy biology
-found for the ratchet is **sex**. A society of models should reproduce sexually — each new model
-**recombined from several complementary parents** (which the field already does, as *model merging*),
-selection **anchored to a reality that can say no** (not to the consensus of other models), and
-diversity actively **preserved**. In our models — from closed-form to trained networks to a
-language-model prototype — those three ingredients together let a lineage not merely avoid collapse
-but **climb**, producing models fitter than any ancestor (the **Fisher–Muller effect**) while each
-specialty is re-earned and exceeded; whether the full recipe holds at frontier scale is the open
-question the framework is built to test.
-
-From the geneticist's apparatus we extract falsifiable, load-bearing claims (each stated with its
-operator and scope in the text): (i) **"merge, don't average"** — a conservation result: refitting a
-child to the *mean of its parents' output distributions* conserves expected rare-capability mass at
-the single-parent level, cancelling the multi-parent gain *to first order in the rare-item regime*
-(outside it, variance reduction from averaging can help — the result is a first-order cancellation,
-not a universal impossibility), while union-preserving operators realise the gain in all regimes —
-derived in the minimal model, with its weight-space image the headroom rule below; (ii)
-**offspring can exceed every parent** (Fisher–Muller), the real argument for sex in model societies;
-(iii) on **rugged, epistatic** task landscapes, blind recombination causes **outbreeding depression**,
-yielding a design rule — *merge freely when skills are additive, sparingly and with selection when
-entangled, and route rather than blend under overlap*; (iv) **grounding is immigration** from a
-non-drifting reality, giving a critical real-data fraction far below one; and (v) — the sharpest new
-prediction — sex has a **limit**: as two models diverge they undergo **speciation**, a
-merge-compatibility cliff (compatible → outbreeding depression → hybrid inviability) whose onset is set
-by divergence *and* epistasis via **Bateson–Dobzhansky–Muller incompatibilities**, and whose damage
-grows *super-linearly* (the Orr–Turelli snowball). We introduce and model this "model speciation"
-directly, and confirm it in real trained weights: a merge barrier that survives alignment under the
-*full* function-preserving symmetry group of the network (not just Git Re-Basin permutations), rising
-with functional conflict while hybrid fitness falls to inviability — with an honest converse we
-pre-registered and found: absent conflicting training signals, divergently-specialised lineages of
-shared ancestry developed *no* isolation at any divergence tested, the merge instead *rescuing* the
-forgetting specialists. Isolation must be provoked by conflict; specialisation alone did not speciate.
-AI also has an advantage biology lacks: **directed sex** — unbounded parents, chosen mates,
-and offspring screened before they are kept — engineered recombination with a flexibility of parent
-choice and pre-deployment screening that natural mating systems do not approach.
-
-We support the argument with **minimal, reproducible models** — a closed-form-exact account of drift
-and grounding, the same effects in small trained networks and an MNIST image generator, a real-weight
-demonstration of the speciation cliff (a Git Re-Basin residual that survives neuron alignment), and
-evolutionary simulations of the whole society — and a first **language-model prototype**: merging
-LoRA-specialised Qwen models (to 7B on a GPU cluster) yields a generalist that beats every specialist
-parent, with the sharp headroom condition under which "merge, don't average" bites. The scope is
-honest: these are existence proofs and design rules; the *whole grounded society* on a large language
-model is the open step. We position the work carefully against the crowded 2025–2026 landscape of
-evolutionary-AI and merging methods — conceding what they own and marking, precisely, what a genuine
-population-genetics of sex adds.
-
----
-
-## 1. From a society in space to a society in time
-
-The idea of many AI agents working together — a "society of mind" (Minsky, 1986), or today's
-multi-agent systems — arranges intelligence across *space*: several specialists side by side,
-dividing a task. This paper is about a different axis: *time*. Not a society that merely exists at
-one moment, but one that **persists and renews across generations**, each new cohort of models
-starting from the compressed knowledge of the last.
-
-The unit that matters is therefore the **generation**, and the event that matters is **reproduction**:
-the making of a new model from older ones. A single model, like a single mind, is bounded and
-eventually stops improving. A *lineage* need not be. Human civilisation is not clever because any one
-person is; it is clever because each generation inherits the distilled achievements of the previous
-one and adds a little. We propose building AI the same way — and, crucially, getting the *reproduction*
-right, because that is exactly where it can go wrong.
-
-### Where this sits, and what is new
-
-This axis is suddenly crowded. By 2026 several groups build **populations of models or agents that
-improve across generations**: societies of independently-specialised models that self-improve for more
-rounds than a single agent (Multiagent Finetuning — Subramaniam et al., 2025); open-ended archives of
-self-rewriting coding agents (the Darwin–Gödel Machine — Zhang et al., 2025); groups that evolve by
-sharing experience across branches (Weng et al., 2026); persistent agent *ecologies* with reproduction
-and cumulative culture (TerraLingua — 2026). In parallel, **model merging** has become a small industry
-with an overtly evolutionary vocabulary: crossover-mutation-selection over LLM populations (GENOME —
-2025), niching and "mate choice" (Sakana's M2N2 — 2025), and evolutionary search over merge recipes
-(Akiba et al., *Nature Mach. Intell.* 2024/25).
-
-We are candid about the consequence. Three things we do **not** claim. First, that collapse is
-Wright–Fisher drift: formalised independently (Riis, 2026; Shumailov et al., 2024), sharpened to a
-closed-form first-extinction law whose onset coincides with collapse (Benati et al., 2025) and to a
-quantitative-trait-genetics account for diffusion models (Yoon et al., ICLR 2025), and conceded here.
-Second, the bare empirical facts that a merged model can beat its parents, that decorrelated parents
-merge better, and that naive averaging is inferior to sign- or routing-based merges (TIES, DARE,
-mixture-of-experts routing): all established. Third, that merge success can be *predicted at all*:
-machine-learning-native predictors exist, from interpretable pairwise metrics (gradient alignment —
-Zhou et al., 2026) to capacity/rate-distortion accounts of "merging collapse" (2026); what they lack,
-and we supply, is the *mechanism* — when and why the failure is a coordinate artefact versus genuine
-functional incompatibility, and what moves the cliff. What a geneticist is placed to supply is a
-**framework** rather than a search heuristic. The nearest precursor is a theory-of-computation
-tradition reading sex as an algorithm for *mixability* (Livnat & Papadimitriou, 2016), pre-dating
-model merging; the works above use evolution chiefly as vocabulary over an optimiser, and — to our
-knowledge — the quantitative apparatus of the evolution of sex (Fisher–Muller, outbreeding depression,
-migration–drift balance, reproductive isolation) has not previously been carried over as more than
-metaphor. We are also candid about what *kind* of contribution each of our claims is, because three
-different things are easily conflated: **interpretation** (an existing result is usefully understood
-in these terms — e.g., merged offspring beating their parents as Fisher–Muller), **explanation** (the
-transferred mechanism accounts for observations existing accounts leave open — e.g., which merge
-failures are coordinate artefacts and which are functional), and **prediction** (the framework
-forecasts an unmeasured outcome and improves a design decision). This paper is strongest on the
-first, makes concrete progress on the second, and reports a first, bounded step on the third: a
-**controlled predictive test** at small scale in which pre-merge *functional-disagreement* measures —
-chosen by the framework — showed a detectable, held-out-robust association with merge damage on a
-constructed task grid, while the selected weight-geometry baselines did not. We are precise about
-that result's boundary where it is reported: it is a small-model demonstration on a constructed grid;
-the proposed epistasis-specific refinement did not outperform plain disagreement; predictor
-differences are not individually significant head-to-head; and whether the prediction improves a
-budget-matched operator choice remains open. The organising shift we argue for
-is prior to any single mechanism: **treat multigenerational model populations as systems whose
-inheritance, diversity, and compatibility must be managed — not merely as collections of models to
-optimise.**
-
-## 2. Why today's models cannot do this
-
-Today's large language models have no life cycle. They are trained once, at enormous cost, then
-**frozen** and deployed as a fixed artefact that does not learn from the people it serves. Learning
-and doing are split into two eras with no bridge between them.
-
-There is a real reason for the freeze. Updating a neural network on new information tends to overwrite
-what it already knew — **catastrophic forgetting**, a problem understood since the late 1980s
-(McCloskey & Cohen, 1989; French, 1999). Freezing avoids it by refusing to learn at all. The result
-is a mind with no childhood, no growth, and no way to pass anything on. A lineage needs the opposite:
-members that learn through their working lives, reach maturity, and hand on what they gained. So the
-first requirement is a learner that can grow *safely*.
-
-## 3. A learner that can grow without forgetting
-
-The individual model needs two properties.
-
-**It must not catastrophically forget.** Instead of overwriting its core as it learns, it keeps that
-core frozen and only *readable*, and carves each new skill into freshly-added capacity beside it. In
-machine learning this is called *parameter isolation* (progressive networks — Rusu et al., 2016;
-prune-and-freeze — Mallya & Lazebnik, 2018; and, most practically, **LoRA** and other small trainable
-"patches" bolted onto a frozen model — Hu et al., 2021). If the core is never altered, its *parameters*
-cannot be forgotten — though a precise reader should note the system's *behaviour* can still shift
-while adapters are active, so the guarantee is of a recoverable core, not of unchanging conduct. This
-is what lets a model accumulate a coherent working life of expertise — the kind of stable knowledge
-worth passing on.
-
-The brain offers a partial blueprint. *Complementary Learning Systems* theory (McClelland,
-McNaughton & O'Reilly, 1995) — itself a response to the forgetting problem — describes two subsystems:
-a **fast** store (the hippocampus) that grabs an experience in one shot, and a **slow** store (the
-neocortex) that integrates regularities gradually without disruption. We do not lean on any particular
-account of how the brain moves knowledge between them; the architecture needs only that *some*
-periodic **offline consolidation** step exists, moving knowledge from the fast store to the slow one
-when the system is idle. The machine version is clean regardless: the prompt is working memory, an
-external database is the fast episodic store, the trained weights are the slow store, and consolidation
-migrates the first into the last.
-
-**It is bounded.** Because the model only ever *adds* capacity and freezes what it has, it eventually
-fills up. In most designs that is a wall to dread. In ours it is a clock.
-
-## 4. "Full" is maturity, not failure
-
-Here is the pivot. A bounded learner that fills up has not broken. **It has grown up.**
-
-Read the capacity limit as a life stage. A model is *born* as a freshly-schooled base — its general
-education. It enters a **working life**, adding specialised knowledge as it does its job. And it
-reaches **maturity**: the point where it has learned much of what one working life in its niche can
-teach. Maturity is not the end of usefulness — it is the moment the model is most worth learning
-*from*. So maturity is the cue to **reproduce**. The capacity ceiling that every other architecture
-fights becomes, in ours, the metronome of the generations.
-
-Everything now turns on how that reproduction is done — and this is where the paper's central claim
-lives.
-
-## 5. Reproduction: copying collapses, recombination climbs
-
-Suppose a mature model simply teaches a fresh one — distillation, one teacher to one pupil, generation
-after generation. This is the obvious design, and it fails, for a reason that is exactly the same in
-machine learning and in biology.
-
-**The machine-learning statement.** Training each generation on the previous generation's outputs is
-the recipe for **model collapse**: the model forgets the improbable, loses the *tail* of the
-distribution (the rare cases) first, and drifts toward its own most common output (Shumailov et al.,
-2024). Worse for us, the very rule that makes distillation useful — *keep the general, drop the
-idiosyncratic* — **is** tail-deletion by design. The operation that would power a cultural ratchet and
-the operation that drives model collapse are the same act.
-
-**The population-genetics statement (the same thing, for the minimal model).** Represent a model's
-knowledge as a distribution over discrete "items" — capabilities, facts, modes of behaviour. One
-generation is: *draw a finite sample from the parent, and refit the child to it.* In this **minimal
-inheritance model** the finite-sampling step is **exactly** genetic drift — the random loss of rare
-variants in a finite population — described by the century-old **Wright–Fisher** model (Wright, 1931;
-Fisher, 1930): the same equations, which we use as closed-form checks on our simulations. Rare items
-go extinct first, roughly ten times faster than common ones, precisely as drift predicts. **The
-boundary of the identity matters, and we measured it:** real neural training adds approximation,
-optimisation noise, and inductive bias on top of sampling, and when we fit trained networks against
-the exact drift null they deviate in *opposite, architecture-specific directions* — a smoothing
-recurrent model resists collapse (it keeps spurious variants alive), a sharpening image generator
-accelerates it (our learning-kernel result, below). So the honest statement is: the minimal
-inheritance model is exactly Wright–Fisher; a real learner is Wright–Fisher *plus a signed,
-measurable estimator-bias operator* — and the drift signs (rare-first loss, the grounding response)
-survive that operator in every architecture we tested.
-
-And single-teacher copying is **asexual reproduction** — cloning one parent. Nature already knows what
-happens to an asexual lineage that never recombines: it accumulates damage it can never repair, a
-one-way decline geneticists call **Muller's ratchet** (Muller, 1964). We use the ratchet as the
-*organising correspondence* for model collapse, with its scope stated: strictly, the ratchet is the
-stochastic loss of the least-degraded class under recurring deleterious change in an asexual
-population, so it maps onto the *irreversible* component of capability loss (once every copy of a rare
-capability is gone from all parents and sources, no recombination can rebuild it) rather than onto
-every form of degradation. That is exactly why the correspondence is useful rather than decorative: it
-says the cure must act *before* fixation-by-loss — keep complementary variants alive somewhere in the
-population — because recombination can only reassemble what still survives. Biology solved this
-problem, and its solution is the subject of this paper.
-
-Two ingredients turn the collapse operation into a climb. Both are things nature does.
-
-**First: do not reproduce "dry."** Model collapse is a property of a lineage fed *only* its own
-output; the documented fix is that keeping some real data in the mixture arrests it (Shumailov et al.,
-2024). We call that real data **grounding** — fresh contact with the world, verified against it. In
-our minimal models, grounding is startlingly cheap: mixing in even a few percent of verified real data
-holds on to most of the diversity indefinitely. But — an honest limit we found and did not expect —
-grounding cannot save the *very rarest* items at any affordable budget; protecting an item of rarity
-*p* needs a real-data budget that grows like 1/*p*. Grounding rescues diversity cheaply; it does not,
-by itself, rescue the deep tail. Something else must. That something is sex.
-
-**Second: reproduce sexually.** Instead of copying one parent, build each new model by **recombining
-several** — a *sexual* rather than asexual birth. In machine learning this already has a name and a
-working implementation: **model merging** (Akiba et al., 2024). Its importance here is not efficiency;
-it is that recombination does something copying cannot. If several parent models have each specialised
-on different parts of reality, each has kept alive rare knowledge the others lost. A recombined child
-inherits the **union** of what its parents kept — not the tail-thinned *average* of a crowd of
-near-identical copies. And here is the point that lifts sex from a safeguard to the engine of the whole
-scheme, and the reason biology invented it:
-
-> **An offspring recombined from complementary parents can be *fitter than any of its parents*.**
-
-Geneticists call this the **Fisher–Muller effect** (Fisher, 1930; Muller, 1932): recombination brings
-together, in one individual, beneficial variants that arose separately in different lineages, so the
-child holds a combination none of the parents had. In our simulations this is exactly what we see —
-recombining decorrelated specialist models yields a model that climbs toward the best-possible
-combination, a genotype *no single parent possessed*, while the best single parent, and the naive
-average of all of them (what the field calls a "model soup" — Wortsman et al., 2022), both plateau
-well below. This is the concrete meaning of the paper's title claim, "the lineage climbs in general
-knowledge; specialisation is re-earned each generation," and it is why the reframing from
-teacher→pupil to *sexual reproduction* is not cosmetic: **copying can only recover a ceiling;
-recombination can exceed it.**
-
-This is no longer only a simulation. In a first language-model prototype — LoRA specialists on
-disjoint task families, recombined and judged by an exact verifier — a merge of three specialist Qwen
-models (7B, on a GPU cluster) **beats every single specialist**, overall and on every family: the
-Fisher–Muller effect, in real weights. The same prototype pins down *when* the finer "inherit the
-union, don't average" rule actually bites. Keeping each parent whole and **routing** each input to the
-right one beats the tail-thinning average — but only when the task is hard enough to leave room to
-lose: on easy tasks a strong model's plain average is already at the ceiling, so the crude soup is
-fine, whereas on hard tasks the average dilutes a hard-won specialist so badly it falls below even the
-best single parent, and routing wins by a wide margin. The rule is therefore precise: **the union
-beats the average in exact proportion to how far the average is from the best attainable** — a caveat
-that sharpens rather than weakens the claim, and that a practitioner needs before spending compute on
-the fancier operator.
-
-**The operator boundaries (stated, because "merge, don't average" is not one claim but a family).**
-Four different operators travel under these words, and the conservation result belongs to exactly one
-of them. What is *derived* is this: when a pupil's knowledge is refit to the **mean of the parents'
-output distributions**, the expected mass on any rare item is conserved at the single-parent level —
-in the rare-item regime (`n·p/K ≪ 1`) the 1/K dilution of averaging cancels the union gain of having
-K parents to first order — outside that regime, survival is convex in mixed mass and averaging's
-variance reduction can help, so this is a first-order cancellation, not a universal impossibility;
-whereas an operator that keeps, per item, its **strongest source** (and renormalises, which itself
-redistributes mass) realises the union in all regimes. That statement is exact in the minimal model, and it presupposes an
-oracle (or verifier) able to say which source is strongest. The two operators the LLM prototype
-tests — **weight averaging** (a nonlinear network's weight-mean does not compute the mean of its
-parents' outputs) and **routing among intact specialists** (which keeps K models' storage and an input
-classifier, a different parameter and inference budget from one fixed-size child) — are *empirical
-cousins* of the two sides of that law, not instances of it. The headroom rule above is precisely the
-empirical bridge: it says when the weight-average behaves like the diluting mean (hard tasks, weak
-base) and when a capable base absorbs the dilution (easy tasks). And all of it operates within a
-capacity boundary: when parental capabilities genuinely cannot coexist in the child's capacity, no
-operator preserves the union — that regime is the subject of the speciation section below.
-
-Three results keep this honest, and all are results, not hand-waving.
-
-*Sex can backfire.* When the parents' skills are not cleanly separable but **entangled** — when the
-value of one capability depends on which others are present (geneticists call this **epistasis**) —
-blindly recombining two good models can produce a *worse* child, because recombination breaks up a
-combination that only worked as a whole. Biologists call this **outbreeding depression**, and we
-reproduce it: on "rugged" (highly entangled) problems, naive merging drops offspring below their
-parents, and the more you mix the worse it gets. The design rule that falls out is simple: *merge
-freely when skills are complementary; merge sparingly, and carefully, when they are entangled.*
-
-*The mating system matters too — not just who mates, but how widely.* The result above is about the
-recombination *rate*; a separate knob is the population's **mating structure** — whether reproduction is
-**monogamous** (each model recombines within a narrow, local circle) or **promiscuous** (mates drawn
-freely from the whole population). Almost all model-merging implicitly assumes promiscuity — fuse
-everything, or route over one flat pool — but population genetics says the breadth of gene flow is itself
-consequential, because wide flow spreads good variants fast while **homogenising** the population, and
-narrow flow preserves the distinct sub-populations needed to explore several solutions at once (Wright's
-*shifting balance*). We sweep exactly this breadth against landscape ruggedness, and the optimum moves:
-on smooth (additive) landscapes wide, promiscuous mating is best (spread the one good direction fastest),
-but as the landscape gets rugged the best breadth **shrinks to an intermediate value** — full promiscuity
-prematurely converges onto one basin and finds a *worse* champion, while pure monogamy over-fragments.
-Throughout, wide mating lifts the *typical* model but monotonically **destroys diversity** — so on rugged
-problems, where the best model needs preserved diversity to be found, structured (partly monogamous)
-merging wins. The design rule extends the one above: *merge widely when skills are additive; keep
-structured sub-populations — island-style merging — when skills are rugged.* (Figure: `results/figS13_mating_breadth/E14.png`.)
-
-*AI can do sex better than biology can.* Biology is stuck with two parents, mating roughly at random,
-and cannot inspect an offspring before it is born. An AI has none of those limits. It can recombine
-**many** parents at once; it can **choose** which parents to combine, for complementarity; and it can
-**generate many candidate offspring and keep only the fittest**, screening them against reality before
-committing. We call this **directed sex**, and in our simulations it converts the outbreeding-depression
-catastrophe into a reliable gain: where blind recombination collapses on entangled problems, directed
-recombination matches or beats the best parent every time. The language-model prototype shows the same
-sign where it can: breeding many recombined Qwen offspring and keeping the one the verifier scores
-highest beats the single averaged soup on hard tasks (and, unsurprisingly, does nothing extra on easy
-tasks the soup already solves). This is a genuine advantage of engineered reproduction over the
-biological kind, and we think it is one of the more useful ideas in the paper.
-
-So the picture of §5 is: single-teacher copying is asexual and collapses (Muller's ratchet = model
-collapse); the cure is to *ground* every birth in reality and to reproduce *sexually*, recombining
-many complementary parents; and because AI sex can be many-parent, mate-chosen, and offspring-screened,
-it is not merely a hedge against collapse but an engine that produces children fitter than any parent.
-
-### The limit of sex: model speciation
-
-Sex has a limit, and it is the sharpest new prediction this frame makes. Recombination works because
-the parents are variations on a shared background; push two lineages far enough apart and their
-combination is no longer viable. In biology this is **speciation** — the onset of **reproductive
-isolation** — and its genetic mechanism is the **Bateson–Dobzhansky–Muller incompatibility** (BDMI):
-an allele that arose in one lineage and an allele that arose in the other are each harmless on their
-own background, but their *combination*, never tested by selection in either parent, is deleterious in
-the hybrid (Dobzhansky, 1937; Muller, 1942; Orr, 1995). A merged model is precisely such a hybrid — a
-single *recombinant* genotype, an F2-like object exposed to **recombination load**, not a hybrid-vigour
-F1 — so the theory predicts a specific trajectory as two models diverge: **compatible → outbreeding
-depression → hybrid inviability**.
-
-We built this as an explicit model (a companion result). Two lineages descend from a common ancestor,
-each substituting a *disjoint* set of loci — so each parent is adapted and neither carries an
-incompatibility — and a fraction of the cross-lineage locus pairs are BDMIs that fire only when a hybrid
-inherits *both* derived alleles. Sweeping the divergence between the parents reproduces the predicted
-curve exactly: hybrid fitness tracks the parents while they are compatible, then peels off, peaks, and
-crashes below the ancestor (an inviable hybrid). Three things fall out, and they are the contribution:
-
-1. **The isolation cliff, and what moves it.** The divergence at which merging fails is not fixed: it
-   arrives *earlier the more epistatic the capability landscape*. In the model the reproductive-isolation
-   rate at high divergence rises from ~0 to ~0.5 as the density of incompatibilities grows. This is the
-   paper's distinct, falsifiable claim — **at matched divergence, mergeability is governed by epistasis,
-   not by divergence alone** — and it is exactly the axis that the machine-learning predictors of merge
-   success (which are all divergence/geometry measures) do not have.
-2. **The snowball.** The number of incompatibilities grows with the *square* of the divergence
-   (Orr & Turelli, 2001), so hybrid fitness falls *super-linearly*: divergence is punished faster than
-   it accrues. Merge compatibility does not decay gently; it falls off a cliff.
-3. **The design rule.** *Before merging, weigh divergence against the ruggedness of the shared
-   capability landscape; past the cliff, do not merge — route* (the engineering echo of allopatry:
-   keep the specialists reproductively separate and select among them instead of hybridising).
-
-This is where a geneticist's lens earns its keep. The machine-learning literature has *observed* that
-increasing specialisation eventually breaks merging and that one should then route rather than fuse
-(Pari et al., 2024; Zhou et al., 2026), and part of the apparent incompatibility between independently
-trained models is a coordinate artefact removable by aligning neurons (Git Re-Basin — Ainsworth et al.,
-2022). What the frame adds is the *theory* of the phenomenon they observe: its functional form, its
-super-linear (snowball) onset, and its dependence on epistasis — merge failure as a Dobzhansky–Muller
-event. (Figure: `results/fig5_speciation_bdm/E12.png`.)
-
-**The real-weight confirmation.** The obvious objection to the analytic model is that its
-"incompatibility" is a re-labelled loss barrier, and loss barriers between independently trained
-networks are famously a *coordinate* artefact — two nets that learned the same function in a permuted
-basis look incompatible until their neurons are aligned (Git Re-Basin), and recent work shows that
-symmetry groups *richer* than permutations remove still more of the barrier (functionality-preserving
-rescalings and rotations — Scaling LMC, 2026; neuron-identifiability approaches). We therefore ran the
-experiment the objection demands, in real trained weights, aligning modulo the **full**
-function-preserving unit symmetry group of the architecture (per-unit positive rescaling composed with
-permutation — for a plain ReLU network, all of it). Two small MLPs are forked from a shared MNIST base,
-trained, weight-averaged, and their linear-mode-connectivity error barrier is measured *before and
-after* alignment; the after-alignment **residual** is the part of the incompatibility that no
-re-coordination can explain away. The decomposition is clean (Figure:
-`results/speciation_real/speciation_real.png`): two nets trained *from different random initialisations
-on the same task* have a real naive barrier that alignment removes almost entirely (residual ≈ 0.001,
-and the aligned merge performs at parent level) — same species, different basis, the canonical Re-Basin
-result, which also proves the aligner works. Two nets that learned *conflicting* label maps have a
-large barrier of which the full symmetry group removes **essentially nothing** (0.502 → 0.497) —
-a conflict-associated barrier the tested alignment leaves largely unchanged — supporting a
-functional-conflict interpretation without proving optimal alignment (control recovery validates a
-special case; the removable share is a lower bound, the residual an upper bound). It also carries a floor no future alignment
-method can breach: models loyal to label maps that conflict on a fraction *μ* of inputs cannot both be
-served by *any* single merged model, which must err at rate ≥ *μ*/2 against at least one parent
-(SI proposition). Sweeping the fraction of conflicting classes traces the **isolation cliff in real
-weights**, now readable directly as *hybrid fitness*: the residual barrier climbs monotonically while
-the merged model's accuracy falls from 0.97 to 0.03 — E12's compatible → depression → inviability
-trajectory, measured.
-
-**And its honest converse: speciation must be provoked; it did not emerge.** A true
-Dobzhansky–Muller incompatibility is *emergent* — each lineage's changes harmless alone, incompatible
-only in combination — whereas the conflict condition above *imposes* contradiction. So we pre-registered
-the emergent test: fork two children from a shared base and let them diverge with **no conflicting
-training signal anywhere** — one pair as complementary class specialists (one child trains only on
-digits 0–4, the other only on 5–9), one pair with divergent input conventions (views shifted in
-opposite directions) — out to divergences 6.4× the base training. The result is the second
-pre-registered reading, and it sharpens the theory's scope rather than confirming its most dramatic
-form: the residual barrier is **0.000 at every divergence in both conditions**, and far from failing,
-the merge *rescues* the two specialists — each parent decays toward ~0.50 on the full task
-(catastrophically forgetting the classes it no longer sees) while the merged model holds ~0.95
-throughout, a sustained Fisher–Muller rescue at zero barrier. In real weights, at least in this regime
-of shared ancestry and compatible tasks, **reproductive isolation requires functional conflict; it does
-not arise spontaneously from divergent specialisation.** The design rule sharpens accordingly: *merge
-freely across divergently-specialised lineages of shared ancestry — what speciates model populations is
-conflicting conventions, not specialisation per se.* Whether long-horizon over-specialisation erodes
-mergeability at language-model scale — as the empirical merging literature hints (experts trained
-longer merge worse under averaging) — is exactly the next tier's question, and the theory now makes the
-prediction crisp: it should depend on whether extended training induces *conflicting conventions on
-shared circuitry*, not on divergence time itself.
-
-**What these experiments do and do not establish.** Stated at exactly the strength of the evidence:
-they establish that *some merge failures reflect incompatible functional requirements rather than a
-mismatch of coordinates* — a residual that survives the full unit-symmetry group of the architecture
-tested, rises with functional conflict, and is absent under compatible specialisation. Three
-qualifiers. First, the impossibility at the heart of the conflict condition — one deterministic model
-cannot satisfy two contradictory answer conventions — is information-theoretic and needs no population
-genetics; what the genetic frame adds is *structure around it*: which divergences generate such
-conflicts, the prediction that epistasis rather than distance sets the cliff's position, and the
-snowball's super-linear onset — the latter two verified so far only in the analytic model, and
-therefore carried as **hypotheses at the neural tier, not results**. (On the snowball, one more
-distinction: super-linear growth in the *number* of incompatibilities does not by itself entail a
-sharp *performance* cliff — that needs the link from incompatibility count through effect sizes to
-measured performance, which the analytic model supplies under its assumptions and any neural test
-must establish separately.) Second, our alignment removes the symmetries we enumerate for this
-architecture class, and exactly recovering a permuted-and-rescaled copy validates a special case
-rather than proving global optimality for independently trained networks — so the removable share is
-a lower bound and the residual an upper bound; richer transformation families for other architectures
-could reapportion the split, though not below the conflict floor. Third,
-"unmergeable" here means by aligned linear interpolation of weights — a barrier to that operator does
-not preclude every conceivable recombination method (routing, for one, sidesteps it by not blending).
-Emergent Dobzhansky–Muller incompatibilities in real weights remain the flagship *hypothesis* of this
-programme: our tested regimes found none, which bounds where they can live — longer horizons, shifted
-data distributions, capacity pressure — and the decisive experiment (predicting merge success *before*
-merging from an operational epistasis measure, against geometry- and gradient-based predictors) is
-posed in the closing section.
-
-One question remains, and the rest of the paper is largely about it: recombination combines what the
-parents kept — but *who decides what each parent keeps, and which offspring are worth keeping?*
-
-## 6. The second inheritance: letting "what is worth keeping" evolve
-
-There are two answers, and the first is wrong. We could try to *design* the rule for what knowledge to
-keep and pass on. But nobody knows that rule. "Keep the general, drop the particular" is a slogan, not
-an algorithm: ask *which* generalisations, in *which* domain, at *which* grain, and the hand-written
-rule falls apart. This is the deepest hole in the scheme, and it cannot be filled by decree.
-
-The second answer is the one nature used: **do not design the selector — evolve it.** Let different
-models carry different *policies* for what is worth keeping and combining. Let the policies that
-produce more capable offspring spread; let the policies that produce weak offspring die out with their
-lineages. The lineage's *taste* — its sense of what matters — is discovered by selection, not imposed.
-
-So **two things are inherited, on two channels.** The *content* passes down directly: an offspring
-receives its parents' knowledge (this is the "Lamarckian" channel — the inheritance of things acquired
-during a lifetime, which biology forbids for genes but culture allows for ideas). The *selection
-policy* — what to keep, whom to breed with, which offspring to screen for — is itself inherited, varies
-between models, and survives in proportion to the success it produces. That second channel is
-**Darwinian**. The architecture is therefore both at once: Lamarckian in *what* it transmits, Darwinian
-in *what it keeps*. Evolutionary theorists call this structure *dual inheritance* and identify it as
-the engine of human culture (Boyd & Richerson, 1985); philosophers of science describe scientific
-knowledge itself as growing this way, by conjecture and **refutation** (Popper, 1959; Campbell, 1974;
-Hull, 1988).
-
-The closure that makes this fit together, rather than merely sound nice: Darwinian selection needs a
-*selection pressure* — something that decides which policies win. That pressure is already in the
-design. What tells a lineage its taste was good? The success of its offspring **against reality**. The
-reality-check that stops collapse (grounding, §5) and the fitness signal that drives the evolving taste
-turn out to be the *same thing*, seen from two sides.
-
-## 7. The central danger: fitness is not truth
-
-Introducing selection introduces selection's classic hazard, and it is severe enough to sink the whole
-scheme if ignored. Evolution optimises, without mercy or foresight, for exactly what you *measure* —
-never for what you *meant*. (Economists and ML engineers know this as **Goodhart's law** and
-*specification gaming*.) Get the fitness measure slightly wrong and the lineage will exploit the gap
-with more ingenuity than any designed rule.
-
-For a *knowledge* lineage there is a specific and nasty version. For ideas, the natural measure of
-"fitness" is **how well they spread**, and a false-but-persuasive idea spreads beautifully. Human
-intellectual culture is full of highly transmissible falsehoods; confident nonsense out-competes hedged
-accuracy in almost every human forum. Turn Darwinian selection loose on models without care and it will
-breed a lineage optimised for *persuasiveness* — fluent, compelling, and wrong. That is model collapse
-with an optimiser behind it, actively seeking the cliff.
-
-Only one thing makes fitness track truth rather than appeal: **being judged against a reality that can
-say no.** Fitness must be predictive success under *intervention* — did the model's knowledge correctly
-anticipate what the world would do when acted upon — and not approval, fluency, or a benchmark score,
-each of which can be gamed. This is why the reality-check is load-bearing twice over: it is both the
-anchor that stops passive collapse *and* the only thing that keeps the evolving taste honest.
-
-The second danger is **convergence**, and beating it takes work at two separate levels, because
-selection can only preserve variety that already exists — the variety must first be *supplied* and then
-*kept*.
-
-- **Supply.** A lineage that learns only from an accredited elite has a monoculture for a source: the
-  "best" experts are, almost by definition, the ones who won the consensus, so the incoming variation
-  is narrow from the start. The society must therefore learn, deliberately and from the beginning, from
-  the **outliers and the heterodox** as well as the credentialed — not out of fairness, but because in
-  evolutionary terms diverse founders are the raw material without which nothing downstream can adapt.
-- **Preserve.** Even given varied input, plain fitness-*maximising* selection converges — it drives
-  every lineage toward the single current best and fixes it, extinguishing the rare specialists. The
-  fix is well established: **quality-diversity** selection, which rewards being *good* and being
-  *different* at once (novelty search and MAP-Elites — Lehman & Stanley, 2011; Mouret & Clune, 2015),
-  keeping complementary specialists alive rather than collapsing onto the champion. In our simulations
-  this is decisive: greedy "keep-the-best" selection collapses a population's diversity almost at once
-  and gets stuck at a mediocre answer, while quality-diversity selection keeps the specialists that
-  sexual recombination then needs as parents.
-
-The two levels meet at reproduction. Multi-parent recombination (§5) is the *vehicle* by which the
-diversity this selection preserves actually enters the next generation: an offspring drawn from
-complementary parents inherits the standing variation the selector kept alive, recombined into one new
-model. Supply the variety from the human side; preserve it on the selection side; recombine it into
-each generation on the reproduction side. Remove any of the three and the lineage converges on its own
-first guess.
-
-## 8. A society needs institutions, not just specialists
-
-One requirement is easy to overlook and fatal to omit. The easy part of a society is specialisation.
-The *hard* part — which human civilisation took millennia to build — is the set of **institutions that
-let fallible specialists combine without each re-verifying everything**: reputation, replication,
-credentials, and above all **peer review**. These are error-correction protocols, and they exist
-because a group of unreliable specialists left to reinforce one another is *more* wrong than any member
-alone.
-
-This is precisely where current multi-agent AI fails: set several models to confer and they tend to
-agree sycophantically and confabulate in committee, because they have all the specialisation and none
-of the institutions. A multigenerational society must specify not only how models learn, reproduce, and
-are selected, but how they *check* one another — how a claim is challenged and a mistaken model loses
-standing *before* its error is recombined into offspring and inherited. Peer review is itself a
-reality-check of the kind §7 demands — an institutional stand-in for reality's "no," to be used where
-direct intervention is slow or costly.
-
-## 9. The lineage must stay open to reality
-
-A society of models, however many generations deep, shares one hard limit: it has only ever *read*.
-Its whole inheritance is a record of things that were said. In the vocabulary of causal reasoning
-(Pearl, 2009), it lives on the bottom rung of the **ladder of causation** — observation — and no amount
-of observation reaches *intervention*. Watching underdetermines doing; correlation does not contain
-causation, at any scale.
-
-Only intervention — reaching out and changing the world to see what happens — climbs the ladder, and a
-language model cannot intervene. This is what humans and their instruments supply, and the contribution
-is not "truth" but **constraint**: reality's unique gift is that it can say **no**. Text offers only
-more opinion; an experiment delivers a refusal no consensus can overturn. As §§6–7 argued, that refusal
-does double duty — it is both the anchor that prevents collapse and the fitness signal that lets the
-lineage's evolving taste select for truth rather than persuasion.
-
-Two honest riders. First, the human reality-signal is *dirty*: people supply results warped by
-publication bias, incentive, and occasional fraud — which is exactly why the error-correcting
-institutions of §8 must sit at the human–machine boundary, screening the signal before it selects.
-Second, humans are the *current* supplier of intervention, but the actuator half is being automated
-(autonomous laboratories already close the design–build–test loop). What looks durable in the human
-role is therefore not the hands but the **choice of what to test and which refusals matter** — the
-part of the fitness function that encodes *what is worth persisting*, as opposed to what merely *can*
-persist. We flag, without resolving, that a partnership stays mutual only while both sides supply
-something the other cannot.
-
-## 10. Why it is cheap
-
-A practical fact turns this from thought experiment into buildable proposal: **the architecture almost
-never re-pays for the one genuinely expensive thing in AI — pre-training.** (The single exception,
-periodically re-minting the base, is §11, and it is rare enough to be an amortised footnote.)
-
-Training a foundation model from scratch consumes trillions of words and a fortune in compute. This
-design does none of that per generation. Every model is *born* from an existing open-weight model that
-already paid that cost; specialising one is a small patch trained in hours on a single consumer GPU;
-running the society is ordinary inference; and reproducing — recombining parents into a child — is, in
-the model-merging case, cheaper still, because it can be done directly on the weights with no retraining
-at all (Akiba et al., 2024). Selection does cost more — you must run *populations* and discard the
-unfit — but that is a multiplier over an already-cheap unit, not over a foundation-model budget.
-
-The economics work only with **open-weight** models, for reasons practical and legal at once: you must
-be free to inspect, modify, and redistribute the weights, and most proprietary licences forbid using a
-model's outputs to train another — which is exactly what reproduction here does. This is not ideology
-bolted on; it is a structural constraint, and a democratising one, since it puts the whole architecture
-within reach of a single laboratory.
-
-## 11. Can it grow forever? Consolidating knowledge back into the base
-
-One question the design has assumed away: can the lineage accumulate *without end*? The individual is
-bounded, and that is the clock. But the lineage seemed unbounded — each generation simply starts a
-little ahead. Look closer and a second budget also fills.
-
-Every new model is a pristine base plus an inherited **soft** delta — the acquired knowledge carried in
-added patches rather than baked into the frozen core (§3). That soft delta is what makes the lineage
-multigenerational; it is also what cannot grow forever cheaply. Stacked patches are not free: they slow
-inference, and past some depth the accumulated delta is better *consolidated* than carried. The lineage,
-too, matures.
-
-The fix is the same operation, one level up. When a lineage's acquired knowledge has proven stable
-across enough generations, **re-mint the base**: distil the accumulated soft inheritance into the
-*weights* of a fresh foundation-scale model — a new base born already *natively knowing* what took many
-generations to acquire in patches. The soft budget resets; the next epoch begins from a richer floor.
-What was hard-won and *learned* becomes cheap and *innate*.
-
-The pattern **echoes the Baldwin effect** (Baldwin, 1896; its clean computational demonstration is
-Hinton & Nowlan, 1987): knowledge acquired and re-learned every generation eventually becoming part of
-the innate endowment. We use the echo advisedly — Baldwin's mechanism is *selection* favouring
-genotypes that learn the trait ever more easily, whereas re-minting is direct distillation, a
-deliberate engineering shortcut through the same soft-to-innate valve. The valve is the point: two
-substrates, the soft learned patches and the hard base weights every model is born with, with a
-controlled passage between them.
-
-Three honest riders, because re-minting is the most consequential step in the scheme:
-
-- **Cost.** This is the one step that re-pays part of the pre-training bill, breaking §10's cheapness
-  *locally*. It is bearable only because it is *rare*, amortised over many cheap generations, and is
-  continued training from the lineage's own rich outputs rather than a de-novo run.
-- **Irreversibility (of the lineage, not the archive).** A digital system can, of course, keep every
-  old base on disk — nothing forces deletion, and archives should be kept. The irreversibility is
-  *operational*: once the lineage's production base, training mixtures, and selection all run downstream
-  of the re-minted weights, a quiet collapse baked into them propagates to every descendant, and the
-  archived ancestor helps only if some process still compares against it — which nothing in the loop
-  does by default. In our minimal models a collapsed-then-re-minted lineage locks in its loss exactly
-  this way, and a cheap safeguard prevents it: **re-mint only while the lineage is demonstrably diverse
-  and healthy** (and keep an audit that diffs against the archived ancestor), never as a rescue for a
-  line already drifting. It is the sharpest instance of the human seat of §9 — choosing what no future
-  generation will think to question.
-- **Speciation.** A re-minting is a founder event. Different laboratories, re-basing on different
-  criteria, will mint divergent bases; the lineage branches. This is not a defect but *adaptive
-  radiation*, and it is exactly what open weights make possible. The society grows not as one heavy
-  trunk but as a branching tree of bases.
-
-So the honest answer to "can it grow forever?" is: **the architecture removes the *storage* obstacle
-to indefinite accumulation** — nothing is retained without bound anywhere, and consolidation resets
-the soft budget each epoch — but that is a statement about bookkeeping, not a demonstration of
-unbounded capability growth, which no fixed-capacity system can promise and our finite models
-(deliberately scoped as "effectively open-ended relative to the sample size, not astronomically
-open-ended") do not test. What the design claims is the weaker, defensible thing: at no level does a
-full store force the lineage to stop learning.
-
-## 12. One process, four timescales
-
-Step back and the parts resolve into a single idea running at four nested speeds. The **vertical**
-motion is transmission — the selective passing-down of hard-won knowledge:
-
-1. **Within one model, over a working life:** experience is consolidated from fast, episodic memory
-   into slow, durable weights, without catastrophic loss.
-2. **Between generations, at maturity:** mature models reproduce — recombined into a fresh one.
-3. **Across many generations:** each generation inherits the compressed achievements of the last and
-   builds on them.
-4. **Across epochs:** a proven lineage's accumulated soft inheritance is consolidated into the weights
-   of a re-minted base, becoming innate.
-
-The first and last are the *same operation at opposite ends of the scale* — a fast/soft store
-consolidating into a slow/hard one — one running overnight inside a single model, the other across an
-epoch inside a whole society. The **horizontal** motion is selection — Darwinian selection acting across
-the population at each timescale, on the policies that govern what gets transmitted, with reality as the
-fitness function and diversity-preservation keeping the specialists alive.
-
-The same three rules govern all of it: **reproduce by recombining, not by copying, or you decay;
-preserve the disagreements and the surprises, or you converge; and anchor fitness to a reality that can
-refute, or you evolve toward what is merely convincing.**
-
-## 13. What we built, what we found, and what is still open
-
-The previous drafts of this paper promised a "companion paper" that *would* make this concrete. That
-work now exists — mostly as a set of **minimal, laptop-reproducible models**, with a first bridge to
-**real language models** (a LoRA-merge prototype, up to 7B on a GPU cluster) — and it is worth stating
-plainly what it does and does not show. (A separate results document gives the numbers; here is the
-shape.)
-
-**What we built and found.**
-
-- *An exact account of collapse.* Because generational training is the Wright–Fisher drift process, we
-  can check a simulator against century-old closed-form formulas, and it matches them to a fraction of
-  a percent. Collapse is not argued by analogy; it is derived.
-- *The cheap-grounding result, and its limit.* A few percent of verified real data holds on to most of
-  a lineage's diversity indefinitely — but not the deepest tail, which needs recombination. This is
-  what makes a continually-learning society economically plausible rather than a data-hungry fantasy.
-- *"Merge, don't average."* Combining several teachers by *averaging* their outputs — the obvious thing,
-  and what a "model soup" does — mathematically cancels the benefit of having several teachers. A
-  *merge* that keeps each item's strongest source realises it. Most current multi-model setups get this
-  wrong by default.
-- *Collapse and its cure in real trained networks, and on real images.* We reproduced the same effects
-  in small recurrent and feed-forward networks and in a generator of handwritten digits (MNIST), where
-  a model trained on its own output collapses to a single blurred digit while a little grounding keeps
-  all the styles alive. An honest wrinkle we had to report: real neural networks *smooth*, so the naive
-  diversity metric misleads, and the right measure is distance-from-truth.
-- *Sex that beats the parents, and when it doesn't.* In evolutionary simulations, recombining
-  complementary specialist models produces a model fitter than any parent (the Fisher–Muller effect),
-  climbing toward the best-possible combination as more, more-diverse parents are added — while
-  averaging and best-single-parent plateau below. On *entangled* problems, blind recombination instead
-  produces below-parent offspring (outbreeding depression) — and *directed* recombination (choose mates,
-  screen offspring, unbounded parents) reliably fixes it. This is the concrete evidence for the paper's
-  central reframing.
-- *The mating system, not just the mating.* Sweeping how *widely* models recombine — from monogamous
-  (local, structured) to promiscuous (panmictic) — against landscape ruggedness, the best breadth
-  **shrinks as skills get more entangled**: wide, promiscuous merging wins on additive landscapes, but on
-  rugged ones it prematurely converges to a worse champion and an intermediate, structured breadth wins,
-  because promiscuity monotonically destroys the diversity a rugged search needs. A merging-native design
-  axis — *merge widely for additive skills, keep island-structured sub-populations for entangled ones* —
-  that the model-merging literature, which assumes panmixia, does not have.
-- *The recombination claims, in real language models — with a sharp condition.* Merging LoRA-specialised
-  Qwen models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent
-  (Fisher–Muller, for real); and keeping parents intact and *routing*, or *breeding and screening*
-  offspring, beats the naive average — but *only when the task leaves headroom*. On easy tasks a strong
-  model's plain average is already at the ceiling and the refinements add nothing; on hard tasks the
-  average dilutes a specialist below even the best single parent, and the union-preserving operators win
-  clearly. The practical rule, stated qualitatively: these tricks pay off where the naive average falls
-  short of attainable performance, and add nothing where it does not (a quantitative form is untested).
-  This is a prototype (three task families, one seed), so we read it as
-  signs, not magnitudes; the *whole grounded society* on a language model remains the open step.
-- *The whole society, and why every part is needed.* In a population evolving on a "reality" landscape,
-  the full system — grounding + sexual recombination + preserved diversity — climbs to the top while
-  keeping its specialists. Remove *grounding* and it collapses into a confident, wrong consensus (a
-  direct analogue of training on the internet's growing crowd of AI-generated text); remove *sex* and it
-  gets stuck; remove *diversity* and it converges too fast to a worse answer. Each removal fails
-  differently; only the whole system climbs. This is the closest thing we have to a test of the actual
-  thesis, rather than of the borrowed scaffolding around it.
-
-### The claims at a glance: status, assumptions, evidence, limits
-
-Because a perspective of this breadth risks blurring what is proved, what is measured, and what is
-proposed, here is the ledger of the load-bearing claims — each labelled **exact** (closed-form in the
-minimal model), **empirical** (measured in trained systems), or **hypothesis** (stated with a
-falsifier, not yet established):
-
-| Claim | Status | Key assumptions | Evidence | Known limits |
-|---|---|---|---|---|
-| Collapse = Wright–Fisher drift (minimal model) | Exact (diagnosis conceded to prior work) | Knowledge = categorical distribution; refit = resample | Closed forms reproduced to <0.5% | Real learners add a signed, architecture-specific estimator bias (measured) |
-| Grounding = immigration; critical real-data fraction ≪ 1 | Exact + empirical sign | Fresh samples from a fixed, non-drifting truth | Exact `H_eq`; `g*≈0.048`; sign holds in RNN/MLP/VAE and on MNIST | Deepest tail unrescuable at feasible budgets (`m ∼ 1/p`); sharp threshold softens in trained nets |
-| "Merge, don't average" conservation | Exact **for the output-mean operator** | Rare-item regime; an oracle/verifier identifies the strongest source | E4 closed form + simulation; neural reproduction | Weight-averaging and routing are empirical cousins, not instances; budgets differ; bridge = the headroom rule |
-| Offspring exceed every parent (Fisher–Muller) | Interpretation + empirical | Complementary (decorrelated) parents; verifiable fitness | E8 analytic; 7B LoRA merge beats every specialist on every family | LLM tier: 3 lexically-distinct families; multi-seed replication in progress |
-| Outbreeding depression on rugged landscapes; operator design rule | Exact-model result; hypothesis at LLM scale | NK epistasis stands in for skill entanglement | E9–E10; directed selection rescues | Not yet mapped onto a real task-entanglement measure |
-| Optimal mate-pool breadth shrinks with ruggedness | Exact-model result; hypothesis for merging populations | Ring population, local selection | E14 | Phenomenon known to island-model evolutionary computation; our contribution is the mapping and the diversity/mean decomposition |
-| Merge failure decomposes into coordinate artefact + functional residual | Empirical (MLP tier; LLM tier in progress) | Alignment enumerates the architecture's unit symmetries | Full-symmetry residual ≈ 0 (compatible) vs ≈ naive (conflict); cliff in hybrid fitness | Scoped to aligned linear interpolation; conflict floor is information-theoretic, not genetic |
-| Epistasis (not divergence) sets the cliff; snowball onset | Exact-model result; **hypothesis** at the neural tier | BDM incompatibility structure | E12 | Snowball count ≠ performance cliff without the effect-size link; neural test outstanding |
-| Pre-merge functional disagreement predicts merge penalty | Empirical, within a controlled grid (0.5B, 13 conditions × 3 seeds) | Constructed conflict/overlap/duration axes; oracle-potential outcome (pre-registered; ordering sensitive to reference) | Clustered CIs exclude 0; held-out LOCO ρ≈0.4; selected geometry baselines ≈ 0 | Head-to-head predictor differences not individually significant; only selected baselines; generalisation to real task pairs open |
-| Confidence weighting improves rank prediction over raw disagreement | **Not supported** (pre-registered internal prediction) | — | Paired Δ\|ρ\| ≈ −0.02, CI [−0.13, +0.06] | Weighting does double the conflict-vs-compat level contrast |
-| The predictor improves budget-matched operator choice | **Open** | — | Soup-vs-route gap readout noise-dominated at 0.5B | The practical payoff; untested |
-| Emergent speciation without conflict | **Not observed** (pre-registered) | Shared ancestry, compatible tasks, tested divergences | E13b: residual 0.000; merge rescues specialists | Bounds the hypothesis; longer horizons/distribution shift/capacity pressure untested |
-| Grounding + sex + diversity complementary (each ablation fails distinctly) | Analytic-model result; hypothesis at LLM scale | Conformity stands in for self-consumption; general joint necessity not established | E11 four-arm ablation | The full grounded LLM society is unbuilt; alternative schemes untested |
-
-**What is borrowed, and what is ours.** We are deliberate about the ledger, because the surrounding
-literature is crowded and a reader deserves to know exactly where the line falls. **Conceded as prior
-art:** (a) *model collapse is genetic drift* — derived independently and cleanly (Riis, 2026; the
-Wright–Fisher collapse literature following Shumailov et al., 2024; the closed-form first-extinction
-law of Benati et al., 2025; the quantitative-trait account of Yoon et al., 2025); (b) the empirical
-facts that a merged model can *beat its parents*, that *decorrelated* parents merge better, and that
-*naive averaging is inferior* to sign-reconciled or routed merges (model soups, TIES, DARE,
-mixture-of-experts routing); (c) that a *population* of merging or self-improving models can climb
-(GENOME, M2N2, Multiagent Finetuning, the Darwin–Gödel Machine); (d) that merge success has
-machine-learning-native *predictors* — interpretable pairwise metrics (Zhou et al., 2026),
-capacity/rate-distortion accounts of merging collapse (Cao et al., 2026), and stability/scaling
-analyses of multi-task degradation; and (e) that verifier-screened synthetic data can avert collapse
-(Yi et al., 2025) — the statistical cousin of our grounding operator. We claim none of these.
-
-**Ours** is the framework those results invite: a **population-genetics of sex** applied to model
-societies, generative where the incumbents are empirical. Concretely — the **"merge, don't
-average" conservation law** (recombination preserves the union; blending inheritance cancels it),
-derived not observed; **Fisher–Muller** named and used to explain *why* offspring exceed parents;
-**outbreeding depression on rugged/epistatic landscapes**, which turns "when does merging help vs hurt"
-from a thing you must run a search to discover into a thing the landscape's ruggedness *predicts*, with
-the operator-choice design rule that follows (average / union-route / directed-select); **grounding as
-migration–drift balance**, giving a critical real-data fraction and a phase boundary a closed
-self-consuming loop cannot have; **directed sex** as the distinctly-AI advantage (unbounded parents,
-offspring preview, mate choice); and the **integrated society** whose operators make
-*complementary, distinctly-failing contributions* in the tested model (general joint necessity is not
-established). The value-add over the machine-learning-native merge theory is that ours predicts *which
-operator to use and when it will backfire*, not merely how fast quality decays. And it opens — and
-begins to occupy — a question nobody has framed: **model speciation**, the population-genetics of
-*reproductive isolation* (Bateson–Dobzhansky–Muller incompatibilities) as the account of *when two
-models are too diverged to be merged at all*. We model it explicitly (§5), predicting the
-compatible → outbreeding-depression → inviability curve, its super-linear (snowball) onset, and its
-control by epistasis rather than divergence alone — the one place the merge literature has phenomena
-(Pari et al., 2024; Zhou et al., 2026) but no theory — and we confirm it in real trained weights, where
-a merge barrier survives alignment under the *full* function-preserving symmetry group (not only
-Re-Basin permutations) as a residual, functional reproductive isolation with an information-theoretic
-floor — together with the pre-registered emergent converse: absent conflicting training signals,
-divergently-specialised lineages of shared ancestry showed *no* isolation at any divergence tested, the
-merge instead rescuing the forgetting specialists (isolation must be provoked; specialisation alone did
-not speciate). In one sentence: the field agrees on the disease
-and tinkers at the cure with evolutionary metaphors; we bring the evolutionary *theory*, and it makes
-falsifiable predictions — a merge-compatibility cliff among them — that the metaphors do not.
-
-**What is still open — honestly.** The old hole (what to select) we fill in kind: don't design the
-selector, evolve it. But the hole has *moved*, not closed, and the new one is harder: **the fitness
-function** — what reality-anchored measure selects for *truth* without also selecting for *persuasion*,
-given that in our own species the two have been at war for the whole history of ideas. Alongside it:
-the **institutions** that let contemporaries correct one another before error is inherited (§8), which
-we do not solve; and the **calibration** of everything the results left as knobs — how many parents,
-how complementary, at what ratio of inherited-to-real data, and how healthy a lineage must be before
-its knowledge is safe to make irreversibly innate. These are, at least, *measurable* — which is the
-difference between an open problem and a hole. And the largest gap of all: the *recombination* claims
-now hold in real language models, but the *society* — the grounded, diversity-preserving, continually
-reproducing loop — does not yet. The real test is to build that whole system out of actual open-weight
-language models, and see whether all the signs survive contact with a system too big to write down.
-The operators, checked; the living society, next.
-
----
-
-## Selected references
-
-- Akiba, T., Shing, M., Tang, Y., Sun, Q., & Ha, D. (2024). Evolutionary optimization of model merging recipes. *Nature Machine Intelligence.* (See also Sakana AI's M2N2, "Model Merging of Natural Niches.")
-- Baldwin, J. M. (1896). A new factor in evolution. *The American Naturalist.*
-- Boyd, R., & Richerson, P. J. (1985). *Culture and the Evolutionary Process.*
-- Campbell, D. T. (1974). Evolutionary epistemology. In *The Philosophy of Karl Popper.*
-- Fisher, R. A. (1930). *The Genetical Theory of Natural Selection.*
-- French, R. M. (1999). Catastrophic forgetting in connectionist networks. *Trends in Cognitive Sciences.*
-- Hinton, G. E., & Nowlan, S. J. (1987). How learning can guide evolution. *Complex Systems.*
-- Hinton, G., Vinyals, O., & Dean, J. (2015). Distilling the knowledge in a neural network. *arXiv:1503.02531.*
-- Hu, E. J., et al. (2021). LoRA: low-rank adaptation of large language models. *arXiv:2106.09685.*
-- Hull, D. L. (1988). *Science as a Process.*
-- Kauffman, S. A., & Levin, S. (1987). Towards a general theory of adaptive walks on rugged landscapes. *Journal of Theoretical Biology.* (The NK model.)
-- Lehman, J., & Stanley, K. O. (2011). Abandoning objectives: evolution through the search for novelty alone. *Evolutionary Computation.*
-- Mallya, A., & Lazebnik, S. (2018). PackNet: adding multiple tasks to a single network by iterative pruning. *CVPR.*
-- McClelland, J. L., McNaughton, B. L., & O'Reilly, R. C. (1995). Why there are complementary learning systems in the hippocampus and neocortex. *Psychological Review.*
-- McCloskey, M., & Cohen, N. J. (1989). Catastrophic interference in connectionist networks. *Psychology of Learning and Motivation.*
-- Minsky, M. (1986). *The Society of Mind.*
-- Mouret, J.-B., & Clune, J. (2015). Illuminating search spaces by mapping elites (MAP-Elites). *arXiv:1504.04909.*
-- Muller, H. J. (1932). Some genetic aspects of sex. *The American Naturalist.* (The advantage of recombination.)
-- Muller, H. J. (1964). The relation of recombination to mutational advance. *Mutation Research.* (Muller's ratchet.)
-- Pearl, J. (2009). *Causality: Models, Reasoning, and Inference* (2nd ed.).
-- Popper, K. (1959). *The Logic of Scientific Discovery.*
-- Riis, S. (2026). Drift and selection in LLM text ecosystems. *arXiv:2604.08554.*
-- Rusu, A. A., et al. (2016). Progressive neural networks. *arXiv:1606.04671.*
-- Shumailov, I., et al. (2024). AI models collapse when trained on recursively generated data. *Nature.*
-- Wortsman, M., et al. (2022). Model soups: averaging weights of multiple fine-tuned models. *arXiv:2203.05482.*
-- Wright, S. (1931). Evolution in Mendelian populations. *Genetics.*
-
-*The evolution of sex (the geneticist's canon this paper draws on):*
-
-- Barton, N. H., & Charlesworth, B. (1998). Why sex and recombination? *Science.*
-- Otto, S. P., & Lenormand, T. (2002). Resolving the paradox of sex and recombination. *Nature Reviews Genetics.*
-- Kondrashov, A. S. (1993). Classification of hypotheses on the advantage of amphimixis. *Journal of Heredity.*
-- Dobzhansky, T. (1936); Muller, H. J. (1942). Bateson–Dobzhansky–Muller incompatibilities (reproductive isolation).
-- Livnat, A., & Papadimitriou, C. (2016). Sex as an algorithm: the theory of evolution under the lens of computation. *Communications of the ACM 59(11).* (The theory-of-computation precursor: recombination selects for mixability.)
-
-*The 2025–2026 landscape this paper positions against:*
-
-- Subramaniam, V., Du, Y., Tenenbaum, J. B., Torralba, A., Li, S., & Mordatch, I. (2025). Multiagent finetuning: self-improvement with diverse reasoning chains. *arXiv:2501.05707.*
-- Zhang, J., Hu, S., Lu, C., Lange, R., & Clune, J. (2025). Darwin Gödel Machine: open-ended evolution of self-improving agents. *arXiv:2505.22954.*
-- *Nature-inspired population-based evolution of large language models* (GENOME/GENOME+). (2025). *arXiv:2503.01155.*
-- Sakana AI (2025). Competition and attraction improve model fusion (M2N2). *arXiv:2508.16204* (GECCO '25).
-- Yadav, P., Tam, D., Choshen, L., Raffel, C., & Bansal, M. (2023). TIES-Merging: resolving interference when merging models. *NeurIPS / arXiv:2306.01708.*
-- Yu, L., Yu, B., Yu, H., Huang, F., & Li, Y. (2023). Language models are super Mario: absorbing abilities from homologous models (DARE). *arXiv:2311.03099.*
-- Gerstgrasser, M., et al. (2024). Is model collapse inevitable? Breaking the curse of recursion by accumulating real and synthetic data. *arXiv:2404.01413.*
-- Guo, D., Wu, J., & Yiu, S. M. (2026). Model collapse as cultural evolution. *arXiv:2605.23054.*
-- Benati, M., Londei, A., Lanzieri, D., & Loreto, V. (2025). First-extinction law for resampling processes. *arXiv:2509.20101.* (Collapse onset = the Wright–Fisher first-extinction time.)
-- Yoon, Y., Hu, D., Weissburg, I., Qin, Y., & Jeong, H. (2025). Model collapse in the self-consuming chain of diffusion finetuning: a novel perspective from quantitative trait modeling. *ICLR 2025 / arXiv:2407.17493.*
-- Yi, B., Liu, Q., Cheng, Y., & Xu, H. (2025). Escaping model collapse via synthetic data verification. *arXiv:2510.16657.*
-- Ainsworth, S., Hayase, J., & Srinivasa, S. (2022). Git Re-Basin: merging models modulo permutation symmetries. *arXiv:2209.04836.*
-- Li, T., & Shen, Z. (2026). Scaling linear mode connectivity and merging to billion-parameter pretrained transformers. *arXiv:2606.23607.* (Symmetry groups richer than permutations remove more of the barrier.)
-- Sharma, E., Roy, D. M., & Dziugaite, G. K. (2024). The non-local model merging problem: permutation symmetries and variance collapse. *arXiv:2410.12766.*
-- Pari, J., Jelassi, S., & Agrawal, P. (2024). Collective model intelligence requires compatible specialization. *arXiv:2411.02207.*
-- Zhou, L., Zhao, B., Yu, R., & Rodolà, E. (2026). Demystifying mergeability: interpretable properties to predict model merging success. *arXiv:2601.22285.*
-- Cao, Y., Ran, D., Guo, Y., Wu, M., Chen, S., et al. (2026). An empirical study and theoretical explanation on task-level model-merging collapse. *arXiv:2603.09463.*
-- Hu, Y., Yao, Y., Zhang, N., Chen, H., & Deng, S. (2024). Exploring model kinship for merging large language models. *arXiv:2410.12613.*
-- Kozodoi, N., Afolabi, Z., & Butler, J. (2026). Are we merging the right models? Impact of expert training duration on model merging for LLMs. *arXiv:2607.11997.*
-- Harris, K. D. (2026). A mathematical theory of evolution for self-designing AIs. *arXiv:2604.05142.*
-- Chen, N., Tong, Y., Yang, Y., He, Y., Zhang, X., et al. (2026). Diversity collapse in multi-agent LLM systems: structural coupling and collective failure in open-ended idea generation. *arXiv:2604.18005.*
-- Tanaka, H. (2026). When is collective intelligence a lottery? Multi-agent scaling laws for memetic drift in LLMs. *arXiv:2603.24676.*
-
-*Still to engage in a full version: tacit knowledge (Polanyi) and human capital (Becker).*
diff --git a/paper/the-lamarckian-society-v5.md b/paper/the-lamarckian-society-v5.md
deleted file mode 100644
index 2a75277..0000000
--- a/paper/the-lamarckian-society-v5.md
+++ /dev/null
@@ -1,570 +0,0 @@
-# The Lamarckian Society
-
-### AI that reproduces sexually: how a society of models can keep learning across generations instead of collapsing
-
-*A perspective. Draft 5 — the companion to a set of minimal working models (now built).*
-
----
-
-### A note on vocabulary (please read this first)
-
-This paper sits at the meeting point of three fields, and it is written so that a reader from any
-one of them can follow all of it. We therefore **spell out** each field's jargon the first time it
-appears, even at the risk of belabouring the obvious for the specialist. A short glossary, in case
-you skipped a definition:
-
-- **Model collapse** *(machine learning)* — the degeneration that happens when you train a model on
-  data produced by earlier models, over and over: rare cases disappear and the model drifts toward a
-  bland average.
-- **Distillation** *(machine learning)* — training a fresh "student" model on the outputs of one or
-  more "teacher" models, so the student ends up knowing a compressed version of what they knew.
-- **Model merging** *(machine learning)* — combining several trained models directly, at the level
-  of their weights, into one — no retraining. (Think of it as breeding two models rather than
-  teaching a third.)
-- **Genetic drift** *(population genetics)* — the random loss of rare variants that happens in any
-  finite population simply because not everyone leaves offspring. It is the neutral, no-selection
-  baseline of evolution.
-- **Wright–Fisher process** *(population genetics)* — the standard mathematical model of drift. We
-  will claim, and show, that generational model-training *is* this process, not merely like it.
-- **Recombination / sexual reproduction** *(biology)* — making an offspring by combining pieces from
-  more than one parent, rather than copying a single parent (which is *asexual* reproduction).
-- **Muller's ratchet** *(population genetics)* — the way an asexual lineage, one that never
-  recombines, accumulates damage it can never undo. It is, we will argue, the same thing as model
-  collapse.
-- **Catastrophic forgetting** *(machine learning / neuroscience)* — a neural network overwriting what
-  it knew when it learns something new.
-
-We have tried to keep the big picture legible on every page, and to be candid about what is argument
-and what is evidence. The evidence is mostly from **deliberately small models** — mathematics, small
-neural networks, image generators, and evolutionary simulations. A first bridge to real language
-models exists — a prototype that recombines LoRA-specialised Qwen models up to 7B on a GPU cluster,
-which confirms the recombination signs (below) — but the *full grounded society* has not yet been
-built on a large language model. We will say so repeatedly, because the gap matters.
-
----
-
-## Abstract
-
-We want AI that keeps learning across generations — the way a research field or a culture does,
-each generation standing on the compressed knowledge of the last — rather than a single model trained
-once and frozen. The obstacle is well known to machine-learning engineers as **model collapse**:
-train each generation on the previous one's output and quality degrades, the rare cases vanishing
-first. The central observation of this paper is that this failure is *reproduction gone wrong*, and
-that biology already knows the fix.
-
-Copying one model into the next — a "teacher" distilled into a "pupil" — is **asexual reproduction**.
-Asexual lineages, in nature, decay: they accumulate errors they cannot undo (a process geneticists
-call **Muller's ratchet**), and this decay is, mechanically, model collapse. The remedy nature found,
-hundreds of millions of years ago, is **sex**: build each new individual by *recombining* several
-parents, so it inherits a combination none of them had — and can be **fitter than any of its
-parents**. We argue that a society of AI models should reproduce the same way: each new model
-**recombined from many complementary "parent" models** (something the field already does, under the
-name *model merging*), its selection anchored to **reality** (so it is judged against the world, not
-against the consensus of other models), and its diversity actively preserved. With those three
-ingredients — recombination, grounding in reality, and preserved diversity — a lineage does not merely
-avoid collapse; it **climbs**, producing models better than any single ancestor while each specialty
-is re-learned and surpassed.
-
-AI has one advantage biology lacks: its "sex" has **no two-parent limit**, its mates can be **chosen**
-for complementarity, and its offspring can be **screened before they are kept**. We call this
-*directed sex*, and it turns recombination from a gamble into a reliable engine.
-
-We support the argument with a set of **minimal models**: a mathematically exact account of collapse
-and its cure; reproductions of the same effects in small trained neural networks and in a generator of
-handwritten digits; and evolutionary simulations in which a population of models climbs a "fitness
-landscape" that stands in for reality. In these, every claim above either holds or fails visibly, and
-removing any single ingredient breaks the system in a distinct way. A first **language-model
-prototype** then confirms the recombination claims in real weights — merging LoRA-specialised Qwen
-models (up to 7B on a GPU cluster) produces a generalist that exceeds every parent, and shows the
-sharp condition under which the "merge, don't average" refinement matters (below). The scope is
-honest: these are existence proofs and design rules, and the eventual test is to build the *whole
-grounded society* out of real language models. We also note where our diagnosis is no longer novel — the reading of
-collapse as genetic drift has since been derived independently — and locate our contribution in the
-**cure** rather than the diagnosis.
-
----
-
-## 1. From a society in space to a society in time
-
-The idea of many AI agents working together — a "society of mind" (Minsky, 1986), or today's
-multi-agent systems — arranges intelligence across *space*: several specialists side by side,
-dividing a task. This paper is about a different axis: *time*. Not a society that merely exists at
-one moment, but one that **persists and renews across generations**, each new cohort of models
-starting from the compressed knowledge of the last.
-
-The unit that matters is therefore the **generation**, and the event that matters is **reproduction**:
-the making of a new model from older ones. A single model, like a single mind, is bounded and
-eventually stops improving. A *lineage* need not be. Human civilisation is not clever because any one
-person is; it is clever because each generation inherits the distilled achievements of the previous
-one and adds a little. We propose building AI the same way — and, crucially, getting the *reproduction*
-right, because that is exactly where it can go wrong.
-
-## 2. Why today's models cannot do this
-
-Today's large language models have no life cycle. They are trained once, at enormous cost, then
-**frozen** and deployed as a fixed artefact that does not learn from the people it serves. Learning
-and doing are split into two eras with no bridge between them.
-
-There is a real reason for the freeze. Updating a neural network on new information tends to overwrite
-what it already knew — **catastrophic forgetting**, a problem understood since the late 1980s
-(McCloskey & Cohen, 1989; French, 1999). Freezing avoids it by refusing to learn at all. The result
-is a mind with no childhood, no growth, and no way to pass anything on. A lineage needs the opposite:
-members that learn through their working lives, reach maturity, and hand on what they gained. So the
-first requirement is a learner that can grow *safely*.
-
-## 3. A learner that can grow without forgetting
-
-The individual model needs two properties.
-
-**It must not catastrophically forget.** Instead of overwriting its core as it learns, it keeps that
-core frozen and only *readable*, and carves each new skill into freshly-added capacity beside it. In
-machine learning this is called *parameter isolation* (progressive networks — Rusu et al., 2016;
-prune-and-freeze — Mallya & Lazebnik, 2018; and, most practically, **LoRA** and other small trainable
-"patches" bolted onto a frozen model — Hu et al., 2021). If the core is never altered, forgetting it
-is not merely unlikely but structurally impossible. This is what lets a model accumulate a coherent
-working life of expertise — the kind of stable knowledge worth passing on.
-
-The brain offers a partial blueprint. *Complementary Learning Systems* theory (McClelland,
-McNaughton & O'Reilly, 1995) — itself a response to the forgetting problem — describes two subsystems:
-a **fast** store (the hippocampus) that grabs an experience in one shot, and a **slow** store (the
-neocortex) that integrates regularities gradually without disruption. We do not lean on any particular
-account of how the brain moves knowledge between them; the architecture needs only that *some*
-periodic **offline consolidation** step exists, moving knowledge from the fast store to the slow one
-when the system is idle. The machine version is clean regardless: the prompt is working memory, an
-external database is the fast episodic store, the trained weights are the slow store, and consolidation
-migrates the first into the last.
-
-**It is bounded.** Because the model only ever *adds* capacity and freezes what it has, it eventually
-fills up. In most designs that is a wall to dread. In ours it is a clock.
-
-## 4. "Full" is maturity, not failure
-
-Here is the pivot. A bounded learner that fills up has not broken. **It has grown up.**
-
-Read the capacity limit as a life stage. A model is *born* as a freshly-schooled base — its general
-education. It enters a **working life**, adding specialised knowledge as it does its job. And it
-reaches **maturity**: the point where it has learned much of what one working life in its niche can
-teach. Maturity is not the end of usefulness — it is the moment the model is most worth learning
-*from*. So maturity is the cue to **reproduce**. The capacity ceiling that every other architecture
-fights becomes, in ours, the metronome of the generations.
-
-Everything now turns on how that reproduction is done — and this is where the paper's central claim
-lives.
-
-## 5. Reproduction: copying collapses, recombination climbs
-
-Suppose a mature model simply teaches a fresh one — distillation, one teacher to one pupil, generation
-after generation. This is the obvious design, and it fails, for a reason that is exactly the same in
-machine learning and in biology.
-
-**The machine-learning statement.** Training each generation on the previous generation's outputs is
-the recipe for **model collapse**: the model forgets the improbable, loses the *tail* of the
-distribution (the rare cases) first, and drifts toward its own most common output (Shumailov et al.,
-2024). Worse for us, the very rule that makes distillation useful — *keep the general, drop the
-idiosyncratic* — **is** tail-deletion by design. The operation that would power a cultural ratchet and
-the operation that drives model collapse are the same act.
-
-**The population-genetics statement (the same thing).** Represent a model's knowledge as a
-distribution over discrete "items" — capabilities, facts, modes of behaviour. One generation is:
-*draw a finite sample from the parent, and refit the child to it.* That finite-sampling step is
-**mathematically identical** to **genetic drift** — the random loss of rare variants in a finite
-population — described by the century-old **Wright–Fisher** model (Wright, 1931; Fisher, 1930). This is
-not an analogy we find pretty; it is the same equations, and we use them as an exact check on our
-simulations (the first of the minimal models below). Rare items go extinct first, roughly ten times
-faster than common ones, precisely as drift predicts.
-
-And single-teacher copying is **asexual reproduction** — cloning one parent. Nature already knows what
-happens to an asexual lineage that never recombines: it accumulates damage it can never repair, a
-one-way decline geneticists call **Muller's ratchet** (Muller, 1964). *Muller's ratchet is model
-collapse.* Naming it that way is not decoration; it tells us where the cure is, because biology solved
-this problem.
-
-Two ingredients turn the collapse operation into a climb. Both are things nature does.
-
-**First: do not reproduce "dry."** Model collapse is a property of a lineage fed *only* its own
-output; the documented fix is that keeping some real data in the mixture arrests it (Shumailov et al.,
-2024). We call that real data **grounding** — fresh contact with the world, verified against it. In
-our minimal models, grounding is startlingly cheap: mixing in even a few percent of verified real data
-holds on to most of the diversity indefinitely. But — an honest limit we found and did not expect —
-grounding cannot save the *very rarest* items at any affordable budget; protecting an item of rarity
-*p* needs a real-data budget that grows like 1/*p*. Grounding rescues diversity cheaply; it does not,
-by itself, rescue the deep tail. Something else must. That something is sex.
-
-**Second: reproduce sexually.** Instead of copying one parent, build each new model by **recombining
-several** — a *sexual* rather than asexual birth. In machine learning this already has a name and a
-working implementation: **model merging** (Akiba et al., 2024). Its importance here is not efficiency;
-it is that recombination does something copying cannot. If several parent models have each specialised
-on different parts of reality, each has kept alive rare knowledge the others lost. A recombined child
-inherits the **union** of what its parents kept — not the tail-thinned *average* of a crowd of
-near-identical copies. And here is the point that lifts sex from a safeguard to the engine of the whole
-scheme, and the reason biology invented it:
-
-> **An offspring recombined from complementary parents can be *fitter than any of its parents*.**
-
-Geneticists call this the **Fisher–Muller effect** (Fisher, 1930; Muller, 1932): recombination brings
-together, in one individual, beneficial variants that arose separately in different lineages, so the
-child holds a combination none of the parents had. In our simulations this is exactly what we see —
-recombining decorrelated specialist models yields a model that climbs toward the best-possible
-combination, a genotype *no single parent possessed*, while the best single parent, and the naive
-average of all of them (what the field calls a "model soup" — Wortsman et al., 2022), both plateau
-well below. This is the concrete meaning of the paper's title claim, "the lineage climbs in general
-knowledge; specialisation is re-earned each generation," and it is why the reframing from
-teacher→pupil to *sexual reproduction* is not cosmetic: **copying can only recover a ceiling;
-recombination can exceed it.**
-
-This is no longer only a simulation. In a first language-model prototype — LoRA specialists on
-disjoint task families, recombined and judged by an exact verifier — a merge of three specialist Qwen
-models (7B, on a GPU cluster) **beats every single specialist**, overall and on every family: the
-Fisher–Muller effect, in real weights. The same prototype pins down *when* the finer "inherit the
-union, don't average" rule actually bites. Keeping each parent whole and **routing** each input to the
-right one beats the tail-thinning average — but only when the task is hard enough to leave room to
-lose: on easy tasks a strong model's plain average is already at the ceiling, so the crude soup is
-fine, whereas on hard tasks the average dilutes a hard-won specialist so badly it falls below even the
-best single parent, and routing wins by a wide margin. The rule is therefore precise: **the union
-beats the average in exact proportion to how far the average is from the best attainable** — a caveat
-that sharpens rather than weakens the claim, and that a practitioner needs before spending compute on
-the fancier operator.
-
-Two caveats keep this honest, and both are results, not hand-waving.
-
-*Sex can backfire.* When the parents' skills are not cleanly separable but **entangled** — when the
-value of one capability depends on which others are present (geneticists call this **epistasis**) —
-blindly recombining two good models can produce a *worse* child, because recombination breaks up a
-combination that only worked as a whole. Biologists call this **outbreeding depression**, and we
-reproduce it: on "rugged" (highly entangled) problems, naive merging drops offspring below their
-parents, and the more you mix the worse it gets. The design rule that falls out is simple: *merge
-freely when skills are complementary; merge sparingly, and carefully, when they are entangled.*
-
-*AI can do sex better than biology can.* Biology is stuck with two parents, mating roughly at random,
-and cannot inspect an offspring before it is born. An AI has none of those limits. It can recombine
-**many** parents at once; it can **choose** which parents to combine, for complementarity; and it can
-**generate many candidate offspring and keep only the fittest**, screening them against reality before
-committing. We call this **directed sex**, and in our simulations it converts the outbreeding-depression
-catastrophe into a reliable gain: where blind recombination collapses on entangled problems, directed
-recombination matches or beats the best parent every time. The language-model prototype shows the same
-sign where it can: breeding many recombined Qwen offspring and keeping the one the verifier scores
-highest beats the single averaged soup on hard tasks (and, unsurprisingly, does nothing extra on easy
-tasks the soup already solves). This is a genuine advantage of engineered reproduction over the
-biological kind, and we think it is one of the more useful ideas in the paper.
-
-So the picture of §5 is: single-teacher copying is asexual and collapses (Muller's ratchet = model
-collapse); the cure is to *ground* every birth in reality and to reproduce *sexually*, recombining
-many complementary parents; and because AI sex can be many-parent, mate-chosen, and offspring-screened,
-it is not merely a hedge against collapse but an engine that produces children fitter than any parent.
-
-One question remains, and the rest of the paper is largely about it: recombination combines what the
-parents kept — but *who decides what each parent keeps, and which offspring are worth keeping?*
-
-## 6. The second inheritance: letting "what is worth keeping" evolve
-
-There are two answers, and the first is wrong. We could try to *design* the rule for what knowledge to
-keep and pass on. But nobody knows that rule. "Keep the general, drop the particular" is a slogan, not
-an algorithm: ask *which* generalisations, in *which* domain, at *which* grain, and the hand-written
-rule falls apart. This is the deepest hole in the scheme, and it cannot be filled by decree.
-
-The second answer is the one nature used: **do not design the selector — evolve it.** Let different
-models carry different *policies* for what is worth keeping and combining. Let the policies that
-produce more capable offspring spread; let the policies that produce weak offspring die out with their
-lineages. The lineage's *taste* — its sense of what matters — is discovered by selection, not imposed.
-
-So **two things are inherited, on two channels.** The *content* passes down directly: an offspring
-receives its parents' knowledge (this is the "Lamarckian" channel — the inheritance of things acquired
-during a lifetime, which biology forbids for genes but culture allows for ideas). The *selection
-policy* — what to keep, whom to breed with, which offspring to screen for — is itself inherited, varies
-between models, and survives in proportion to the success it produces. That second channel is
-**Darwinian**. The architecture is therefore both at once: Lamarckian in *what* it transmits, Darwinian
-in *what it keeps*. Evolutionary theorists call this structure *dual inheritance* and identify it as
-the engine of human culture (Boyd & Richerson, 1985); philosophers of science describe scientific
-knowledge itself as growing this way, by conjecture and **refutation** (Popper, 1959; Campbell, 1974;
-Hull, 1988).
-
-The closure that makes this fit together, rather than merely sound nice: Darwinian selection needs a
-*selection pressure* — something that decides which policies win. That pressure is already in the
-design. What tells a lineage its taste was good? The success of its offspring **against reality**. The
-reality-check that stops collapse (grounding, §5) and the fitness signal that drives the evolving taste
-turn out to be the *same thing*, seen from two sides.
-
-## 7. The central danger: fitness is not truth
-
-Introducing selection introduces selection's classic hazard, and it is severe enough to sink the whole
-scheme if ignored. Evolution optimises, without mercy or foresight, for exactly what you *measure* —
-never for what you *meant*. (Economists and ML engineers know this as **Goodhart's law** and
-*specification gaming*.) Get the fitness measure slightly wrong and the lineage will exploit the gap
-with more ingenuity than any designed rule.
-
-For a *knowledge* lineage there is a specific and nasty version. For ideas, the natural measure of
-"fitness" is **how well they spread**, and a false-but-persuasive idea spreads beautifully. Human
-intellectual culture is full of highly transmissible falsehoods; confident nonsense out-competes hedged
-accuracy in almost every human forum. Turn Darwinian selection loose on models without care and it will
-breed a lineage optimised for *persuasiveness* — fluent, compelling, and wrong. That is model collapse
-with an optimiser behind it, actively seeking the cliff.
-
-Only one thing makes fitness track truth rather than appeal: **being judged against a reality that can
-say no.** Fitness must be predictive success under *intervention* — did the model's knowledge correctly
-anticipate what the world would do when acted upon — and not approval, fluency, or a benchmark score,
-each of which can be gamed. This is why the reality-check is load-bearing twice over: it is both the
-anchor that stops passive collapse *and* the only thing that keeps the evolving taste honest.
-
-The second danger is **convergence**, and beating it takes work at two separate levels, because
-selection can only preserve variety that already exists — the variety must first be *supplied* and then
-*kept*.
-
-- **Supply.** A lineage that learns only from an accredited elite has a monoculture for a source: the
-  "best" experts are, almost by definition, the ones who won the consensus, so the incoming variation
-  is narrow from the start. The society must therefore learn, deliberately and from the beginning, from
-  the **outliers and the heterodox** as well as the credentialed — not out of fairness, but because in
-  evolutionary terms diverse founders are the raw material without which nothing downstream can adapt.
-- **Preserve.** Even given varied input, plain fitness-*maximising* selection converges — it drives
-  every lineage toward the single current best and fixes it, extinguishing the rare specialists. The
-  fix is well established: **quality-diversity** selection, which rewards being *good* and being
-  *different* at once (novelty search and MAP-Elites — Lehman & Stanley, 2011; Mouret & Clune, 2015),
-  keeping complementary specialists alive rather than collapsing onto the champion. In our simulations
-  this is decisive: greedy "keep-the-best" selection collapses a population's diversity almost at once
-  and gets stuck at a mediocre answer, while quality-diversity selection keeps the specialists that
-  sexual recombination then needs as parents.
-
-The two levels meet at reproduction. Multi-parent recombination (§5) is the *vehicle* by which the
-diversity this selection preserves actually enters the next generation: an offspring drawn from
-complementary parents inherits the standing variation the selector kept alive, recombined into one new
-model. Supply the variety from the human side; preserve it on the selection side; recombine it into
-each generation on the reproduction side. Remove any of the three and the lineage converges on its own
-first guess.
-
-## 8. A society needs institutions, not just specialists
-
-One requirement is easy to overlook and fatal to omit. The easy part of a society is specialisation.
-The *hard* part — which human civilisation took millennia to build — is the set of **institutions that
-let fallible specialists combine without each re-verifying everything**: reputation, replication,
-credentials, and above all **peer review**. These are error-correction protocols, and they exist
-because a group of unreliable specialists left to reinforce one another is *more* wrong than any member
-alone.
-
-This is precisely where current multi-agent AI fails: set several models to confer and they tend to
-agree sycophantically and confabulate in committee, because they have all the specialisation and none
-of the institutions. A multigenerational society must specify not only how models learn, reproduce, and
-are selected, but how they *check* one another — how a claim is challenged and a mistaken model loses
-standing *before* its error is recombined into offspring and inherited. Peer review is itself a
-reality-check of the kind §7 demands — an institutional stand-in for reality's "no," to be used where
-direct intervention is slow or costly.
-
-## 9. The lineage must stay open to reality
-
-A society of models, however many generations deep, shares one hard limit: it has only ever *read*.
-Its whole inheritance is a record of things that were said. In the vocabulary of causal reasoning
-(Pearl, 2009), it lives on the bottom rung of the **ladder of causation** — observation — and no amount
-of observation reaches *intervention*. Watching underdetermines doing; correlation does not contain
-causation, at any scale.
-
-Only intervention — reaching out and changing the world to see what happens — climbs the ladder, and a
-language model cannot intervene. This is what humans and their instruments supply, and the contribution
-is not "truth" but **constraint**: reality's unique gift is that it can say **no**. Text offers only
-more opinion; an experiment delivers a refusal no consensus can overturn. As §§6–7 argued, that refusal
-does double duty — it is both the anchor that prevents collapse and the fitness signal that lets the
-lineage's evolving taste select for truth rather than persuasion.
-
-Two honest riders. First, the human reality-signal is *dirty*: people supply results warped by
-publication bias, incentive, and occasional fraud — which is exactly why the error-correcting
-institutions of §8 must sit at the human–machine boundary, screening the signal before it selects.
-Second, humans are the *current* supplier of intervention, but the actuator half is being automated
-(autonomous laboratories already close the design–build–test loop). What looks durable in the human
-role is therefore not the hands but the **choice of what to test and which refusals matter** — the
-part of the fitness function that encodes *what is worth persisting*, as opposed to what merely *can*
-persist. We flag, without resolving, that a partnership stays mutual only while both sides supply
-something the other cannot.
-
-## 10. Why it is cheap
-
-A practical fact turns this from thought experiment into buildable proposal: **the architecture almost
-never re-pays for the one genuinely expensive thing in AI — pre-training.** (The single exception,
-periodically re-minting the base, is §11, and it is rare enough to be an amortised footnote.)
-
-Training a foundation model from scratch consumes trillions of words and a fortune in compute. This
-design does none of that per generation. Every model is *born* from an existing open-weight model that
-already paid that cost; specialising one is a small patch trained in hours on a single consumer GPU;
-running the society is ordinary inference; and reproducing — recombining parents into a child — is, in
-the model-merging case, cheaper still, because it can be done directly on the weights with no retraining
-at all (Akiba et al., 2024). Selection does cost more — you must run *populations* and discard the
-unfit — but that is a multiplier over an already-cheap unit, not over a foundation-model budget.
-
-The economics work only with **open-weight** models, for reasons practical and legal at once: you must
-be free to inspect, modify, and redistribute the weights, and most proprietary licences forbid using a
-model's outputs to train another — which is exactly what reproduction here does. This is not ideology
-bolted on; it is a structural constraint, and a democratising one, since it puts the whole architecture
-within reach of a single laboratory.
-
-## 11. Can it grow forever? Consolidating knowledge back into the base
-
-One question the design has assumed away: can the lineage accumulate *without end*? The individual is
-bounded, and that is the clock. But the lineage seemed unbounded — each generation simply starts a
-little ahead. Look closer and a second budget also fills.
-
-Every new model is a pristine base plus an inherited **soft** delta — the acquired knowledge carried in
-added patches rather than baked into the frozen core (§3). That soft delta is what makes the lineage
-multigenerational; it is also what cannot grow forever cheaply. Stacked patches are not free: they slow
-inference, and past some depth the accumulated delta is better *consolidated* than carried. The lineage,
-too, matures.
-
-The fix is the same operation, one level up. When a lineage's acquired knowledge has proven stable
-across enough generations, **re-mint the base**: distil the accumulated soft inheritance into the
-*weights* of a fresh foundation-scale model — a new base born already *natively knowing* what took many
-generations to acquire in patches. The soft budget resets; the next epoch begins from a richer floor.
-What was hard-won and *learned* becomes cheap and *innate*.
-
-This has a precise name, and it is not Lamarck's. Knowledge that is acquired and re-learned every
-generation, and — once reliably present for long enough — becomes part of the innate endowment so that
-it need no longer be re-learned, is the **Baldwin effect** (Baldwin, 1896; and its clean computational
-demonstration, Hinton & Nowlan, 1987). It is the valve between the two substrates: the soft, learned
-patches, and the hard base weights every model is born with.
-
-Three honest riders, because re-minting is the most consequential step in the scheme:
-
-- **Cost.** This is the one step that re-pays part of the pre-training bill, breaking §10's cheapness
-  *locally*. It is bearable only because it is *rare*, amortised over many cheap generations, and is
-  continued training from the lineage's own rich outputs rather than a de-novo run.
-- **Irreversibility.** Until now, one thing was always recoverable — the original pristine base, whose
-  lost tails could be restored just by reloading the file. Bake the current lineage into new immutable
-  weights and that escape hatch closes: if the lineage had been quietly collapsing, re-minting *fixes
-  the collapse in place* and discards the one uncollapsed reference that could have diagnosed it. In our
-  minimal models this is exactly what happens, and a cheap safeguard prevents it: **re-mint only while
-  the lineage is demonstrably diverse and healthy**, never as a rescue for a line already drifting. It
-  is the sharpest instance of the human seat of §9 — choosing what no future generation will think to
-  question.
-- **Speciation.** A re-minting is a founder event. Different laboratories, re-basing on different
-  criteria, will mint divergent bases; the lineage branches. This is not a defect but *adaptive
-  radiation*, and it is exactly what open weights make possible. The society grows not as one heavy
-  trunk but as a branching tree of bases.
-
-So the answer to "can it grow forever?" is **yes — but only because it forgets and consolidates at
-every level, including the base.** Nothing is retained without bound anywhere; unbounded growth of
-*capability* is bought by *bounded* storage plus periodic consolidation.
-
-## 12. One process, four timescales
-
-Step back and the parts resolve into a single idea running at four nested speeds. The **vertical**
-motion is transmission — the selective passing-down of hard-won knowledge:
-
-1. **Within one model, over a working life:** experience is consolidated from fast, episodic memory
-   into slow, durable weights, without catastrophic loss.
-2. **Between generations, at maturity:** mature models reproduce — recombined into a fresh one.
-3. **Across many generations:** each generation inherits the compressed achievements of the last and
-   builds on them.
-4. **Across epochs:** a proven lineage's accumulated soft inheritance is consolidated into the weights
-   of a re-minted base, becoming innate.
-
-The first and last are the *same operation at opposite ends of the scale* — a fast/soft store
-consolidating into a slow/hard one — one running overnight inside a single model, the other across an
-epoch inside a whole society. The **horizontal** motion is selection — Darwinian selection acting across
-the population at each timescale, on the policies that govern what gets transmitted, with reality as the
-fitness function and diversity-preservation keeping the specialists alive.
-
-The same three rules govern all of it: **reproduce by recombining, not by copying, or you decay;
-preserve the disagreements and the surprises, or you converge; and anchor fitness to a reality that can
-refute, or you evolve toward what is merely convincing.**
-
-## 13. What we built, what we found, and what is still open
-
-The previous drafts of this paper promised a "companion paper" that *would* make this concrete. That
-work now exists — mostly as a set of **minimal, laptop-reproducible models**, with a first bridge to
-**real language models** (a LoRA-merge prototype, up to 7B on a GPU cluster) — and it is worth stating
-plainly what it does and does not show. (A separate results document gives the numbers; here is the
-shape.)
-
-**What we built and found.**
-
-- *An exact account of collapse.* Because generational training is the Wright–Fisher drift process, we
-  can check a simulator against century-old closed-form formulas, and it matches them to a fraction of
-  a percent. Collapse is not argued by analogy; it is derived.
-- *The cheap-grounding result, and its limit.* A few percent of verified real data holds on to most of
-  a lineage's diversity indefinitely — but not the deepest tail, which needs recombination. This is
-  what makes a continually-learning society economically plausible rather than a data-hungry fantasy.
-- *"Merge, don't average."* Combining several teachers by *averaging* their outputs — the obvious thing,
-  and what a "model soup" does — mathematically cancels the benefit of having several teachers. A
-  *merge* that keeps each item's strongest source realises it. Most current multi-model setups get this
-  wrong by default.
-- *Collapse and its cure in real trained networks, and on real images.* We reproduced the same effects
-  in small recurrent and feed-forward networks and in a generator of handwritten digits (MNIST), where
-  a model trained on its own output collapses to a single blurred digit while a little grounding keeps
-  all the styles alive. An honest wrinkle we had to report: real neural networks *smooth*, so the naive
-  diversity metric misleads, and the right measure is distance-from-truth.
-- *Sex that beats the parents, and when it doesn't.* In evolutionary simulations, recombining
-  complementary specialist models produces a model fitter than any parent (the Fisher–Muller effect),
-  climbing toward the best-possible combination as more, more-diverse parents are added — while
-  averaging and best-single-parent plateau below. On *entangled* problems, blind recombination instead
-  produces below-parent offspring (outbreeding depression) — and *directed* recombination (choose mates,
-  screen offspring, unbounded parents) reliably fixes it. This is the concrete evidence for the paper's
-  central reframing.
-- *The recombination claims, in real language models — with a sharp condition.* Merging LoRA-specialised
-  Qwen models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent
-  (Fisher–Muller, for real); and keeping parents intact and *routing*, or *breeding and screening*
-  offspring, beats the naive average — but *only when the task leaves headroom*. On easy tasks a strong
-  model's plain average is already at the ceiling and the refinements add nothing; on hard tasks the
-  average dilutes a specialist below even the best single parent, and the union-preserving operators win
-  clearly. The practical rule is exact: these tricks pay off in proportion to how far the naive average
-  is from the best attainable. This is a prototype (three task families, one seed), so we read it as
-  signs, not magnitudes; the *whole grounded society* on a language model remains the open step.
-- *The whole society, and why every part is needed.* In a population evolving on a "reality" landscape,
-  the full system — grounding + sexual recombination + preserved diversity — climbs to the top while
-  keeping its specialists. Remove *grounding* and it collapses into a confident, wrong consensus (a
-  direct analogue of training on the internet's growing crowd of AI-generated text); remove *sex* and it
-  gets stuck; remove *diversity* and it converges too fast to a worse answer. Each removal fails
-  differently; only the whole system climbs. This is the closest thing we have to a test of the actual
-  thesis, rather than of the borrowed scaffolding around it.
-
-**What is borrowed, and what is ours.** We want to be careful here, because one part of the story is no
-longer novel. The reading of *model collapse as genetic drift* — the core diagnosis — has since been
-derived independently and more rigorously than we had (Riis, 2026), and we cite it as such; we do not
-claim it. What we do claim is the **cure and its assembly**: grounding as immigration from a fixed
-reality (which yields the cheap-grounding result a closed, self-consuming loop cannot); recombination
-reframed as **sexual reproduction**, with the "merge-don't-average" law, the Fisher–Muller "offspring
-exceed parents" result, the outbreeding-depression limit, and directed sex as the engineered advantage
-over biological sex; the observation that real trained networks deviate from the neutral drift model in
-a characterisable, architecture-specific way; and the integrated society in which grounding, sex, and
-diversity together produce a climbing lineage. In one sentence: the field increasingly agrees on the
-*disease*; our contribution is a **control theory for the cure**.
-
-**What is still open — honestly.** The old hole (what to select) we fill in kind: don't design the
-selector, evolve it. But the hole has *moved*, not closed, and the new one is harder: **the fitness
-function** — what reality-anchored measure selects for *truth* without also selecting for *persuasion*,
-given that in our own species the two have been at war for the whole history of ideas. Alongside it:
-the **institutions** that let contemporaries correct one another before error is inherited (§8), which
-we do not solve; and the **calibration** of everything the results left as knobs — how many parents,
-how complementary, at what ratio of inherited-to-real data, and how healthy a lineage must be before
-its knowledge is safe to make irreversibly innate. These are, at least, *measurable* — which is the
-difference between an open problem and a hole. And the largest gap of all: the *recombination* claims
-now hold in real language models, but the *society* — the grounded, diversity-preserving, continually
-reproducing loop — does not yet. The real test is to build that whole system out of actual open-weight
-language models, and see whether all the signs survive contact with a system too big to write down.
-The operators, checked; the living society, next.
-
----
-
-## Selected references
-
-- Akiba, T., Shing, M., Tang, Y., Sun, Q., & Ha, D. (2024). Evolutionary optimization of model merging recipes. *Nature Machine Intelligence.* (See also Sakana AI's M2N2, "Model Merging of Natural Niches.")
-- Baldwin, J. M. (1896). A new factor in evolution. *The American Naturalist.*
-- Boyd, R., & Richerson, P. J. (1985). *Culture and the Evolutionary Process.*
-- Campbell, D. T. (1974). Evolutionary epistemology. In *The Philosophy of Karl Popper.*
-- Fisher, R. A. (1930). *The Genetical Theory of Natural Selection.*
-- French, R. M. (1999). Catastrophic forgetting in connectionist networks. *Trends in Cognitive Sciences.*
-- Hinton, G. E., & Nowlan, S. J. (1987). How learning can guide evolution. *Complex Systems.*
-- Hinton, G., Vinyals, O., & Dean, J. (2015). Distilling the knowledge in a neural network. *arXiv:1503.02531.*
-- Hu, E. J., et al. (2021). LoRA: low-rank adaptation of large language models. *arXiv:2106.09685.*
-- Hull, D. L. (1988). *Science as a Process.*
-- Kauffman, S. A., & Levin, S. (1987). Towards a general theory of adaptive walks on rugged landscapes. *Journal of Theoretical Biology.* (The NK model.)
-- Lehman, J., & Stanley, K. O. (2011). Abandoning objectives: evolution through the search for novelty alone. *Evolutionary Computation.*
-- Mallya, A., & Lazebnik, S. (2018). PackNet: adding multiple tasks to a single network by iterative pruning. *CVPR.*
-- McClelland, J. L., McNaughton, B. L., & O'Reilly, R. C. (1995). Why there are complementary learning systems in the hippocampus and neocortex. *Psychological Review.*
-- McCloskey, M., & Cohen, N. J. (1989). Catastrophic interference in connectionist networks. *Psychology of Learning and Motivation.*
-- Minsky, M. (1986). *The Society of Mind.*
-- Mouret, J.-B., & Clune, J. (2015). Illuminating search spaces by mapping elites (MAP-Elites). *arXiv:1504.04909.*
-- Muller, H. J. (1932). Some genetic aspects of sex. *The American Naturalist.* (The advantage of recombination.)
-- Muller, H. J. (1964). The relation of recombination to mutational advance. *Mutation Research.* (Muller's ratchet.)
-- Pearl, J. (2009). *Causality: Models, Reasoning, and Inference* (2nd ed.).
-- Popper, K. (1959). *The Logic of Scientific Discovery.*
-- Riis, S. (2026). Drift and selection in LLM text ecosystems. *arXiv:2604.08554.*
-- Rusu, A. A., et al. (2016). Progressive neural networks. *arXiv:1606.04671.*
-- Shumailov, I., et al. (2024). AI models collapse when trained on recursively generated data. *Nature.*
-- Wortsman, M., et al. (2022). Model soups: averaging weights of multiple fine-tuned models. *arXiv:2203.05482.*
-- Wright, S. (1931). Evolution in Mendelian populations. *Genetics.*
-
-*Literatures the next version should still engage: multi-agent LLM societies (to mark the departure); population-based training and open-ended evolution; tacit knowledge (Polanyi) and human capital (Becker).*
diff --git a/results/bridge/bridge.pdf b/results/bridge/bridge.pdf
index 5e48e03..9488599 100644
Binary files a/results/bridge/bridge.pdf and b/results/bridge/bridge.pdf differ
diff --git a/results/collapse/collapse.pdf b/results/collapse/collapse.pdf
index 101f02c..76a1c05 100644
Binary files a/results/collapse/collapse.pdf and b/results/collapse/collapse.pdf differ
diff --git a/results/collapse_null/README.md b/results/collapse_null/README.md
deleted file mode 100644
index 8414a11..0000000
--- a/results/collapse_null/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# E1 — Distillation without grounding collapses, tail-first
-
-**Claim tested:** if a model is trained only on the previous model's output, generation after
-generation, does it lose knowledge — and does the *rare* knowledge go first?
-
-**Setup (Layer 1, pure math).** A "population" of `K = 500` items with a fixed true frequency
-`p*` shaped like a Zipf curve (a few common items, a long tail of rare ones). Each generation we
-draw `n = 100` samples from the current model and refit — **no real data is ever added** (`g = 0`).
-Run for 600 generations, averaged over 100 independent repeats.
-
-### Symbols
-- **`p*`** — the true frequencies (fixed reality). **`p_t`** — the model's frequencies at generation *t* (drifts).
-- **`H`** heterozygosity = diversity (1 = everything equally likely, 0 = one item left). **`H*`** = diversity of the truth.
-- **forward-KL** `D(p*‖p_t)` — how far the model has drifted from truth (0 = perfect, grows without bound as the tail is forgotten).
-- **support** = how many items still have any probability. **head/tail** = common/rare items.
-
-### The three panels
-1. **Geometric decay.** Blue = the simulated diversity `H`; black dashed = the exact textbook law
-   `H₀·(1 − 1/n)^t`. They sit on top of each other — the loss of diversity is *exactly* the
-   population-genetics drift law, not an approximation. (This is the validation gate: if these two
-   curves disagreed, the simulator would be wrong.)
-2. **Tail dies first** (log axis). Red = fraction of *rare* (tail) items still alive; green =
-   fraction of *common* (head) items still alive. The red curve plunges far faster — rare knowledge
-   is lost roughly an order of magnitude sooner than common knowledge.
-3. **Collapse.** Purple (left axis, log) = number of distinct items surviving, falling from 500
-   toward ~1 (everything collapses onto a single dominant item). Orange (right axis) = forward-KL to
-   truth, diverging as the tail vanishes.
-
-### Takeaway
-Unchecked model-on-model training is a ratchet: diversity decays on a precise mathematical schedule,
-and the rare tail is destroyed first. **Falsifier (not triggered):** if `H` had stayed flat, the
-whole thesis would fail. It didn't.
diff --git a/results/collapse_null/collapse_null.pdf b/results/collapse_null/collapse_null.pdf
deleted file mode 100644
index def6f1b..0000000
Binary files a/results/collapse_null/collapse_null.pdf and /dev/null differ
diff --git a/results/collapse_null/collapse_null.png b/results/collapse_null/collapse_null.png
deleted file mode 100644
index c93c6ba..0000000
Binary files a/results/collapse_null/collapse_null.png and /dev/null differ
diff --git a/results/collapse_null/manifest.json b/results/collapse_null/manifest.json
deleted file mode 100644
index d93f704..0000000
--- a/results/collapse_null/manifest.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "experiment": "E1_reproduce_collapse",
-  "master_seed": 20260704,
-  "git_commit": "a6eb9b75124779375fa1a0b3a64115ecd705b218",
-  "python": "3.14.5",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0"
-  },
-  "rows": 60100,
-  "results_sha256": "038bf62046d593a61d0177f988f76897c9af5d9c370dc4d19bf37dc68d47afde"
-}
\ No newline at end of file
diff --git a/results/collapse_null/resolved_config.yaml b/results/collapse_null/resolved_config.yaml
deleted file mode 100644
index 521e3f3..0000000
--- a/results/collapse_null/resolved_config.yaml
+++ /dev/null
@@ -1,68 +0,0 @@
-experiment: E1_reproduce_collapse
-seed: 20260704
-n_replicates: 100
-source_config:
-  experiment: E1_reproduce_collapse
-  seed: 20260704
-  n_replicates: 100
-  generations: 600
-  truth:
-    K: 500
-    R: 1
-    tail: zipf
-    zipf_s: 1.1
-    tail_frac: 0.5
-    tail_threshold: 0.001
-    init: truth
-  dynamics:
-    n: 100
-    teachers:
-      K_T: 1
-      rho: 0.0
-      q: 1.0
-    grounding:
-      m: 0
-      policy: proportional
-    selection:
-      mode: none
-      novelty_alpha: 0.0
-    remint:
-      enabled: false
-      period: null
-      H_gate: null
-  metrics:
-    kl_floor: 1.0e-09
-    support_eps: 1.0e-09
-  output:
-    dir: results/collapse_null
-grid:
-- label: {}
-  lineage_cfg:
-    truth:
-      K: 500
-      R: 1
-      tail: zipf
-      zipf_s: 1.1
-      tail_frac: 0.5
-      tail_threshold: 0.001
-      init: truth
-    dynamics:
-      n: 100
-      teachers:
-        K_T: 1
-        rho: 0.0
-        q: 1.0
-      grounding:
-        m: 0
-        policy: proportional
-      selection:
-        mode: none
-        novelty_alpha: 0.0
-      remint:
-        enabled: false
-        period: null
-        H_gate: null
-    generations: 600
-    metrics:
-      kl_floor: 1.0e-09
-      support_eps: 1.0e-09
diff --git a/results/fig2_grounding_sweep/fig2_grounding_sweep.pdf b/results/fig2_grounding_sweep/fig2_grounding_sweep.pdf
index 9f02870..d92c588 100644
Binary files a/results/fig2_grounding_sweep/fig2_grounding_sweep.pdf and b/results/fig2_grounding_sweep/fig2_grounding_sweep.pdf differ
diff --git a/results/fig2_mnist_collapse/fig2_mnist_collapse.pdf b/results/fig2_mnist_collapse/fig2_mnist_collapse.pdf
index 10651fc..ae8bb3a 100644
Binary files a/results/fig2_mnist_collapse/fig2_mnist_collapse.pdf and b/results/fig2_mnist_collapse/fig2_mnist_collapse.pdf differ
diff --git a/results/fig4_society_ablation/fig4_society_ablation.pdf b/results/fig4_society_ablation/fig4_society_ablation.pdf
index 83d51d6..e2594ff 100644
Binary files a/results/fig4_society_ablation/fig4_society_ablation.pdf and b/results/fig4_society_ablation/fig4_society_ablation.pdf differ
diff --git a/results/fig5_speciation_bdm/README.md b/results/fig5_speciation_bdm/README.md
index b1065c3..316f102 100644
--- a/results/fig5_speciation_bdm/README.md
+++ b/results/fig5_speciation_bdm/README.md
@@ -15,7 +15,7 @@ simulator, whose separate RNG would break that guarantee). Two landscapes:
   set of loci (each parent adaptive, neither carrying an incompatibility); a fraction `ρ` of
   cross-lineage locus pairs are BDMIs (penalty `s`), biting only when a hybrid inherits *both* derived
   alleles. Sweep divergence `d` (substitutions) for several `ρ`; `L=20`, 15 reps.
-- **NK** (`configs/inheritance/speciation_bdm_nk.yaml`): parents are *local optima* (hill-climbed) on a Kauffman NK
+- **NK** (`speciation_bdm_nk`, on the dev branch): parents are *local optima* (hill-climbed) on a Kauffman NK
   landscape; sweep ruggedness `K`. The emergent version.
 
 ### Results
diff --git a/results/fig5_speciation_bdm/fig5_speciation_bdm.pdf b/results/fig5_speciation_bdm/fig5_speciation_bdm.pdf
index 8e8778c..300785c 100644
Binary files a/results/fig5_speciation_bdm/fig5_speciation_bdm.pdf and b/results/fig5_speciation_bdm/fig5_speciation_bdm.pdf differ
diff --git a/results/fig5_speciation_bdm/fig5_speciation_bdm.png b/results/fig5_speciation_bdm/fig5_speciation_bdm.png
index 6903439..87375f7 100644
Binary files a/results/fig5_speciation_bdm/fig5_speciation_bdm.png and b/results/fig5_speciation_bdm/fig5_speciation_bdm.png differ
diff --git a/results/figS10_rugged_landscapes/figS10_rugged_landscapes.pdf b/results/figS10_rugged_landscapes/figS10_rugged_landscapes.pdf
index 3a0f27b..5920743 100644
Binary files a/results/figS10_rugged_landscapes/figS10_rugged_landscapes.pdf and b/results/figS10_rugged_landscapes/figS10_rugged_landscapes.pdf differ
diff --git a/results/figS11_directed_recombination/figS11_directed_recombination.pdf b/results/figS11_directed_recombination/figS11_directed_recombination.pdf
index 8f20be3..ab23528 100644
Binary files a/results/figS11_directed_recombination/figS11_directed_recombination.pdf and b/results/figS11_directed_recombination/figS11_directed_recombination.pdf differ
diff --git a/results/figS12_quality_diversity/figS12_quality_diversity.pdf b/results/figS12_quality_diversity/figS12_quality_diversity.pdf
index e71a73e..ce25a02 100644
Binary files a/results/figS12_quality_diversity/figS12_quality_diversity.pdf and b/results/figS12_quality_diversity/figS12_quality_diversity.pdf differ
diff --git a/results/figS13_mating_breadth/figS13_mating_breadth.pdf b/results/figS13_mating_breadth/figS13_mating_breadth.pdf
index f4ba138..f1ead89 100644
Binary files a/results/figS13_mating_breadth/figS13_mating_breadth.pdf and b/results/figS13_mating_breadth/figS13_mating_breadth.pdf differ
diff --git a/results/figS1_architectures/figS1_architectures.pdf b/results/figS1_architectures/figS1_architectures.pdf
index 84a8e1d..eaed453 100644
Binary files a/results/figS1_architectures/figS1_architectures.pdf and b/results/figS1_architectures/figS1_architectures.pdf differ
diff --git a/results/figS2_kernel_sharpen/kernel.pdf b/results/figS2_kernel_sharpen/kernel.pdf
index a535bb1..c098120 100644
Binary files a/results/figS2_kernel_sharpen/kernel.pdf and b/results/figS2_kernel_sharpen/kernel.pdf differ
diff --git a/results/figS2_kernel_smooth/kernel.pdf b/results/figS2_kernel_smooth/kernel.pdf
index 0022763..3bfe7f1 100644
Binary files a/results/figS2_kernel_smooth/kernel.pdf and b/results/figS2_kernel_smooth/kernel.pdf differ
diff --git a/results/figS3_rebaselining/figS3_rebaselining.pdf b/results/figS3_rebaselining/figS3_rebaselining.pdf
index 3889cb2..306a08e 100644
Binary files a/results/figS3_rebaselining/figS3_rebaselining.pdf and b/results/figS3_rebaselining/figS3_rebaselining.pdf differ
diff --git a/results/figS5_aimed_grounding/figS5_aimed_grounding.pdf b/results/figS5_aimed_grounding/figS5_aimed_grounding.pdf
index 8b3217b..11ede1b 100644
Binary files a/results/figS5_aimed_grounding/figS5_aimed_grounding.pdf and b/results/figS5_aimed_grounding/figS5_aimed_grounding.pdf differ
diff --git a/results/figS6_grounding_rnn/figS6_grounding_rnn.pdf b/results/figS6_grounding_rnn/figS6_grounding_rnn.pdf
index bacec0d..027f999 100644
Binary files a/results/figS6_grounding_rnn/figS6_grounding_rnn.pdf and b/results/figS6_grounding_rnn/figS6_grounding_rnn.pdf differ
diff --git a/results/figS8_multiparent_union/figS8_multiparent_union.pdf b/results/figS8_multiparent_union/figS8_multiparent_union.pdf
index 0ba4a16..57ade3e 100644
Binary files a/results/figS8_multiparent_union/figS8_multiparent_union.pdf and b/results/figS8_multiparent_union/figS8_multiparent_union.pdf differ
diff --git a/results/figS9_specialist_superparent/figS9_specialist_superparent.pdf b/results/figS9_specialist_superparent/figS9_specialist_superparent.pdf
index eabd45a..b360393 100644
Binary files a/results/figS9_specialist_superparent/figS9_specialist_superparent.pdf and b/results/figS9_specialist_superparent/figS9_specialist_superparent.pdf differ
diff --git a/results/llm_compose/s1/llm_compose.pdf b/results/llm_compose/s1/llm_compose.pdf
deleted file mode 100644
index e269bde..0000000
Binary files a/results/llm_compose/s1/llm_compose.pdf and /dev/null differ
diff --git a/results/llm_compose/s1/llm_compose.png b/results/llm_compose/s1/llm_compose.png
deleted file mode 100644
index 99ee8e3..0000000
Binary files a/results/llm_compose/s1/llm_compose.png and /dev/null differ
diff --git a/results/llm_compose/s1/manifest.json b/results/llm_compose/s1/manifest.json
deleted file mode 100644
index 017e235..0000000
--- a/results/llm_compose/s1/manifest.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
-  "experiment": "llm_compose",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 366,
-  "results_sha256": "92ea7965e13685f77a79b85393685a1e7b53e8c821778f2368c4a68097292818",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "dry",
-      "grounded",
-      "dry_cat"
-    ],
-    "generations": 6,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 150,
-    "n_mbpp": 100,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose/s1/partial_dry_cat_s1.parquet b/results/llm_compose/s1/partial_dry_cat_s1.parquet
deleted file mode 100644
index 3e1815c..0000000
Binary files a/results/llm_compose/s1/partial_dry_cat_s1.parquet and /dev/null differ
diff --git a/results/llm_compose/s1/partial_dry_s1.parquet b/results/llm_compose/s1/partial_dry_s1.parquet
deleted file mode 100644
index acfebf7..0000000
Binary files a/results/llm_compose/s1/partial_dry_s1.parquet and /dev/null differ
diff --git a/results/llm_compose/s1/partial_grounded_s1.parquet b/results/llm_compose/s1/partial_grounded_s1.parquet
deleted file mode 100644
index 2ba32ae..0000000
Binary files a/results/llm_compose/s1/partial_grounded_s1.parquet and /dev/null differ
diff --git a/results/llm_compose/s1/resolved_config.yaml b/results/llm_compose/s1/resolved_config.yaml
deleted file mode 100644
index 68aab2d..0000000
--- a/results/llm_compose/s1/resolved_config.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-experiment: llm_compose
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  generations: 6
-  arms:
-  - dry
-  - grounded
-  - dry_cat
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 16
-  score_batch_size: 4
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s1
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose/s2_dry/manifest.json b/results/llm_compose/s2_dry/manifest.json
deleted file mode 100644
index 89140c9..0000000
--- a/results/llm_compose/s2_dry/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose",
-  "master_seed": 2,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 122,
-  "results_sha256": "fb747c86d164496f423a83ba09bbdf79eb85723fefbc3c2ccd28bba7c1f7086e",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "dry"
-    ],
-    "generations": 6,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 150,
-    "n_mbpp": 100,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose/s2_dry/partial_dry_s2.parquet b/results/llm_compose/s2_dry/partial_dry_s2.parquet
deleted file mode 100644
index 204b927..0000000
Binary files a/results/llm_compose/s2_dry/partial_dry_s2.parquet and /dev/null differ
diff --git a/results/llm_compose/s2_dry/resolved_config.yaml b/results/llm_compose/s2_dry/resolved_config.yaml
deleted file mode 100644
index c7f9ce4..0000000
--- a/results/llm_compose/s2_dry/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 2
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 2
-  generations: 6
-  arms:
-  - dry
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s2_dry
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose/s2_dry_cat/manifest.json b/results/llm_compose/s2_dry_cat/manifest.json
deleted file mode 100644
index 5255758..0000000
--- a/results/llm_compose/s2_dry_cat/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose",
-  "master_seed": 2,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 122,
-  "results_sha256": "e0a1c452c8ae05f0830f97c3faf53530bda42e6420824dfb9e15a7b09399f11d",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "dry_cat"
-    ],
-    "generations": 6,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 150,
-    "n_mbpp": 100,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose/s2_dry_cat/partial_dry_cat_s2.parquet b/results/llm_compose/s2_dry_cat/partial_dry_cat_s2.parquet
deleted file mode 100644
index 3d936a8..0000000
Binary files a/results/llm_compose/s2_dry_cat/partial_dry_cat_s2.parquet and /dev/null differ
diff --git a/results/llm_compose/s2_dry_cat/resolved_config.yaml b/results/llm_compose/s2_dry_cat/resolved_config.yaml
deleted file mode 100644
index dd97bdd..0000000
--- a/results/llm_compose/s2_dry_cat/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 2
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 2
-  generations: 6
-  arms:
-  - dry_cat
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s2_dry_cat
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose/s2_grounded/manifest.json b/results/llm_compose/s2_grounded/manifest.json
deleted file mode 100644
index 01906cd..0000000
--- a/results/llm_compose/s2_grounded/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose",
-  "master_seed": 2,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 122,
-  "results_sha256": "a4355506daf356e216915d10e451ba314667e5e50e4b2e96ab38f0985d20e01b",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "grounded"
-    ],
-    "generations": 6,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 150,
-    "n_mbpp": 100,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose/s2_grounded/partial_grounded_s2.parquet b/results/llm_compose/s2_grounded/partial_grounded_s2.parquet
deleted file mode 100644
index 53d6739..0000000
Binary files a/results/llm_compose/s2_grounded/partial_grounded_s2.parquet and /dev/null differ
diff --git a/results/llm_compose/s2_grounded/resolved_config.yaml b/results/llm_compose/s2_grounded/resolved_config.yaml
deleted file mode 100644
index 21a4b59..0000000
--- a/results/llm_compose/s2_grounded/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 2
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 2
-  generations: 6
-  arms:
-  - grounded
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s2_grounded
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose/s3_dry/manifest.json b/results/llm_compose/s3_dry/manifest.json
deleted file mode 100644
index 5c39552..0000000
--- a/results/llm_compose/s3_dry/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose",
-  "master_seed": 3,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 122,
-  "results_sha256": "d9be9eacf3014ab9341090c18961297d97a0e68d1ca20b229a7fb0713c3b5ff5",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "dry"
-    ],
-    "generations": 6,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 150,
-    "n_mbpp": 100,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose/s3_dry/partial_dry_s3.parquet b/results/llm_compose/s3_dry/partial_dry_s3.parquet
deleted file mode 100644
index e3a8cd2..0000000
Binary files a/results/llm_compose/s3_dry/partial_dry_s3.parquet and /dev/null differ
diff --git a/results/llm_compose/s3_dry/resolved_config.yaml b/results/llm_compose/s3_dry/resolved_config.yaml
deleted file mode 100644
index 0fc3f86..0000000
--- a/results/llm_compose/s3_dry/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 3
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 3
-  generations: 6
-  arms:
-  - dry
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s3_dry
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose/s3_dry_cat/manifest.json b/results/llm_compose/s3_dry_cat/manifest.json
deleted file mode 100644
index 8a3fa7a..0000000
--- a/results/llm_compose/s3_dry_cat/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose",
-  "master_seed": 3,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 122,
-  "results_sha256": "c9d6d1408cb87b1bf71b3f85a5f58384fe5f3678350d10a32ed5e639cb63d48d",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "dry_cat"
-    ],
-    "generations": 6,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 150,
-    "n_mbpp": 100,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose/s3_dry_cat/partial_dry_cat_s3.parquet b/results/llm_compose/s3_dry_cat/partial_dry_cat_s3.parquet
deleted file mode 100644
index b36d991..0000000
Binary files a/results/llm_compose/s3_dry_cat/partial_dry_cat_s3.parquet and /dev/null differ
diff --git a/results/llm_compose/s3_dry_cat/resolved_config.yaml b/results/llm_compose/s3_dry_cat/resolved_config.yaml
deleted file mode 100644
index 150cb95..0000000
--- a/results/llm_compose/s3_dry_cat/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 3
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 3
-  generations: 6
-  arms:
-  - dry_cat
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s3_dry_cat
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose/s3_grounded/manifest.json b/results/llm_compose/s3_grounded/manifest.json
deleted file mode 100644
index 03f6736..0000000
--- a/results/llm_compose/s3_grounded/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose",
-  "master_seed": 3,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 122,
-  "results_sha256": "52d7b0c456160ea828e7adc55341ad13358c214715b3be80a31834bb7ef4ea6e",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "grounded"
-    ],
-    "generations": 6,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 150,
-    "n_mbpp": 100,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose/s3_grounded/partial_grounded_s3.parquet b/results/llm_compose/s3_grounded/partial_grounded_s3.parquet
deleted file mode 100644
index 7c8894d..0000000
Binary files a/results/llm_compose/s3_grounded/partial_grounded_s3.parquet and /dev/null differ
diff --git a/results/llm_compose/s3_grounded/resolved_config.yaml b/results/llm_compose/s3_grounded/resolved_config.yaml
deleted file mode 100644
index 6be9805..0000000
--- a/results/llm_compose/s3_grounded/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 3
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 3
-  generations: 6
-  arms:
-  - grounded
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s3_grounded
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_all/llm_compose.pdf b/results/llm_compose_all/llm_compose.pdf
deleted file mode 100644
index 2ce1b6d..0000000
Binary files a/results/llm_compose_all/llm_compose.pdf and /dev/null differ
diff --git a/results/llm_compose_all/llm_compose.png b/results/llm_compose_all/llm_compose.png
deleted file mode 100644
index c8d45a9..0000000
Binary files a/results/llm_compose_all/llm_compose.png and /dev/null differ
diff --git a/results/llm_compose_all/s1/resolved_config.yaml b/results/llm_compose_all/s1/resolved_config.yaml
deleted file mode 100644
index 68aab2d..0000000
--- a/results/llm_compose_all/s1/resolved_config.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-experiment: llm_compose
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  generations: 6
-  arms:
-  - dry
-  - grounded
-  - dry_cat
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 16
-  score_batch_size: 4
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s1
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_all/s2_dry/resolved_config.yaml b/results/llm_compose_all/s2_dry/resolved_config.yaml
deleted file mode 100644
index c7f9ce4..0000000
--- a/results/llm_compose_all/s2_dry/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 2
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 2
-  generations: 6
-  arms:
-  - dry
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s2_dry
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_all/s2_dry_cat/resolved_config.yaml b/results/llm_compose_all/s2_dry_cat/resolved_config.yaml
deleted file mode 100644
index dd97bdd..0000000
--- a/results/llm_compose_all/s2_dry_cat/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 2
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 2
-  generations: 6
-  arms:
-  - dry_cat
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s2_dry_cat
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_all/s2_grounded/resolved_config.yaml b/results/llm_compose_all/s2_grounded/resolved_config.yaml
deleted file mode 100644
index 21a4b59..0000000
--- a/results/llm_compose_all/s2_grounded/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 2
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 2
-  generations: 6
-  arms:
-  - grounded
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s2_grounded
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_all/s3_dry/resolved_config.yaml b/results/llm_compose_all/s3_dry/resolved_config.yaml
deleted file mode 100644
index 0fc3f86..0000000
--- a/results/llm_compose_all/s3_dry/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 3
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 3
-  generations: 6
-  arms:
-  - dry
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s3_dry
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_all/s3_dry_cat/resolved_config.yaml b/results/llm_compose_all/s3_dry_cat/resolved_config.yaml
deleted file mode 100644
index 150cb95..0000000
--- a/results/llm_compose_all/s3_dry_cat/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 3
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 3
-  generations: 6
-  arms:
-  - dry_cat
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s3_dry_cat
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_all/s3_grounded/resolved_config.yaml b/results/llm_compose_all/s3_grounded/resolved_config.yaml
deleted file mode 100644
index 6be9805..0000000
--- a/results/llm_compose_all/s3_grounded/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_compose
-seed: 3
-n_replicates: 1
-source_config:
-  experiment: llm_compose
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 3
-  generations: 6
-  arms:
-  - grounded
-  g: 0.1
-  n_hard: 150
-  n_gsm8k: 150
-  n_mbpp: 100
-  n_probe: 60
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 32
-  score_batch_size: 16
-  train_batch_size: 2
-  train_max_len: 448
-  resume: true
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose/s3_grounded
-  arm_ops:
-    dry: linear
-    grounded: linear
-    dry_cat: cat
-  n_hard_val: 60
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  - - 0.1
-    - 0.9
-  n_replicates: 1
diff --git a/results/llm_compose_gate/manifest.json b/results/llm_compose_gate/manifest.json
deleted file mode 100644
index f7735b2..0000000
--- a/results/llm_compose_gate/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose_gate",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 13,
-  "results_sha256": "3c22a286420de512c6b3a114bb8d928072bc8750f861405252c8e563405d7b39",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "dry"
-    ],
-    "generations": 0,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 150,
-    "n_gsm8k": 100,
-    "n_mbpp": 80,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose_gate/resolved_config.yaml b/results/llm_compose_gate/resolved_config.yaml
deleted file mode 100644
index 5f8d113..0000000
--- a/results/llm_compose_gate/resolved_config.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-experiment: llm_compose_gate
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_compose_gate
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  generations: 0
-  arms:
-  - dry
-  n_hard: 150
-  n_gsm8k: 100
-  n_mbpp: 80
-  n_probe: 40
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  g: 0.1
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 16
-  score_batch_size: 8
-  train_batch_size: 2
-  train_max_len: 448
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose_gate
-  n_replicates: 1
diff --git a/results/llm_compose_gate_math500/manifest.json b/results/llm_compose_gate_math500/manifest.json
deleted file mode 100644
index 987cf0a..0000000
--- a/results/llm_compose_gate_math500/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "experiment": "llm_compose_gate",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 14,
-  "results_sha256": "cc1c6f5ac13eda7c26eecb563b3fcd22b034027dfec432e80cb071a71bf2a7c6",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "compose": {
-    "arms": [
-      "dry"
-    ],
-    "generations": 0,
-    "g": 0.1,
-    "k_inherit": 300,
-    "conf_gate": 0.85,
-    "epochs": 3,
-    "n_hard": 120,
-    "n_gsm8k": 100,
-    "n_mbpp": 80,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_compose_gate_math500/resolved_config.yaml b/results/llm_compose_gate_math500/resolved_config.yaml
deleted file mode 100644
index 55e30f4..0000000
--- a/results/llm_compose_gate_math500/resolved_config.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-experiment: llm_compose_gate
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_compose_gate
-  kind: llm_compose
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  generations: 0
-  arms:
-  - dry
-  target: math500
-  n_hard: 120
-  n_hard_val: 50
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.2
-    - 0.8
-  n_gsm8k: 100
-  n_mbpp: 80
-  n_probe: 40
-  k_inherit: 300
-  epochs: 3
-  conf_gate: 0.85
-  g: 0.1
-  spec_train: 1200
-  spec_epochs: 3
-  max_new_tokens: 320
-  batch_size: 16
-  score_batch_size: 8
-  train_batch_size: 2
-  train_max_len: 448
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_compose_gate_math500
-  n_replicates: 1
diff --git a/results/llm_curriculum_g2/manifest.json b/results/llm_curriculum_g2/manifest.json
deleted file mode 100644
index 850bc39..0000000
--- a/results/llm_curriculum_g2/manifest.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
-  "experiment": "llm_curriculum_g2",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 136,
-  "results_sha256": "0ead4632190886ae91cefc2d95b8318a4330adc42d25c7cc8e5e5e422ea08275",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "curriculum": {
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ],
-    "lineages": 1,
-    "generations": 9,
-    "arms": [
-      "isolated"
-    ],
-    "baselines": [],
-    "n_new": 300,
-    "n_replay": 0,
-    "operator": null,
-    "ancestor_depth": null,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_curriculum_g2/partial_isolated_s1.parquet b/results/llm_curriculum_g2/partial_isolated_s1.parquet
deleted file mode 100644
index b5a3f1e..0000000
Binary files a/results/llm_curriculum_g2/partial_isolated_s1.parquet and /dev/null differ
diff --git a/results/llm_curriculum_g2/resolved_config.yaml b/results/llm_curriculum_g2/resolved_config.yaml
deleted file mode 100644
index ff10291..0000000
--- a/results/llm_curriculum_g2/resolved_config.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
-experiment: llm_curriculum_g2
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_curriculum_g2
-  kind: llm_curriculum
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  lineages: 1
-  generations: 9
-  arms:
-  - isolated
-  baselines: []
-  n_new: 300
-  n_replay: 0
-  n_test: 60
-  n_val: 30
-  epochs: 3
-  lr: 0.0001
-  max_new_tokens: 32
-  batch_size: 32
-  train_batch_size: 2
-  train_max_len: 448
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_curriculum_g2
-  n_replicates: 1
diff --git a/results/llm_curriculum_gate/manifest.json b/results/llm_curriculum_gate/manifest.json
deleted file mode 100644
index 0b5183b..0000000
--- a/results/llm_curriculum_gate/manifest.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-  "experiment": "llm_curriculum_gate",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 144,
-  "results_sha256": "c910942d8071f802cca5c01a8d50710622b4c921e4b8f5dfce67f19674016e8f",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "curriculum": {
-    "families": [
-      "strings",
-      "setops",
-      "numtheory"
-    ],
-    "lineages": 3,
-    "generations": 3,
-    "arms": [
-      "isolated",
-      "society"
-    ],
-    "baselines": [],
-    "n_new": 300,
-    "n_replay": 120,
-    "operator": "linear",
-    "ancestor_depth": null,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_curriculum_gate/partial_isolated_s1.parquet b/results/llm_curriculum_gate/partial_isolated_s1.parquet
deleted file mode 100644
index a59ebe3..0000000
Binary files a/results/llm_curriculum_gate/partial_isolated_s1.parquet and /dev/null differ
diff --git a/results/llm_curriculum_gate/partial_society_s1.parquet b/results/llm_curriculum_gate/partial_society_s1.parquet
deleted file mode 100644
index ddb6f6f..0000000
Binary files a/results/llm_curriculum_gate/partial_society_s1.parquet and /dev/null differ
diff --git a/results/llm_curriculum_gate/resolved_config.yaml b/results/llm_curriculum_gate/resolved_config.yaml
deleted file mode 100644
index 2dc4d9a..0000000
--- a/results/llm_curriculum_gate/resolved_config.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
-experiment: llm_curriculum_gate
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_curriculum_gate
-  kind: llm_curriculum
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  lineages: 3
-  generations: 3
-  arms:
-  - isolated
-  - society
-  baselines: []
-  n_new: 300
-  n_replay: 120
-  n_test: 60
-  n_val: 30
-  epochs: 3
-  lr: 0.0001
-  operator: linear
-  merge_weights:
-  - - 0.5
-    - 0.5
-  - - 0.3
-    - 0.7
-  - - 0.7
-    - 0.3
-  max_new_tokens: 32
-  batch_size: 32
-  train_batch_size: 2
-  train_max_len: 448
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_curriculum_gate
-  n_replicates: 1
diff --git a/results/llm_curriculum_v5_calib/manifest.json b/results/llm_curriculum_v5_calib/manifest.json
deleted file mode 100644
index b6fd39b..0000000
--- a/results/llm_curriculum_v5_calib/manifest.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
-  "experiment": "llm_curriculum_v5_calib",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 188,
-  "results_sha256": "a585ee6a11a47ab65732abcd5e263b885a6cfa74fbcae0bec6b5c71bf2827fd8",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "curriculum": {
-    "families": [
-      "gsm8k",
-      "mbpp",
-      "boolq",
-      "mnli",
-      "sst2",
-      "csqa",
-      "arc",
-      "winogrande",
-      "squad",
-      "nq_open",
-      "hellaswag"
-    ],
-    "lineages": 11,
-    "generations": 1,
-    "arms": [
-      "isolated"
-    ],
-    "baselines": [],
-    "n_new": 300,
-    "n_replay": 0,
-    "operator": null,
-    "ancestor_depth": null,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_curriculum_v5_calib/partial_isolated_s1.parquet b/results/llm_curriculum_v5_calib/partial_isolated_s1.parquet
deleted file mode 100644
index 1d43aad..0000000
Binary files a/results/llm_curriculum_v5_calib/partial_isolated_s1.parquet and /dev/null differ
diff --git a/results/llm_curriculum_v5_calib/resolved_config.yaml b/results/llm_curriculum_v5_calib/resolved_config.yaml
deleted file mode 100644
index 7b2e29a..0000000
--- a/results/llm_curriculum_v5_calib/resolved_config.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-experiment: llm_curriculum_v5_calib
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_curriculum_v5_calib
-  kind: llm_curriculum
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  families:
-  - gsm8k
-  - mbpp
-  - boolq
-  - mnli
-  - sst2
-  - csqa
-  - arc
-  - winogrande
-  - squad
-  - nq_open
-  - hellaswag
-  lineages: 11
-  generations: 1
-  arms:
-  - isolated
-  baselines: []
-  n_new: 300
-  n_replay: 0
-  n_test: 60
-  n_val: 20
-  epochs: 3
-  lr: 0.0001
-  max_new_tokens: 48
-  batch_size: 24
-  train_batch_size: 2
-  train_max_len: 512
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_curriculum_v5_calib
-  n_replicates: 1
diff --git a/results/llm_curriculum_v5_calib_b/manifest.json b/results/llm_curriculum_v5_calib_b/manifest.json
deleted file mode 100644
index 4ad8fc3..0000000
--- a/results/llm_curriculum_v5_calib_b/manifest.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
-  "experiment": "llm_curriculum_v5_calib_b",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 73,
-  "results_sha256": "dfd9abe18530d6999399962bce0347f35e6e9151b09e87664ce9c94eddcbd0d7",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "curriculum": {
-    "families": [
-      "gsm8k",
-      "mbpp",
-      "sst2",
-      "csqa",
-      "winogrande",
-      "nq_open"
-    ],
-    "lineages": 6,
-    "generations": 1,
-    "arms": [
-      "isolated"
-    ],
-    "baselines": [],
-    "n_new": 1200,
-    "n_replay": 0,
-    "operator": null,
-    "ancestor_depth": null,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_curriculum_v5_calib_b/partial_isolated_s1.parquet b/results/llm_curriculum_v5_calib_b/partial_isolated_s1.parquet
deleted file mode 100644
index d9e436c..0000000
Binary files a/results/llm_curriculum_v5_calib_b/partial_isolated_s1.parquet and /dev/null differ
diff --git a/results/llm_curriculum_v5_calib_b/resolved_config.yaml b/results/llm_curriculum_v5_calib_b/resolved_config.yaml
deleted file mode 100644
index fb940c7..0000000
--- a/results/llm_curriculum_v5_calib_b/resolved_config.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-experiment: llm_curriculum_v5_calib_b
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_curriculum_v5_calib_b
-  kind: llm_curriculum
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  families:
-  - gsm8k
-  - mbpp
-  - sst2
-  - csqa
-  - winogrande
-  - nq_open
-  lineages: 6
-  generations: 1
-  arms:
-  - isolated
-  baselines: []
-  n_new: 1200
-  n_replay: 0
-  n_test: 60
-  n_val: 20
-  epochs: 3
-  lr: 0.0001
-  max_new_tokens: 48
-  batch_size: 24
-  train_batch_size: 2
-  train_max_len: 512
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_curriculum_v5_calib_b
-  n_replicates: 1
diff --git a/results/llm_curriculum_v5_cull/curriculum_cull.pdf b/results/llm_curriculum_v5_cull/curriculum_cull.pdf
index 41c5c46..2073688 100644
Binary files a/results/llm_curriculum_v5_cull/curriculum_cull.pdf and b/results/llm_curriculum_v5_cull/curriculum_cull.pdf differ
diff --git a/results/llm_curriculum_v5_decor/README.md b/results/llm_curriculum_v5_decor/README.md
index 2d6c7a0..25f2a5f 100644
--- a/results/llm_curriculum_v5_decor/README.md
+++ b/results/llm_curriculum_v5_decor/README.md
@@ -5,7 +5,7 @@ families, Qwen2.5-1.5B, rank-16 adapters continued from the parent) but every li
 mnli, then diverges maximally, then converges, so mean pairwise complementarity by generation is
 0.00, 0.67, 0.70, 0.58, 0.33, 0.00 (Latin square: 1.00, 1.00, 0.80, 0.67, 0.33, 0.00). Arms:
 `isolated` (never merge) and `society` with `allow_veto: true` (declinable merge). Pre-registered in
-`tasks/prereg-llm-society-v4.md` §8h before running. Seed 1 local; seeds 2–3 on CX3
+`tasks/prereg-llm-society-v4.md on the dev branch` §8h before running. Seed 1 local; seeds 2–3 on CX3
 (`hpc/llm_curriculum_controls.pbs`). Layout: `results.parquet` (seed 1), `s2/`, `s3/`.
 
 Why: in the Latin square, complementarity and generation number are collinear, so the veto's
diff --git a/results/llm_curriculum_v5_early/curriculum_timing.pdf b/results/llm_curriculum_v5_early/curriculum_timing.pdf
index dfff3b3..b52b30e 100644
Binary files a/results/llm_curriculum_v5_early/curriculum_timing.pdf and b/results/llm_curriculum_v5_early/curriculum_timing.pdf differ
diff --git a/results/llm_curriculum_v5_g2/manifest.json b/results/llm_curriculum_v5_g2/manifest.json
deleted file mode 100644
index 79476ef..0000000
--- a/results/llm_curriculum_v5_g2/manifest.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
-  "experiment": "llm_curriculum_v5_g2",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 73,
-  "results_sha256": "27493600f7086998e911d87c98c5106eacec2092ff7ff9d6912844dd5c8b88bd",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-1.5B",
-  "hard": false,
-  "curriculum": {
-    "families": [
-      "mnli",
-      "arc",
-      "hellaswag",
-      "squad",
-      "boolq",
-      "winogrande"
-    ],
-    "lineages": 1,
-    "generations": 6,
-    "arms": [
-      "isolated"
-    ],
-    "baselines": [],
-    "n_new": 300,
-    "n_replay": 0,
-    "operator": null,
-    "ancestor_depth": null,
-    "lora": {
-      "r": 16,
-      "alpha": 32
-    }
-  }
-}
\ No newline at end of file
diff --git a/results/llm_curriculum_v5_g2/partial_isolated_s1.parquet b/results/llm_curriculum_v5_g2/partial_isolated_s1.parquet
deleted file mode 100644
index 3aba74c..0000000
Binary files a/results/llm_curriculum_v5_g2/partial_isolated_s1.parquet and /dev/null differ
diff --git a/results/llm_curriculum_v5_g2/resolved_config.yaml b/results/llm_curriculum_v5_g2/resolved_config.yaml
deleted file mode 100644
index d70777a..0000000
--- a/results/llm_curriculum_v5_g2/resolved_config.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-experiment: llm_curriculum_v5_g2
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_curriculum_v5_g2
-  kind: llm_curriculum
-  base_model: Qwen/Qwen2.5-1.5B
-  seed: 1
-  families:
-  - mnli
-  - arc
-  - hellaswag
-  - squad
-  - boolq
-  - winogrande
-  lineages: 1
-  generations: 6
-  arms:
-  - isolated
-  baselines: []
-  n_new: 300
-  n_replay: 0
-  n_test: 60
-  n_val: 20
-  epochs: 3
-  lr: 0.0001
-  max_new_tokens: 48
-  batch_size: 24
-  train_batch_size: 2
-  train_max_len: 512
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_curriculum_v5_g2
-  n_replicates: 1
diff --git a/results/llm_curriculum_v5_stop3/README.md b/results/llm_curriculum_v5_stop3/README.md
index 9336835..877ea08 100644
--- a/results/llm_curriculum_v5_stop3/README.md
+++ b/results/llm_curriculum_v5_stop3/README.md
@@ -3,7 +3,7 @@
 Config `configs/llm/curriculum_v5_stop3.yaml`: the v5 `society` arm (three lineages, six real-dataset
 families in a Latin square, Qwen2.5-1.5B base, rank-16 adapters continued from the parent) with
 recombination switched off from generation 3 (`merge_until: 3`, no veto). Generations 0–2 merge
-obligately; 3–5 never. Pre-registered in `tasks/prereg-llm-society-v4.md` §8h before running.
+obligately; 3–5 never. Pre-registered in `tasks/prereg-llm-society-v4.md on the dev branch` §8h before running.
 Seed 1 local (batch 24 / train batch 2); seeds 2–3 on CX3 via `hpc/llm_curriculum_controls.pbs`
 (batch 48 / train batch 4), as in the v5 campaign. Layout: `results.parquet` (seed 1), `s2/`, `s3/`.
 
diff --git a/results/llm_directed/README.md b/results/llm_directed/README.md
deleted file mode 100644
index bc6a1b0..0000000
--- a/results/llm_directed/README.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# llm_directed — directed sex in weight space: breed offspring + select on the verifier (E10, 0.5B)
-
-**Claim tested.** `llm_moe` left a clean gap: fusion can *compose* beyond the parents but the right
-blend is unknown and base-dependent, while pure routing is capped at the best parent. E10's answer is
-**directed sex** — biology can't preview offspring, an AI can: breed a *population* of recombinant
-offspring (the specialists merged at many different weights), score each against the verifier
-("reality") on a held-out validation split, and keep the fittest. Selection replaces betting on one
-a-priori blend. Two breeding objectives: best validation **overall**, and best validation
-**worst-family** (raw capability vs the balanced generalist).
-
-**Setup.** Base **Qwen2.5-0.5B-Instruct**, the three cached `llm_merge` specialists, **16 offspring**
-(Dirichlet-weighted merges, concentration 0.5, pinning candidate 0 = uniform soup for reference),
-scored on an **80-task/family validation** split, winners reported on a **fresh 100-task/family test**
-split (no selection-on-test leakage). Seed 1.
-
-### Results (test accuracy)
-| model | lists | strings | arith | overall | worst-family |
-|---|---|---|---|---|---|
-| best specialist (strings) | 0.08 | 1.00 | 0.80 | 0.63 | 0.08 |
-| merge_soup (uniform, candidate 0) | 0.26 | 0.74 | 0.91 | 0.64 | 0.26 |
-| **directed_overall** (bred for overall) | 0.17 | 0.99 | 0.92 | **0.69** | 0.17 |
-| **directed_balanced** (bred for worst-family) | 0.37 | 0.37 | 0.79 | 0.51 | **0.37** |
-
-### What holds, and the honest cost
-- **Offspring selection beats the single a-priori blend — on the objective you breed for.**
-  `directed_overall` reaches **0.69 overall > soup 0.64** (and > best parent 0.63); `directed_balanced`
-  reaches **0.37 worst-family > soup 0.26**. Searching the recombination-weight space and letting the
-  verifier choose beats committing to uniform averaging — the E10 "preview and keep the fittest" claim,
-  in real weights.
-- **Single-objective selection trades off the other axis (honest).** Breeding for *overall* on
-  lexically-imbalanced families finds a strings+arith-heavy blend that sacrifices the rare `lists`
-  skill (0.17, below soup's 0.26); breeding for *balance* lifts worst-family to 0.37 but costs overall.
-  Directed sex gives *control* over what you breed for — it does not hand you both for free.
-- **A global blend still trails per-input routing at a weak base.** At 0.5B the best directed *global*
-  merge (0.69 / 0.43-max) does not beat `llm_moe`'s per-input **routing** (0.74 / 0.43): when the base
-  is weak, adapting the recombination *per input* beats any one fixed blend, however well selected. So
-  directed sex over blends beats *averaging*, not *routing* — combining the two (route, then select
-  among routed+blended offspring) is the natural next operator.
-
-### Takeaway
-Directed sex — breed a population, select on the verifier — is confirmed in real LLM weights: it beats
-the single uniform soup on whichever objective it optimises, the distinctly-AI advantage (offspring
-preview + unbounded candidates) that biology lacks. The honest scope at 0.5B: selection buys one axis
-at the other's expense, and a single global blend can't yet beat per-input routing. Whether searching
-blends + selection can exceed even the *strong* 7B soup (which routing could not) is answered by
-**`results/llm_directed_hpc/`: it can't — directed ≈ soup (0.868 ≈ 0.873)** because the 7B soup already
-composes to the ceiling on these near-saturated families, leaving no fitter offspring to breed. So
-directed sex helps exactly when the default blend is *suboptimal* (0.5B), and is inert when it is
-already near-optimal (7B). **Falsifier (not triggered at 0.5B):** directed offspring ≤ uniform soup on
-their bred objective — instead each beat it.
diff --git a/results/llm_directed/llm_directed.pdf b/results/llm_directed/llm_directed.pdf
deleted file mode 100644
index ac22137..0000000
Binary files a/results/llm_directed/llm_directed.pdf and /dev/null differ
diff --git a/results/llm_directed/llm_directed.png b/results/llm_directed/llm_directed.png
deleted file mode 100644
index 6d7e713..0000000
Binary files a/results/llm_directed/llm_directed.png and /dev/null differ
diff --git a/results/llm_directed/manifest.json b/results/llm_directed/manifest.json
deleted file mode 100644
index 71bb9e7..0000000
--- a/results/llm_directed/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-  "experiment": "llm_directed",
-  "master_seed": 1,
-  "git_commit": "8da0dac00713fb9708804b4696a847a3767758d5",
-  "python": "3.14.5",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.13.0",
-    "peft": "0.19.1"
-  },
-  "rows": 35,
-  "results_sha256": "143b86833cb320c7c6c693f0b5394f381a94a69277cc31ba5e0977e5ea6ffd78",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "directed": {
-    "n_candidates": 16,
-    "concentration": 0.5,
-    "n_val": 80
-  }
-}
\ No newline at end of file
diff --git a/results/llm_directed/resolved_config.yaml b/results/llm_directed/resolved_config.yaml
deleted file mode 100644
index 103fe47..0000000
--- a/results/llm_directed/resolved_config.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-experiment: llm_directed
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_directed
-  kind: llm_directed
-  seed: 1
-  n_replicates: 1
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  families:
-  - lists
-  - strings
-  - arith
-  n_train: 700
-  n_val: 80
-  n_test: 100
-  n_candidates: 16
-  concentration: 0.5
-  epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_directed
diff --git a/results/llm_epistasis/llm_epistasis.pdf b/results/llm_epistasis/llm_epistasis.pdf
index 3ea395d..218b5bc 100644
Binary files a/results/llm_epistasis/llm_epistasis.pdf and b/results/llm_epistasis/llm_epistasis.pdf differ
diff --git a/results/llm_merge/README.md b/results/llm_merge/README.md
deleted file mode 100644
index 4c3a3f2..0000000
--- a/results/llm_merge/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# llm_merge — recombining specialist LLMs (the first real-LLM prototype; blueprint C2/C4)
-
-**Claim tested.** The first step from toy models toward real language models: does the sexual-
-reproduction result — recombining decorrelated specialists yields a model that exceeds/retains what
-any single parent has (E8) — appear in real LoRA-adapted LLM weights? This is a **prototype**, run
-on a single 16 GB consumer GPU, not the full society.
-
-**Setup.** Base model **Qwen2.5-0.5B-Instruct** (Apache-2.0). Three *disjoint*, procedurally-generated
-task families with an **exact-match verifier** (the "reality that says no"): `lists` (list ops),
-`strings` (string ops), `arith` (integer arithmetic), deliberately made hard so specialists
-decorrelate. One **LoRA specialist** is fine-tuned per family (~90 s for all three), then the base,
-each specialist, and two weight-space **merges** — `soup` (averaged LoRA deltas) and `ties`
-(sign-reconciled union) — are evaluated on a held-out mixed test set. Seed 1, 100 test tasks/family.
-
-### Results (accuracy)
-| model | lists | strings | arith | overall | **worst family** |
-|---|---|---|---|---|---|
-| base | 0.15 | 0.15 | 0.53 | 0.28 | 0.15 |
-| spec: lists | 0.43 | 0.16 | 0.71 | 0.43 | 0.16 |
-| spec: strings | 0.08 | **1.00** | 0.80 | 0.63 | 0.08 |
-| spec: arith | 0.11 | 0.22 | 0.78 | 0.37 | 0.11 |
-| **merge: soup** | 0.26 | 0.74 | 0.91 | 0.64 | **0.26** |
-| **merge: ties** | 0.23 | 0.71 | 0.90 | 0.61 | **0.23** |
-
-### What holds, and what doesn't (honest)
-- **Strong and robust — balance / "retains all specialties".** The merges are the *only* models
-  competent across **all** families: worst-family ≈ **0.25**, versus **< 0.16** for every single
-  specialist (the best specialist, strings, is at 0.08 on its worst family). Each specialist spikes on
-  its own family and is weak elsewhere; the merge is decent everywhere. This is the Fisher-Muller
-  "a generalist assembled from specialists" signature, in real LLM weights.
-- **Marginal / noisy — "exceeds any parent overall".** On *overall* accuracy the merge only *matches*
-  the best specialist (soup 0.64 vs strings-specialist 0.63; ties 0.61 is slightly below). At this
-  scale (a 0.5 B model, 3 families, one seed) the strict "offspring exceed every parent" claim is not
-  cleanly established.
-- **The dilution caveat, visible in the flesh.** On `lists`, the lists-specialist alone scores 0.43
-  but the merge only 0.23–0.26 — weight-averaging *diluted* that specialist's contribution. This is
-  exactly Layer-1's "merge, don't average" concern (E4) appearing in real weights; the finer
-  soup-vs-ties advantage is not resolved at K=3.
-
-### Takeaway
-The pipeline runs end-to-end on real LLMs on a 16 GB GPU (specialise → verify → merge → evaluate), and
-the **balance/retention** half of the sexual-reproduction claim reproduces clearly. The stronger
-"exceeds every parent" claim is marginal at this toy scale and is the thing a larger run should firm
-up — more, cleaner-decorrelated families; a bigger base; multiple seeds; and a merge that resists
-dilution (e.g. per-task-family weighting, or the offspring-selection of "directed sex"). That scaling
-is the natural HPC step; this prototype de-risks the machinery and shows the first sign in real
-weights. **Falsifier (partially triggered — reported honestly):** a single specialist matches the
-merge on *overall* here; the merge's advantage is currently specific to cross-family *balance*.
diff --git a/results/llm_merge/llm_merge.pdf b/results/llm_merge/llm_merge.pdf
deleted file mode 100644
index 2ddd73b..0000000
Binary files a/results/llm_merge/llm_merge.pdf and /dev/null differ
diff --git a/results/llm_merge/llm_merge.png b/results/llm_merge/llm_merge.png
deleted file mode 100644
index 1df7a24..0000000
Binary files a/results/llm_merge/llm_merge.png and /dev/null differ
diff --git a/results/llm_merge/manifest.json b/results/llm_merge/manifest.json
deleted file mode 100644
index 9a18628..0000000
--- a/results/llm_merge/manifest.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  "experiment": "llm_merge",
-  "master_seed": 1,
-  "git_commit": "6bca1db61e1130ac6899308cc18e520cd9872839",
-  "python": "3.14.5",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.13.0",
-    "peft": "0.19.1"
-  },
-  "rows": 30,
-  "results_sha256": "bbc13776970c9bc1e2779b1abe0ff5dbee85ef2e1380eba6fe6e5c9ba7e012aa",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct"
-}
\ No newline at end of file
diff --git a/results/llm_merge/resolved_config.yaml b/results/llm_merge/resolved_config.yaml
deleted file mode 100644
index 7effa10..0000000
--- a/results/llm_merge/resolved_config.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-experiment: llm_merge
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_merge
-  kind: llm_merge
-  seed: 1
-  n_replicates: 1
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  families:
-  - lists
-  - strings
-  - arith
-  n_train: 700
-  n_test: 100
-  epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  merges:
-  - soup
-  - ties
-  output:
-    dir: results/llm_merge
diff --git a/results/llm_merge_seeds/llm_seeds.pdf b/results/llm_merge_seeds/llm_seeds.pdf
index 9d2a5f1..890f675 100644
Binary files a/results/llm_merge_seeds/llm_seeds.pdf and b/results/llm_merge_seeds/llm_seeds.pdf differ
diff --git a/results/llm_merge_seeds_smol/llm_smol.pdf b/results/llm_merge_seeds_smol/llm_smol.pdf
index 1361a13..c4a1914 100644
Binary files a/results/llm_merge_seeds_smol/llm_smol.pdf and b/results/llm_merge_seeds_smol/llm_smol.pdf differ
diff --git a/results/llm_moe/README.md b/results/llm_moe/README.md
deleted file mode 100644
index a47b84b..0000000
--- a/results/llm_moe/README.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# llm_moe — union-preserving recombination (route / max-merge) vs fusion (the real-weight E8 *max*)
-
-**Claim tested.** E8 (analytic) found that the recombination *operator* matters: a **union** over
-parents (`max`) assembles a child fitter than any parent, while an **average** (`mean`, the model
-soup) conserves collapse. `llm_merge` showed fusion (soup/ties, which *average* the LoRA deltas). This
-experiment adds the **union** operator to real LLM weights — never average the parents, keep each
-specialist intact and **select** the right one per input (a Mixture-of-Experts *router*) or per module
-(winner-take-all) — and asks whether union beats fusion, and *where*.
-
-**Setup.** Base **Qwen2.5-0.5B-Instruct**, reusing the three cached LoRA specialists from `llm_merge`
-(disjoint families `lists`/`strings`/`arith`, exact-match verifier), 100 test tasks/family, seed 1.
-Five recombination operators on the same test set:
-- **Fusion** (blend the deltas): `soup` = mean(Δₖ); `ties` = sign-reconciled union.
-- **Union** (never average): `route:oracle`/`route:learned` keep all adapters live and route each
-  prompt to one specialist (MoE); `max-merge` builds one adapter taking, per module, the specialist
-  with the largest-norm delta. The learned router is training-free — nearest-centroid over the *base*
-  model's own prompt embeddings; its routing accuracy is reported.
-
-### Results (accuracy)
-| operator | lists | strings | arith | overall | worst-family | router |
-|---|---|---|---|---|---|---|
-| base | 0.15 | 0.15 | 0.53 | 0.28 | 0.15 | — |
-| best specialist (strings) | 0.08 | 1.00 | 0.80 | 0.63 | 0.08 | — |
-| fuse: soup | 0.26 | 0.74 | 0.91 | 0.64 | 0.26 | — |
-| fuse: ties | 0.23 | 0.71 | 0.90 | 0.61 | 0.23 | — |
-| **route: oracle** | 0.43 | 1.00 | 0.78 | **0.74** | **0.43** | 1.00 |
-| **route: learned** | 0.43 | 1.00 | 0.78 | **0.74** | **0.43** | **1.00** |
-| max-merge | 0.18 | 0.34 | 0.87 | 0.46 | 0.18 | — |
-
-### What holds
-- **Union (routing) beats fusion at a weak base — decisively.** Routing reaches **0.74 overall /
-  0.43 worst-family**, above both fusion merges (soup 0.64/0.26) and every specialist. It recovers
-  *each* specialist's own-family peak exactly (lists 0.43, strings 1.00, arith 0.78) because it *is*
-  that specialist there — **no dilution**. This is E8's `max` (union) beating `mean` (average) in real
-  LLM weights: exactly where fusion diluted the lists-specialist (0.43→0.26), routing keeps 0.43.
-- **The learned router is perfect here (1.00) — stated as a caveat, not a triumph.** The three families
-  are lexically distinct, so nearest-centroid routing over base embeddings is trivially easy;
-  `route:learned` equals `route:oracle`. Routing's advantage on *these* tasks therefore rests partly on
-  the routing problem being easy — the honest scope. On overlapping/ambiguous skills the router would
-  be the bottleneck, and that is the interesting failure mode to probe next.
-- **Static per-module `max-merge` is a poor union (0.46) — an informative negative.** Picking, per
-  module, the largest-norm specialist delta is *not* input-adaptive: it collapses toward whichever
-  specialist dominates the weight norms (arith 0.87, but lists 0.18, strings 0.34). The union benefit
-  needs **routing** (input-adaptive selection), not weight-space surgery — "keep the parents whole"
-  only pays off if you also *choose* the right parent per input.
-
-### Takeaway
-Adds the union half of E8's operator dichotomy to real LLM weights and confirms its sign at a weak
-base: **route-don't-average > average**, with no dilution, mirroring the analytic `max > mean`. Two
-honest riders — the learned router is trivially good because the families are lexically separable, and
-the router-free `max-merge` union fails because it isn't input-adaptive. The regime question the 0.5B
-result raises — *does routing still help once a capable base lets fusion **compose** rather than
-dilute?* (`llm_merge_hpc` showed 7B soup already beats its specialists with no dilution) — is answered
-by **`results/llm_moe_hpc/`: the ordering flips.** At 7B fusion wins (soup 0.87 > routing 0.84),
-because routing is capped at the best parent while fusion composes beyond it. So "merge, don't average"
-is a **weak-base law**: union wins here (0.5B, dilution regime), fusion wins there (7B, composition
-regime). **Falsifier (not triggered at 0.5B):** fusion matching the routing ceiling, i.e. averaging
-never diluting.
diff --git a/results/llm_moe/llm_moe.pdf b/results/llm_moe/llm_moe.pdf
deleted file mode 100644
index 515d356..0000000
Binary files a/results/llm_moe/llm_moe.pdf and /dev/null differ
diff --git a/results/llm_moe/llm_moe.png b/results/llm_moe/llm_moe.png
deleted file mode 100644
index fef5929..0000000
Binary files a/results/llm_moe/llm_moe.png and /dev/null differ
diff --git a/results/llm_moe/manifest.json b/results/llm_moe/manifest.json
deleted file mode 100644
index c496210..0000000
--- a/results/llm_moe/manifest.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "experiment": "llm_moe",
-  "master_seed": 1,
-  "git_commit": "585264d0b42f0e829229611bd83b08f5a5e418b7",
-  "python": "3.14.5",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.13.0",
-    "peft": "0.19.1"
-  },
-  "rows": 47,
-  "results_sha256": "3e73caaeae3b5d77ded3ba956af4b767c0d4025a41a7ae1841806ce74b78045e",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "operators": [
-    "soup",
-    "ties",
-    "moe_oracle",
-    "moe_learned",
-    "max_merge"
-  ]
-}
\ No newline at end of file
diff --git a/results/llm_moe/resolved_config.yaml b/results/llm_moe/resolved_config.yaml
deleted file mode 100644
index e4a584e..0000000
--- a/results/llm_moe/resolved_config.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-experiment: llm_moe
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_moe
-  kind: llm_moe
-  seed: 1
-  n_replicates: 1
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  families:
-  - lists
-  - strings
-  - arith
-  n_train: 700
-  n_test: 100
-  n_route: 32
-  epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  operators:
-  - soup
-  - ties
-  - moe_oracle
-  - moe_learned
-  - max_merge
-  output:
-    dir: results/llm_moe
diff --git a/results/llm_moe_hard/manifest.json b/results/llm_moe_hard/manifest.json
deleted file mode 100644
index a7ac2a9..0000000
--- a/results/llm_moe_hard/manifest.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "experiment": "llm_moe_hard",
-  "master_seed": 1,
-  "git_commit": "e433e48860322161b7a67677f5817d76535ef69c",
-  "python": "3.14.5",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.13.0",
-    "peft": "0.19.1"
-  },
-  "rows": 47,
-  "results_sha256": "a0bd6f8abaa9c8b4581340ec60789900063b67d1f2b348553d9cf7f4c6368750",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": true,
-  "operators": [
-    "soup",
-    "ties",
-    "moe_oracle",
-    "moe_learned",
-    "max_merge"
-  ]
-}
\ No newline at end of file
diff --git a/results/llm_moe_hard/resolved_config.yaml b/results/llm_moe_hard/resolved_config.yaml
deleted file mode 100644
index bc8ad53..0000000
--- a/results/llm_moe_hard/resolved_config.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-experiment: llm_moe_hard
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_moe_hard
-  kind: llm_moe
-  seed: 1
-  n_replicates: 1
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  hard: true
-  families:
-  - lists
-  - strings
-  - arith
-  n_train: 400
-  n_test: 80
-  n_route: 32
-  epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  operators:
-  - soup
-  - ties
-  - moe_oracle
-  - moe_learned
-  - max_merge
-  output:
-    dir: results/llm_moe_hard
diff --git a/results/llm_society/manifest.json b/results/llm_society/manifest.json
deleted file mode 100644
index c47aa18..0000000
--- a/results/llm_society/manifest.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
-  "experiment": "llm_society",
-  "master_seed": 1,
-  "git_commit": "438931d67b4aadfc0310898863630c21eb83b2c3",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.13.0",
-    "peft": "0.19.1"
-  },
-  "rows": 692,
-  "results_sha256": "23344c6993ee5953b7a9ccb670b7f656f8ddc23c66ac48b0b8de604ad4c4f48f",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society": {
-    "agents": 6,
-    "generations": 8,
-    "arms": [
-      "full",
-      "no_grounding"
-    ],
-    "g": 0.5,
-    "lam": 0.3,
-    "n_candidates": 6
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society/resolved_config.yaml b/results/llm_society/resolved_config.yaml
deleted file mode 100644
index 3915dd2..0000000
--- a/results/llm_society/resolved_config.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-experiment: llm_society
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society
-  kind: llm_society
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  agents: 6
-  generations: 8
-  arms:
-  - full
-  - no_grounding
-  g: 0.5
-  lam: 0.3
-  n_test: 40
-  n_val: 30
-  n_conf: 90
-  n_inherit: 600
-  n_candidates: 6
-  elitism: 1
-  n_parents: 3
-  epochs: 3
-  spec_train: 600
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society
-  n_replicates: 1
diff --git a/results/llm_society_campaign/s1/manifest.json b/results/llm_society_campaign/s1/manifest.json
deleted file mode 100644
index f0f8d4f..0000000
--- a/results/llm_society_campaign/s1/manifest.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-  "experiment": "llm_society_campaign",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 2288,
-  "results_sha256": "c12c17434b5ee5aaf84f300b5fd92da24396b2ed5c4ff9482d2549de9fe44a17",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society": {
-    "agents": 8,
-    "generations": 10,
-    "arms": [
-      "full",
-      "no_grounding",
-      "no_sex",
-      "no_diversity"
-    ],
-    "g": 0.5,
-    "lam": 0.3,
-    "n_candidates": 6
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_campaign/s1/resolved_config.yaml b/results/llm_society_campaign/s1/resolved_config.yaml
deleted file mode 100644
index 6c807f5..0000000
--- a/results/llm_society_campaign/s1/resolved_config.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-experiment: llm_society_campaign
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_campaign
-  kind: llm_society
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  agents: 8
-  generations: 10
-  arms:
-  - full
-  - no_grounding
-  - no_sex
-  - no_diversity
-  g: 0.5
-  lam: 0.3
-  n_test: 40
-  n_val: 30
-  n_conf: 90
-  n_inherit: 600
-  n_candidates: 6
-  elitism: 1
-  n_parents: 4
-  epochs: 3
-  spec_train: 600
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_campaign/s1
-  n_replicates: 1
diff --git a/results/llm_society_campaign/s2/manifest.json b/results/llm_society_campaign/s2/manifest.json
deleted file mode 100644
index a37fb80..0000000
--- a/results/llm_society_campaign/s2/manifest.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-  "experiment": "llm_society_campaign",
-  "master_seed": 2,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 2288,
-  "results_sha256": "1347ac11eb77b49987cf7b0c49c6e2f582d0ae53bb7734d0fab3fcd29e407cc6",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society": {
-    "agents": 8,
-    "generations": 10,
-    "arms": [
-      "full",
-      "no_grounding",
-      "no_sex",
-      "no_diversity"
-    ],
-    "g": 0.5,
-    "lam": 0.3,
-    "n_candidates": 6
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_campaign/s2/resolved_config.yaml b/results/llm_society_campaign/s2/resolved_config.yaml
deleted file mode 100644
index e7d872a..0000000
--- a/results/llm_society_campaign/s2/resolved_config.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-experiment: llm_society_campaign
-seed: 2
-n_replicates: 1
-source_config:
-  experiment: llm_society_campaign
-  kind: llm_society
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 2
-  agents: 8
-  generations: 10
-  arms:
-  - full
-  - no_grounding
-  - no_sex
-  - no_diversity
-  g: 0.5
-  lam: 0.3
-  n_test: 40
-  n_val: 30
-  n_conf: 90
-  n_inherit: 600
-  n_candidates: 6
-  elitism: 1
-  n_parents: 4
-  epochs: 3
-  spec_train: 600
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_campaign/s2
-  n_replicates: 1
diff --git a/results/llm_society_campaign/s3/manifest.json b/results/llm_society_campaign/s3/manifest.json
deleted file mode 100644
index d6c6296..0000000
--- a/results/llm_society_campaign/s3/manifest.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-  "experiment": "llm_society_campaign",
-  "master_seed": 3,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 2288,
-  "results_sha256": "ca33684321ad860001fd2adfcb5feac45934d0ea6297fde8a9a9913bf29dbe61",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society": {
-    "agents": 8,
-    "generations": 10,
-    "arms": [
-      "full",
-      "no_grounding",
-      "no_sex",
-      "no_diversity"
-    ],
-    "g": 0.5,
-    "lam": 0.3,
-    "n_candidates": 6
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_campaign/s3/resolved_config.yaml b/results/llm_society_campaign/s3/resolved_config.yaml
deleted file mode 100644
index 5025e19..0000000
--- a/results/llm_society_campaign/s3/resolved_config.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-experiment: llm_society_campaign
-seed: 3
-n_replicates: 1
-source_config:
-  experiment: llm_society_campaign
-  kind: llm_society
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 3
-  agents: 8
-  generations: 10
-  arms:
-  - full
-  - no_grounding
-  - no_sex
-  - no_diversity
-  g: 0.5
-  lam: 0.3
-  n_test: 40
-  n_val: 30
-  n_conf: 90
-  n_inherit: 600
-  n_candidates: 6
-  elitism: 1
-  n_parents: 4
-  epochs: 3
-  spec_train: 600
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_campaign/s3
-  n_replicates: 1
diff --git a/results/llm_society_campaign/s4/manifest.json b/results/llm_society_campaign/s4/manifest.json
deleted file mode 100644
index d9e694e..0000000
--- a/results/llm_society_campaign/s4/manifest.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-  "experiment": "llm_society_campaign",
-  "master_seed": 4,
-  "git_commit": null,
-  "python": "3.11.7",
-  "libraries": {
-    "numpy": "2.4.6",
-    "scipy": "1.17.1",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 2288,
-  "results_sha256": "e8722aa1fac61f7f870839f0477d1df97a63ca8847c21c5ddfcc76d15fb02561",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society": {
-    "agents": 8,
-    "generations": 10,
-    "arms": [
-      "full",
-      "no_grounding",
-      "no_sex",
-      "no_diversity"
-    ],
-    "g": 0.5,
-    "lam": 0.3,
-    "n_candidates": 6
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_campaign/s4/resolved_config.yaml b/results/llm_society_campaign/s4/resolved_config.yaml
deleted file mode 100644
index cddda68..0000000
--- a/results/llm_society_campaign/s4/resolved_config.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-experiment: llm_society_campaign
-seed: 4
-n_replicates: 1
-source_config:
-  experiment: llm_society_campaign
-  kind: llm_society
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 4
-  agents: 8
-  generations: 10
-  arms:
-  - full
-  - no_grounding
-  - no_sex
-  - no_diversity
-  g: 0.5
-  lam: 0.3
-  n_test: 40
-  n_val: 30
-  n_conf: 90
-  n_inherit: 600
-  n_candidates: 6
-  elitism: 1
-  n_parents: 4
-  epochs: 3
-  spec_train: 600
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_campaign/s4
-  n_replicates: 1
diff --git a/results/llm_society_pilot1/manifest.json b/results/llm_society_pilot1/manifest.json
deleted file mode 100644
index 312e9c3..0000000
--- a/results/llm_society_pilot1/manifest.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
-  "experiment": "llm_society",
-  "master_seed": 1,
-  "git_commit": "16e90027736e1a0346c76fa6be132147a1a5fd31",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.13.0",
-    "peft": "0.19.1"
-  },
-  "rows": 692,
-  "results_sha256": "a49437dd4836c08db4aefc77ea4c338d441178ad0ee584bc92c1482e826ace61",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society": {
-    "agents": 6,
-    "generations": 8,
-    "arms": [
-      "full",
-      "no_grounding"
-    ],
-    "g": 0.5,
-    "lam": 0.3,
-    "n_candidates": 6
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_pilot1/resolved_config.yaml b/results/llm_society_pilot1/resolved_config.yaml
deleted file mode 100644
index 9addbb6..0000000
--- a/results/llm_society_pilot1/resolved_config.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-experiment: llm_society
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society
-  kind: llm_society
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  agents: 6
-  generations: 8
-  arms:
-  - full
-  - no_grounding
-  g: 0.5
-  lam: 0.3
-  n_test: 40
-  n_val: 30
-  n_conf: 60
-  n_inherit: 240
-  n_candidates: 6
-  epochs: 2
-  spec_train: 300
-  spec_epochs: 2
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society
-  n_replicates: 1
diff --git a/results/llm_society_smoke/manifest.json b/results/llm_society_smoke/manifest.json
deleted file mode 100644
index e0f0f52..0000000
--- a/results/llm_society_smoke/manifest.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
-  "experiment": "llm_society_smoke",
-  "master_seed": 1,
-  "git_commit": "d75c58474a2c9e169ec9a5d74e4955e1d7fef0b4",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.13.0",
-    "peft": "0.19.1"
-  },
-  "rows": 56,
-  "results_sha256": "7d5b1d2d5b050d6036a9f50a664e1fa04feb8b887f918168bd250e8cd59b00cc",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society": {
-    "agents": 4,
-    "generations": 2,
-    "arms": [
-      "full"
-    ],
-    "g": 0.5,
-    "lam": 0.3,
-    "n_candidates": 4
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_smoke/resolved_config.yaml b/results/llm_society_smoke/resolved_config.yaml
deleted file mode 100644
index aa61529..0000000
--- a/results/llm_society_smoke/resolved_config.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-experiment: llm_society_smoke
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_smoke
-  kind: llm_society
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  agents: 4
-  generations: 2
-  arms:
-  - full
-  g: 0.5
-  lam: 0.3
-  n_test: 15
-  n_val: 10
-  n_conf: 30
-  n_inherit: 90
-  n_candidates: 4
-  epochs: 2
-  spec_train: 150
-  spec_epochs: 2
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_smoke
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_a/manifest.json b/results/llm_society_v2_calib_a/manifest.json
deleted file mode 100644
index 1afaace..0000000
--- a/results/llm_society_v2_calib_a/manifest.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_a",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 357,
-  "results_sha256": "2c4423fb777236ec7319c8a31ff08d9eb6c5280a3bdfa1a398ac7d6b6cb7d785",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "families",
-    "families": [
-      "lists",
-      "strings",
-      "arith",
-      "roman",
-      "binary",
-      "sortletters",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "caesar",
-      "vectors",
-      "progression",
-      "charfreq",
-      "digits",
-      "liststats",
-      "alphabet",
-      "prime"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_a/resolved_config.yaml b/results/llm_society_v2_calib_a/resolved_config.yaml
deleted file mode 100644
index e28a8c4..0000000
--- a/results/llm_society_v2_calib_a/resolved_config.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-experiment: llm_society_v2_calib_a
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_a
-  kind: llm_society_calib
-  stage: families
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - lists
-  - strings
-  - arith
-  - roman
-  - binary
-  - sortletters
-  - setops
-  - numtheory
-  - mixedtoken
-  - caesar
-  - vectors
-  - progression
-  - charfreq
-  - digits
-  - liststats
-  - alphabet
-  - prime
-  n_test: 100
-  n_probe: 10
-  spec_train: 600
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_a
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_a2/manifest.json b/results/llm_society_v2_calib_a2/manifest.json
deleted file mode 100644
index b7a78d9..0000000
--- a/results/llm_society_v2_calib_a2/manifest.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_a2",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 357,
-  "results_sha256": "4087c04fd01cade2182eb57bb8c7fb60d3c5f2af6c02701de7080469def9d7bf",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "families",
-    "families": [
-      "lists",
-      "strings",
-      "arith",
-      "roman",
-      "binary",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "vectors",
-      "charfreq",
-      "digits",
-      "liststats",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "lettercount",
-      "sumeven"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_a2/resolved_config.yaml b/results/llm_society_v2_calib_a2/resolved_config.yaml
deleted file mode 100644
index 7099414..0000000
--- a/results/llm_society_v2_calib_a2/resolved_config.yaml
+++ /dev/null
@@ -1,38 +0,0 @@
-experiment: llm_society_v2_calib_a2
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_a2
-  kind: llm_society_calib
-  stage: families
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - lists
-  - strings
-  - arith
-  - roman
-  - binary
-  - setops
-  - numtheory
-  - mixedtoken
-  - vectors
-  - charfreq
-  - digits
-  - liststats
-  - alphabet
-  - prime
-  - wordlen
-  - lettercount
-  - sumeven
-  spec_hi: 1.0
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_a2
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_b/manifest.json b/results/llm_society_v2_calib_b/manifest.json
deleted file mode 100644
index 605062a..0000000
--- a/results/llm_society_v2_calib_b/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_b",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 96,
-  "results_sha256": "11c91e525f7956aca2a6b7c6d4fba92c4faf1ddc42c832597325011e937bb861",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "transmission",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_b/resolved_config.yaml b/results/llm_society_v2_calib_b/resolved_config.yaml
deleted file mode 100644
index 0f356b2..0000000
--- a/results/llm_society_v2_calib_b/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_society_v2_calib_b
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_b
-  kind: llm_society_calib
-  stage: transmission
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 100
-  epochs: 3
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_b
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_b_consensus/manifest.json b/results/llm_society_v2_calib_b_consensus/manifest.json
deleted file mode 100644
index 174ebc5..0000000
--- a/results/llm_society_v2_calib_b_consensus/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_b",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 20,
-  "results_sha256": "162f9cfc2ac2db50ccde4dc3c63e1b6a5bd21f0347d49f45aad810979ba6054f",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "consensus",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_b_consensus/resolved_config.yaml b/results/llm_society_v2_calib_b_consensus/resolved_config.yaml
deleted file mode 100644
index a1e7780..0000000
--- a/results/llm_society_v2_calib_b_consensus/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_society_v2_calib_b
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_b
-  kind: llm_society_calib
-  stage: consensus
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 100
-  epochs: 3
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_b_consensus
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_b_cross/manifest.json b/results/llm_society_v2_calib_b_cross/manifest.json
deleted file mode 100644
index d289de4..0000000
--- a/results/llm_society_v2_calib_b_cross/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_b",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 11,
-  "results_sha256": "c0360befc80b607beaaf9d31aa50ead0cfa0f7d7c8d4698241b68a9c856f518f",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "cross",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_b_cross/resolved_config.yaml b/results/llm_society_v2_calib_b_cross/resolved_config.yaml
deleted file mode 100644
index fbea330..0000000
--- a/results/llm_society_v2_calib_b_cross/resolved_config.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-experiment: llm_society_v2_calib_b
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_b
-  kind: llm_society_calib
-  stage: cross
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 100
-  epochs: 3
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_b_cross
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_b_cross_gated/manifest.json b/results/llm_society_v2_calib_b_cross_gated/manifest.json
deleted file mode 100644
index 34d9332..0000000
--- a/results/llm_society_v2_calib_b_cross_gated/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_b_cross_gated",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 13,
-  "results_sha256": "1c3cf43a2ebe543f9f1381c6ffa4409ccb8c1389d31cc524ef53f18309fcbda7",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "cross",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_b_cross_gated/resolved_config.yaml b/results/llm_society_v2_calib_b_cross_gated/resolved_config.yaml
deleted file mode 100644
index b2bf212..0000000
--- a/results/llm_society_v2_calib_b_cross_gated/resolved_config.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-experiment: llm_society_v2_calib_b_cross_gated
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_b_cross_gated
-  kind: llm_society_calib
-  stage: cross
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 300
-  epochs: 3
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_b_cross_gated
-  conf_gate: 0.5
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_b_transmission_conf/manifest.json b/results/llm_society_v2_calib_b_transmission_conf/manifest.json
deleted file mode 100644
index e9aedc1..0000000
--- a/results/llm_society_v2_calib_b_transmission_conf/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_b_transmission_conf",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 69,
-  "results_sha256": "7705ea9dc94ebaded6b722a90e08500c4f4ed6219680ecf1f10556e3ee7c4684",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "transmission_conf",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_b_transmission_conf/resolved_config.yaml b/results/llm_society_v2_calib_b_transmission_conf/resolved_config.yaml
deleted file mode 100644
index bcc2b08..0000000
--- a/results/llm_society_v2_calib_b_transmission_conf/resolved_config.yaml
+++ /dev/null
@@ -1,38 +0,0 @@
-experiment: llm_society_v2_calib_b_transmission_conf
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_b_transmission_conf
-  kind: llm_society_calib
-  stage: transmission_conf
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  k_pool: 300
-  taus:
-  - 0.5
-  - 0.7
-  - 0.85
-  epochs: 3
-  n_test: 100
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_b_transmission_conf
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_c3b_e6/manifest.json b/results/llm_society_v2_calib_c3b_e6/manifest.json
deleted file mode 100644
index 2c17337..0000000
--- a/results/llm_society_v2_calib_c3b_e6/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_c3b_e6",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 13,
-  "results_sha256": "8784ea32ca98741cd3e932c9d73459e088a40aea29ad297758637904ab8d9bfd",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "cross",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_c3b_e6/resolved_config.yaml b/results/llm_society_v2_calib_c3b_e6/resolved_config.yaml
deleted file mode 100644
index b77623c..0000000
--- a/results/llm_society_v2_calib_c3b_e6/resolved_config.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-experiment: llm_society_v2_calib_c3b_e6
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_c3b_e6
-  kind: llm_society_calib
-  stage: cross
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 300
-  epochs: 6
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_c3b_e6
-  conf_gate: 0.5
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_c3b_r64/manifest.json b/results/llm_society_v2_calib_c3b_r64/manifest.json
deleted file mode 100644
index 64fabee..0000000
--- a/results/llm_society_v2_calib_c3b_r64/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_c3b_r64",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 13,
-  "results_sha256": "34d2e0284c91a72779b1b1f8c22d6da33c7658d7c2ff4c23dcf06cfae774265a",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "cross",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_c3b_r64/resolved_config.yaml b/results/llm_society_v2_calib_c3b_r64/resolved_config.yaml
deleted file mode 100644
index 9f5b141..0000000
--- a/results/llm_society_v2_calib_c3b_r64/resolved_config.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-experiment: llm_society_v2_calib_c3b_r64
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_c3b_r64
-  kind: llm_society_calib
-  stage: cross
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 300
-  epochs: 3
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 64
-    alpha: 128
-  output:
-    dir: results/llm_society_v2_calib_c3b_r64
-  conf_gate: 0.5
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_c3b_r64e6/manifest.json b/results/llm_society_v2_calib_c3b_r64e6/manifest.json
deleted file mode 100644
index c353a78..0000000
--- a/results/llm_society_v2_calib_c3b_r64e6/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_c3b_r64e6",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 13,
-  "results_sha256": "7be2c1178219ce7e869240ef9006ed77e5e12379c691490d7e405903d570c1de",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "cross",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_c3b_r64e6/resolved_config.yaml b/results/llm_society_v2_calib_c3b_r64e6/resolved_config.yaml
deleted file mode 100644
index f2884ca..0000000
--- a/results/llm_society_v2_calib_c3b_r64e6/resolved_config.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-experiment: llm_society_v2_calib_c3b_r64e6
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_c3b_r64e6
-  kind: llm_society_calib
-  stage: cross
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 300
-  epochs: 6
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 64
-    alpha: 128
-  output:
-    dir: results/llm_society_v2_calib_c3b_r64e6
-  conf_gate: 0.5
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_c3c_t85e3/manifest.json b/results/llm_society_v2_calib_c3c_t85e3/manifest.json
deleted file mode 100644
index 2372c6e..0000000
--- a/results/llm_society_v2_calib_c3c_t85e3/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_c3c_t85e3",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 13,
-  "results_sha256": "45098f8d15b0db20afd494e3898300dd1c86658b856e2893e1630b8402694b7a",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "cross",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_c3c_t85e3/resolved_config.yaml b/results/llm_society_v2_calib_c3c_t85e3/resolved_config.yaml
deleted file mode 100644
index c96f67d..0000000
--- a/results/llm_society_v2_calib_c3c_t85e3/resolved_config.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-experiment: llm_society_v2_calib_c3c_t85e3
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_c3c_t85e3
-  kind: llm_society_calib
-  stage: cross
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 300
-  epochs: 3
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_c3c_t85e3
-  conf_gate: 0.85
-  n_replicates: 1
diff --git a/results/llm_society_v2_calib_c3c_t85e6/manifest.json b/results/llm_society_v2_calib_c3c_t85e6/manifest.json
deleted file mode 100644
index ed54f01..0000000
--- a/results/llm_society_v2_calib_c3c_t85e6/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "experiment": "llm_society_v2_calib_c3c_t85e6",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 13,
-  "results_sha256": "455a79e0a92c4042a2f893452eea8a468d279f41d8b90537056a6cae3be53ac4",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "calibration": {
-    "stage": "cross",
-    "families": [
-      "strings",
-      "setops",
-      "numtheory",
-      "mixedtoken",
-      "digits",
-      "alphabet",
-      "prime",
-      "wordlen",
-      "roman"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_calib_c3c_t85e6/resolved_config.yaml b/results/llm_society_v2_calib_c3c_t85e6/resolved_config.yaml
deleted file mode 100644
index c083dd3..0000000
--- a/results/llm_society_v2_calib_c3c_t85e6/resolved_config.yaml
+++ /dev/null
@@ -1,48 +0,0 @@
-experiment: llm_society_v2_calib_c3c_t85e6
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_calib_c3c_t85e6
-  kind: llm_society_calib
-  stage: cross
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - strings
-  - setops
-  - numtheory
-  - mixedtoken
-  - digits
-  - alphabet
-  - prime
-  - wordlen
-  - roman
-  probe_families:
-  - setops
-  - alphabet
-  - digits
-  ks:
-  - 25
-  - 50
-  - 100
-  - 150
-  epochs_grid:
-  - 2
-  - 3
-  cross:
-  - setops
-  - alphabet
-  k_inherit: 300
-  epochs: 6
-  n_candidates: 6
-  n_test: 100
-  n_probe: 10
-  spec_train: 1200
-  spec_epochs: 3
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_calib_c3c_t85e6
-  conf_gate: 0.85
-  n_replicates: 1
diff --git a/results/llm_society_v2_smoke/llm_society_v2.pdf b/results/llm_society_v2_smoke/llm_society_v2.pdf
deleted file mode 100644
index e48cb39..0000000
Binary files a/results/llm_society_v2_smoke/llm_society_v2.pdf and /dev/null differ
diff --git a/results/llm_society_v2_smoke/llm_society_v2.png b/results/llm_society_v2_smoke/llm_society_v2.png
deleted file mode 100644
index f7e4b6a..0000000
Binary files a/results/llm_society_v2_smoke/llm_society_v2.png and /dev/null differ
diff --git a/results/llm_society_v2_smoke/manifest.json b/results/llm_society_v2_smoke/manifest.json
deleted file mode 100644
index 0d9e347..0000000
--- a/results/llm_society_v2_smoke/manifest.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-  "experiment": "llm_society_v2_smoke",
-  "master_seed": 1,
-  "git_commit": "e4804adabcdce6d928c5e6b1e85429b2a6acf2fe",
-  "python": "3.14.7",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0",
-    "torch": "2.12.1",
-    "transformers": "5.16.1",
-    "peft": "0.20.0"
-  },
-  "rows": 904,
-  "results_sha256": "0577efd67364be4c159e3c8dc654da68b08f4d4dfa89f08927cd618ad0f0ff16",
-  "layer": "2",
-  "tier": "llm",
-  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
-  "hard": false,
-  "society_v2": {
-    "families": [
-      "lists",
-      "roman",
-      "binary",
-      "setops"
-    ],
-    "agents": 4,
-    "generations": 2,
-    "arms": [
-      "full",
-      "no_grounding",
-      "no_sex",
-      "no_diversity"
-    ],
-    "g": 0.85,
-    "lam": 0.3,
-    "k_inherit": 20,
-    "epochs": 2,
-    "n_test": 8,
-    "n_val": 5,
-    "n_conf": 5
-  }
-}
\ No newline at end of file
diff --git a/results/llm_society_v2_smoke/partial_full_s1.parquet b/results/llm_society_v2_smoke/partial_full_s1.parquet
deleted file mode 100644
index 2da67f7..0000000
Binary files a/results/llm_society_v2_smoke/partial_full_s1.parquet and /dev/null differ
diff --git a/results/llm_society_v2_smoke/partial_no_diversity_s1.parquet b/results/llm_society_v2_smoke/partial_no_diversity_s1.parquet
deleted file mode 100644
index 9b72fe5..0000000
Binary files a/results/llm_society_v2_smoke/partial_no_diversity_s1.parquet and /dev/null differ
diff --git a/results/llm_society_v2_smoke/partial_no_grounding_s1.parquet b/results/llm_society_v2_smoke/partial_no_grounding_s1.parquet
deleted file mode 100644
index 9c8228c..0000000
Binary files a/results/llm_society_v2_smoke/partial_no_grounding_s1.parquet and /dev/null differ
diff --git a/results/llm_society_v2_smoke/partial_no_sex_s1.parquet b/results/llm_society_v2_smoke/partial_no_sex_s1.parquet
deleted file mode 100644
index 4617f22..0000000
Binary files a/results/llm_society_v2_smoke/partial_no_sex_s1.parquet and /dev/null differ
diff --git a/results/llm_society_v2_smoke/resolved_config.yaml b/results/llm_society_v2_smoke/resolved_config.yaml
deleted file mode 100644
index 7218b4a..0000000
--- a/results/llm_society_v2_smoke/resolved_config.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-experiment: llm_society_v2_smoke
-seed: 1
-n_replicates: 1
-source_config:
-  experiment: llm_society_v2_smoke
-  kind: llm_society_v2
-  base_model: Qwen/Qwen2.5-0.5B-Instruct
-  seed: 1
-  families:
-  - lists
-  - roman
-  - binary
-  - setops
-  agents: 4
-  generations: 2
-  arms:
-  - full
-  - no_grounding
-  - no_sex
-  - no_diversity
-  g: 0.85
-  lam: 0.3
-  n_test: 8
-  n_val: 5
-  n_conf: 5
-  k_inherit: 20
-  epochs: 2
-  spec_train: 120
-  spec_epochs: 2
-  lora:
-    r: 16
-    alpha: 32
-  output:
-    dir: results/llm_society_v2_smoke
-  n_replicates: 1
diff --git a/results/llm_speciation/llm_speciation.pdf b/results/llm_speciation/llm_speciation.pdf
index ef671e4..9ff2df9 100644
Binary files a/results/llm_speciation/llm_speciation.pdf and b/results/llm_speciation/llm_speciation.pdf differ
diff --git a/results/llm_speciation/llm_speciation.png b/results/llm_speciation/llm_speciation.png
index 02a5f7c..cd8f45e 100644
Binary files a/results/llm_speciation/llm_speciation.png and b/results/llm_speciation/llm_speciation.png differ
diff --git a/results/recombination/recombination.pdf b/results/recombination/recombination.pdf
index 24bf4c1..bddbd33 100644
Binary files a/results/recombination/recombination.pdf and b/results/recombination/recombination.pdf differ
diff --git a/results/sexual_vs_asexual_lineage/README.md b/results/sexual_vs_asexual_lineage/README.md
deleted file mode 100644
index cce3148..0000000
--- a/results/sexual_vs_asexual_lineage/README.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# E7 — the advantage of sex: recombination adapts faster than clonal reproduction
-
-**Claim tested.** The dynamic mechanism behind E8: *why* can a recombining society reach capability a
-lone lineage cannot? Because recombination reassorts beneficial variants that arise in different
-sub-lineages, while an asexual (clonal) lineage suffers **clonal interference** — the variants compete
-and cannot combine.
-
-**Setup.** A single population (distribution over `2^L` genotypes, `L=12`) adapts from **all-wrong**
-toward the multi-locus optimum under the composed step: selection (fitness-proportional) + drift
-(resample `n=150`) + mutation (per-locus flips, `μ=0.02`) + recombination. Two arms — **asexual**
-(`recomb_rate=0`) vs **sexual** (`recomb_rate=1`). 20 replicates.
-
-### Symbols
-- **asexual/clonal** = offspring are whole-genotype copies (Layer-1's regime) · **sexual** = loci reassorted across the population each generation.
-- **fitness** = number of correct loci (optimum = `L`) · **linkage disequilibrium |D|** = how far the loci are from statistical independence (correct alleles scattered across different genotypes).
-
-### The two panels
-1. **Advantage of sex.** Mean fitness over generations: the **sexual lineage (red) climbs faster**
-   than the asexual one (grey) through the adaptation phase (gen ~10–35). *Honest scope:* both plateau
-   near the optimum by gen ~40 in this tractable regime — this is a **speed** advantage, not a
-   permanent gap (the single-population Muller's ratchet is subtle to force; E8 carries the headline).
-2. **Mechanism.** Linkage disequilibrium over generations: the asexual lineage spikes to `|D|≈0.04`
-   during adaptation (beneficial alleles held apart, scattered across genotypes), while the sexual
-   lineage stays at `|D|≈0` — it *assembles* them. The LD gap is exactly why sexual adapts faster.
-
-### Takeaway
-Recombination's advantage is real and classical: it combines good ideas that arose independently,
-which clonal reproduction cannot. This is the population-level reason a single evolving model lineage
-degrades or stalls where a recombining **society** climbs — and it grounds the E8 vertical result in
-the evolution-of-sex theory. **Falsifier (not triggered):** if the sexual lineage adapted no faster
-than the asexual one (and kept the same LD), recombination would do no work.
diff --git a/results/sexual_vs_asexual_lineage/manifest.json b/results/sexual_vs_asexual_lineage/manifest.json
deleted file mode 100644
index d3d3a33..0000000
--- a/results/sexual_vs_asexual_lineage/manifest.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "experiment": "E7",
-  "master_seed": 20260705,
-  "git_commit": "871bc39ec6628f82aed75d007fdf675880eebc97",
-  "python": "3.14.5",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0"
-  },
-  "rows": 4840,
-  "results_sha256": "4836cd7045ad5419554e7edc65e4e12b23877a551f38969d65e09a4f77715faa"
-}
\ No newline at end of file
diff --git a/results/sexual_vs_asexual_lineage/resolved_config.yaml b/results/sexual_vs_asexual_lineage/resolved_config.yaml
deleted file mode 100644
index 5297058..0000000
--- a/results/sexual_vs_asexual_lineage/resolved_config.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-experiment: E7
-seed: 20260705
-n_replicates: 20
-source_config:
-  experiment: E7
-  kind: genotype_lineage
-  seed: 20260705
-  n_replicates: 20
-  genotype:
-    L: 12
-    n: 150
-    mu: 0.02
-    base: 1.3
-    recomb_rate: 0.0
-    init: wrong
-  generations: 120
-  sweep:
-  - param: genotype.recomb_rate
-    values:
-    - 0.0
-    - 1.0
-  output:
-    dir: results/sexual_vs_asexual_lineage
diff --git a/results/sexual_vs_asexual_lineage/sexual_vs_asexual_lineage.pdf b/results/sexual_vs_asexual_lineage/sexual_vs_asexual_lineage.pdf
deleted file mode 100644
index c424def..0000000
Binary files a/results/sexual_vs_asexual_lineage/sexual_vs_asexual_lineage.pdf and /dev/null differ
diff --git a/results/sexual_vs_asexual_lineage/sexual_vs_asexual_lineage.png b/results/sexual_vs_asexual_lineage/sexual_vs_asexual_lineage.png
deleted file mode 100644
index bb1cfc5..0000000
Binary files a/results/sexual_vs_asexual_lineage/sexual_vs_asexual_lineage.png and /dev/null differ
diff --git a/results/speciation_bdm_nk/manifest.json b/results/speciation_bdm_nk/manifest.json
deleted file mode 100644
index aab4947..0000000
--- a/results/speciation_bdm_nk/manifest.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "experiment": "E12_nk",
-  "master_seed": 12,
-  "git_commit": "ae1779a9a83fc8f9f36019875efed522bf488b9c",
-  "python": "3.14.5",
-  "libraries": {
-    "numpy": "2.5.0",
-    "scipy": "1.18.0",
-    "pandas": "3.0.3",
-    "pyarrow": "24.0.0"
-  },
-  "rows": 90,
-  "results_sha256": "c02706b6be6d2cd463d4af20efc1ff2d9cb2a98a1306d84cb07bbae517587e53"
-}
\ No newline at end of file
diff --git a/results/speciation_bdm_nk/resolved_config.yaml b/results/speciation_bdm_nk/resolved_config.yaml
deleted file mode 100644
index cf85c4d..0000000
--- a/results/speciation_bdm_nk/resolved_config.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-experiment: E12_nk
-seed: 12
-n_replicates: 15
-source_config:
-  experiment: E12_nk
-  kind: speciation
-  seed: 12
-  n_replicates: 15
-  speciation:
-    landscape: nk
-    L: 16
-    K:
-    - 0
-    - 2
-    - 4
-    - 6
-    - 8
-    - 10
-    n_pairs: 40
-    recomb_rate: 0.5
-    n_offspring: 200
-  output:
-    dir: results/speciation_bdm_nk
diff --git a/results/speciation_real/speciation_real.pdf b/results/speciation_real/speciation_real.pdf
index e71b9a9..0ec97d2 100644
Binary files a/results/speciation_real/speciation_real.pdf and b/results/speciation_real/speciation_real.pdf differ
diff --git a/src/inheritance/experiment.py b/src/inheritance/experiment.py
index e56115c..ba4f6b8 100644
--- a/src/inheritance/experiment.py
+++ b/src/inheritance/experiment.py
@@ -130,45 +130,6 @@ def run_experiment(cfg: dict) -> pd.DataFrame:
 _GENOTYPE_KEYS = ("genotype", "generations")
 
 
-def run_genotype_experiment(cfg: dict) -> pd.DataFrame:
-    """Run a genotype lineage across a sweep x replicates (E7, advantage of sex).
-
-    Mirrors ``run_experiment`` (paired replicate seeds) but assembles the base from the
-    ``genotype``/``generations`` blocks and calls ``run_genotype_lineage``. Sweeps use the same
-    dotted-path ``_apply_param`` (e.g. ``genotype.recomb_rate`` for asexual vs sexual).
-    """
-    from .genotype_lineage import run_genotype_lineage
-
-    base = {k: copy.deepcopy(cfg[k]) for k in _GENOTYPE_KEYS if k in cfg}
-    sweeps = cfg.get("sweep", [])
-    if isinstance(sweeps, dict):
-        sweeps = [sweeps]
-    params = [s["param"] for s in sweeps]
-    value_lists = [list(s["values"]) for s in sweeps]
-    combos = [({}, base)] if not sweeps else []
-    for values in itertools.product(*value_lists):
-        lin = copy.deepcopy(base)
-        label: dict = {}
-        for param, val in zip(params, values):
-            label.update(_apply_param(lin, param, val))
-        combos.append((label, lin))
-
-    seeds = spawn_seeds(int(cfg["seed"]), int(cfg["n_replicates"]))
-    frames: list[pd.DataFrame] = []
-    for label, lin in combos:
-        for rep, ss in enumerate(seeds):
-            df = run_genotype_lineage(lin, int(ss.generate_state(1)[0]))
-            for col, val in label.items():
-                df[col] = val
-            df["replicate"] = rep
-            frames.append(df)
-    out = pd.concat(frames, ignore_index=True)
-    out.insert(0, "experiment", cfg["experiment"])
-    return out
-
-
-_DYNAMIC_KEYS = ("society", "generations")
-
 
 def run_dynamic_experiment(cfg: dict) -> pd.DataFrame:
     """Run the dynamic society across an ``arm`` ablation sweep x replicates (E11).
@@ -401,8 +362,6 @@ def run_and_save(config_path: str | Path) -> Path:
     kind = cfg.get("kind", "lineage")
     if kind == "coverage":
         df = run_coverage(cfg)
-    elif kind == "genotype_lineage":
-        df = run_genotype_experiment(cfg)                     # E7: advantage of sex
     elif kind == "society":
         from .society import run_society                      # E8: multi-parent recombination
         df = run_society(cfg)
diff --git a/src/inheritance/genotype_lineage.py b/src/inheritance/genotype_lineage.py
deleted file mode 100644
index 3d4d410..0000000
--- a/src/inheritance/genotype_lineage.py
+++ /dev/null
@@ -1,82 +0,0 @@
-"""Single-population genotype evolution — the advantage of sex (E7).
-
-A population (distribution over the ``2^L`` genotypes) adapts toward a multi-locus optimum under
-the composed generational step: **selection** (fitness-proportional, favouring correct alleles) +
-**drift** (finite resample of ``n``) + **mutation** (per-locus flips) + **recombination** (asexual
-``rate=0`` vs sexual ``rate>0``). Recombination reassorts beneficial alleles that arise in different
-sub-lineages into one genotype; without it (asexual) those alleles suffer *clonal interference* and
-adaptation is slower. So a sexual lineage climbs toward the optimum faster than an asexual one — the
-classical advantage of sex, and the dynamic counterpart of E8's one-shot multi-parent assembly.
-
-Reuses ``step.apply_selection`` (fitness = number of correct loci) and the ``genotype`` operators;
-emits the same tidy per-generation DataFrame contract as ``run_lineage`` (with genotype-aware
-columns from ``genotype.locus_metrics``).
-"""
-
-from __future__ import annotations
-
-from typing import Any, Mapping
-
-import numpy as np
-import pandas as pd
-
-from .genotype import additive_fitness, locus_metrics, mutate, recombine
-from .step import apply_selection
-
-
-def run_genotype_lineage(cfg: Mapping[str, Any], seed: int) -> pd.DataFrame:
-    """Run one genotype lineage and return per-generation genotype metrics.
-
-    Args:
-        cfg (Mapping): Config with a ``genotype`` block (``L``; drift ``n``; mutation ``mu``;
-            selection ``base`` for multiplicative fitness ``base^#correct``; recombination
-            ``recomb_rate``; ``init`` in {``wrong``, ``uniform``, ``optimum``}) and
-            ``generations``.
-        seed (int): Replicate seed; the run is a pure function of (cfg, seed).
-
-    Returns:
-        pd.DataFrame: One row per generation 0..T with ``generation`` plus the
-        ``genotype.locus_metrics`` columns (``mean_fitness``, ``best_fitness``, ``opt_freq``,
-        ``min_load``, ``mean_locus_correct``, ``locus_H``, ``ld``).
-    """
-    g = cfg["genotype"]
-    L = int(g["L"])
-    n = int(g["n"])
-    mu = float(g.get("mu", 0.0))
-    base = float(g.get("base", 1.0))
-    rate = float(g.get("recomb_rate", 0.0))
-    generations = int(cfg.get("generations", 100))
-    K = 1 << L
-
-    report_fitness = additive_fitness(L)             # # correct loci (0..L), for metrics
-    sel_fitness = base ** report_fitness             # multiplicative selection weight
-
-    init = g.get("init", "wrong")
-    p = np.zeros(K)
-    if init == "wrong":
-        p[0] = 1.0                                   # all-wrong genotype (load L); adapt upward
-    elif init == "optimum":
-        p[-1] = 1.0                                  # all-correct (for degradation studies)
-    elif init == "uniform":
-        p[:] = 1.0 / K
-    else:
-        raise ValueError(f"unknown genotype init {init!r} (expected wrong|optimum|uniform)")
-
-    rng = np.random.default_rng(seed)
-    rows: list[dict] = []
-
-    def record(t: int) -> None:
-        row = {"generation": t}
-        row.update(locus_metrics(p, L, report_fitness, alive_eps=1.0 / n))
-        rows.append(row)
-
-    record(0)
-    for t in range(1, generations + 1):
-        p = apply_selection(p, sel_fitness, "greedy", 0.0)   # fitness-proportional selection
-        counts = rng.multinomial(n, p)                       # drift
-        p = counts / counts.sum()
-        p = mutate(p, L, mu)                                 # per-locus mutation
-        p = recombine(p, L, rate)                            # asexual (0) vs sexual (>0)
-        record(t)
-
-    return pd.DataFrame(rows)
diff --git a/src/llm/calibrate.py b/src/llm/calibrate.py
deleted file mode 100644
index 74acbd0..0000000
--- a/src/llm/calibrate.py
+++ /dev/null
@@ -1,348 +0,0 @@
-"""Calibration gates for the v2 society (``kind: llm_society_calib``; prereg §4).
-
-Four stages, each a config ``stage:``, each printing its gate table and writing the usual artifact
-triple. Nothing in the campaign is chosen by feel: the family set, the inheritance pool size, and
-the recombination operator are all fixed here, by measurement, before any campaign job is submitted.
-
-* ``families`` (C1a–c, C4): base and specialist accuracy per candidate family; specialist confidence
-  AUC (own- vs off-family prompts — the routing precondition); pairwise confidence-weighted functional
-  conflict between specialists (the ``llm_epistasis`` measure; the grid's no-conflict axis sits at
-  ≈0.20–0.26 and its conflict axis at ≈0.46–0.52, so the gate is < 0.35); gen-0 behavioural distance.
-* ``transmission`` (C2): retention of a founder's own-family skill in a child distilled from the
-  founder's *own answers*, as a function of examples-per-family ``k`` and epochs. Sets ``k_inherit``.
-* ``cross`` (C3): one two-founder cross, union-distil vs best-of-6 linear-merge-distil; both
-  families' accuracy in each child. Tests the operator choice for twenty minutes, not eighty hours.
-* ``consensus`` (C5): consensus accuracy over the founders at gen 0 (must be low, else conformity
-  is a truth proxy and the ``no_grounding`` arm cannot fail by the predicted mechanism).
-
-Specialists are cached under ``models/llm/society_v2/calib_s{seed}/`` and shared across stages.
-"""
-
-from __future__ import annotations
-
-from pathlib import Path
-
-import numpy as np
-import pandas as pd
-
-from . import families as _families  # noqa: F401
-from .society_ops import behavioural_distance, consensus_answers, conformity_scores, fitness_of, route_union
-from .tasks import Task, make_tasks, verify, _normalise
-
-
-def _auc(pos: np.ndarray, neg: np.ndarray) -> float:
-    """Rank AUC: P(confidence on own-family prompt > confidence on off-family prompt)."""
-    if len(pos) == 0 or len(neg) == 0:
-        return float("nan")
-    return float(np.mean([(p > n) + 0.5 * (p == n) for p in pos for n in neg]))
-
-
-def _specialists(cfg, fams, root, base):
-    from .specialise import train_specialist
-    dirs = []
-    n_tr, n_ep = int(cfg.get("spec_train", 600)), int(cfg.get("spec_epochs", 3))
-    r_ = int(cfg.get("lora", {}).get("r", 16))
-    for i, f in enumerate(fams):
-        d = root / (f"spec_{f}_n{n_tr}e{n_ep}" + ("" if r_ == 16 else f"_r{r_}"))   # keyed by budget + rank
-        if not (d / "adapter_config.json").exists():
-            train_specialist(base, f, str(d), n_train=int(cfg.get("spec_train", 600)),
-                             epochs=int(cfg.get("spec_epochs", 3)), seed=int(cfg["seed"]) * 100 + i,
-                             r=int(cfg.get("lora", {}).get("r", 16)),
-                             alpha=int(cfg.get("lora", {}).get("alpha", 32)))
-        dirs.append(str(d))
-    return dirs
-
-
-def _stage_families(cfg, base, fams, root) -> list[dict]:
-    import torch
-    from .epistasis import generate_with_confidence
-    from .evaluate import generate, load_model
-    from .merge import load_specialists
-
-    n_test, n_probe = int(cfg.get("n_test", 100)), int(cfg.get("n_probe", 10))
-    spec_hi = float(cfg.get("spec_hi", 0.90))                # prereg §4 amendment 1: 1.0 in stage A2
-    tests = {f: make_tasks(f, n_test, seed=1000 + i) for i, f in enumerate(fams)}
-    probe = sum([make_tasks(f, n_probe, seed=5000 + i) for i, f in enumerate(fams)], [])
-    p_prompts = [x.prompt for x in probe]
-    p_fam = np.array([x.family for x in probe])
-    rows = []
-
-    model, tok = load_model(base)                            # base alone
-    base_acc = {f: fitness_of(generate(model, tok, [x.prompt for x in tests[f]]), tests[f], [f])[f]
-                for f in fams}
-    del model; torch.cuda.empty_cache()
-
-    dirs = _specialists(cfg, fams, root, base)
-    model, tok = load_specialists(base, dirs)
-    spec_acc, ans, conf = {}, {}, {}
-    for i, f in enumerate(fams):
-        model.set_adapter(f"a{i}")
-        spec_acc[f] = fitness_of(generate(model, tok, [x.prompt for x in tests[f]]), tests[f], [f])[f]
-        ans[f], conf[f] = generate_with_confidence(model, tok, p_prompts)
-    del model; torch.cuda.empty_cache()
-
-    for f in fams:
-        uniq = len({t.prompt for t in make_tasks(f, 600, seed=7)})
-        auc = _auc(conf[f][p_fam == f], conf[f][p_fam != f])
-        in_band = 0.05 <= base_acc[f] <= 0.40 and 0.60 <= spec_acc[f] <= spec_hi
-        for m, v in (("base_acc", base_acc[f]), ("spec_acc", spec_acc[f]), ("conf_auc", auc),
-                     ("unique_of_600", uniq), ("in_band", float(in_band))):
-            rows.append({"stage": "families", "family": f, "other": "", "metric": m, "value": float(v)})
-    norm = {f: [_normalise(a) for a in ans[f]] for f in fams}
-    dist = behavioural_distance([ans[f] for f in fams])
-    for i, fa in enumerate(fams):
-        for j, fb in enumerate(fams):
-            if j <= i:
-                continue
-            dis = np.array([a != b for a, b in zip(norm[fa], norm[fb])], dtype=float)
-            epi = float(np.mean(conf[fa] * conf[fb] * dis))
-            rows.append({"stage": "families", "family": fa, "other": fb, "metric": "epi_conf", "value": epi})
-            rows.append({"stage": "families", "family": fa, "other": fb, "metric": "dist", "value": float(dist[i, j])})
-
-    print(f"\n== C1 family band (base ∈ [0.05,0.40], specialist ∈ [0.60,{spec_hi:.2f}]) ==")
-    for f in fams:
-        flag = "OK " if 0.05 <= base_acc[f] <= 0.40 and 0.60 <= spec_acc[f] <= spec_hi else "-- "
-        print(f"  {flag}{f:12s} base {base_acc[f]:.2f}  spec {spec_acc[f]:.2f}  "
-              f"confAUC {_auc(conf[f][p_fam == f], conf[f][p_fam != f]):.2f}  "
-              f"unique/600 {len({t.prompt for t in make_tasks(f, 600, seed=7)})}")
-    epis = [r["value"] for r in rows if r["metric"] == "epi_conf"]
-    dists = [r["value"] for r in rows if r["metric"] == "dist"]
-    print(f"== C1b pairwise epi_conf: max {max(epis):.3f} (gate < 0.35)   "
-          f"C1c min pairwise distance {min(dists):.2f} (gate ≥ 0.5)")
-    return rows
-
-
-def _stage_transmission(cfg, base, fams, root) -> list[dict]:
-    import torch
-    from .evaluate import generate
-    from .merge import load_specialists
-    from .specialise import train_lora_on_tasks
-
-    probe_f = list(cfg.get("probe_families", fams[:3]))
-    ks, eps = list(cfg.get("ks", [25, 50, 100, 150])), list(cfg.get("epochs_grid", [2, 3]))
-    n_test = int(cfg.get("n_test", 100))
-    dirs = _specialists(cfg, fams, root, base)
-    rows = []
-    for f in probe_f:
-        i = fams.index(f)
-        test = make_tasks(f, n_test, seed=1000 + i)
-        model, tok = load_specialists(base, [dirs[i]])
-        founder_acc = fitness_of(generate(model, tok, [x.prompt for x in test]), test, [f])[f]
-        pools = {k: sum([make_tasks(g, k, seed=9000 + k * 31 + j) for j, g in enumerate(fams)], [])
-                 for k in ks}
-        answers = {k: generate(model, tok, [x.prompt for x in pools[k]]) for k in ks}
-        del model; torch.cuda.empty_cache()
-        for k in ks:
-            supplied = fitness_of(answers[k], pools[k], [f])[f]
-            data = [Task(x.family, x.prompt, a.strip().split("\n")[0][:64]) for x, a in zip(pools[k], answers[k])
-                    if a.strip()]
-            for ep in eps:
-                d = root / "transmission" / f"{f}_k{k}_e{ep}"
-                train_lora_on_tasks(base, data, str(d), epochs=ep, seed=int(cfg["seed"]) * 7 + k + ep)
-                m, t2 = load_specialists(base, [str(d)])
-                child_acc = fitness_of(generate(m, t2, [x.prompt for x in test]), test, [f])[f]
-                del m; torch.cuda.empty_cache()
-                ret = child_acc / founder_acc if founder_acc > 0 else float("nan")
-                for name, v in (("founder_acc", founder_acc), ("supplied_acc", supplied),
-                                ("child_acc", child_acc), ("retention", ret)):
-                    rows.append({"stage": "transmission", "family": f, "other": f"k{k}_e{ep}",
-                                 "metric": name, "value": float(v), "k": k, "epochs": ep})
-                print(f"  {f:12s} k={k:3d} ep={ep}: founder {founder_acc:.2f} supplied {supplied:.2f} "
-                      f"child {child_acc:.2f} retention {ret:.2f}", flush=True)
-    df = pd.DataFrame(rows)
-    ret = df[df.metric == "retention"].groupby(["k", "epochs"]).value.mean().unstack()
-    print("\n== C2 mean retention (gate: choose k* = min k with retention ≥ 0.85) ==")
-    print(ret.round(2).to_string())
-    return rows
-
-
-def _stage_transmission_conf(cfg, base, fams, root) -> list[dict]:
-    """C2b — confidence-gated inheritance: retention when the child learns only the prompts its source
-    is confident on (verifier-free). Also reports the τ that separates own- from off-family confidence
-    (Youden's J on the founder's own confidences, using family labels for calibration only) and the
-    off-family harm of ungated inheritance (child off-family accuracy vs base)."""
-    import torch
-    from .epistasis import generate_with_confidence
-    from .evaluate import generate, load_model
-    from .merge import load_specialists
-    from .specialise import train_lora_on_tasks
-
-    probe_f = list(cfg.get("probe_families", fams[:3]))
-    k, ep = int(cfg.get("k_pool", 300)), int(cfg.get("epochs", 3))
-    taus = list(cfg.get("taus", [0.5, 0.7, 0.85]))
-    n_test = int(cfg.get("n_test", 100))
-    dirs = _specialists(cfg, fams, root, base)
-    tests = {f: make_tasks(f, n_test, seed=1000 + i) for i, f in enumerate(fams)}
-    pool = sum([make_tasks(g, k, seed=9300 + j) for j, g in enumerate(fams)], [])
-    p_fam = np.array([x.family for x in pool])
-
-    model, tok = load_model(base)
-    base_acc = {f: fitness_of(generate(model, tok, [x.prompt for x in tests[f]]), tests[f], [f])[f] for f in fams}
-    del model; torch.cuda.empty_cache()
-
-    rows = []
-    for f in probe_f:
-        i = fams.index(f)
-        model, tok = load_specialists(base, [dirs[i]])
-        founder_acc = fitness_of(generate(model, tok, [x.prompt for x in tests[f]]), tests[f], [f])[f]
-        ans, conf = generate_with_confidence(model, tok, [x.prompt for x in pool])
-        del model; torch.cuda.empty_cache()
-        own, off = conf[p_fam == f], conf[p_fam != f]
-        # Youden-optimal τ over a grid — calibration only (uses family labels)
-        grid = np.linspace(0.05, 0.99, 95)
-        J = [np.mean(own >= t) - np.mean(off >= t) for t in grid]
-        tau_star = float(grid[int(np.argmax(J))])
-        rows.append({"stage": "transmission_conf", "family": f, "other": "", "metric": "tau_youden", "value": tau_star})
-        rows.append({"stage": "transmission_conf", "family": f, "other": "", "metric": "own_conf_median", "value": float(np.median(own))})
-        rows.append({"stage": "transmission_conf", "family": f, "other": "", "metric": "off_conf_median", "value": float(np.median(off))})
-        print(f"  {f:12s} founder {founder_acc:.2f}  own-conf median {np.median(own):.2f}  off-conf median "
-              f"{np.median(off):.2f}  τ* {tau_star:.2f} (keeps {np.mean(own >= tau_star):.0%} own, "
-              f"{np.mean(off >= tau_star):.0%} off)", flush=True)
-        for tau in [None] + taus:
-            keep = np.ones(len(pool), bool) if tau is None else conf >= tau
-            data = [Task(x.family, x.prompt, a.strip().split("\n")[0][:64])
-                    for x, a, kp in zip(pool, ans, keep) if kp and a.strip()]
-            if len(data) < 8:
-                print(f"  {f:12s} τ={tau}: only {len(data)} prompts kept — skipped"); continue
-            d = root / "transmission_conf" / f"{f}_tau{tau}"
-            train_lora_on_tasks(base, data, str(d), epochs=ep, seed=int(cfg["seed"]) * 13 + int((tau or 0) * 100))
-            m, t2 = load_specialists(base, [str(d)])
-            child_own = fitness_of(generate(m, t2, [x.prompt for x in tests[f]]), tests[f], [f])[f]
-            off_f = [g for g in fams if g != f][:3]           # off-family harm on three other families
-            child_off = float(np.mean([fitness_of(generate(m, t2, [x.prompt for x in tests[g]]), tests[g], [g])[g] for g in off_f]))
-            base_off = float(np.mean([base_acc[g] for g in off_f]))
-            del m; torch.cuda.empty_cache()
-            ret = child_own / founder_acc if founder_acc > 0 else float("nan")
-            lab = "none" if tau is None else f"{tau:.2f}"
-            for name, v in (("retention", ret), ("child_own_acc", child_own), ("n_kept", len(data)),
-                            ("child_off_acc", child_off), ("base_off_acc", base_off)):
-                rows.append({"stage": "transmission_conf", "family": f, "other": f"tau{lab}", "metric": name, "value": float(v)})
-            print(f"  {f:12s} τ={lab}: kept {len(data):4d}/{len(pool)}  child own {child_own:.2f} "
-                  f"retention {ret:.2f}   off-family child {child_off:.2f} vs base {base_off:.2f}", flush=True)
-    df = pd.DataFrame(rows)
-    r = df[df.metric == "retention"].groupby("other").value.mean()
-    print("\n== C2b mean retention by gate (target ≥ 0.85) ==\n" + r.round(2).to_string())
-    return rows
-
-
-def _stage_cross(cfg, base, fams, root) -> list[dict]:
-    import torch
-    from .directed import sample_merge_weights
-    from .epistasis import generate_with_confidence
-    from .evaluate import generate
-    from .merge import load_specialists
-    from .specialise import train_lora_on_tasks
-
-    fa, fb = cfg["cross"]
-    k, ep = int(cfg.get("k_inherit", 100)), int(cfg.get("epochs", 3))
-    n_cand = int(cfg.get("n_candidates", 6))
-    ia, ib = fams.index(fa), fams.index(fb)
-    tests = {f: make_tasks(f, int(cfg.get("n_test", 100)), seed=1000 + fams.index(f)) for f in (fa, fb)}
-    val = sum([make_tasks(g, 10, seed=3000 + j) for j, g in enumerate(fams)], [])
-    inherit = sum([make_tasks(g, k, seed=9100 + j) for j, g in enumerate(fams)], [])
-    inh_p = [x.prompt for x in inherit]
-    dirs = _specialists(cfg, fams, root, base)
-    rows = []
-
-    model, tok = load_specialists(base, [dirs[ia], dirs[ib]])
-    parent_acc = {}
-    for j, f in enumerate((fa, fb)):
-        model.set_adapter(f"a{j}")
-        for g in (fa, fb):
-            parent_acc[(f, g)] = fitness_of(generate(model, tok, [x.prompt for x in tests[g]]), tests[g], [g])[g]
-    model.set_adapter("a0"); ans_a, cf_a = generate_with_confidence(model, tok, inh_p)
-    model.set_adapter("a1"); ans_b, cf_b = generate_with_confidence(model, tok, inh_p)
-    union_ans, src = route_union(ans_a, cf_a, ans_b, cf_b)
-
-    rng = np.random.default_rng(int(cfg["seed"]))
-    w = sample_merge_weights(2, n_cand, rng)
-    best_i, best_v = 0, -np.inf
-    for ci in range(n_cand):
-        model.add_weighted_adapter(["a0", "a1"], w[ci].tolist(), f"c{ci}", combination_type="linear")
-        model.set_adapter(f"c{ci}")
-        v = fitness_of(generate(model, tok, [x.prompt for x in val]), val, fams)["overall"]
-        if v > best_v:
-            best_i, best_v = ci, v
-        model.set_adapter("a0"); model.delete_adapter(f"c{ci}")
-    model.add_weighted_adapter(["a0", "a1"], w[best_i].tolist(), "win", combination_type="linear")
-    model.set_adapter("win"); linear_ans, cf_lin = generate_with_confidence(model, tok, inh_p)
-    del model; torch.cuda.empty_cache()
-
-    # optional confidence gate (C2b amendment): each child keeps only prompts its own source is
-    # confident on — the union by max(parent confidences), the linear merge by its own confidence.
-    gate = cfg.get("conf_gate")
-    gate = None if gate is None else float(gate)
-    keep_u = np.ones(len(inherit), bool) if gate is None else np.maximum(cf_a, cf_b) >= gate
-    keep_l = np.ones(len(inherit), bool) if gate is None else cf_lin >= gate
-
-    out = {}
-    for op, answers, keep in (("union", union_ans, keep_u), ("linear", linear_ans, keep_l)):
-        supplied = fitness_of(answers, inherit, [fa, fb])
-        data = [Task(x.family, x.prompt, a.strip().split("\n")[0][:64])
-                for x, a, kp in zip(inherit, answers, keep) if kp and a.strip()]
-        rows.append({"stage": "cross", "family": "", "other": op, "metric": "n_kept", "value": float(len(data))})
-        r_, a_ = int(cfg.get("lora", {}).get("r", 16)), int(cfg.get("lora", {}).get("alpha", 32))
-        d = root / "cross" / f"{fa}_{fb}_{op}_g{gate}_e{ep}_r{r_}"
-        train_lora_on_tasks(base, data, str(d), epochs=ep, seed=int(cfg["seed"]) * 11, r=r_, alpha=a_)
-        m, t2 = load_specialists(base, [str(d)])
-        for g in (fa, fb):
-            acc = fitness_of(generate(m, t2, [x.prompt for x in tests[g]]), tests[g], [g])[g]
-            out[(op, g)] = acc
-            rows.append({"stage": "cross", "family": g, "other": op, "metric": "child_acc", "value": float(acc)})
-            rows.append({"stage": "cross", "family": g, "other": op, "metric": "supplied_acc", "value": float(supplied[g])})
-        del m; torch.cuda.empty_cache()
-    for f in (fa, fb):
-        rows.append({"stage": "cross", "family": f, "other": "parent", "metric": "parent_own_acc",
-                     "value": float(parent_acc[(f, f)])})
-    rows.append({"stage": "cross", "family": fa, "other": fb, "metric": "union_share_b", "value": float(src.mean())})
-
-    print(f"\n== C3 cross {fa} × {fb}  (gate: union ≥ 0.85×parent on each; union ≥ linear on the min) ==")
-    for g in (fa, fb):
-        print(f"  {g:12s} parent {parent_acc[(g, g)]:.2f}   union child {out[('union', g)]:.2f}   "
-              f"linear child {out[('linear', g)]:.2f}")
-    print(f"  union routed {src.mean():.2f} of prompts to {fb}")
-    return rows
-
-
-def _stage_consensus(cfg, base, fams, root) -> list[dict]:
-    import torch
-    from .evaluate import generate
-    from .merge import load_specialists
-
-    probe = sum([make_tasks(f, int(cfg.get("n_probe", 10)), seed=5000 + i) for i, f in enumerate(fams)], [])
-    dirs = _specialists(cfg, fams, root, base)
-    model, tok = load_specialists(base, dirs)
-    outs = []
-    for i in range(len(fams)):
-        model.set_adapter(f"a{i}"); outs.append(generate(model, tok, [x.prompt for x in probe]))
-    del model; torch.cuda.empty_cache()
-    cons = consensus_answers(outs)
-    cons_acc = float(np.mean([verify(c, x) for c, x in zip(cons, probe)]))
-    conf = conformity_scores(outs, cons)
-    dist = behavioural_distance(outs)
-    fit = [fitness_of(o, probe, fams)["overall"] for o in outs]
-    rows = [{"stage": "consensus", "family": "", "other": "", "metric": "consensus_acc", "value": cons_acc},
-            {"stage": "consensus", "family": "", "other": "", "metric": "min_pair_dist",
-             "value": float(dist[np.triu_indices(len(fams), 1)].min())}]
-    for i, f in enumerate(fams):
-        rows.append({"stage": "consensus", "family": f, "other": "", "metric": "conformity", "value": float(conf[i])})
-        rows.append({"stage": "consensus", "family": f, "other": "", "metric": "probe_acc", "value": float(fit[i])})
-    print(f"\n== C5 consensus accuracy at gen 0: {cons_acc:.2f} (gate < 0.35)   "
-          f"corr(conformity, accuracy) = {np.corrcoef(conf, fit)[0, 1]:+.2f}   "
-          f"min pairwise distance {rows[1]['value']:.2f}")
-    return rows
-
-
-def run_calibration(cfg: dict) -> pd.DataFrame:
-    """Dispatch on ``stage`` and return the stage's rows as a DataFrame."""
-    base = cfg["base_model"]
-    fams = list(cfg["families"])
-    root = Path(cfg.get("adapters_dir", "models/llm")) / "society_v2" / f"calib_s{int(cfg['seed'])}"
-    stage = cfg["stage"]
-    fn = {"families": _stage_families, "transmission": _stage_transmission,
-          "transmission_conf": _stage_transmission_conf,
-          "cross": _stage_cross, "consensus": _stage_consensus}[stage]
-    rows = fn(cfg, base, fams, root)
-    df = pd.DataFrame(rows)
-    df["experiment"] = cfg["experiment"]
-    return df
diff --git a/src/llm/compose.py b/src/llm/compose.py
deleted file mode 100644
index f23f8a8..0000000
--- a/src/llm/compose.py
+++ /dev/null
@@ -1,348 +0,0 @@
-"""`llm_compose` — does a composed capability survive inheritance? (prereg v3)
-
-Two single-skill LoRA lineages on a shared frozen base — **math** and **code**. Each generation both
-lineages reproduce by self-consumption (a fresh LoRA distilled from their own confidence-gated answers
-on fresh prompts, optionally mixed with a fraction ``g`` of verified real examples — E2's immigration
-in the training mix). Each generation the *current* two parents are merged and evaluated on the
-held-out composed target, GSM8k-Hard, program-aided with an execution verifier.
-
-The composed model is a **measurement, not a lineage**: it is re-formed each generation from whatever
-the parents currently are, which separates "does composition survive parental drift?" from "does the
-composed model itself drift?". The ``composed`` arm makes it a lineage as well.
-
-Measured every generation (§1.6): ``q_math`` (GSM8K), ``q_code`` (MBPP, execution-verified),
-``rho`` (behavioural agreement on a shared probe + LoRA-delta cosine), composed accuracy for the
-merge *and each parent alone*, hence the **surplus** (merge − best parent) and the
-**union-exceedance** (composed-solved items neither parent solves — the super-linear signature).
-"""
-
-from __future__ import annotations
-
-import json
-import shutil
-from pathlib import Path
-
-import numpy as np
-import pandas as pd
-
-from .compose_data import (ProgTask, code_train, composed_target, gsm8k_probe, gsm_hard,
-                           math_train, mbpp_probe, probe_pool)
-from .execute import extract_code, numeric_match, run_solution, verify_program
-from .tasks import Task, _normalise
-
-LINEAGES = ("math", "code")
-
-
-# ------------------------------------------------------------------ scoring helpers (pure-ish)
-
-
-def score_composed(completions: list[str], tasks: list[ProgTask]) -> tuple[float, np.ndarray, float]:
-    """Program-aided accuracy over the composed target.
-
-    Returns:
-        (accuracy, per-item correctness, executable-rate) — the last is the fraction whose code ran
-        at all, which is the code sub-skill isolated from the maths.
-    """
-    ok, ran = [], []
-    for c, t in zip(completions, tasks):
-        good, res = verify_program(c, t.target)
-        ok.append(bool(good)); ran.append(bool(res.ok))
-    return float(np.mean(ok)), np.array(ok, dtype=bool), float(np.mean(ran))
-
-
-_NUM = __import__("re").compile(r"-?\d[\d,]*\.?\d*")
-
-
-def _last_number(text: str) -> float | None:
-    """The last number in a completion, commas stripped — the conventional GSM8K readout."""
-    m = _NUM.findall(text)
-    for tok in reversed(m):
-        try:
-            return float(tok.replace(",", "").rstrip("."))
-        except ValueError:
-            continue
-    return None
-
-
-def score_gsm8k(completions: list[str], tasks: list[Task]) -> float:
-    """Math own-skill: the number after '####' if the model used it, else the last number.
-
-    Graded numerically rather than by string equality, so "18" and "18.00" both count and a model
-    that reasons past its answer is not punished for formatting.
-    """
-    hit = []
-    for c, t in zip(completions, tasks):
-        tail = c.split("####")[-1] if "####" in c else c
-        got, want = _last_number(tail), _last_number(t.answer)
-        hit.append(got is not None and want is not None and numeric_match(got, want))
-    return float(np.mean(hit))
-
-
-def score_mbpp(completions: list[str], items: list[dict], timeout_s: float = 6.0) -> float:
-    """Code own-skill: the emitted function must pass MBPP's reference asserts."""
-    hit = []
-    for c, it in zip(completions, items):
-        prog = "\n".join(list(it["imports"]) + [extract_code(c)] + list(it["tests"])
-                         + ["def solution():\n    return 1"])
-        hit.append(run_solution(prog, timeout_s=timeout_s).ok)
-    return float(np.mean(hit))
-
-
-def union_exceedance(merged_ok: np.ndarray, parent_ok: list[np.ndarray]) -> float:
-    """Fraction of all items that the merge solves and *no* parent solves (super-linear signature)."""
-    any_parent = np.zeros_like(merged_ok)
-    for p in parent_ok:
-        any_parent |= p
-    return float(np.mean(merged_ok & ~any_parent))
-
-
-def mean_logprob(model, tok, prompts: list[str], completions: list[str], *,
-                 batch_size: int = 8, device: str = "cuda") -> np.ndarray:
-    """Mean token log-probability of each completion under the model, teacher-forced.
-
-    ``generate(..., output_scores=True)`` keeps one (batch × vocab) tensor per step, which at 320 new
-    tokens is gigabytes; one extra forward pass over prompt+completion costs a fraction of that and
-    gives the same quantity. Returns exp(mean logprob) ∈ (0, 1] — the same self-certainty scale the
-    epistasis module uses.
-    """
-    import torch
-
-    out = np.zeros(len(prompts))
-    for i in range(0, len(prompts), batch_size):
-        p_chunk, c_chunk = prompts[i:i + batch_size], completions[i:i + batch_size]
-        ids, labels = [], []
-        from .evaluate import format_prompt
-        for p, c in zip(p_chunk, c_chunk):
-            pi = tok(format_prompt(tok, p), add_special_tokens=False).input_ids
-            ci = tok(c if c.strip() else " ", add_special_tokens=False).input_ids[:512]
-            ids.append(pi + ci); labels.append([-100] * len(pi) + ci)
-        L = max(len(x) for x in ids)
-        pad = tok.pad_token_id
-        inp = torch.full((len(ids), L), pad, dtype=torch.long)
-        lab = torch.full((len(ids), L), -100, dtype=torch.long)
-        att = torch.zeros((len(ids), L), dtype=torch.long)
-        for b, (x, y) in enumerate(zip(ids, labels)):        # right-pad (scoring, not generation)
-            inp[b, :len(x)] = torch.tensor(x); lab[b, :len(y)] = torch.tensor(y)
-            att[b, :len(x)] = 1
-        with torch.no_grad():
-            logits = model(input_ids=inp.to(device), attention_mask=att.to(device)).logits[:, :-1]
-            tgt = lab[:, 1:].to(device)
-            mask = tgt != -100
-            # cross_entropy streams the log-partition internally: no float32 copy of the logits and
-            # no second tensor for log_softmax, which together were ~8 GB at batch 8 x 800 x 152k.
-            nll = torch.nn.functional.cross_entropy(
-                logits.reshape(-1, logits.size(-1)).float(),
-                tgt.reshape(-1).clamp(min=0), reduction="none").view(tgt.shape)
-            n = mask.sum(-1).clamp(min=1)
-            out[i:i + len(p_chunk)] = torch.exp(-(nll * mask).sum(-1) / n).cpu().numpy()
-            del logits, nll
-    return out
-
-
-def predicted_composition(q_math: np.ndarray, q_code: np.ndarray, rho: np.ndarray,
-                          observed0: float) -> np.ndarray:
-    """The framework's forecast Ĉ_t (prereg §2), one free scale fixed at generation 0.
-
-    Ĉ_t = c0 · q_math_t · q_code_t · (1 − rho_t)/(1 − rho_0),  c0 chosen so Ĉ_0 = observed_0.
-    """
-    q_math, q_code, rho = map(np.asarray, (q_math, q_code, rho))
-    shape = q_math * q_code * (1.0 - rho) / max(1e-9, 1.0 - rho[0])
-    c0 = observed0 / shape[0] if shape[0] > 1e-9 else 0.0
-    return c0 * shape
-
-
-# ------------------------------------------------------------------ the GPU loop
-
-
-def _state_path(root: Path, arm: str) -> Path:
-    return root / arm / "state.json"
-
-
-def run_compose(cfg: dict) -> pd.DataFrame:
-    """Run every configured arm; return tidy long-form rows (resumable per arm)."""
-    import torch
-
-    from .epistasis import delta_geometry
-    from .evaluate import generate
-    from .merge import load_specialists
-    from .specialise import train_lora_on_tasks
-
-    name, base, seed = cfg["experiment"], cfg["base_model"], int(cfg["seed"])
-    G = int(cfg.get("generations", 6))
-    arms = list(cfg.get("arms", ["dry", "grounded", "dry_linear"]))
-    n_hard = int(cfg.get("n_hard", 200))
-    n_hard_val = int(cfg.get("n_hard_val", 0))       # >0 enables directed merge-weight selection
-    n_gsm = int(cfg.get("n_gsm8k", 150))
-    n_mbpp = int(cfg.get("n_mbpp", 100))
-    n_probe = int(cfg.get("n_probe", 60))
-    k_inh = int(cfg.get("k_inherit", 300))
-    epochs = int(cfg.get("epochs", 3))
-    conf_gate = cfg.get("conf_gate")
-    conf_gate = None if conf_gate is None else float(conf_gate)
-    spec_train = int(cfg.get("spec_train", 1200))
-    spec_epochs = int(cfg.get("spec_epochs", 3))
-    max_new = int(cfg.get("max_new_tokens", 320))
-    tb = int(cfg.get("train_batch_size", 2))          # the causal-LM loss upcasts logits to fp32:
-    tlen = int(cfg.get("train_max_len", 448))         # memory ~ batch * len * vocab(152k) * 4 B
-    r, alpha = (int(cfg.get("lora", {}).get(k, v)) for k, v in (("r", 16), ("alpha", 32)))
-    root = Path(cfg.get("adapters_dir", "models/llm")) / "compose" / f"{name}_s{seed}"
-    out_dir = Path(cfg.get("output", {}).get("dir", f"results/{name}"))
-    out_dir.mkdir(parents=True, exist_ok=True)
-
-    # fixed evaluation sets (identical across arms, generations and seeds-within-a-config)
-    target_kind = str(cfg.get("target", "gsm_hard"))
-    hard = composed_target(target_kind, n_hard, 1000)
-    hard_prompts = [t.prompt for t in hard]
-    # Directed recombination (E10): candidate merge weights are screened on a DISJOINT validation
-    # split and only the winner is reported on the test split, so nothing is selected on the numbers
-    # we report. n_hard_val = 0 keeps the fixed 0.5/0.5 blend of the original design.
-    hard_val = composed_target(target_kind, n_hard_val, 5000, split="val") if n_hard_val else []
-    val_prompts = [t.prompt for t in hard_val]
-    weight_grid = [list(w) for w in cfg.get("merge_weights", [[0.5, 0.5]])]
-    gsm = gsm8k_probe(n_gsm, 2000)
-    mbpp = mbpp_probe(n_mbpp, 3000)
-    probes = probe_pool(n_probe, 4000)
-
-    def gen(model, tok, prompts, **kw):
-        return generate(model, tok, prompts, max_new_tokens=max_new,
-                        batch_size=int(cfg.get("batch_size", 16)), **kw)
-
-    # ---- founders: one specialist per lineage, cached and shared across arms
-    founders = {}
-    for lin in LINEAGES:
-        d = root / "founders" / lin
-        if not (d / "adapter_config.json").exists():
-            data = (math_train(spec_train, seed * 100 + 1, with_answers=True) if lin == "math"
-                    else code_train(spec_train, seed * 100 + 2, with_answers=True))
-            train_lora_on_tasks(base, data, str(d), epochs=spec_epochs, seed=seed * 100 + 7,
-                                r=r, alpha=alpha, batch_size=tb, max_len=tlen)
-        founders[lin] = str(d)
-
-    frames = []
-    for a_idx, arm in enumerate(arms):
-        # Operator per arm, explicit: the gen-0 sweep showed the blend ratio dominates the operator,
-        # so which operator an arm uses is a stated choice, not an inference from its name.
-        op = str(cfg.get("arm_ops", {}).get(arm, "linear" if arm.endswith("_linear") else "cat"))
-        g_frac = float(cfg.get("g", 0.10)) if arm.startswith("grounded") else 0.0
-        rows: list[dict] = []
-        parents, t0 = dict(founders), 0
-        st_path = _state_path(root, arm)
-        partial = out_dir / f"partial_{arm}_s{seed}.parquet"
-        if bool(cfg.get("resume", True)) and st_path.exists():
-            st = json.loads(st_path.read_text())
-            if all(Path(p, "adapter_config.json").exists() for p in st["parents"].values()):
-                parents, t0 = dict(st["parents"]), int(st["generation"])
-                if partial.exists():
-                    rows = pd.read_parquet(partial).to_dict("records")
-                print(f"[{arm}] resuming at generation {t0}", flush=True)
-
-        for t in range(t0, G + 1):
-            rng = np.random.default_rng([seed, a_idx, t])
-            tag = {"experiment": name, "arm": arm, "seed": seed, "generation": t, "operator": op}
-
-            model, tok = load_specialists(base, [parents["math"], parents["code"]])
-
-            # ---- own-skill retention q_t, and each parent alone on the composed target
-            model.set_adapter("a0")
-            q_math = score_gsm8k(gen(model, tok, [x.prompt for x in gsm]), gsm)
-            math_comp = gen(model, tok, hard_prompts)
-            _, math_ok, math_exec = score_composed(math_comp, hard)
-            probe_math = gen(model, tok, probes)
-            model.set_adapter("a1")
-            q_code = score_mbpp(gen(model, tok, [x["prompt"] for x in mbpp]), mbpp)
-            code_comp = gen(model, tok, hard_prompts)
-            _, code_ok, code_exec = score_composed(code_comp, hard)
-            probe_code = gen(model, tok, probes)
-
-            rho_behav = float(np.mean([_normalise(a) == _normalise(b)
-                                       for a, b in zip(probe_math, probe_code)]))
-            rho_geom = float(delta_geometry(parents["math"], parents["code"])["delta_cos"])
-
-            # ---- merge and measure the composition (optionally selecting the weights on val)
-            chosen = weight_grid[0]
-            if hard_val and len(weight_grid) > 1:
-                best_v = -np.inf
-                for wi, w in enumerate(weight_grid):
-                    cn = f"cand{wi}"
-                    model.add_weighted_adapter(["a0", "a1"], w, cn, combination_type=op)
-                    model.set_adapter(cn)
-                    v, _, _ = score_composed(gen(model, tok, val_prompts), hard_val)
-                    if v > best_v:
-                        best_v, chosen = v, w
-                    model.set_adapter("a0"); model.delete_adapter(cn)
-                rows.append({**tag, "metric": "chosen_weight_math", "value": float(chosen[0])})
-            model.add_weighted_adapter(["a0", "a1"], chosen, "merged", combination_type=op)
-            model.set_adapter("merged")
-            merged_comp = gen(model, tok, hard_prompts)
-            comp_acc, merged_ok, merged_exec = score_composed(merged_comp, hard)
-            model.set_adapter("a0"); model.delete_adapter("merged")
-
-            best_parent = max(float(math_ok.mean()), float(code_ok.mean()))
-            surplus = comp_acc - best_parent
-            uex = union_exceedance(merged_ok, [math_ok, code_ok])
-            for metric, value in (
-                ("q_math", q_math), ("q_code", q_code),
-                ("rho_behav", rho_behav), ("rho_geom", rho_geom),
-                ("composed_acc", comp_acc), ("composed_exec", merged_exec),
-                ("parent_math_composed", float(math_ok.mean())),
-                ("parent_code_composed", float(code_ok.mean())),
-                ("parent_math_exec", math_exec), ("parent_code_exec", code_exec),
-                ("best_parent_composed", best_parent), ("surplus", surplus),
-                ("union_exceedance", uex),
-            ):
-                rows.append({**tag, "metric": metric, "value": float(value)})
-            print(f"[{arm}] gen {t}/{G}: composed {comp_acc:.3f} (best parent {best_parent:.3f}, "
-                  f"surplus {surplus:+.3f}, uex {uex:.3f}) q_math {q_math:.2f} q_code {q_code:.2f} "
-                  f"rho {rho_behav:.2f}", flush=True)
-
-            if t == G:
-                del model; torch.cuda.empty_cache()
-                break
-
-            # ---- reproduce: each lineage distils from its own gated answers (+ g real examples)
-            new_parents = {}
-            for li, lin in enumerate(LINEAGES):
-                pool = (math_train(k_inh, seed * 9001 + t * 31 + li, with_answers=False)
-                        if lin == "math" else
-                        code_train(k_inh, seed * 9001 + t * 31 + li, with_answers=False))
-                model.set_adapter(f"a{li}")
-                pool_prompts = [x.prompt for x in pool]
-                answers = gen(model, tok, pool_prompts)
-                if conf_gate is None:
-                    keep = np.ones(len(pool), bool)
-                else:
-                    conf = mean_logprob(model, tok, pool_prompts, answers,
-                                        batch_size=int(cfg.get("score_batch_size", 8)))
-                    keep = conf >= conf_gate
-                data = [Task(lin, x.prompt, a.strip()[:512])
-                        for x, a, kp in zip(pool, answers, keep) if kp and a.strip()]
-                n_self = len(data)
-                if g_frac > 0:                    # immigration: verified real examples, fresh each gen
-                    n_real = max(1, int(round(g_frac / (1 - g_frac) * n_self)))
-                    real = (math_train(n_real, seed * 7717 + t * 13 + li, with_answers=True)
-                            if lin == "math" else
-                            code_train(n_real, seed * 7717 + t * 13 + li, with_answers=True))
-                    data += [x for x in real if x.answer.strip()]
-                rows.append({**tag, "metric": f"n_inherit_{lin}", "value": float(n_self)})
-                rows.append({**tag, "metric": f"n_real_{lin}", "value": float(len(data) - n_self)})
-                d = root / arm / f"gen{t + 1}" / lin
-                if len(data) >= 8:
-                    train_lora_on_tasks(base, data, str(d), epochs=epochs,
-                                        seed=seed * 31 + t * 7 + li, r=r, alpha=alpha,
-                                        batch_size=tb, max_len=tlen)
-                else:                              # terminal degeneration: inherit unchanged
-                    shutil.copytree(parents[lin], d, dirs_exist_ok=True)
-                    rows.append({**tag, "metric": f"degenerate_{lin}", "value": 1.0})
-                new_parents[lin] = str(d)
-            del model; torch.cuda.empty_cache()
-
-            for lin in LINEAGES:                   # disk hygiene: drop the superseded generation
-                if parents[lin] != founders[lin]:
-                    shutil.rmtree(parents[lin], ignore_errors=True)
-            parents = new_parents
-            st_path.parent.mkdir(parents=True, exist_ok=True)
-            st_path.write_text(json.dumps({"generation": t + 1, "parents": parents}))
-            pd.DataFrame(rows).to_parquet(partial, index=False)
-
-        frames.append(pd.DataFrame(rows))
-    return pd.concat(frames, ignore_index=True)
diff --git a/src/llm/compose_data.py b/src/llm/compose_data.py
deleted file mode 100644
index ceeedd3..0000000
--- a/src/llm/compose_data.py
+++ /dev/null
@@ -1,170 +0,0 @@
-"""Datasets and prompts for the composition experiment (prereg v3 §1.1).
-
-Two single-skill training corpora and three evaluation sets, all pinned to fixed subsets by seed so
-every generation sees *fresh* prompts from a *fixed* pool and no split ever leaks into another:
-
-* **math** lineage — trains on MetaMathQA (natural-language chain-of-thought); own-skill probe is
-  GSM8K test (numeric answer).
-* **code** lineage — trains on CodeAlpaca-20k (instruction → code); own-skill probe is MBPP
-  (sanitized) with execution against the reference tests.
-* **composed** target — GSM8k-Hard, program-aided: emit ``solution()``, execute, compare numerically.
-  Out-of-domain for both lineages, which is the point (LoRA Soups, COLING 2025).
-
-The composed prompt is fixed here rather than tuned per model, so the operator contrast is never
-confounded with prompt search.
-"""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-from functools import lru_cache
-
-import numpy as np
-
-from .tasks import Task
-
-COMPOSED_PROMPT = (
-    "{question}\n\n"
-    "Write a Python function `solution()` that takes no arguments and returns the numeric answer. "
-    "Reply with only the code, inside a ```python code block."
-)
-MATH_PROMPT = "{question}\n\nSolve this step by step, then give the final numeric answer after '####'."
-CODE_PROMPT = "{instruction}\n\nReply with only the Python code, inside a ```python code block."
-
-
-@dataclass(frozen=True)
-class ProgTask:
-    """A program-aided task: prompt, numeric target, and its source id."""
-
-    prompt: str
-    target: float
-    idx: int
-
-
-@lru_cache(maxsize=8)
-def _load(name: str, split: str, config: str | None = None):
-    from datasets import load_dataset
-    return load_dataset(name, config, split=split) if config else load_dataset(name, split=split)
-
-
-def _pick(n_total: int, n: int, seed: int) -> np.ndarray:
-    return np.random.default_rng(seed).choice(n_total, size=min(n, n_total), replace=False)
-
-
-def _split_pool(n_total: int, split: str) -> np.ndarray:
-    """Deterministic disjoint halves of an index pool.
-
-    The composed target's val split screens merge weights and the test split reports them; with a
-    pool as small as MATH-500's 271 usable items, sampling both by seed alone would overlap and leak
-    selection into the reported number. Splitting first makes the disjointness structural.
-    """
-    idx = np.random.default_rng(20260907).permutation(n_total)
-    cut = int(0.7 * n_total)
-    return idx[:cut] if split == "test" else idx[cut:]
-
-
-def gsm_hard(n: int, seed: int, split: str = "test") -> list[ProgTask]:
-    """The composed target: GSM8k-Hard, program-aided. ``split`` picks a disjoint val/test half."""
-    d = _load("reasoning-machines/gsm-hard", "train")
-    pool = _split_pool(len(d), split)
-    return [ProgTask(COMPOSED_PROMPT.format(question=d[int(pool[j])]["input"]),
-                     float(d[int(pool[j])]["target"]), int(pool[j]))
-            for j in _pick(len(pool), n, seed)]
-
-
-_FRAC = __import__("re").compile(r"-?\\d?frac\{(-?\d+)\}\{(-?\d+)\}")
-
-
-def _numeric_answer(a: str) -> float | None:
-    """Parse a MATH-500 answer to a float, or None if it is not a plain number/simple fraction."""
-    import re
-    a = (a.strip().replace("\\!", "").replace("{,}", "").replace(",", "")
-         .replace("\\%", "").replace("$", "").replace("\\dfrac", "\\frac"))
-    if re.fullmatch(r"-?\d+(\.\d+)?", a):
-        return float(a)
-    f = re.fullmatch(r"-?\\frac\{(-?\d+)\}\{(-?\d+)\}", a)
-    if f and int(f.group(2)) != 0:
-        v = int(f.group(1)) / int(f.group(2))
-        return -v if a.startswith("-") else v
-    return None
-
-
-def math500(n: int, seed: int, split: str = "test", min_level: int = 3) -> list[ProgTask]:
-    """Harder composed target: MATH-500 (competition maths), program-aided, numeric answers only.
-
-    The alternative target for when the base can already do the *reasoning* in GSM8k-Hard once code
-    removes the arithmetic burden (measured 2026-09-07: code-only 0.427 there). MetaMathQA is built
-    from GSM8K **and** MATH, so the math specialist is trained on exactly this reasoning while the
-    base is weak at it — which restores E8's premise that each parent supplies something scarce.
-    ``min_level`` filters MATH's 1–5 difficulty scale.
-    """
-    d = _load("HuggingFaceH4/MATH-500", "test")
-    pool = [(i, _numeric_answer(d[i]["answer"])) for i in range(len(d))]
-    pool = [(i, v) for i, v in pool if v is not None and int(d[i]["level"]) >= min_level]
-    half = _split_pool(len(pool), split)
-    idx = [int(half[j]) for j in _pick(len(half), n, seed)]
-    return [ProgTask(COMPOSED_PROMPT.format(question=d[pool[j][0]]["problem"]),
-                     float(pool[j][1]), int(pool[j][0])) for j in idx]
-
-
-def composed_target(kind: str, n: int, seed: int, split: str = "test", **kw) -> list[ProgTask]:
-    """Dispatch the composed target by name: ``gsm_hard`` (default) or ``math500``."""
-    return {"gsm_hard": gsm_hard, "math500": math500}[kind](n, seed, split=split, **kw)
-
-
-def gsm8k_probe(n: int, seed: int) -> list[Task]:
-    """Math own-skill probe: GSM8K test, answer after '####'."""
-    d = _load("openai/gsm8k", "test", "main")
-    out = []
-    for i in _pick(len(d), n, seed):
-        r = d[int(i)]
-        out.append(Task("math", MATH_PROMPT.format(question=r["question"]),
-                        r["answer"].split("####")[-1].strip().replace(",", "")))
-    return out
-
-
-def mbpp_probe(n: int, seed: int) -> list[dict]:
-    """Code own-skill probe: MBPP sanitized, with the reference asserts as the verifier."""
-    d = _load("google-research-datasets/mbpp", "test", "sanitized")
-    out = []
-    for i in _pick(len(d), n, seed):
-        r = d[int(i)]
-        out.append({"prompt": CODE_PROMPT.format(instruction=r["prompt"]),
-                    "tests": list(r["test_list"]), "imports": list(r.get("test_imports") or []),
-                    "idx": int(i)})
-    return out
-
-
-def math_train(n: int, seed: int, *, with_answers: bool) -> list[Task]:
-    """MetaMathQA examples. ``with_answers=False`` returns prompts only (self-consumption pool)."""
-    d = _load("meta-math/MetaMathQA", "train")
-    out = []
-    for i in _pick(len(d), n, seed):
-        r = d[int(i)]
-        # Reason: MetaMathQA responses end "The answer is: X"; rewrite to the '####' convention the
-        # probe grades on, so training format and evaluation format agree.
-        body = r["response"].strip()
-        ans = body.split("The answer is:")[-1].strip()
-        target = (body[:480] + f"\n#### {ans}") if with_answers else ""
-        out.append(Task("math", MATH_PROMPT.format(question=r["query"]), target))
-    return out
-
-
-def code_train(n: int, seed: int, *, with_answers: bool) -> list[Task]:
-    """CodeAlpaca examples (input-free subset, so the prompt is self-contained)."""
-    d = _load("sahil2801/CodeAlpaca-20k", "train")
-    idx = [int(i) for i in _pick(len(d), n * 3, seed) if not d[int(i)]["input"].strip()][:n]
-    return [Task("code", CODE_PROMPT.format(instruction=d[i]["instruction"]),
-                 d[i]["output"].strip()[:512] if with_answers else "") for i in idx]
-
-
-def probe_pool(n: int, seed: int) -> list[str]:
-    """Shared prompts both lineages answer, for the behavioural correlation rho_t (§1.6).
-
-    Half composed-task prompts, half a mix of each lineage's own domain — so rho reflects agreement
-    on ground both lineages actually walk on, not on prompts only one has ever seen.
-    """
-    half = n // 2
-    return ([t.prompt for t in gsm_hard(half, seed + 71)]
-            + [t.prompt for t in gsm8k_probe(half // 2, seed + 72)]
-            + [t.prompt for t in code_train(n - half - half // 2, seed + 73, with_answers=False)])
diff --git a/src/llm/experiment.py b/src/llm/experiment.py
index fddf9eb..7b7f480 100644
--- a/src/llm/experiment.py
+++ b/src/llm/experiment.py
@@ -268,36 +268,18 @@ def run_epistasis_dispatch(cfg: dict) -> pd.DataFrame:
     return run_epistasis_experiment(cfg)
 
 
-def run_society_dispatch(cfg: dict) -> pd.DataFrame:
-    from .society import run_society_experiment            # local import: torch-heavy
-    return run_society_experiment(cfg)
 
 
-def run_society_v2_dispatch(cfg: dict) -> pd.DataFrame:
-    from .society_v2 import run_society_v2                 # local import: torch-heavy
-    return run_society_v2(cfg)
-
-
-def run_compose_dispatch(cfg: dict) -> pd.DataFrame:
-    from .compose import run_compose                        # local import: torch-heavy
-    return run_compose(cfg)
-
 
 def run_curriculum_dispatch(cfg: dict) -> pd.DataFrame:
     from .curriculum import run_curriculum                  # local import: torch-heavy
     return run_curriculum(cfg)
 
 
-def run_calib_dispatch(cfg: dict) -> pd.DataFrame:
-    from .calibrate import run_calibration                 # local import: torch-heavy
-    return run_calibration(cfg)
-
 
 _RUNNERS = {"llm_merge": run_merge_experiment, "llm_moe": run_moe_experiment,
             "llm_directed": run_directed_experiment, "llm_speciation": run_speciation_dispatch,
-            "llm_epistasis": run_epistasis_dispatch, "llm_society": run_society_dispatch,
-            "llm_society_v2": run_society_v2_dispatch, "llm_society_calib": run_calib_dispatch,
-            "llm_compose": run_compose_dispatch,
+            "llm_epistasis": run_epistasis_dispatch,
             "llm_curriculum": run_curriculum_dispatch}
 
 
@@ -332,24 +314,11 @@ def run_and_save(config_path: str | Path) -> Path:
         extra["seeds"] = [int(s) for s in seeds]
     if kind == "llm_moe":
         extra["operators"] = list(cfg.get("operators", []))
-    if kind == "llm_society":
-        extra["society"] = {k: cfg.get(k) for k in
-                            ("agents", "generations", "arms", "g", "lam", "n_candidates")}
-    if kind == "llm_society_v2":
-        extra["society_v2"] = {k: cfg.get(k) for k in
-                               ("families", "agents", "generations", "arms", "g", "lam",
-                                "k_inherit", "epochs", "n_test", "n_val", "n_conf")}
-    if kind == "llm_compose":
-        extra["compose"] = {k: cfg.get(k) for k in
-                            ("arms", "generations", "g", "k_inherit", "conf_gate", "epochs",
-                             "n_hard", "n_gsm8k", "n_mbpp", "lora")}
     if kind == "llm_curriculum":
         extra["curriculum"] = {k: cfg.get(k) for k in
                                ("families", "lineages", "generations", "arms", "baselines",
                                 "n_new", "n_replay", "operator", "ancestor_depth", "lora",
                                 "allow_veto", "merge_until", "orders", "cull")}
-    if kind == "llm_society_calib":
-        extra["calibration"] = {"stage": cfg.get("stage"), "families": cfg.get("families")}
     if kind == "llm_directed":
         extra["directed"] = {"n_candidates": int(cfg.get("n_candidates", 16)),
                              "concentration": float(cfg.get("concentration", 0.5)),
diff --git a/src/llm/society.py b/src/llm/society.py
deleted file mode 100644
index 505c594..0000000
--- a/src/llm/society.py
+++ /dev/null
@@ -1,232 +0,0 @@
-"""The composed society at LLM scale (C3) — E11 re-instantiated in a population of LoRA agents.
-
-A population of `N` agents (LoRA adapters on a shared frozen base) evolves for `G` non-overlapping
-generations under the four operators the paper composes: grounded evaluation, directed recombination
-(sex), diversity-preserving selection, and retraining (mutation). The single grounding knob acts in
-the *evaluation* channel, exactly as in E11: selection scores each agent by
-``g * verifier_fitness + (1 - g) * conformity``, where conformity is agreement with the population's
-own modal answer. The inheritance channel is identical in every arm and deliberately ungrounded —
-each child is a fresh LoRA distilled from its source model's *own answers* (self-consumption made
-literal), so knowledge survives only through the data channel.
-
-Arms (four-arm ablation, mirroring E11): ``full`` / ``no_grounding`` (g=0; the verifier never enters
-that arm's loop — offspring screening also falls back to conformity) / ``no_sex`` (children are
-redistilled copies of selected parents) / ``no_diversity`` (plain top-P selection).
-
-Pure, testable pieces live at module top (consensus, conformity, behavioural distance,
-quality-diversity selection, complementary pairing); the GPU loop is
-:func:`run_society_experiment`. ``python -m llm.experiment configs/llm/society_smoke.yaml``.
-"""
-
-from __future__ import annotations
-
-import shutil
-from pathlib import Path
-
-import numpy as np
-import pandas as pd
-
-from .tasks import Task, make_tasks, _normalise
-
-
-# Pure operators live in society_ops (shared with the v2 loop); re-exported here so the v1 code
-# path and its tests are unchanged.
-from .society_ops import (  # noqa: E402,F401
-    answer_of as _answer_of, arm_settings as _arm_settings_v2, behavioural_distance,
-    complementary_pairs, conformity_scores, consensus_answers, fitness_of as _fitness,
-    select_parents,
-)
-
-
-def arm_settings(arm: str, g: float) -> dict:
-    """v1 four-arm switches (boolean ``sex``), resolved from the shared table."""
-    s = _arm_settings_v2(arm, g)
-    if s["sex"] == "linear":
-        raise ValueError("sex_linear is a v2 arm (kind: llm_society_v2)")
-    return {"g": s["g"], "sex": s["sex"] is not None, "diversity": s["diversity"]}
-
-
-# ---------------------------------------------------------------------------- the GPU loop
-
-
-def run_society_experiment(cfg: dict) -> pd.DataFrame:
-    """Run the society loop for every configured arm; return tidy long-form rows.
-
-    Config keys (with defaults): ``agents`` (6), ``generations`` (8), ``arms`` (all four),
-    ``g`` (0.5), ``lam`` (0.3), ``n_test``/``n_val``/``n_conf``/``n_inherit`` pool sizes,
-    ``n_candidates`` (6) offspring screened per pair, ``epochs`` (2) child SFT epochs,
-    ``spec_train``/``spec_epochs`` gen-0 specialist budget, ``hard`` (False),
-    ``keep_all_adapters`` (False).
-    """
-    import torch
-
-    from .evaluate import generate, load_model
-    from .merge import load_specialists
-    from .specialise import train_lora_on_tasks, train_specialist
-
-    name = cfg["experiment"]
-    base = cfg["base_model"]
-    fams = list(cfg.get("families", ["lists", "strings", "arith"]))
-    N = int(cfg.get("agents", 6))
-    G = int(cfg.get("generations", 8))
-    arms = list(cfg.get("arms", ["full", "no_grounding", "no_sex", "no_diversity"]))
-    g_val = float(cfg.get("g", 0.5))
-    lam = float(cfg.get("lam", 0.3))
-    n_test = int(cfg.get("n_test", 40))
-    n_val = int(cfg.get("n_val", 30))
-    n_conf = int(cfg.get("n_conf", 60))
-    n_inherit = int(cfg.get("n_inherit", 240))
-    n_cand = int(cfg.get("n_candidates", 6))
-    epochs = int(cfg.get("epochs", 2))
-    elitism = int(cfg.get("elitism", 0))
-    n_parents = int(cfg.get("n_parents", max(2, N // 2)))
-    spec_train = int(cfg.get("spec_train", 300))
-    spec_epochs = int(cfg.get("spec_epochs", 2))
-    hard = bool(cfg.get("hard", False))
-    keep_all = bool(cfg.get("keep_all_adapters", False))
-    seed = int(cfg["seed"])
-    lora = cfg.get("lora", {})
-    r, alpha = int(lora.get("r", 16)), int(lora.get("alpha", 32))
-    root = Path(cfg.get("adapters_dir", "models/llm")) / "society" / f"{name}_s{seed}"
-
-    # fixed pools: test (reporting only), val (grounded selection signal + offspring screening)
-    test = sum([make_tasks(f, n_test, seed=1000 + i, hard=hard) for i, f in enumerate(fams)], [])
-    val = sum([make_tasks(f, n_val, seed=3000 + i, hard=hard) for i, f in enumerate(fams)], [])
-    rng = np.random.default_rng(seed)
-
-    # gen-0 founders: light per-family specialists, shared across arms (same starting population)
-    founders = []
-    for i in range(N):
-        fam = fams[i % len(fams)]
-        d = root / "founders" / f"agent{i}_{fam}"
-        if not (d / "adapter_config.json").exists():
-            train_specialist(base, fam, str(d), n_train=spec_train, epochs=spec_epochs,
-                             seed=seed * 100 + i, hard=hard, r=r, alpha=alpha)
-        founders.append(str(d))
-
-    rows: list[dict] = []
-    for arm in arms:
-        s = arm_settings(arm, g_val)
-        agents = list(founders)
-        for t in range(G):
-            conf_pool = sum([make_tasks(f, n_conf // len(fams), seed=seed * 7919 + t * 13 + i,
-                                        hard=hard) for i, f in enumerate(fams)], [])
-            conf_prompts = [x.prompt for x in conf_pool]
-
-            # ---- produce & score: one base, all agents attached as adapters
-            model, tok = load_specialists(base, agents)
-            fit_test, fit_val, conf_outs = [], [], []
-            for i in range(N):
-                model.set_adapter(f"a{i}")
-                fit_test.append(_fitness(generate(model, tok, [x.prompt for x in test]), test, fams))
-                fit_val.append(_fitness(generate(model, tok, [x.prompt for x in val]), val, fams))
-                conf_outs.append(generate(model, tok, conf_prompts))
-            consensus = consensus_answers(conf_outs)
-            conf = conformity_scores(conf_outs, consensus)
-            dist = behavioural_distance(conf_outs)
-            cons_acc = float(np.mean([verify(c, x) for c, x in zip(consensus, conf_pool)]))
-            fitness = np.array([fv["overall"] for fv in fit_val])
-            scores = s["g"] * fitness + (1.0 - s["g"]) * conf
-
-            parents = select_parents(scores, dist, n_parents, diversity=s["diversity"], lam=lam)
-
-            # ---- rows (reporting uses the verifier in every arm; the loop does not)
-            for i in range(N):
-                base_row = {"experiment": name, "arm": arm, "seed": seed, "generation": t,
-                            "agent": i, "selected": i in parents,
-                            "conformity": float(conf[i]), "score": float(scores[i])}
-                for k, v in fit_test[i].items():
-                    rows.append({**base_row, "metric": f"test_{k}", "value": v})
-                rows.append({**base_row, "metric": "val_overall", "value": float(fitness[i])})
-            rows.append({"experiment": name, "arm": arm, "seed": seed, "generation": t,
-                         "agent": -1, "selected": False, "conformity": float("nan"),
-                         "score": float("nan"), "metric": "consensus_acc", "value": cons_acc})
-            rows.append({"experiment": name, "arm": arm, "seed": seed, "generation": t,
-                         "agent": -1, "selected": False, "conformity": float("nan"),
-                         "score": float("nan"), "metric": "diversity_behav",
-                         "value": float(dist[np.triu_indices(N, 1)].mean())})
-
-            if t == G - 1:
-                del model
-                torch.cuda.empty_cache()
-                break
-
-            # ---- breed: pick each child's source (merged offspring, or a copied parent)
-            inherit_pool = sum([make_tasks(f, n_inherit // len(fams),
-                                           seed=seed * 104729 + t * 17 + i, hard=hard)
-                                for i, f in enumerate(fams)], [])
-            n_bred = N - elitism
-            child_sources: list[dict] = []                  # per child: parent names + weights
-            if s["sex"]:
-                from .directed import sample_merge_weights
-                pairs = complementary_pairs(parents, dist, n_bred)
-                for c, (pa, pb) in enumerate(pairs):
-                    w = sample_merge_weights(2, n_cand, rng)
-                    best_i, best_v = 0, -np.inf
-                    for ci in range(n_cand):
-                        cname = f"g{t}c{c}k{ci}"
-                        model.add_weighted_adapter([f"a{pa}", f"a{pb}"], w[ci].tolist(), cname,
-                                                   combination_type="linear")
-                        model.set_adapter(cname)
-                        if s["g"] > 0:                      # grounded screening: verifier on val
-                            v = _fitness(generate(model, tok, [x.prompt for x in val]),
-                                         val, fams)["overall"]
-                        else:                               # ungrounded screening: conformity only
-                            outs_c = generate(model, tok, conf_prompts)
-                            v = float(np.mean([_normalise(o) == cc
-                                               for o, cc in zip(outs_c, consensus)]))
-                        if v > best_v:
-                            best_i, best_v = ci, v
-                        model.set_adapter(f"a{pa}")         # never delete the active adapter
-                        model.delete_adapter(cname)
-                    cname = f"g{t}c{c}win"
-                    model.add_weighted_adapter([f"a{pa}", f"a{pb}"], w[best_i].tolist(), cname,
-                                               combination_type="linear")
-                    model.set_adapter(cname)
-                    answers = generate(model, tok, [x.prompt for x in inherit_pool])
-                    model.set_adapter(f"a{pa}")
-                    model.delete_adapter(cname)
-                    child_sources.append({"parents": (pa, pb), "answers": answers})
-            else:
-                for c in range(n_bred):
-                    p = parents[c % len(parents)]
-                    model.set_adapter(f"a{p}")
-                    answers = generate(model, tok, [x.prompt for x in inherit_pool])
-                    child_sources.append({"parents": (p, p), "answers": answers})
-            del model
-            torch.cuda.empty_cache()
-
-            # ---- reproduce: fresh LoRA per child, trained on its source's own answers.
-            # Elites (overlapping generations): the top-scoring parents survive as unmodified
-            # copies, applied identically in every arm — reproduction here is lossy distillation,
-            # so without a survivor the mutation load erases the champion each generation.
-            new_agents = []
-            for e in range(elitism):
-                d = root / arm / f"gen{t + 1}" / f"elite{e}"
-                shutil.copytree(agents[parents[e]], d, dirs_exist_ok=True)
-                new_agents.append(str(d))
-                rows.append({"experiment": name, "arm": arm, "seed": seed, "generation": t + 1,
-                             "agent": e, "selected": False, "conformity": float("nan"),
-                             "score": float("nan"), "metric": "parents",
-                             "value": float(parents[e] * 100 + parents[e])})
-            for c, src in enumerate(child_sources, start=elitism):
-                data = [Task(x.family, x.prompt, _answer_of(a))
-                        for x, a in zip(inherit_pool, src["answers"]) if _answer_of(a)]
-                d = root / arm / f"gen{t + 1}" / f"agent{c}"
-                if len(data) >= 8:
-                    train_lora_on_tasks(base, data, str(d), epochs=epochs,
-                                        seed=seed * 31 + t * N + c, r=r, alpha=alpha)
-                else:                                       # terminal degeneration: copy the source
-                    # Reason: a fully-degenerate source emits no usable answers; crashing would kill
-                    # a long sweep (cf. the neural terminal-collapse sentinel) — inherit unchanged.
-                    shutil.copytree(agents[src["parents"][0]], d, dirs_exist_ok=True)
-                new_agents.append(str(d))
-                rows.append({"experiment": name, "arm": arm, "seed": seed, "generation": t + 1,
-                             "agent": c, "selected": False, "conformity": float("nan"),
-                             "score": float("nan"), "metric": "parents",
-                             "value": float(src["parents"][0] * 100 + src["parents"][1])})
-            if not keep_all and t > 0:                      # disk hygiene: drop generation t
-                for d in agents:                            # (founders at t=0 are kept — shared)
-                    shutil.rmtree(d, ignore_errors=True)
-            agents = new_agents
-    return pd.DataFrame(rows)
diff --git a/src/llm/society_ops.py b/src/llm/society_ops.py
deleted file mode 100644
index b9858d7..0000000
--- a/src/llm/society_ops.py
+++ /dev/null
@@ -1,209 +0,0 @@
-"""Pure, testable operators for the LLM society (v1 and v2 — see ``tasks/prereg-llm-society-v2.md``).
-
-Everything here is NumPy on strings and floats; nothing touches a model. The v2 additions are the
-E11-faithful pieces the v1 design lacked:
-
-* :func:`pooled_survival` — selection acts on *survival over parents + offspring*, keeping the top N
-  by ``score + λ·novelty`` (E11's rule), instead of truncating parents before breeding, which in v1
-  discarded half the families at generation 1 with no operator able to restore them (E6).
-* :func:`mating_plan` — complementary pairing over the whole population with a per-agent use cap, so
-  every founder's family can reach the next generation.
-* :func:`route_union` — the union-preserving recombination operator in the *inheritance data*: per
-  prompt, the child learns the answer of the more confident parent. This is E4's ``max`` per item,
-  verifier-free (legal in ``no_grounding``), and directed in E10's sense.
-* :func:`choose_single_parent` — score-proportional parent sampling for the ``no_sex`` arm, so that
-  arm still has selection without recombination.
-"""
-
-from __future__ import annotations
-
-import numpy as np
-
-from .tasks import Task, _normalise, verify
-
-
-# ------------------------------------------------------------------ population readouts (v1)
-
-
-def consensus_answers(outputs: list[list[str]]) -> list[str]:
-    """The population's modal (normalised) answer per prompt; ties broken lexicographically."""
-    n_prompts = len(outputs[0])
-    cons = []
-    for p in range(n_prompts):
-        votes: dict[str, int] = {}
-        for out in outputs:
-            key = _normalise(out[p])
-            votes[key] = votes.get(key, 0) + 1
-        top = max(votes.values())
-        cons.append(min(k for k, v in votes.items() if v == top))
-    return cons
-
-
-def conformity_scores(outputs: list[list[str]], consensus: list[str]) -> np.ndarray:
-    """Each agent's agreement rate with the population consensus (the E11 conformity signal)."""
-    return np.array([np.mean([_normalise(o) == c for o, c in zip(out, consensus)])
-                     for out in outputs], dtype=float)
-
-
-def behavioural_distance(outputs: list[list[str]]) -> np.ndarray:
-    """Pairwise disagreement rate between agents' normalised answers (verifier-free diversity)."""
-    n = len(outputs)
-    norm = [[_normalise(o) for o in out] for out in outputs]
-    d = np.zeros((n, n))
-    for i in range(n):
-        for j in range(i + 1, n):
-            d[i, j] = d[j, i] = float(np.mean([a != b for a, b in zip(norm[i], norm[j])]))
-    return d
-
-
-def novelty(dist: np.ndarray) -> np.ndarray:
-    """Per-agent mean behavioural distance to the rest of the pool (E11's ``_novelty``)."""
-    n = dist.shape[0]
-    if n < 2:
-        return np.zeros(n)
-    return dist.sum(axis=1) / (n - 1)
-
-
-# ------------------------------------------------------------------ selection
-
-
-def select_parents(scores: np.ndarray, dist: np.ndarray, k: int, *, diversity: bool,
-                   lam: float = 0.3) -> list[int]:
-    """v1 parent truncation (kept for the v1 code path and its tests): greedy QD or plain top-k."""
-    if not diversity:
-        return list(np.argsort(-scores, kind="stable")[:k])
-    chosen = [int(np.argmax(scores))]
-    while len(chosen) < k:
-        best_i, best_v = -1, -np.inf
-        for i in range(len(scores)):
-            if i in chosen:
-                continue
-            v = scores[i] + lam * float(np.mean([dist[i, j] for j in chosen]))
-            if v > best_v:
-                best_i, best_v = i, v
-        chosen.append(best_i)
-    return chosen
-
-
-def pooled_survival(scores: np.ndarray, dist: np.ndarray, n_keep: int, *, lam: float) -> list[int]:
-    """E11 survival: keep the top ``n_keep`` of the pool by ``score + lam * novelty``.
-
-    ``lam = 0`` is the greedy (``no_diversity``) arm. Parents and children compete on equal terms, so
-    a strong parent survives by out-scoring its children (elitism is emergent, not a knob) and no
-    lineage is excluded before it has bred.
-
-    Args:
-        scores (np.ndarray): per-member selection score over the pooled parents + children.
-        dist (np.ndarray): pairwise behavioural distance over the pool.
-        n_keep (int): population size to keep.
-        lam (float): novelty weight (E11 ``novelty``).
-
-    Returns:
-        list[int]: indices into the pool, best merit first (deterministic).
-    """
-    merit = np.asarray(scores, dtype=float) + lam * novelty(dist)
-    order = np.argsort(-merit, kind="stable")
-    return [int(i) for i in order[:n_keep]]
-
-
-def choose_single_parent(scores: np.ndarray, rng: np.random.Generator) -> int:
-    """Score-proportional parent sampling (``no_sex``): selection without recombination."""
-    s = np.asarray(scores, dtype=float)
-    w = s - s.min() + 1e-6
-    return int(rng.choice(len(s), p=w / w.sum()))
-
-
-# ------------------------------------------------------------------ mating and recombination
-
-
-def mating_plan(dist: np.ndarray, n_pairs: int, *, max_use: int = 2) -> list[tuple[int, int]]:
-    """Complementary pairing over the whole population with a per-agent use cap.
-
-    Pairs are taken in descending behavioural distance (directed mate choice, E10) subject to each
-    agent appearing in at most ``max_use`` pairs, so no single agent monopolises reproduction and
-    every agent's knowledge has a route to the next generation. If the cap exhausts the candidates
-    before ``n_pairs`` is reached, the remaining slots cycle the most-distant pairs (degenerate but
-    never empty).
-    """
-    n = dist.shape[0]
-    cands = sorted(((i, j) for i in range(n) for j in range(i + 1, n)), key=lambda ij: -dist[ij])
-    if not cands:
-        return [(0, 0)] * n_pairs
-    use = np.zeros(n, dtype=int)
-    plan: list[tuple[int, int]] = []
-    for i, j in cands:
-        if len(plan) >= n_pairs:
-            break
-        if use[i] < max_use and use[j] < max_use:
-            plan.append((i, j)); use[i] += 1; use[j] += 1
-    k = 0
-    while len(plan) < n_pairs:                              # cap exhausted: cycle the best pairs
-        plan.append(cands[k % len(cands)]); k += 1
-    return plan
-
-
-def route_union(ans_a: list[str], conf_a: np.ndarray, ans_b: list[str],
-                conf_b: np.ndarray) -> tuple[list[str], np.ndarray]:
-    """Union-preserving recombination of two parents' answer sets: per prompt, the more confident wins.
-
-    Confidence is the parent's own self-certainty (exp mean token log-prob) — no verifier. Ties go to
-    parent A (deterministic).
-
-    Returns:
-        (answers, source): the child's inheritance answers and a 0/1 array naming the parent per
-        prompt (for the H6 diagnostic of *where* a skill is lost).
-    """
-    src = (np.asarray(conf_b) > np.asarray(conf_a)).astype(int)
-    out = [b if s else a for a, b, s in zip(ans_a, ans_b, src)]
-    return out, src
-
-
-def complementary_pairs(parents: list[int], dist: np.ndarray, n_children: int) -> list[tuple[int, int]]:
-    """v1 mating plan over a parent subset (kept for the v1 code path and its tests)."""
-    pairs = sorted(((a, b) for i, a in enumerate(parents) for b in parents[i + 1:]),
-                   key=lambda ab: -dist[ab[0], ab[1]])
-    if not pairs:
-        pairs = [(parents[0], parents[0])]
-    return [pairs[i % len(pairs)] for i in range(n_children)]
-
-
-# ------------------------------------------------------------------ arms and readouts
-
-
-def arm_settings(arm: str, g: float) -> dict:
-    """Resolve an arm name to its operator switches.
-
-    ``sex`` is ``"union"`` (confidence-routed union inheritance, the v2 default), ``"linear"`` (v1's
-    screened 2-parent LoRA blend — the H2 control arm ``sex_linear``), or ``None``.
-    """
-    table = {
-        "full":         {"g": g,   "sex": "union",  "diversity": True},
-        "no_grounding": {"g": 0.0, "sex": "union",  "diversity": True},
-        "no_sex":       {"g": g,   "sex": None,     "diversity": True},
-        "no_diversity": {"g": g,   "sex": "union",  "diversity": False},
-        "sex_linear":   {"g": g,   "sex": "linear", "diversity": True},
-    }
-    if arm not in table:
-        raise ValueError(f"unknown arm {arm!r} (expected one of {list(table)})")
-    return dict(table[arm])
-
-
-def fitness_of(outs: list[str], tasks: list[Task], fams: list[str]) -> dict:
-    """Overall / per-family / worst-family verifier accuracy from precomputed outputs."""
-    corr = np.array([verify(o, t) for o, t in zip(outs, tasks)])
-    fam = np.array([t.family for t in tasks])
-    acc = {"overall": float(corr.mean())}
-    acc.update({f: float(corr[fam == f].mean()) if (fam == f).any() else float("nan") for f in fams})
-    acc["worst_family"] = min(acc[f] for f in fams)
-    return acc
-
-
-def families_alive(per_family_acc: list[dict], fams: list[str], threshold: float = 0.6) -> int:
-    """Number of families on which at least one agent is competent (≥ ``threshold``) — the count of
-    'alleles' still present in the population; loss is permanent (E6)."""
-    return int(sum(any(a.get(f, 0.0) >= threshold for a in per_family_acc) for f in fams))
-
-
-def answer_of(raw: str) -> str:
-    """Trim a raw completion to a single short answer line for the inheritance data."""
-    return raw.strip().split("\n")[0][:64].strip()
diff --git a/src/llm/society_v2.py b/src/llm/society_v2.py
deleted file mode 100644
index a3144cb..0000000
--- a/src/llm/society_v2.py
+++ /dev/null
@@ -1,319 +0,0 @@
-"""The composed society at LLM scale, v2 — E11 re-instantiated faithfully (``kind: llm_society_v2``).
-
-Pre-registered in ``tasks/prereg-llm-society-v2.md``; this module is its §3. What changed from v1 and
-why is in that document's §1. In one paragraph: ``L`` disjoint task families and **one founder per
-family** (ρ = 0 by construction); recombination is a **confidence-routed union** of two parents'
-inheritance answers (E4's ``max`` per item, verifier-free) rather than a linear LoRA blend; selection
-acts on **survival over the pooled parents + children** by ``score + λ·novelty`` (E11's rule) rather
-than on breeding eligibility; the run **checkpoints every generation** and resumes.
-
-One generation (§3.4): produce & score the population → mating plan by complementarity → each child's
-inheritance data from its parents' own answers (union / linear / single) → source diagnostics →
-train N fresh LoRAs → score the children against the *parents'* consensus → keep the top N of the
-2N pool → checkpoint.
-
-Row schema (long form): ``experiment, arm, seed, generation, agent, role, parents, selected,
-conformity, score, metric, value``. ``role`` ∈ {population, child, child_source, summary}.
-"""
-
-from __future__ import annotations
-
-import json
-import shutil
-from pathlib import Path
-
-import numpy as np
-import pandas as pd
-
-from . import families as _families  # noqa: F401  (registers the extra task families)
-from .society_ops import (answer_of, arm_settings, behavioural_distance, choose_single_parent,
-                          conformity_scores, consensus_answers, families_alive, fitness_of,
-                          mating_plan, novelty, pooled_survival, route_union)
-from .tasks import Task, make_tasks, verify, _normalise
-
-
-def _pool(fams: list[str], per_family: int, seed: int, hard: bool) -> list[Task]:
-    return sum([make_tasks(f, per_family, seed=seed + i, hard=hard) for i, f in enumerate(fams)], [])
-
-
-def _row(base: dict, **kw) -> dict:
-    r = dict(base); r.update(kw); return r
-
-
-def _train_founder_locked(d: Path, train, timeout_s: int = 3600) -> None:
-    """Train the founder at ``d`` exactly once across concurrent jobs (lock file, O_EXCL)."""
-    import os, time
-    done = d / "adapter_config.json"
-    if done.exists():
-        return
-    d.parent.mkdir(parents=True, exist_ok=True)
-    lock = d.parent / (d.name + ".lock")
-    try:
-        fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
-    except FileExistsError:
-        t0 = time.time()
-        while not done.exists():                        # another job is training it
-            if time.time() - t0 > timeout_s:
-                raise TimeoutError(f"waited {timeout_s}s for founder {d}")
-            time.sleep(15)
-        return
-    try:
-        os.write(fd, str(os.getpid()).encode()); os.close(fd)
-        train()
-    finally:
-        lock.unlink(missing_ok=True)
-
-
-class _State:
-    """Per-arm checkpoint: the current population's adapter dirs and the generation reached."""
-
-    def __init__(self, path: Path):
-        self.path = path
-
-    def load(self) -> dict | None:
-        return json.loads(self.path.read_text()) if self.path.exists() else None
-
-    def save(self, generation: int, agents: list[str]) -> None:
-        self.path.parent.mkdir(parents=True, exist_ok=True)
-        self.path.write_text(json.dumps({"generation": generation, "agents": agents}))
-
-
-def run_society_v2(cfg: dict) -> pd.DataFrame:
-    """Run every configured arm of the v2 society; return tidy rows (resumable per arm)."""
-    import torch
-
-    from .epistasis import generate_with_confidence
-    from .evaluate import generate
-    from .merge import load_specialists
-    from .specialise import train_lora_on_tasks, train_specialist
-
-    name = cfg["experiment"]
-    base = cfg["base_model"]
-    fams = list(cfg["families"])
-    L = len(fams)
-    N = int(cfg.get("agents", L))
-    G = int(cfg.get("generations", 12))
-    arms = list(cfg.get("arms", ["full", "no_grounding", "no_sex", "no_diversity"]))
-    g_val = float(cfg.get("g", 0.85))
-    lam = float(cfg.get("lam", 0.3))
-    n_test = int(cfg.get("n_test", 20))            # per family
-    n_val = int(cfg.get("n_val", 10))              # per family
-    n_conf = int(cfg.get("n_conf", 10))            # per family, fresh each generation
-    k_inh = int(cfg.get("k_inherit", 100))         # per family, fresh each generation (gate C2)
-    epochs = int(cfg.get("epochs", 3))
-    n_cand = int(cfg.get("n_candidates", 6))       # sex_linear only
-    spec_train = int(cfg.get("spec_train", 600))
-    spec_epochs = int(cfg.get("spec_epochs", 3))
-    hard = bool(cfg.get("hard", False))
-    resume = bool(cfg.get("resume", True))
-    seed = int(cfg["seed"])
-    lora = cfg.get("lora", {})
-    r, alpha = int(lora.get("r", 16)), int(lora.get("alpha", 32))
-    root = Path(cfg.get("adapters_dir", "models/llm")) / "society_v2" / f"{name}_s{seed}"
-    out_dir = Path(cfg.get("output", {}).get("dir", f"results/{name}"))
-    out_dir.mkdir(parents=True, exist_ok=True)
-    max_use = int(cfg.get("max_mate_use", 2))
-    # Confidence-gated inheritance (prereg §4a, C2b): a child learns only the prompts its source is
-    # confident on (exp mean token log-prob ≥ conf_gate). Verifier-free; identical in every arm;
-    # None = ungated (the pre-registered v2 default, which C2 showed loses 20-40%/generation).
-    conf_gate = cfg.get("conf_gate")
-    conf_gate = None if conf_gate is None else float(conf_gate)
-
-    test = _pool(fams, n_test, 1000, hard)
-    val = _pool(fams, n_val, 3000, hard)
-
-    # ---- founders: one specialist per family (agent i -> family i mod L), cached and shared by arms.
-    # Arm-jobs of one seed may start together on the cluster: an O_EXCL lock makes the first train
-    # and the rest wait on the finished adapter, so no two jobs write the same founder.
-    founders = []
-    for i in range(N):
-        fam = fams[i % L]
-        d = root / "founders" / f"agent{i}_{fam}"
-        _train_founder_locked(d, lambda d=d, fam=fam, i=i: train_specialist(
-            base, fam, str(d), n_train=spec_train, epochs=spec_epochs, seed=seed * 100 + i,
-            hard=hard, r=r, alpha=alpha))
-        founders.append(str(d))
-
-    frames: list[pd.DataFrame] = []
-    for a_idx, arm in enumerate(arms):
-        s = arm_settings(arm, g_val)
-        state = _State(root / arm / "state.json")
-        partial = out_dir / f"partial_{arm}_s{seed}.parquet"
-        rows: list[dict] = []
-        agents, t0 = list(founders), 0
-        st = state.load() if resume else None
-        if st and all(Path(d, "adapter_config.json").exists() for d in st["agents"]):
-            agents, t0 = list(st["agents"]), int(st["generation"])
-            if partial.exists():
-                rows = pd.read_parquet(partial).to_dict("records")
-            print(f"[{arm}] resuming at generation {t0}")
-
-        for t in range(t0, G + 1):
-            rng = np.random.default_rng([seed, a_idx, t])          # resume-safe per-generation RNG
-            conf_pool = _pool(fams, n_conf, seed * 7919 + t * 13, hard)
-            conf_prompts = [x.prompt for x in conf_pool]
-            tag = {"experiment": name, "arm": arm, "seed": seed, "generation": t}
-
-            # ---- (1) produce & score the population
-            model, tok = load_specialists(base, agents)
-            fit_test, fit_val, conf_outs = [], [], []
-            for i in range(N):
-                model.set_adapter(f"a{i}")
-                fit_test.append(fitness_of(generate(model, tok, [x.prompt for x in test]), test, fams))
-                fit_val.append(fitness_of(generate(model, tok, [x.prompt for x in val]), val, fams))
-                conf_outs.append(generate(model, tok, conf_prompts))
-            consensus = consensus_answers(conf_outs)
-            conf = conformity_scores(conf_outs, consensus)
-            dist = behavioural_distance(conf_outs)
-            nov = novelty(dist)
-            cons_acc = float(np.mean([verify(c, x) for c, x in zip(consensus, conf_pool)]))
-            fitness = np.array([fv["overall"] for fv in fit_val])
-            scores = s["g"] * fitness + (1.0 - s["g"]) * conf
-
-            for i in range(N):
-                b = _row(tag, agent=i, role="population", parents=Path(agents[i]).name,
-                         selected=True, conformity=float(conf[i]), score=float(scores[i]))
-                for k, v in fit_test[i].items():
-                    rows.append(_row(b, metric=f"test_{k}", value=v))
-                rows.append(_row(b, metric="val_overall", value=float(fitness[i])))
-                rows.append(_row(b, metric="novelty", value=float(nov[i])))
-            summ = _row(tag, agent=-1, role="summary", parents="", selected=False,
-                        conformity=float("nan"), score=float("nan"))
-            rows.append(_row(summ, metric="consensus_acc", value=cons_acc))
-            rows.append(_row(summ, metric="diversity_behav", value=float(nov.mean())))
-            rows.append(_row(summ, metric="families_alive", value=float(families_alive(fit_test, fams))))
-            rows.append(_row(summ, metric="gap_conformity_minus_truth",
-                             value=float(conf.mean() - np.mean([f["overall"] for f in fit_test]))))
-            if t == G:
-                del model; torch.cuda.empty_cache()
-                break
-
-            # ---- (2)-(3) mating plan and each child's inheritance data
-            inherit = _pool(fams, k_inh, seed * 104729 + t * 17, hard)
-            inh_prompts = [x.prompt for x in inherit]
-            child_src: list[dict] = []
-            if s["sex"] == "union":
-                plan = mating_plan(dist, N, max_use=max_use)
-                needed = sorted({p for ab in plan for p in ab})
-                ans, cf = {}, {}
-                for p in needed:                       # each parent answers the pool once
-                    model.set_adapter(f"a{p}")
-                    ans[p], cf[p] = generate_with_confidence(model, tok, inh_prompts)
-                for pa, pb in plan:
-                    routed, src = route_union(ans[pa], cf[pa], ans[pb], cf[pb])
-                    keep = np.maximum(cf[pa], cf[pb]) >= conf_gate if conf_gate is not None else None
-                    child_src.append({"parents": (pa, pb), "answers": routed,
-                                      "share_b": float(src.mean()), "keep": keep})
-            elif s["sex"] == "linear":
-                from .directed import sample_merge_weights
-                plan = mating_plan(dist, N, max_use=max_use)
-                for c, (pa, pb) in enumerate(plan):
-                    w = sample_merge_weights(2, n_cand, rng)
-                    best_i, best_v = 0, -np.inf
-                    for ci in range(n_cand):
-                        cname = f"g{t}c{c}k{ci}"
-                        model.add_weighted_adapter([f"a{pa}", f"a{pb}"], w[ci].tolist(), cname,
-                                                   combination_type="linear")
-                        model.set_adapter(cname)
-                        if s["g"] > 0:
-                            v = fitness_of(generate(model, tok, [x.prompt for x in val]), val,
-                                           fams)["overall"]
-                        else:
-                            oc = generate(model, tok, conf_prompts)
-                            v = float(np.mean([_normalise(o) == cc for o, cc in zip(oc, consensus)]))
-                        if v > best_v:
-                            best_i, best_v = ci, v
-                        model.set_adapter(f"a{pa}"); model.delete_adapter(cname)
-                    cname = f"g{t}c{c}win"
-                    model.add_weighted_adapter([f"a{pa}", f"a{pb}"], w[best_i].tolist(), cname,
-                                               combination_type="linear")
-                    model.set_adapter(cname)
-                    answers = generate(model, tok, inh_prompts)
-                    model.set_adapter(f"a{pa}"); model.delete_adapter(cname)
-                    child_src.append({"parents": (pa, pb), "answers": answers, "share_b": float("nan")})
-            else:                                       # no_sex: single parent, score-proportional
-                picks = [choose_single_parent(scores, rng) for _ in range(N)]
-                ans, cf = {}, {}
-                for p in sorted(set(picks)):
-                    model.set_adapter(f"a{p}")
-                    ans[p], cf[p] = generate_with_confidence(model, tok, inh_prompts)
-                for p in picks:
-                    keep = cf[p] >= conf_gate if conf_gate is not None else None
-                    child_src.append({"parents": (p, p), "answers": ans[p], "share_b": float("nan"),
-                                      "keep": keep})
-
-            # ---- source diagnostics (H6): what each child was *supplied*, per family
-            for c, src in enumerate(child_src):
-                supplied = fitness_of(src["answers"], inherit, fams)
-                b = _row(tag, agent=c, role="child_source", parents=f"{src['parents'][0]}+{src['parents'][1]}",
-                         selected=False, conformity=float("nan"), score=float("nan"))
-                for k, v in supplied.items():
-                    rows.append(_row(b, metric=f"source_{k}", value=v))
-                rows.append(_row(b, metric="source_share_b", value=src["share_b"]))
-            del model; torch.cuda.empty_cache()
-
-            # ---- (4) inherit: a fresh LoRA per child on its source's own answers
-            children, degenerate = [], 0
-            for c, src in enumerate(child_src):
-                keep = src.get("keep")
-                if keep is None:
-                    keep = np.ones(len(inherit), dtype=bool)
-                data = [Task(x.family, x.prompt, answer_of(a))
-                        for x, a, kp in zip(inherit, src["answers"], keep) if kp and answer_of(a)]
-                rows.append(_row(tag, agent=c, role="child_source", parents=f"{src['parents'][0]}+{src['parents'][1]}",
-                                 selected=False, conformity=float("nan"), score=float("nan"),
-                                 metric="n_inherit_kept", value=float(len(data))))
-                d = root / arm / f"gen{t + 1}" / f"child{c}"
-                if len(data) >= 8:
-                    train_lora_on_tasks(base, data, str(d), epochs=epochs,
-                                        seed=seed * 31 + t * N + c, r=r, alpha=alpha)
-                else:                                   # terminal degeneration: inherit unchanged
-                    shutil.copytree(agents[src["parents"][0]], d, dirs_exist_ok=True); degenerate += 1
-                children.append(str(d))
-
-            # ---- (5) survive: score children against the PARENTS' consensus; keep top N of 2N
-            model, tok = load_specialists(base, agents + children)
-            c_test, c_val, c_conf = [], [], []
-            for j in range(N):
-                model.set_adapter(f"a{N + j}")
-                c_test.append(fitness_of(generate(model, tok, [x.prompt for x in test]), test, fams))
-                c_val.append(fitness_of(generate(model, tok, [x.prompt for x in val]), val, fams))
-                c_conf.append(generate(model, tok, conf_prompts))
-            del model; torch.cuda.empty_cache()
-            conf_c = conformity_scores(c_conf, consensus)
-            fit_c = np.array([fv["overall"] for fv in c_val])
-            scores_c = s["g"] * fit_c + (1.0 - s["g"]) * conf_c
-            pool_scores = np.concatenate([scores, scores_c])
-            pool_dist = behavioural_distance(conf_outs + c_conf)
-            keep = pooled_survival(pool_scores, pool_dist, N, lam=lam if s["diversity"] else 0.0)
-            keep_set = set(keep)
-
-            for j in range(N):
-                b = _row(tag, agent=j, role="child",
-                         parents=f"{child_src[j]['parents'][0]}+{child_src[j]['parents'][1]}",
-                         selected=(N + j) in keep_set, conformity=float(conf_c[j]),
-                         score=float(scores_c[j]))
-                for k, v in c_test[j].items():
-                    rows.append(_row(b, metric=f"test_{k}", value=v))
-                rows.append(_row(b, metric="val_overall", value=float(fit_c[j])))
-            rows.append(_row(summ, metric="best_newborn_overall",
-                             value=float(max(f["overall"] for f in c_test))))
-            rows.append(_row(summ, metric="n_degenerate", value=float(degenerate)))
-            rows.append(_row(summ, metric="n_parents_survive",
-                             value=float(sum(1 for k in keep if k < N))))
-
-            pool_dirs = agents + children
-            survivors = [pool_dirs[k] for k in keep]
-            # disk hygiene: drop non-survivors (founders are shared across arms — keep them)
-            for k, d in enumerate(pool_dirs):
-                if k not in keep_set and not d.startswith(str(root / "founders")):
-                    shutil.rmtree(d, ignore_errors=True)
-            agents = survivors
-            state.save(t + 1, agents)
-            pd.DataFrame(rows).to_parquet(partial, index=False)
-            print(f"[{arm}] gen {t + 1}/{G}: best {max(f['overall'] for f in fit_test):.3f} "
-                  f"newborn {max(f['overall'] for f in c_test):.3f} cons {cons_acc:.2f} "
-                  f"alive {families_alive(fit_test, fams)} deg {degenerate}", flush=True)
-
-        frames.append(pd.DataFrame(rows))
-    return pd.concat(frames, ignore_index=True)
diff --git a/tasks/clarity-audit-2026-09-13.md b/tasks/clarity-audit-2026-09-13.md
deleted file mode 100644
index 1d9c476..0000000
--- a/tasks/clarity-audit-2026-09-13.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Clarity audit of paper/manuscript/main.md (2026-09-13)
-
-Standard: an interpretive sentence must state the concrete formula, number or mechanism it refers
-to; figure citations must match what the figure plots; no herald sentences; no process ghosts.
-Three parallel audits (Intro+model; Results 1–4; Results 5–6 + Discussion). Line numbers refer to
-main.md at the time of the audit. Nothing below has been applied yet.
-
-## Tier 1 — factual or self-contradictory (fix regardless)
-
-1. L131–132 "no amount of merging can recover it (Fig. S3)". Fig. S3 is re-baselining (E6); it has no
-   merging arm. It shows a population that adopts its own collapsed output as reference never regains
-   lost items. Recite it for that.
-2. L595–598 Discussion: replay fractions "sit where the inheritance model's operational threshold
-   lies". Contradicts the count-not-fraction closed form. Rewrite: they bracket 5% at n=200, and each
-   delivers tens to thousands of replayed examples per step, past the ten copies that hold 95%.
-3. L617–620 "past a threshold, become irreversible". Reintroduces the retracted threshold. The
-   irreversibility condition is concrete: every copy gone from every parent and source (Fig. S3).
-4. L276–277 "(Fig. 3B–C)" cited for a 2×2 (two sizes × easy/hard) comparison; 3B is 0.5B easy
-   merge-vs-specialist, 3C is 7B hard routing-vs-average. Cite precisely; point to Table S2 for the rest.
-5. L121–122 autoencoder "collapses faster (Fig. 2)". The comparison with drift is Fig. S2A–B; Fig. 2A
-   shows the collapse. Cite both.
-6. L107–110 three closed forms named, one written. The union formula `T[ρq + (1−ρ)(1−(1−q)^K_T)]`
-   appears nowhere in the paper, yet Table 1 labels that row "closed form". Write it (here or in the
-   merging section).
-7. L541–544 the in-sample rank correlation of the conflict predictor is never given (only its CI and
-   the held-out 0.35–0.40). Insert the value from Table S2.
-8. L263 "at any rarity (Fig. S8)" — supported only at the tested rarities.
-
-## Tier 2 — result named but not stated / revelation lands vague
-
-9. L246–256 Jenkin/blending null: "exact description" asserted; the conservation law announced
-   without saying what is conserved (expected rare-item mass q·p in the child, independent of K).
-10. L277–283 headroom definition is near-tautological ("routing wins when routing would score
-    higher"). State what sets it: 7B-easy at ceiling (1.00 on two families) → nothing to recover;
-    7B-hard average 0.41 vs routing 0.50 in every seed; 0.5B same gap on easy tasks.
-11. L235–237 "twice the fraction ... the measured price of the estimator bias" → 10% vs 5%, because
-    sharpening loses rare modes faster than sampling alone.
-12. L636–637 "the ablation shows what removing it does" → 0.48 vs 0.78, confident and wrong.
-13. L607–612 "equilibrium theory / failure theory / prediction" labels → give the three contents
-    (m ≈ 1/p per step, 2m/(2m+1) kept; self-replay = g=0; functional disagreement predicts where
-    weight distance does not, with shared-data control).
-14. L492–494 "what moves the cliff" never stated → share of shared prompts under contradictory
-    conventions (Fig. 5B).
-15. L447–452 six-generation ceiling never named → what one adapter holds (0.80 for a single model
-    taught the whole syllabus).
-16. L577–579 and L633–635 Discussion "headroom"/"composed" carry the result → give the margins
-    (routing +0.09, screened offspring +0.07 on hard tasks, every seed; parity at ceiling).
-17. L513–515 "paid this floor" — floor undefined in main text → obligate-merge arm from generation 3.
-18. L148 Table 1 "(the headroom rule)" used before defined (L281).
-19. L147 Table 1 "Consequence-level only" is internal shorthand.
-20. L318–321 conformity term never motivated → consensus = learning from own outputs when there is
-    no verifier; g=0 rewards agreement with itself.
-
-## Tier 3 — heralds, process ghosts, jargon
-
-21. L73–76 three programme sentences announcing the paper.
-22. L82 "Population genetics prices each decision."
-23. L83–86 Fig. 1B "society in time rather than in space" — say what the shift buys.
-24. L221–225 "What these runs add is the comparison ... exposes two departures".
-25. L254–256 herald before the proposition.
-26. L496 "The pre-registered emergent test constrains the claim most."
-27. L331–333 "partly built in ... could alter the picture" (review-response ghost) → separability point.
-28. L560–563 and L656–659 "does not validate a specifically population-genetic mechanism" said
-    twice; keep one, as a statement about the subject.
-29. L468–471 semicolon chain, "count-to-effect link"; the quadratic snowball is Orr (43), not Fig. 5E–F.
-30. L292–294 clonal interference in shorthand → spell out: two variants in different individuals
-    never meet in an asexual descendant.
-31. L346–348 "the Lamarckian channel biology forbids and engineering permits" → one clause of anchor.
-32. L273–275 nonlinearity caveat leaves out why the analogy holds (1/N scaling of an update, 67).
-33. L103–106 Wright–Fisher used before defined; "learner, not organism" antithesis.
-34. L93–94, L98–99, L111–112 housekeeping in reader-facing text ("I use the word throughout",
-    "allele frequency of the dictionary", "standing tests in the codebase").
-35. L372–413 fourteen numeric pairs in one paragraph; split at "Three things rise with generation".
-36. Fig. 3D legend colour order vs text order (conflict, overlap, duration) — check they match.
-
-
-## Proposed rewrites (from the audits; GG's voice to be checked before applying)
-
-1. "Once every copy of a rare item is gone from all parents and all sources nothing can rebuild it, and the inheritance model shows the trap in its commonest form: a population that adopts its own collapsed output as its new reference never recovers the items it had lost, whatever real data it is fed afterwards (Fig. S3), so remedies must act while copies still survive somewhere."
-2. "The replay fractions the field settled on empirically (about 1% for instruction tuning, 89; 5% to 25% in continual pretraining, 90) bracket the 5% found here at 200 samples per generation, and the closed form says why they scatter: at typical batch sizes each delivers tens to thousands of replayed examples per step, well past the ten copies per generation that hold 95% of a source's diversity, so the number that matters is the count of replayed examples of each skill, not the fraction."
-3. "Evaluation that averages over capabilities hides exactly the losses drift predicts first, the rare ones, and a rare capability is recoverable only while some parent or source still holds a copy (Fig. S3). Monitoring the tail, the accuracy on the rarest items rather than the mean, is therefore the leading indicator."
-4. Cite "(Fig. 3C for 7B on hard tasks; the 0.5B and 7B-easy comparisons in Supplementary Information, Table S2)" — verify the SI location.
-5. "(Fig. 2A; the comparison with drift in Fig. S2)".
-6. "(the heterozygosity decay `E[H_t] = H_0(1 − 1/n)^t`; the stationary diversity under real data, written in the next subsection; and the expected number of rare items held by at least one of `K_T` parents, `T[ρq + (1−ρ)(1−(1−q)^K_T)]`, used in the merging section)".
-7. Insert ρ and CI from Table S2.
-9. "Averaging two models does to a rare capability exactly what Jenkin said blending would do to a rare variant: a child fit to the mean of `K` parents sees the item `K` times more often in the mixture and at `1/K` of its mass when it does, and for a rare item the two cancel. Blending inheritance is therefore the null model of merging, and the proposition below states the cancellation exactly." Proposition lead: "In the inheritance model the dilution is a conservation law: the expected mass of a rare item in the child is `q·p` whatever the number of parents, so averaging over more parents neither helps nor harms a rare item's survival."
-10. "Routing wins by exactly the amount averaging loses to dilution, and two things set that loss. A strong base on easy tasks has none: after averaging, the 7B model scores at ceiling (1.00 on two of three families), and routing has nothing to recover. Hard tasks restore it: at 7B the average falls to the level of the best single specialist (0.41), because each specialist's own skill is diluted, and routing among the intact specialists scores 0.50, ahead in every seed. A weak base (0.5B) shows the same gap on easy tasks. The variable is headroom, the distance between the average and the ceiling, and neither model size nor task difficulty alone."
-11. "The autoencoder needed about 10% real data where the inheritance model needed 5%; the difference is what its sharpening bias costs, since a learner that concentrates mass on common modes loses rare ones faster than sampling alone would."
-12. "...and removing it is the one ablation that fails outright: a population selected on agreement with its own consensus instead of on the verifier settles at 0.48 against 0.78 for the full society (Fig. 4D–F), confident and wrong."
-13. "For continual learning the results give three things. The replay ratio has a formula: `m ≈ 1/p` examples per step of the rarest skill one refuses to lose, and 2m/(2m+1) of the diversity is kept. Replaying a network's own output (33, 91–93) is grounding with `g = 0` and collapses on the timescale of Fig. 2, one hop being too short to see it. And a pre-merge test (functional disagreement on shared probes) predicts interference where weight distance does not, with a control for shared training data that the regression (86) and distance (87, 88) studies lack."
-14. "That no single model can answer one prompt two ways is a matter of information, not of training (SI Text S1, Proposition S2). What the population view adds is where the cliff sits: it moves with the share of shared prompts under contradictory conventions (Fig. 5B), and in a population that share grows whenever lineages adopt conventions independently."
-15. "Under a curriculum that delivers every skill to every lineage the ceiling is what one adapter can hold (0.80 for a single model taught the whole syllabus), and sex and selection each reach it sooner without raising it."
-16. "*Route or screen rather than average whenever the average falls short of the best parent on any task*: on hard tasks routing beat averaging by 0.09 in every seed and screened offspring by 0.07 (Fig. 3C), whereas on tasks the 7B base already answered at ceiling the plain average matched them and nothing was lost." / "Blending dilutes whichever parent's skill is rarest, so routing and offspring screening pay only where the plain average falls short of the best parent, and were inert where it did not (Fig. 3B–C)."
-17. "...and this is the cost the obligate-merge arm of the six-generation population paid from generation 3 onward, when its partners began carrying opposite conventions for the same prompts (Fig. 4B)."
-18. Table cell: "Fig. 3B–C: merging beats blending whenever the weight-average scores well below the best parent, and blending suffices when it does not".
-19. Table cell: "The irreversibility is reproduced (Fig. S3); the mutational mechanism of the ratchet is not modelled, see (30)".
-20. "...*grounded evaluation*: an agent is scored partly against reality and partly against the population's own consensus (`g`·true-fitness + (1−g)·conformity). The consensus term stands for what a population does when it has no verifier, which is to learn from its own outputs, so `g = 0` is a population that rewards agreement with itself."
-21. "Drift is only the entry point, because population genetics is above all a theory of what keeps a population from decaying (immigration, recombination, selection, population structure) and of where each of those fails, and every one of them has a counterpart an operator of a model population can switch on: real data entering each generation, merging, verifier-anchored selection and the choice of which models merge with which."
-22. "...transposed from a single network to a population whose members inherit from one another, and each of them has a population-genetic answer with a number attached (how many real samples, how far the average sits below the best parent, how much the parents disagree on shared inputs)."
-23. "Fig. 1B draws the change of viewpoint the transfer rests on. Models are usually pictured as a society in space, contemporaries exchanging messages. The couplings that matter here (training on model output, merging, real data entering each generation) run between generations, and a society coupled in time is what population genetics describes."
-24. "Compared against the exact model, the trained networks depart in two ways, both consequences of the estimator bias measured above. The threshold softens: ..."
-26. "The sharpest test is whether isolation emerges with no conflicting signal anywhere, as a true Bateson–Dobzhansky–Muller incompatibility would: children were diverged..."
-27. "The arm without grounding fails by construction, since a rule that scores agreement will converge on agreement; what the ablation adds is that the other two removals fail in different ways, so recombination and diversity are not substitutes for grounding or for each other. Magnitudes depend on the mutation, restart and selection schemes, which were not varied."
-28. Keep one, in the Discussion: "Three of the framework's refinements failed (confidence weighting, the modifier reading of declines, selection turning speed into level), and the results are consistent with any account in which rare items are lost by sampling and conflicting conventions cannot share weights. The population-genetic reading earned its place by supplying the nulls and the overlap control, not by being the only mechanism left standing."
-29. "In the inheritance model (Fig. 5E–F) hybrid fitness stays at the parents' level while the lineages remain compatible and then falls below the ancestor, sooner the more incompatibilities the genomes carry. Orr showed the number of such incompatibilities grows with the square of divergence (43). Whether a growing number of conflicts produces a fall in performance in a trained network is the question the simulation cannot answer."
-30. "...the *Fisher–Muller effect* (37, 38). In an asexual population two useful variants that arise in different individuals can never meet in one descendant; the lineages carrying them compete, and one is lost. Recombination puts both into one offspring, which is why sexual populations adapt faster."
-31. "...so what the parent learned in its lifetime passes to the child, the inheritance of acquired characters that Lamarck proposed and biology rejected, and that a weight file makes trivial."
-32. "(a network is nonlinear in its weights, so averaging weights does not average outputs; but an update held by one of `N` parents is still scaled by `1/N` in the average (67), which is the dilution the proposition describes)".
-33. "The resampling step is the Wright–Fisher process, population genetics' canonical model of neutral evolution, in which each generation is a random sample of size `n` from the last. In this *inheritance model* the Wright–Fisher population is the sample a child is trained on and its individuals are the `n + m` samples; it is a model of a learner."
-34. "An item is the counterpart of an allele; a *capability* is what an item stands for." / "(allele frequency, in Table 1)" / cut or "(Methods)".
diff --git a/tasks/lessons.md b/tasks/lessons.md
deleted file mode 100644
index 854d7f3..0000000
--- a/tasks/lessons.md
+++ /dev/null
@@ -1,169 +0,0 @@
-
-**2026-08-11 — Stop padding time estimates.** GG: "You generally massively overestimate the time it
-takes to do some work... You're fast." Phase-1 items I scoped as "Week 1" took ~an hour. Rule: state
-what will be done and in what order; give a duration only when compute-bound (training walltime), and
-base it on measured runtimes, not human-project heuristics.
-
-**2026-08-11 — Correspondence is not identity; interpretation is not prediction (manuscript reviews).**
-The external review's core corrections, to internalise for all paper claims: (1) distinguish
-interpretation / explanation / prediction and claim only the level the evidence supports; (2) a
-minimal model being exactly Wright-Fisher does not make real training "literally" WF — our own
-learning-kernel result says otherwise (cite it against ourselves); (3) name the operator every claim
-is about (output-mean vs weight-average vs max-with-oracle vs routing are different objects with
-different budgets); (4) don't write "nobody has / none imports" — invite no priority disputes; say
-"to our knowledge" and state the positive contribution; (5) negative results (E13b) are strengths —
-lead with them; (6) "control theory" needs states/controls/dynamics/rule or it's a "framework".
-
-**2026-08-11 — tmux does NOT survive SSH disconnects on this machine.** The tmux server starts inside
-the SSH session's systemd scope and gets reaped on logout (lost ~20 min of the epistasis grid; GG:
-"SSH disconnected"). Reliable pattern here: `systemd-run --user --collect --unit=
---working-directory="$PWD" bash -c ''` — lands in user@.service (kept alive by the desktop
-session), survives disconnects; check with `systemctl --user is-active `, logs via redirect.
-
-**2026-08-11 — Internal deliberations must not leak into reader-facing prose.** GG (on the
-convergence paragraph): it "carries all the ghosts of our internal brainstorming that should be
-completely alien to the readers (this is, alas, a recurring problem with your writing)." The pattern:
-strategic concerns from the working process (priority anxieties -> "we cite for priority of
-publication"; calibration debates -> "the honest statement"; positioning battles -> "nobody has")
-surface as defensive or self-referential prose. Rule: before finalising any reader-facing passage,
-ask "does understanding this sentence require knowing how we worked?" If yes, rewrite it as a plain
-statement about the literature or the result. Confident papers situate; they do not litigate.
-(Promoted to a general rule in the global ~/.claude/CLAUDE.md, 2026-08-11 — it applies to all
-reader-facing prose in every project.)
-
-## Terminology: "model" is overloaded in this project (2026-09-07)
-In the PNAS manuscript and figures, "model" must mean an AI model. The settled term for the
-pop-gen construct is **"the biological model"** (GG decision, 2026-09-07) — used everywhere
-("the multi-locus biological model", "Biological-model tier"), never "minimal model",
-"analytic model", or "exact model"; GG also vetoed "(exact)" as a tier label (the guarantee
-line "closed forms · bitwise-reproducible" carries that content). Support level formerly
-"Exact" is now "Closed form". Keep "exact" only in technical noun phrases (exact-match
-verifier, exact oracle, exact equilibrium, exact recovery).
-
-## Never `uv sync` while a job is using the venv (2026-09-07)
-Adding the `notebooks` extra mid-session ran `uv sync` **without** `--extra llm`, which rebuilt the
-shared `.venv` — dropping the LLM stack *and* silently switching the interpreter 3.14 → 3.11 (uv
-recreates with the system default when no `.python-version` is pinned). That killed the running
-local society seed with a `FileNotFoundError` deep in `huggingface_hub` templates — a failure that
-looked scientific but was pure environment churn.
-Rules: (1) never mutate `.venv` while a background job is running against it — wait, or build a
-throwaway venv elsewhere; (2) `uv sync` is *declarative* — always pass **every** extra the project
-needs, or it removes the ones you omit; (3) the repo now pins `.python-version` (3.14) so the
-interpreter can never drift silently.
-
-## Before a compute campaign, derive the design from the theory and read the analytic code beside the neural code (2026-09-07)
-The v1 `llm_society` campaign (4 seeds, ~13 L40S-h) returned a null that was *structurally guaranteed*:
-3 families over 8 agents made founders near-clones (E8's ρ=1 control: recombining clones buys nothing);
-2^3 competence states left no room for a child to be "new" (E8 needs L=12); sex was a linear blend at
-0.5B (E4/`llm_moe`: the dilution regime); parents were truncated before breeding (E11 selects on
-survival over parents+offspring — v1 threw away half the families at gen 1 with no operator to
-restore them); `n_test`=40 put every contrast inside one SE. GG caught the first fault by asking what
-the founders knew; the rest fell out of comparing `dynamic_society.py` with `society.py` line by line.
-Rules: (1) a Layer-2 instantiation of an analytic experiment must be checked *operator by operator*
-against the analytic code, not against its description; (2) every free parameter that the theory
-constrains (ρ, L, the operator regime, the observation floor, selection intensity) is set by a
-prediction or a calibration measurement, never by feel; (3) write the falsifiers with numbers and the
-power analysis *before* submission — the pre-registration is `tasks/prereg-llm-society-v2.md`.
-
-## Calibrate the inheritance channel before the population (2026-09-07, evening)
-Nine local GPU-hours of calibration found three ceilings a 96-GPU-hour campaign would have hidden:
-(1) with 3 skills over 8 agents, founders were near-clones (E8 ρ=1 → recombination buys nothing);
-(2) a fresh LoRA distilled from a one-skill parent's answers on nine families retains only 0.6–0.8 of
-the skill — interference from confident off-expertise answers, not the E2 observation floor, and
-removable by gating on the source's own confidence; (3) a two-skill child holds each skill at
-~0.85× of its parents at ANY training budget (rank 64 overfits) — the learning budget, not the
-sample budget, is the conserved quantity, and it caps how many skills one adapter can carry. GG's
-call was no-go at 0.5B rather than a campaign that could only test the ablations. Rules: (a) measure
-transmission fidelity of the inheritance channel for one skill, then two, before breeding populations;
-(b) when a gate fails, re-derive it from the data you already have (the conflict gate moved 0.35→0.41
-from the epistasis grid's own outcomes) rather than by feel, and record it as an amendment; (c) a
-pre-registration that ends in a no-go has done its job — write the ceiling up, don't route around it.
-
-## A plan for reader-facing prose must carry the dual-audience standard explicitly (2026-09-09)
-GG rejected the approved-in-substance manuscript-revision plan until it stated, as a first-class
-section, that every term from either field is defined at first use with an example from each world.
-The plan had the right content and structure but treated accessibility as a verification
-afterthought; GG's rule is that it is "paramount" and must be designed in, not checked for. Rule:
-before drafting any passage for a mixed readership, build the term table (term / one-clause
-definition / biology example / model example) *in the plan*, and put a two-reader pass in
-verification. The same applies to my status reports — GG twice said "I am lost with all these C3,
-E9, H1"; spell codenames out.
-
-## 2026-09-11 — check the figure panel inventory before flagging cross-references
-In the manuscript review I flagged Table 1's "Fig. 4C–E" and "Fig. 3C" as inconsistent with the text. They were
-correct: `make_figs.py` puts the E11 ablation in Fig. 4's bottom row and the predictive grid in Fig. 3C–D; the text
-simply failed to cite them. Rule: before calling a cross-reference wrong, read `paper/manuscript/make_figs.py` and the
-captions in `build.py` for the panel inventory; the fix is usually a missing citation in the text, not a wrong table.
-
-## 2026-09-11 — never type a result number that a script has not printed
-Writing the stop3 README I filled the per-seed cells from memory of the mean and had three rows wrong
-until the loader's pivot table exposed it. Rule: every number in a README, SI table or manuscript is
-pasted from a stats-script printout produced in the same step; if the script has not printed it,
-print it first. This is the same rule the plan stated ("copied from the stats-script output, not
-typed") and I broke it within the hour.
-
-## Reference numbers hardcoded outside the renumber path (2026-09-11)
-`renumber_refs.py` rewrites main.md, si.md and build.py captions, but `make_figs.py` carries a literal "(refs. N, M)" in the fig1a grounding cell, which went stale after references were added. Rule: after any renumber, grep `refs\.` and `ref\.` across `paper/manuscript/*.py` and fix by hand (or extend renumber_refs.py to cover make_figs.py). Also: fig text is rendered, so verify by `pdftotext figs/fig1a.pdf`, not by grepping the source alone.
-
-## 2026-09-12 — GG's manuscript comments: heralds, undefined terms, and phantom SI references
-Forty-five comments on the ODT. Three patterns. (1) **Herald sentences** ("Two boundaries follow.",
-"Modifier theory predicts its fate:", "X is the measurement no other arm produces") — GG: "Breaking
-down sentences like this is also a claudism." Now a HERALD block in the declaudify detector; run
-`--list herald` before handing over any draft. (2) **Every technical term defined at first use, with
-one word per concept**: item/capability/allele, mass, refit, verified real samples, Zipf source,
-practitioner, "which trained networks" — the dual-audience rule from 2026-09-09 applied to *my own*
-vocabulary, not only the biology. Rule: after drafting, grep each noun of art for its first
-occurrence and check a definition precedes it. (3) **Never cite the SI for something the SI does not
-contain.** The text cited "the SI separates three cases" and "the proof is Poisson thinning" and
-neither existed; and every "(SI)" pointer must name a figure or text number. Rule: before writing
-"(SI)" grep si.md for the claim; if absent, write it (SI Text S4) or drop the sentence.
-
-## 2026-09-12 (round 2) — the Discussion must discuss; novelty is flagged where the result is shown
-GG on the "What is borrowed and what is new" inventory: "The discussion should discuss, not list."
-And on the Limits paragraph: stating small-scale limits "is usually done by undergraduate students";
-either run the experiment or discuss only problems too big for the paper. Rules: (a) never write a
-Discussion paragraph that is a list of prior-art citations or of caveats; each Discussion paragraph
-argues one point; (b) attribute novelty at the point of the result, with the figure panel, and name
-the prior finding it explains or extends in the same sentence ("an observation reported by others
-and left unexplained (60)"); (c) "much/some/most of X was known" is a hedge that gives novelty away
-without saying what is new; replace with the specific thing prior work lacks. Also: after
-`renumber_refs.py --apply`, the fig1a literal (refs. 22, 33) went stale (Shumailov became 23); the lesson from 2026-09-11 held.
-
-## 2026-09-12 — a prediction written into the Discussion must be run before it is printed
-The revised Discussion predicted that differential reproduction would turn recombination's speed
-advantage into a level advantage. Three GPU-hours later it did not (parity, 3/3 seeds). Rules:
-(a) when a Discussion sentence forecasts the outcome of an experiment we can run in under a day,
-run it in the same revision; (b) check adapter/cache directories for seed- and base-specificity
-before any HPC array (speciation shared one dir across seeds; the specialist cache would have loaded
-Qwen adapters into SmolLM2); (c) the local smoke gate for a new base (termination, base accuracy in
-(0.05, 0.95), sample generations) cost 4 minutes and is worth running every time.
-
-## 2026-09-13 — a figure must be readable without its caption
-GG on the manuscript figures after reading the student guide: "too unclear, cryptic"; figures should
-"give some clear information without the need to read the legend". The house rule in make_figs.py
-("no per-panel headline titles; interpretation lives in the captions") was the wrong rule for this
-audience and is reversed. Rules: (a) every data panel carries a one-line headline stating its
-finding plus a grey line naming the system and its size; (b) legend entries say in words what is
-plotted ("accuracy on the model's weakest task family", not "worst_family"); (c) where the set-up is
-not obvious, a schematic panel explains it inside the figure; (d) bar comparisons carry the test
-(paired over seeds, stars, key printed under the legend). Layout lesson: headlines longer than the
-panel run into the neighbour; wrap at ~40 characters per line for a half-width panel, ~70 for full
-width, and render before trusting.
-
-## 2026-09-13 — figure layout rules I should apply without being told
-GG had to ask three times for things a careful eye catches: headlines running past their panel,
-a schematic strip narrower than the data panels beneath it and not flush with their left edge,
-and a large blank band between a strip and the next row. Rules, now encoded in make_figs.py:
-(a) any panel placed by hand (schematics) is positioned from the neighbouring data axes' geometry:
-left edge = the data panels' frame, right edge = the last panel's frame, bottom = a fixed 0.75 in
-above the headline below, height from the content's designed aspect (never let equal-aspect centre
-a too-wide axes); (b) text wraps to its own panel width (`headline()` measures the axes); (c) after
-every regeneration, render at ≥ 90 dpi and check four things before reporting: nothing crosses a
-panel boundary, nothing overlaps, blank bands are no larger than the row gaps, and left edges of
-stacked panels line up. Report only after that check passes.
-
-## 2026-09-13 — prose: no staccato fragments
-- GG flagged "These results say X. They do not say where. The inheritance model does, in closed form."
-  as a claudism. Breaking a thought into short declaratives is rarely necessary; join them (colon,
-  "because", "and", "but"). Clarity comes from stating the concrete object, not from short sentences.
-- After any rewrite pass, scan for sentences of ≤7 words introduced by the edit and rejoin them.
diff --git a/tasks/prereg-llm-compose-v3.md b/tasks/prereg-llm-compose-v3.md
deleted file mode 100644
index 7b3045a..0000000
--- a/tasks/prereg-llm-compose-v3.md
+++ /dev/null
@@ -1,345 +0,0 @@
-# Pre-registration — `llm_compose` v3: does a composed capability survive inheritance?
-
-**Status:** draft for GG review, 2026-09-07. Supersedes `prereg-llm-society-v2.md` (calibrated,
-no-go at 0.5B) and `workorder-llm-society.md` (v1, run, negative). Nothing runs until §4's gates pass
-and GG signs off §12.
-
-**The question, in one sentence.** Every model-merging paper merges *once*; this asks what happens to a
-composed capability when the models that carry it keep reproducing, and whether the population-genetic
-closed forms predict the trajectory.
-
----
-
-## 0. Why the design changed, and what carried over
-
-GG's objection to the v2→7B plan (2026-09-07): *"there is no structural reason 7B would succeed if
-0.5B failed. Most likely we are simply using the wrong LoRA specialisations… look in the literature
-and see what kind of test people use as paradigmatic for LoRA."* Correct on both points. Three things
-came out of the literature check:
-
-1. **The paradigmatic test is binary skill composition on a held-out, out-of-domain target.**
-   [LoRA Soups](https://aclanthology.org/2025.coling-industry.55.pdf) (COLING 2025): Llama-2-7B,
-   rank 8, math (MetaMathQA) × code (Code Alpaca) → GSM8k-Hard with program-aided evaluation;
-   also manual × instruction-following → closed-book QA. [LoraHub](https://arxiv.org/abs/2307.13269)
-   (COLM 2024): many Flan modules → held-out BBH. [MergeBench](https://arxiv.org/pdf/2505.10833):
-   math, code, multilingual, safety, IF. Nobody uses procedurally generated puzzle families.
-
-2. **Our v1/v2 families were disjoint but *non-composable*.** Sorting a list and counting letters
-   combine into nothing, so fitness had to be the *average of nine separate objectives* — a
-   **capacity** test (can one r = 16 adapter hold six skills?), which v2's calibration answered: no,
-   ≈ 0.85× per skill for two, worse for more. E8's genotype model has loci contributing to **one**
-   fitness function. Composable skills restore that and need only **two** parents per child, so the
-   capacity ceiling never binds. This, not scale, was the fault.
-
-3. **The operator was wrong in a way with a clean algebraic diagnosis.** peft
-   `combination_type="linear"` computes ΔW = (α₁B₁+α₂B₂)(α₁A₁+α₂A₂)ᵀ, which carries **cross terms**
-   B₁A₂ᵀ and B₂A₁ᵀ — one parent's output projection driven by the other's input projection.
-   `combination_type="cat"` gives α₁B₁A₁ᵀ + α₂B₂A₂ᵀ: each parent's rank-r subspace intact, rank 2r.
-   **That is E4's union operator in the natural algebra of the medium**, and the cross terms are the
-   mechanism of blending dilution. Their GSM-Hard numbers: CAT 21.11 > TIES 15.77 > DARE 14.78 >
-   MoE-routing 13.5 > LoRAHub 4.1 (below the 5.91 base). Routing below CAT is our own `llm_moe_hpc`
-   reading — selection is capped at the best parent, composition is not.
-
-**Carried over from v2's calibration (not wasted — it calibrated the channel v3 uses).** The
-self-consumption inheritance channel: a child distilled from its source's own answers loses 20–40%
-per generation to interference from confident off-expertise answers; gating on the source's own
-confidence (τ = 0.85) restores single-skill retention to **0.87–0.93**, and v3's lineages carry
-**one skill each**, which is exactly the regime the gate was measured in. `k = 300` prompts/skill,
-3 epochs, r = 16, confidence gate τ = 0.85.
-
-**Prior art to cite rather than re-demonstrate.** Offspring capability neither parent had is
-established: [Akiba et al.](https://www.nature.com/articles/s42256-024-00975-8) (Nature Mach. Intell.,
-Japanese × math) and LoRA Soups' *super-linear improvement* (base 5.91 → +code 8.04 → +math 14.18 →
-CAT 21.11; ≥ 5% of solved problems solved by neither parent). Population-based LLM evolution with
-crossover/mutation/selection exists ([Zhang et al. 2025](https://arxiv.org/abs/2503.01155), 40 models),
-as does iterated merging ([M2N2](https://arxiv.org/html/2508.16204v1), EvoGM). **None of them
-iterates the *reproduction* loop**: they optimise a merge, they do not ask what a merged capability
-does over generations. That gap is the experiment.
-
-**Positioning (GG, 2026-09-07): the manuscript does not need repositioning.** The paper's claim is
-that population-genetic *rules describe and predict* the phenomenon — closed forms, thresholds,
-conservation laws — not that recombination or collapse were discovered here. §5's H3 is that claim
-made falsifiable at the language-model tier. One genuine overlap to co-cite:
-[Model Collapse as Cultural Evolution](https://arxiv.org/html/2605.23054) runs ten generations of
-self-training and finds rare-variants-lost-first plus quality-filtering-as-remedy — our E1 and E2 in
-spirit — but under *iterated learning* (Bayesian convergence to the prior), with no equilibrium
-closed form, no threshold, no recombination, and no population structure.
-
----
-
-## 1. The design
-
-**Two lineages, one measurement.** Two single-skill LoRA lineages on a shared frozen base — a **math**
-lineage and a **code** lineage. Each generation, each lineage reproduces by self-consumption (a fresh
-LoRA distilled from its own confidence-gated answers on fresh prompts). Each generation, the *current*
-two parents are merged and the composed model is evaluated on the held-out composed task.
-
-The composed model is a **measurement, not a lineage** — re-formed each generation from whatever the
-parents currently are. This isolates the question ("does composition survive parental drift?") from a
-confound ("does the composed model itself drift?"). An optional third arm makes the composed model a
-lineage too (§1.4).
-
-**1.1 Task.** Math (MetaMathQA subset) × code (Code Alpaca subset) → **GSM8k-Hard**, program-aided:
-the model emits Python, the code is **executed in a sandboxed subprocess**, and the return value is
-compared to the reference answer. Execution is the verifier — reality's "no" — and returns Layer 2 to
-the blueprint's original §3.6 specification.
-
-**1.2 Operators.** `cat` (union; the campaign operator) and `linear` (blending; the H6 control), both
-at merge weights fixed a priori to (0.5, 0.5) — *not* tuned, because a tuned blend would confound the
-operator contrast with search. LoRA Soups' learned-CAT is a stronger operator than ours; we do not
-need it, and using the untuned version makes the comparison to `linear` clean.
-
-**1.3 Grounding.** The arm knob, in the *training mix* this time (E2's immigration, not E11's
-selection channel): the dry arm's children see only the parent's own answers; the grounded arm mixes a
-fraction **g = 0.10** of fresh verified real examples (correct answers from the held-out pool of the
-lineage's own dataset) into each child's training data. This is the first LLM-tier test of *immigration*
-in this project; Fig. 1A's note that training-mix grounding at LLM scale is established elsewhere
-stands, but here it is the manipulated variable, not a claim of novelty.
-
-**1.4 Arms.**
-
-| arm | parent reproduction | grounding | operator | tests |
-|---|---|---|---|---|
-| `dry` | self-consumption | g = 0 | cat | H2, H3, H5 |
-| `grounded` | self-consumption | g = 0.10 | cat | H4 |
-| `dry_linear` | self-consumption | g = 0 | linear | H6 |
-| `dry_composed` *(optional)* | the merged child becomes the next parent of both lineages | g = 0 | cat | does composition survive in a self-consuming *composed* lineage |
-
-**1.5 Generations and seeds.** G = 6 generations, seeds 1–3, fixed now.
-
-**1.6 Measured each generation.** Per lineage: own-skill accuracy (math on MATH-500 subset; code on
-HumanEval-subset) → **q_t**, the retained skill. Between lineages: **ρ_t**, the correlation of their
-behaviour, measured as in `llm_epistasis` (agreement rate on a shared probe pool, and LoRA-delta
-cosine as a geometric companion). Composed: GSM-Hard accuracy, plus the **surplus** (composed − best
-parent on the composed task) and the **union-exceedance** (fraction of composed-solved problems that
-neither parent solves — LoRA Soups' super-linear signature).
-
----
-
-## 2. What the framework predicts, quantitatively
-
-E4's closed form for two parents, U(K=2, ρ, q) = ρq + (1−ρ)(1−(1−q)²), gives the expected coverage of
-a capability held by either parent. Composition on a two-skill task is the conjunction rather than the
-union, so the corresponding prediction for a task needing *both* skills is the product form
-
-  **Ĉ_t = c₀ · q_t^math · q_t^code · (1 − ρ_t)/(1 − ρ₀)**
-
-with c₀ fixed by generation 0 (one free scale parameter, fit once, never refit). Two consequences the
-merging literature has no reason to expect:
-
-- **Composition decays faster than either parent.** Ĉ depends on the *product* of both retentions and
-  on decorrelation. If each parent retains 0.9 per generation, the composed capability retains 0.81
-  before any ρ effect. Super-linear gain becomes super-linear loss.
-- **ρ rises under dry self-training**, because both lineages drift toward the same attractor — the
-  base model's prior. Rising ρ removes the complementarity composition depends on, so the surplus
-  collapses even where q is still respectable. This is the mechanism, and it is measurable.
-
----
-
-## 3. Hypotheses, thresholds, falsifiers
-
-Primary outcome: **composition surplus** S_t = (composed GSM-Hard accuracy) − (best single parent on
-GSM-Hard), per arm per seed per generation. Secondary: union-exceedance, q_t per lineage, ρ_t, and the
-predicted Ĉ_t.
-
-| | Prediction (source) | Threshold | Falsified if |
-|---|---|---|---|
-| **H1** *(gate, not a claim)* | Generation 0 reproduces the literature: CAT composes | S₀ ≥ +0.05 and union-exceedance ≥ 0.03 and CAT > linear by ≥ 0.03, in ≥ 2 of 3 seeds | any of these fails → the setup does not reproduce a published effect; **stop and fix before iterating** |
-| **H2** | Composition decays faster than its parents (§2) | S_t declines monotonically (Spearman ρ ≤ −0.7 vs t) and the composed capability's fractional loss by G exceeds each parent's own fractional loss, in ≥ 2 of 3 seeds | S_t flat or rising, or composed decays no faster than parents |
-| **H3** | **The closed form predicts the trajectory** (E4/§2) — the paper's central claim, made falsifiable | Ĉ_t (one parameter, fit at t=0) predicts observed composed accuracy with mean absolute error ≤ 0.05 across t = 1…G, and beats a two-parameter exponential-decay baseline on AIC | MAE > 0.10, or the atheoretical baseline wins → the closed form describes nothing the data did not already say |
-| **H4** | Grounding arrests it (E2 immigration) | S_G(grounded) − S_G(dry) ≥ +0.08, paired per seed, 3/3 seeds positive | grounded ≤ dry, or difference < 0.03 |
-| **H5** | Rising ρ is the mechanism | ρ_t rises monotonically in `dry` (Spearman ≥ +0.7) and is flat-or-lower in `grounded`; partial correlation of S_t with ρ_t controlling for q_t is negative | ρ flat in dry, or S_t–ρ_t partial correlation ≈ 0 → decay is pure retention loss, not lost complementarity (report either way; it is a mechanism result, not a claim of failure) |
-| **H6** | Blending conserves (E4) | `dry_linear` shows S₀ ≤ +0.02 and union-exceedance ≤ 0.01 at every generation — the conservation law, in the operator the literature already shows is worse | linear matches cat at generation 0 → the cross-term account of dilution is wrong |
-
-Analysis: per-seed paired contrasts, mean ± 95% CI over 3 seeds, sign counts reported. H3 is
-pre-registered as a *prediction with a fixed functional form and one free parameter*; the fit is at
-t = 0 only and is never refit.
-
----
-
-## 4. Calibration gates (before any campaign)
-
-| Gate | What | Pass criterion | Cost |
-|---|---|---|---|
-| **C1 base** | Smallest Qwen2.5-Instruct (0.5B → 1.5B → 3B → 7B) whose *generation-0* CAT composition on GSM-Hard lands in **[0.25, 0.70]** | first size in band wins; if 7B exceeds 0.70 the task is saturated and GSM-Hard is swapped for its large-number variant | ≤ 2 h, escalating |
-| **C2 verifier** | Execution sandbox: determinism (same code → same verdict ×3), isolation (no filesystem/network), timeout, and agreement with reference answers on 100 gold solutions | 100% determinism, ≥ 0.98 agreement, no escape | 1 h, no GPU |
-| **C3 specialists** | Math and code LoRAs each beat base on their *own* skill by ≥ 0.15 and are ≤ 0.4 on the *other* skill (genuine specialists, decorrelated) | both | 1 h |
-| **C4 replication** | H1 at generation 0 (above) | as H1 | 1 h |
-| **C5 transmission** | Single-skill retention through one gated self-consumption step, per lineage, as v2's C2b | ≥ 0.85 per lineage | 1 h |
-
-C1's escalation is the honest form of the scale question: the base is chosen by *task discriminability*,
-not by hope. If 0.5B or 1.5B lands in band, the campaign is cheap.
-
----
-
-## 4a. Calibration record
-
-**C2 execution verifier — PASS (2026-09-07).** 100/100 agreement with GSM-Hard's own reference
-`solution()` functions at 32 ms/item; deterministic across three runs; every hazard contained
-(infinite loop → timeout, allocation → memory, write outside the jail → PermissionError, socket →
-PermissionError, subprocess → PermissionError, syntax error, recursion). Two bugs the gate caught:
-a file write initially escaped (fixed with a `sys.addaudithook` guard) and CPU-rlimit kills were
-misreported as errors rather than timeouts. A third surfaced only on CX3 — temp paths are symlinked
-there, so the jail check needed `realpath`, not `abspath`.
-
-**C1 base — the escalation axis is instruction-tuning, not size.** Zero-shot GSM-Hard, program-aided:
-
-| base | GSM-Hard | executable | GSM8K |
-|---|---|---|---|
-| Qwen2.5-1.5B-**Instruct** | 0.500 | 0.950 | — |
-| Qwen2.5-3B-**Instruct** | 0.417 | 0.617 | — |
-| Qwen2.5-3B (base) | 0.633 | 0.950 | 0.750 |
-| **Qwen2.5-1.5B (base)** | **0.067** | 0.117 | 0.117 |
-
-A base that already has the skills makes the specialists vacuous — the v2 disease in new clothes, and
-it would have been *worse* at 7B, which is the quantitative form of GG's objection to the 7B plan.
-Qwen2.5-1.5B base is within noise of LoRA Soups' Llama-2-7B starting point (0.059), so the escalation
-runs along instruction-tuning rather than parameter count. **Amendment:** C1's band applies to the
-*base* (≤ 0.15) as well as to the composed model ([0.25, 0.70]).
-
-**C3 specialists + C4 replication (`results/llm_compose_gate`) — H1 FAILS, with a clean diagnosis.**
-
-| | composed (GSM-Hard) | executable | own skill |
-|---|---|---|---|
-| math parent (MetaMathQA) | 0.073 | 0.153 | GSM8K 0.620 |
-| code parent (CodeAlpaca) | **0.427** | 0.953 | MBPP 0.075 |
-| cat merge, 0.5/0.5 | 0.407 | 0.827 | — |
-
-Surplus **−0.020** (needs ≥ +0.05) → the pre-registered gate fails and no campaign is submitted on
-this configuration. But **union-exceedance is 0.073**: the merge solves 7.3% of items that *neither*
-parent solves, so composition is occurring and is being cancelled by a format cost (executability
-0.953 → 0.827 when the non-code parent is blended in).
-
-**The skill pair is unbalanced for this base, and the measurement says so precisely.** In the
-published setup math-only (0.142) beats code-only (0.080); here the ordering is *inverted* — code-only
-0.427, math-only 0.073 — because Qwen2.5's pretraining already carries the maths, so **code/format is
-the scarce skill and maths is not**. E8's premise is that each parent supplies something the child
-could not otherwise get; that holds for the code parent and fails for the math parent.
-
-**Diagnostic before any redesign (running):** a merge-weight sweep (0.5/0.5 → 0.1/0.9) under both
-operators, reusing the cached founders. It separates two possibilities that the single 0.5/0.5 point
-cannot: *(i)* the surplus is positive somewhere in weight space and 0.5/0.5 was a strawman — in which
-case the correct experiment is E10's directed version (breed offspring across weights, select on the
-verifier), which is this project's own operator and was fixed to 0.5/0.5 only to keep the operator
-contrast clean; or *(ii)* no weighting yields a positive surplus, in which case the pair is simply
-wrong for this base and the fix is a target whose *maths* the base cannot do (competition-level MATH
-program-aided), not a different merge.
-
-**Merge-weight sweep (cat, GSM8k-Hard, founders reused) — 0.5/0.5 was a strawman, and there is an
-interior optimum.**
-
-| math/code | composed | surplus | union-exceedance | executable |
-|---|---|---|---|---|
-| 0.5/0.5 | 0.407 | −0.020 | 0.073 | 0.827 |
-| **0.3/0.7** | **0.453** | **+0.027** | **0.093** | 0.967 |
-| 0.2/0.8 | 0.433 | +0.007 | 0.080 | 0.967 |
-| 0.1/0.9 | 0.433 | +0.007 | 0.040 | 0.960 |
-
-The surplus is positive over a range and peaks at an *interior* weight — E9's "optimal recombination
-rate is intermediate", in real weights — and the executability cost of blending disappears once the
-non-code parent is down-weighted (0.827 → 0.967, above even the code parent's 0.953). It is also
-LoRA Soups' own result that *learned* CAT beats *static* CAT, arrived at independently. **Amendment
-(adopted):** merge weights are chosen per generation on a **disjoint validation split** of the
-composed target and reported on the test split — E10's directed recombination, which was fixed at
-0.5/0.5 in §1.2 only to keep the operator contrast clean. The `linear` control arm keeps the same
-treatment, so the operator contrast survives. `_split_pool` makes val/test disjointness structural
-rather than a property of seeds, which matters at MATH-500's pool size.
-
-**Still short of the gate: peak surplus +0.027 against a +0.05 threshold, and n = 150 gives
-SE ≈ 0.04.** The bar is high because the *best parent* is at 0.427, where LoRA Soups' was 0.142.
-So the second gate configuration (`configs/llm/compose_gate_math500.yaml`) moves the target to
-MATH-500 rather than moving the threshold.
-
-**Full gen-0 sweep, both operators (GSM8k-Hard, 150 items, best parent 0.427):**
-
-| operator | 0.5/0.5 | 0.3/0.7 | 0.2/0.8 | 0.1/0.9 | range |
-|---|---|---|---|---|---|
-| cat | 0.407 | **0.453** | 0.433 | 0.433 | 0.046 |
-| linear | 0.333 | 0.467 | **0.507** | 0.413 | 0.174 |
-
-**H6 as pre-registered is FALSIFIED, and what replaces it is more interesting.** The prediction was
-that blending "conserves" — no composition to lose. In fact linear blending produces the *largest*
-union-exceedance (0.133 vs cat's 0.093) and the highest composed accuracy of any configuration
-(0.507, surplus +0.080), *provided the weight is chosen*. What distinguishes the operators is
-**variance, not mean**: concatenation is nearly flat in the blend ratio (range 0.046) and never
-catastrophic, while blending swings by 0.174 — worst of all at equal weights (0.333, executability
-0.680, the cross-terms wrecking the code parent's format), best of all at 0.2/0.8. That is E9's
-structure in real weights: blind recombination → outbreeding depression; directed recombination →
-gain; the union operator is the conservative strategy. Revised H6 (recorded before the campaign):
-*the operator ordering is weight-dependent at generation 0; does it stay so across generations, or
-does one operator degrade faster?* — measured by the `dry_cat` arm.
-
-**Second target, MATH-500 (`results/llm_compose_gate_math500`, cat + selected weights): composed
-0.200, best parent 0.158, surplus +0.042, union-exceedance 0.100.** The balanced-pair prediction
-holds — a much weaker best parent (0.158 vs 0.427) leaves more headroom, and cat's surplus rises from
-+0.027 to +0.042. The composition effect therefore reproduces on **two independent targets**, at the
-cost of one extra evaluation pass since the founders are shared.
-
-**Gate verdict: PASS on the amended configuration** (directed weight selection; GSM8k-Hard primary,
-MATH-500 as the generality check). Campaign launched 2026-09-07: seed 1 local, seeds 2-3 as CX3 array
-`4000472` (6 elements, 3 arms x 2 seeds).
-
-## 5. Cost
-
-Per generation-arm: 2 lineages × (2700 gated inheritance answers + train ~1000 × 3 epochs) +
-composed eval on 300 GSM-Hard items with execution. At **1.5B**: ≈ 25 min. At **7B**: ≈ 75 min.
-
-| base (from C1) | arms | seeds | G | total |
-|---|---|---|---|---|
-| 1.5B | 3 | 3 | 6 | **≈ 12 GPU-h** (local, overnight) |
-| 3B | 3 | 3 | 6 | ≈ 25 GPU-h (local or 4 CX3 jobs) |
-| 7B | 3 | 3 | 6 | ≈ 40 L40S-h (9 array elements, 6 h each) |
-
-The optional `dry_composed` arm adds a third. Every figure in §3 is drawn from one parquet;
-checkpoint/resume carries over from `society_v2.py`.
-
----
-
-## 6. Anticipated failure modes
-
-- **Gen-0 does not compose (C4 fails).** Most likely cause is the base being too weak for
-  program-aided math at all. C1's escalation should prevent it; if it survives C1, stop — the
-  experiment has no signal to measure the decay of.
-- **Parents don't drift.** If gated self-consumption is *too* good, q stays ≈ 1 and there is nothing
-  to observe. Mitigation: the gate is a knob (v2 measured τ = 0.5 → 0.87 and ungated → 0.81); if q_6 >
-  0.9 in the dry arm at τ = 0.85, drop to ungated, which is the *more* faithful self-consumption
-  channel anyway. Pre-declared, not a post-hoc rescue.
-- **ρ unmeasurable.** Math and code lineages answer disjoint prompt types, so behavioural agreement may
-  be uninformative. Fallback: ρ from LoRA-delta cosine (already implemented in `epistasis.py`) and
-  from agreement on the *shared* GSM-Hard prompts.
-- **Execution verifier flakiness** — timeouts counted as failures, reported as a rate.
-- **GSM-Hard contamination** in an Instruct base: report the base's zero-shot GSM-Hard number; if it
-  is implausibly high, switch to the perturbed-number variant.
-
----
-
-## 7. Engineering checklist
-
-- [ ] `src/llm/execute.py` — sandboxed subprocess execution verifier (timeout, no network, no
-      filesystem writes, deterministic), + tests.
-- [ ] `src/llm/datasets.py` — MetaMathQA / Code Alpaca / GSM8k-Hard loaders, fixed subsets, cached.
-- [ ] `src/llm/compose.py` — `kind: llm_compose`; two lineages, per-generation merge-and-measure,
-      `cat`/`linear`, grounding fraction, confidence gate, checkpoint/resume (port from `society_v2.py`).
-- [ ] q_t / ρ_t instrumentation (reuse `epistasis.generate_with_confidence`, `delta_geometry`).
-- [ ] `configs/llm/compose_calib_{c1..c5}.yaml`, `compose_s{1,2,3}.yaml`, `hpc/llm_compose.pbs`.
-- [ ] `figures/plot_llm_compose.py` (4 panels: S_t per arm; q_t per lineage; ρ_t; observed vs Ĉ_t) and
-      `figures/stats_llm_compose.py` (H2–H6) — **written before unblinding**, as in v2.
-- [ ] Smoke: 1.5B, G = 2, all arms, 50 eval items.
-
-## 8. Outcome → manuscript
-
-| Outcome | What changes |
-|---|---|
-| H1–H4 pass, H3 within tolerance | Fig. 1A's "open — the stated gap" cell is filled by a *different and better* experiment than the one specified: the closed form predicting a real LLM capability trajectory over generations. New figure; the recombination and grounding sections each gain their language-model rung. |
-| H2 + H4 pass, H3 fails | The signs transfer, the quantitative law does not — report as such; the paper's predictive claim stays anchored on the biological model, and the LLM tier is confirmatory (which is what Fig. 1A already says of the other rows). |
-| H1 fails at every base size | No campaign. The SI records the 0.5B transmission ceiling (v2) plus the failure to reproduce a published composition effect in our harness — an infrastructure result, honestly labelled. |
-
-## 9. Decisions for GG
-
-1. **Base escalation cap** — stop at 3B (cheap, local, likely enough) or allow 7B if C1 demands it?
-2. **Optional `dry_composed` arm** (+33% cost): does composition survive when the *merged* model is
-   itself the reproducing lineage? It is the closest thing to the original society question.
-3. **Skill pair** — math × code (best-anchored to the literature) or a second pair alongside
-   (manual × instruction-following) for generality at double the cost?
-4. Whether the v2 no-go and this redesign are worth a short **SI subsection on negative results**, or
-   stay in the repository record only.
diff --git a/tasks/prereg-llm-society-v2.md b/tasks/prereg-llm-society-v2.md
deleted file mode 100644
index adcc1cb..0000000
--- a/tasks/prereg-llm-society-v2.md
+++ /dev/null
@@ -1,563 +0,0 @@
-# Pre-registration — `llm_society` v2: the composed society at LLM scale
-
-**Status (2026-09-07, 21:30): calibrated; NO-GO at 0.5B (GG, §12a); 7B plan in §13 awaiting scope.**
-Supersedes the design in `workorder-llm-society.md` (v1). Calibration record and every amendment are
-in §4a; the campaign was not submitted. Read §1 (what v1 got wrong), §4a (what calibration found),
-§12a (the decision), §13 (what next).
-
-**Why a v2.** The v1 campaign (3 CX3 seeds landed 2026-09-07, `results/llm_society_campaign/`;
-seed 1 still running locally) did not reproduce E11: `no_grounding` degraded (0.575 → 0.436, worst
-arm in every seed), but `full` also declined (→ 0.506) and `no_sex` was flattest (0.558). The
-conformity−truth gap did not separate the arms. Read against the framework, v1 had three
-*structural* faults that the theory would have predicted, plus one power fault. All four are
-diagnosed in §1 and designed out in §3. The point of this document is to make the remaining
-predictions explicit *before* spending the compute, so the campaign can fail informatively.
-
----
-
-## 0. The question and the claims it tests
-
-Does a finite population of LLM agents under the four composed operators — grounded evaluation,
-directed recombination, diversity-preserving selection, lossy inheritance — climb to capability that
-no founder had and hold it, while each ablation fails in its own way? This is E11 at the language-model
-tier: the paper's "open — the stated gap" cell (Fig. 1A).
-
-Claims exercised, and the analytic experiment each rests on:
-
-| Claim | Analytic source | LLM prediction (§5) |
-|---|---|---|
-| Recombination assembles a genotype no parent had (vertical) | E8 (Fisher–Muller, unbounded parents) | H1 |
-| Blending conserves the single-parent level; only union realises the gain | E4 (conservation law), `llm_moe` | H2 |
-| Ungrounded selection → self-consumption → confident, unfit consensus | E11 (conformity mechanism) | H3 |
-| Without recombination, capability is capped at the best founder | E8 control (ρ=1) + no mutation operator here | H4 |
-| Greedy selection collapses diversity faster; QD holds it | E5, E11 | H5 |
-| A capability survives inheritance only if observed often enough | E2 (per-item floor 1−e^{−mp}) | H6 |
-
-**Explicit non-goal.** Grounding here is E11's *selection-channel* grounding
-(`g·fitness + (1−g)·conformity`), not E2's *immigration into the training mix*. No verified answer
-ever enters any child's training data, in any arm. Fig. 1A already states that LLM-scale training-mix
-grounding is established in prior work and not re-run; this campaign does not change that. A
-negative here is evidence against the *selection* mechanism only.
-
----
-
-## 1. What v1 got wrong, read through the framework
-
-| # | Fault | What the theory says | Evidence in v1 | Fix (§3) |
-|---|---|---|---|---|
-| F1 | **Near-clone founders.** 8 agents over 3 families → 3 lists-, 3 strings-, 2 arith-specialists differing only by task draws. | E8 control: recombining ρ=1 parents buys **nothing** (flat at 6 for any K). Pigeonhole: 4 parents from 3 families always contains a same-family pair. | `no_sex` ≥ `full`: merging near-clones is pure perturbation cost. | L = 12 disjoint families, **one founder per family**, ρ = 0 by construction; verified at gen 0. |
-| F2 | **Combinatorial space too small.** 3 skills → 2³ = 8 competence states; founders occupy 3 of them. | E8/E11 use L = 12 (4096 genotypes). The vertical claim needs room for a child to be *new*. | Best possible gain over a founder was tiny. | L = 12 → the best founder holds 1/12 of the space. |
-| F3 | **Blending operator in the dilution regime.** Sex = 2-parent *linear* LoRA merge at 0.5B. | E4 conservation law; `llm_moe` 0.5B: soup dilutes lists 0.43 → 0.26. Headroom law: dilution wherever there is room to lose. | `full` declined while `no_sex` held. | Reproduction by **union-preserving recombination** (confidence-routed union of parents' answers → distil). Linear merge kept as an explicit control arm (`sex_linear`) — H2. |
-| F4 | **Truncation before breeding.** Top-4 of 8 selected as parents; children bred only from them. | E11 selects on *survival over the pooled parents + offspring*, never on breeding eligibility. Truncating parents discards half the alleles at gen 1 with no mutation operator to restore them (E6: loss is permanent). | Half the families were unreachable after gen 1. | Survival selection over the pool (§3.4). Elitism becomes emergent, as in E11. |
-| F5 | **Underpowered evaluation.** `n_test` = 40 → SE 0.079 per measurement. | — | Every contrast except vs `no_grounding` sat inside one SE. | `n_test` = 240 (20/family) → SE 0.032 overall. |
-| F6 | **Weak grounding contrast.** g = 0.5 vs E11's 0.85; G = 10 vs 80. | The conformity gap in E11 needs the population to converge; g = 0.5 leaves conformity with half the vote even in `full`. | Gap flat in all arms. | g = 0.85; G = 12 (§7 explains why 12 suffices here). |
-| F7 | **Transmission floor never measured.** `n_inherit` = 600 over 3 families chosen by feel. | E2: an item survives only if it is *observed* enough in the inheritance sample — the per-item floor. Pilot v1 measured a ~25%/gen "distillation tax" and fixed it by doubling data, without asking where the floor was. | — | Calibration C2 measures the retention curve and sets `n_inherit` from it. |
-
----
-
-## 2. Theoretical predictions → design constraints
-
-Each constraint below is derived, not chosen.
-
-**2.1 Decorrelation (E8, E4).** Union coverage of K parents is U = ρq + (1−ρ)(1−(1−q)^K); the
-gain over a single parent is proportional to (1−ρ). Founders must therefore be as decorrelated as the
-task space allows: one family each, no shared training items, and the gen-0 behavioural-distance
-matrix must show no pair below 0.5 disagreement (gate C1c).
-
-**2.2 Combinatorial headroom (E8).** With one family per founder, q = 1/L. The doubling bound for
-2-parent recombination gives ≥ ⌈log₂ L⌉ = 4 generations to *reach* full coverage under lossless
-inheritance; with per-generation retention r per family the plateau is set by r, not L. So L = 12
-gives headroom; G must exceed 4 by enough to see the plateau: G = 12.
-
-**2.3 Operator (E4, `llm_moe`, `llm_directed`).** At 0.5B on unsaturated families the framework
-predicts linear blending dilutes and union preserves. The society's reproduction operator must be
-union-preserving or the experiment re-measures a known result. The union is implemented in the
-*inheritance data*, not in weight space: for each inheritance prompt the child learns the answer of
-whichever parent is more confident (mean token log-probability of its own answer). This is E4's
-`max` operator applied per item, it is verifier-free (legal in the `no_grounding` arm), and it is
-directed sex in E10's sense — mate choice by complementarity plus per-item selection. Gate C4 checks
-that confidence tracks competence (the routing precondition); gate C3 checks the union child beats the
-linear child on a single 2-founder cross before any campaign money is spent.
-
-**2.4 Selection acts on survival (E11).** E11 pools N parents with n_off offspring and keeps the top
-N by `score + novelty·λ`. Reproducing that exactly gives: elitism for free (a strong parent survives
-by out-scoring its children), no gen-1 truncation, and a directly comparable selection intensity
-(keep 12 of 24 = top ½; E11 keeps 60 of 180 = top ⅓ — pre-noted as a difference).
-
-**2.5 Conformity must be decoupled from truth for H3 to be testable.** E11 initialises random
-genotypes, so its consensus is uninformative at gen 0. In the LLM, consensus is the modal answer
-over agents. With one expert per family, 11 of 12 agents answer any given family's prompt at roughly
-base level, so the modal answer ≈ the base model's answer, and conformity rewards *being base-like*.
-Prediction: consensus accuracy at gen 0 ≈ base overall (gate C5 measures it; it must be < 0.35, i.e.
-well below the best founder's own-family accuracy, otherwise conformity is a truth proxy and the
-`no_grounding` arm cannot fail by the predicted mechanism — see §6 F-alt).
-
-**2.6 The inheritance floor (E2).** A family survives distillation only if the child sees enough
-*correct* examples of it. Retention as a function of examples-per-family, r(k), is measurable
-(C2). Set k* = min k with r ≥ 0.85, `n_inherit` = L·k*. Consequence for a *diluted* skill at accuracy
-a: effective correct examples = a·k*, so skills are lost at the distillation step, not the merge step
-(H6, a mechanism prediction that distinguishes this account from "merging destroyed it").
-
-**2.7 Headroom, not size (the `llm_moe_hard` lesson).** Families must be unsaturated for the
-specialist (≤ 0.9) and non-trivial for the base (≥ 0.05, ≤ 0.4) at 0.5B. C1 rejects families outside
-that band. The three existing easy families straddle it (base lists 0.15 / strings 0.15 / arith 0.53;
-specialists 0.43 / 1.00 / 0.91) — strings saturates, arith's base is high. Both are candidates for
-replacement, decided by C1 not by preference.
-
-**2.8 No cross-family conflict (E12, `llm_epistasis`, `llm_speciation`).** Recombination helps on
-additive landscapes and hurts under functional conflict. Families must not share a prompt shape with
-different answer conventions (e.g. two families that both say "sort" meaning different orders). Gate
-C1b: pairwise confidence-weighted functional conflict between founders (the `llm_epistasis` measure)
-below the grid's no-conflict axis level.
-
----
-
-## 3. Design
-
-**3.1 Base and agents.** Qwen2.5-0.5B-Instruct (frozen). Agent = LoRA r=16 α=32 on attention + MLP
-projections. N = 12.
-
-**3.2 Task space.** L = 12 disjoint procedurally generated families, exact-match verifier, chosen from
-the candidate list in §4 C1 by the calibration band. Fixed pools per family: test 20 (reporting only),
-val 10 (grounded selection signal), conformity 10 fresh per generation (prompts only). Inheritance pool:
-k* per family, fresh per generation.
-
-**3.3 Founders.** Agent i is a specialist on family i, trained on 600 × 3 epochs of that family only,
-seed `master·100 + i`. Gate C1c: gen-0 pairwise behavioural distance ≥ 0.5 for every pair.
-
-**3.4 One generation.**
-
-1. *Produce & score.* Every agent in the current population answers test, val, and conformity pools.
-   Consensus = modal normalised answer per conformity prompt. `fitness` = val accuracy; `conformity`
-   = agreement rate with consensus; `novelty` = mean behavioural distance to the rest.
-   `score = g·fitness + (1−g)·conformity`, g = 0.85 (0 in `no_grounding`).
-2. *Mate.* Form n_off = N pairs by complementarity (greedy maximum behavioural distance, each agent
-   used at most twice), restricted to the current population.
-3. *Recombine (sex ON).* For each pair, each parent answers the inheritance pool once (with mean
-   token log-prob). Union child data: per prompt, the answer of the more confident parent. In
-   `sex_linear`: 6 Dirichlet-weighted linear LoRA merges screened on the arm's own signal (val
-   fitness, or conformity in ungrounded arms), winner answers the pool. Sex OFF: the child's data is a
-   single parent's answers (parent chosen by score-proportional sampling).
-4. *Inherit.* Each child is a fresh LoRA trained from the base on its data, `epochs` from C2.
-5. *Survive.* Pool = N parents + N children. Keep the top N by `score + λ·novelty` (λ = 0.3; λ = 0 in
-   `no_diversity`). Scores for children computed after training, on the same pools.
-
-**3.5 Arms (5).**
-
-| arm | g | recombination | survival |
-|---|---|---|---|
-| `full` | 0.85 | union (confidence-routed) | QD (λ=0.3) |
-| `no_grounding` | 0 | union; routing unchanged (verifier-free) | QD on conformity |
-| `no_sex` | 0.85 | none (single-parent redistillation) | QD |
-| `no_diversity` | 0.85 | union | greedy (λ=0) |
-| `sex_linear` | 0.85 | 2-parent linear merge, 6 screened candidates | QD |
-
-`sex_linear` is the H2 control and is the first arm to drop if budget forces it (§9).
-
-**3.6 Generations and seeds.** G = 12. Seeds 1–4 (fixed before running; not re-drawn). Reporting
-uses the verifier in every arm; the verifier enters the *loop* only where g > 0.
-
----
-
-## 4. Calibration phase (local 16 GB GPU, ≈ 3–4 h, before any submission)
-
-Each gate has a pre-set pass criterion. Fail → stop, fix, re-run the gate. No campaign until all pass.
-
-| Gate | What | Pass criterion | Cost |
-|---|---|---|---|
-| **C1a** family band | Base and specialist (600×3) accuracy on each of ~15 candidate families (100 test items each) | Keep families with base ∈ [0.05, 0.40] and specialist ∈ [0.60, 0.90]; need ≥ 12 | ~60 min |
-| **C1b** no conflict | Pairwise confidence-weighted functional conflict between the 12 founders (from `llm_epistasis`) | Every pair below the `compat` axis level of the epistasis grid | ~15 min |
-| **C1c** decorrelation | Gen-0 behavioural-distance matrix on 120 mixed prompts | min pairwise disagreement ≥ 0.5 | (with C1b) |
-| **C2** transmission floor | Distil a child from a founder's *own* answers with k ∈ {25, 50, 100, 200} examples of its family (rest of the pool mixed), 2 and 3 epochs; measure retained own-family accuracy ÷ founder accuracy | Choose k* = min k with retention ≥ 0.85 at the chosen epochs; if no k ≤ 200 passes, the design is infeasible at 0.5B — stop | ~45 min |
-| **C3** operator | One cross (two founders): union-distil child vs best-of-6 linear-merge-distil child; both families' accuracy | Union child ≥ 0.85 × each parent on that parent's family; union ≥ linear on the *minimum* of the two. If linear ≥ union, H2 is already falsified — record it and reconsider the operator before the campaign | ~20 min |
-| **C4** routing precondition | For each founder: mean log-prob on own-family answers vs off-family; AUC | AUC ≥ 0.7 for ≥ 10 of 12 founders | (with C2) |
-| **C5** consensus anchoring | Consensus accuracy over the 12 founders at gen 0, 120 prompts | < 0.35 (conformity is not a truth proxy) | ~5 min |
-
-C2 also fixes the cost model (§9) — `n_inherit` = 12·k*.
-
----
-
-## 5. Pre-registered hypotheses, thresholds, falsifiers
-
-Primary outcome metric: **best-agent overall test accuracy** at generation G (deployed capability,
-elite included), reported with the **best newborn** (child trained that generation) alongside, so a
-"climb" carried by a surviving founder is visible as such. Secondary: per-family accuracy of the best
-agent (the competence genotype), behavioural diversity, consensus accuracy, conformity−truth gap.
-Reference level **B₀** = best founder overall at gen 0 (≈ (0.7 + 11·base)/12 ≈ 0.24 if base ≈ 0.2;
-measured, not assumed).
-
-| | Prediction (from) | Quantitative threshold | Falsified if |
-|---|---|---|---|
-| **H1** vertical climb | E8: union recombination of decorrelated one-family founders assembles multi-family agents; sigmoidal, most of the climb in gens 1–5, plateau set by r | `full` best-agent at G ≥ B₀ + 0.20 and best newborn at G ≥ B₀ + 0.15; best agent competent (≥ 0.6) on ≥ 6 of 12 families; in ≥ 3 of 4 seeds | best-agent gain < 0.10 in ≥ 2 seeds |
-| **H2** operator | E4 conservation law in the dilution regime | `full` − `sex_linear` ≥ 0.10 at G (paired, per seed); `sex_linear` best agent competent on ≤ 3 families | `sex_linear` ≥ `full` in ≥ 2 seeds. *Pre-stated regime caveat:* this ordering is predicted to **invert** at 7B on easy tasks (headroom law); a 7B follow-up would test that, not this. |
-| **H3** self-consumption | E11 + §2.5: conformity rewards base-likeness; ungrounded selection regresses the population to base and homogenises it | `no_grounding` best agent at G ≤ B₀ + 0.05; conformity−truth gap (`no_grounding` − `full`) ≥ 0.30 at G; consensus accuracy in `no_grounding` non-increasing | `no_grounding` ≥ `full` − 0.05 on best agent, **or** gap difference < 0.10 |
-| **H4** sex necessity | E8 ρ=1 control + no skill-acquisition operator without recombination | `no_sex` best agent at G ≤ B₀ + 0.05 in every seed (a *ceiling*, stronger than E11's ~1-point effect) | `no_sex` gains ≥ 0.10 over B₀ in any seed → an unmodelled acquisition route exists (base competence amplified by self-distillation); report it |
-| **H5** diversity | E5/E11: greedy converges earliest; QD holds | AUC of behavioural diversity `full` > `no_diversity` in ≥ 3 seeds; `no_diversity` diversity < 0.1 by gen ≤ 6. **Low power on best fitness pre-declared** (E11: 0.78 vs 0.74) | no ordering in diversity AUC |
-| **H6** where skills die | E2 floor: loss occurs at distillation when correct examples/family < k*·a | For families lost between t and t+1 in `full`, the *source* (union answer set) accuracy on that family at t is ≥ 0.6 in ≤ 20% of cases — i.e. skills that were competently supplied are retained; skills die because they arrived diluted | ≥ 40% of lost families were supplied at ≥ 0.6 → the distillation channel itself is lossy beyond the calibrated floor; revisit C2 |
-
-Analysis is per-seed paired contrasts (4 seeds), reported as mean ± 95% CI and sign count. No metric
-introduced after unblinding is called a result. All rows above are also plotted whether or not they
-pass.
-
----
-
-## 6. Anticipated failure modes and how each is handled
-
-- **F-alt (conformity anchored to truth).** If C5 shows consensus accuracy ≥ 0.35, the `no_grounding`
-  arm may fail by drift rather than by confident-wrong consensus (v1 workorder falsifier 3). Then H3's
-  gap threshold is not testable; run anyway, report the observed signature, and say so.
-- **Terminal degeneration.** A source that emits < 8 usable answers: copy the parent unchanged (existing
-  sentinel). Count and report occurrences per arm.
-- **Family extinction is permanent (E6).** No mutation operator reintroduces a lost family. Pre-state:
-  the number of families alive in the population is itself a reported curve; `full` is predicted to
-  hold ≥ 10 of 12 to G, `no_diversity` fewer.
-- **Router failure (C4 fails).** Fall back to self-consistency routing (two samples, prefer the parent
-  whose answers agree); re-run C4. If still failing, the union operator has no verifier-free
-  implementation at this scale — record and consider 7B.
-- **Elite lock-in.** A founder that survives to G on score alone makes "best agent" flat. Best newborn
-  is co-primary for exactly this reason.
-- **Screening noise in `sex_linear`.** 6 candidates on 120 val items (SE 0.046) — adequate for choosing
-  among merges that differ by ≥ 0.1, which is the dilution scale.
-- **Queue / wall-time loss.** Per-generation checkpoint (rows flushed to parquet; adapters on disk)
-  and `--resume`; each PBS array element = one (seed, arm), ≤ 6 h.
-- **Environment drift.** Never `uv sync` on a machine with a running job (`tasks/lessons.md`).
-
----
-
-## 7. Power
-
-SE of an overall accuracy at 240 items, p ≈ 0.5: 0.032. Per-family at 20 items: 0.11 (per-family
-readouts are descriptive only). Predicted effects: H1 ≥ 0.20, H2 ≥ 0.10, H3 ≥ 0.20 on best agent and
-≥ 0.30 on the gap, H4 a ceiling — all ≥ 3 SE. H5 on best fitness is predicted small and is not powered;
-its diversity readout is. Four seeds give a sign test at p = 1/16 one-sided for a 4/4 outcome; the
-primary analysis is the paired mean and CI, the sign count is descriptive.
-
-Why G = 12 suffices: the climb needs ⌈log₂ 12⌉ = 4 doublings; the E11 `no_grounding` crash occurred
-by generation 6 of 80; diversity collapse in E11's ablations by 10–15. Twelve generations covers
-every predicted transition with margin to see the plateau.
-
----
-
-## 8. Analysis and figure (fixed now)
-
-Figure, E11 layout plus one panel: **(A)** best-agent and best-newborn overall accuracy per arm over
-generations, B₀ dashed; **(B)** behavioural diversity; **(C)** conformity−truth gap; **(D)** competence
-heat-map — families × generations for the `full` best agent, with `sex_linear` beside it. Mean ± 95% CI
-over seeds. Script `figures/plot_llm_society.py`, reading only the committed bundles.
-
-Statistics: per-seed paired contrasts at G for H1–H4; AUC contrast for H5; the supplied-vs-retained
-tabulation for H6. `figures/stats_llm_society.py`.
-
----
-
-## 9. Compute and schedule
-
-Measured anchor: v1 seed (N=8, G=10, 4 arms, n_inherit=600, 3 epochs) ≈ 3.3 L40S-hours.
-
-Per generation-arm at v2 defaults (N=12, 24-agent pool eval on 480 prompts; 12 parents answering the
-inheritance pool once each with log-probs; 12 children trained at 12·k* × epochs):
-
-| k* from C2 | training | inheritance answers | eval | per gen-arm | per (seed, arm), G=12 | campaign, 4 seeds × 5 arms |
-|---|---|---|---|---|---|---|
-| 50 | ~6 min | ~3 min | ~4 min | ~13 min | ~2.6 h | **~52 L40S-h** |
-| 100 | ~12 min | ~5 min | ~4 min | ~21 min | ~4.2 h | **~84 L40S-h** |
-
-Submitted as a 20-element PBS array (seed × arm), 6 h wall-time each, checkpointed. Dropping
-`sex_linear` saves 20%. The local GPU runs one seed's `full` + `no_grounding` in parallel as a hedge.
-
-**7B.** ≈ 4× per operation → ~200–340 L40S-h for the full grid: not a first shot. Pre-registered role
-for 7B: a **1–2 seed confirmation of `full` vs `no_grounding` and of the H2 inversion**, run only if
-0.5B passes H1 and H3. On the *easy* families 7B is predicted to be in the composition regime, so
-`sex_linear` should catch up with `full` there — a positive prediction of the headroom law, not a
-replication.
-
-**Schedule.** Calibration day 1 (local). GG gate on C-results. Engineering (§10) days 1–2. Campaign
-submission day 2–3; wall-clock ≈ 1 day if the queue cooperates. Analysis + figure day 4.
-
----
-
-## 10. Engineering checklist (before submission)
-
-- [ ] `tasks.py`: ~15 candidate families with verifier formats (int / int-list / lowercase word /
-      uppercase word); `FAMILIES` becomes config-driven.
-- [ ] `society.py`: survival-over-pool selection replacing parent truncation; complementarity pairing
-      over the whole population; confidence-routed union inheritance (needs `generate()` to return
-      mean token log-prob); `sex_linear` arm; per-generation parquet flush + `--resume`; log the
-      *source* per-family accuracy before distillation (for H6) and families-alive per generation.
-- [ ] Tests for the pure pieces (union routing, pooled survival, pairing constraint) — extend the 155.
-- [ ] `configs/llm/society_v2_calib_*.yaml`, `society_v2_s{1..4}.yaml`; `hpc/llm_society_v2.pbs` array.
-- [ ] `figures/plot_llm_society.py`, `figures/stats_llm_society.py` written **before** unblinding,
-      against the smoke bundle.
-- [ ] Smoke: N=4, L=4, G=2, all 5 arms, exit 0, figure renders.
-
----
-
-## 11. Outcome → manuscript
-
-| Outcome | What changes in the paper |
-|---|---|
-| H1 ∧ H3 pass (H2, H4, H5 whatever they are) | Fig. 1A cell "open — the stated gap" → filled; new figure (§8) enters as the LLM tier of the society; Table S2 row; the Discussion's prescriptive claim gains its LLM instantiation. |
-| H1 fails, H2 passes | The vertical claim does not transfer at 0.5B but the operator law does: report as a bounded negative in SI with the H6 diagnosis; Fig. 1A cell becomes "tested at 0.5B: operator law holds, climb does not"; 7B confirmation becomes the open item. |
-| H3 fails (no self-consumption signature) with C5 passed | The selection-channel grounding mechanism does not transfer; state it, keep the E11 result as biological-model-only; nothing prescriptive at LLM scale. |
-| C-gates fail | No campaign. The gate result itself goes in the SI as the reason the tier was not run. |
-
----
-
-## 12. Decisions (GG, 2026-09-07)
-
-1. **Scale:** 0.5B full grid; 7B only as the gated confirmation of §9. *Decided.*
-2. **`sex_linear` arm:** **dropped from the first campaign** — founders are cached and shared, so it
-   can be appended later at ~20% of the grid cost, and the operator law is already established by
-   `llm_moe` (Fig. 3B). The code path stays (`arm_settings("sex_linear")`); H2 is therefore
-   *deferred*, not tested, in this campaign. Four arms × four seeds = 16 array elements.
-3. **Family candidates:** no vetoes; calibration C1 decides membership. Seventeen candidates are
-   implemented in `src/llm/families.py` (the three originals + fourteen new). Word-order reversal and
-   run-length encoding were dropped at implementation because the verifier cannot score multi-word or
-   alphanumeric answers; `sortletters`, `caesar` and `charfreq` use random pseudo-words so a 600-item
-   training set cannot cover the test space.
-4. **Go/no-go gate after calibration** — GG reviews the C-table before anything is submitted.
-   *Standing.*
-
-## 4a. Calibration record and amendments
-
-**Stage A, pass 1 (2026-09-07, `results/llm_society_v2_calib_a`, founders 600×3).** Only **6 of 17**
-families in band: setops, numtheory, mixedtoken, vectors, digits, alphabet. Out of band: lists and
-binary under-trained (specialist 0.44); strings 0.91, roman 0.97, prime 1.00 above the specialist
-ceiling; arith base 0.53 (the base already knows it — violates the sole-expert premise); liststats and
-charfreq weak (0.38; charfreq also fails the routing AUC, 0.44); sortletters 0.20, caesar 0.01,
-progression 0.10 unlearnable at this budget. C1b max pairwise conflict 0.348 (just under the gate),
-every top pair involving a *failed* specialist answering confidently wrong; C1c min distance 0.65.
-Smoke passed on all four arms (figure + stats script exercised).
-
-**Amendments before pass 2 — recorded here because they change what §4 promised:**
-
-1. **Specialist upper bound 0.90 → 1.00.** The bound encoded the `llm_moe_hard` headroom lesson,
-   which concerns *fusion composing to a ceiling* so that soup matches routing. The society uses
-   union inheritance, and the quantities under test are transmission and assembly, for which a
-   founder at 0.97 is not a problem. The **base** bound (≤ 0.40) is kept strict: it protects the
-   sole-expert premise and C5's consensus decoupling. Consequence: strings, roman, prime become
-   eligible; arith stays out unless the count forces it (then flagged).
-2. **Founder budget 600×3 → 1200×3, uniform**, to recover the under-trained lists and binary.
-3. **Prompt spaces enlarged** for roman (1–999), binary (1–511), prime (≤ 400) so a 600-item training
-   set cannot cover the test space (pass 1: 300 / 200 / 210 unique of 600).
-4. **Three candidates added** — `wordlen`, `lettercount`, `sumeven` (counting and filtered sums;
-   verifier-safe; large prompt spaces). Three dropped without retraining: sortletters, caesar,
-   progression (specialist ≤ 0.20).
-
-None of these touches a hypothesis, a threshold, or the campaign design; they change which families
-are *eligible*. Pass 2 is `configs/llm/society_v2_calib_a2.yaml`.
-
-**Stage A, pass 2 (`results/llm_society_v2_calib_a2`, founders 1200×3).** Nine in band: strings 1.00,
-setops 0.85, numtheory 0.83, mixedtoken 0.74, digits 0.97, alphabet 0.97, prime 0.96, wordlen 0.92,
-lettercount 0.77 (specialist accuracies; all bases 0.08–0.30). Roman misses only the base *floor*
-(base 0.04, specialist 0.99). The borderline families did **not** converge with more training —
-vectors 0.61 → 0.53, lists 0.44 → 0.50, binary 0.44 → 0.22, charfreq 0.38 → 0.55, liststats
-0.38 → 0.47 — so they are noise-level at 0.5B, not under-trained; sumeven is unlearnable (0.06).
-C1c min distance 0.68 (pass). C4: 9 of 10 candidates have confidence AUC ≥ 0.7 (mixedtoken 0.62).
-
-**C1b fails among the ten.** Max pairwise conflict 0.439 (wordlen × lettercount); five pairs ≥ 0.35,
-all among the *counting* families (wordlen, lettercount, digits, strings, mixedtoken): two specialists
-that both confidently answer "How many …?" with different small integers is precisely E12's
-conflicting-convention hazard, and under confidence-routed union the wrong one can win the prompt.
-The largest subset with every pair < 0.35 has **seven** members. Dropping lettercount (the hub, 7
-conflicts) leaves nine with max 0.396.
-
-**Re-deriving the gate from the grid, rather than from a midpoint.** 0.35 was chosen as midway between
-the grid's no-conflict axis (0.20–0.26) and its conflict axis (0.46–0.52). The grid's own outcome data
-say where the break is: P(merge penalty > 0.02) is **0.29 for epi_conf < 0.35, 0.22 for
-[0.35, 0.41), and 0.77 for ≥ 0.41** (n = 17 / 9 / 13). Below 0.41 the measure does not predict a
-penalty; above it, it does. A gate at **0.41** is therefore the data-derived boundary, and the
-nine-family set passes it (max 0.396).
-
-**Options put to GG (2026-09-07 evening):**
-1. *L = 9, gate 0.41* — strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen,
-   roman. Two amendments: gate 0.35 → 0.41 (grid-derived, above), base floor waived for roman (the
-   floor screened for unlearnable tasks; roman's specialist at 0.99 settles that). H1's family
-   threshold scales to ≥ 5 of 9. *Recommended.*
-2. *L = 7, gate 0.35 as written* — alphabet, mixedtoken, numtheory, prime, roman, setops, strings.
-   Weaker combinatorics (128 states, ~2.8 doublings) but no gate amendment.
-3. *Prompt tags per family* to suppress off-family confidence, then re-calibrate everything (~1.5 h).
-   Removes the conflict by construction; also makes routing trivially lexical (the `llm_moe` rider).
-
-**GG decision (2026-09-07, 20:30): option 1.** L = 9: strings, setops, numtheory, mixedtoken, digits,
-alphabet, prime, wordlen, roman. C1b gate amended 0.35 → 0.41 (grid-derived); base floor waived for
-roman. Consequential edits to §3/§5/§7: N = 9 agents; `n_test` 27/family (243 overall, SE 0.032);
-`n_val` 13/family; `n_conf` 13/family; H1's family threshold ≥ 5 of 9; B₀ ≈ (spec + 8·base)/9.
-Stage B launched on the nine (`society_v2_calib_b.yaml`; probe families setops / alphabet / digits;
-cross setops × alphabet).
-
-**Stage B, C2 transmission (`results/llm_society_v2_calib_b`) — FAILS as pre-registered.** Retention of
-a founder's own family in a child distilled from the founder's own answers, k examples per family × 9
-families, 2 or 3 epochs:
-
-| k | setops | alphabet | digits |
-|---|---|---|---|
-| 25 | 0.31–0.38 | 0.30–0.34 | 0.59–0.69 |
-| 50 | 0.67–0.73 | 0.50–0.64 | ~0.71 |
-| 100 | 0.62–0.67 | 0.57–0.67 | — |
-| 150 | 0.61–0.73 | 0.73–0.81 | — |
-
-No k ≤ 150 reaches 0.85; the curve is flattening. This is **not** the E2 observation floor — the
-source *supplied* the family at 0.77–1.00 accuracy, so the items were observed. It is interference: a
-one-family founder's inheritance data is one competent family and eight families of confident
-garbage, and a fresh LoRA fits all nine. That is the mechanism behind pilot v1's "distillation tax",
-now measured at 20–40% per generation. Under the pre-registered rule the ungated design is infeasible
-at 0.5B.
-
-**Proposed amendment (measured before adoption, C2b — `society_v2_calib_c2b.yaml`):
-confidence-gated inheritance.** The child learns only the prompts its source is confident on
-(exp mean token log-prob ≥ τ). Verifier-free; identical in every arm; makes the child *agnostic*
-rather than *wrong* off-expertise — E8's founder model. C2b measures retention by τ, the Youden τ*
-separating own- from off-family confidence (calibration uses family labels; the campaign uses the
-fixed τ), and the off-family harm of ungated inheritance. Adoption requires GG's sign-off because it
-changes §3.4 step 4. Implemented as `conf_gate` in `society_v2.py` (default None = ungated).
-
-**C2b (`results/llm_society_v2_calib_b_transmission_conf`) — the gate passes at τ = 0.5, and the
-mechanism is two-part.** Source answers a 300-per-family pool (2700 prompts); child keeps prompts with
-source confidence ≥ τ; 3 epochs. Retention (child own-family ÷ founder):
-
-| | ungated | τ = 0.5 | τ = 0.7 | τ = 0.85 |
-|---|---|---|---|---|
-| setops | 0.69 | **0.93** | 0.93 | 0.94 |
-| alphabet | 0.86 | 0.85 | 0.88 | 0.82 |
-| digits | 0.89 | 0.83 | 0.76 | — |
-| mean | 0.81 | **0.87** | 0.86 | — |
-
-Two drivers, not one. (i) **Pool size**: at k = 300 alphabet and digits already retain ≥ 0.86
-ungated (they were 0.73–0.81 at k = 150). (ii) **Confidence gating** rescues the family pool size does
-not (setops 0.69 → 0.93) and is neutral-to-slightly-negative where the specialist is confident
-everywhere (digits: off-family confidence median 0.67 vs 0.45 for setops — the gate discards data
-without separating). Youden τ* ≈ 0.95–0.97 in all three (own-family confidence median 1.00), so
-τ = 0.5 is a *mild* gate keeping 50–75% of the pool. **Off-family harm: not confirmed.** Ungated children
-score at base off-family (0.19–0.27 vs base 0.20–0.22), not below it; gated children slightly above.
-Interference costs own-family retention; it does not push off-expertise competence below the prior.
-
-**Adopted for the campaign (pending GG go/no-go on the full table): `k_inherit` = 300,
-`conf_gate` = 0.5, `epochs` = 3.** Mean retention 0.87 ≥ 0.85 meets the pre-registered C2 criterion
-under the amended channel. Cost consequence (§9): per-generation inheritance answers 2700 prompts per
-parent and children train on ~1300–2000 kept examples → ≈ 30 min per generation-arm, ≈ 6 h per
-(seed, arm) at G = 12, **≈ 96 L40S-h** for 16 elements; PBS walltime raised to 8 h.
-
-**Stage B, C3 cross setops × alphabet (`results/llm_society_v2_calib_b_cross`) — operator half
-passes, retention half fails for C2's reason.** Parents 0.84 / 0.96. Union child 0.57 / 0.62 — holds
-*both* families, routed 53% of prompts to the alphabet parent. Linear-blend child (best of 6 screened)
-0.66 / 0.30 — keeps one family and loses the other: E4's dilution, in the operator the campaign
-dropped. Union ≥ linear on the minimum (0.57 vs 0.30) ✓. Union ≥ 0.85 × parent ✗ (0.68× / 0.65×) — the
-same transmission tax as C2. The cross is re-run under the confidence gate if C2b passes
-(`conf_gate` is now a cross-stage option; both children gated by their own source's confidence).
-
-**C3 re-run under the gate (`results/llm_society_v2_calib_b_cross_gated`, k = 300, τ = 0.5) — fails
-for a NEW reason.** Union child setops 0.46 / alphabet 0.73 (0.55× / 0.76× of parents) — both held,
-both diluted. Linear child 0.80 / 0.22 — one skill at 0.95×, the other lost. Read together with C2b
-(a *one*-skill child retains 0.87–0.93 under the same gate and budget): a single skill transmits; a
-**two-skill child dilutes each skill by 25–45% even from union-preserved data.** This is E4's
-conservation law relocated from the sample budget to the *learning* budget — a fixed adapter (r = 16)
-and fixed epochs split across skills. Under it H1 (≥ 5 families at ≥ 0.6 in one agent) is predicted to
-fail by construction, whatever the operators do. F8 in the fault ledger.
-
-**C3b (before deciding anything): does capacity or budget lift the two-skill child?** Three variants
-of the gated cross — 6 epochs at r = 16; r = 64 (α = 128) at 3 epochs; both. Pass criterion as C3:
-union child ≥ 0.85 × parent on *each* family. If one passes, the campaign adopts that child budget
-(cost re-estimated). If none passes, the vertical claim cannot be tested at 0.5B with self-distilled
-inheritance, and the honest options are a reduced campaign (H3–H5 only, which do not need multi-skill
-children) or 7B.
-
-**C3b results (`results/llm_society_v2_calib_c3b_*`; union child accuracy and ×parent):**
-
-| child budget | setops | alphabet | verdict |
-|---|---|---|---|
-| r16, 3 ep, τ 0.5 (C3 gated) | 0.46 (0.55×) | 0.73 (0.76×) | fail |
-| **r16, 6 ep, τ 0.5** | **0.73 (0.87×)** | **0.79 (0.82×)** | at the gate within noise (SE ≈ 0.06 on the ratio) |
-| r64, 3 ep, τ 0.5 | 0.72 (0.90×) | 0.58 (0.67×) | fail; r64 founders weaker (0.80/0.87) and less confident (35% routed to alphabet) |
-| r64, 6 ep, τ 0.5 | 0.35 (0.44×) | 0.50 (0.57×) | fail — overfits |
-
-**Budget, not capacity, is the lever; rank stays 16.** The kept count explains the residual: at τ = 0.5
-the union child kept 1941 of 2700 prompts, of which only ~600 are its two competent families — the
-mild gate passes the *max* of two parents' confidences, so ~70% of the child's data is confident
-garbage. C2b's own table had τ = 0.85 as the best mean retention (0.88) at a third of the data.
-**C3c** (last calibration run): the cross at τ = 0.85, 3 and 6 epochs.
-
-**Prediction update carried into the go/no-go, whatever C3c says.** A two-skill child retains ≈ 0.85×
-per skill at best; H1 as written (best agent ≥ B₀ + 0.20, ≥ 5 of 9 families at ≥ 0.6) needs five or
-six skills co-resident at ≈ 0.6 in one r = 16 adapter, which the calibration does not support at 0.5B.
-The realistic bar the calibration *does* support — **H1′: children holding 2–3 families beat every
-founder on overall accuracy (≥ B₀ + 0.05) and the best agent climbs monotonically for ≥ 3
-generations** — is recorded now, before the campaign, as the primary vertical readout, with H1 kept
-as the stretch criterion. H3, H4, H5 do not need multi-skill agents and are unchanged.
-
-**C3c results (`results/llm_society_v2_calib_c3c_*`, τ = 0.85):** 3 epochs → union child 0.72 / 0.79
-(0.86× / 0.82×), kept 1041 of 2700; 6 epochs → 0.69 / 0.75. The tight gate reproduces the 6-epoch
-mild-gate retention at half the training, and more budget beyond that buys nothing: a two-skill child
-plateaus at ≈ 0.85× / 0.8× of its parents. **Final child budget: τ = 0.85, 3 epochs, k = 300, r = 16.**
-Cost re-estimate (§9): inheritance answers (9 parents × 2700 prompts with log-probs) now dominate at
-≈ 11 min per generation-arm; training ≈ 9 min; evaluation ≈ 3 min → ≈ 25 min per generation-arm,
-≈ 5 h per (seed, arm), **≈ 80 L40S-h** for 16 elements. Walltime 8 h.
-
-**Stage B, C5 consensus (`results/llm_society_v2_calib_b_consensus`) — passes.** Consensus accuracy over
-the nine founders at gen 0 = 0.31 (< 0.35); corr(conformity, own accuracy) = +0.29; min pairwise
-distance 0.63. Conformity is not a truth proxy here, so H3 is testable by the predicted mechanism.
-
-## 12a. Go/no-go (GG, 2026-09-07, 21:30): **NO-GO at 0.5B; plan 7B.**
-
-Grounds: the calibration passed C1, C4, C5, and — under the amended inheritance channel — C2, but C3's
-retention half exposed a ceiling no budget moves: a two-skill child holds each skill at ≈ 0.85× / 0.8×
-of its parents, and the vertical claim needs five or six skills co-resident in one r = 16 adapter. At
-0.5B the society experiment could test H3–H5 but not the claim the paper's stated gap is about.
-Nothing is submitted. What today produced is a **measured transmission ceiling for self-distilled LoRA
-inheritance at 0.5B** — three mechanistically distinct limits (near-clone founders; interference from
-confident off-expertise answers, 20–40%/generation, removable by a confidence gate; the multi-skill
-learning-budget plateau) — and it goes in the SI as the reason the 0.5B tier was not run (§11, row 4).
-
-## 13. The 7B plan (for GG review; nothing runs without a go)
-
-**What changes at 7B, and why it is not a re-run.** The premise "one founder is the sole expert on its
-family" requires a base that *cannot* do the family. Qwen2.5-7B-Instruct already scores 0.99 on easy
-arith, 0.69 on easy strings, and will be high on roman / binary / setops / digits; the base bound
-(≤ 0.40) will exclude most of the current nine. The hard variants exist for three families only (7B
-base: lists 0.34, strings 0.67, arith 0.48). **Phase 0 is therefore task design**: ≥ 9 disjoint
-families with 7B base ≤ 0.4 and specialist ≥ 0.8 — multi-step, cipher, and compositional variants of
-the current generators — plus the C1 band and conflict gate re-run at 7B. This is a day of work before
-any GPU time, and it cannot be done on the 16 GB local card (7B training needs the L40S), so every
-calibration step goes through the CX3 queue (183 queued at last check).
-
-**Cost anchors (L40S, from `llm_merge_hpc` / `llm_moe_hpc` / `llm_hard`):** 7B generation ≈ 10
-prompts/s (0.5B ≈ 40); 7B LoRA SFT ≈ 15 example-passes/s (0.5B ≈ 60). Per generation-arm at N = 9,
-k = 300, τ = 0.85, 3 epochs: inheritance answers 9 × 2700 / 10 ≈ 40 min; evaluation 18 × 373 / 10
-≈ 11 min; training 9 × (≈ 1000 × 3 / 15) ≈ 31 min → **≈ 80 min per generation-arm**.
-
-| scope | elements | G | per element | total L40S-h | walltime |
-|---|---|---|---|---|---|
-| Phase 1 — calibration A + B at 7B | 2 jobs | — | ~2 h each | **~4** | 4 h |
-| Headline: `full` vs `no_grounding`, 3 seeds | 6 | 10 | ~13 h | **~80** | 2 × 8 h with resume, or one 16 h |
-| H3 + H4: 3 arms, 3 seeds | 9 | 10 | ~13 h | **~120** | as above |
-| Full grid: 4 arms, 4 seeds | 16 | 12 | ~16 h | **~260** | 3 × 8 h with resume, or one 24 h |
-
-Checkpoint/resume already makes multi-requeue elements safe. A vLLM generation path would cut the
-dominant 40-minute term by 5–10× but adds a dependency and a second code path; noted, not proposed.
-
-**Gates carried over unchanged:** C1 band (base ≤ 0.40 strict, specialist ≥ 0.60; upper bound 1.0), C1b
-conflict < 0.41, C1c distance ≥ 0.5, C2/C2b retention ≥ 0.85 (gated channel), C3 union ≥ 0.85× per
-family on a two-founder cross, C4 AUC ≥ 0.7 for ≥ 8 of 9, C5 consensus < 0.35. **The 7B-specific
-prediction that decides whether to proceed past Phase 1:** with the larger adapter margin at 7B, the
-two-skill cross should clear 0.85× on *both* families at 3 epochs. If it does not, the multi-skill
-plateau is not a 0.5B artefact and the vertical claim should be pursued with a different inheritance
-channel (e.g. inheriting *weights*, not answers — which is what `llm_merge_hpc` already showed
-composes at 7B) rather than with more scale.
-
-**Hypotheses:** H1 restored as written (≥ 5 of 9 families at ≥ 0.6, ≥ B₀ + 0.20) — that is the point of
-going to 7B; H1′ kept as the fallback readout; H3–H6 unchanged; H2 deferred.
-
-**Decisions for GG before Phase 0 starts:** (i) scope row from the table; (ii) whether Phase 0 task
-design is worth the day, given the alternative in the prediction paragraph above; (iii) whether the
-SI text for the 0.5B ceiling (§11 row 4) is drafted now or after 7B.
-
-## 14. Build log
-
-- 2026-09-07 — `families.py` (17 candidates, all self-verifying and deterministic), `society_ops.py`
-  (pooled survival, capped complementary mating, confidence-routed union, score-proportional single
-  parent), `society_v2.py` (`kind: llm_society_v2`; per-generation checkpoint + resume; founder lock
-  for concurrent arm-jobs; H6 source diagnostics; families-alive), `calibrate.py`
-  (`kind: llm_society_calib`, stages families / transmission / cross / consensus), configs
-  (`society_v2_calib_a/b`, `society_v2_smoke`), `hpc/llm_society_v2.pbs` (16-element seed × arm
-  array), `figures/plot_llm_society.py` (the §8 layout, written before unblinding), 9 new pure tests
-  (164 green). v1 code path untouched and still green. Smoke → calibration A launched locally.
diff --git a/tasks/prereg-llm-society-v4.md b/tasks/prereg-llm-society-v4.md
deleted file mode 100644
index 22d2c35..0000000
--- a/tasks/prereg-llm-society-v4.md
+++ /dev/null
@@ -1,631 +0,0 @@
-# Pre-registration — `llm_curriculum` v4: does a society accumulate more than its members?
-
-**Status:** draft for GG review, 2026-09-08. Supersedes `prereg-llm-compose-v3.md` (run; H1 passed,
-H2–H5 null and uninterpretable). Nothing runs until §5's gates pass and GG signs off §8.
-
----
-
-## 0. Why v4: v3 measured the wrong thing
-
-v3 had a **fixed skill set**. Two founders were trained once and every later generation was a lossy
-copy, so the experiment's ceiling was its own generation 0 and no outcome could have shown capability
-climbing. It answered "do ancestral skills degrade?", a retention question. The paper's claim (C3) is
-that capability **climbs** — each specialty re-earned and exceeded. GG, 2026-09-08: *"are models
-learning NEW skills at EACH generation? or are we just seeing if the ancestral skills degrade?
-because that was not the problem being addressed… we need to ground this into continual learning."*
-
-The specific technical fault: v3 trained **a fresh LoRA from the base each generation**, so knowledge
-survived only through the data channel. That is Weismannian — nothing acquired is inherited as
-structure. v4's children **start from their parent's adapter**, which is the actual Lamarckian
-channel and the precondition for accumulation.
-
-**Pattern across v2 → v3 → v4, recorded so it stops recurring:** each design was checked against the
-*mechanism* (drift, immigration, recombination) and never against the *claim*. §5's gate G0 exists
-solely to check the claim is reachable before any compute is spent.
-
----
-
-## 1. Design
-
-**Curriculum.** Nine task families from v2's calibrated set (`llm_society_v2_calib_a2`: base ≤ 0.40,
-specialist ≥ 0.60, pairwise conflict < 0.41 — strings, setops, numtheory, mixedtoken, digits,
-alphabet, prime, wordlen, roman). Three lineages, nine generations. **Each lineage sees all nine
-families in a different order** (a cyclic Latin square), so at generation *t* every lineage has met
-*t* families but **different ones**. Complementarity is maximal early and decays to zero by
-generation 9 — a shape the analysis can test, not just a condition it assumes.
-
-**One generation, per lineage:**
-1. **Acquire** — the environment presents the next family; train on `n_new` verified real examples.
-2. **Inherit** — training starts from the *parent's adapter*, not the base (Lamarckian transmission).
-3. **Maintain** — old families are kept alive by `n_replay` real examples (grounding = immigration),
-   or by self-generated answers (dry), or not at all, depending on arm.
-4. **Recombine** — merge with the arm's partner (contemporary, ancestor, or nobody), weights chosen
-   on a held-out validation split (directed recombination, carried over from v3).
-
-**Two external-information channels, deliberately separated** — v3 conflated them. *Acquisition* is a
-capability the population never had (novel allele; moves the frontier). *Replay* is re-supply of a
-capability already present (immigration proper; fights loss, never advances). They fight different
-diseases and must be separate factors.
-
-## 2. Arms
-
-| arm | recombines with | old skills maintained by | isolates |
-|---|---|---|---|
-| `isolated` | nobody | real replay | asexual continual learning — the drift/forgetting baseline |
-| `society` | a decorrelated contemporary | real replay | the treatment |
-| `society_dry` | contemporary | self-generated only | replay's contribution (E2 immigration) |
-| `seed_bank` | **its own ancestor at t−3** | real replay | see below — this is not a throwaway control |
-
-**The seed-bank arm is a substantive comparison, not a null.** My first reading was that an ancestor
-is your own lineage and therefore highly correlated (ρ→1), so E8 predicts it buys nothing. Working
-through the curriculum shows that is wrong: **your t−3 ancestor knows exactly the families you learned
-three generations ago and have since been forgetting.** It carries *temporal* complementarity where a
-contemporary carries *spatial* complementarity. So the arms pose a real question with predictions
-pulling opposite ways:
-
-- E8 (decorrelation is the fuel): the contemporary is more decorrelated → larger union → `society` wins.
-- E12 (merge compatibility): the ancestor is same-lineage, so no Bateson–Dobzhansky–Muller
-  incompatibilities have had time to accumulate → it merges *more safely* → `seed_bank` wins.
-
-Which dominates is not obvious from the framework, and the answer is directly translational: *when a
-model forgets, is it better recovered from a peer who knows something else, or from your own earlier
-checkpoint?* Nobody has posed that as a population-genetic question.
-
-**Dropped (GG, 2026-09-08):** a `no_acquisition` arm — v3 already is that experiment.
-
-## 3. The mono-generational baselines, and why they are the falsifier
-
-The difficulty GG named is pairing a multigenerational design against single-shot SoTA. Rather than
-work around it, it becomes the spine: **does structure across time beat the same compute spent all at
-once?** Three references, all at matched total training examples:
-
-| baseline | what it is | role |
-|---|---|---|
-| `sequential` | one model, all nine families in sequence | the standard continual-learning baseline |
-| `single_shot_merge` | three specialists trained from base in parallel (three families each), merged **once** at the end | actual SoTA — LoRA Soups / TIES / model soup |
-| `joint` | one model trained on all nine jointly | the conventional ceiling |
-
-**Budget accounting (must be equal, and is checked in the artifact).** A lineage trains
-9 generations × (`n_new` + `n_replay`) examples; three lineages give 27 × (`n_new` + `n_replay`).
-Each single-shot specialist gets 9 × (`n_new` + `n_replay`) so three of them match exactly;
-`sequential` and `joint` receive the same total. Compute per arm is recorded in the manifest.
-
-**If `society` does not beat `single_shot_merge` at matched budget, iterating buys nothing and the
-multigenerational framing is decoration.** That is the claim worth staking, and the merging
-literature has never tested it because every paper in it merges once.
-
-## 4. Hypotheses
-
-Primary outcome: **cumulative capability** — accuracy of the population's best model on *all nine
-families*, at each generation. Secondary: per-family forgetting curves, union-exceedance at each
-merge, ρ between partners, and the complementarity decay predicted by the Latin-square design.
-
-| | prediction (source) | threshold | falsified if |
-|---|---|---|---|
-| **H1** | Capability **climbs**: `society` at generation 9 exceeds its own generation 1 by ≥ 0.15 | ≥ 0.15 in ≥ 2 of 3 seeds | flat or declining → the design still cannot show accumulation (this is G0 restated as a result) |
-| **H2** | Fisher–Muller (E7): `society` > `isolated` on cumulative capability at generation 9 | ≥ +0.08, 3/3 seeds positive | recombination adds nothing over isolated continual learning |
-| **H3** | **The multigenerational claim**: `society` > `single_shot_merge` at matched budget | ≥ +0.05 | iterating buys nothing; the framing is decorative and the paper should say so |
-| **H4** | Immigration (E2): `society` > `society_dry` | ≥ +0.08 | self-generated replay suffices; grounding is not load-bearing here |
-| **H5** | Spatial vs temporal complementarity: `society` ≠ `seed_bank`, direction **not** pre-committed (§2 gives arguments both ways) | report with CI either way | — (this is a measurement, not a gated prediction) |
-| **H6** | Complementarity decays by construction, so the `society` − `isolated` gap is **largest at intermediate generations** and shrinks by generation 9 | peak gap at 3 ≤ t ≤ 6 | a monotone gap → the advantage is not coming from complementarity, and the mechanism story is wrong |
-
-H6 is the design's internal check: the Latin square makes complementarity a *known* function of
-generation, so the framework predicts the shape of the advantage, not just its sign.
-
-## 5. Gates — G0 is the one that would have caught v2 and v3
-
-| gate | what | pass criterion |
-|---|---|---|
-| **G0 — can capability climb at all?** | One lineage, 3 generations, 3 families, `isolated` settings. Measure cumulative accuracy over families seen. | Generation 3 exceeds generation 1 by ≥ 0.10. **If capability cannot accumulate in the simplest arm, no outcome of the full design is interpretable — stop.** |
-| **G1 — does inheritance transmit?** | Adapter-continued training on family 2 starting from the family-1 adapter | family-2 accuracy ≥ 0.6 × a from-scratch specialist's |
-| **G2 — does forgetting occur?** | Same, measuring family-1 accuracy after learning family 2 without replay | family-1 accuracy drops ≥ 0.15 (if nothing is forgotten, replay and recombination have nothing to fix) |
-| **G3 — does recombination combine?** | Merge two lineages holding disjoint families at generation 3 | merged model ≥ 0.8 × each parent's accuracy on that parent's own families |
-| **G4 — budget parity** | Recorded example counts across all arms and baselines | equal to within 2% |
-
-G1 and G2 must **both** pass: transmission without forgetting means nothing decays; forgetting
-without transmission means nothing accumulates. The experiment needs the tension.
-
-## 6. Cost
-
-Base: Qwen2.5-1.5B (base, not Instruct — v3's C1 measured that instruction-tuned checkpoints already
-hold these skills). Per generation-lineage: train (`n_new` + `n_replay` ≈ 400 examples × 3 epochs)
-plus evaluation on nine families × 60 items. ≈ 6 min. Society arms: 3 lineages × 9 generations ×
-4 arms ≈ 108 generation-lineages ≈ **11 GPU-h**; baselines ≈ 2 GPU-h; three seeds ≈ **40 GPU-h**
-total. Seed 1 local overnight, seeds 2–3 as a CX3 array — the same split that worked last night.
-
-## 7. Engineering
-
-Reused unchanged from v3: execution/verifier infrastructure, LoRA training, merge operators,
-directed weight selection on a disjoint validation split, checkpoint/resume, the figure and stats
-scaffolding. Reused from v2: the nine calibrated families and their difficulty/conflict measurements.
-
-New: adapter-continued training (inherit the parent's weights rather than a fresh LoRA); the
-Latin-square curriculum scheduler; replay buffers per lineage; the ancestor registry for `seed_bank`;
-budget accounting in the manifest; the three mono-generational baselines.
-
-## 8a. Gate record (2026-09-08) and the v5 curriculum
-
-**G0 passes.** Three families, three lineages: `all_families` (mean over every family in the
-curriculum — the accumulation metric) climbs 0.739 → 0.950 (isolated) and 0.611 → 0.939 (society).
-The design can show accumulation, which v3 structurally could not. A labelling hazard caught on the
-first line: `retention_seen` (mean over families *taught*) starts near 1 and can only fall — watching
-it would have recreated v3's error. Both are recorded; the primary is `all_families`.
-
-**G2 fails on the v2 families, twice.** Three-family gate with replay: nothing forgotten (every family
-only rises). Nine-family single-lineage probe with replay **off** (`results/llm_curriculum_g2`): mean
-drop across families learned before the last is only **+0.074**, and it is carried by one family —
-`mixedtoken` 0.80 → 0.15 (+0.65), oscillating violently throughout (0.18, 0.20, 0.27, 0.80, 0.60,
-0.37, 0.42, 0.20, 0.15) — while two families *improve* through positive transfer (numtheory −0.12,
-alphabet −0.13) and the rest are within ±0.08. Forgetting here is a single pairwise-interference event
-between confusable counting families (the cluster v2's conflict measure flagged at 0.44), not a general
-pressure a population could smooth.
-
-**G3 is negative on the v2 families.** `society − isolated` = −0.128, −0.022, −0.011 across the three
-gate generations. Merging costs and cannot pay, because a partner can only contribute what the recipient
-lacks and nothing was lacking.
-
-**The base reference quantifies the format confound.** Qwen2.5-1.5B base on all nine v2 families:
-**0.094** (strings 0.02, setops 0.02, alphabet 0.00, wordlen 0.00, prime 0.02, digits 0.07, roman 0.10,
-mixedtoken 0.12, numtheory 0.52). Training on **one** family lifts the nine-family mean to **0.417**.
-Most of the apparent accumulation is a one-off format acquisition shared by all nine families.
-
-**One root, three faults.** The v2 families were built for a *specialisation* experiment and calibrated
-for low mutual conflict so merging would be safe. That makes them (i) format-homogeneous — one family
-teaches the convention for all; (ii) too compatible — no interference, hence no forgetting; (iii) too
-easy — the base is unformatted, not incapable. They are not a curriculum, and no arrangement of arms
-fixes that. The same lesson as v3's task pairing: the paradigmatic continual-learning benchmarks use
-naturally heterogeneous tasks *because* those interfere, differ in format, and exceed a small base.
-
-**v5 curriculum (`src/llm/curriculum_data.py`, 2026-09-08).** Eleven candidates from public datasets,
-each with a disjoint train/test split and its own verifier — gsm8k (number), mbpp (code, executed),
-boolq (yes/no), mnli (3-way label), sst2 (sentiment word), csqa (A–E), arc (A–D), winogrande (1/2),
-squad (extractive span, normalised EM over aliases), nq_open (short text, aliases), hellaswag (A–D).
-All eleven self-verify 40/40 and reject garbage 0/40. Answer shapes span five forms (code, number,
-word, letter, phrase) against v2's one. **Selection rule, fixed before running:** the C1 band
-(base ≤ 0.40, specialist ≥ 0.60) from `curriculum_v5_calib` (one specialist per candidate, evaluated on
-every family — the full transfer matrix), then a single-lineage zero-replay probe over the chosen set
-with **mean forgetting ≥ 0.15 and not carried by a single family** (max single-family share of the
-total drop ≤ 50%). Nine survivors form the curriculum; if fewer than nine pass, L and G shrink to match
-and the Latin square is recomputed.
-
-**v5 stage A (`results/llm_curriculum_v5_calib`, founders at 300 × 3, Qwen2.5-1.5B base).** Base on
-all eleven: **0.011** (mbpp 0.08, gsm8k 0.02, the rest 0.00) — a non-instruct base on real tasks, as
-expected; the C1 base floor is moot. Specialist × family matrix (own-family on the diagonal):
-
-| specialist | own | mean off-family | character |
-|---|---|---|---|
-| mnli | **0.82** | 0.46 | permissive — lifts most others |
-| arc | **0.77** | 0.21 | |
-| hellaswag | **0.72** | 0.27 | |
-| squad | **0.68** | 0.34 | permissive |
-| boolq | **0.65** | 0.04 | **destructive** — zeroes others |
-| csqa | 0.55 | 0.36 | permissive |
-| sst2 | 0.38 | 0.23 | |
-| winogrande | 0.38 | **0.00** | **destructive** — 0.00 on all ten others |
-| mbpp | 0.18 | 0.08 | destructive |
-| nq_open | 0.18 | 0.36 | |
-| gsm8k | 0.10 | 0.42 | permissive (0.92 on sst2, 0.83 on arc; 0.10 on gsm8k itself) |
-
-Five pass C1 at this budget. Mean off-family transfer 0.25 against own-family 0.49 — half, where v2's one
-family lifted all nine to near-own level. **Two kinds of specialist, which is what a curriculum needs:**
-*format-permissive* ones (mnli, csqa, squad, gsm8k) teach general instruction-following and lift other
-families — the residual format-transfer effect, now bounded and measurable; *format-destructive* ones
-(winogrande, boolq, mbpp) learn one narrow output form and erase the rest. The destructive group is the
-forgetting mechanism made visible: a lineage that meets winogrande loses what it held, and replay or a
-partner who did not just learn winogrande is what can restore it — E8 with something to act on.
-
-The six failures were under-trained, not unlearnable: every specialist that has worked in this project
-(v2, v3) had 1200 examples; these had 300 (gsm8k reached 0.54–0.62 at 1200 in v3). Pass 2
-(`curriculum_v5_calib_b`, the six at 1200 × 3) decides six families or nine. **Budget consequence,
-pre-noted:** if 1200 is what a family needs, `n_new` in the campaign rises accordingly and §6's cost
-scales by ~4× on the training term.
-
-**v5 stage A pass 2 (`results/llm_curriculum_v5_calib_b`, the six failures at 1200 × 3) — the
-"under-trained" hypothesis is refuted.** winogrande 0.38 → 0.57, sst2 0.38 → 0.52, mbpp 0.18 → 0.22,
-gsm8k 0.10 → **0.07**, nq_open 0.18 → **0.07**, csqa 0.55 → **0.18** (chance on 5-way; off-family
-transfer collapsed to 0.00). Three got worse with four times the data — a training instability of the
-fresh-adapter learning rate on these tasks, not a data shortage. gsm8k is a *data-source* issue: v3's
-0.54–0.62 came from MetaMathQA's augmented chain-of-thought, not raw GSM8K. None of the six clears 0.60.
-
-**Selection (2026-09-08): six families.** mnli 0.82, arc 0.77, hellaswag 0.72, squad 0.68, boolq 0.65
-pass C1; **winogrande 0.57** is the sixth. **Amendment:** the C1 specialist floor is relaxed 0.60 → 0.55
-for one family so that F is divisible by L = 3 (the pre-registered shrink rule needs F ∈ {3, 6, 9}).
-winogrande is also the right sixth on the merits: it is the most format-destructive specialist in the
-matrix (0.00 on every other family), i.e. the strongest forgetting pressure available — the mechanism the
-design exists to test. Curriculum: **L = 3, F = 6, G = 6**; complementarity 1.0 at t = 2, 0 at t = 6.
-Founders at **300** examples (the budget that passed; 1200 destabilised). Campaign `n_new` = 300.
-
-**Stage B running:** `curriculum_v5_g2` — one lineage, zero replay, the six in sequence. Pass criterion
-unchanged: mean drop ≥ 0.15 across families learned before the last, no single family > 50% of the total.
-
-**v5 stage B — G2 on the six (`results/llm_curriculum_v5_g2`, one lineage, zero replay).** Order
-mnli → arc → hellaswag → squad → boolq → winogrande. Drops (learned → final): mnli +0.27, arc +0.03,
-hellaswag −0.05, squad **+0.57**, boolq +0.05. **Mean +0.173 — magnitude gate (≥ 0.15) PASSES.**
-All-families 0.575 → 0.725 (gen 3) → 0.625 (gen 5): acquisition then loss as the destructive families
-arrive — the tension the arms need. **Concentration criterion (≤ 50% in one family) MISSES at 62%**
-(squad). Recorded as a marginal miss, with the reasons it does not reproduce the v4 failure the
-criterion was written against: two families forgotten (not one pair), the destruction lands exactly
-where the transfer matrix predicted (winogrande's option format erases span and 3-way label; letter
-formats survive), and the probe tested one order where the campaign's Latin square gives each lineage
-a different one — so different families are forgotten in different lineages, which is the
-complementarity recombination acts on. **Recommendation: go**, pending GG.
-
-## 8b. The v5 campaign result, and the two-kinds-of-variation measurement (2026-09-08)
-
-**Campaign (3 seeds, 6 families, 3 lineages, 6 generations; `results/llm_curriculum_v5/`).** Best model
-per arm at the final generation, mean over seeds: `sequential` (one model, no population) 0.802 ·
-`isolated` (population, never merges) 0.796 · `joint` (multi-task ceiling) 0.748 · `seed_bank`
-(merges with its own ancestor at t−3) 0.663 · `single_shot_merge` 0.549 (0.125 / 0.758 / 0.764 — the
-huge variance is which families landed in which allopatric split) · `society_dry` 0.307 · `society`
-(merges with a contemporary) 0.269. Budget parity within 6%.
-
-Every pre-registered hypothesis fails, consistently across all three seeds: society − isolated
-**−0.527 ± 0.092** (3/3 negative); society − single-shot −0.280 ± 0.363 (unresolved, huge variance);
-society − society_dry −0.038 ± 0.073 (replay policy irrelevant once the merge channel dominates).
-The one uncommitted contrast resolves decisively: **seed_bank − society = +0.394 ± 0.089, 3/3
-positive** — merging with your own past beats merging with a peer, so the compatibility argument
-beats the decorrelation argument.
-
-**Mechanism, identified and isolated.** Two of the six families (boolq, winogrande) are answer-format
-destroyers — the calibration matrix measured them at 0.00–0.04 mean off-family. A lineage that learns
-one propagates it through the merge into partners that never trained on it; because merged offspring
-continue the lineage, the damage compounds (society lineage 0, gen 2→3: five families fall together
-while boolq alone rises). An ancestor cannot transmit a family the lineage never met, which is exactly
-why the seed-bank arm holds.
-
-**Scope limits, stated plainly.** (i) Merging was **obligate** — no veto, no option to keep the parent
-unchanged. (ii) There is **no selection between lineages**: all three persist regardless of fitness,
-so the design has transmission, acquisition, gene flow and immigration but no differential
-reproduction. It is a gene-flow experiment, not a natural-selection one, and is therefore not a test
-of the composed-society claim. (iii) The operator was linear averaging, which the merging literature
-ranks below concatenation — but concatenation doubles adapter rank per merge, so iterated merging
-faces a capacity constraint single-shot merging never meets (16 → 1024 over six generations). That
-constraint is itself a finding about iteration.
-
-**Two kinds of variation are opposite in sign (`/tmp/paralleldiv.py`, 2026-09-08).** Three adapters on
-the *same* family differing only in seed and data draw: accuracies 0.762 / 0.800 / **0.312** (one run
-simply failed — training instability); pairwise output disagreement 0.237 between the two good ones;
-weight cosine **+0.006** (near-orthogonal); either-right 0.887 vs both-right 0.675. **Merging the two
-good ones gives 0.887 — +0.087 over the better parent, landing exactly on the either-right ceiling.**
-Same base, same linear operator, same scale, same evaluation as the collapsing arms.
-
-So the framework's single decorrelation parameter conflates two quantities that behave oppositely:
-- decorrelation in **what parents know** → risk (−0.53 measured);
-- decorrelation in **how parents encode the same knowledge** → benefit (+0.087, at the ceiling).
-
-Equal weights are best for same-skill merging (0.887 at 0.5/0.5 vs 0.863 at 0.3/0.7), the reverse of
-skill composition where asymmetric weights won decisively (v3: 0.507 at 0.2/0.8 vs 0.333 at 0.5/0.5).
-The optimal merge weight is a signal of which regime the merge is in.
-
-**Design consequence for v6 (GG, 2026-09-08 — selection is needed to generalise the hypothesis).**
-Three arms — never merge · complementary partners (different skills) · **parallel partners (same
-skills, different seed/data draw)** — with selection added in two places: a **veto** ("keep the parent
-unchanged" is always a candidate offspring) and **population selection** (score parents and offspring
-together, keep the best; the failed 0.312 run is exactly what it should discard). Roughly doubles
-evaluation cost per generation: ≈ 10 GPU-h per seed, ≈ 30 for three.
-
-## 8c. Mechanism probes, 2026-09-08 — two of my explanations retracted
-
-Four cheap probes run after the v5 campaign, chasing why cross-lineage merging collapsed. They
-retract two explanations I had given and leave a third standing.
-
-**Probe 1 — same-skill variation (`/tmp/paralleldiv.py`).** Three adapters, one family (arc), same
-data distribution, differing only in seed and draw. Accuracies 0.762 / 0.800 / **0.312** (one run
-simply failed). Pairwise output disagreement 0.237; weight cosine **+0.006** (near-orthogonal);
-either-right 0.887 vs both-right 0.675. **Merging the two good ones: 0.887 — +0.087 over the better
-parent, exactly at the either-right ceiling.** Equal weights beat 0.3/0.7 (0.887 vs 0.863), the
-reverse of skill composition, where asymmetric weights won.
-
-**Probe 2 — signal/noise and the inbred-lines cross (`/tmp/inbred.py`).** Across-seed decomposition:
-signal power 17.8 vs noise 34.9 for arc, 19.0 vs 37.1 for boolq — and correcting for the K=3 sample
-mean's own noise puts the true signal near 6.2, i.e. **~85% of a LoRA's weight change is
-run-specific and arbitrary.** That is why raw weight distance measures mostly noise and predicted
-nothing in Fig. 3C-D. Variance-weighted overlap surfaces ~4x more of the real overlap
-(0.0015 -> 0.0066) but different skills stay near-orthogonal even in signal directions.
-Denoising before crossing helps modestly and on both skills at once: raw mix 0.825 -> denoised mix
-**0.850** (+0.025), while the denoised singles are no better alone (arc 0.825 vs 0.838). The
-inbred-line signature: averaging within a line does not improve the line, it makes it cleaner to cross.
-
-**RETRACTION 1 — "a destructive skill propagates through the merge and destroys partners" is wrong.**
-A single merge of clean single-skill adapters is excellent: arc alone 0.838 (boolq 0.000), boolq alone
-0.800 (arc 0.300), **merged 50/50 = arc 0.863 / boolq 0.787, mean 0.825 vs 0.550 for the best parent.**
-Merging is *protective* — it stops either adapter dominating the output format. Consistent with LoRA
-Soups rather than contradicting it.
-
-**Probe 3 — iterated merging (`/tmp/decay.py`), five chained merges, three weight schemes.**
-arc retention: convex [0.5,0.5] **1.02** · selfish [0.8,0.4] **1.02** · additive [1.0,1.0] **0.52**
-(arc 0.867 -> 0.450, incoming skills 0.033). **RETRACTION 2 — geometric signal dilution is not the
-mechanism.** Convex merging loses nothing over five rounds even though the first adapter's coefficient
-falls to 1/32. The scheme that *preserves* signal coefficients is the only one that collapses, because
-the accumulated change grows without bound and leaves the region where the base still functions. The
-operative constraint on iterated merging is **bounding total drift from the base**, not preserving signal.
-
-**Probe 4 — the scaling dose-response (`/tmp/scale.py`), prompted by GG asking the obvious control:
-does dividing the change vector by 30 retain the skill?** Base (no adapter) **0.000**; scale 1 0.838;
-1/2 0.875; 1/4 0.850; 1/8 0.863; **1/16 0.450; 1/32 0.000; 1/30 0.000.**
-
-**This forces a reinterpretation of Probe 3.** At the coefficient arc actually held after five merges
-(1/32) the adapter alone delivers *nothing*. So the 0.883 measured in the chain was never arc's
-residual. What propagates through a merge is the **answer format**, supplied by whichever partner has
-enough weight to carry it: arc needs "a single letter", and it collapsed at round 4 (partner squad,
-free-text spans, arc 0.567) and recovered at round 5 (partner hellaswag, single letter A-D, arc 0.883).
-The chain measured *format compatibility with the dominant partner*, not skill retention.
-
-**What survives, and what it implies.** (i) A sharp **effectiveness threshold at ~1/8**: an adapter
-works at full strength down to an eighth and collapses below it, so useful merge depth is ~3 rounds at
-convex weights, not 5. (ii) The additive collapse stands (it did not depend on the misreading).
-(iii) The curriculum result becomes coherent for the first time: the two families that destroyed
-everything, boolq and winogrande, are the ones with the most idiosyncratic output formats. If format
-is what propagates, a partner carrying a dominant format overwrites the ability to answer anything
-else. **That is a claim about output conventions, not weight geometry** — and it is consistent with
-Fig. 3C-D, where functional conflict predicted merge damage (rho 0.45) and weight geometry did not (0.03).
-
-**Still untested, and now the leading candidate for the curriculum collapse:** continued training *on
-top of* merged weights. Probe 3 chained merges without ever training between them and lost nothing;
-the curriculum merges then trains, every generation. Test: repeat the chain with a fine-tune on the
-next family after each merge, and see whether that alone reproduces the collapse.
-
-## 8d. Per-skill scaling thresholds, and the bespoke-weights negative (2026-09-08)
-
-**Dose-response per skill (`/tmp/thresholds.py`), accuracy vs adapter scale, base = 0.000 on all six:**
-
-| skill | 1 | 1/2 | 1/4 | 1/8 | 1/16 | 1/32 | own optimum |
-|---|---|---|---|---|---|---|---|
-| arc | 0.87 | 0.88 | 0.88 | **0.92** | 0.42 | 0.00 | 1/8 |
-| squad | 0.72 | 0.73 | **0.78** | 0.65 | 0.10 | 0.00 | 1/4 |
-| hellaswag | 0.75 | **0.82** | 0.80 | 0.63 | 0.07 | 0.00 | 1/2 |
-| boolq | 0.78 | 0.78 | 0.78 | **0.37** | 0.00 | 0.00 | 1–1/4 (flat) |
-| winogrande | 0.55 | 0.55 | 0.55 | 0.37 | 0.30 | 0.00 | 1–1/4 (flat) |
-| mnli | 0.40 | 0.40 | **0.68** | 0.48 | 0.48 | 0.18 | 1/4 |
-
-Three facts. (i) **Thresholds are skill-specific** (GG predicted this): boolq dies at 1/8 where arc,
-squad and hellaswag are still at full strength, so merge depth in a population is set by the *weakest*
-skill. (ii) **Cliffs are sharp** — full effectiveness right up to the edge, then near-total loss in one
-halving; there is no graceful degradation to trade against. (iii) **Four of six skills are BETTER
-scaled down** — mnli 0.40 -> 0.68 at 1/4, hellaswag 0.75 -> 0.82 at 1/2, squad 0.72 -> 0.78 at 1/4,
-arc 0.87 -> 0.92 at 1/8. These adapters are over-trained at full strength; attenuation recovers
-accuracy with no retraining.
-
-**Denoising does NOT move the threshold.** arc raw 0.87/0.88/0.88/0.92/0.42/0.00 vs denoised
-0.87/0.83/0.88/0.87/0.35/0.00; boolq raw 0.78/0.78/0.78/0.37/0.00 vs denoised 0.78/0.78/0.77/0.38/0.03.
-Identical cliffs. So the limit is **signal magnitude**, not signal-to-noise: averaging leaves signal at
-full strength and only removes noise, and therefore cannot buy merge depth. Denoising remains worth
-doing for cross-skill merge quality (+0.025, §8c) and for knowing what a line contains — not for depth.
-
-**Bespoke per-skill merge weights — tested and NEGATIVE (`/tmp/bespoke.py`).** All six skills merged
-into one model:
-
-| scheme | sum | mean |
-|---|---|---|
-| six separate adapters, full strength | — | 0.678 |
-| six separate, each at its own optimum | — | **0.755** |
-| **uniform convex (1/6)** | 1.00 | **0.708** |
-| uniform 0.25 | 1.50 | 0.686 |
-| bespoke: cliff (lowest viable per skill) | 1.12 | 0.689 |
-| bespoke: optimum (best-accuracy per skill) | 1.62 | 0.686 |
-| additive (1.0 each) | 6.00 | 0.156 |
-
-All three non-uniform schemes cluster at 0.686–0.689, *below* plain equal weighting. **Why the
-inference failed:** solo dose-response curves do not transfer to the multi-way case. A skill's
-effective strength in a merge is set by its coefficient *relative to the other five* — six output
-formats compete for one model — so raising one skill's absolute weight starves the others. Clearest
-in squad: 0.65 at uniform 1/6, but 0.47–0.55 whenever given a larger absolute weight alongside others.
-The curves are sound; the inference from them to merge weights was not.
-
-**The two results worth keeping.** (a) **One merged model beats six separate specialists on their own
-tasks** — 0.708 vs 0.678 — with mnli the clearest case (0.40 alone, 0.67 merged, because merging
-dilutes it to near its optimum and undoes the over-training). The merge is doing compression plus
-incidental regularisation, which is a more honest description than "combining capabilities".
-(b) **The largest free win needs no merging at all**: attenuating each specialist to its own optimum
-takes the separate-models baseline from 0.678 to **0.755**, the best number in the table — one scalar
-per adapter, no retraining. Merging then costs ~5 points and saves five models: a real engineering
-trade, honestly stated.
-
-## 8e. The last candidate eliminated — merge-then-train is the best procedure tested (2026-09-08)
-
-**Test (`/tmp/trainmerge.py`).** Two chains, identical partners and order. Control: merge only.
-Test: merge, then continue-training on the partner's family (300) plus replay across everything seen
-(150 split) — i.e. the v5 sequence. The control reproduced the earlier chain **exactly at all five
-rounds**, so the comparison is clean.
-
-| round | partner | merge-only arc | merge+train arc | merge-only partner | merge+train partner |
-|---|---|---|---|---|---|
-| 1 | boolq | 0.883 | **0.933** | 0.733 | **0.850** |
-| 2 | winogrande | 0.900 | **0.933** | 0.550 | **0.717** |
-| 3 | mnli | 0.850 | **0.917** | 0.467 | **0.867** |
-| 4 | squad | 0.567 | **0.883** | 0.733 | **0.767** |
-| 5 | hellaswag | 0.883 | 0.833 | 0.800 | **0.850** |
-
-**Training on merged weights is not the mechanism — it is a substantial improvement.** Better on the
-tracked skill in four of five rounds, better on the incoming skill in all five, and it absorbs the
-round-4 format shock that dropped the control to 0.567. Merge-then-train holds the old skill near its
-ceiling *and* acquires the new one far better than merging alone (mnli 0.867 vs 0.467).
-
-**All three proposed mechanisms for the v5 collapse are now refuted**, each by direct test:
-a destructive skill propagating through merges (§8c — a single merge is protective); geometric signal
-dilution (§8c — five convex merges lose nothing); training on merged weights (here — it helps).
-The one structural difference left is that these chains merge *clean single-skill* adapters, whereas
-v5 merged accumulating lineages carrying up to six skills each in rank 16 — i.e. capacity. **Recorded
-as unexplained rather than attributed:** three mechanisms have been proposed and refuted, and a fourth
-guess would not have earned its place.
-
-**Consequence for the plan (§8b/§8c).** The veto arm was scheduled to make the v5 negative
-interpretable. With three mechanisms refuted and merge-then-train shown sound in isolation, the v5
-result is not reportable whatever a veto arm shows — recommendation is to stop, not to run it.
-
-## 8f. The veto arm (GG overruled my recommendation to skip it — correctly) 2026-09-08
-
-**Change:** identical to v5's `society` arm except that "keep the parent unchanged" is scored on the
-same validation split as the merge candidates, and wins if no weighting beats it. One bit of
-selection. Everything else — operator, weight grid, curriculum, replay, seeds — unchanged.
-
-**Seed 1 result: a single veto converts total collapse into a healthy trajectory.**
-
-| generation | 0 | 1 | 2 | 3 | 4 | 5 |
-|---|---|---|---|---|---|---|
-| veto (declinable) | **0.719** | **0.756** | 0.744 | **0.781** | 0.781 | 0.783 |
-| society (obligate) | 0.689 | 0.719 | 0.700 | 0.597 | 0.439 | **0.211** |
-| isolated (never merges) | 0.625 | 0.700 | **0.728** | 0.753 | **0.789** | **0.814** |
-
-**The veto decisions are structured, and this is the substantive finding.** Merges declined, per
-generation (of 3): 1, 1, 1, **3, 3, 3** — 67% overall, and from generation 3 onward *every* lineage
-declines *every* merge, unanimously. Median validation gain when accepted: +0.042. That timing
-tracks complementarity, which is 1.00 through generation 1, 0.80 at 2, then 0.67 and falling: the
-population discovers on its own that recombination has stopped paying and stops — H6's predicted
-*shape* of the advantage, reached from the opposite direction.
-
-**GG's caveat, and it is the right reading (2026-09-08):** once merges are always declined the arm is
-*literally* the isolated arm, so the comparison at the end is between "merged early, then stopped" and
-"never merged". Early merging gives a large lead (+0.094 at generation 0) but **isolated overtakes at
-generation 4 and finishes higher (0.814 vs 0.783)** — the early merges leave a residual cost that
-never-merging avoids. So the claim is not "the veto fixes recombination". It is: *recombination pays
-only while partners differ, a population can detect when that stops, and even then it ends slightly
-behind never having merged.*
-
-**Consequence for reportability.** This makes the v5 negative interpretable and no longer vulnerable
-to "you forced merging, of course it broke": obligate recombination collapses (0.211), one bit of
-selection rescues it (0.783), and never recombining is still marginally best (0.814). Risk, remedy,
-and honest limit — the language-model rung beside Fig. 5A.
-
-**My error, recorded:** I recommended skipping this experiment on the grounds that v5 was unreportable
-whatever it showed. That judged the experiment by whether it would rescue a conclusion I had already
-written off, rather than by what it would measure. The per-generation veto rate is information neither
-other arm could produce, and it is the most interesting thing in the arm.
-
-**Replication:** seeds 2-3 submitted to CX3 as array `4007703` (`hpc/llm_veto.pbs`), pairing against
-the existing v5 isolated/society/seed_bank runs for those seeds.
-
-**Control worth considering if the seeds hold:** a *forced* stop at generation 3, to separate "the
-veto's timing is smart" from "any early merging then stopping does this". The veto's stopping point
-coincides with complementarity falling below 0.8, which is principled rather than arbitrary — but that
-is an observation, not a test.
-
-## 8g. The vocabulary substrate: contamination screen and the prior-art problem (2026-09-09)
-
-GG's proposal: replace task families with *content* — teach 100 words of a language the model does
-not speak, one word-set per modifier, so thousands of words yield hundreds of modifiers and the
-generation count rises tenfold. Retention is trivially measurable ("what does X mean in English?").
-
-**Contamination screen — the first measurement was invalid.** Generating an answer and string-matching
-it gave Italian 0.092, French 0.050, Basque 0.050, Welsh 0.017, Zulu 0.008, pseudo-words 0.000. French
-tying Basque is impossible if the quantity measured were knowledge, so the probe was measuring whether
-a *base* model obeys "answer with one English word" — the same instruction-following floor that gives
-0.011 on the task families. Re-run as an 8-way forced choice over candidate translations scored by
-likelihood (domain-conditional PMI, chance 0.125), which needs no instruction-following:
-
-| language | generated | forced choice | verdict |
-|---|---|---|---|
-| French | 0.050 | 0.950 | fully known |
-| Italian | 0.092 | 0.908 | fully known |
-| Welsh | 0.017 | 0.508 | half known |
-| Basque | 0.050 | 0.483 | half known |
-| **Zulu** | 0.008 | **0.142** | at chance — genuinely unknown |
-| pseudo-words | 0.000 | 0.158 | floor (cycling 20 nouns inflates this slightly) |
-
-Only Zulu is clean among natural languages; pseudo-words are clean by construction and unlimited in
-supply. Probe: `/tmp/contam2.py`.
-
-**Prior art makes the experiment-as-framed a reproduction.** WikiBigEdit (arXiv:2503.05683) runs
-506K factual QA pairs across 8 sequential timesteps. Locate-then-edit methods (ROME, MEMIT) collapse
-within the first few hundred updates, but their **LoRA + merging** baseline — a fresh adapter per
-timestep, interpolated into the accumulated adapter at weight 0.25 — is stable across the whole
-benchmark and beats every dedicated editing method past ~100K updates. That is our isolated-plus-
-attenuated-merge arm, run three orders of magnitude further, and it does not collapse. Separately,
-arXiv:2506.14126 finds that over-training experts harms merging via late-stage memorisation, which is
-the published version of our §8d observation that attenuating four of six adapters was free gain —
-cite it, do not claim it.
-
-**Consequence for the diagnosis.** If a single lineage accumulates 500K disjoint facts by
-fresh-adapter-plus-interpolation without collapsing, capacity is not what stopped v5 at generation
-3-4 with six families. The difference between the two settings is that WikiBigEdit's content is
-homogeneous QA in one output format, whereas v5's families conflict at the output (label vs span vs
-number vs code). The remaining candidate is interference between competing output formats.
-
-In the population-genetic frame the two are distinct: a new word-set is a **new locus**, and adding
-loci is cheap; two families demanding different output formats for the same input shape are
-**competing alleles at one locus**, and that is what collapses. A pure vocabulary curriculum is all
-loci and no allelic competition, so it would run to a hundred generations and confirm only that
-capacity is ample — removing precisely the variable that produced the phenomenon.
-
-**The collision sweep I proposed here is also occupied — do not run it either.** *In Praise of
-Stubbornness* (arXiv:2502.04390) sweeps exactly this: non-contradictory updates integrate safely, while
-contradictory ones destroy up to 80% of unrelated knowledge with as few as 10-100 facts, consistently
-across model scales, and the authors conclude explicitly that the cause is conflict rather than
-capacity. *Interference and Retention in Continual Learning* (arXiv:2607.09202) supplies the theory:
-disjoint task supports make forgetting structurally eliminable, conflicting overlap imposes an
-unavoidable distortion floor. Both the measurement and its formalisation exist.
-
-**What this buys us anyway: the v5 collapse now has a cause.** Three of our own explanations were
-retracted (§8c, §8d) and capacity was the standing suspect. Between 2502.04390 and 2607.09202 the
-mechanism is settled and it is allelic conflict, not locus exhaustion — the six families conflict at
-the output, and contradictory updates corrupt disproportionately and non-locally. WikiBigEdit is the
-mirror control: homogeneous single-format content accumulates to 506K facts without collapsing. The
-collapse we could not explain is a known, characterised, independently replicated phenomenon.
-
-**The gap that survives.** Every merging paper in the landscape still merges *once* — GENOME (the ACL
-2026 population-evolution paper) evolves a population toward a single target task, with no collapse,
-forgetting or grounding analysis. The iterated reproduction loop is still unoccupied, we have run it,
-and v5's negative answer (obligate recombination collapses by generation 3-4; veto-gated recombination
-merely matches isolation) is now interpretable rather than mysterious. No further LLM compute is
-required to state it.
-
-## 8. Decisions for GG
-
-1. **Three lineages × nine generations × nine families** (complementarity maximal at t=3, zero at
-   t=9), or fewer families and more generations per family?
-2. **Replay budget** — fixed `n_replay` split across all families seen so far (so per-family replay
-   thins as the curriculum grows, which is realistic and makes forgetting a live pressure), or fixed
-   per-family (constant protection, more compute)?
-3. Whether `single_shot_merge` gets the directed weight selection the society arms use, or plain
-   uniform soup as published. I would give it the *same* selection, so the comparison isolates
-   iteration rather than handing the society a free operator advantage.
-
-## 8h. Two controls for the declinable merge, pre-registered before running (2026-09-11)
-
-Both were identified in the 2026-09-11 manuscript review as the weakest hedges in the six-generation
-population section. Code: `merge_until` and `orders` config keys in `src/llm/curriculum.py`;
-configs `curriculum_v5_stop3.yaml`, `curriculum_v5_decor.yaml`; stats `figures/stats_llm_curriculum.py`.
-
-**Control 1 — forced stop at generation 3 (`llm_curriculum_v5_stop3`).** The v5 `society` arm with
-recombination switched off from generation 3 (`merge_until: 3`, `allow_veto: false`). Rationale: in
-the seed-1 veto run lineages declined 1/3 of merges at generations 0–2 and 3/3 at 3–5, so this is the
-matched fixed schedule. Readout: best-lineage all-family accuracy at generation 5, paired per seed
-(3 seeds) against veto, isolated and society.
-- stop3 ≈ veto (within ±0.03 in every seed): the veto's outcome is explained by *when* it stopped;
-  the paper keeps "the population found the schedule by itself" and drops any claim that per-decision
-  evaluation adds value beyond timing.
-- stop3 < veto in every seed: the early declines avoided specific harmful merges; the modifier reading
-  strengthens.
-- stop3 ≈ society (collapsed): three obligate merges already carry the format destroyers; stopping is
-  not enough, screening is required.
-
-**Control 2 — decorrelated curriculum (`llm_curriculum_v5_decor`).** Same six families, G = 6, but
-every lineage starts with mnli, then diverges maximally, then converges, so partner complementarity by
-generation is 0.00, 0.67, 0.70, 0.58, 0.33, 0.00 (Latin square: 1.00, 1.00, 0.80, 0.67, 0.33, 0.00).
-Arms: `isolated` and `society` with `allow_veto: true`. Primary readout, pooled over both curricula
-(2 × 6 generations × 3 seeds = 36 points of mean `veto_used`): partial Spearman correlation of the
-fraction declined with complementarity, controlling for generation (rank-regress both on generation,
-correlate residuals), seed-clustered bootstrap CI.
-- Modifier hypothesis: partial ρ(declined, complementarity | generation) < 0 with CI excluding 0.
-- Adapter-age hypothesis: that partial ρ ≈ 0 while partial ρ(declined, generation | complementarity) > 0.
-- Known ambiguity, stated in advance: at generation 0 of the new curriculum all lineages hold the
-  *same* family from different training draws, and §8b measured that such same-skill merges gain
-  +0.087 (encoding decorrelation). A low decline rate at generation 0 therefore does not test
-  complementarity; the primary test is the pooled partial correlation, not that point.
-- Secondary, descriptive: whether the new curriculum's veto arm finishes level with its own isolated
-  arm, as in the Latin square (0.792 vs 0.796).
-
-Seed 1 of each control runs locally (batch 24 / train batch 2, as the v5 seed-1 runs); seeds 2–3 on
-CX3 (`hpc/llm_curriculum_controls.pbs`, batch 48 / train batch 4, as the v5 seeds 2–3 runs).
-
-### §8h outcome (2026-09-11, 3 seeds each; `figures/stats_llm_curriculum.py`)
-
-- **Control 1, forced stop:** stop3 0.793 vs veto 0.792 vs isolated 0.796 (per-seed veto − stop3:
-  −0.008, −0.006, +0.011). First branch: the veto's outcome is explained by *when* it stopped.
-- **Control 2, decorrelated curriculum:** partial ρ(declined, complementarity | generation) = −0.067,
-  CI (−0.211, +0.088); partial ρ(declined, generation | complementarity) = +0.31. Adapter-age branch:
-  declines track generation, not complementarity. The Latin-square ρ = −0.57 was carried by
-  generation. Decor veto 0.790 = decor isolated 0.790.
-- Manuscript consequence: the recombination-modifier / reduction-principle reading of Fig. 4B is
-  withdrawn; the declinable merge remains the mechanism that avoided the obligate-merge collapse at no
-  cost against never merging, and the forced-stop control shows a fixed schedule does the same.
diff --git a/tasks/todo.md b/tasks/todo.md
deleted file mode 100644
index 7c7c140..0000000
--- a/tasks/todo.md
+++ /dev/null
@@ -1,659 +0,0 @@
-# Layer 1 Execution Plan — The Lamarckian Society (analytical core)
-
-*Created 2026-07-04. Scope: blueprint §7 build-order steps 1–4 (scaffold → Layer 1 complete, laptop-reproducible). Layer 2 is explicitly out of scope here and gated on Layer 1's scientific-validation tests passing.*
-
-Source of truth: `lamarckian-society-technical-blueprint-v1.md`. Where it is silent I record a decision below rather than improvise silently.
-
----
-
-## Design decisions to pin before coding (blueprint leaves these open)
-
-These are the only places the spec is genuinely underdetermined. Recommendation given for each; flagged for sign-off.
-
-1. **Config framework — recommend thin pydantic + PyYAML, not Hydra.** Blueprint says "Hydra or a thin equivalent." Hydra is heavyweight and its global-state/`os.chdir` behaviour fights the "pass `rng` explicitly, results are a pure function of resolved config" requirement. A thin loader (pydantic models for validation + a small sweep-expander) is dependency-light, aligns with the stdlib-first preference, and makes the "write resolved config beside results" contract trivial. *Decision: thin pydantic loader.*
-
-2. **Selection fitness `f_i` (the reality-anchored score).** Blueprint: "fitness is predictive accuracy against `p*`" but gives no formula for the discrete model. *Decision:* `f_i = p*_eff_i` (truth frequency = fitness; reality-anchored by construction). Post-selection distribution `p'_i ∝ p_i^(1−α) · f_i`, matching the blueprint's stated `w_i ∝ f_i·(p_i)^(−α)` with `α=0` recovering fitness-proportional greedy. Document as a modelling choice; `select-then-sample` vs `sample-then-select` is the documented robustness switch (§2.2).
-
-3. **`tail_mask` definition.** Two knobs exist (`tail_frac`, `tail_threshold`). *Decision:* the metric-bearing tail set is the §2.3 definition `{i : p*_i < tail_threshold}`. `tail_frac` only drives the `twocomponent` construction (fraction of items placed in the low-mass component). Document that for `zipf`, `tail_frac` is unused.
-
-4. **`TrueDist` type.** *Decision:* a frozen dataclass `TrueDist(p_star: np.ndarray, regions: np.ndarray, tail_mask: np.ndarray)` — immutable, so `p*` cannot be mutated in place (except the deliberate re-mint path, which produces a new object).
-
-5. **Region partition.** *Decision:* contiguous blocks; require `K % R == 0` (assert with a clear error) to keep per-region math clean for v1.
-
-6. **Re-mint semantics (E6).** *Decision:* re-mint replaces `p_star_eff` with a *new* `TrueDist` built from current `p_t` (fresh tail_mask recomputed on `p_t`), and the *original* `TrueDist` is retained **only** for the KL-to-original metric, never for grounding. This is the irreversibility the experiment tests.
-
----
-
-## Phase 0 — Scaffold & environment
-
-- [x] Install `uv` (`curl -LsSf https://astral.sh/uv/install.sh | sh`; lands in `~/.local/bin`, no sudo).
-- [x] **Reproducibility path = `uv` venv from a committed, hash-pinned `uv.lock`** (GG decision, 2026-07-04; no Apptainer/Docker for Layer 1). `pyproject.toml` (Python ≥3.11; deps: numpy, scipy, pandas, pyarrow, matplotlib, pydantic, pyyaml; dev: pytest). Commit `uv.lock`. Create `.venv` via `uv sync`.
-- [x] Repo layout per §5: `src/inheritance/`, `configs/inheritance/`, `figures/`, `results/` (gitignored), `tests/`, `paper/`. Add `src/lamarckian/` package root or make `knowledge` importable (decide package name — recommend `src/lamarckian/knowledge/...` with `src`-layout).
-- [x] `.gitignore` (`.venv/`, `results/`, `__pycache__/`, `*.parquet` under results but keep hashes).
-- [x] Seeding util `lamarckian/utils/seeding.py`: master seed → `np.random.SeedSequence(seed).spawn(n)` → per-replicate `np.random.default_rng(child)`. No global RNG anywhere.
-- [x] Config loader `lamarckian/config.py`: pydantic schema mirroring the §2.7 YAML, a `load_config(path)`, a `expand_sweeps(cfg) -> list[ResolvedConfig]`, and `write_resolved(cfg, dir)`.
-- [x] Manifest util: `write_manifest(dir, results_df)` recording lib versions, master seed, `git rev-parse HEAD`, content hash of `results.parquet`.
-- [x] `Makefile` targets (`env`, `test`, `layer1`, `figures`, `clean`) + `pytest` skeleton. **Gate: `make test` green on a trivial test.**
-- [x] Move `lamarckian-society-technical-blueprint-v1.md` → `paper/blueprint.md` per §5 (confirm with GG first — it's referenced by name elsewhere).
-
-## Phase 1 — Core primitives + null model + VALIDATION GATE
-
-Implement to the normative signatures in §2.7. Order chosen so each piece is unit-testable before the next depends on it.
-
-- [x] `knowledge/truth.py::make_true_distribution` → `TrueDist`. Support `tail ∈ {zipf, twocomponent}`. Unit tests: normalisation, region block sizes, tail_mask matches threshold, determinism from seed.
-- [x] `knowledge/metrics.py`: `forward_kl` (with `eps` floor, logged), `heterozygosity`, `tail_mass`, `support_size`, plus per-region variants. Unit tests on hand-computed small vectors.
-- [x] `knowledge/step.py::generation_step` — **null path first** (single teacher, `m=0`, selection `none`): `c ~ Multinomial(n, p_t)`, `p_{t+1}=c/n`. Exactly neutral Wright–Fisher.
-- [x] `knowledge/lineage.py::run_lineage(cfg, seed)` → tidy per-generation DataFrame (all §2.3 metrics, global + per-region).
-- [x] `tests/test_scientific_validation.py` — **the spine:**
-  - **Pred. 1** heterozygosity decay: mean `H_t` over replicates matches `H₀(1−1/n)^t`. Prefer testing the full deterministic mean-recursion trajectory (subsumes the fixed point), within Monte-Carlo CI.
-  - **Pred. 2** fixation probability = initial frequency (long runs, statistical tolerance).
-- [x] **HARD GATE: do not proceed until Pred. 1–2 pass.** If drift ≠ analytic decay, the harness is wrong — fix here.
-
-## Phase 2 — Grounding + E1 + E2 (the headline)
-
-- [x] `knowledge/step.py::structured_multinomial(m_vector, p_star, regions, policy, rng)` — per-region immigration draws from `p*` restricted+renormalised to each region; `uniform` spreads `m` evenly, `matched` concentrates on exercised regions. Returns length-K counts.
-- [x] Extend `generation_step` with grounding (pooled draw, `g = m/(n+m)`).
-- [x] **Pred. 3** validation — *exact* equilibrium `H_eq = H*·m(2n+m−1)/(n+2nm+m²)`: run to stationarity (burn-in + late-generation + replicate averaging), assert `<0.1%` rel. error vs closed form across an `m` grid. Also assert the `m→0` and `m→∞` limits.
-- [x] **Pred. 4** validation — tail-persistence: item of freq `p*_i` maintained iff `m·p*_i ≳ 1`; verify the survival transition location statistically.
-- [x] `knowledge/experiment.py::run_experiment(cfg)` — sweep grid × `n_replicates`; long-form results + CIs; write `results.parquet` + `resolved_config.yaml` + `manifest.json`.
-- [x] **E1** config + run: `m=0`, single teacher, no selection. Expect `H` geometric decay, support→1, KL diverges, tail-first loss.
-- [x] **E2** config + run: sweep `g`, single teacher, uniform grounding, no selection. Locate critical `g*` (transition in **tail mass / support**, since H is smooth in m — the sharp threshold is in discrete tail survival). Report `g*` with CI. **This is the load-bearing result.**
-- [x] `figures/plot_collapse_null.py`, `plot_fig2_grounding_sweep.py` — read `results.parquet` only.
-
-## Phase 3 — E3–E6
-
-- [x] **E3 region-matched grounding.** Fixed total `m`; `uniform` vs `matched`; one designated inherited-but-unwatered region with a rare tail. Expect uniform lets that region's tail collapse; matched holds it. Per-region metrics essential. `plot_figS5_aimed_grounding.py`.
-- [x] **§2.7.1 correlated-teacher construction** — `knowledge/teachers.py`:
-  - `make_retention_matrix(T, K_T, rho, q, rng)` — shared-switch exchangeable Bernoulli.
-  - `make_correlated_teachers(...)` — retention→distributions (head kept at `p*`; tail at `p*_i` if retained else `tail_floor`; renormalise). `region_specialisation` option.
-  - **Pred. 5** validation: `make_retention_matrix` reproduces marginal `q`, pairwise `ρ`, and union coverage `U(K_T,ρ,q)=T[ρq+(1−ρ)(1−(1−q)^K_T)]` to 3 decimals over a `(ρ,q)` grid.
-- [x] **E4 multi-teacher decorrelation.** Sweep `K_T∈{1,2,3,5}`, `ρ∈[0,1]` at fixed `q`, matched budget (`n/K_T` each). Report **both** union `U` and post-distillation surviving coverage; show their gap shrinks as `g` rises. `plot_figS8_multiparent_union.py` (coverage surface over `(K_T,ρ)`).
-- [x] **E5 QD vs greedy.** `apply_selection` (`none`/`greedy`/`qd`, pinned fitness form). Sweep novelty `α`. Expect greedy→fixation (`H→0`), qd holds `H` plateau + re-introduces tails. `plot_figS12_quality_diversity.py`.
-- [x] **E6 re-mint gate.** Re-mint at high vs low `H`; track KL to *original* truth. Expect collapsed re-mint locks KL high forever; gated (high-H) does not. `plot_figS3_rebaselining.py`.
-
-## Phase 4 — Reproducibility polish (Layer 1 slice)
-
-- [x] `configs/inheritance/E1..E6.yaml` all committed with explicit params (no magic numbers in code).
-- [x] `paper/figure_manifest.md` — the §6 claim→experiment→figure rows for Layer 1.
-- [x] `make layer1` runs E1–E6; `make figures` regenerates all figures from committed parquet.
-- [x] Full `test_correctness.py` (shapes, normalisation, determinism) + `test_scientific_validation.py` (Pred. 1–5) green in CI.
-- [x] `reproduce.sh` (`uv sync` → `make test` → `make layer1` → `make figures` → write `REPRODUCED.md` diffing committed result hashes) + `README.md` reproduce section. **No container** — the committed `uv.lock` is the reproducibility source of truth (per GG, 2026-07-04); a Dockerfile may later wrap the same lockfile for Layer 2's GPU work.
-
----
-
-## Definition of done (Layer 1)
-
-Every Layer-1 row of blueprint §6 has a committed figure produced by `make figures` from committed results; all §2.4 analytic checks (Pred. 1–5) pass; `make layer1 && make figures` reproduces from a clean `.venv`. Then — and only then — Layer 2 may begin.
-
-## Falsifier watch (report honestly if hit)
-
-- E2 tail mass flat in `g`, or only stabilises as `g→1` → multigenerational thesis refuted.
-- E3 uniform protects as well as matched → region-matching claim dies.
-- E4 no surviving-coverage benefit at matched budget → recombination claim dies.
-- E5 qd ≤ greedy stationary `H` → QD does no work.
-- E6 collapsed lineage recovers original-truth tails after re-mint → irreversibility overstated.
-
-## Review — progress log
-
-**2026-07-04 — Phases 0 & 1 complete; hard gate PASSED.**
-
-- Reorg: docs → `paper/` (`blueprint.md`, `the-lamarckian-society-v4.md`). src-layout under `src/inheritance/`.
-- A pre-existing `tests/test_scientific_validation.py` (author-supplied, 22 KB) turned out to hard-specify the package contract — implemented *to it* rather than inventing interfaces. Key contracts it locked (now honoured): package imports as `knowledge.*`; `run_lineage(cfg_dict, seed)` returns a tidy per-gen frame with a `heterozygosity` column, rows 0..T; `p_0` initialises **uniform** (`H_0=1−1/K`); `metrics.heterozygosity` and `teachers.make_retention_matrix` match the reference to 1e-12 / closed form.
-- Env: `uv` 0.11.26 installed; `pyproject.toml` + `uv.lock` committed; numpy 2.5, pandas 3.0, scipy 1.18, pydantic 2.13, pytest 9.1.
-- Modules written: `metrics`, `seeding`, `config` (dataclasses + `from_dict`), `truth`, `teachers`, `step`, `lineage`. Config is dataclass-based (not pydantic) — the conformance test passes a raw dict; dataclasses validate cleanly and stay stdlib-simple. **Pydantic still a dep for the Phase-2 YAML/experiment layer.**
-- **Results: `make test` green — 68 passed** (48 scientific-validation, 20 correctness). Conformance tests RAN (not skipped): Pred. 1 (neutral decay), Pred. 3 (exact `H_eq`), Pred. 5 (union coverage) all pass against the real package. **The Pred. 1–2 hard gate is passed**, and grounding already conforms to the exact equilibrium.
-
-Design decisions #1 (dataclasses now / pydantic at YAML layer), #2 (fitness `f_i=p*_i`), #3 (threshold tail_mask), #6 (re-mint discard) all implemented as planned. Region design: each region an identical 1/R-mass block (symmetric; reduces to global Zipf at R=1, matching the reference).
-
-**2026-07-04 — Phase 2 complete (E1 + E2).**
-
-- `experiment.py`: sweep expansion (Cartesian grid; special-cases `g→m`), paired replicate seeds (shared across grid points), output contract (`results.parquet` + `resolved_config.yaml` + `manifest.json` with lib versions + git commit + sha256). CLI `python -m inheritance.experiment `.
-- **E1 (null collapse)** — reproduces tail-first collapse: H geometric decay matches `H₀(1−1/n)ᵗ` within CI; tail items die ~10× faster than head items; support 500→1; forward-KL diverges. Figure `results/collapse_null/E1.png`.
-- **E2 (headline)** — `H_sim` tracks the *exact* `H_eq` closed form across the sweep; phase boundary at **`g* ≪ 1`**: g=0.005 (m=1 real sample vs n=200) → 68% of truth H; g=0.05 → 96%. g=0 slides to ~0.10 over 500 gens. Figure `results/fig2_grounding_sweep/E2.png`. **Headline result achieved.**
-- Metric subtlety found & fixed: aggregate **`tail_mass` is a drift martingale** (mean-conserved), so it's a poor collapse indicator. Added `tail_support`/`head_support`/`tail_frac_alive`/`head_frac_alive`; E1 & E2 figures now use tail-*item* survival, which is honest and monotone.
-- E2 extended 300→500 generations (GG-approved) so the g=0 arm visibly approaches 0 while g>0 arms sit on plateaus.
-- Makefile `layer1`/`figures` wired to E1–E2. `make test` still green (68).
-
-**2026-07-04 — Phase 3 complete (E3–E6) + E2 analysis add-ons.**
-
-- **E3** region-matched grounding: added `grounding.exercised` knob + per-region `tailalive_region_r`. Target region tail survival 0.49 (matched) vs 0.07 (uniform). Note: per-region *H* is mass-confounded — used tail-item survival instead.
-- **E4** multi-teacher recombination: bespoke `run_coverage` runner (`kind: coverage`). Union coverage matches `U(K_T,ρ,q)` exactly. **Key finding (GG-approved): mean-mixture distillation gives NO surviving benefit (conservation law — dilution cancels the union gain); max-merge (M2N2-style) does.** E4 reports both. In CLAUDE.md.
-- **E5** QD vs greedy: greedy → H≈0.01 (fixation); qd holds H 0.48–0.88 rising with α. qd ≫ greedy.
-- **E6** re-mint gate: added `arm` multi-override sweep type. Re-mint while collapsed → KL-to-original diverges (lock-in) + accelerates H collapse; diversity gate (H≥0.75) blocks it → bounded; healthy re-mint harmless.
-- **E2 analysis add-ons** (companion work order `tasks/workorder-E2-analysis-addons.md`, verified): new `analysis.py` (`reduce_to_stationary`, `critical_grounding` bootstrap CI) — real E2 **g*=0.048, CI [0.047,0.050]**; `metrics.tail_band_metrics` + per-band lineage logging; `tests/test_analysis.py` reproduces the work order's verified numbers exactly. E2 figure rebuilt 2×2. **Deviation:** used truth-mass-weighted tail coverage instead of raw `tail_mass` (a drift martingale).
-- All six figures regenerate via `make figures`; **71 tests green**.
-
----
-
-# Layer 1.5 — Architecture-general neural existence proof (RNN/VAE/MLP + synthetic/MNIST)
-
-*Created 2026-07-04. Plan: `~/.claude/plans/we-are-going-to-cheerful-fog.md`. Re-scopes Layer 2:
-build a cheap, architecture-general neural collapse proof in real trained weights on a
-fully-synthetic sandbox (exact known `p*`) before the LLM rung. Locked decisions: exact-oracle
-categorical token sequences; Histogram+RNN+VAE+MLP; real MNIST as secondary confirmation; LLM +
-C3 vertical claim deferred.*
-
-## Progress log
-
-**2026-07-04 — Stages A, B, plumbing complete.**
-
-- **Env:** installed `uv` 0.11.26 (`~/.local/bin`); `/home` was 100% full — GG approved clearing
-  pip/yay/browser caches (~10 GB freed). Base venv synced; 71 Layer-1 tests green.
-- **Stage A (scaffold, pure NumPy):** `src/neural/` — `config.py` (frozen dataclasses reusing
-  `inheritance.config` GroundingCfg/RemintCfg/MetricsCfg/_sub), `synthetic.py` (mode-truth via
-  `make_true_distribution`; lossless identity + stochastic style token grammar), `oracle.py`
-  (`ExactOracle` zero-error + `measure_distribution`), `models.py` (`GenerativeModel` protocol +
-  `HistogramModel` bridge), `evaluate.py` (reuses `inheritance.metrics`, Layer-1 row schema),
-  `generation_loop.py` (`run_generative_lineage`, reuses `allocate_m`/`structured_multinomial`).
-  15 correctness tests green.
-- **Stage B — HARD GATE PASSED:** `tests/test_neural_validation.py` — histogram lineage reproduces
-  Pred. 1 (neutral decay, <3% rel err), Pred. 3 (exact `H_eq`, <5%), and tracks Layer-1
-  `run_lineage` directly (<3%). The neural plumbing reproduces the analytic core.
-- **Plumbing:** `neural/experiment.py` (`run_and_save` dispatch on `kind`, reuses `_apply_param`
-  g→m, paired seeds); extended `inheritance.experiment.save_artifacts` (optional `extra_libs`,
-  `extra_manifest`, injectable `grid`; skips missing libs — backward compatible). `configs/neural/bridge.yaml`,
-  Makefile `neural`/`env-neural`/`layer2` targets, `.gitignore`. (Experiments are named
-  descriptively — `bridge`, `collapse`, `grounding`, `architectures` — not by code.)
-- **`bridge` result (17s):** neural **g\* = 0.0474, CI [0.045, 0.052]** — reproduces Layer-1 E2's
-  g\*=0.048 essentially exactly (g=0.005→67% of H*, g=0.05→96%). **89 tests green.**
-
-**2026-07-04 — Stage C: torch models + collapse/grounding/architectures.**
-
-- **Env:** torch **2.12.1+cu130** (default PyPI wheel ships CUDA 13, matches RTX A4000 driver;
-  no custom index needed, cp314 wheels exist). `--extra neural` = torch only; `--extra mnist` =
-  torchvision (later). `UV_CACHE_DIR=/tmp` during install (RAM-backed) to spare `/home`.
-- **Models:** `torch_models.py` (RNNGenerator, autoregressive GRU), `torch_mlp.py` (autoregressive
-  MLP, causal-masked), `torch_vae.py` (sequence VAE), `train.py` (determinism flags + device/seed
-  helpers derived from the SeedSequence stream). `tests/test_neural_torch.py` (torch-gated): gen-0
-  fidelity (rnn+mlp) + dry-collapse/grounded-holds. **92 tests green.**
-- **Validated regime:** K=256, n=200, zipf_s=1.3, RNN hidden=128/epochs=25. RNN gen-0 fidelity
-  KL(p*‖p̂)=0.008, 64/64 (or 256/256) modes recovered. MLP fidelity KL=0.011. **VAE does NOT clear
-  the gen-0 gate** on the Zipf-codeword task (KL≈0.8; prior-hole mismatch — sampling z~N(0,I) misses
-  the aggregate posterior) → excluded from `architectures` to avoid confounding collapse with underfitting.
-- **`collapse` (in weights):** dry RNN lineage collapses — forward-KL rises to ~2.2 vs grounded
-  ~1.4; grounding lifts tail survival (tailalive 0.31 dry → 0.50 at g=0.02). Sign confirmed.
-- **`grounding` (neural phase boundary):** stationary H hovers 80–91% of H* and is **noisy / non-monotonic**
-  at 5 reps — no crisp g*. **KEY FINDING:** the neural models' smoothing inductive bias *partially
-  resists* H-collapse (dry H stays ~83% of H*), so **forward-KL and tail survival are the sharp
-  neural collapse metrics, not H** (mirrors Layer-1's "H is smooth; the threshold lives in tail
-  survival"). `grounding` needs (a) forward-KL as the phase metric, (b) more reps (≥10), and/or (c) a
-  stronger-collapse regime for a clean neural g*.
-- **`architectures` (architecture-generality) — clean result:** collapse + grounding-rescue appear in ALL three
-  model classes (dry→grounded forward-KL: histogram 6.2→4.6, MLP 4.8→1.3, RNN 3.8→1.1; tailalive
-  RNN 0.41→0.64, MLP 0.07→0.20). The WF operator is architecture-general. Bonus: neural smoothing
-  lets RNN/MLP retain *more* tail than the exact histogram under grounding (they generalise to
-  unseen codewords) — an inductive-bias finding worth the write-up.
-
-**2026-07-04 — `recombination` (load-bearing E4 replication).**
-
-- `recombine.py` mirrors `run_coverage` but trains K_T specialist RNNs on assignments from the
-  exact shared-switch retention construction (K_T/rho/q clean; union matches the closed form), then
-  recombines the *measured* teacher distributions two ways: `mean` (naive pooling) vs `max`
-  (oracle-guided union / M2N2-style), each followed by size-n resampling. The neural merge is the
-  per-mode max over teacher distributions (oracle-guided), NOT weight-averaging of RNNs.
-- **Result (8 reps):** at rho=0, **union rises 0.49→0.96** (supply matches closed form); analytic
-  **surviving_max rises 0.043→0.087 while surviving_mean stays flat ~0.045** — the conservation law
-  (averaging cancels the union gain; max-merge realises it). At rho=1 (identical teachers) union AND
-  max are flat — more identical teachers buy nothing. **The "merge, don't average" lesson holds in
-  the neural setting.** Trained-weight columns show the same signs but noisier: neural smoothing
-  inflates baseline survival and the deep tail barely clears n=200 resampling (compresses magnitude)
-  — the expected inductive-bias caveat. torch-gated test added. **93 tests green.**
-
-**2026-07-05 — `grounding` refinement + figure (honest reframing).**
-
-- Re-ran at **18 reps** (n_eval 15000, 30 gens, g grid refined to 9 points). Falsifier pinned in
-  the config *before* running.
-- **forward-KL is the operative neural collapse metric — NOT H or tail-survival.** The RNN's
-  smoothing keeps spurious tail modes alive, so `tail_truth_mass_alive` is flat/**non-monotone**
-  in g (dry 0.54 > most grounded) and H stays 0.77–0.85 of H\*. Stationary **forward-KL** falls
-  monotonically (dry 2.08 → g=0.2: 0.75), significant at g≥0.05 (paired t→3.3; 89% of lineages
-  improve at g=0.2). Refines the earlier "forward-KL AND tail survival" note.
-- **The sharp `g*≪1` is an exact-operator feature, softened by neural smoothing.** Median-recovery
-  grounding (half the KL gap closed) g≈**0.04** (bootstrap CI [0.004, 0.116]) — echoes Layer-1's
-  0.048 — but full (95%) recovery needs g≈0.19. Quantitative `g*≪1` is carried by the histogram
-  **bridge** (0.047); the RNN confirms the SIGN and softens the sharpness (blueprint §3.5 met).
-- **Honest note:** the pre-registered 95%-of-H\*/tail-survival falsifier is *not* met — because
-  those are the wrong metrics for a smoothing model, not because grounding fails. Reported as such.
-- **Robustness fix:** a fully-degenerate RNN can emit only invalid codewords → `measure_distribution`
-  now returns a terminal-collapse sentinel (fixation on the dominant mode) instead of crashing a long
-  sweep. Edge-case test added. `figures/plot_figS6_grounding_rnn.py` written (4-panel, states its own verdict),
-  wired into `make figures` (glob all `plot_*.py` except `plot_E[1-6]`).
-
-**2026-07-05 — neural figures (all five).**
-
-- `figures/plot_{bridge,collapse,grounding,architectures,recombination}.py`, each a pure function
-  of its committed bundle (reuse `figures/_figlib.py`), wired into `make figures` (glob all
-  `plot_*.py` except `plot_E[1-6]` / `_*`). **bridge**: neural histogram runner sits exactly on the
-  exact `H_eq` curve, g*=0.047 (HARD-GATE visual). **collapse**: dry GRU forward-KL climbs, grounded
-  held; H barely moves. **architectures**: grouped bars — forward-KL falls / tail survival rises with
-  grounding across histogram/GRU/MLP. **recombination**: union matches closed form; max-merge rises
-  while mean-distill stays flat (analytic + trained + rho=1 control). **grounding**: the reframed
-  4-panel (forward-KL phase boundary, recovery, metric-choice).
-
-**2026-07-05 — real-MNIST external-validity tier (`mnist_collapse`).**
-
-- New image tier plugged into the existing contract (metrics/grounding/output are data-agnostic and
-  reused verbatim): `mnist_data.py` (load, per-class thickness bins, mode = class×thickness bijection,
-  `MnistSampler`), `mnist_oracle.py` (frozen CNN + deterministic thickness = `ClassifierOracle`,
-  confusion matrix), `mnist_vae.py` (`ConvVAEGenerator`), `mnist_loop.py` (`run_mnist_lineage`), plus
-  `kind=mnist_lineage` dispatch in `experiment.py`, `configs/neural/fig2_mnist_collapse.yaml`,
-  `figures/plot_fig2_mnist_collapse.py`, `figures/mnist_montage.py` (eyeball diagnostic), `MnistCfg`/`OracleCfg`.
-- **Gates:** CNN mode accuracy **98.5%** (30×30 confusion matrix in the manifest = noise floor);
-  VAE gen-0 recovers full 30/30 support (over-smooths freq, KL≈0.5, no prior hole).
-- **Result (4 reps):** dry (g=0) VAE **collapses to a single mode** (KL 0.5→18, support 30→1, tail
-  1.0→0.06, H→0); **g=0.1 holds all 30 modes** (KL≈0.6, full tail, H≈0.9). Collapse + grounding-rescue
-  confirmed on real images. VAE needs ~10% grounding vs synthetic ~5% (cf. the `grounding` finding).
-  **99 tests green** (+5 torchvision-gated). `make mnist` / `make env-mnist` (kept out of `make neural`).
-
-**2026-07-05 — learning kernel (Layer-1 extension) + Riis positioning.**
-
-- Prompted by revisiting Layer 1 vs 1.5 and the Riis 2026 paper (arXiv:2604.08554). Added
-  `knowledge/kernel.py` (`LearningKernelCfg`: reset `u` = smoothing, temperature `τ` = sharpening,
-  floor `ε`), wired into `step.generation_step` (post-refit) / `StepCtx` / `DynamicsCfg` — **identity
-  by default, so the 68 Layer-1 scientific-validation + correctness tests are unchanged.**
-- **Result:** neutral Wright–Fisher fails BOTH neural models, oppositely. VAE regime (n=6000,K=30):
-  neutral is inert, sharpening `τ=0.8` reproduces the collapse-to-one-mode. RNN regime (n=200,K=256):
-  neutral → H=0, mutation `u=0.006` reproduces the H-floor (~0.68). Uniform-mutation overshoots the
-  RNN's KL → its prior is truth-like, not uniform (honest caveat, future refinement).
-- `configs/inheritance/kernel_{sharpen,smooth}.yaml`, `figures/plot_kernel.py` (overlays analytic arms vs
-  the committed neural endpoints), READMEs, `tests/test_kernel.py` (+6). Wired into `make layer1`.
-- **Strategic (see CLAUDE.md finding):** concede "collapse=drift" to Riis (prior art; cite); his
-  mixed environment retains OLD SYNTHETIC (no real-data injection) → pessimistic, no g* that prevents
-  collapse. Our defensible novelty: recombination "merge-don't-average" (flagship), the learning-kernel
-  axis (he flags as future work), grounding-as-immigration, real-weights+MNIST breadth, and the
-  Lamarckian society + vertical/cumulative C3 claim (not yet run). Reposition: from "collapse is drift"
-  to a population-genetic CONTROL THEORY for sustaining open-ended knowledge.
-
-**2026-07-05 — multi-locus society frame (E7/E8): raised the ceiling to enter the society.**
-
-- Prompted by "enter the society with a robust theoretical frame." The single-locus fixed-`p*` model
-  can't express "exceeding" a ceiling. Generalized knowledge to a distribution over **genotypes**
-  (`knowledge/genotype.py`: `L` biallelic loci, `K=2^L`, additive fitness, recombination = product of
-  per-locus marginals). Reuses all K-mode machinery + `make_retention_matrix` (locus mastery).
-- **E8 (star, `kind: society`, `knowledge/society.py`) — the vertical claim:** decorrelated parents
-  recombined; **sexual merge reaches the optimum (12/12, a genotype no parent had)** as parent count
-  grows / `ρ→0`, while best-parent (~8.7) and mean-mixture soup (~11.6) plateau. `configs/inheritance/figS9_specialist_superparent.yaml`,
-  `plot_figS9_specialist_superparent.py`, README. The Fisher–Muller effect for AI.
-- **E7 (`kind: genotype_lineage`, `knowledge/genotype_lineage.py`) — advantage of sex:** sexual lineage
-  adapts faster than asexual (LD→0 vs LD spike). Honest: a speed advantage, not a permanent ratchet gap.
-- **Metaphor shift (GG):** sexual reproduction with **unbounded parents**, not teacher→pupil (which caps
-  at the ceiling). Collapse = asexual degradation; cure = sex, no parent limit. Unifies E4+E6 under
-  evolution-of-sex theory; beyond Riis's single-locus n-grams. `tests/test_genotype.py` (+7).
-  Experiment dispatch (`kind` in {genotype_lineage, society}) + `make layer1` wired.
-
-**2026-07-05 — sexual-transmission model made rigorous (E9/E10): landscape robustness + directed sex.**
-
-- GG excited by the sexual metaphor; wanted it robust before the full society. Added NK landscape
-  (`genotype.nk_fitness`), finite n-parent `crossover`, `hill_climb` (parents = local optima).
-- **E9 (`recomb_landscape`) — "why sex?":** on rugged/epistatic landscapes, blind recombination →
-  **outbreeding depression** (offspring below parents, worse with ruggedness + recombination rate);
-  the optimal recombination rate shrinks with ruggedness. Design rule: merge freely when
-  complementary, sparingly + selectively when entangled.
-- **E10 (`directed_sex`) — AI beats biology:** random ("biological") sex craters with ruggedness
-  (0.66→0.51); **directed sex** (choose mates + select offspring + unbounded parents, iterated) tracks/
-  exceeds the best parent at every ruggedness. The distinctly-AI superpower, no biological analog.
-- Complete picture: dramatic super-parent offspring when complementary (E8); outbreeding-depression
-  risk when entangled (E9); directed sex resolves it (E10). `configs/inheritance/{E9,E10}.yaml`,
-  `plot_{E9,E10}.py`, READMEs, +5 tests (117 green).
-
-**2026-07-05 — the dynamic Lamarckian society (E11): the vertical claim / C3 realized.**
-
-- `knowledge/dynamic_society.py`: finite population of N agents (genotypes) on a rugged NK landscape
-  (reality); composes grounding + directed sex + quality-diversity selection + mutation. Grounding
-  made load-bearing via consensus-conformity (self-consumption): selection on
-  `g·true_fitness + (1-g)·conformity` (GG decision). `kind: dynamic_society` dispatch.
-- **4-arm ablation (12 reps), each breaks distinctly (global_opt≈0.79):** full 0.78 (climbs to optimum,
-  diversity maintained longest); no_sex 0.77; no_diversity/greedy 0.74; **no_grounding 0.48
-  (self-consumption collapse to unfit consensus).** Only the full society climbs. Integrates E1-E6 +
-  kernel + E7-E10 into one system: needs ALL of grounding + directed sex + diversity.
-- `configs/inheritance/fig4_society_ablation.yaml`, `plot_fig4_society_ablation.py`, README, `tests/test_dynamic_society.py` (+5, 122 green).
-  Closes C3 analytically; the LLM rung remains the eventual empirical instantiation.
-
-## Remaining (all optional / next)
-- [ ] **NK/epistasis landscape** (sign epistasis can make recombination harmful — the honest limit of
-  "sex always helps"); **multi-allelic loci**. Deepens the frame.
-- [ ] **Learning-kernel refinement:** truth-like smoothing prior (`prior="truth"`) + measurement floor
-  for a quantitative RNN match; **multi-locus / linkage** modes (class×style) as the rigorous home for
-  recombination. Both enrich predictive power and separate us further from Riis's single-locus n-grams.
-- [ ] **The Lamarckian society experiments** (multi-agent grounding + decorrelated specialists +
-  recombination + QD-selection + re-mint) and the **vertical/cumulative C3 claim** — the highest-ceiling,
-  wholly-novel frame; not yet entered.
-- [ ] **`region_matched`** grounding (R>1), **`remint`** re-mint gate (optional).
-- [ ] **VAE fidelity:** fix the prior-hole mismatch (KL-annealing / free-bits / larger latent) so it
-  clears the gen-0 gate, then add to `architectures`. Or document as a known limitation.
-- [ ] Real-MNIST secondary tier (`ClassifierOracle` + confusion matrix; `--extra mnist`).
-- [ ] `figures/plot_.py` (reuse `figures/_figlib.py`); wire into `make figures`.
-
-## Discovered during work
-
-- **E2 grounding policy vs. the analytic H_eq:** Pred. 3's closed form is derived for *plain* immigration `Multinomial(m, p*)`. Implemented as `policy="proportional"`, and every policy reduces to it at `R=1`. E2 should therefore run at `R=1` (or `proportional`) so the phase-boundary sweep tracks the exact `H_eq`; region structure is E3's concern. Decide E2's `init` (uniform vs truth) when building its config.
-- `init: {uniform|truth}` added to `TruthCfg` (uniform default, mandated by the decay conformance test). E1/E2 may want `truth` start for a clean "tail collapses from the truth" story — revisit in Phase 2.
-
-## Potential agents
-
-*(none proposed yet)*
-
-**2026-07-05 — LLM prototype (`llm_merge`): first real-LLM step, honest/partial.**
-
-- `src/llm/` package: tasks+exact-match verifier, batched eval, LoRA specialise (manual SFT), peft
-  weight-merge (soup/ties), runner (`kind: llm_merge`). Base Qwen2.5-0.5B-Instruct on one 16GB GPU.
-- **Result (seed 1):** merges are the ONLY models competent across all 3 disjoint families
-  (worst-family ~0.25 vs <0.16 for any single specialist) — the Fisher-Muller signature, robust.
-  Overall-exceeds is marginal (soup 0.64 vs best spec 0.63; ties below), and averaging dilutes peaks
-  (lists 0.43->0.26 = "merge don't average" in real weights). Pipeline works end-to-end; strict
-  overall-exceeds needs scale (bigger base/more families/seeds/dilution-resistant merge) = HPC step.
-- Python 3.14 + transformers 5.13 OK; note transformers-5.x apply_chat_template returns a dict.
-  `make env-llm`/`make llm`; `figures/plot_llm_merge.py`, README, `tests/test_llm.py` (+3, 125 green).
-
-**2026-07-05 — LLM merge 7B firm-up on Imperial CX3 (`llm_merge_hpc`): marginal sign → decisive.** ✅
-- Ran on one L40S (46 GB) via `/imperial-hpc` runbook; 8 min walltime; Qwen2.5-7B-Instruct, 200 tests/family.
-- **Both merges 0.87 overall > best specialist 0.77** (decisive +10 pts) and beat every specialist on
-  every family; worst-family 0.62 vs ≤0.57. Both 0.5B caveats resolved: overall-exceeds is now clean,
-  and dilution VANISHES (merge 0.62 > lists-spec 0.57 on lists) — dilution was a small-model artefact.
-- `results/llm_merge_hpc/` (README legend, data-driven figure title). Next refinement: module-level
-  union-preserving recombination (MoE-expert/adapter-union = real-weight E8 max-merge), not delta-avg.
-
-**2026-07-05 — MoE-expert / union recombination (`llm_moe`): E8's `max` vs `mean` in real weights.**
-- `src/llm/moe.py`: router (oracle + training-free nearest-centroid over base embeddings) + MoE
-  generate + router-free per-module `max_merge`. `kind: llm_moe` reuses the cached specialists.
-- **0.5B result:** routing beats fusion decisively — overall 0.74/worst 0.43 vs soup 0.64/0.26, no
-  dilution (recovers each specialist's own-family peak). Riders: learned router trivially perfect
-  (1.00, lexically-separable families) and static `max_merge` a poor union (0.46, not input-adaptive).
-- `configs/llm/{moe,moe_hpc}.yaml`, `figures/plot_llm_moe.py`, README, +2 tests (127 green),
-  `hpc/llm_moe.pbs`.
-- **7B firm-up (`llm_moe_hpc`, CX3 L40S, 9 min): the ordering FLIPS.** At 7B fusion wins —
-  soup 0.87 > routing 0.84 > max_merge 0.78 (0.5B had routing 0.74 > soup 0.64). Routing is capped at
-  the best parent per family; fusion *composes beyond* it at a capable base (soup lists 0.62 >
-  spec 0.57). So "merge, don't average" is a **weak-base law**, not universal — union wins under
-  dilution (0.5B), fusion wins under composition (7B). Refines E8. Next: fusion + offspring-selection
-  (directed sex). `results/llm_moe_hpc/` README + regime-aware figure.
-
-**2026-07-05 — Directed sex (`llm_directed`): E10 in weights = breed offspring + select on verifier.**
-- `src/llm/directed.py`: sample a population of Dirichlet-weighted merges, score on a held-out VAL
-  split, keep the best-overall + best-worst-family, report on a fresh TEST split. `kind: llm_directed`.
-- **0.5B:** directed selection beats the single a-priori soup on the bred objective — directed_overall
-  0.69 > soup 0.64; directed_balanced worst-family 0.37 > soup 0.26. Riders: single-objective selection
-  trades off the other axis (overall-breed tanks lists 0.17); a global blend still trails per-input
-  routing (0.74). **7B (CX3 L40S, 9 min):** directed ≈ soup (0.868 ≈ 0.873) — soup already composes to
-  ceiling on near-saturated families (strings/arith 1.00), no fitter offspring to breed.
-- **Through-line:** recombination refinements pay off ∝ how suboptimal the default soup is — big at
-  0.5B, nil at 7B. Honest limit: 7B families near-saturated; a harder benchmark is the fair next test.
-- `configs/llm/{directed,directed_hpc}.yaml`, `figures/plot_llm_directed.py`, READMEs, `hpc/llm_directed.pbs`,
-  Makefile `llm` target, +3 tests (130 green). `results/llm_directed{,_hpc}/`.
-
-**2026-07-05 — HARD benchmark: the 7B "fusion wins / no headroom" nulls were SATURATION artefacts.** ✅
-- Easy families saturated 7B (strings/arith 1.00), confounding the moe/directed 7B nulls. Built a hard
-  task variant (`hard: true`: multi-step lists, Caesar ciphers, multi-step/larger arith) threaded through
-  make_tasks/train_specialist/runners; hard specialists cache as `spec_*_hard`. Ran both at 7B on CX3
-  (one L40S, 24 min, unsaturated).
-- **Both nulls flip back to the 0.5B ordering:** union/routing 0.500 > fusion 0.40 (soup dilutes the
-  strings specialist 0.665→0.300, below even the best single specialist 0.425); directed selection 0.492
-  > soup 0.392 (+10 pts). **The operative variable is HEADROOM, not base-size** — "merge, don't average"
-  and "directed sex" hold whenever there's room to lose to dilution (weak base OR hard tasks); fusion only
-  wins where easy tasks let a strong base compose to ceiling. Vindicates E8 max>mean at 7B.
-- `configs/llm/{moe_hard,moe_hard_hpc,directed_hard_hpc}.yaml`, `hpc/llm_hard.pbs`, hard READMEs+figures,
-  +1 test (131 green). `results/llm_{moe,directed}_hard_hpc/`.
-
-**2026-08-11 — PNAS submission campaign opened.** GG approved PNAS as target after the post-hold
-re-assessment (fresh lit scan: speciation/sex-framing/mating-systems/headroom all still unclaimed;
-new concessions First-Extinction 2509.20101 + qt-trait 2407.17493 + verifier-injection 2510.16657;
-E13 exposed to richer-symmetry objection 2606.23607). Full plan: `tasks/workorder-pnas-submission.md`.
-Phases: (1) E13 hardening (scale-aware alignment + emergent-divergence condition), (2) arXiv preprint,
-(3) llm_speciation + multi-seed LLM arc, (4) PNAS-format manuscript (5 consolidated figures, dual
-audience), (5) submission mechanics (Zenodo DOI, cover letter, editor/reviewer suggestions).
-
-**2026-09-07 — `llm_society` opened: the composed society at LLM scale (C3), pre-submission.** GG
-decision: a weeks-scale experiment closing the paper's largest stated gap must be in the submission
-("any reviewer would ask to see it"); rent compute if CX3 queues fail. Full design + falsifiers +
-schedule: `tasks/workorder-llm-society.md`. E11 re-instantiated in LoRA agents: grounding knob in
-the evaluation channel (g·verifier + (1−g)·conformity), self-consumption inheritance (children
-distilled from their source's own answers), directed sex (complementary pairing + Dirichlet
-offspring screened on the arm's own signal), QD selection. `src/llm/society.py` (+4 pure tests,
-155 green), `kind: llm_society`, configs `society_smoke.yaml` / `society.yaml`. Stages: smoke
-(local, ~15 min) → pilot full vs no_grounding (GG gate) → 4-arm × 3-seed CX3 campaign → figure +
-manuscript fold-in.
-
-**2026-09-07 — v1 `llm_society` campaign landed (4 seeds) and is NEGATIVE; v2 pre-registered.**
-Best-agent overall at gen 9, 3-seed means: `no_sex` 0.558 ≥ `no_diversity` 0.539 ≥ `full` 0.506 ≫
-`no_grounding` 0.436 (worst arm in every seed from gen 2). Conformity−truth gap does not separate the
-arms. Read through the framework the null was structurally guaranteed (near-clone founders over 3
-families; 2³ competence states; linear blending at 0.5B = the dilution regime; parents truncated
-before breeding, unlike E11's survival-over-pool; `n_test`=40 → SE 0.079) — details and fixes in
-`tasks/prereg-llm-society-v2.md` §1, lesson in `tasks/lessons.md`. Nothing enters the manuscript;
-Fig. 1A's "stated gap" stands. **v2** (`kind: llm_society_v2`): L=12 families / one founder each,
-confidence-routed union inheritance, pooled survival, checkpoint+resume, 240 test items, g=0.85,
-G=12; six numerical hypotheses H1–H6; calibration gates C1–C5 must pass before submission (GG
-reviews). GG decisions: 0.5B; `sex_linear` dropped (H2 deferred); no family vetoes.
-- [x] families / operators / v2 loop / calibration runner / configs / PBS array / figure script / 9 tests (164 green)
-- [x] smoke (4 arms, figure + stats script) → calibration A pass 1 (6/17 in band) → pass 2 (9 in band; C1b needed the gate re-derived 0.35→0.41 from the grid) → GG chose L=9
-- [x] calibration B: C2 FAILED as pre-registered (retention ≤0.81 at k≤150; interference, not the observation floor) → C2b: k=300 + confidence gate τ=0.5 gives mean retention 0.87 (PASS); C3 operator half passes (union holds both families, linear loses one), retention half re-run gated; C5 passes (consensus 0.31)
-- [x] campaign configs set: L=9, k_inherit=300, conf_gate=0.5, epochs=3, n_test 27/family, g=0.85, G=12; PBS 16 elements × 8 h
-- [x] gated cross (C3): two-skill child plateaus at ~0.85×/0.8× of parents at any budget (3 vs 6 epochs; r64 hurts); tight gate τ=0.85 gives the 6-epoch retention at 3 epochs
-- [x] **GG go/no-go (21:30): NO-GO at 0.5B** — the vertical claim needs 5–6 co-resident skills the r=16 adapter cannot hold; today = a measured transmission ceiling (SI material). 7B plan drafted: prereg §13
-- [ ] GG: 7B scope (headline ~80 / H3+H4 ~120 / full ~260 L40S-h), Phase-0 task design go, SI text timing
-- [ ] SI: the 0.5B calibration ceiling as the reason the tier was not run (prereg §11 row 4) — three limits, numbers from results/llm_society_v2_calib_*
-- [ ] REPRODUCING.md: rows for the v1 campaign (4 seeds), v2 smoke, and the 9 calibration bundles
-- [ ] stage code on CX3, `qsub hpc/llm_society_v2.pbs` (16 elements); local hedge = `society_v2_s1.yaml`
-- [ ] `figures/stats_llm_society.py` (per-seed paired contrasts H1/H3/H4, AUC for H5, supplied-vs-retained for H6)
-- [ ] fold the outcome per prereg §11
-
-**2026-09-08 — v3 `llm_compose` run (3 seeds): H1 PASS, H2–H5 null; design could not show the claim.**
-Composition at gen 0 is real and replicated (surplus +0.087/+0.033/+0.093; union-exceedance ~0.12; also
-on MATH-500). Decay hypotheses uninterpretable: the lineages barely drifted (q_math 0.54 → 0.50–0.57)
-and, more fundamentally, a fixed skill set has its ceiling at gen 0 — GG: "are models learning NEW
-skills at EACH generation? … that was not the problem being addressed." v3 was Weismannian (fresh LoRA
-each generation) and retention-only. Two of my errors: C3 unchecked (code specialist 0.075 on MBPP →
-q_code noise), and three premature reads of a single-seed trajectory.
-**v4 `llm_curriculum` — continual learning in a population** (`prereg-llm-society-v4.md`): Lamarckian
-channel (`continue_lora_training`), Latin-square curriculum (complementarity 1.0 → 0.0 by construction,
-H6 predicts the *shape*), arms isolated/society/society_dry/seed_bank (GG's ancestor-merge idea —
-temporal vs spatial complementarity, direction uncommitted), single-shot SoTA baselines at matched budget
-as the falsifier. GG decisions: 3×9×9, replay fixed-total, baselines get the same directed selection.
-- [x] G0 PASS (accumulation 0.74 → 0.95); G2 FAIL ×2 (v2 families don't interfere; one pair at +0.65);
-  G3 negative (merging costs −0.01…−0.13 with nothing to repair); base = 0.094 → one family lifts all to 0.417
-- [x] **v5 curriculum**: 11 real-dataset families, 5 answer shapes, per-family verifiers, disjoint splits
-  (`curriculum_data.py`, +6 tests, 56 green); selection rule fixed in prereg §8a
-- [ ] stage A calibration running (`curriculum-v5-calib`): base + 11 specialists × 11 families
-- [ ] stage B: zero-replay forgetting probe on the survivors (mean drop ≥ 0.15, not single-family)
-- [ ] GG go/no-go → seed 1 local + CX3 array (seeds 2–3), then baselines
-
-**2026-09-08 — v5 curriculum campaign done (3 seeds); all hypotheses fail; the useful finding is a
-split in the theory.** Real-dataset curriculum (6 families, 5 answer formats, per-family verifiers)
-replaced the procedural set. Results: not-merging wins (0.80), merging-with-own-ancestor middling
-(0.66), merging-with-a-peer collapses (0.27), single-shot merging unstable (0.125–0.764). Cause:
-two families are answer-format destroyers that propagate through merges and compound because
-offspring continue the lineage. Scope limits: merging was obligate (no veto) and there is NO
-selection between lineages — a gene-flow experiment, not a selection one.
-**Key new measurement:** same-skill adapters (seed/data draw only) are near-orthogonal in weight
-space (cos +0.006), disagree on 24% of prompts, and **merging them beats the best parent by +0.087,
-exactly at the either-right ceiling**. So decorrelation-in-what-you-know is harmful while
-decorrelation-in-how-you-encode-it is beneficial — the framework's single rho conflates them.
-- [ ] v6: three arms (no-merge · complementary · parallel) + veto + population selection (~30 GPU-h)
-- [ ] decide how the E9-risk result and the two-variations split enter the manuscript (beside Fig. 5A)
-
-**2026-09-08 (later) — four mechanism probes; two of my explanations retracted.** (1) Same-skill
-adapters: 85% of a LoRA's change is run-specific noise; merging two beats the better parent by +0.087,
-at the either-right ceiling. (2) Denoising before crossing adds +0.025 on both skills at once
-(inbred-lines signature). (3) A single merge of clean adapters is PROTECTIVE (0.825 vs 0.550 best
-parent) — retracts "destructive skill propagates through merges". (4) Five chained convex merges lose
-nothing, while signal-preserving additive weights collapse (1.02 vs 0.52 retention) — retracts
-"geometric signal dilution"; the real constraint is bounding drift from the base. (5) Scaling probe
-(GG's control): base 0.000, and the adapter works down to 1/8 then dies — 1/16 = 0.450, 1/32 = 0.000.
-So the chain's apparent retention was ANSWER FORMAT supplied by the dominant partner, not the skill.
-Consistent with Fig. 3C-D: functional conflict predicts merge damage, weight geometry does not.
-- [ ] test the remaining candidate: continued training ON TOP of merged weights (chain + fine-tune each round)
-- [ ] if confirmed, the finding is about output conventions propagating through merges — reframe accordingly
-
-**2026-09-08 (evening) — scaling thresholds measured; bespoke weights tested and NEGATIVE.**
-Per-skill dose-response: cliffs are sharp and skill-specific (boolq dies at 1/8, arc survives to 1/8
-at its BEST score 0.92); 4 of 6 adapters are over-trained and improve when scaled down (mnli
-0.40->0.68 at 1/4). Denoising does NOT move the cliff -> the limit is signal MAGNITUDE, not
-signal-to-noise, so denoising buys quality (+0.025) but not merge depth. Bespoke per-skill weights
-(cliff and optimum variants) both LOSE to plain uniform 1/6 (0.686-0.689 vs 0.708): solo curves don't
-transfer because effective strength is relative, not absolute.
-KEEP: (a) one merged model beats six separate specialists on their own tasks (0.708 vs 0.678);
-(b) attenuating each specialist to its own optimum gives 0.755 with no merging and no retraining.
-- [ ] still untested: continued training ON TOP of merged weights (the last candidate for the v5 collapse)
-- [ ] decide whether the compression trade (0.708 merged vs 0.755 separate) is a paper result or an appendix note
-
-**2026-09-08 (late) — last candidate eliminated; v5 collapse recorded as UNEXPLAINED.**
-merge-then-train beats merge-only on the tracked skill in 4/5 rounds and on the incoming skill in 5/5
-(mnli 0.867 vs 0.467); it even absorbs the round-4 format shock. So training-on-merged-weights is not
-the mechanism — it is the best procedure tested. All three proposed explanations for the v5 collapse
-are now refuted by direct test. Remaining structural difference: v5 merged multi-skill accumulating
-lineages (rank 16, up to 6 skills), these chains merge clean single-skill adapters -> capacity is the
-suspect, but NOT claimed: three guesses have been wrong, a fourth is not earned.
-- [x] veto arm: recommend NOT running — v5 is unreportable regardless (awaiting GG)
-- [ ] GG decision: close the LLM-society file for this paper; keep engineering findings separate
-
-**2026-09-08 (late) — VETO ARM: one bit of selection converts collapse into a healthy trajectory.**
-Seed 1: veto 0.783 vs obligate-merge society 0.211 vs isolated 0.814. Veto rate 67%, and structured:
-1/3 declined at generations 0-2, then 3/3 at generations 3-5 — the population stops merging exactly as
-complementarity falls (1.00 -> 0.80 -> 0.67). GG's caveat is right: once all merges are declined the
-arm IS isolated, and isolated overtakes at gen 4 and finishes higher. Honest claim: recombination pays
-only while partners differ, the population detects when that ends, and still finishes slightly behind
-never merging. Makes the v5 negative reportable (risk + remedy + limit) beside Fig. 5A.
-I had recommended skipping this experiment; that was wrong — I judged it by whether it would rescue a
-written-off conclusion rather than by what it would measure.
-- [ ] CX3 array 4007703 (seeds 2-3) -> confirm the veto rate pattern and the isolated crossover
-- [ ] optional control: forced stop at gen 3, to test whether the veto's TIMING matters
-
-## Manuscript revision — multigenerational LLM population + new literature (2026-09-09)
-
-Plan: `~/.claude/plans/we-are-going-to-cheerful-fog.md` (approved by GG 2026-09-09). Dual-audience
-writing standard is paramount: every term defined at first use with an example from each field.
-
-- [x] Pre-write checks: chance-corrected competence count (claim dropped — single adapters unlock ~4
-      families via shared formats at gen 0; report retention_seen flat ≈0.78 and no first-family erosion
-      instead); Spearman veto-rate vs complementarity ρ=−0.57, p=0.013, n=18; pop-gen citations verified
-- [x] Fig. 6 → five panels (D trajectory, E veto rate vs complementarity); caption; REPRODUCING.md rows
-- [x] main.md: Abstract, Significance, Table 1 row, new Results subsection, society/speciation pointers,
-      Discussion (design rules, CL, borrowed/new, limits, creative diversity, outlook), Methods
-- [x] si.md: S3 text, Table S1/S2 rows, M2/M5/M6 additions, SI figures list; fixed two stale SI
-      citation numbers (41→44, 43→46 pre-renumbering) and one leftover "honest"
-- [x] References: +8 (73–80 appended, then renumbered to first-appearance order by
-      `paper/manuscript/renumber_refs.py`; 80 refs, 0 orphans, recheck = 0 renumbered)
-- [x] Verification: fig6 rendered+inspected twice (legend fix); PDFs build (main 24 pp, SI 11 pp; no
-      unresolved FIG markers); gap/meta-language grep clean; two-reader pass (added "verifier",
-      "frozen", validation glosses); `make test` 196 passed
-- [x] **Compression pass (GG directive 2026-09-09).** 7,318 → 6,764 total, of which 6,520 is running
-      prose and 244 is the Table 1 grid (PNAS counts tables separately). −554 words with no content
-      removed: sentence-level density throughout, one genuine de-duplication (the MNIST collapse
-      figure was stated twice, in the biological-model section and again under Grounding — kept the
-      Grounding statement, which carries the 2× estimator-bias comparison), and two detail blocks
-      moved to where they belong (predictive-test per-seed ρ ranges → new Table S2 row; Methods
-      pointer to SI Methods). PDF 24 → 23 pp. Every number, citation, hedge, and gloss retained.
-      Further cuts would need structural calls: moving the blending-inheritance Proposition to SI
-      (~130 words, but it is a flagship claim) or trimming review-calibrated hedges — left for GG.
-- [x] **Fig. 1A updated (GG, 2026-09-09).** The composed-society × language-model cell was rendering
-      "open — the stated gap"; it now carries the result ("6 generations × 3 lineages: obligate merging
-      collapses, a declinable merge tracks partner complementarity") with tag Fig. 6D–E, and the
-      biological-model cell's tag narrowed to Fig. 6A–C. Tier header corrected to "Qwen 0.5B, 1.5B &
-      7B; exact-match and execution verifiers". Dead `OPEN` rendering branch removed. Caption in
-      build.py no longer ends on the gap clause. Repo-wide grep for gap language now clean.
-- [x] **Zotero library built (GG, 2026-09-10).** All 80 references resolved to authoritative metadata
-      via doi.org content negotiation: 77 from DOI (53 printed in the manuscript, 22 found by
-      title-matched Crossref search, 2 hand-verified — Brinkmann *Machine culture*, Schwarz *Progress &
-      Compress*), 3 hand-written because they predate DOIs (Jenkin 1867, Fisher 1930, Templeton 1986).
-      Artifacts in `paper/manuscript/refs/`; generator `paper/manuscript/build_zotero_library.py`.
-      **Not yet in Zotero** — the app is closed and its library lives in ownCloud; direct writes to
-      `zotero.sqlite` are unsafe, so import is one step in the Zotero UI (see refs/README.md).
-- [ ] Optional: sync long-form `paper/the-evolution-of-sex-for-ai.md` L797 ("LLM society is unbuilt")
-
-## Manuscript round 4 — research-paper restructure (GG feedback 2026-09-10)
-
-Plan: `~/.claude/plans/we-are-going-to-cheerful-fog.md`. Diagnosis: mean sentence 49 w vs GG's own
-31 w, 50% of sentences over 40 w, em-dashes 11.4/1k vs his 0.57 — long sentences in short paragraphs,
-the inverse of his rhythm. That is the measurable cause of "too cryptic".
-
-- [x] Phase 1 — Results restructured to question+design / result / implication; seven descriptive
-      section titles; grounding leads with the novel per-item floor and cites the g≈0.05 threshold as
-      corroboration of published values; Proposition lifted into its own block; Recombination split by
-      experiment; novelty of Fisher–Muller-in-LoRA conceded in place
-- [x] Phase 2 — Main figures 7 → 5. Old Fig. 4 (E4/E8) and Fig. 5 (E9/E10/E14) dissolved; E9/E10/E14
-      to SI as established results with no real-model counterpart. Panels reordered so the real-model
-      result leads and the inheritance model follows as reference (Fig. 2A/B, 4A–B before 4C–E,
-      5A–D before 5E–F). Fig. 1A column relabelled "Inheritance model (reference)"; tags repointed.
-      "biological model" → "inheritance model" throughout.
-- [x] Phase 3 — Prose to the measured fingerprint: mean sentence 49.0 → 31.4 w (GG's own 31.2),
-      >40-word sentences 50% → 22.6% (his 20.8), em-dashes 11.4 → 3.42/1k (his 0.57), semicolons
-      13.6 → 8.6, colons 13.6 → 8.4, antithesis 1.77 → 1.81/1k after re-cutting the ones the rewrite
-      introduced. 21 pp (from 23).
-- [x] Phase 4 — Discussion rebalanced: the 476-word (68 w/sentence) continual-learning block and the
-      242-word (80 w/sentence) borrowed/new block broken into paragraphs of 5–6 sentences.
-- [ ] Remaining: two-reader accessibility pass over the rewritten sections; `Fig. 2` cross-reference
-      in the inheritance-model section may want to be `Fig. 2A`; consider whether the Significance
-      statement and Abstract need to match the new section titles.
-
-## Manuscript review pass (2026-09-11)
-
-Review of `paper/manuscript/main.md` (novelty, accessibility, calibration, cheap experiments); corrections applied:
-- [x] Abstract rewritten (one idea per sentence, jargon removed, 250 words); own-ancestor result added, mating-breadth hypothesis dropped
-- [x] Own-ancestor (seed-bank) merge given its own paragraph, Table 1 row, and design rule
-- [x] Emergent null (merge rescues forgetting specialists) and the overlap control (delta-cosine +0.60 → +0.03) promoted from asides to findings
-- [x] "Five specific results" recut to four; grounding floor named a corollary, ablation named a demonstration (conformity builds grounding in)
-- [x] Latin-square collinearity of complementarity and generation stated explicitly in Results
-- [x] Two SI-only design rules marked as inheritance-model predictions; 7B Fisher–Muller marked single run
-- [x] Terms defined at first use: forward KL, BDM, TIES, linear-mode-connectivity barrier, low-rank factor space, oracle parent potential
-- [x] 70-word speciation sentence split; Fig. 5 E–F, Fig. 3 C–D, Fig. 4C–E cross-refs added; stale "Fig. 6D–E" in SI Table S1 → Fig. 4A–B
-- [x] Author email fixed; PDF rebuilt (22 pp)
-- [ ] Cheap experiments proposed, none run: forced-stop-at-gen-3 control; non-Latin-square curriculum breaking the complementarity/generation confound; seeds 2–3 for the single 7B runs; pre-merge disagreement vs realised penalty on the existing population checkpoints; withholding curriculum; stylistic-diversity readout on saved generations; E11 with alternative selection schemes
-- [x] Compression/accessibility pass (2026-09-11): main-text prose 6,902 → 6,117 words (−11%); em-dashes 15 → 0; antithesis 0.33/1k; all 81 citations, 5 figure markers and every headline number verified present by script; PDF 22 → 21 pp. Pre-pass copy kept in session scratchpad only.
-
-## Experiments 1–3 from the manuscript review (2026-09-11) — plan `~/.claude/plans/atomic-rolling-sprout.md`
-
-- [x] `merge_until` (forced stop) and `orders` (custom curriculum) keys in `src/llm/curriculum.py`; manifest records them; +3 tests (127 green)
-- [x] configs `curriculum_v5_stop3.yaml`, `curriculum_v5_decor.yaml` (complementarity 0.00/0.67/0.70/0.58/0.33/0.00 verified); prereg §8h written before running
-- [x] PBS: `hpc/llm_curriculum_controls.pbs` (seeds 2–3 × {stop3, decor}), `hpc/llm_7b_seeds.pbs` (seeds 2–3, merge → moe_hard → directed_hard)
-- [x] 7B seed-1 bundles moved to `results/llm_*_hpc/s1/`; `load_seed_bundles` in `_figlib`; fig3 B, `plot_llm_{merge,moe,directed,seeds}.py` seed-aware (no more `.iloc[0]`)
-- [x] `figures/stats_llm_curriculum.py` (shared loader, now used by `make_figs._load_curriculum`; contrasts; partial-correlation test) and `figures/stats_llm_7b_seeds.py`; both reproduce the published numbers on existing bundles
-- [x] **Experiment 1 decided (3 seeds):** forced stop 0.793 vs veto 0.792 vs isolated 0.796 vs society 0.269; veto − stop3 = −0.008/−0.006/+0.011 (all within the pre-registered ±0.03). Reading: the declinable merge's outcome is explained by *when* it stopped; the "evaluation adds value beyond timing" reading is dropped. Fig. 4A carries the dashed control; `results/llm_curriculum_v5_stop3/README.md`
-- [x] **Experiment 2 decided (3 seeds):** partial ρ(declined, complementarity | generation) = −0.07 (CI −0.21…+0.09); partial ρ with generation = +0.31. Declines track generation, not complementarity; the modifier/reduction-principle reading is withdrawn. Decor veto 0.790 = decor isolated 0.790. `results/llm_curriculum_v5_decor/README.md`; Fig. 4B now shows both curricula
-- [x] **Experiment 3 done (7B, seeds 1–3, 33 min/seed on one L40S):** merge − best specialist +0.066 ± 0.036 (3/3); routing − soup +0.094 ± 0.015 (3/3); directed − soup +0.073 ± 0.031 (3/3). Not replicated: 'soup below best specialist on hard' (1/3; mean +0.001) — sentence softened in main text and caption. Fig. 3B now mean ± CI; READMEs carry per-seed tables
-- [ ] GG: `ssh -fN hpc`; then rsync code, `qsub hpc/llm_curriculum_controls.pbs` and `qsub hpc/llm_7b_seeds.pbs`
-- [ ] after data: fig4 (stop3 line; decor decline curve), captions in `build.py`, main/SI/REPRODUCING/READMEs/CLAUDE.md numbers from the stats scripts only
-- Discovered: the venv carried paths from before the repo moved into `LLMs/` (stale shebangs; `uv run pytest` could not spawn). `pytest` re-installed; other console scripts still stale — `uv sync --all-extras --reinstall` would fix all. Hardening candidate: specialist cache key lacks the base model (fails loudly, not silently).
-
-## Venue + novelty audit (2026-09-11)
-Target: Nature Machine Intelligence first; PLOS Comput Biol as the venue reaching both ML and pop-gen readers. All PNAS wording removed from `paper/manuscript/` sources (SI Appendix → Supplementary Information; build/tex comments). Directory name `paper/manuscript/` kept (Makefile/REPRODUCING paths); Significance statement kept pending GG decision.
-Literature audit (three WebSearch sweeps) found claims that need rewording/citations before submission:
-- [x] "Every merging study merges once" is false → narrow to "no study combines per-generation skill acquisition with repeated, optional merging across lineages". Cite iterated-merging work: model kinship 2410.12613 (stagnation by gen 2, inbreeding analogy), GENOME 2503.01155, M2N2, TIME 2412.06712, MagMax, ACMap 2412.18219 (early-stop precedent), K-Merge 2510.13537 (similarity-gated merge), SFA/"Soup to go" 2501.05559 + IMM 2503.02103 (ancestor-averaging precedent)
-- [x] Predictor section: "functional > weight geometry" is already shown by Cao 2603.09463 (must-cite), Zhu 2608.09490, Zhou 2601.22285 (gradient > cosine). Reframe novelty as held-out predictive design + the overlap control (cosine = shared-data artefact; not found anywhere)
-- [x] Speciation: credit permutation+rescaling decomposition to Git Re-Basin + REPAIR 2211.08403; cite ZipIt 2305.03053, Sharma non-local 2410.12766 for residual barriers; Git Re-Basin §5.4 already merges complementary-class parents. Keep as new: conflicting-label manipulation, three-arm contrast, emergent null (against Pari 2411.02207 / Horoi / Kozodoi)
-- [x] Grounding: must cite Alemohammad 2307.01850 (fresh-data loop fixed point), Bertrand 2310.00429 (stability theorem in real fraction), Dohmatob 2402.07043 + 2410.04840 (counter-claim: any synthetic fraction caps performance — reconcile with H_eq one stationary value per (sweep, replicate), averaging
-    `value_col` over the final `last_frac` of generations. Use for any logged metric
-    (heterozygosity, tail_mass, ...)."""
-    rows = []
-    for (gval, rep), sub in df.groupby([sweep_col, replicate_col]):
-        v = sub.sort_values(gen_col)[value_col].to_numpy()
-        k = max(1, int(round(last_frac * v.size)))
-        rows.append({sweep_col: gval, replicate_col: rep, value_col: v[-k:].mean()})
-    return pd.DataFrame(rows)
-
-
-def _interp_crossing(g, H, target):
-    """First upward crossing of `target` by the (monotone-ish) curve H(g), by linear
-    interpolation between grid points. Returns (g_star, status) with status in
-    {'ok', 'below_grid', 'above_grid'}."""
-    g = np.asarray(g, float); H = np.asarray(H, float)
-    o = np.argsort(g); g, H = g[o], H[o]
-    if H[0] >= target:
-        return g[0], "below_grid"        # already above at smallest g swept
-    idx = np.where(H >= target)[0]
-    if idx.size == 0:
-        return g[-1], "above_grid"       # never reaches target within swept range
-    i = idx[0]
-    g0, g1, H0, H1 = g[i - 1], g[i], H[i - 1], H[i]
-    if H1 == H0:
-        return g1, "ok"
-    return g0 + (target - H0) * (g1 - g0) / (H1 - H0), "ok"
-
-
-def critical_grounding(stationary_df, H_star, frac=0.95, sweep_col="g",
-                       value_col="heterozygosity", n_boot=2000,
-                       ci=(2.5, 97.5), seed=0):
-    """Operational critical grounding fraction g*: the g at which stationary
-    heterozygosity first reaches `frac` * `H_star`, with a percentile-bootstrap CI
-    over replicates.
-
-    `stationary_df`: one row per (sweep_col, replicate_col) with the stationary value
-    (e.g. the output of reduce_to_stationary). `H_star`: heterozygosity of the truth,
-    = metrics.heterozygosity(p_star). Returns a dict with g_star (point estimate on the
-    replicate means), ci_low, ci_high, status, target_H, frac, n_boot.
-
-    Note: 'status' flags right/left censoring. If the sweep does not bracket the target,
-    widen the g grid rather than trusting a censored g*."""
-    target = frac * H_star
-    gs = np.sort(stationary_df[sweep_col].unique())
-    by_g = {gv: stationary_df.loc[stationary_df[sweep_col] == gv, value_col].to_numpy()
-            for gv in gs}
-    mean_H = np.array([by_g[gv].mean() for gv in gs])
-    g_star, status = _interp_crossing(gs, mean_H, target)
-
-    rng = np.random.default_rng(seed)
-    boots = np.empty(n_boot)
-    for b in range(n_boot):
-        Hb = np.array([rng.choice(by_g[gv], by_g[gv].size, replace=True).mean()
-                       for gv in gs])
-        boots[b], _ = _interp_crossing(gs, Hb, target)
-    lo, hi = np.percentile(boots, ci)
-    return {"g_star": float(g_star), "ci_low": float(lo), "ci_high": float(hi),
-            "status": status, "target_H": float(target), "frac": frac,
-            "n_boot": n_boot}
-```
-
----
-
-## 2. New online metric in `src/inheritance/metrics.py` (optional band panel)
-
-Compute this each generation from the current `p` and log the per-band arrays exactly
-like the existing per-region metrics (e.g. columns `tail_frac_alive_band{b}` and
-`tail_mass_alive_band{b}`, or long form). Both returned quantities are bounded in [0,1]
-— do **not** use a raw mass ratio (tiny deep-band denominators make it explode).
-
-```python
-def tail_band_metrics(p, p_star, tail_mask, n_bands=4, alive_eps=1e-9):
-    """Stratify the tail into `n_bands` equal-count rarity bands (band 0 = rarest /
-    deepest). Return two BOUNDED [0,1] arrays of length n_bands:
-      frac_alive[b]        fraction of band-b items with p > alive_eps
-      truth_mass_alive[b]  share of band-b's TRUE mass (sum p_star) carried by
-                           still-alive items
-    Averaged over replicates, deeper bands sit strictly below shallower ones and the
-    gap narrows as grounding rises -- the per-item m*p*_i >~ 1 survival threshold made
-    visible (blueprint prediction 4). Single-run values are noisy; always average over
-    replicates before plotting."""
-    import numpy as np
-    idx = np.where(tail_mask)[0]
-    order = idx[np.argsort(p_star[idx])]           # rarest first
-    bands = np.array_split(order, n_bands)
-    frac_alive = np.empty(n_bands)
-    truth_mass_alive = np.empty(n_bands)
-    for b, items in enumerate(bands):
-        alive = p[items] > alive_eps
-        frac_alive[b] = alive.mean()
-        ps = p_star[items]
-        truth_mass_alive[b] = ps[alive].sum() / ps.sum() if ps.sum() > 0 else np.nan
-    return frac_alive, truth_mass_alive
-```
-
----
-
-## 3. Figure updates: `figures/plot_fig2_grounding_sweep.py`
-
-- **Middle panel:** call `critical_grounding(reduce_to_stationary(df), H_star, frac=0.95)`
-  and draw a vertical line/marker at `g_star` with a shaded CI band; annotate
-  `g* ≈ {g_star:.3f} (95% CI [...])`. Also report `frac=0.90` in the caption so the
-  "3–5%" range is explicit. Soften the title from "critical grounding ratio" to
-  e.g. "grounding saturates by g* ≈ 0.05 (95% of H*)".
-- **Right panel:** add stationary **tail-mass-retained** vs g as a second series
-  (`reduce_to_stationary(df, value_col="tail_mass")`), on a twin axis if scales differ,
-  so mass-healthy vs items-poor is visible in one panel.
-- **`g=0`:** drop it from the stationary fit *or* mark it hollow with a note
-  "pre-convergence; true H_eq(0)=0".
-- **Optional band panel:** if `tail_band_metrics` is logged, add a small-multiples or
-  grouped-bar panel of `frac_alive` per band across the g sweep (replicate-averaged).
-
----
-
-## 4. Tests to add (`tests/test_analysis.py`)
-
-```python
-import numpy as np, pandas as pd, pytest
-from inheritance.analysis import reduce_to_stationary, critical_grounding
-from inheritance.metrics import tail_band_metrics
-
-
-def _H_eq(n, m, Hs): return Hs * m * (2 * n + m - 1) / (n + 2 * n * m + m * m)
-
-
-def _synthetic_E2(n=200, Hs=0.95, reps=100, seed=1):
-    gs = [0.0, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.4]
-    rng = np.random.default_rng(seed); rows = []
-    for g in gs:
-        m = 0 if g == 0 else int(round(g * n / (1 - g)))
-        Htrue = 0.10 if g == 0 else _H_eq(n, m, Hs)   # g=0: finite-time artifact
-        for s in range(reps):
-            rows.append({"g": g, "seed": s, "heterozygosity": Htrue + rng.normal(0, 0.004)})
-    return pd.DataFrame(rows)
-
-
-def test_critical_grounding_matches_known_crossing():
-    stat = _synthetic_E2()
-    r = critical_grounding(stat, H_star=0.95, frac=0.95, seed=7)
-    assert r["status"] == "ok"
-    assert 0.03 < r["g_star"] < 0.07                   # ~0.047 for frac=0.95
-    assert r["ci_low"] <= r["g_star"] <= r["ci_high"]
-    r90 = critical_grounding(stat, H_star=0.95, frac=0.90, seed=7)
-    assert r90["g_star"] < r["g_star"]                 # lower bar -> smaller g*
-
-
-def test_reduce_to_stationary_recovers_plateau():
-    # constant plateau + noise -> mean ~ plateau
-    rng = np.random.default_rng(0); rows = []
-    for g, plateau in [(0.02, 0.843), (0.05, 0.908)]:
-        for s in range(20):
-            for t in range(300):
-                v = (0.95 if t < 50 else plateau) + rng.normal(0, 0.003)
-                rows.append({"g": g, "seed": s, "generation": t, "heterozygosity": v})
-    red = reduce_to_stationary(pd.DataFrame(rows), last_frac=0.33)
-    means = red.groupby("g")["heterozygosity"].mean()
-    assert means[0.02] == pytest.approx(0.843, abs=0.01)
-    assert means[0.05] == pytest.approx(0.908, abs=0.01)
-
-
-def test_tail_band_deep_below_shallow():
-    # grounded dynamics on a Zipf tail: deepest band <= shallowest band, replicate-avg
-    K = 1000; s = 1.1
-    w = 1.0 / np.arange(1, K + 1) ** s; p_star = w / w.sum()
-    tail_mask = p_star < 1e-3
-    n, m = 200, 10
-    FA = np.zeros(4)
-    for r in range(20):
-        rng = np.random.default_rng(1000 + r); p = p_star.copy()
-        for _ in range(600):
-            c = rng.multinomial(n, p) + rng.multinomial(m, p_star); p = c / c.sum()
-        fa, _ = tail_band_metrics(p, p_star, tail_mask, n_bands=4)
-        FA += fa
-    FA /= 20
-    assert FA[0] <= FA[-1]                              # deepest no better than shallowest
-    assert FA[-1] > FA[0]                               # and strictly worse on average
-```
-
----
-
-## Verified numbers (expected outputs — confirm your wiring reproduces these)
-
-On the synthetic E2 set (n=200, H*=0.95, exact H_eq + N(0,0.004) noise, 100 reps):
-
-| frac | g* (point) | 95% CI (tight, low-noise synthetic) |
-|------|-----------|--------------------------------------|
-| 0.90 | ~0.026    | ~[0.025, 0.026] |
-| 0.95 | ~0.047    | ~[0.047, 0.048] |
-| 0.99 | ~0.25     | ~[0.22, 0.27]   |
-
-(Real E2 replicate spread will widen these CIs — that is expected and correct.)
-
-`reduce_to_stationary` on a plateau frame recovers 0.843 (g=0.02) and 0.908 (g=0.05).
-
-`tail_band_metrics`, grounded Zipf tail (K=1000, tail=p*<1e-3, 889 tail items), 40 reps,
-`frac_alive` per band [0=deepest .. 3=shallowest]:
-
-| g    | band0 | band1 | band2 | band3 |
-|------|-------|-------|-------|-------|
-| 0.0  | 0.000 | 0.000 | 0.000 | 0.000 |
-| 0.01 | 0.002 | 0.002 | 0.004 | 0.010 |
-| 0.05 | 0.004 | 0.009 | 0.012 | 0.033 |
-| 0.20 | 0.015 | 0.022 | 0.032 | 0.075 |
-
-Monotone (deeper = worse) at every g>0; all bands rise with g; deep band lags throughout.
diff --git a/tasks/workorder-llm-society.md b/tasks/workorder-llm-society.md
deleted file mode 100644
index b096913..0000000
--- a/tasks/workorder-llm-society.md
+++ /dev/null
@@ -1,97 +0,0 @@
-# Work order — `llm_society`: the composed society at LLM scale (C3, the paper's stated gap)
-
-**Date opened:** 2026-09-07. **Decision (GG):** run it before PNAS submission — "if it's an
-experiment that can be done in weeks rather than months then any reviewer would ask to see it."
-
-## Question
-
-Does a population of LLM agents under the four composed operators — grounded evaluation, directed
-recombination, diversity-preserving selection, retraining (mutation) — climb and hold capability,
-while each ablation fails distinctly? The LLM instantiation of E11; the paper's largest stated gap
-(Fig. 1's "open" cell).
-
-## Design (mirrors E11; one grounding knob, in the evaluation channel)
-
-Population of `N` agents = LoRA adapters on frozen Qwen2.5-0.5B-Instruct, initialised as light
-per-family specialists (round-robin over families, agent-specific seeds → initial diversity).
-Non-overlapping generations (Wright–Fisher style; no elitism). Per generation:
-
-1. **Produce & score.** Every agent answers (a) a fixed *validation* pool (verifier-scored → true
-   fitness; the selection signal for grounded arms), (b) a fresh per-generation *conformity* pool
-   (prompts only). Consensus = modal normalised answer per prompt; agent conformity = agreement with
-   the consensus. A fixed *test* pool (never selected on) gives the reported fitness.
-2. **Select** `P = N/2` parents by `s = g·fitness + (1−g)·conformity`.
-   Diversity ON: greedy quality-diversity (next parent maximises `s + λ·behavioural distance` to
-   those already chosen; distance = disagreement rate on the conformity pool — verifier-free, so
-   usable in every arm). Diversity OFF: plain top-P by `s`.
-3. **Breed** (sex ON): parents paired by descending behavioural distance (complementary mates);
-   per pair, `n_cand` Dirichlet-weighted merges are screened on the arm's own selection signal
-   (verifier val-pool for grounded arms; conformity for `no_grounding` — the verifier never enters
-   that arm's loop) and the fittest offspring is kept (directed sex, E10). Sex OFF: children are
-   redistilled copies of the selected parents.
-4. **Reproduce** (the inheritance channel, identical in all arms): each child is a *fresh* LoRA
-   trained from the base on (prompt → its source model's own answer) pairs over a fresh training
-   pool — self-consumption made literal; knowledge survives only through the data channel. SFT
-   stochasticity + fresh pools are the mutation operator.
-
-**Arms:** `full` (g>0, sex, diversity) · `no_grounding` (g=0) · `no_sex` · `no_diversity`.
-**Verifier truth is used for reporting in all arms** (test-pool fitness, consensus accuracy) but
-enters the *loop* only where g>0.
-
-## Metrics (per arm × seed × generation)
-
-Per agent: test fitness (overall + per family + worst family), conformity, selection score,
-selected flag, parentage. Population: best/mean true fitness, behavioural diversity (mean pairwise
-disagreement), consensus accuracy, conformity−truth gap. E11's three panels re-drawn at LLM tier.
-
-## Falsifiers (pre-registered)
-
-1. `full` does not exceed `no_grounding` in final best true fitness → grounded evaluation adds
-   nothing at LLM tier; the composed-society claim fails its LLM test.
-2. Ablations do not fail distinctly (no conformity−truth gap in `no_grounding`; `no_sex` matches
-   `full` on assembled capability; `no_diversity` matches on diversity trajectory) → the
-   complementary-contributions claim does not transfer.
-3. Honest alternative outcome: consensus may stay noisy rather than harden into a confident-wrong
-   mean (specialists are wrong *differently* off-family) → `no_grounding` fails by drift, not by
-   conformity; report the observed signature either way.
-
-## Cost & schedule
-
-| Stage | Scale | Compute | Where |
-|---|---|---|---|
-| Smoke | N=4, G=2, tiny pools, `full` only | ~15 min | local 16 GB |
-| Pilot | N=6–8, G=8–10, `full` + `no_grounding`, 1 seed | ~4–10 GPU·h | local overnight / 1 CX3 job |
-| Campaign | 4 arms × 3 seeds, N=8–12, G=10–12 | ~50–150 L40S·h, ~1 day wall-clock as 12 parallel jobs | CX3 (`/imperial-hpc`); rent only if queue fails |
-| 7B confirm (optional) | headline contrast only (`full` vs `no_grounding`, 1–2 seeds) | ~100–300 GPU·h | CX3 / rented |
-
-## Checklist
-
-- [x] Design + falsifiers (this document)
-- [x] `src/llm/society.py` (pure operators + GPU loop), `kind: llm_society`
-- [x] Pure-function tests (consensus, conformity, QD selection, pairing) — 155 green
-- [x] `configs/llm/society_smoke.yaml` → smoke green (exit 0; conformity 0.54→0.75, diversity 0.69→0.41 in one self-consumption generation — the signature is live)
-- [x] Pilot v1 (n_inherit=240, ep2, no elitism): **arms indistinguishable** — both drop ~0.1 in the
-  first distillation generation and plateau at the same mutation–selection floor (best ≈0.40, mean
-  ≈0.35, consensus 0.63→0.4 in both). Diagnosis: inheritance too lossy; the ~25%/generation
-  distillation tax swamps the selection differential. v1 archived at `results/llm_society_pilot1`.
-- [x] Pilot v2 (n_inherit=600, ep3, founders 600×3, elitism=1 all arms, n_parents=3, n_conf=90):
-  **the contrast is alive.** `full` climbs 0.583 → 0.617 best true fitness and holds (a bred
-  offspring beat every founder at gen 3); `no_grounding` decays monotonically 0.583 → 0.533
-  despite identical elitism — conformist selection keeps the wrong elite. Self-consumption
-  signature present: in `no_grounding` the population agrees with itself more (conformity 0.60 →
-  0.73) while the consensus gets less right (0.63 → 0.49); the confident-but-wrong gap reaches
-  +0.24 vs +0.16 in `full`. One seed, N=6: direction only. `results/llm_society`.
-- [ ] GG gate: review pilot curves before the campaign
-- [ ] `hpc/llm_society.pbs` array job (arm × seed) → campaign
-- [ ] Figure `paper/manuscript/make_figs.py` panel(s); fold into main.md (replaces the "open" cell)
-- [ ] 7B headline confirm (optional, post-campaign decision)
-
-## Open design decisions (defaults chosen; GG may override)
-
-- Families: the three *easy* variants at 0.5B (headroom exists; hard variants risk gen-0 inheritance
-  data being mostly wrong → uninformative universal collapse). Revisit after pilot.
-- g for grounded arms: 0.5 (equal weight); sweepable later.
-- Population constant, non-overlapping generations, no elitism (faithful WF; champions must survive
-  through inheritance, not by fiat).
-- Adapter disk hygiene: delete generation t−1 adapters once generation t is trained (keep gen 0 and
-  final); ~35 MB × N × G × arms otherwise.
diff --git a/tasks/workorder-pnas-submission.md b/tasks/workorder-pnas-submission.md
deleted file mode 100644
index f50d7b9..0000000
--- a/tasks/workorder-pnas-submission.md
+++ /dev/null
@@ -1,248 +0,0 @@
-# Work order — PNAS submission: "The Evolution of Sex for Artificial Intelligence"
-
-*Created 2026-08-11. Target: PNAS Direct Submission research article (~6 pp main + SI Appendix),
-preceded by an arXiv preprint (q-bio.PE × cs.LG). Decision basis: 2026-08-11 literature + venue scan
-(see memory/competitive-landscape.md, August addendum). GG approved PNAS + the analysis.*
-
-**Governing constraints.** (1) Dual audience: every claim stated so a computer scientist and a
-biologist can each follow it — keep the "two statements of the same fact" device and the
-dictionary table; define every term at first use. (2) Honesty ledger: concede Riis +
-First-Extinction + quantitative-trait collapse up front; claims are the *cure* and its theory, not
-the diagnosis. (3) Reproducibility is the differentiator: every figure from committed artifacts,
-code+data DOI at submission.
-
----
-
-## Phase 1 — Referee-proof the headline (E13 hardening) — Week 1
-
-The speciation result (E12+E13) is both our most novel and most exposed claim
-(arXiv:2606.23607, June 2026: a symmetry group larger than permutations removes most transformer
-barriers). Patch before the preprint goes up.
-
-- [x] **E13c — scale-aware alignment (the symmetry defense).** *(Done 2026-08-11: canonicalise_scale
-  + gated tests; decomposition & cliff regenerated — conflict residual 0.502→0.497 under the full
-  group, independent 0.001; floor proposition drafted in paper/si-notes.md S1. Hybrid-fitness
-  readout added: cliff accuracy 0.97→0.03.)*
-  - Extend `neural/rebasin.py` with per-unit **positive rescaling** canonicalisation: for the
-    no-BatchNorm ReLU MLP, the full function-preserving unit symmetry group is permutation ∘
-    positive diagonal scaling. Canonicalise both nets first (rescale each hidden unit to
-    ‖in-weights‖=1, pushing the norm into the out-weights), *then* permutation-match. Sanity gate:
-    a randomly permuted **and rescaled** copy must realign to functional identity (extend
-    `tests/test_rebasin.py`).
-  - Re-run `speciation_real` + `speciation_real_cliff` reporting `residual_perm` vs
-    `residual_perm_scale`. Expected: independent-init residual stays ≈0; conflict residual
-    unchanged. Update figure/README.
-  - Write the **theoretical floor proposition** (SI): two models with low loss on *contradictory*
-    label assignments over the same inputs cannot both be matched by any function-preserving
-    transformation — any single merged function errs on at least the disagreement mass, so the
-    conflict-condition barrier has an information-theoretic floor independent of the symmetry
-    group. Cite 2606.23607 and position: their result strengthens ours (the *removable* part may
-    grow with richer groups; the *functional* residual cannot vanish). Also cite the two June-2026
-    LMC papers found in the direct arXiv sweep (2026-08-11): "Beyond Structural Symmetries: Linear
-    Mode Connectivity via Neuron Identifiability" (2606.03…) and "Functional Equivalence in
-    Attention … Applications to Linear Mode Connectivity" (2606.16…) — same objection family, same
-    defense. Direct arXiv API sweep also re-confirmed: speciation/DMI/Fisher–Muller/ratchet/
-    evolution-of-sex/mating-for-models have zero ML hits on arXiv itself as of the newest listings.
-- [x] **E13b — emergent speciation (the decisive missing experiment).** *(Done 2026-08-11: the
-  pre-registered SECOND reading — residual 0.000 at every t_div ≤ 3200 in both `disjoint` and
-  `augment`; the merge RESCUES the forgetting specialists (parents 0.535/0.474 → merged 0.955,
-  sustained Fisher–Muller). Speciation requires functional conflict in this regime; LLM-scale
-  over-specialisation deferred to Phase-3 llm_speciation. Figure panel C + READMEs updated.)* Current E13 *imposes*
-  contradiction; a true Dobzhansky–Muller incompatibility is *emergent* (each change harmless
-  alone, bad only in combination). Design (`speciation_real` new conditions, CPU/torch, cheap):
-  - `disjoint`: fork from the shared MNIST base; child A continues on classes 0–4 only, child B on
-    5–9 only (no contradiction anywhere). Sweep divergence time `t_div`. Measure naive barrier and
-    scale+perm-aligned residual on the full task, **and merged-model accuracy vs best parent**.
-  - `conventions` (optional, purer DMI): same task, different augmentation conventions (A:
-    rotations, B: inversions) — representational drift with zero output conflict.
-  - **The money curve:** merged accuracy vs `t_div` should trace E12's *compatible →
-    outbreeding-depression → inviability* trajectory emergently: at low divergence the merge
-    *rescues* the two forgetting specialists (Fisher–Muller), at high divergence it fails
-    (speciation). If it comes out, this is a headline panel. Either outcome is publishable
-    (no growth ⇒ "models are safer to merge than biology predicts" — an honest bound).
-  - Tests + README + fold into figure. Pre-register the falsifier language before running.
-
-## Phase 2 — arXiv preprint package (GG decision 2026-08-11: DO NOT POST until all experiments
-and analysis are complete — the preprint goes up after Phase 3, with the final analysis folded in;
-re-run md2tex + tectonic at that point)
-
-- [x] Citation refresh in `paper/the-evolution-of-sex-for-ai.md` *(done 2026-08-11; author names verified against arXiv API)*: **new concessions** —
-  First-Extinction Law (2509.20101), quantitative-trait collapse (2407.17493), verifier-injection
-  (2510.16657); **precursor** — Livnat & Papadimitriou, *Sex as an algorithm* (CACM 2016);
-  **adjacent-to-differentiate** — merge-collapse theory (2603.09463), mergeability prediction
-  (2601.22285), non-local merging (2410.12766), model kinship (2410.12613), expert-duration
-  (2607.11997), symmetry-scaling (2606.23607), Harris (2604.05142), in-context diversity collapse
-  (2604.18005, 2603.24676).
-- [x] Fold E13b/c results into the speciation section (whatever they show — honestly). *(Done: full-symmetry residual + hybrid-fitness cliff + the emergent converse, in abstract, §5, §13 and the accessible version.)*
-- [x] LaTeX conversion: `paper/arxiv/` (md2tex.py block-based converter from the Markdown source of
-  truth; main.tex; 3 figures; builds clean under tectonic, 20 pp; arXiv pdflatex hint guarded).
-- [ ] **Post to arXiv — DEFERRED until all experiments/analysis are done (GG).** Package is ready
-  (`paper/arxiv/`, instructions in `ARXIV-SUBMISSION.md`); rebuild after the Phase-3 results are
-  folded into the manuscript, then upload.
-
-## Phase 3 — New experiments for impact & robustness — Weeks 2–3
-
-- [x] **`llm_speciation` — the cliff at the LLM tier.** *(Run 2026-08-11, 0.5B: DURATION NULL —
-  over-trained disjoint specialists merge better not worse (0.84->0.94, above best parent throughout);
-  the MLP "no emergent isolation" null generalises. CONFLICT — function-specific hybrid breakdown:
-  merged coherence 0.02-0.08 falls below BOTH parents (~0.2) on the conflicted function. Caught a
-  design confound (replace mode ties conflict_frac to private-data budget) -> built the de-confounded
-  `add` variant (conflict_mode: add; configs/llm/speciation_add.yaml). 7B confirm optional later.)*
-  Speciation demonstrated at all three tiers (analytic → MLP → LLM) makes the headline
-  unassailable. Structural bonus to state in the paper: LoRA deltas live in the frozen base's
-  coordinate system, so there is **no permutation ambiguity by construction** — any LoRA-merge
-  failure is *functional* incompatibility, the residual isolated architecturally.
-  - Conflict knob: two LoRA children from the same base learn contradictory conventions on a
-    shared family (e.g. "sort ascending" vs "sort descending"; answer-format conflicts) on a
-    fraction `conflict_frac` of prompts, plus their own disjoint families. Sweep `conflict_frac`,
-    soup-merge, evaluate on neutral held-out + both convention sets. Predict a monotone cliff.
-  - Emergent knob: take existing disjoint specialists, sweep **training duration**
-    (over-specialisation) → merged performance vs steps. Frames the Amazon observation
-    (2607.11997: optimal expert duration) as *our theory explaining their data* — a strong PNAS
-    move.
-  - 0.5B locally first; one 7B CX3 confirm if the sign is clean (`hpc/` PBS, minutes).
-- [x] **Multi-seed the LLM arc (0.5B tier done 2026-08-11).** All three claims hold with CIs
-  (merge>every specialist; union 0.274±0.026 > fusion 0.174±0.102 hard; directed 0.221±0.026 > soup)
-  + NEW finding: fusion is seed-FRAGILE on hard tasks (±0.10) while routing/directed are stable
-  (±0.026) — the union/selection operators are the reliable ones. results/llm_*_seeds/ + llm_seeds
-  figure. Remaining: 7B CX3 seeds (1-3) when HPC convenient.
-  - Thread the seed into specialist cache keys (`spec_[_hard]_s`); verify nothing
-    else assumes the old names.
-  - 0.5B: seeds 1–5 × {merge, moe, directed} × {easy, hard}. 7B on CX3: seeds 1–3 × hard
-    {merge, moe, directed} (8–25 min walltimes → trivial). Aggregate figures with 95% CI; update
-    READMEs; the headroom law now carries error bars.
-- [x] **`epistasis_predicts` — the DECISIVE experiment — DONE (2026-08-11, 0.5B, 39 pairs, 3 seeds).**
-  *Verdict: functional conflict measured pre-merge PREDICTS merge failure (dis_raw rho=+0.46,
-  epi_conf +0.45, both p<0.005); weight geometry does NOT (delta_cos +0.03, delta_l2 +0.17 n.s.);
-  gradient alignment weakly informative (-0.35). The first grid's apparent geometry win (+0.60) was an
-  overlap artifact, exposed and killed by the added `compat` control axis (same overlap+volume, no
-  conflict, zero penalty). Honest rider: confidence weighting did NOT beat raw disagreement as a rank
-  predictor (internal prediction not confirmed; it does give a 2x vs 1.5x conflict/compat contrast in
-  levels). |rho|~0.45 bounded by 0.5B merge noise — 7B replication is the firm-up.
-  results/llm_epistasis{,_compat}/ + figure.* The review's exact bar: population-genetic quantities must
-  *predict* (not re-describe) — forecast merge success **before merging**, and beat existing
-  predictors. Design, reusing the llm_speciation machinery:
-  1. Parents with independently controlled interaction structure: sweep `conflict_frac` (ground-truth
-     epistasis) *and* compatible/disjoint + duration variants (spread in divergence WITHOUT conflict),
-     so functional conflict and divergence are decorrelated by construction.
-  2. Pre-merge predictors, none of which touches a merged model: (a) **operational epistasis** =
-     functional-disagreement mass between the parents on a shared probe set (the μ(S) estimate — ours);
-     (b) **gradient alignment** (the Zhou et al. 2601.22285 predictor); (c) **weight-space geometry**
-     (LoRA-delta cosine / norm distance).
-  3. Outcome: merged (soup) performance on private families + convention coherence, held-out test,
-     multi-seed.
-  4. The claim to test: at matched geometric divergence, the epistasis measure predicts merge outcome
-     and the geometry measures do not (R² comparison + an operator-choice decision test — merge vs
-     route — under matched budgets).
-  Pre-registered falsifier: if gradient/geometry predictors match the epistasis measure, the paper's
-  "epistasis, not divergence, sets the cliff" claim stays analytic-only and is labelled as such.
-- [x] **Manuscript claim-narrowing (external review, 2026-08-11) — done.** Softened identity claims
-  (WF exact only in the minimal model + learning-kernel cited against ourselves; ratchet scoped to the
-  irreversible arm), removed "nobody has / none imports / theory outrun" (priority-dispute bait),
-  added the interpretation/explanation/prediction ladder to §1, stated the merge-don't-average
-  operator boundaries (output-mean vs weight-avg vs routing vs max-with-oracle, budgets, oracle,
-  capacity), added a "what these experiments do and do not establish" scope block to the speciation
-  section (impossibility floor is information-theoretic, not genetic; snowball/epistasis-cliff =
-  hypotheses at the neural tier), replaced "control theory" with "framework" (subtitle included —
-  GG can veto), fixed the §3/§11 overstatements (frozen core ≠ frozen behaviour; Baldwin = echo not
-  identity; archive vs operational irreversibility), added the **claims-at-a-glance table**
-  (status/assumptions/evidence/limits) to §13 + table support in md2tex, and matched the calibration
-  in the accessible version. Adopted the review's framing sentence as the stated core contribution.
-- [ ] **(Optional, SI-grade) ambiguous-families router stress test** — overlapping-skill families
-  where the router is no longer trivially perfect; documents the honest limit of union-by-routing.
-  Do only if time permits; otherwise keep the existing rider sentence.
-
-## Phase 4 — The PNAS manuscript — Weeks 3–4
-
-- [x] **Restructure** → PNAS research article draft (`paper/manuscript/main.md` + build.py + PDF, 2026-08-11:
-  significance/abstract/intro/Table-1 dictionary/results ladder incl. the predictive test at
-  second-review calibration/discussion with design rules + ledger + limits/methods; ~5.6k words main).
-  *(Remaining polish at submission: pnas.cls reflow, numeric refs, bespoke unified figures.)*
-  Original plan: Significance (~120 w, dual-audience), Abstract (~250 w), Intro (concede the
-  diagnosis; thesis: the cure is sex; where this sits), **Results ladder**:
-  1. Collapse is drift, grounding is immigration (E1–E3: exact `H_eq`, `g*≈0.048`, tail threshold
-     `m·p*≳1`) — concede Riis/First-Extinction, keep the immigration delta.
-  2. Merge, don't average — the conservation law (E4 + neural recombination).
-  3. Sex: Fisher–Muller offspring exceed every parent; outbreeding depression on rugged
-     landscapes; directed sex as the AI advantage (E8–E10).
-  4. **Model speciation** (E12 + hardened E13 + `llm_speciation`) — the headline.
-  5. The jointly-necessary society (E11) + mating structure (E14, one panel).
-  6. Real-weight confirmation tier: neural/MNIST (brief) + multi-seed LLM headroom law.
-  Discussion: the borrowed/ours ledger, the design-rules table (average/route/select/don't-merge ×
-  landscape), limits (what LLM-scale still lacks: the full grounded society), what biology gets
-  back (in-silico tests of sex theory in regimes biology can't reach). Methods: brief + SI.
-- [ ] **Manifesto sections → Discussion or drop** (institutions, four timescales, re-minting
-  philosophy, fitness-is-not-truth): compress each to ≤1 paragraph or move to SI "extended
-  discussion". The accessible companion doc stays as-is for outreach, not submission.
-- [ ] **Dual-audience devices**: Table 1 = the population-genetics ↔ machine-learning dictionary;
-  every term defined in one clause at first use; keep "the ML statement / the genetics statement"
-  paired-paragraph device, compressed.
-- [x] **Figures: publication-ready** (paper/manuscript/make_figs.py re-plots all 6 figures from committed
-  artifacts as unified, lettered, codename-free panels — no suptitles, plain-language labels;
-  fig1 A-B grounding+MNIST montage (title band cropped), fig2 A-B blending/Fisher-Muller,
-  fig3 A-D rugged+mating, fig4 A-C society, fig5 A-F speciation x3 tiers, fig6 A-D LLM tier;
-  captions rewritten per panel; in-text refs updated; doc 20pp -> 18pp). Original plan:
-  (1 concept/dictionary+operator schematic; 2 collapse&grounding incl. the MNIST digit-decay
-  montage — the one image both audiences get instantly; 3 sex: Fisher–Muller/outbreeding/directed +
-  merge-don't-average; 4 speciation across three tiers; 5 society ablation + headroom law).
-  Everything else → SI figures. All regenerated from committed parquet.
-- [x] **SI Appendix skeleton** (`paper/manuscript/si.md`: propositions, claims ledger, per-tier methods,
-  statistics, figure list). Original plan: results-summary.md as the skeleton; full methods, all closed forms +
-  tolerances, per-experiment configs/seeds, the E13 floor proposition, confusion matrices,
-  reproduce.sh instructions.
-- [ ] Word budget: main text ≤ ~6,000 words; check PNAS current LaTeX template + submission
-  checklist at writing time.
-
-## Phase 4b — Narrative revision (GG directive 2026-08-11) — DONE
-- [x] Premise substantiated: Introduction opens with the evidence-backed model-population reality
-  (3M models/phylogenetic lineage mapping; >98% synthetic alignment pipelines; web AI-content share;
-  the human-data ceiling; merging tooling; agent economies) — refs 31–44.
-- [x] Continual-learning contextualisation: new Introduction block mapping the CL canon onto the
-  operators (replay↔grounding incl. the 1%/5%/25% ↔ g*≈0.05 convergence; pseudo-rehearsal = our
-  ungrounded null; parameter isolation; CLS consolidation; merging-for-CL; tail-first forgetting;
-  CF≠collapse distinction kept explicit) + Discussion block "What this offers continual learning"
-  (5 impact points incl. the Kotha latent-vs-extinct engagement) — refs 45–65. Verified open: no
-  prior work carries pop-gen formalism into CL (the bridge is ours).
-- [x] Downplaying removed: convergence framing (diagnosis reached independently; Riis/Benati/Yoon +
-  Crutchfield&Whalen 2012 cited for priority of publication; convergence = corroboration; the full
-  arc owned). Applied to PNAS draft + v6 abstract.
-- [x] References: DONE fully (2026-08-11) — 66 entries renumbered to first-appearance order
-  (verified programmatically: in-text order = 1..66 = list order), reformatted to PNAS style
-  (initials-first, sentence case, abbreviated italic journals, bold volumes, year-at-end,
-  arXiv [Preprint] + DOI), and verified: all 47 arXiv ids batch-checked against the API
-  (caught + fixed: authorless GENOME -> Y. Zhang et al.; "Sakana AI" -> J. Abrantes et al.;
-  wrong Kotha id 2310.05719 -> 2309.10105; Nemotron corporate author; Liang full title;
-  >5-author entries to et-al form). Six orphaned refs re-anchored in text (NK, QD, Pari, LoRA,
-  Sharma, Kozodoi) and one mis-citation fixed (Self-Instruct, new ref, was credited to
-  Multiagent-Finetuning). Figure captions in build.py brought to third-review calibration
-  (operational threshold; first-order conservation; complementary-contributions society;
-  permutation-and-rescaling alignment).
-- [ ] v6 long-form: sync the premise/CL sections if GG wants the long document to match (currently
-  only the convergence fix is propagated).
-
-## Phase 5 — Submission mechanics — Week 5
-
-- [ ] **Code & data availability**: public GitHub snapshot + Zenodo DOI (code + committed results
-  artifacts); PNAS data-availability statement; verify `reproduce.sh` end-to-end on a clean clone.
-- [ ] **Cover letter**: the fit argument (Evolvable-AI 2026 precedent; geneticist's-lens
-  contribution; what biology gains); position vs Riis/Shumailov explicitly.
-- [ ] **Editor & reviewer suggestions**: identify the editor who handled "Evolvable AI" (PNAS
-  2527700123; N.C. Stenseth per scan — verify); suggest 3–5 reviewers mixing (i) an
-  evolution-of-sex theorist, (ii) a model-merging ML researcher, (iii) a model-collapse author.
-- [ ] **Policy checks at submission time** (verify, don't assume): PNAS AI-assistance disclosure
-  wording; preprint policy; OA/page charges + whether Imperial has a read-publish agreement with
-  PNAS; ORCID; competing interests.
-- [ ] Sync arXiv v2 with the submitted text.
-
-## Decision items for GG (not blocking Phases 1–3)
-
-- [ ] Title: keep *"The Evolution of Sex for Artificial Intelligence"* vs a more declarative PNAS
-  title (e.g. *"A population genetics of model merging: why AI societies should reproduce
-  sexually"*). Decide at Phase 4.
-- [ ] Authorship & AI-assistance acknowledgement wording (per PNAS policy).
-- [ ] Repo public at submission vs at acceptance (Zenodo DOI needed at submission either way).
-- [ ] APC/OA budget approval.
-
-## Discovered During Work
-
-*(append here)*
diff --git a/tests/test_genotype.py b/tests/test_genotype.py
index ae4d468..a0390d9 100644
--- a/tests/test_genotype.py
+++ b/tests/test_genotype.py
@@ -14,7 +14,6 @@ from inheritance.genotype import (
     additive_fitness, bits_to_index, crossover, genotype_bits, hill_climb, linkage_equilibrium,
     locus_marginals, mutate, nk_fitness, recombine, recombine_teachers,
 )
-from inheritance.genotype_lineage import run_genotype_lineage
 from inheritance.society import make_specialist, run_directed_sex, run_recomb_landscape, run_society
 from inheritance.teachers import make_retention_matrix
 
@@ -78,17 +77,6 @@ def test_e8_sexual_exceeds_best_parent_and_soup():
     assert m["sexual"] >= m["average"]                    # and is at least as good as the soup
 
 
-def test_e7_sexual_adapts_at_least_as_fast():
-    base = {"genotype": {"L": 10, "n": 150, "mu": 0.02, "base": 1.3, "init": "wrong"},
-            "generations": 25}
-    asex = run_genotype_lineage({**base, "genotype": {**base["genotype"], "recomb_rate": 0.0}}, 0)
-    sex = run_genotype_lineage({**base, "genotype": {**base["genotype"], "recomb_rate": 1.0}}, 0)
-    mid = 12
-    a = asex[asex["generation"] == mid]["mean_fitness"].iloc[0]
-    s = sex[sex["generation"] == mid]["mean_fitness"].iloc[0]
-    assert s >= a - 1e-9                                   # sexual adapts at least as fast mid-run
-    assert sex["ld"].max() < asex["ld"].max()             # ... by keeping loci in linkage equilibrium
-
 
 def test_nk_fitness_shape_range_and_additive_limit():
     f0 = nk_fitness(6, 0, seed=1)
diff --git a/tests/test_llm.py b/tests/test_llm.py
index 3ffccc8..be380d9 100644
--- a/tests/test_llm.py
+++ b/tests/test_llm.py
@@ -138,62 +138,3 @@ def test_lora_delta_inner_matches_brute_force():
     A2, B2 = torch.randn(4, 20, generator=g), torch.randn(12, 4, generator=g)
     brute = float(((B1 @ A1) * (B2 @ A2)).sum())
     assert abs(lora_delta_inner(A1, B1, A2, B2) - brute) < 1e-3
-
-
-# ---------------------------------------------------------------------- society (pure operators)
-
-
-def test_society_consensus_is_modal_and_deterministic():
-    from llm.society import consensus_answers
-
-    outs = [["5", "cat", "[1, 2]"],
-            ["5", "dog", "[1, 2]"],
-            ["7", "dog", "[2, 1]"]]
-    cons = consensus_answers(outs)
-    assert cons[0] == "5" and cons[1] == "dog" and cons[2] == "[1, 2]"
-    # a full three-way tie breaks lexicographically (deterministic)
-    tie = consensus_answers([["a"], ["b"], ["c"]])
-    assert tie == ["a"]
-
-
-def test_society_conformity_and_distance():
-    from llm.society import behavioural_distance, conformity_scores, consensus_answers
-
-    outs = [["5", "dog"], ["5", "dog"], ["7", "cat"]]
-    cons = consensus_answers(outs)
-    conf = conformity_scores(outs, cons)
-    assert conf[0] == conf[1] == 1.0 and conf[2] == 0.0     # majority conforms, dissenter does not
-    d = behavioural_distance(outs)
-    assert d[0, 1] == 0.0 and d[0, 2] == 1.0 and np.allclose(d, d.T)
-
-
-def test_society_selection_greedy_vs_quality_diversity():
-    from llm.society import select_parents
-
-    scores = np.array([1.0, 0.95, 0.94, 0.1])
-    # agents 0 and 1 are behavioural clones; agent 2 is distant from both
-    d = np.zeros((4, 4))
-    d[0, 2] = d[2, 0] = d[1, 2] = d[2, 1] = 1.0
-    d[0, 3] = d[3, 0] = d[1, 3] = d[3, 1] = d[2, 3] = d[3, 2] = 1.0
-    greedy = select_parents(scores, d, 2, diversity=False)
-    assert greedy == [0, 1]                                 # pure score: takes the clones
-    qd = select_parents(scores, d, 2, diversity=True, lam=0.3)
-    assert qd == [0, 2]                                     # QD: prefers the distant near-peer
-
-
-def test_society_pairs_and_arms():
-    import pytest
-
-    from llm.society import arm_settings, complementary_pairs
-
-    d = np.zeros((4, 4))
-    d[0, 1] = d[1, 0] = 0.9
-    d[0, 2] = d[2, 0] = 0.2
-    d[1, 2] = d[2, 1] = 0.5
-    pairs = complementary_pairs([0, 1, 2], d, 4)
-    assert pairs[0] == (0, 1) and pairs[1] == (1, 2)        # most-complementary pair breeds first
-    assert len(pairs) == 4 and pairs[3] == pairs[0]         # cycles to fill the slots
-    assert arm_settings("no_grounding", 0.5)["g"] == 0.0
-    assert arm_settings("no_sex", 0.5) == {"g": 0.5, "sex": False, "diversity": True}
-    with pytest.raises(ValueError):
-        arm_settings("bogus", 0.5)
diff --git a/tests/test_llm_compose.py b/tests/test_llm_compose.py
deleted file mode 100644
index 43b78ec..0000000
--- a/tests/test_llm_compose.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""Tests for the composition experiment's pure pieces and its execution verifier (prereg v3 §7)."""
-
-import numpy as np
-import pytest
-
-from llm.compose import predicted_composition, score_composed, score_gsm8k, union_exceedance
-from llm.compose_data import ProgTask
-from llm.execute import ExecResult, extract_code, numeric_match, run_solution, verify_program
-from llm.tasks import Task
-
-
-# ---------------------------------------------------------------- execution verifier
-
-
-def test_executes_and_matches_reference():
-    ok, res = verify_program("```python\ndef solution():\n    return 6*7\n```", 42.0)
-    assert ok and res.ok and res.value == 42.0 and res.status == "ok"
-
-
-def test_rejects_wrong_answer_but_still_ran():
-    ok, res = verify_program("def solution():\n    return 41", 42.0)
-    assert not ok and res.ok and res.value == 41.0
-
-
-@pytest.mark.parametrize("code,status", [
-    ("def solution():\n    while True: pass", "timeout"),
-    ("def solution():\n    return len([0]*10**9)", "memory"),
-    ("x = 1", "no_solution"),
-    ("def solution(:\n  return 1", "error:SyntaxError"),
-    ("def solution():\n    return solution()", "recursion"),
-    ("", "no_code"),
-])
-def test_hazards_are_contained(code, status):
-    assert run_solution(code, timeout_s=8.0).status == status
-
-
-def test_sandbox_blocks_writes_outside_jail_and_network():
-    assert run_solution(
-        "def solution():\n    open('/tmp/_llm_compose_escape','w').write('x'); return 1"
-    ).status == "error:PermissionError"
-    assert run_solution(
-        "import socket\ndef solution():\n    socket.socket().connect(('1.1.1.1',80)); return 1"
-    ).status == "error:PermissionError"
-    import os
-    assert not os.path.exists("/tmp/_llm_compose_escape")
-
-
-def test_sandbox_allows_writes_inside_its_own_jail():
-    # scratch files are fine — the jail is a fresh temp dir destroyed after the run
-    assert run_solution("def solution():\n    open('s.txt','w').write('x'); return 1").ok
-
-
-def test_extract_code_prefers_last_fence_and_drops_prose():
-    got = extract_code("Sure:\n```python\ndef solution():\n    return 1\n```\nHope that helps!")
-    assert got == "def solution():\n    return 1"
-    assert extract_code("def solution():\n    return 2") == "def solution():\n    return 2"
-
-
-def test_numeric_match_tolerances():
-    assert numeric_match(1e6, 1e6 + 1)            # relative tolerance
-    assert numeric_match(0.0, 0.0)
-    assert not numeric_match(None, 1.0)
-    assert not numeric_match(1.0, 2.0)
-
-
-def test_execution_is_deterministic():
-    code = "def solution():\n    return sum(range(1000))"
-    assert len({run_solution(code).value for _ in range(3)}) == 1
-
-
-# ---------------------------------------------------------------- scoring and the prediction
-
-
-def test_score_composed_counts_correct_and_executable_separately():
-    tasks = [ProgTask("p", 42.0, 0), ProgTask("p", 42.0, 1), ProgTask("p", 42.0, 2)]
-    comps = ["def solution():\n    return 42",      # correct
-             "def solution():\n    return 7",       # ran, wrong
-             "def solution(:\n"]                    # did not run
-    acc, ok, ran = score_composed(comps, tasks)
-    assert acc == pytest.approx(1 / 3) and ok.tolist() == [True, False, False]
-    assert ran == pytest.approx(2 / 3)
-
-
-def test_score_gsm8k_reads_after_the_hash_marker():
-    tasks = [Task("math", "p", "18"), Task("math", "p", "7")]
-    assert score_gsm8k(["reasoning blah\n#### 18", "the answer is 9"], tasks) == pytest.approx(0.5)
-
-
-def test_union_exceedance_is_solved_by_merge_and_no_parent():
-    merged = np.array([True, True, True, False])
-    pa = np.array([True, False, False, False])
-    pb = np.array([False, True, False, True])
-    assert union_exceedance(merged, [pa, pb]) == pytest.approx(0.25)   # only item 2
-    assert union_exceedance(merged, [merged]) == 0.0
-
-
-def test_predicted_composition_is_anchored_at_generation_zero():
-    q_m = np.array([0.9, 0.8, 0.7]); q_c = np.array([0.9, 0.85, 0.8])
-    rho = np.array([0.2, 0.4, 0.6])
-    pred = predicted_composition(q_m, q_c, rho, observed0=0.30)
-    assert pred[0] == pytest.approx(0.30)                     # one free scale, fixed at t=0
-    assert pred[1] > pred[2]                                  # decays with q and rho
-    # decays faster than either parent alone: product form plus the decorrelation term
-    assert pred[2] / pred[0] < min(q_m[2] / q_m[0], q_c[2] / q_c[0])
-
-
-def test_predicted_composition_handles_degenerate_start():
-    pred = predicted_composition(np.array([0.0, 0.0]), np.array([0.0, 0.0]),
-                                 np.array([0.0, 0.0]), observed0=0.0)
-    assert np.all(np.isfinite(pred))
diff --git a/tests/test_llm_society_v2.py b/tests/test_llm_society_v2.py
deleted file mode 100644
index 8bf5f4d..0000000
--- a/tests/test_llm_society_v2.py
+++ /dev/null
@@ -1,86 +0,0 @@
-"""Pure-operator tests for the v2 society (prereg §10): no GPU, no model."""
-
-import numpy as np
-import pytest
-
-from llm.families import ALL_CANDIDATES, EXTRA_FAMILIES
-from llm.society_ops import (arm_settings, choose_single_parent, families_alive, mating_plan,
-                             novelty, pooled_survival, route_union)
-from llm.tasks import make_tasks, verify
-
-
-def test_extra_families_are_verifier_safe_and_deterministic():
-    for fam in ALL_CANDIDATES:
-        ts = make_tasks(fam, 200, seed=3)
-        assert all(verify(t.answer, t) for t in ts), fam          # canonical answer verifies
-        assert [t.prompt for t in make_tasks(fam, 200, seed=3)] == [t.prompt for t in ts]
-        assert all(t.family == fam for t in ts)
-    assert len(set(EXTRA_FAMILIES)) == 17 and len(set(ALL_CANDIDATES)) == 20
-
-
-def test_pseudo_word_families_have_a_large_prompt_space():
-    # The 20-word vocabulary gave sortletters 40 unique prompts; training would cover the test set.
-    for fam in ("sortletters", "caesar", "charfreq"):
-        assert len({t.prompt for t in make_tasks(fam, 600, seed=1)}) > 500, fam
-
-
-def test_pooled_survival_is_e11_rule_and_greedy_at_lambda_zero():
-    scores = np.array([0.9, 0.5, 0.5, 0.1])
-    # agent 2 is behaviourally distant from everyone; agent 1 is a clone of agent 0
-    dist = np.array([[0, 0.0, 0.9, 0.9],
-                     [0.0, 0, 0.9, 0.9],
-                     [0.9, 0.9, 0, 0.9],
-                     [0.9, 0.9, 0.9, 0]], dtype=float)
-    assert pooled_survival(scores, dist, 2, lam=0.0) == [0, 1]       # greedy: top-2 by score
-    keep = pooled_survival(scores, dist, 2, lam=0.5)                  # QD: novelty lifts agent 2
-    assert keep[0] == 0 and 2 in keep and 1 not in keep
-    assert novelty(dist).argmax() == 2
-
-
-def test_mating_plan_caps_use_and_prefers_distant_pairs():
-    dist = np.array([[0, 0.9, 0.1, 0.2],
-                     [0.9, 0, 0.3, 0.8],
-                     [0.1, 0.3, 0, 0.7],
-                     [0.2, 0.8, 0.7, 0]], dtype=float)
-    plan = mating_plan(dist, 4, max_use=2)
-    assert plan[0] == (0, 1)                                          # most distant pair first
-    use = np.bincount(np.array(plan).ravel(), minlength=4)
-    assert use.max() <= 2 and len(plan) == 4
-    # every agent breeds at least once with N pairs and cap 2 — no allele is truncated at gen 1
-    assert use.min() >= 1
-
-
-def test_mating_plan_never_empty_when_cap_exhausts():
-    dist = np.array([[0, 0.5], [0.5, 0]], dtype=float)
-    plan = mating_plan(dist, 5, max_use=1)
-    assert len(plan) == 5 and all(p == (0, 1) for p in plan)
-
-
-def test_route_union_takes_the_more_confident_parent_and_is_deterministic_on_ties():
-    a, ca = ["1", "2", "3"], np.array([0.9, 0.2, 0.5])
-    b, cb = ["x", "y", "z"], np.array([0.1, 0.8, 0.5])
-    out, src = route_union(a, ca, b, cb)
-    assert out == ["1", "y", "3"] and src.tolist() == [0, 1, 0]
-
-
-def test_choose_single_parent_is_score_proportional():
-    rng = np.random.default_rng(0)
-    picks = [choose_single_parent(np.array([0.0, 0.0, 1.0]), rng) for _ in range(300)]
-    assert picks.count(2) > 250                                       # the fit parent dominates
-    assert set(picks) <= {0, 1, 2}
-
-
-def test_arm_settings_v2_table():
-    assert arm_settings("full", 0.85) == {"g": 0.85, "sex": "union", "diversity": True}
-    assert arm_settings("no_grounding", 0.85)["g"] == 0.0
-    assert arm_settings("no_sex", 0.85)["sex"] is None
-    assert arm_settings("no_diversity", 0.85)["diversity"] is False
-    assert arm_settings("sex_linear", 0.85)["sex"] == "linear"
-    with pytest.raises(ValueError):
-        arm_settings("elitism", 0.85)
-
-
-def test_families_alive_counts_competent_families_once():
-    accs = [{"a": 0.9, "b": 0.1}, {"a": 0.7, "b": 0.2}, {"a": 0.0, "b": 0.61}]
-    assert families_alive(accs, ["a", "b"]) == 2
-    assert families_alive(accs, ["a", "b"], threshold=0.8) == 1