# Ticket #000012 — Selection & consensus protocol (Merkle-AGI v8)

**Status:** in progress · Phase 1a (ForkScore) landed 2026-05-08; Phase 1b (consensus paper, `docs/_source/merkle-agi-v8-consensus.rst`) landed 2026-05-10; Phase 1c (branch-set persistence — `fork_score_branches` sibling table + `persist_branch_score` / `branch_set_density` + 6 default-off CLI flags on `arborist substrate score`) landed 2026-05-10, with the #000037 §12 Trigger 1 probe wired 2026-05-11 (`trigger_1_branch_density` reads `fork_score_branches` via `branch_set_density()` — measurable, not yet fired: no branch sets persisted yet); §8 below = the ForkScore threshold-calibration handoff received from #000025 §10.14 (2026-05-11), incl. the `delta_aggregator` knob (#000047) and the harder-fixture-tier line (#000046 closed; #000048 closed; #000049 open). Phase 2 (the v8 multi-validator consensus implementation) gates on a multi-validator deployment, like #000045
**Opened:** 2026-05-07
**Scope:** Spec for the loop-closing consensus protocol that turns the v7
substrate + v9.8 runtime into actual Darwinian selection across multiple
validators. Covers fitness scoring, mutation acceptance, fork-choice on
disagreement, Sybil resistance, validator economics. Doc-only — no code
in this ticket; this is the design substrate paper that follow-up
implementation tickets will reference.
**Audience:** fox + future blackops shifts + downstream Merkle-AGI v8
authors.
**Hard constraint:** no per-query consensus. Selection runs at
checkpoint cadence (Proof-of-Upgrade scope), not at inference time.
v9.8 cache_key invariant stays at 8 dims; consensus-state lives in a
sibling table, not folded into the answer-cache.

---

## 1. Problem statement

The DNA ↔ Merkle-DAG analogy promises:

```text
copy → vary → express → test → select → preserve → repeat
```

Merkle-AGI v7 ships everything except `select`. Section 13.4
("Proof-of-Upgrade") sketches a procedure where a candidate model
(M', C(M')) is admitted if it shows non-regression on a published
eval set + non-regression of ε-coverage at sentinel frontiers. That
procedure is **single-validator regression testing with a Merkle
receipt**. It is not consensus.

Specifically v7 § 13.4 leaves these gaps:

| Gap | Concrete failure under current spec |
|---|---|
| Validator set discovery | No protocol. Two labs reach different verdicts on (M'); no fork-choice. |
| Byzantine fault tolerance | One dishonest validator can sign acceptance for a poisoned (M'). |
| Sybil resistance | A single actor spinning up 50 validators wins every quorum. |
| Fork-choice on disagreement | If validators V1, V2 publish conflicting acceptance receipts, downstream nodes have no rule to pick. |
| Liveness vs safety trade | No bound on how long acceptance can stall when validators are offline. |
| Validator incentives | Why would anyone run a validator? What's the slashing condition for cheating? |
| Stake / membership semantics | Permissionless? Permissioned? Hybrid? v7 silent. |

Without a protocol that closes these, "digital evolution" is
metaphor — a single lab signing its own upgrades, no different in
trust model from current model-card releases.

### 1.1 Concrete failure scenario

Lab A trains M_t → M_{t+1} with a backdoor that triggers on a rare
input pattern. Lab A signs Proof-of-Upgrade: ε-coverage non-
regressed at every published frontier (because the backdoor lives at
a non-published frontier). Lab A publishes C(M_{t+1}). Anyone pulling
the registry sees an "accepted" upgrade. v7 has no way to surface
that no independent validator audited the upgrade.

The selection protocol must make "accepted under v8" mean
"accepted by N independent validators meeting policy P," verifiable
to anyone, not "Lab A signed it."

---

## 2. Design choices

### 2.1 Validator set: permissionless vs permissioned

**A. Permissionless (Bitcoin-style).** Anyone with stake (or proof-of-work
or proof-of-storage) can validate. Maximum censorship resistance.
Costs: economic incentive design, possible centralization through
mining/staking concentration, latency.

**B. Permissioned (consortium).** Validator set is curated by a
governance body. Easier to bootstrap, easier to slash, lower
latency. Costs: who curates? captures regulatory risk; "patch the
planet" mission frowns on gatekeepers.

**C. Hybrid (delegated proof-of-stake-like).** Permissionless
participation but stake required; misbehavior slashable. Common
middle-ground (Tezos / Cosmos). Bootstrappable and Sybil-resistant.

**Recommendation:** Hybrid. Permissionless joining with stake is
compatible with permacomputer values (no gatekeepers) and Sybil-
resistant in practice. Bootstrap from a small honest set with
explicit graduation criteria.

### 2.2 Fitness function: who defines it?

**A. Lab-defined (per upgrade).** Submitter declares the metric set
and eval digests; validators check non-regression. Flexible, gameable.

**B. Registry-defined (canonical bench suite).** A single canonical
suite gates every upgrade. Simple, brittle, hard to evolve.

**C. Layered (canonical floor + lab-declared ceiling).** Every upgrade
must non-regress on the canonical floor; lab can additionally
declare metrics they want validated. Default-on safety; allows
specialization.

**Recommendation:** C (Layered). Canonical floor is the safety
substrate every release passes through. Layered specialization
keeps domain models from being stuck behind irrelevant gates.

### 2.3 Quorum rule

**A. Simple majority (51%).** Liveness-friendly. Vulnerable to slim
majorities and hostile takeovers.

**B. Supermajority (2/3 +).** Standard BFT bound. Tolerates 1/3
Byzantine. Tighter than majority, slower under partition.

**C. Threshold signature (k-of-n).** Cryptographic accumulator;
single signature represents quorum. Cheap verification downstream;
needs ceremony to mint signing key.

**Recommendation:** B for safety floor, with threshold signature
(C) as a downstream optimization once the protocol stabilizes.
Tolerates the canonical 1/3 Byzantine fraction without giving up
liveness on small disagreements.

### 2.4 Slashing condition

A validator that signs acceptance for a candidate that subsequently
fails the canonical floor's audit replay loses stake. A validator
that signs conflicting acceptances (forks) loses stake. A validator
that double-signs (signs both accept and reject for same C(M'))
loses stake.

This requires:
- An audit-replay protocol that re-runs the canonical floor and
  publishes a Merkle-bound result.
- A challenge window during which any party can submit a
  counter-receipt invalidating an earlier acceptance.
- Time-locked stake unbonding so a validator can't sign and exit
  before challenges land.

### 2.5 Fork choice

When two valid acceptance chains diverge, downstream nodes must
pick one. Options:

**A. Longest valid chain.** Bitcoin-style. Vulnerable to deep reorgs.

**B. Highest-stake-weighted acceptance.** Eth-style finality.
Resistant to short-range reorgs.

**C. First-finalized wins.** GRANDPA-style. Once 2/3+ stake signs,
no reorg.

**Recommendation:** C. Once 2/3+ validators finalize an upgrade,
it's permanent. Latency is acceptable for checkpoint-cadence
selection (not inference).

---

## 3. Recommendation

Hybrid permissionless validator set with stake, layered fitness
floor + per-upgrade ceiling, supermajority quorum (2/3+), slashing
on audit-replay disagreement and equivocation, GRANDPA-style fork
choice. Bootstrap from a small honest set with explicit slashing
window before opening to permissionless joining.

---

## 4. Implementation sketch

This ticket commissions Merkle-AGI v8 as a sister paper to v7. v8
must specify:

1. **Validator state machine.**
   - States: `bonding`, `active`, `challenged`, `slashed`,
     `unbonding`.
   - Transitions: stake deposit, signature, challenge, slash, exit.
2. **Acceptance protocol.**
   - Proposer submits `(C(M'), eval_digest, frontier_coverage_diff,
     metric_delta_signed)`.
   - Validators run audit-replay, sign accept/reject within window.
   - Aggregate signature minted at quorum.
3. **Challenge protocol.**
   - Anyone submits `(C(M'), counter_evidence)` within challenge
     window.
   - If counter-evidence verifies (re-runs canonical floor and
     finds regression), all signing validators are slashed.
4. **Fork choice rule.**
   - Validators only sign on candidates whose parent C(M_t) is
     finalized.
   - Once 2/3+ stake signs C(M_{t+1}), it's finalized.
5. **Stake mechanics.**
   - Bond / unbond windows.
   - Slashing fraction per offense class.
   - Reward distribution per honest signature.
6. **Mesh wire format.**
   - Extension to arborist `mesh/wire.py` for validator gossip.
   - Aggregate signature canonicalization (so verification is
     stake-weight-independent).

The paper itself is the ticket-#000012 deliverable. Code lives in
follow-up tickets that cite this one.

### 4.1 Concrete artifacts this ticket produces

- `docs/merkle-agi-v8-consensus.rst` — sister to the v7 substrate
  paper. Sections: validator state machine, acceptance, challenge,
  fork choice, slashing, mesh wire format, BFT analysis.
- `docs/v8-policy-fields.md` — the policy fields v8 introduces and
  how they fold into `governance_policy_hash` (or whether they
  live in a sibling `consensus_policy_hash`).
- A worked-example Merkle-AGI v8 acceptance ledger reflecting one
  imaginary upgrade cycle (paper's appendix; does not require
  running validators).

### 4.2 What does not change

- v9.8 8-dim cache_key. Consensus state lives in `consensus_events`
  (sibling table), not in `cache_key`.
- arborist's per-shard audit chain. v8 is checkpoint-cadence,
  cross-validator; per-shard audit chain stays per-node.
- Existing falsification-state semantics (`live`, `failed`, `stale`,
  `quarantined`).

---

## 5. Out of scope

- Implementation of the v8 protocol in code. That is at minimum 3-4
  follow-up tickets (validator state machine, mesh wire extension,
  audit-replay harness, slashing accountant).
- Economic parameter calibration (stake amounts, slashing fractions,
  reward rates). v8 paper specifies the *form*; calibration is a
  governance decision.
- Cross-chain anchoring (publishing v8 finalizations to Bitcoin /
  Ethereum / etc.). Optional bolt-on.
- Selection of frontier benchmark fixtures (covered by ticket
  #000021).

---

## 6. Risks & open questions

- **Liveness vs censorship.** A validator set that requires 2/3+ to
  finalize stalls under 1/3 hostile partition. Acceptable for
  checkpoint cadence; needs explicit liveness floor in the spec.
- **Bootstrap honesty.** The initial validator set must be honest
  for the protocol to converge. Solution: explicit bootstrap window
  with permissioned set + scheduled transition to permissionless.
- **Stake captures.** Large staker can dominate. Mitigation: cap on
  individual stake weight, or convex weighting (sqrt-stake).
- **Re-staking attacks.** Validators staking the same capital across
  multiple v8 instances. Out of scope here; addressed by
  cross-instance slashing accumulator if/when v8 multiplies.

---

## 7. Status

**In progress · Phase 1a + Phase 1b landed 2026-05-10; Phase 1c
remains proposed-not-opened.**

### Phase 1a — ForkScore (landed)

Per fox's 2026-05-08 review note that the substrate is "good enough
to support v8 selection/fork-choice design," shipping the scoring
function ahead of the consensus paper:

- `arborist/substrate/fork_score.py` — pure ScoredFork dataclass +
  scoring function over (parent, child) BatteryResult bundles.
  Consumes every metric this session shipped: 5S/5T/5F sub-battery
  rates, `adaptation_efficiency_*` and `feedback_efficiency_*`
  (incl. inf-aware aggregation per fbd99a8 review), capital-cost
  delta, memory-invalidation count. (Originally landed at
  `arborist/v8/fork_score.py`; moved 2026-05-10 when the
  version-prefixed namespace pattern was retired.)
- `arborist/substrate/weights.py` — `WeightSet` dataclass with α…λ +
  `DEFAULT_WEIGHTS` (single-validator-tuned) + `from_dict` adapter
  handling the `"lambda"`/`lambda_` Python-reserved-word issue.
  (Originally `arborist/v8/weights.py`.)
- CLI: `arborist substrate score --parent P.json --child C.json
  [--weights W.json]`. Exits 1 on REJECT (CI-gateable).
- Verdict thresholds: ACCEPT (≥ SIGNAL_FLOOR=0.05), MARGINAL
  ([0, SIGNAL_FLOOR)), REJECT (negative score OR hard-regression
  flag OR `NEG_INF_REGRESSION` flag).
- Reference doc: `docs/v8-fork-score.md`.
- Tests: <!--AUTOCOUNT:tests:tests/test_fork_score.py-->31<!--/AUTOCOUNT-->
  cases in `tests/test_fork_score.py` pin the pure ScoredFork
  dataclass + scoring contract (SIGNAL_FLOOR=0.05,
  HARD_REGRESSION_FLOOR=0.05, score = sum-of-breakdown closure,
  NEG_INF_REGRESSION hard-reject). The CLI surface is covered
  by <!--AUTOCOUNT:tests:tests/test_substrate_fork_score.py-->27<!--/AUTOCOUNT-->
  additional cases in `tests/test_substrate_fork_score.py` —
  adapter tests + 4 in-process CLI tests via `build_parser()` +
  1 real subprocess invocation catching entry-point / sys.argv
  drift the in-process tests miss. Test file renamed from
  `tests/test_v8_fork_score.py` in `a4058a4` 2026-05-10 per the
  v-prefix-retirement convention (substrate-paper version vs
  schema-version disambiguation). Suite at Phase 1a landing
  (2026-05-08): 1186 passed; today: 2337 passed, 37 skipped.

### Phase 1b — Consensus paper (landed 2026-05-10)

Paper landed at `docs/_source/merkle-agi-v8-consensus.rst` (834
lines, RST sister to the v7-W substrate paper at the same path).
Sections delivered:

- **Part 1** — Introduction & motivation; gap table from v7 § 13.4;
  concrete backdoor-attack scenario; paper IS/IS-NOT scope.
- **Part 2** — Substrate definition; SQD A1/A2/A3 inheritance;
  `consensus_events` row schema; `consensus_policy_hash` sibling
  (never enters `cache_key`).
- **Part 3** — Validator state machine (bonding / active /
  challenged / slashed / unbonding) with full transition graph and
  invariants.
- **Part 4** — Acceptance protocol; layered fitness floor
  (canonical + lab-declared ceiling); audit-replay procedure;
  2/3-stake quorum + GRANDPA-style finalization; liveness floor.
- **Part 5** — Challenge protocol; counter-evidence shape;
  adjudication; challenger reward (30% recommended) + frivolous-
  challenge bond.
- **Part 6** — Stake mechanics; bond/unbond/challenge window
  recommendations; offense-class slashing schedule; reward
  distribution; optional stake cap + sqrt-weighting.
- **Part 7** — Fork choice rule (GRANDPA-style); finalization
  permanence; pre-finality fork-choice constraints; liveness
  recovery from offline-stake partition.
- **Part 8** — Mesh wire format extension (three new message
  kinds; BLS-or-concat aggregate signatures; bandwidth profile).
- **Part 9** — BFT analysis (safety, liveness, Sybil resistance,
  bootstrap honesty, re-staking attacks).
- **Part 10** — Worked example: 7-validator deployment, one
  upgrade cycle with successful challenge against one fraudulent
  validator.
- **Part 11** — Out of scope (implementation, calibration, cross-
  chain anchoring, fixture selection, bootstrap-set membership,
  cross-instance slashing accumulator, branch-set persistence).
- **Closure** — open questions: initial-set composition, threshold-
  key ceremony, ZK-replay, policy-hash transition mechanics.

What's deliberately NOT in Phase 1a (now specified in the v8 paper):

- Validator state machine (bonding / signing / slashing). → Part 3
- Acceptance protocol. → Part 4
- Challenge protocol. → Part 5
- Fork-choice rule. → Part 7
- Mesh wire format extensions. → Part 8
- Stake mechanics + economic incentives. → Part 6
- Cross-validator ZK proof exchange. → Closure §; deferred to #000016

### Phase 1c — Branch-set persistence (landed 2026-05-10)

**Status:** landed. Pressure-1 satisfied (Phase 1b paper landed
2026-05-10 — multi-branch deployment paper-spec is closed); fox
operator-go to wire the data path so #000037 §12 Trigger 1 gains
empirical surface. Implementation pinned below.

- **Schema:** `fork_score_branches` sibling table created via
  `arborist.store._migrate_fork_score_branches`; indexes on
  `branch_set_id` and `parent_root`. PK `(branch_set_id, branch_id)`.
- **Helpers:** `arborist.substrate.fork_score.persist_branch_score`
  (upsert one row; ON CONFLICT replaces score / verdict /
  breakdown_blob / weights_id / estimator_version / recorded_at) +
  `branch_set_density(conn, branch_set_id)` (count distinct branches
  for a checkpoint — the function the #000037 §12 Trigger 1 probe
  reads).
- **CLI:** `arborist substrate score` gains six flags
  (`--branch-set`, `--branch-id`, `--parent-root`, `--child-root`,
  `--persist-shard`, `--weights-id`). Default off — `--branch-set`
  absent ⇒ pure-function semantics preserved (Phase 1a behavior
  unchanged for every existing caller).
- **Estimator version pin:** module-level
  `ESTIMATOR_VERSION = "fork-score-v1"` constant; bump when a code
  change would produce a different `ScoredFork` from the same
  inputs (algorithm change, weight semantics, hard-regression
  policy). Persisted on every row so a reader can filter by
  estimator generation.
- **#000037 §12 Trigger 1 probe wired (2026-05-11):** original
  proposal step 4 — `bench/prometheus_sigma_trigger_probe.py`
  `trigger_1_branch_density` now reads `fork_score_branches` via
  `branch_set_density()` instead of returning the
  "density check not yet implemented" stub. It groups rows by
  `branch_set_id`, fires when the most-recently-recorded checkpoint
  carries ≥ 4 branches (`BRANCH_DENSITY_FLOOR`), and surfaces
  `n_checkpoints` / `latest_density` / `max_density` /
  `n_checkpoints_clearing_floor` in the markdown report so §12's
  "regularly" qualifier stays visible. With zero branch sets
  persisted yet, the probe correctly reports "table present but
  empty across shards; no branch sets persisted yet" —
  `data_available: True`, `fires: False` — not a false negative.
- **Tests:** 5 in `tests/test_fork_score.py` (migration-creates-
  table, persist-writes-one-row, upsert-on-pk, branch_set_density-
  counts-by-set, breakdown_blob-round-trips-as-json; suite 18 → 23)
  + 6 in `tests/test_prometheus_trigger_probe.py` (probe loaded via
  `importlib`; no-table → no data, empty-table → data-available-no-
  fire, latest-checkpoint-≥4 → fires, earlier-dense-but-latest-
  sparse → no fire, density-sums-across-shards, report-renders-
  density-lines).
- **Hard constraints honored:** sibling table never enters
  `audit_events.event_hash` preimage; no behavioral change to
  single-validator scoring; default-off CLI; no mesh wire format
  change; `weights_id` opaque (folding weights into a hash stays a
  Phase 1b/wire concern). The probe stays pure-measurement — no
  mutation, no LLM call, no schema change.

The original Phase-1c proposal text is preserved below for design-
log continuity. Re-read it as the authoritative spec; the bullets
above are the landing receipt.

#### Original proposal (preserved)

**Problem.** Phase 1a scores one ``(parent, child)`` fork at a time;
Phase 1b is the consensus paper. Neither persists *multiple
candidate branches at the same checkpoint*. Ticket #000037 §12
Trigger 1 ("ForkScore regularly receives ≥4 candidate branches per
checkpoint") gates Phase 1 of the Prometheus-Σ controller on this
data existing — and as of `bench/results/prometheus-sigma-triggers-
2026-05-10.md`, no `fork_score%` table exists across any shard, so
Trigger 1 structurally cannot fire.

This is the missing seam. Proposal scope (doc-only here; code lands
in a follow-up if/when fox approves):

**1. New table `fork_score_branches`** (sibling to `audit_events`,
similar to `capital_ledger` — does NOT enter `audit_events.event_hash`
preimage, so retroactive scoring cannot break the audit chain).

```sql
CREATE TABLE IF NOT EXISTS fork_score_branches (
    branch_set_id   TEXT NOT NULL,    -- checkpoint identity
                                       --   (e.g. parent_root + ts)
    branch_id       TEXT NOT NULL,    -- fork identifier
                                       --   (child_root or proposer key)
    parent_root     TEXT NOT NULL,    -- shared parent
    child_root      TEXT,             -- nullable for in-flight branches
    score           REAL NOT NULL,    -- ScoredFork.score
    verdict         TEXT NOT NULL,    -- ACCEPT / MARGINAL / REJECT
    breakdown_blob  TEXT NOT NULL,    -- canonical-JSON of full breakdown
    weights_id      TEXT NOT NULL,    -- which WeightSet was used
    estimator_version TEXT NOT NULL,  -- per Phase 1a fork_score module ver.
    recorded_at     INTEGER NOT NULL,
    PRIMARY KEY (branch_set_id, branch_id)
);
CREATE INDEX idx_fork_score_branches_set ON fork_score_branches(branch_set_id);
CREATE INDEX idx_fork_score_branches_parent ON fork_score_branches(parent_root);
```

PK is `(branch_set_id, branch_id)` so re-scoring the same fork
under the same set is a clean upsert, not a duplicate row.

**2. CLI surface.** Extend `arborist substrate score` with two optional
flags:

- ``--branch-set <ID>`` — names the checkpoint a result belongs to.
  When present, writes a row to `fork_score_branches` in addition to
  printing the verdict. When absent, behavior is unchanged (Phase 1a
  pure-function semantics preserved).
- ``--persist-shard <PATH>`` — names the SQLite file to write to.
  Defaults to ``$ARBORIST_QA_DB`` when set, else stays no-op.

**3. Read API.** One pure function in `arborist/substrate/fork_score.py`:

```python
def branch_set_density(conn, branch_set_id: str) -> int:
    """Count distinct branches recorded under a checkpoint id.

    Used by the #000037 trigger probe to satisfy Trigger 1."""
```

**4. #000037 probe wiring.** The trigger probe
(`bench/prometheus_sigma_trigger_probe.py:trigger_1_branch_density`)
currently reports "no data" when the table doesn't exist. After
Phase 1c lands, it queries `fork_score_branches` for the latest
checkpoint and reports `n_branches >= 4`.

**Hard constraints**:

- Sibling table — never enters `audit_events.event_hash` preimage.
- Schema-only; no behavioral change to single-validator scoring.
- Default off (a `--branch-set` flag absent ⇒ no row written).
- No mesh wire format change (that's Phase 1b's territory).
- `weights_id` is opaque; folding weights into a hash is a Phase 1b
  concern.

**What this enables (downstream tickets):**

- #000037 §12 Trigger 1 gains data; can fire empirically.
- ForkScore + Prometheus-Σ become composable: the controller reads
  a checkpoint's branch set and runs softmax across the persisted
  scores instead of needing to re-score from raw BatteryResults.
- A future operator-facing CLI (`arborist substrate branch-set show ID`)
  becomes trivial — same table powers it.

**Why not now**: this is doc-only because Phase 1c is small
(~80 LOC) but adds storage surface area. The operator pressure for
it lands when:

1. Multi-validator deployments produce competing branches naturally
   (gates on Phase 1b paper closing).
2. **OR** an operator wants to run several alternative π* / model
   configurations against the same parent and pick — that operator
   pressure currently exists only as a hypothesis.

If pressure 2 surfaces (e.g., during #000030 algebra/calculus kernel
expansion when multiple kernel variants compete), Phase 1c opens as
its own ticket. Until then, #000037 Trigger 1 simply reports "no
data — Phase 1c not landed" and the controller's multi-branch path
is paper-only. That matches §12's "measured trigger, not calendar
date" discipline.

---

## 8. ForkScore threshold calibration handoff (from #000025 §10.14)

Ticket #000025 §10.14 ("threshold calibration for v8 selection
acceptance handed off to #000012") landed 2026-05-11. Deliverable:
`bench/scripts/fivef_threshold_calibration.py` →
`bench/results/5f-threshold-calibration-2026-05-11.md`
(regenerate via `make bench-5f-threshold-calibration`). It runs the
canonical 5S/5T/5F packs (the ones `fork_score._BATTERY_RATE_KEYS`
reads) plus the 5F live packs and reports baseline rates,
observability granularity (1/n), and `fork_score` verdicts on the
parent vs synthetic child perturbations. Findings the v8 acceptance
protocol must account for:

1. **Floor constants stay at 0.05.** `SIGNAL_FLOOR = 0.05` and
   `HARD_REGRESSION_FLOOR = 0.05` match `docs/bench-maxing.md`'s
   5-pp signal floor; the synthetic sanity checks (identity →
   MARGINAL; degraded-parent → uniform-recovery → ACCEPT;
   single-sub regression → REJECT) all behave as the docstrings
   claim. The calibration ships **no constant change** — if a
   future revision moves a floor, this ticket owns that edit, and
   it does **not** fold into `governance_policy_hash` (ForkScore
   constants are estimator parameters pinned by
   `ESTIMATOR_VERSION`, not governance-policy inputs).

2. **Pack granularity vs the floors (load-bearing).** The
   foundational 5S packs are *coarser* than `HARD_REGRESSION_FLOOR`:
   `syntax` n=10 → 10.0 pp/fixture, `semantics` n=8 → 12.5
   pp/fixture. On those packs a single fixture flip is ≥ 5 pp, so
   *any* regression on syntax/semantics trips the hard-reject —
   intended zero-tolerance on the base, not a bug, but the v8
   protocol should state it: on the small packs
   `HARD_REGRESSION_FLOOR` is effectively "one fixture", not a
   tunable knob. The n=30 / n=50 / n=62 packs run 3.3 / 2.0 / 1.6
   pp per fixture, so there 5 pp ≈ 1.5–3 fixture flips.

3. **5× averaging dilution.** `_delta_5{s,t,f}` *means* over the 5
   sub-batteries of a battery, so a single sub-battery's rate gain
   contributes a fifth of its face value. "A fork must improve by
   `SIGNAL_FLOOR`" therefore means one of: ≈ 0.25 on a single
   sub-battery, ≈ 0.05 uniform across one battery's 5 subs, or
   ≈ 0.017 uniform across the whole 15-sub suite. Each battery
   contributes a separate Δ-term at weight 1.0, so a uniform
   +`SIGNAL_FLOOR` across every sub of all three batteries scores
   ≈ 0.15. If v8 wants single-sub improvements to weigh equally,
   switch `_delta_*` from mean to a max/sum aggregator. **#000047
   (closed 2026-05-11) ships the knob**: `WeightSet.delta_aggregator`
   ∈ {`mean`, `max`, `sum`}, default `mean` (unchanged → no
   `ESTIMATOR_VERSION` bump); `fork_score._delta_5{s,t,f}` dispatch
   via `_aggregate`; recorded in `ScoredFork.weights["delta_aggregator"]`;
   the per-sub `HARD_REGRESSION_FLOOR` flags are aggregator-independent
   so `max`/`sum` don't soften the regression side. v8 picks per-
   deployment via `WeightSet(delta_aggregator=...)`. The bench data
   behind keeping `mean` as default is in
   `bench/results/5f-threshold-calibration-2026-05-11.md` §5.

4. **Ceiling saturation.** Every 5S/5T/5F pack is at rate 1.0 at
   HEAD, so `α·Δ5s + β·Δ5t + γ·Δ5f` can only be ≤ 0 — an unchanged
   child scores 0 → MARGINAL (not ACCEPT). Until a harder fixture
   tier (or a deliberately-degraded parent baseline) drops a pack
   below ceiling, fork *acceptance* at the current pack difficulty
   is carried by the efficiency bonuses (`adaptation_efficiency` /
   `feedback_efficiency`) + the non-bench terms
   (`selfmodel_calibration_gain`, `audit_completeness`,
   `validator_diversity`, …), not by bench Δ-rate. The harder tier
   is **#000046 (closed 2026-05-11)**: two below-ceiling 5F packs
   (`falsification-hard-v1.jsonl`, `formulate-hard-v1.jsonl`) +
   `verify_quotes`' paraphrase numeric-agreement gate + (#000048
   steps 2.1 + 2.4) the entity salient-token gate and the
   `parse_pointer_claims` clause segmenter, which together lifted
   `falsification-hard` 4/12 → 10/12 and `formulate-hard` 4/12 →
   12/12 on *real* changes (each bench-gated: `make bench-qa` n=3
   before/after showed no STRICT-rate regression on legit answers —
   the 2.1 gate fired on 0 QA answers; the 2.4 segmenter touched 7 of
   450 lattice cells, both verdict changes correct) → `fork_score`'s
   `γ·Δ5f` went positive on them. So the bench Δ-rate *does* now carry
   signal on the 5F/falsification axis, demonstrated end-to-end.
   Remaining headroom: 2 `falsification-hard` recombination
   over-grounds — #000048 step 2.2 (a lexical clause-containment
   check) was attempted and reverted (can't tell recombination from a
   legit cross-sentence summary; #000048 §5), so closing those needs
   an attribution-aware verifier (#000049, its own ticket if
   wanted) — an optional follow-up, not a #000012 blocker.
