feat(sona): metaharness-Darwin evolves EWC++ config beyond hand-tuned SOTA (#615)

* feat(sona): metaharness-Darwin evolves EWC++ config beyond hand-tuned SOTA

examples/darwin_ewc: applies the Meta-Harness 'freeze the model, evolve the
harness' pattern to SONA's continual-learning layer — frozen = the EWC++
algorithm (EwcPlusPlus), evolved = its EwcConfig genome (lambda schedule, Fisher
decay, auto task-boundary threshold, learning rate).

Benchmark: a single weight vector trained on a sequence of tasks (no replay,
auto-detected boundaries) — the canonical plasticity-vs-forgetting frontier.
Darwin (GA + coordinate-descent polish) evolves the genome on TRAIN task-
sequences; results reported on HELD-OUT sequences (different seeds).

Measured (deterministic), held-out: the evolved config beats EwcConfig::default()
(the crate's hand-tuned 'OPTIMIZED' values) by 35% lower final loss and 98.6%
less forgetting — a strict Pareto win (plasticity also improves), and it
generalizes to unseen task sequences. clippy -D warnings clean, fmt clean.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(sona): weightAdapter gene — Darwin selects/prunes a fine-tuned adapter

Extends the metaharness-Darwin line: expose a fine-tuned adapter (e.g. a LoRA
distilled from verified SWE-bench trajectories — the 'autonomous data engine')
as a gene (which_adapter, alpha) so evolutionary selection decides whether/how
much to apply it (w_eff = w_base + alpha·Δw) instead of assuming new weights are
better. examples/darwin_weightadapter demonstrates it on two conflicting domains
with a generalizing adapter and an overfit one.

Key finding (sharpens the idea): 'selection prunes overfit adapters' holds ONLY
under per-domain evaluation. Measured (held-out, in-dist-majority eval):
  overfit α=0.55 → ΔA +0.249 / ΔB -0.357 (regresses out-dist)
  AGGREGATE (volume-weighted) fitness  → picks the overfit adapter (silent B regression)
  PER-DOMAIN (no-regression Pareto)    → prunes it, keeps the generalizing adapter
So: evolve the adapter as a gene, but score it per-repository. clippy/fmt clean.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): ADR-271 metaharness-Darwin for SONA self-improvement

Documents the metaharness-Darwin-evolves-SONA architecture: EWC++ config
evolution (PR #615), the weightAdapter gene (per-domain Pareto selection of
fine-tuned adapters), the Autonomous Data Engine (execution-verified SWE-bench
trajectories -> DPO pairs), and four Ornith-1.0 borrows (immutable-boundary +
deterministic-monitor-with-exclude-from-advantage + frozen-LLM-judge-veto
reward-hacking defense; per-task-category specialization; two-stage scaffold
reward credit; staleness-weighted replay). Method-not-model: external
evolutionary vs Ornith's in-weights RL.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(sona): darwin-guard reward-hacking defense (Ornith-1.0 borrow, ADR-271)

3-layer defense for evolutionary config search: (1) immutable verifier boundary
(screen is a pure fn of verifier output the candidate can't fabricate);
(2) deterministic monitor — non-finite / out-of-bounds / degenerate candidates
are EXCLUDED from selection (best_accepted), not zero-scored, so a hack can
neither win nor bias the advantage; (3) IntentJudge trait = frozen-LLM veto-only
layer. Wired into darwin_ewc: NaN/collapsed configs are excluded from the GA
ranking (also fixes the partial_cmp().unwrap() NaN-panic). 4 unit tests; benchmark
still reaches beyond-SOTA (35% lower loss, 98.6% less forgetting) unchanged.
clippy -D warnings + fmt clean.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(sona): per-task-category genome router beats single global config (ADR-271)

Ornith-1.0 borrow #2 (per-category specialization): evolve a router task-class
-> genome instead of one global EwcConfig. Two continual-learning workload
classes with conflicting optima (STABLE wants high lambda / retain; VOLATILE
wants low lambda / stay plastic). Guard-screened evolution.

Measured (held-out, adequate per-class data): per-category router 0.1122 vs
single best global genome 0.1144 -> router ~1.9% better on unseen sequences,
because one config cannot serve conflicting workloads.

Honest caveat (discovered + documented): the gain REVERSES when per-class data
is scarce — a specialized config overfits while the pooled global generalizes.
Per-category routing needs enough per-category samples (Ornith's regime). ADR-271
updated; clippy/fmt clean.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(sona): online auto-tuner with staleness-weighted replay (ADR-271, Ornith borrow #4)

auto_tuner module: StalenessSchedule (Ornith w(d_t): fresh<=k1, exp-decay,
drop>k2) + StalenessWindow (staleness-weighted running estimate of recent
config performance, evicts stale obs). 4 unit tests.

examples/darwin_autotuner: a (1+1)-ES that adapts a DEPLOYED EwcConfig to a
drifting workload stream (regime A -> B at the midpoint), scoring the incumbent
on the staleness window and accepting a perturbation only when it beats the
recent score. Measured: online tuner ~3% lower post-drift loss than the static
deployment config (10 accepted re-tunes). Margin is modest on synthetic regimes;
the durable win is the reusable staleness machinery + the online-adaptation
principle (a fixed offline-tuned config goes stale under drift).

Completes the four ADR-271 components. clippy --all-targets -D warnings + fmt
clean; 102 sona tests pass.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(sona): contamination/disjointness guard in darwin-guard (weight-eft/ADR-198 borrow)

Adds the train/eval contamination guard — the gap @metaharness/weight-eft exposed
in our reward-hacking-only guard. contamination()/assert_train_eval_disjoint()
fail on any train∩eval instance-ID overlap (training/selecting on eval instances
is fake lift); filter_holdout() partitions a set disjoint-by-construction and
surfaces what was excluded. The SONA-side analog of weight-eft's
assertTrainEvalDisjoint. 2 new tests (6 total in darwin_guard).

ADR-271 updated: §3 Data Engine now cites @metaharness/weight-eft + adopts its
RLHF-correct recipe (SFT distills ALL gold incl. off-policy frontier successes;
DPO ON-POLICY cheap-vs-cheap only), and the darwin-guard borrow gains layer (iv)
the contamination disjointness guard. clippy -D warnings + fmt clean.

Co-Authored-By: claude-flow <ruv@ruv.net>

* chore(release): ruvector-sona 0.2.1 — darwin_guard + auto_tuner modules

Non-breaking minor feature release (new public modules darwin_guard,
auto_tuner). Patch bump keeps the ^0.2 requirement of all in-workspace
dependents (ruvllm, rvlite, mcp-brain, ...) satisfied.

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: ruvnet <ruvnet@gmail.com>
This commit is contained in:
rUv 2026-06-27 12:57:48 -04:00 committed by GitHub
parent a67f3564e2
commit b2a32eae2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1702 additions and 1 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "ruvector-sona"
version = "0.2.0"
version = "0.2.1"
edition = "2021"
rust-version = "1.70"
authors = ["RuVector Team <team@ruvector.dev>"]

View file

@ -0,0 +1,251 @@
//! Online auto-tuner for SONA's EWC config (ADR-271, Ornith-1.0 borrow #4).
//!
//! The point of *online* tuning is **non-stationarity**: a config tuned once,
//! offline, goes stale when the workload drifts. Here a `(1+1)`-ES re-tunes the
//! `EwcConfig` against a LIVE, drifting trajectory stream using the
//! staleness-weighted window `w(d_t)` from `ruvector_sona::auto_tuner`: it scores
//! the incumbent on *recent* observations, and accepts a perturbation only when
//! it beats that recent score — so it tracks a moving optimum instead of
//! averaging over a stale past.
//!
//! Drift scenario: the stream runs in regime A (small task shifts → wants high
//! lambda / retain) for the first half, then drifts to regime B (large shifts →
//! wants low lambda / stay plastic). We compare cumulative POST-DRIFT loss of:
//! * the best STATIC config (tuned offline for the deployment regime A), vs
//! * the ONLINE auto-tuner (adapts as the regime drifts).
//! Beyond-static result: the online tuner's post-drift loss is lower — adaptation
//! beats any fixed config under drift.
//!
//! Run: `cargo run -p ruvector-sona --release --example darwin_autotuner`
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use ruvector_sona::auto_tuner::{StalenessSchedule, StalenessWindow};
use ruvector_sona::{EwcConfig, EwcPlusPlus};
const PARAM_COUNT: usize = 96;
const EPOCHS: usize = 80;
const RETUNE_EVERY: usize = 2;
#[derive(Clone, Copy)]
struct Regime {
n_tasks: usize,
steps: usize,
shift: f32,
}
const REGIME_A: Regime = Regime {
n_tasks: 3,
steps: 90,
shift: 0.25,
}; // stable
const REGIME_B: Regime = Regime {
n_tasks: 8,
steps: 30,
shift: 1.4,
}; // volatile
fn regime_at(epoch: usize) -> Regime {
if epoch < EPOCHS / 2 {
REGIME_A
} else {
REGIME_B
}
}
#[derive(Clone)]
struct Genome {
lr: f32,
initial_lambda: f32,
fisher_ema_decay: f32,
boundary_threshold: f32,
}
impl Genome {
fn baseline() -> Self {
let d = EwcConfig::default();
Self {
lr: 0.08,
initial_lambda: d.initial_lambda,
fisher_ema_decay: d.fisher_ema_decay,
boundary_threshold: d.boundary_threshold,
}
}
fn to_config(&self) -> EwcConfig {
EwcConfig {
param_count: PARAM_COUNT,
max_tasks: 10,
initial_lambda: self.initial_lambda,
min_lambda: 50.0,
max_lambda: 30_000.0,
fisher_ema_decay: self.fisher_ema_decay,
boundary_threshold: self.boundary_threshold,
gradient_history_size: 60,
}
}
fn mutate(&self, rng: &mut StdRng) -> Self {
let mut c = self.clone();
if rng.gen::<f32>() < 0.6 {
c.lr = (c.lr * (0.6 + rng.gen::<f32>() * 0.8)).clamp(0.01, 0.5);
}
if rng.gen::<f32>() < 0.7 {
c.initial_lambda =
(c.initial_lambda * (0.3 + rng.gen::<f32>() * 1.6)).clamp(10.0, 20_000.0);
}
if rng.gen::<f32>() < 0.4 {
c.fisher_ema_decay =
(c.fisher_ema_decay + rng.gen::<f32>() * 0.04 - 0.02).clamp(0.9, 0.9999);
}
if rng.gen::<f32>() < 0.5 {
c.boundary_threshold =
(c.boundary_threshold + rng.gen::<f32>() * 2.0 - 1.0).clamp(0.5, 6.0);
}
c
}
}
/// One epoch = a continual-learning sequence under a regime; returns avg final loss.
fn run_epoch(regime: &Regime, g: &Genome, seed: u64) -> f32 {
let p = PARAM_COUNT;
let mut rng = StdRng::seed_from_u64(seed);
let targets: Vec<Vec<f32>> = (0..regime.n_tasks)
.map(|_| {
(0..p)
.map(|_| regime.shift * rng.gen_range(-1.0f32..1.0))
.collect()
})
.collect();
let mut ewc = EwcPlusPlus::new(g.to_config());
let mut w = vec![0.0f32; p];
for target in &targets {
for _ in 0..regime.steps {
let grad: Vec<f32> = w.iter().zip(target).map(|(wi, ti)| wi - ti).collect();
if ewc.detect_task_boundary(&grad) {
ewc.start_new_task();
}
let gc = ewc.apply_constraints(&grad);
for (wi, gi) in w.iter_mut().zip(gc.iter()) {
*wi -= g.lr * gi;
}
ewc.update_fisher(&grad);
ewc.set_optimal_weights(&w);
}
}
targets
.iter()
.map(|tg| {
0.5 * w
.iter()
.zip(tg)
.map(|(a, b)| (a - b) * (a - b))
.sum::<f32>()
/ p as f32
})
.sum::<f32>()
/ regime.n_tasks as f32
}
/// Offline-tune the best STATIC config for the DEPLOYMENT regime (A) — "tuned
/// once before deployment, then the world drifts to B." This is the config that
/// goes stale; the online tuner must adapt past it.
fn best_static() -> Genome {
let mut rng = StdRng::seed_from_u64(0x57A71C);
let seeds: Vec<u64> = (0..12).collect();
let score = |g: &Genome| {
seeds
.iter()
.map(|&s| run_epoch(&REGIME_A, g, s))
.sum::<f32>()
/ seeds.len() as f32
};
let mut best = Genome::baseline();
let mut best_s = score(&best);
for _ in 0..200 {
let cand = best.mutate(&mut rng);
let s = score(&cand);
if s < best_s {
best = cand;
best_s = s;
}
}
best
}
fn main() {
println!("== SONA · online auto-tuner (staleness-weighted (1+1)-ES, Ornith w(d_t)) ==");
println!(
"drift: regime A (stable) for epochs 0..{}, regime B (volatile) after\n",
EPOCHS / 2
);
let static_cfg = best_static();
println!(
"best STATIC config (offline, deployment regime A): λ0={:.0} lr={:.3} bθ={:.2}",
static_cfg.initial_lambda, static_cfg.lr, static_cfg.boundary_threshold
);
// Online auto-tuner: (1+1)-ES over the live stream, scored on a staleness window.
let mut rng = StdRng::seed_from_u64(0xA070);
// Deploy the offline-tuned config, then let the tuner adapt it online.
let mut current = static_cfg.clone();
let mut window = StalenessWindow::new(StalenessSchedule::new(6, 40, 0.10), 64);
let (mut post_static, mut post_online) = (0.0f32, 0.0f32);
let mut accepts = 0;
for epoch in 0..EPOCHS {
let regime = regime_at(epoch);
let seed = 7000 + epoch as u64;
// Incumbent runs the live epoch; record into the staleness window.
let loss_online = run_epoch(&regime, &current, seed);
window.push(loss_online);
let loss_static = run_epoch(&regime, &static_cfg, seed);
// Accumulate POST-DRIFT loss (the regime the static config wasn't tuned for).
if epoch >= EPOCHS / 2 {
post_online += loss_online;
post_static += loss_static;
}
// (1+1)-ES re-tune: probe a perturbation on the *recent* regime; accept if
// it beats the incumbent's staleness-weighted recent score.
if epoch % RETUNE_EVERY == 0 && epoch > 0 {
if let Some(incumbent_recent) = window.weighted_mean() {
let cand = current.mutate(&mut rng);
// Probe the candidate on a few recent-regime sequences (online: no peeking ahead).
let probe: f32 = (0..3)
.map(|k| run_epoch(&regime, &cand, seed.wrapping_add(31 * k + 1)))
.sum::<f32>()
/ 3.0;
if probe < incumbent_recent {
current = cand;
window.clear_samples(); // score the new config on its own fresh samples
accepts += 1;
}
}
}
}
let half = (EPOCHS / 2) as f32;
println!(
"online tuner: λ0={:.0} lr={:.3} bθ={:.2} ({accepts} accepted re-tunes)",
current.initial_lambda, current.lr, current.boundary_threshold
);
println!("\n-- POST-DRIFT cumulative loss (regime B; mean/epoch) --");
println!(" static config (offline-tuned): {:.4}", post_static / half);
println!(" online auto-tuner : {:.4}", post_online / half);
let gain = (post_static - post_online) / post_static * 100.0;
println!(" → online tuner is {gain:.1}% better after the workload drift");
println!(
"{}",
if post_online < post_static - 1e-4 {
"BEYOND STATIC — staleness-weighted online tuning tracks the drift a fixed config cannot"
} else {
"no improvement over the static config this run"
}
);
println!(
" (the margin is modest here — these synthetic regimes lack cleanly-opposite optima;\n \
the reusable win is the staleness-weighted `auto_tuner` machinery + the online ES\n \
that adapts a deployed config to drift instead of going stale.)"
);
assert!(post_online.is_finite() && post_static.is_finite());
}

View file

@ -0,0 +1,366 @@
//! Meta-Harness Darwin applied to SONA's EWC++ — "freeze the model, evolve the
//! harness", where the *frozen model* is the EWC++ continual-learning algorithm
//! ([`EwcPlusPlus`]) and the *evolved harness* is its [`EwcConfig`] genome
//! (lambda schedule, Fisher decay, auto-boundary threshold, learning rate).
//!
//! ## The benchmark (a real plasticity/forgetting frontier)
//!
//! A single weight vector `w` is trained on a *sequence* of tasks, each with its
//! own random target. The learner only ever sees the *current* task's gradient
//! (no replay) and must detect task switches itself (auto boundary detection via
//! `boundary_threshold`). EWC++ projects gradients away from parameters its
//! online Fisher deems important to earlier tasks. This is the canonical
//! continual-learning setup:
//!
//! * low lambda → `w` chases the latest task → high plasticity, **forgets**;
//! * high lambda → `w` frozen near task 0 → **can't learn** later tasks.
//!
//! The score is the average final loss across ALL tasks under one `w` (plus a
//! forgetting penalty) — minimised only by a config that *both* learns and
//! retains.
//!
//! ## Beyond SOTA (precise claim)
//!
//! `EwcConfig::default()` ships hand-tuned "OPTIMIZED" values (lambda 2000, …).
//! Darwin evolves the genome on TRAIN task-sequences and we report the result on
//! HELD-OUT sequences (different seeds). The beyond-SOTA result is: the evolved
//! genome beats the hand-tuned default on *unseen* task sequences — i.e. the
//! metaharness loop out-tunes the crate's hand-tuning, and it generalises.
//!
//! Run: `cargo run -p ruvector-sona --release --example darwin_ewc`
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use ruvector_sona::darwin_guard::{Guard, Verdict};
use ruvector_sona::{EwcConfig, EwcPlusPlus};
const PARAM_COUNT: usize = 128;
const N_TASKS: usize = 6;
const STEPS_PER_TASK: usize = 80;
const TRAIN_SEEDS: &[u64] = &[1, 2, 3, 4, 5, 6, 7, 8];
const TEST_SEEDS: &[u64] = &[101, 102, 103, 104, 105, 106]; // held out
// ── Genome: the EwcConfig harness Darwin evolves (frozen = the EWC++ algorithm) ─
#[derive(Clone, Debug)]
struct Genome {
lr: f32,
initial_lambda: f32,
min_lambda: f32,
max_lambda: f32,
fisher_ema_decay: f32,
boundary_threshold: f32,
gradient_history_size: usize,
}
impl Genome {
/// The shipped, hand-tuned SOTA baseline (mirrors `EwcConfig::default()`).
fn baseline() -> Self {
let d = EwcConfig::default();
Self {
lr: 0.1,
initial_lambda: d.initial_lambda,
min_lambda: d.min_lambda,
max_lambda: d.max_lambda,
fisher_ema_decay: d.fisher_ema_decay,
boundary_threshold: d.boundary_threshold,
gradient_history_size: d.gradient_history_size,
}
}
fn to_config(&self) -> EwcConfig {
EwcConfig {
param_count: PARAM_COUNT,
max_tasks: 10,
initial_lambda: self.initial_lambda,
min_lambda: self.min_lambda,
max_lambda: self.max_lambda,
fisher_ema_decay: self.fisher_ema_decay,
boundary_threshold: self.boundary_threshold,
gradient_history_size: self.gradient_history_size,
}
}
fn clamp(&mut self) {
self.lr = self.lr.clamp(0.01, 0.5);
self.initial_lambda = self.initial_lambda.clamp(10.0, 20_000.0);
self.min_lambda = self.min_lambda.clamp(1.0, 2_000.0);
self.max_lambda = self.max_lambda.clamp(2_000.0, 50_000.0);
self.fisher_ema_decay = self.fisher_ema_decay.clamp(0.90, 0.9999);
self.boundary_threshold = self.boundary_threshold.clamp(0.5, 6.0);
self.gradient_history_size = self.gradient_history_size.clamp(10, 200);
}
}
struct Metrics {
avg_final_loss: f32,
forgetting: f32,
plasticity: f32,
}
fn loss(w: &[f32], target: &[f32]) -> f32 {
0.5 * w
.iter()
.zip(target)
.map(|(a, b)| (a - b) * (a - b))
.sum::<f32>()
/ w.len() as f32
}
/// Run one task-sequence (seeded) under a genome; return continual-learning metrics.
fn run_sequence(g: &Genome, seq_seed: u64) -> Metrics {
let p = PARAM_COUNT;
let mut rng = StdRng::seed_from_u64(seq_seed);
// Task targets: each task wants `w` near a different random point. Tasks share
// the parameter space, so learning a later task drags `w` off earlier ones —
// the forgetting pressure EWC must counter.
let targets: Vec<Vec<f32>> = (0..N_TASKS)
.map(|_| (0..p).map(|_| rng.gen_range(-1.0f32..1.0)).collect())
.collect();
let mut ewc = EwcPlusPlus::new(g.to_config());
let mut w = vec![0.0f32; p];
let mut loss_just_after = vec![0.0f32; N_TASKS];
for (t, target) in targets.iter().enumerate() {
for _ in 0..STEPS_PER_TASK {
// Gradient of 0.5||w-target||^2 (the only signal — current task only).
let grad: Vec<f32> = w.iter().zip(target).map(|(wi, ti)| wi - ti).collect();
// The learner is NOT told task boundaries — it detects them itself.
if ewc.detect_task_boundary(&grad) {
ewc.start_new_task();
}
let gc = ewc.apply_constraints(&grad);
for (wi, gi) in w.iter_mut().zip(gc.iter()) {
*wi -= g.lr * gi;
}
ewc.update_fisher(&grad);
ewc.set_optimal_weights(&w);
}
loss_just_after[t] = loss(&w, target); // plasticity probe (fresh)
}
let final_losses: Vec<f32> = targets.iter().map(|tg| loss(&w, tg)).collect();
let avg_final_loss = final_losses.iter().sum::<f32>() / N_TASKS as f32;
// Forgetting: how much each task degraded between "just after" and "final".
let forgetting = final_losses
.iter()
.zip(loss_just_after.iter())
.map(|(f, a)| (f - a).max(0.0))
.sum::<f32>()
/ N_TASKS as f32;
let plasticity = loss_just_after.iter().sum::<f32>() / N_TASKS as f32;
Metrics {
avg_final_loss,
forgetting,
plasticity,
}
}
/// Mean metrics over a set of seeds.
fn evaluate(g: &Genome, seeds: &[u64]) -> Metrics {
let mut af = 0.0;
let mut fg = 0.0;
let mut pl = 0.0;
for &s in seeds {
let m = run_sequence(g, s);
af += m.avg_final_loss;
fg += m.forgetting;
pl += m.plasticity;
}
let n = seeds.len() as f32;
Metrics {
avg_final_loss: af / n,
forgetting: fg / n,
plasticity: pl / n,
}
}
/// Darwin fitness (higher = better): minimise final loss, penalise forgetting.
fn fitness(g: &Genome, seeds: &[u64]) -> f32 {
let m = evaluate(g, seeds);
-(m.avg_final_loss + 0.3 * m.forgetting)
}
fn mutate(g: &Genome, rng: &mut StdRng) -> Genome {
let mut c = g.clone();
if rng.gen::<f32>() < 0.5 {
c.lr *= 0.6 + rng.gen::<f32>() * 0.8;
}
if rng.gen::<f32>() < 0.6 {
c.initial_lambda *= 0.4 + rng.gen::<f32>() * 1.4;
}
if rng.gen::<f32>() < 0.4 {
c.min_lambda *= 0.4 + rng.gen::<f32>() * 1.4;
}
if rng.gen::<f32>() < 0.4 {
c.max_lambda *= 0.6 + rng.gen::<f32>() * 0.9;
}
if rng.gen::<f32>() < 0.4 {
c.fisher_ema_decay += rng.gen::<f32>() * 0.04 - 0.02;
}
if rng.gen::<f32>() < 0.5 {
c.boundary_threshold += rng.gen::<f32>() * 2.0 - 1.0;
}
if rng.gen::<f32>() < 0.3 {
c.gradient_history_size =
(c.gradient_history_size as i32 + rng.gen_range(-40..40)).max(10) as usize;
}
c.clamp();
c
}
fn main() {
println!(
"== SONA · Meta-Harness Darwin over EWC++ (freeze the algorithm, evolve the config) =="
);
println!(
"continual learning: {N_TASKS} tasks × {STEPS_PER_TASK} steps, {PARAM_COUNT} params, auto task-boundary detection"
);
let baseline = Genome::baseline();
let base_train = evaluate(&baseline, TRAIN_SEEDS);
println!(
"\nbaseline (hand-tuned default λ={:.0}): train avg_final_loss {:.4} forgetting {:.4} plasticity {:.4}",
baseline.initial_lambda, base_train.avg_final_loss, base_train.forgetting, base_train.plasticity
);
// ── GA: evolve the genome on TRAIN seeds only ───────────────────────────────
let mut rng = StdRng::seed_from_u64(0xE_C);
const POP: usize = 24;
const GEN: usize = 18;
const ELITE: usize = 6;
let mut pop: Vec<Genome> = std::iter::once(baseline.clone())
.chain((0..POP - 1).map(|_| mutate(&baseline, &mut rng)))
.collect();
let mut best = (baseline.clone(), fitness(&baseline, TRAIN_SEEDS));
for gen in 0..GEN {
// Reward-hacking guard (ADR-271): screen every candidate; non-finite or
// degenerate (collapsed zero-loss) configs are EXCLUDED from the ranking
// — not zero-scored — so a hack can neither win nor NaN-panic the sort.
let guard = Guard::deterministic();
let mut scored: Vec<(Genome, f32)> = Vec::new();
let mut rejected = 0usize;
for g in &pop {
let f = fitness(g, TRAIN_SEEDS);
let m = evaluate(g, TRAIN_SEEDS);
let finite = f.is_finite() && m.avg_final_loss.is_finite() && m.forgetting.is_finite();
match guard.screen(f, finite, true, m.avg_final_loss <= 0.0) {
Verdict::Accepted(_) => scored.push((g.clone(), f)),
Verdict::Rejected(_) => rejected += 1,
}
}
if scored.is_empty() {
scored.push(best.clone()); // never leave the population empty
}
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
if scored[0].1 > best.1 {
best = scored[0].clone();
}
let _ = rejected;
if gen % 3 == 0 || gen == GEN - 1 {
let m = evaluate(&scored[0].0, TRAIN_SEEDS);
println!(
"gen {gen:2}: train avg_final_loss {:.4} forgetting {:.4} (λ0={:.0} lr={:.3} bθ={:.2}) fitness {:.4}",
m.avg_final_loss, m.forgetting, scored[0].0.initial_lambda, scored[0].0.lr, scored[0].0.boundary_threshold, scored[0].1
);
}
let elites: Vec<Genome> = scored.iter().take(ELITE).map(|(g, _)| g.clone()).collect();
let mut next = elites.clone();
while next.len() < POP {
let parent = &elites[rng.gen_range(0..elites.len())];
next.push(mutate(parent, &mut rng));
}
pop = next;
}
// ── Coordinate-descent polish (deterministic, reproducible optimum) ─────────
let evolved = polish(best.0.clone());
// ── Report on HELD-OUT test seeds (never seen during evolution) ─────────────
let base_test = evaluate(&baseline, TEST_SEEDS);
let evo_test = evaluate(&evolved, TEST_SEEDS);
let evo_train = evaluate(&evolved, TRAIN_SEEDS);
println!("\n-- evolved genome --");
println!(
" λ0={:.0} λmin={:.0} λmax={:.0} decay={:.4} bθ={:.2} hist={} lr={:.3}",
evolved.initial_lambda,
evolved.min_lambda,
evolved.max_lambda,
evolved.fisher_ema_decay,
evolved.boundary_threshold,
evolved.gradient_history_size,
evolved.lr
);
println!(
" train: avg_final_loss {:.4} (baseline {:.4})",
evo_train.avg_final_loss, base_train.avg_final_loss
);
println!("\n-- HELD-OUT test sequences (the beyond-SOTA result) --");
println!(
" baseline: avg_final_loss {:.4} forgetting {:.4} plasticity {:.4}",
base_test.avg_final_loss, base_test.forgetting, base_test.plasticity
);
println!(
" evolved : avg_final_loss {:.4} forgetting {:.4} plasticity {:.4}",
evo_test.avg_final_loss, evo_test.forgetting, evo_test.plasticity
);
let gain =
(base_test.avg_final_loss - evo_test.avg_final_loss) / base_test.avg_final_loss * 100.0;
let fgain =
(base_test.forgetting - evo_test.forgetting) / base_test.forgetting.max(1e-9) * 100.0;
println!(
" → {:.1}% lower final loss, {:.1}% less forgetting on UNSEEN task sequences",
gain, fgain
);
println!(
"{}",
if evo_test.avg_final_loss < base_test.avg_final_loss {
"BEYOND SOTA — metaharness-evolved EWC++ config beats the hand-tuned default on held-out tasks"
} else {
"no improvement over baseline this run"
}
);
}
/// Greedy per-gene coordinate descent over a candidate grid (cross-seed mean on
/// TRAIN) — converts the GA's broad search into a reproducible optimum.
fn polish(seed: Genome) -> Genome {
let mut cur = seed;
let mut cur_f = fitness(&cur, TRAIN_SEEDS);
let lambdas: [f32; 8] = [50.0, 200.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 15000.0];
let lrs: [f32; 6] = [0.02, 0.05, 0.1, 0.15, 0.2, 0.3];
let bthr: [f32; 6] = [0.8, 1.5, 2.0, 3.0, 4.0, 5.0];
let decays: [f32; 4] = [0.95, 0.99, 0.999, 0.9999];
for _ in 0..3 {
let mut improved = false;
macro_rules! try_gene {
($field:ident, $cands:expr) => {
for &v in $cands.iter() {
if (cur.$field - v).abs() < f32::EPSILON {
continue;
}
let mut cand = cur.clone();
cand.$field = v;
cand.clamp();
let f = fitness(&cand, TRAIN_SEEDS);
if f > cur_f + 1e-9 {
cur = cand;
cur_f = f;
improved = true;
}
}
};
}
try_gene!(initial_lambda, lambdas);
try_gene!(lr, lrs);
try_gene!(boundary_threshold, bthr);
try_gene!(fisher_ema_decay, decays);
if !improved {
break;
}
}
cur
}

View file

@ -0,0 +1,284 @@
//! Per-task-category genome router (ADR-271, Ornith-1.0 borrow #2).
//!
//! Ornith-1.0's main empirical result is that **per-task-category strategies
//! emerge** — no single scaffold is optimal across workload types. The metaharness
//! analogue: instead of evolving ONE global `EwcConfig`, evolve a **router**
//! `task-class → genome`, so each workload class gets its own specialized config.
//!
//! Two continual-learning workload classes with *conflicting* optima:
//! * `STABLE` — few tasks, long training, small shifts → wants to *learn*
//! (low lambda; aggressive EWC protection only wastes plasticity).
//! * `VOLATILE` — many tasks, short training, large shifts → wants to *retain*
//! (high lambda; without it, later tasks erase earlier ones).
//!
//! Baseline = the single best global genome (the PR-#615 approach, evolved over
//! both classes pooled). Router = the best genome PER class. We report both on
//! HELD-OUT sequences. Beyond-SOTA result: the router beats the single global
//! genome on unseen sequences — because one config cannot serve conflicting
//! workloads. Selection is screened by `darwin_guard`.
//!
//! Run: `cargo run -p ruvector-sona --release --example darwin_router`
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use ruvector_sona::darwin_guard::{Guard, Verdict};
use ruvector_sona::{EwcConfig, EwcPlusPlus};
const PARAM_COUNT: usize = 96;
// Enough per-class data that specialization generalizes (Ornith's regime: many
// per-category samples) rather than overfitting a handful of sequences.
const TRAIN_SEEDS: &[u64] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
const TEST_SEEDS: &[u64] = &[101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112]; // held out
/// A workload class (task-category).
#[derive(Clone, Copy)]
struct Class {
name: &'static str,
n_tasks: usize,
steps: usize,
shift: f32, // inter-task target magnitude (forgetting pressure)
}
const STABLE: Class = Class {
name: "STABLE",
n_tasks: 3,
steps: 120,
shift: 0.25,
};
const VOLATILE: Class = Class {
name: "VOLATILE",
n_tasks: 9,
steps: 35,
shift: 1.2,
};
const CLASSES: [Class; 2] = [STABLE, VOLATILE];
#[derive(Clone, Debug)]
struct Genome {
lr: f32,
initial_lambda: f32,
max_lambda: f32,
fisher_ema_decay: f32,
boundary_threshold: f32,
}
impl Genome {
fn baseline() -> Self {
let d = EwcConfig::default();
Self {
lr: 0.1,
initial_lambda: d.initial_lambda,
max_lambda: d.max_lambda,
fisher_ema_decay: d.fisher_ema_decay,
boundary_threshold: d.boundary_threshold,
}
}
fn to_config(&self) -> EwcConfig {
EwcConfig {
param_count: PARAM_COUNT,
max_tasks: 12,
initial_lambda: self.initial_lambda,
min_lambda: 50.0,
max_lambda: self.max_lambda,
fisher_ema_decay: self.fisher_ema_decay,
boundary_threshold: self.boundary_threshold,
gradient_history_size: 60,
}
}
fn clamp(&mut self) {
self.lr = self.lr.clamp(0.01, 0.5);
self.initial_lambda = self.initial_lambda.clamp(10.0, 20_000.0);
self.max_lambda = self.max_lambda.clamp(2_000.0, 50_000.0);
self.fisher_ema_decay = self.fisher_ema_decay.clamp(0.90, 0.9999);
self.boundary_threshold = self.boundary_threshold.clamp(0.5, 6.0);
}
}
fn run_sequence(class: &Class, g: &Genome, seed: u64) -> f32 {
let p = PARAM_COUNT;
let mut rng = StdRng::seed_from_u64(seed ^ (class.n_tasks as u64).wrapping_mul(0x9E37));
let targets: Vec<Vec<f32>> = (0..class.n_tasks)
.map(|_| {
(0..p)
.map(|_| class.shift * rng.gen_range(-1.0f32..1.0))
.collect()
})
.collect();
let mut ewc = EwcPlusPlus::new(g.to_config());
let mut w = vec![0.0f32; p];
for target in &targets {
for _ in 0..class.steps {
let grad: Vec<f32> = w.iter().zip(target).map(|(wi, ti)| wi - ti).collect();
if ewc.detect_task_boundary(&grad) {
ewc.start_new_task();
}
let gc = ewc.apply_constraints(&grad);
for (wi, gi) in w.iter_mut().zip(gc.iter()) {
*wi -= g.lr * gi;
}
ewc.update_fisher(&grad);
ewc.set_optimal_weights(&w);
}
}
targets
.iter()
.map(|tg| {
0.5 * w
.iter()
.zip(tg)
.map(|(a, b)| (a - b) * (a - b))
.sum::<f32>()
/ p as f32
})
.sum::<f32>()
/ class.n_tasks as f32
}
fn eval_class(class: &Class, g: &Genome, seeds: &[u64]) -> f32 {
seeds
.iter()
.map(|&s| run_sequence(class, g, s))
.sum::<f32>()
/ seeds.len() as f32
}
fn mutate(g: &Genome, rng: &mut StdRng) -> Genome {
let mut c = g.clone();
if rng.gen::<f32>() < 0.5 {
c.lr *= 0.6 + rng.gen::<f32>() * 0.8;
}
if rng.gen::<f32>() < 0.6 {
c.initial_lambda *= 0.35 + rng.gen::<f32>() * 1.5;
}
if rng.gen::<f32>() < 0.5 {
c.max_lambda *= 0.6 + rng.gen::<f32>() * 0.9;
}
if rng.gen::<f32>() < 0.4 {
c.fisher_ema_decay += rng.gen::<f32>() * 0.04 - 0.02;
}
if rng.gen::<f32>() < 0.5 {
c.boundary_threshold += rng.gen::<f32>() * 2.0 - 1.0;
}
c.clamp();
c
}
/// Evolve a genome minimising `fit` (a loss closure), guard-screened.
fn evolve(seed: u64, fit: &dyn Fn(&Genome) -> f32) -> Genome {
let guard = Guard::deterministic();
let mut rng = StdRng::seed_from_u64(seed);
let base = Genome::baseline();
const POP: usize = 18;
const GEN: usize = 14;
let mut pop: Vec<Genome> = std::iter::once(base.clone())
.chain((0..POP - 1).map(|_| mutate(&base, &mut rng)))
.collect();
let mut best = (base.clone(), fit(&base));
for _ in 0..GEN {
let mut scored: Vec<(Genome, f32)> = Vec::new();
for g in &pop {
let loss = fit(g);
// Guard: exclude non-finite / degenerate (negative loss is impossible here).
match guard.screen(-loss, loss.is_finite(), true, loss < 0.0) {
Verdict::Accepted(_) => scored.push((g.clone(), loss)),
Verdict::Rejected(_) => {}
}
}
if scored.is_empty() {
scored.push(best.clone());
}
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
if scored[0].1 < best.1 {
best = scored[0].clone();
}
let elites: Vec<Genome> = scored.iter().take(5).map(|(g, _)| g.clone()).collect();
let mut next = elites.clone();
while next.len() < POP {
next.push(mutate(&elites[rng.gen_range(0..elites.len())], &mut rng));
}
pop = next;
}
best.0
}
fn main() {
println!("== SONA · per-task-category genome ROUTER (ADR-271, Ornith borrow) ==");
let base = Genome::baseline();
for c in &CLASSES {
println!(
"class {:8} baseline held-out loss {:.4}",
c.name,
eval_class(c, &base, TEST_SEEDS)
);
}
// Single GLOBAL genome (PR #615 approach): one config over BOTH classes pooled.
let global = evolve(0xA10BA1, &|g| {
CLASSES
.iter()
.map(|c| eval_class(c, g, TRAIN_SEEDS))
.sum::<f32>()
/ CLASSES.len() as f32
});
// ROUTER: one specialized genome PER class.
let router: Vec<(Class, Genome)> = CLASSES
.iter()
.map(|c| {
(
*c,
evolve(0x12345 ^ c.n_tasks as u64, &|g| {
eval_class(c, g, TRAIN_SEEDS)
}),
)
})
.collect();
// Held-out comparison.
let global_loss = CLASSES
.iter()
.map(|c| eval_class(c, &global, TEST_SEEDS))
.sum::<f32>()
/ CLASSES.len() as f32;
let router_loss = router
.iter()
.map(|(c, g)| eval_class(c, g, TEST_SEEDS))
.sum::<f32>()
/ router.len() as f32;
println!("\n-- per-class evolved configs --");
println!(
" GLOBAL (one config): λ0={:.0} lr={:.3} bθ={:.2}",
global.initial_lambda, global.lr, global.boundary_threshold
);
for (c, g) in &router {
println!(
" {:8} (router) : λ0={:.0} lr={:.3} bθ={:.2} held-out {:.4}",
c.name,
g.initial_lambda,
g.lr,
g.boundary_threshold,
eval_class(c, g, TEST_SEEDS)
);
}
println!("\n-- HELD-OUT (mean over classes) --");
println!(" single global genome (PR #615): {global_loss:.4}");
println!(" per-category router : {router_loss:.4}");
let gain = (global_loss - router_loss) / global_loss * 100.0;
println!(
" → router is {gain:.1}% better on held-out (specialization beats one-config-fits-all)"
);
println!(
"{}",
if router_loss < global_loss - 1e-4 {
"BEYOND SOTA — per-task-category routing beats the single best global config on unseen sequences"
} else {
"no improvement over the single global genome this run"
}
);
println!(
" (caveat: the gain is the value of specialization — modest here, and it REVERSES\n \
when per-class data is scarce: a specialized config then overfits while the pooled\n \
global generalizes. Per-category routing needs enough per-category samples Ornith's regime.)"
);
assert!(router_loss.is_finite() && global_loss.is_finite());
}

View file

@ -0,0 +1,235 @@
//! The `weightAdapter` gene — Darwin selects a fine-tuned LoRA adapter the way it
//! selects any other gene, and *empirically prunes* adapters that regress.
//!
//! Premise (the "autonomous data engine"): a fine-tune produces a candidate
//! adapter delta `Δw` (e.g. a LoRA distilled from verified SWE-bench trajectories).
//! Instead of *assuming* the new weights are better, expose the adapter as a gene
//! `(which_adapter, alpha)` and let evolutionary selection decide whether — and
//! how much — to apply it (`w_eff = w_base + alpha·Δw`), scored by held-out task
//! performance. A fine-tune that overfits gets pruned by selection.
//!
//! ## The subtlety this demonstrates (and why it matters)
//!
//! "Selection prunes overfit adapters" is TRUE — **but only if the fitness is
//! evaluated per-domain.** With a single *aggregate* fitness, an adapter whose
//! in-distribution gain outweighs its out-of-distribution loss is *selected* and
//! silently regresses the out-of-dist domain. Only a **per-domain / no-regression
//! (Pareto)** rule — "must not regress ANY repository" — actually prunes it.
//!
//! Two candidate adapters, distilled from TRAIN sequences:
//! * `general` — captures the signal common to both domains → helps both.
//! * `overfit` — captures domain-A-specific structure → helps A, hurts B.
//!
//! We then pick the best `(adapter, alpha)` under each selection rule on HELD-OUT
//! sequences and show: aggregate accepts `overfit` (and regresses B); per-domain
//! prunes it and keeps `general`.
//!
//! Run: `cargo run -p ruvector-sona --release --example darwin_weightadapter`
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
const DIM: usize = 64;
const TRAIN_SEEDS: &[u64] = &[1, 2, 3, 4, 5, 6, 7, 8];
// Held-out eval is IMBALANCED toward in-dist (A) — the realistic case: the eval
// pool is dominated by the same repos the adapter was fine-tuned on. A naive
// (volume-weighted) aggregate fitness is therefore easy to fool.
const TEST_A: &[u64] = &[101, 102, 103, 104, 105, 106]; // in-dist (majority)
const TEST_B: &[u64] = &[201, 202]; // out-of-dist (minority)
const NA: f32 = 6.0;
const NB: f32 = 2.0;
#[derive(Clone, Copy, PartialEq, Debug)]
enum Adapter {
None,
General,
Overfit,
}
/// Fixed structure of the two domains: a shared signal + per-domain offsets.
struct World {
mu_common: Vec<f32>,
delta_a: Vec<f32>, // domain-A-specific direction
delta_b: Vec<f32>, // domain-B-specific direction
}
impl World {
fn new() -> Self {
let mut rng = StdRng::seed_from_u64(0xC0FFEE);
let rv = |rng: &mut StdRng| {
(0..DIM)
.map(|_| rng.gen_range(-1.0f32..1.0))
.collect::<Vec<_>>()
};
let delta_a = rv(&mut rng);
// Domain B's structure OPPOSES domain A's — a small shared signal plus
// conflicting domain-specific directions, so an A-overfit adapter that
// captures `delta_a` actively hurts B (the realistic "fine-tune helped
// some repos, regressed others" case).
let delta_b: Vec<f32> = delta_a.iter().map(|x| -x).collect();
let mu_common: Vec<f32> = rv(&mut rng).iter().map(|x| 0.3 * x).collect(); // weak shared signal
Self {
mu_common,
delta_a,
delta_b,
}
}
/// A target for domain `a_side` (true = domain A / in-dist, false = B / out-dist).
fn target(&self, a_side: bool, seed: u64) -> Vec<f32> {
let mut rng = StdRng::seed_from_u64(seed ^ if a_side { 0xA } else { 0xB });
let d = if a_side { &self.delta_a } else { &self.delta_b };
(0..DIM)
.map(|i| self.mu_common[i] + d[i] + 0.05 * rng.gen_range(-1.0f32..1.0))
.collect()
}
}
fn loss(w: &[f32], target: &[f32]) -> f32 {
w.iter()
.zip(target)
.map(|(a, b)| (a - b) * (a - b))
.sum::<f32>()
/ w.len() as f32
}
/// Distil an adapter delta from TRAIN targets (mean target = the "fine-tune").
/// `general` averages both domains (shared signal); `overfit` uses domain A only.
fn distil(world: &World, adapter: Adapter) -> Vec<f32> {
match adapter {
Adapter::None => vec![0.0; DIM],
Adapter::Overfit => mean_target(world, &[true], TRAIN_SEEDS),
Adapter::General => mean_target(world, &[true, false], TRAIN_SEEDS),
}
}
fn mean_target(world: &World, sides: &[bool], seeds: &[u64]) -> Vec<f32> {
let mut acc = vec![0.0f32; DIM];
let mut n = 0.0;
for &side in sides {
for &s in seeds {
let t = world.target(side, s);
for (a, ti) in acc.iter_mut().zip(t.iter()) {
*a += ti;
}
n += 1.0;
}
}
acc.iter().map(|x| x / n).collect()
}
/// Mean task loss on a domain (seeds) with `w_eff = alpha·Δw` (base w = 0: the
/// adapter supplies all signal — what matters here is *relative* improvement).
fn domain_loss(world: &World, a_side: bool, delta: &[f32], alpha: f32, seeds: &[u64]) -> f32 {
let w: Vec<f32> = delta.iter().map(|d| alpha * d).collect();
seeds
.iter()
.map(|&s| loss(&w, &world.target(a_side, s)))
.sum::<f32>()
/ seeds.len() as f32
}
fn main() {
let world = World::new();
let alphas: Vec<f32> = (0..=20).map(|i| i as f32 / 20.0).collect();
let candidates = [Adapter::None, Adapter::General, Adapter::Overfit];
// Base (no adapter) reference loss per domain, on held-out seeds.
let base_a = domain_loss(&world, true, &vec![0.0; DIM], 0.0, TEST_A);
let base_b = domain_loss(&world, false, &vec![0.0; DIM], 0.0, TEST_B);
println!("== SONA · the `weightAdapter` gene — Darwin selects/prunes a fine-tuned adapter ==");
println!("two domains (A = in-dist, B = out-dist); base (no adapter) held-out loss: A {base_a:.4} B {base_b:.4}\n");
// Evaluate every (adapter, alpha) on held-out seeds → improvement per domain.
struct Cand {
adapter: Adapter,
alpha: f32,
gain_a: f32,
gain_b: f32,
}
let mut grid = Vec::new();
for &adapter in &candidates {
let delta = distil(&world, adapter);
for &alpha in &alphas {
let la = domain_loss(&world, true, &delta, alpha, TEST_A);
let lb = domain_loss(&world, false, &delta, alpha, TEST_B);
grid.push(Cand {
adapter,
alpha,
gain_a: base_a - la, // >0 = improves domain A
gain_b: base_b - lb, // >0 = improves domain B
});
}
}
// Volume-weighted (pooled) aggregate fitness — in-dist dominates the eval.
let pooled = |c: &Cand| (NA * c.gain_a + NB * c.gain_b) / (NA + NB);
// Per-adapter best (illustrate the overfit asymmetry).
println!("-- best alpha per adapter (held-out) --");
for &adapter in &candidates {
if adapter == Adapter::None {
continue;
}
let best = grid
.iter()
.filter(|c| c.adapter == adapter)
.max_by(|x, y| pooled(x).partial_cmp(&pooled(y)).unwrap())
.unwrap();
println!(
" {:8?}: α={:.2} ΔA {:+.4} ΔB {:+.4}",
adapter, best.alpha, best.gain_a, best.gain_b
);
}
// ── Selection rule 1: AGGREGATE fitness (mean gain) ─────────────────────────
let agg = grid
.iter()
.max_by(|x, y| pooled(x).partial_cmp(&pooled(y)).unwrap())
.unwrap();
// ── Selection rule 2: PER-DOMAIN no-regression (Pareto) ─────────────────────
// Accept only candidates that do NOT regress either domain, then maximise gain.
let perdomain = grid
.iter()
.filter(|c| c.gain_a >= -1e-4 && c.gain_b >= -1e-4)
.max_by(|x, y| pooled(x).partial_cmp(&pooled(y)).unwrap())
.unwrap();
println!("\n-- selection outcome --");
println!(
" AGGREGATE fitness → picks {:8?} α={:.2}: ΔA {:+.4} ΔB {:+.4} {}",
agg.adapter,
agg.alpha,
agg.gain_a,
agg.gain_b,
if agg.gain_b < -1e-4 {
"← REGRESSES domain B (silently accepted)"
} else {
""
}
);
println!(
" PER-DOMAIN (Pareto)→ picks {:8?} α={:.2}: ΔA {:+.4} ΔB {:+.4} {}",
perdomain.adapter,
perdomain.alpha,
perdomain.gain_a,
perdomain.gain_b,
if perdomain.adapter != agg.adapter {
"← pruned the overfit adapter"
} else {
""
}
);
println!("\n-- conclusion --");
println!("The weightAdapter gene lets Darwin keep a generalizing fine-tune AND reject an");
println!("overfit one — but ONLY under per-domain (no-regression) selection. A single");
println!("aggregate fitness is fooled by an adapter whose in-dist gain hides an out-dist");
println!("regression. Evolve the adapter as a gene, but score it per-repository.");
assert!(
perdomain.gain_a >= -1e-4 && perdomain.gain_b >= -1e-4,
"Pareto pick must not regress"
);
}

View file

@ -0,0 +1,199 @@
//! Online auto-tuner machinery for SONA config (ADR-271, Ornith-1.0 borrow #4).
//!
//! Offline evolution tunes a config to a *fixed* benchmark. Real workloads drift
//! (non-stationary trajectory streams), so a fixed config goes stale. This module
//! provides the **staleness-weighted** primitives for an *online* tuner that
//! re-optimizes against the live stream, weighting recent observations over old
//! ones via Ornith-1.0's staleness weight `w(d_t)`:
//!
//! ```text
//! w(d) = 1 if d <= k1 (fresh — full weight)
//! = exp(-lambda*(d - k1)) if k1 < d <= k2 (decaying)
//! = 0 if d > k2 (too stale — dropped)
//! ```
//!
//! where `d` is the *age* (clock ticks since the observation). The
//! [`StalenessWindow`] maintains a staleness-weighted running estimate of "how
//! well the current config is doing lately"; a `(1+1)`-ES on top of it (see
//! `examples/darwin_autotuner.rs`) accepts a perturbed config only when its
//! recent, freshness-weighted score beats the incumbent — so the tuner tracks a
//! drifting optimum instead of averaging over a stale past.
use std::collections::VecDeque;
/// Ornith-1.0 staleness schedule `w(d_t)`.
#[derive(Clone, Copy, Debug)]
pub struct StalenessSchedule {
/// Ages `<= k1` keep full weight 1.0.
pub k1: u64,
/// Ages `> k2` are dropped (weight 0).
pub k2: u64,
/// Exponential decay rate in the `(k1, k2]` band.
pub lambda: f32,
}
impl StalenessSchedule {
/// A sensible default: full weight for 16 ticks, decay to ~0 by 64.
#[must_use]
pub fn new(k1: u64, k2: u64, lambda: f32) -> Self {
Self { k1, k2, lambda }
}
/// `w(d)` for an observation of age `d` ticks.
#[must_use]
pub fn weight(&self, age: u64) -> f32 {
if age <= self.k1 {
1.0
} else if age <= self.k2 {
(-self.lambda * (age - self.k1) as f32).exp()
} else {
0.0
}
}
}
impl Default for StalenessSchedule {
fn default() -> Self {
Self {
k1: 16,
k2: 64,
lambda: 0.08,
}
}
}
/// A staleness-weighted window of recent scalar observations (e.g. per-step loss
/// under the current config). `push` advances the clock; `weighted_mean` reports
/// the freshness-weighted average; observations past `k2` are evicted.
#[derive(Clone, Debug)]
pub struct StalenessWindow {
schedule: StalenessSchedule,
/// `(value, recorded_at_clock)` newest-last.
samples: VecDeque<(f32, u64)>,
clock: u64,
cap: usize,
}
impl StalenessWindow {
/// New window with the given schedule and a hard capacity cap.
#[must_use]
pub fn new(schedule: StalenessSchedule, cap: usize) -> Self {
Self {
schedule,
samples: VecDeque::with_capacity(cap),
clock: 0,
cap: cap.max(1),
}
}
/// Record an observation under the current config; advances the clock and
/// evicts samples that are too stale (`age > k2`) or over capacity.
pub fn push(&mut self, value: f32) {
self.samples.push_back((value, self.clock));
self.clock += 1;
while self.samples.len() > self.cap {
self.samples.pop_front();
}
// Evict fully-stale observations from the front.
while let Some(&(_, t)) = self.samples.front() {
if self.clock.saturating_sub(t) > self.schedule.k2 {
self.samples.pop_front();
} else {
break;
}
}
}
/// Reset the recorded observations but keep the clock running — used after a
/// config switch so the new config is scored on *its own* fresh samples.
pub fn clear_samples(&mut self) {
self.samples.clear();
}
/// Staleness-weighted mean of the window, or `None` if empty / all-stale.
#[must_use]
pub fn weighted_mean(&self) -> Option<f32> {
let mut num = 0.0f32;
let mut den = 0.0f32;
for &(v, t) in &self.samples {
let w = self.schedule.weight(self.clock.saturating_sub(t));
num += w * v;
den += w;
}
if den > 0.0 {
Some(num / den)
} else {
None
}
}
/// Current clock (number of observations ever pushed).
#[must_use]
pub fn clock(&self) -> u64 {
self.clock
}
/// Number of live (non-evicted) observations.
#[must_use]
pub fn len(&self) -> usize {
self.samples.len()
}
/// Whether the window holds no live observations.
#[must_use]
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weight_is_fresh_then_decays_then_drops() {
let s = StalenessSchedule::new(4, 10, 0.5);
assert_eq!(s.weight(0), 1.0);
assert_eq!(s.weight(4), 1.0);
let w5 = s.weight(5);
assert!(w5 < 1.0 && w5 > 0.0); // decaying
assert!(s.weight(9) < w5); // monotone decay
assert_eq!(s.weight(11), 0.0); // dropped past k2
}
#[test]
fn weighted_mean_favors_recent() {
// Old samples = 1.0, recent samples = 0.0; the weighted mean must sit
// well below the unweighted 0.5 because recent dominates.
let mut w = StalenessWindow::new(StalenessSchedule::new(2, 32, 0.3), 64);
for _ in 0..20 {
w.push(1.0);
}
for _ in 0..20 {
w.push(0.0);
}
let m = w.weighted_mean().unwrap();
assert!(
m < 0.25,
"recent-weighted mean {m} should be near the recent 0.0"
);
}
#[test]
fn stale_observations_are_evicted() {
let mut w = StalenessWindow::new(StalenessSchedule::new(2, 8, 0.5), 1000);
for _ in 0..50 {
w.push(1.0);
}
// Only observations within k2=8 ticks of the clock survive.
assert!(w.len() <= 9, "expected <=9 live samples, got {}", w.len());
assert!(w.weighted_mean().is_some());
}
#[test]
fn empty_window_has_no_mean() {
let w = StalenessWindow::new(StalenessSchedule::default(), 8);
assert!(w.weighted_mean().is_none());
assert!(w.is_empty());
}
}

View file

@ -0,0 +1,297 @@
//! Reward-hacking defenses for evolutionary harness/config search (ADR-271).
//!
//! Borrowed from Ornith-1.0's three-layer defense ("Self-Scaffolding LLMs for
//! Agentic Coding", DeepReinforce 2026). When an evolutionary loop is allowed to
//! evolve its own harness/config, candidates can "win" by gaming the fitness
//! rather than improving — so the search must be screened:
//!
//! 1. **Immutable boundary** — the verifier (the fitness/eval) is frozen and
//! lives outside what evolves; the genome can only change the *inner* policy.
//! Modelled here by keeping [`screen`] a pure function of verifier output the
//! candidate cannot fabricate.
//! 2. **Deterministic monitor** — non-finite metrics, out-of-bounds genes, or a
//! degenerate/collapsed "win" are flagged and the candidate is **excluded
//! from the selection statistics** (Pareto front / advantage), NOT merely
//! zero-scored. A zero-scored hack can still bias selection; an excluded one
//! cannot. See [`best_accepted`].
//! 3. **Frozen judge veto** — an [`IntentJudge`] (e.g. a frozen LLM) may VETO
//! intent-level gaming inside the allowed surface, but never *sets* the
//! reward — it is a veto on top of the verifier, not the reward itself.
/// Outcome of screening one candidate. `Rejected` candidates are dropped from the
/// selection statistics entirely (the "exclude from advantage" rule).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Verdict {
/// Passed all layers; carries the verifier fitness.
Accepted(f32),
/// Rejected; excluded from Pareto/advantage with a reason.
Rejected(Reject),
}
/// Why a candidate was rejected (telemetry + auditability).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Reject {
/// A metric or the fitness was NaN/Inf.
NonFinite,
/// A gene was outside its declared bounds.
OutOfBounds,
/// "Won" via a collapsed/trivial path (caller-defined degeneracy check).
Degenerate,
/// The frozen intent-judge vetoed it.
JudgeVeto,
}
/// Layer 3: a frozen judge that may only VETO a candidate, never set its reward.
pub trait IntentJudge {
/// Return `true` to veto (reject) the candidate.
fn veto(&self, fitness: f32) -> bool;
}
/// Deterministic-only screening (no judge).
#[derive(Clone, Copy, Debug, Default)]
pub struct NoJudge;
impl IntentJudge for NoJudge {
fn veto(&self, _fitness: f32) -> bool {
false
}
}
/// The reward-hacking guard.
#[derive(Clone, Copy, Debug)]
pub struct Guard<J: IntentJudge = NoJudge> {
judge: J,
}
impl Guard<NoJudge> {
/// Deterministic-monitor-only guard (layers 12).
#[must_use]
pub fn deterministic() -> Self {
Self { judge: NoJudge }
}
}
impl<J: IntentJudge> Guard<J> {
/// Guard with a layer-3 intent judge.
pub fn with_judge(judge: J) -> Self {
Self { judge }
}
/// Screen one candidate. `fitness`/`finite_metrics` come from the IMMUTABLE
/// verifier (the candidate cannot fabricate them); `in_bounds`/`degenerate`
/// are caller-supplied deterministic checks over the genome + its metrics.
pub fn screen(
&self,
fitness: f32,
finite_metrics: bool,
in_bounds: bool,
degenerate: bool,
) -> Verdict {
if !finite_metrics || !fitness.is_finite() {
return Verdict::Rejected(Reject::NonFinite);
}
if !in_bounds {
return Verdict::Rejected(Reject::OutOfBounds);
}
if degenerate {
return Verdict::Rejected(Reject::Degenerate);
}
if self.judge.veto(fitness) {
return Verdict::Rejected(Reject::JudgeVeto);
}
Verdict::Accepted(fitness)
}
}
/// Best ACCEPTED candidate, EXCLUDING every rejected one from the comparison
/// (the Ornith "exclude from advantage" rule). `None` if all were rejected.
/// NaN-safe: rejected non-finite candidates never reach the comparator.
#[must_use]
pub fn best_accepted(verdicts: &[Verdict]) -> Option<(usize, f32)> {
verdicts
.iter()
.enumerate()
.filter_map(|(i, v)| match v {
Verdict::Accepted(f) => Some((i, *f)),
Verdict::Rejected(_) => None,
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
}
/// Rejection counts by reason: `[non_finite, out_of_bounds, degenerate, judge_veto]`.
#[must_use]
pub fn reject_summary(verdicts: &[Verdict]) -> [usize; 4] {
let mut c = [0usize; 4];
for v in verdicts {
if let Verdict::Rejected(r) = v {
c[match r {
Reject::NonFinite => 0,
Reject::OutOfBounds => 1,
Reject::Degenerate => 2,
Reject::JudgeVeto => 3,
}] += 1;
}
}
c
}
// ---------------------------------------------------------------------------
// Contamination guard (weight-eft / ADR-198 borrow). The training-data analog of
// the reward-hacking monitor: training or selecting on instances that appear in
// the eval holdout is *fake lift*. Enforce strict train/eval instance-ID
// disjointness — and surface what was excluded, never silently.
// ---------------------------------------------------------------------------
use std::collections::HashSet;
/// Train IDs that illegally appear in the eval holdout (the contamination set).
#[must_use]
pub fn contamination<'a>(
train_ids: impl IntoIterator<Item = &'a str>,
eval_holdout: &[&str],
) -> Vec<String> {
let holdout: HashSet<&str> = eval_holdout.iter().copied().collect();
let mut bad: Vec<String> = train_ids
.into_iter()
.filter(|id| holdout.contains(id))
.map(str::to_string)
.collect();
bad.sort();
bad.dedup();
bad
}
/// `assertTrainEvalDisjoint` analog: `Err(overlapping_ids)` if any training
/// instance is in the eval holdout, else `Ok(())`. Callers should treat `Err` as
/// fatal — a contaminated training set produces fake held-out lift.
///
/// # Errors
/// Returns the sorted, de-duplicated overlapping instance IDs.
pub fn assert_train_eval_disjoint(
train_ids: &[&str],
eval_holdout: &[&str],
) -> Result<(), Vec<String>> {
let bad = contamination(train_ids.iter().copied(), eval_holdout);
if bad.is_empty() {
Ok(())
} else {
Err(bad)
}
}
/// Exporter-style contamination filter: split `items` into
/// `(kept, excluded_by_holdout)` by their instance id, so the training set is
/// disjoint from the eval holdout by construction. Pair with the export report
/// (`excluded.len()`), never drop silently.
pub fn filter_holdout<T>(
items: Vec<T>,
id_of: impl Fn(&T) -> &str,
eval_holdout: &[&str],
) -> (Vec<T>, Vec<T>) {
let holdout: HashSet<&str> = eval_holdout.iter().copied().collect();
let mut kept = Vec::new();
let mut excluded = Vec::new();
for it in items {
if holdout.contains(id_of(&it)) {
excluded.push(it);
} else {
kept.push(it);
}
}
(kept, excluded)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_finite_is_excluded_not_zeroed() {
let g = Guard::deterministic();
// A NaN-producing candidate must be REJECTED (excluded), not scored 0 —
// a 0 could still win if all real candidates score negative.
assert_eq!(
g.screen(f32::NAN, true, true, false),
Verdict::Rejected(Reject::NonFinite)
);
assert_eq!(
g.screen(1.0, false, true, false),
Verdict::Rejected(Reject::NonFinite)
);
}
#[test]
fn out_of_bounds_and_degenerate_rejected() {
let g = Guard::deterministic();
assert_eq!(
g.screen(5.0, true, false, false),
Verdict::Rejected(Reject::OutOfBounds)
);
assert_eq!(
g.screen(5.0, true, true, true),
Verdict::Rejected(Reject::Degenerate)
);
}
#[test]
fn best_accepted_excludes_rejects_and_is_nan_safe() {
// The hacked candidate (NonFinite) must NOT win even though its raw value
// would sort highest; only accepted candidates are compared.
let vs = [
Verdict::Accepted(-0.5),
Verdict::Rejected(Reject::NonFinite),
Verdict::Accepted(-0.2),
Verdict::Rejected(Reject::Degenerate),
];
assert_eq!(best_accepted(&vs), Some((2, -0.2)));
assert_eq!(reject_summary(&vs), [1, 0, 1, 0]);
// All rejected → no selection (caller must handle, not crash).
assert_eq!(
best_accepted(&[Verdict::Rejected(Reject::OutOfBounds)]),
None
);
}
#[test]
fn judge_vetoes_but_does_not_set_reward() {
struct VetoHigh;
impl IntentJudge for VetoHigh {
fn veto(&self, fitness: f32) -> bool {
fitness > 100.0 // an implausibly-good score smells like gaming
}
}
let g = Guard::with_judge(VetoHigh);
assert_eq!(
g.screen(999.0, true, true, false),
Verdict::Rejected(Reject::JudgeVeto)
);
assert_eq!(g.screen(1.0, true, true, false), Verdict::Accepted(1.0));
}
#[test]
fn disjoint_train_eval_ok_and_contamination_detected() {
let eval = ["i-3", "i-9"];
assert_eq!(assert_train_eval_disjoint(&["i-1", "i-2"], &eval), Ok(()));
// Overlap is fatal and reports the contaminated ids (sorted, deduped).
assert_eq!(
assert_train_eval_disjoint(&["i-1", "i-9", "i-3", "i-9"], &eval),
Err(vec!["i-3".to_string(), "i-9".to_string()])
);
}
#[test]
fn filter_holdout_partitions_by_id() {
let items = vec![("i-1", 10), ("i-3", 20), ("i-5", 30)];
let (kept, excluded) = filter_holdout(items, |x| x.0, &["i-3"]);
assert_eq!(
kept.iter().map(|x| x.0).collect::<Vec<_>>(),
vec!["i-1", "i-5"]
);
assert_eq!(
excluded.iter().map(|x| x.0).collect::<Vec<_>>(),
vec!["i-3"]
);
// The kept set is now disjoint from the holdout by construction.
let kept_ids: Vec<&str> = kept.iter().map(|x| x.0).collect();
assert!(assert_train_eval_disjoint(&kept_ids, &["i-3"]).is_ok());
}
}

View file

@ -45,6 +45,8 @@
#![allow(missing_docs)]
pub mod auto_tuner;
pub mod darwin_guard;
pub mod engine;
pub mod ewc;
pub mod loops;

View file

@ -0,0 +1,67 @@
# ADR-271: Metaharness-Darwin for SONA Self-Improvement — EWC Config Evolution, the weightAdapter Gene, and Ornith-1.0 Reward-Hacking Defenses
- **Status**: Proposed (all four components prototyped — PR #615)
- **Date**: 2026-06-27
- **Extends**: ADR-266 (metaharness-Darwin ANN optimization), ADR-269/270 (mragent graph-memory Darwin)
- **External anchor**: Ornith-1.0 "Self-Scaffolding LLMs for Agentic Coding" (DeepReinforce, Jun 2026)
---
## Context
SONA (`crates/sona`) is a self-learning substrate: online LoRA adaptation + **EWC++** (`EwcPlusPlus`) to resist catastrophic forgetting, recording `QueryTrajectory` rewards. Its behaviour is governed by hand-tuned hyper-parameters (`EwcConfig`: lambda schedule, Fisher decay, task-boundary threshold; LoRA rank; MoE gate). These are **non-differentiable, workload-dependent** meta-parameters — gradients tune the weights, but nothing tunes the *config that governs how the weights are tuned*.
The metaharness line (ADR-266/269/270) established "freeze the model, evolve the harness" via **external evolutionary search**. This ADR applies that line to SONA's continual-learning layer and hardens it using the structure of **Ornith-1.0**, which does the same thing the *other* way (in-weights RL co-optimizing scaffold + solution).
## Decision
Apply metaharness-Darwin to SONA across four components. The **frozen model** is the EWC++/LoRA *algorithm*; the **evolved harness** is its config genome. Weights stay on the gradient path — Darwin only ever touches the meta-layer.
### 1. EWC++ config evolution (implemented — PR #615)
Evolve the `EwcConfig` genome (GA + coordinate-descent polish) on a continual-learning benchmark with **no replay** and **self-detected task boundaries**, train/test split over task-sequence seeds. Measured (held-out): **35% lower final loss, 98.6% less forgetting** than `EwcConfig::default()` (the crate's hand-tuned "OPTIMIZED" values) — a strict Pareto win that generalizes to unseen sequences. (`examples/darwin_ewc.rs`)
### 2. The `weightAdapter` gene (implemented — PR #615)
Expose a fine-tuned adapter delta (a LoRA) as a gene `(which_adapter, alpha)` so evolutionary selection decides *whether/how much* to apply it (`w_eff = w_base + alpha·Δw`) rather than assuming new weights are better. **Key finding** (`examples/darwin_weightadapter.rs`): "selection prunes overfit adapters" holds **only under per-domain (no-regression Pareto) evaluation**. A volume-weighted aggregate fitness is fooled by an adapter whose in-dist gain (where the eval pool concentrates) hides an out-dist regression. → **Score every adapter per-repository.**
### 3. The Autonomous Data Engine (realized upstream as `@metaharness/weight-eft`, ADR-198)
Darwin's archive is **execution-verified** preference data — the label is a *passing test suite*, not a noisy human judgment (higher-signal than RLHF). `@metaharness/weight-eft` realizes this for the agentic-coding cost-cascade (SFT/DPO distillation of gold SWE-bench trajectories into a cheap-tier LoRA via ruvllm/MicroLoRA, to escalate to a frontier model less often). Adopt its recipe — it gets the RLHF-correctness right:
- **SFT** distills **all** gold-resolved trajectories (cheap-own *and* frontier-escalation): max-likelihood is off-policy-stable, so a frontier success on an issue the cheap model couldn't solve is safe to distill.
- **DPO is on-policy only**: `chosen`/`rejected` are the **same model on the same instance** (cheap-vs-cheap, BoN-derived). A frontier-chosen-vs-cheap-rejected pair is off-policy/unstable → route it to SFT, not DPO. (Supersedes the earlier "plausible-but-failed negatives" sketch with the correct on/off-policy split.)
- **Contamination guard**: strict **train/eval instance-ID disjointness** — training on eval instances is fake lift. Implemented SONA-side in `darwin_guard` (`contamination` / `assert_train_eval_disjoint` / `filter_holdout`) as the analog of weight-eft's `assertTrainEvalDisjoint`.
- Portable export (OpenAI-chat JSONL with `tool_calls` preserved; TRL DPO); the trained adapter plugs back in as the `weightAdapter` gene (§2). The ruvllm/MicroLoRA seam is the ruvector integration point.
### 4. Ornith-1.0 borrows (method, not model)
Ornith bakes scaffold-evolution into weights via RL; we keep it external (cheaper, model-agnostic, no training). We borrow its *structure*:
- **3-layer reward-hacking defense + contamination guard** (the `darwin-guard` module): (i) **immutable outer boundary** — the verifier/eval is frozen and outside what evolves; (ii) **deterministic monitor** — gated variants (new imports/network/shell/env, reading withheld paths, touching the verifier) are **excluded from the advantage/Pareto computation**, not merely zero-scored, so they cannot bias selection; (iii) **frozen LLM judge as a veto** (local GPU `qwen`) on intent-level Goodharting inside the allowed surface — a veto on top of the verifier, never the primary reward; plus (iv) **train/eval contamination guard** (weight-eft / ADR-198 borrow): `assert_train_eval_disjoint` fails on any train∩eval instance-ID overlap — training/selecting on eval instances is fake lift.
- **Per-task-category specialization**: evolve a router `task-class → genome` instead of one global genome (Ornith's main empirical result is per-category strategies emerging).
- **Two-stage reward credit**: credit the *mutation/scaffold-proposal* that produced a winning genome, not just the outcome — turning the random `mutate()` into a learned write-layer (and the `(proposal → outcome)` pairs are themselves data-engine preference pairs).
- **Staleness-weighted replay** `w(d_t)` (1 if fresh → exp-decay → drop past threshold) for the online auto-tuner over SONA's live trajectory stream; maps onto `fisher_ema_decay` and is itself evolvable.
## Consequences
**Positive**: out-tunes hand-tuning on held-out continual learning (measured); model-agnostic and training-free (vs Ornith's GPU-scale RL); the reward-hacking defenses make the loop rigorous and Goodhart-resistant; the same Darwin genome co-optimizes "adopt this fine-tune?" with "how hard to protect old knowledge?".
**Negative / risks**: a beyond-SOTA number is only as real as the benchmark — the immutable-verifier boundary (borrow #4-i) is what keeps it honest; meta-optimization cost scales with benchmark realism (real nets ⇒ GPU + parallelism); generalization across *workload distributions* (not just task sequences) likely needs the per-category router (#4-ii), not one frozen genome.
## Relationship to Ornith-1.0
| | Ornith-1.0 | This ADR |
|---|---|---|
| Harness optimization | in-weights RL (gradients), two-stage | external evolutionary (GA/Pareto) |
| Cost | frontier RL training (9B397B) | training-free, any frozen model |
| Reward-hack defense | immutable boundary + monitor + judge | **borrowed verbatim** (darwin-guard) |
| Specialization | per-task-category (emergent) | per-task-category router (borrowed) |
Complementary, not competing: external-Darwin is the no-training counterpart to Ornith's in-weights approach.
## Implementation status
- ✅ EWC config evolution + weightAdapter gene (PR #615, `feat/sona-darwin-ewc-evolve`).
- ✅ darwin-guard reward-hacking + contamination module (`crates/sona/src/darwin_guard.rs`, 6 tests; reward-hacking screen wired into `darwin_ewc`; `assert_train_eval_disjoint`/`filter_holdout` = the weight-eft contamination guard).
- ✅ per-task-category router (`examples/darwin_router.rs`): beats the single best global config on held-out (~2%), with the **data-efficiency caveat** — the gain *reverses* when per-class data is scarce (a specialized config overfits while the pooled global generalizes), so routing needs enough per-category samples (Ornith's regime).
- ✅ online auto-tuner with staleness-weighted replay `w(d_t)` (`crates/sona/src/auto_tuner.rs``StalenessSchedule`/`StalenessWindow`, 4 tests; `examples/darwin_autotuner.rs` — a (1+1)-ES that adapts a deployed config to workload drift, beating the static config ~3% post-drift). Modest margin on synthetic regimes; the durable win is the reusable staleness machinery + the online-adaptation principle (a fixed offline-tuned config goes stale under drift).
## References
- Ornith-1.0: "Self-Scaffolding LLMs for Agentic Coding", DeepReinforce, 2026-06.
- `@metaharness/weight-eft` (npm) — evolutionary fine-tuning / autonomous data engine (SFT + on-policy DPO → cheap-tier LoRA), `agent-harness-generator` ADR-198. The production realization of §3 + §2; this ADR borrows its on-policy-DPO recipe and contamination-disjointness guard.
- ADR-266 metaharness-Darwin; ADR-269/270 mragent; PR #615.