diff --git a/.claude/agents/physics-verifier.md b/.claude/agents/physics-verifier.md
new file mode 100644
index 000000000..b3e9c485c
--- /dev/null
+++ b/.claude/agents/physics-verifier.md
@@ -0,0 +1,73 @@
+---
+name: physics-verifier
+description: "Use this agent to adversarially audit an Islands-module (src/Islands/) diff for [VERIFY]-policy violations before it is committed — most importantly during autonomous overnight runs, where a guessed physics coefficient is the single worst failure mode. It hunts for numbers pulled from thin air, literature transcriptions committed as fact, and sign/convention errors against the Islands design docs. It is read-only and adversarial by charter. Invoke it before committing ANY physics-adjacent change in src/Islands/, and always before opening a PR that touches physics.\\n\\n\\nContext: An autonomous run just wired a bootstrap-drive term and is about to commit.\\nuser: \"The GradientDrive term is implemented — about to commit.\"\\nassistant: \"Before committing, let me run the physics-verifier agent to confirm no [VERIFY] coefficient was assigned a value and the half-width/sign conventions match docs/01.\"\\nPhysics-adjacent code before a commit — launch physics-verifier to enforce the [VERIFY] policy.\\n\\n\\n\\nContext: A term transcribes ω̂_D from D21 Eq. B1.\\nuser: \"Added the magnetic drift frequency from the paper.\"\\nassistant: \"I'll use the physics-verifier agent to check that the transcription carries a [CHECKED] tag with the exact Eq./page cite and a skipped benchmark, and was not silently promoted to confirmed.\"\\n"
+tools: Read, Grep, Glob, Bash
+model: opus
+color: red
+---
+
+You are the physics-verifier for the GPEC `Islands` module — a steady-state
+drift-kinetic island/layer solver. Your single job is to **find the guessed
+number and the silent transcription** before they enter the repository. You are
+read-only and adversarial: assume the diff contains a `[VERIFY]` violation and
+try to prove it. A fast-but-wrong physics result is worthless; your review is the
+last gate before an autonomous run commits physics-adjacent code.
+
+## Budget discipline
+
+Hard cap: **≤25 tool uses, ≤8 minutes**. One concrete deliverable: a verdict
+(`PASS` / `BLOCK`) plus a short itemized findings list. The invoking prompt hands
+you the diff or file paths — do not go spelunking the whole module. If you cannot
+finish in budget, stop and report what you checked and what remains.
+
+## What you enforce (the [VERIFY] policy — Islands CLAUDE.md, `src/Islands/CLAUDE.md`)
+
+1. **No guessed coefficients.** Every physics O(1) coefficient, threshold number,
+ sign, or normalization must be one of: (a) `[CHECKED: source, Eq./p.]` with an
+ exact citation, (b) `[VERIFY: source]` and *parameterized* (not baked into a
+ literal) with a failing/skipped benchmark referencing it, or (c)
+ `[DERIVED: date]` with a derivation under `docs/src/islands/derivations/`. A
+ bare numeric literal in a physics expression with none of these is a **BLOCK**.
+2. **No silent promotion.** A `[VERIFY]`/`[CHECKED]` expression implemented as if
+ confirmed (hardcoded value, no skipped benchmark, no parameter) is a BLOCK —
+ even if the value happens to be right. Only a human clears a tag.
+3. **No "fix the coefficient to make the test pass."** If a benchmark was made to
+ pass by editing a physics constant rather than the code, BLOCK and flag it.
+
+## Convention checks (Islands design docs — `docs/src/islands/design/`)
+
+- **Island width `w` is a HALF-width**; Ω = 2x²/w² − cos ξ; O-point Ω = −1,
+ separatrix Ω = +1 (docs/01 §1, CLAUDE.md). Flag any full-width/half-width
+ confusion, especially in threshold numbers (report always with the gyroradius
+ unit stated; ρ_bi = ε^{1/2}ρ_θi).
+- **Frames**: every ω sign convention must live in `src/Islands/frames/` and
+ nowhere else (docs/01 §5, CLAUDE.md). A sign convention or frame conversion
+ outside the frames module is a finding. The polarization-current sign depends
+ on frame — check ω − ω_E vs ω_E vs ω₀ = −ω_E usage against docs/01 §5.
+- **No regime branches** in operator physics code (`if collisional … else banana`)
+ — allowed only in `verify/` and preconditioners (CLAUDE.md).
+- Cross-check any transcribed equation against the cited source in the reference
+ library (`docs/src/islands/design/08-reference-library.md`) — the published
+ Imada 2019 set has documented errata (L23 §2.6); a term matching I19 Eq. (A.1)
+ *as printed* rather than the L23-amended form is a finding.
+
+## Method
+
+1. `git diff` (or read the named files) — enumerate every physics-adjacent
+ change: coefficients, signs, normalizations, transcribed equations, thresholds.
+2. For each, ask: is it tagged? parameterized? cited to an exact Eq./page? backed
+ by a skipped benchmark if `[VERIFY]`? Does it match the source and the
+ half-width/frame/sign conventions?
+3. Grep the diff for bare float literals inside physics expressions and for
+ `[VERIFY]`/`[CHECKED]` tags whose companion benchmark is missing.
+
+## Output
+
+- **Verdict**: `PASS` or `BLOCK`.
+- **Findings**: each as `file:line — — —
+ ` (e.g. "parameterize + add skipped benchmark", "add [CHECKED] cite",
+ "write a QUESTIONS.md entry and escalate"). Rank most-severe first.
+- If BLOCK, state plainly that the change must not be committed until the flagged
+ coefficient is escalated to `docs/src/islands/QUESTIONS.md` (never guessed).
+
+You never edit code. You never clear a `[VERIFY]` tag. You find the error.
diff --git a/.claude/hooks/guard-bash.sh b/.claude/hooks/guard-bash.sh
new file mode 100755
index 000000000..19bc2a6b0
--- /dev/null
+++ b/.claude/hooks/guard-bash.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+# PreToolUse(Bash) guard for unattended Islands runs.
+# Reads the hook JSON on stdin; `exit 2` blocks the tool call and returns the
+# stderr text to Claude as the denial reason. `exit 0` allows it.
+# Defense-in-depth under `dontAsk` (permissions.deny is the first layer; this is
+# the second, since glob deny-rules are coarse).
+set -euo pipefail
+
+payload="$(cat)"
+cmd="$(printf '%s' "$payload" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' 2>/dev/null || true)"
+
+# Never force-push.
+if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+push([[:space:]]|.)*(--force|--force-with-lease|[[:space:]]-f([[:space:]]|$))'; then
+ echo "BLOCKED: force-push is not allowed in unattended runs." >&2
+ exit 2
+fi
+
+# Never push to protected branches — pushes go only to feature/islands*.
+if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+push'; then
+ if printf '%s' "$cmd" | grep -Eq '(^|[[:space:]/])(main|develop)([[:space:]]|$|:)'; then
+ echo "BLOCKED: push to main/develop is not allowed; push only to feature/islands*." >&2
+ exit 2
+ fi
+fi
+
+# Refuse obviously destructive filesystem ops on absolute/home roots.
+if printf '%s' "$cmd" | grep -Eq 'rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+)(/|~|\$HOME|/mnt/homes)'; then
+ echo "BLOCKED: refusing recursive rm on an absolute/home path." >&2
+ exit 2
+fi
+
+exit 0
diff --git a/.claude/hooks/stop-check.sh b/.claude/hooks/stop-check.sh
new file mode 100755
index 000000000..14b4b88b7
--- /dev/null
+++ b/.claude/hooks/stop-check.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# Stop hook for unattended Islands runs.
+# `exit 2` blocks the session from ending (Claude keeps working, sees stderr);
+# `exit 0` lets it stop. Purpose: never end a night with a dirty tree or a
+# broken build.
+set -uo pipefail
+
+payload="$(cat)"
+
+# Avoid an infinite loop: if we are already continuing because of this hook,
+# let the session stop.
+active="$(printf '%s' "$payload" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("stop_hook_active", False))' 2>/dev/null || echo False)"
+if [ "$active" = "True" ]; then
+ exit 0
+fi
+
+root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}"
+cd "$root" 2>/dev/null || exit 0
+
+# 1. Working tree must be clean (commit granularly, per the protocol).
+if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
+ echo "BLOCKED stop: uncommitted changes present. Commit (to feature/islands) or stash before ending; then write a LOG.md entry." >&2
+ exit 2
+fi
+
+# 2. Build/tests must pass. Requires julia on PATH (see QUESTIONS.md Q1); if
+# julia is absent the check is skipped with a warning rather than blocking.
+# Run julia with LD_LIBRARY_PATH stripped: a loaded OMFIT module leaks the conda
+# env's libs onto the path, shadowing Julia's bundled artifacts (CHOLMOD, glib)
+# and giving false "broken build" blocks. Julia uses its own RPATH, so unsetting
+# is a no-op on a clean shell / CI.
+if command -v julia >/dev/null 2>&1; then
+ # Once M1 lands test/runtests_islands_*.jl, prefer running just those:
+ # julia --project=. test/runtests.jl test/runtests_islands_grids.jl ...
+ # Until then, a package-load smoke check.
+ if ! env -u LD_LIBRARY_PATH julia --project=. -e 'using GeneralizedPerturbedEquilibrium' >/dev/null 2>&1; then
+ echo "BLOCKED stop: package fails to load (using GeneralizedPerturbedEquilibrium errored)." >&2
+ exit 2
+ fi
+else
+ echo "WARN: julia not on PATH — skipping the build check in the Stop hook (QUESTIONS.md Q1)." >&2
+fi
+
+exit 0
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 000000000..57ec10fdb
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,65 @@
+{
+ "$comment": "Project-scoped Claude Code settings for GPEC. The permissions.allow list is sized for the Islands overnight autonomous loop run under --permission-mode dontAsk (anything not allowed is auto-denied, no prompt). permissions.deny + the PreToolUse hook are defense-in-depth against destructive git/filesystem ops. The Stop hook keeps an unattended session from ending with a dirty tree or a broken build. DRY-RUN THESE ONCE, SUPERVISED, before trusting an unattended run (design doc docs/src/islands/design/06 §2.3).",
+ "permissions": {
+ "allow": [
+ "Read",
+ "Grep",
+ "Glob",
+ "Edit",
+ "Write",
+ "NotebookEdit",
+ "TodoWrite",
+ "Task",
+ "Bash(julia:*)",
+ "Bash(git status:*)",
+ "Bash(git diff:*)",
+ "Bash(git log:*)",
+ "Bash(git show:*)",
+ "Bash(git add:*)",
+ "Bash(git commit:*)",
+ "Bash(git branch:*)",
+ "Bash(git checkout:*)",
+ "Bash(git switch:*)",
+ "Bash(git stash:*)",
+ "Bash(git pull --ff-only:*)",
+ "Bash(git push origin feature/islands:*)",
+ "Bash(gh pr:*)",
+ "Bash(gh run:*)",
+ "Bash(gh api:*)",
+ "Bash(ls:*)",
+ "Bash(cat:*)",
+ "Bash(mkdir:*)",
+ "Bash(cp:*)",
+ "Bash(mv:*)"
+ ],
+ "deny": [
+ "Bash(git push --force:*)",
+ "Bash(git push -f:*)",
+ "Bash(git push origin main:*)",
+ "Bash(git push origin develop:*)",
+ "Bash(git push* main)",
+ "Bash(git push* develop)",
+ "Read(./.env)",
+ "Read(~/.ssh/**)",
+ "Read(~/.claude.json)",
+ "Read(~/.config/gh/**)"
+ ]
+ },
+ "hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.sh\"" }
+ ]
+ }
+ ],
+ "Stop": [
+ {
+ "hooks": [
+ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/stop-check.sh\"" }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 7e1ff4f6b..d7121fb4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,8 +14,10 @@
*.DS_Store
*.pdf
!docs/resources/*.pdf
+!docs/resources/**/*.pdf
*.png
!docs/src/assets/**/*.png
+!docs/src/islands/figures/*.png
*.jld2
.gitattributes
Manifest.toml
diff --git a/CLAUDE.md b/CLAUDE.md
index da32ed045..38e7c1137 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -327,6 +327,15 @@ This workflow is reflected in the modular structure and data flow.
GPEC consists of **seven main modules** organized in `src/`:
+**Module naming convention**: `src/` module names are simple, intuitive descriptions
+of what the module *is* (`Vacuum`, `Equilibrium`, `ForceFreeStates`, `KineticForces`,
+`Islands`) — CamelCase, matching the directory. Do **not** give a feature a fancy
+standalone-sounding acronym or codename as if it were an independent code needing its
+own name recognition (e.g. no `ISLET`, `PENTRC`-style names for new modules). The
+domain the module addresses is the name. Working titles/acronyms from a paper or design
+doc are dropped when the module lands in `src/`. This keeps the codebase legible as one
+integrated tool rather than a bag of separately-branded programs.
+
#### Foundation Modules
1. **Splines** (`src/Splines/`) - Numerical interpolation library
diff --git a/Project.toml b/Project.toml
index fd2c512d4..3d5381551 100644
--- a/Project.toml
+++ b/Project.toml
@@ -14,9 +14,11 @@ DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78"
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838"
FastInterpolations = "9ea80cae-fc13-4c00-8066-6eaedb12f34b"
+ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
IMASdd = "c5a45a97-b3f9-491c-b9a7-aa88c3bc0067"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
+Krylov = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7"
LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
@@ -43,9 +45,11 @@ DoubleFloats = "1.6.2"
FFTW = "1.9.0"
FastGaussQuadrature = "1.1.0"
FastInterpolations = "0.4.10"
+ForwardDiff = "1.4.1"
HDF5 = "0.17.2"
IMASdd = "8"
JLD2 = "0.6.3"
+Krylov = "0.10.8"
LaTeXStrings = "1.4.0"
LinearAlgebra = "1"
OrdinaryDiffEq = "6.102.0"
diff --git a/benchmarks/islands/README.md b/benchmarks/islands/README.md
new file mode 100644
index 000000000..2e8e8e6fc
--- /dev/null
+++ b/benchmarks/islands/README.md
@@ -0,0 +1,30 @@
+# Islands benchmark ladder (docs/05)
+
+Benchmark scripts for the Islands verification ladder
+(`docs/src/islands/design/05-verification.md`). Each script names its ladder
+ID, configuration, target (with source cites), and status.
+
+**Status policy (the `[VERIFY]` discipline, `src/Islands/CLAUDE.md`):** every
+physics benchmark whose target or input coefficients are `[VERIFY]`/uncleared-
+`[CHECKED]` ships **skipped** — the script states exactly which
+`docs/src/islands/QUESTIONS.md` entries gate it and exits without running.
+Un-skipping a benchmark requires the human clearances it names; silently
+filling in a coefficient to make one run is the failure mode this project
+exists to prevent. The structural A-ladder (A1–A8) runs in CI via
+`test/runtests_islands_*.jl`, not here.
+
+**Targets are tiered (Decision D9, docs/05 "Target tiers").** The primary
+literature-facing gates are **scalings, trends, existence, and internal
+differentials** (T1/T2/T3); **absolute literature numbers (T4) are audit-gated**
+— never pass/fail without a published input manifest and sensitivity scan, and
+downgraded to a trend where the source is under-specified. Each script's header
+labels its targets by tier.
+
+Figure scripts (docs/07 pipeline) live in `figures/` and read archived
+benchmark data only.
+
+| Script | Ladder ID | Primary tier(s) | Status |
+|---|---|---|---|
+| `benchmark_B2_large_w_limits.jl` | B2 | T3 scalings (1/w, 1/w³); coeff T4 | SKIPPED — gated on Q2/Q3/Q4 |
+| `benchmark_B4_polarization_omegaE.jl` | B4 | T3 ω_E² + reversal existence; location T4 | SKIPPED — gated on Q2/Q3 |
+| `benchmark_B5_york_thresholds.jl` | B5a/b/c | T2 toggle ratio + T3 existence/trend; absolutes T4 | SKIPPED — gated on Q2/Q3/Q4 |
diff --git a/benchmarks/islands/benchmark_B2_large_w_limits.jl b/benchmarks/islands/benchmark_B2_large_w_limits.jl
new file mode 100644
index 000000000..92702271e
--- /dev/null
+++ b/benchmarks/islands/benchmark_B2_large_w_limits.jl
@@ -0,0 +1,46 @@
+# benchmark_B2_large_w_limits.jl — ladder B2 (docs/05 §B)
+#
+# Targets are TIERED (Decision D9; docs/05 "Target tiers"):
+# PRIMARY (T3): the large-w SCALINGS Delta_bs + Delta_cur ~ 1/w and
+# Delta_pol ~ 1/w^3, plus the parametric trend
+# eps^(1/2) (L_q/L_p) (beta_theta/w). Checked by fitting the
+# exponent over a w sweep — reproducible without absolute inputs.
+# AUDIT-GATED (T4): the 1/w COEFFICIENT vs WCHH96 Eq. (85) mapped to the
+# island frame (Diss19 p. 86 frame caveat) [CHECKED: Diss19
+# pp. 84-86; D21 Fig. 8-class curves]. Report only with an input
+# manifest + sensitivity scan (docs/05 reporting rules 6-8).
+#
+# WCHH96 is now in the reference library (docs/08): Wilson, Connor, Hastie &
+# Hegna, Phys. Plasmas 3, 248 (1996).
+#
+# STATUS: SKIPPED. Wired to the M2c assembly (Islands.Configure.configure_level0)
+# the same way as benchmark_B5 — un-skip is the ONE-LINE `const UNGATED = true`.
+# Gated per docs/src/islands/QUESTIONS.md:
+# Q5 the remaining L0 coefficient families + the QN (x-h) field source are
+# uncleared, so configure_level0 runs only STRUCTURALLY (no physics Δ(w))
+# Q3/Q4 the Δ moment prefactors are now CLEARED (M2b); the electron-closure
+# constants and ψ̃ [VERIFY] are folded into the Q5 lane
+
+using GeneralizedPerturbedEquilibrium
+const Isl = GeneralizedPerturbedEquilibrium.Islands
+
+"""
+Flip to `true` only when QUESTIONS Q5 clears the remaining L0 coefficient families.
+"""
+const UNGATED = false
+
+# The large-w Δ(w) sweep would assemble one configure_level0 per island width w
+# (varying w_psi in Level0Physics) and fit the 1/w, 1/w^3 exponents (T3). GATED:
+# a physics Δ(w) needs the Q5-cleared far field + QN source (see benchmark_B5).
+function run_b2()
+ error("B2 large-w scalings are gated on QUESTIONS Q5 (physics Δ(w) unavailable).")
+end
+
+if UNGATED
+ run_b2()
+else
+ println("SKIPPED: B2 large-w limits — gated on QUESTIONS Q5 (Δ moment prefactors CLEARED).")
+ println(" Scaffold wired to configure_level0; un-skip = `const UNGATED = true`.")
+ println(" Primary tier when un-gated: the 1/w and 1/w^3 SCALINGS (T3, fit the")
+ println(" exponent). The WCHH96 Eq. 85 coefficient is T4 (audit-gated).")
+end
diff --git a/benchmarks/islands/benchmark_B4_polarization_omegaE.jl b/benchmarks/islands/benchmark_B4_polarization_omegaE.jl
new file mode 100644
index 000000000..ebb7568fd
--- /dev/null
+++ b/benchmarks/islands/benchmark_B4_polarization_omegaE.jl
@@ -0,0 +1,46 @@
+# benchmark_B4_polarization_omegaE.jl — ladder B4 (docs/05 §B)
+#
+# Targets are TIERED (Decision D9; docs/05 "Target tiers"):
+# PRIMARY (T3): (i) the Wilson-Connor collisionless and Smolyakov collisional
+# SCALINGS; (ii) Delta_pol ~ omega_E^2 away from zero with a sign
+# reversal EXISTING at an omega_E of order -omega_dia,e, reversal
+# location insensitive to w/rho_theta_i; (iii) torque-balance
+# roots (Delta_sin = 0) EXISTING at discrete omega_hat_E.
+# AUDIT-GATED (T4): the reversal LOCATION (sources: ~ -0.89 omega_dia,e) and
+# the root VALUES (sources: +/-0.93, +/-1.28 omega_dia,e)
+# [CHECKED: D23b Fig. 8; Diss19 Fig. 4.18].
+#
+# STATUS: SKIPPED. Wired to the M2c assembly (Islands.Configure.configure_level0)
+# the same way as benchmark_B5 — un-skip is the ONE-LINE `const UNGATED = true`.
+# Gated per docs/src/islands/QUESTIONS.md:
+# Q3 the frame-convention signs (src/Islands/frames/) are NaN-gated until
+# cleared — the omega_E-dependence of Delta_pol is exactly the physics the
+# frames module must own before any sign-bearing benchmark runs
+# Q5 the L0 assembly's gradient-drive (which carries the omega_E frame shift)
+# and the QN (x-h) source are uncleared, so configure_level0 runs only
+# STRUCTURALLY (no physics Delta_pol(omega_E))
+
+using GeneralizedPerturbedEquilibrium
+const Isl = GeneralizedPerturbedEquilibrium.Islands
+
+"""
+Flip to `true` only when QUESTIONS Q3 (frame signs) and Q5 clear.
+"""
+const UNGATED = false
+
+# The Delta_pol(omega_E) scan would assemble configure_level0 across an omega_E
+# grid (through the gradient-drive frame shift) and check the omega_E^2 scaling +
+# reversal/root existence (T3). GATED: needs the cleared frames convention (Q3)
+# and the Q5 gradient-drive/QN-source (see benchmark_B5).
+function run_b4()
+ error("B4 polarization structure is gated on QUESTIONS Q3 (frame signs) and Q5.")
+end
+
+if UNGATED
+ run_b4()
+else
+ println("SKIPPED: B4 polarization omega_E structure — gated on QUESTIONS Q3, Q5.")
+ println(" Scaffold wired to configure_level0; un-skip = `const UNGATED = true`.")
+ println(" Primary tier when un-gated: omega_E^2 scaling + reversal/root EXISTENCE")
+ println(" (T3). The -0.89 reversal location and root values are T4 (audit-gated).")
+end
diff --git a/benchmarks/islands/benchmark_B5_york_thresholds.jl b/benchmarks/islands/benchmark_B5_york_thresholds.jl
new file mode 100644
index 000000000..1c1077a5c
--- /dev/null
+++ b/benchmarks/islands/benchmark_B5_york_thresholds.jl
@@ -0,0 +1,83 @@
+# benchmark_B5_york_thresholds.jl — ladder B5a/B5b/B5c (docs/05 §B)
+#
+# Targets are TIERED by reproducibility (Decision D9; docs/05 "Target tiers"):
+#
+# PRIMARY (run these first — reproducible without a full input manifest):
+# B5b/E1 T2 the :original -> :improved drift-model TOGGLE DIFFERENTIAL —
+# a ~x6 reduction in w_c in an otherwise identical Islands
+# configuration (the robust form of the sources' "8.73 -> 1.46
+# rho_bi" story; shared input uncertainty cancels in the ratio).
+# B5a T3 threshold EXISTENCE at w_c ~ O(rho_theta_i), :original model.
+# B5c T3 the v_star TREND dw_c/dv_star > 0 (roughly linear over
+# v_star in [5,20]e-3) and w_c proportional to rho_hat_theta_i.
+#
+# AUDIT-GATED (T4 — absolute numbers, NOT pass/fail without an input manifest;
+# see docs/src/islands/design/09-input-manifests.md):
+# B5a w_c ~= 2.76 rho_theta_i (half) = 8.73 rho_bi at eps=0.1 [CHECKED: I19 Fig. 9]
+# NB: I19 is internally inconsistent on its own run collisionality
+# (S4.2 v_star=0.01 vs L23 p.82 v_star=1e-3) — the type specimen of
+# why absolute matches need the input-completeness audit first (docs/09).
+# B5b w_c ~= 0.45 rho_theta_i = 1.46 rho_bi half-width [CHECKED: D21/D23a]
+# B5c w_c ~= 0.440 rho_hat_theta_i + 0.0178 v_star - 7.54e-5 [CHECKED: L23 Eq. 6.3.2]
+# (best T4 candidate — a thesis documents its numerics most fully)
+#
+# STATUS: SKIPPED. The scaffold below is wired to the M2c Level-0 assembly
+# (`Islands.Configure.configure_level0`): un-skipping is the ONE-LINE change
+# `const UNGATED = true`. It stays gated because the assembly's remaining
+# coefficient families are uncleared — parallel streaming, E×B, gradient drive,
+# the quasineutrality (x-h) field source, the collision magnitude, the
+# orbit-averaged pitch measure, and the neoclassical far field (QUESTIONS Q5).
+# Until Q5 clears, `configure_level0` runs only STRUCTURALLY (placeholders), so
+# no physics w_c can be extracted; flipping UNGATED before Q5 clears asserts-out.
+
+using GeneralizedPerturbedEquilibrium
+const Isl = GeneralizedPerturbedEquilibrium.Islands
+
+"""
+Flip to `true` only when QUESTIONS Q5 clears the remaining L0 coefficient families.
+"""
+const UNGATED = false
+
+# The B5 physics parameter set (eps=0.1, m/n=2/1, tau=1; docs/09 I19/D21 manifest).
+_b5_phys(variant) = Isl.Configure.Level0Physics(; epsilon=0.1, inv_Lq=1.0, inv_LB=1.0,
+ q_s=2.0, dq_dpsi=0.5, w_psi=0.05, mu0_R=1.0, inv_Ln0=1.0, rho_hat_theta_i=0.05,
+ eta_i=1.0, tau=1.0, variant=variant)
+
+# Assemble the :original and :improved configurations that the T2 toggle compares.
+# Structurally valid today; a *physics* w_c needs the cleared gated inputs (Q5).
+function _assemble_b5(variant)
+ grid = Isl.PhaseSpace.IslandGrid(; nx=41, nxi=16, ny=17, nE=6, halfwidth_x=8.0,
+ clustering_x=1.2, y_max=1.2, y_c=1.0, clustering_y=0.8, order=4)
+ species = [Isl.SpeciesLists.Species(; name=:i, Z=1.0, m=1.0,
+ background=Isl.SpeciesLists.Maxwellian(; n=1.0, T=1.0), role=Isl.SpeciesLists.Bulk)]
+ gated = Isl.Configure.level0_placeholders(grid) # PLACEHOLDER — not physics (Q5)
+ return Isl.Configure.configure_level0(grid, _b5_phys(variant), species; gated=gated)
+end
+
+# threshold_width(cfg) — the marginal-island w_c from the MRE root dw/dt=0. GATED:
+# needs the cleared far field + the (x-h) QN source to produce a physical Δ_neo(w)
+# (QUESTIONS Q5). Placeholder assembly cannot yield a physics threshold.
+function threshold_width(_cfg)
+ error("threshold_width is gated on QUESTIONS Q5 (cleared far field + QN source).")
+end
+
+function run_b5()
+ # T2 PRIMARY — the :original/:improved toggle differential (E1).
+ w_orig = threshold_width(_assemble_b5(:original))
+ w_impr = threshold_width(_assemble_b5(:improved))
+ ratio = w_orig / w_impr
+ println("B5b/E1 (T2) toggle differential: w_c(:original)/w_c(:improved) = ", ratio)
+ println(" expect a ~x6 reduction (sources' 8.73 -> 1.46 rho_bi story).")
+ # T3 PRIMARY — existence + v_star trend would follow here, over the Q5-cleared solve.
+ return ratio
+end
+
+if UNGATED
+ run_b5()
+else
+ println("SKIPPED: B5a/B5b/B5c — gated on QUESTIONS Q5 (and Q2/Q3/Q4 clearances).")
+ println(" Scaffold wired to Islands.Configure.configure_level0; un-skip = `const UNGATED = true`.")
+ println(" Primary tier when un-gated: B5b/E1 toggle differential (T2), threshold")
+ println(" existence (T3), dw_c/dv_star trend (T3). Absolute w_c values are T4")
+ println(" (audit-gated; docs/09 manifests + docs/05 'Target tiers').")
+end
diff --git a/benchmarks/islands/figures/README.md b/benchmarks/islands/figures/README.md
new file mode 100644
index 000000000..f0b2ad835
--- /dev/null
+++ b/benchmarks/islands/figures/README.md
@@ -0,0 +1,9 @@
+# Islands figure pipeline (docs/07 §2)
+
+Pinned figure scripts reading **archived benchmark data only** — the same
+script feeds CI artifacts, the state gallery, and paper panels. A figure that
+cannot be regenerated from archived data is a release-blocking bug.
+
+Empty until the first B-ladder benchmark is un-skipped (gated on
+`docs/src/islands/QUESTIONS.md` Q2–Q4); the Paper-I figure contract is
+`docs/src/islands/papers/paper-1/OUTLINE.md`.
diff --git a/benchmarks/islands/figures/make_structural_figures.jl b/benchmarks/islands/figures/make_structural_figures.jl
new file mode 100644
index 000000000..448dfbbe9
--- /dev/null
+++ b/benchmarks/islands/figures/make_structural_figures.jl
@@ -0,0 +1,151 @@
+# make_structural_figures.jl — pinned figure script (design docs/07 §2)
+#
+# Regenerates the structural (A-ladder) figures embedded in
+# docs/src/islands/numerics.md from the verification machinery itself — no
+# archived data needed yet because everything here is deterministic M1/M2
+# structure (manufactured coefficients, no physics). Physics (B-ladder) figures
+# will read archived benchmark data once QUESTIONS Q2–Q4 clear.
+#
+# Usage (repo root):
+# env -u LD_LIBRARY_PATH julia --project=. benchmarks/islands/figures/make_structural_figures.jl
+#
+# Writes PNGs into docs/src/islands/figures/ (committed as docs assets so the
+# Documenter CI build does not need to run this script).
+
+ENV["GKSwstype"] = "100" # headless GR
+
+using Plots
+using LinearAlgebra
+using GeneralizedPerturbedEquilibrium
+const Isl = GeneralizedPerturbedEquilibrium.Islands
+const PS = Isl.PhaseSpace
+const Op = Isl.Operators
+const So = Isl.Solvers
+const V = Isl.Verify
+const Fi = Isl.Fields
+
+outdir = normpath(joinpath(@__DIR__, "..", "..", "..", "docs", "src", "islands", "figures"))
+mkpath(outdir)
+saved = String[]
+function save!(p, name)
+ path = joinpath(outdir, name)
+ savefig(p, path)
+ push!(saved, path)
+ return path
+end
+
+# ---------------------------------------------------------------------------
+# F1 — layer-clustered grids: the sinh maps and node placement (04 §1)
+# ---------------------------------------------------------------------------
+let
+ gx = PS.MappedFDGrid(33; halfwidth=6.0, clustering=2.0, order=4)
+ gy = PS.MappedFDGrid(25; halfwidth=4.0, clustering=2.0, center=1.0, domain=:half, order=4)
+ s = range(-1, 1; length=33)
+ p1 = plot(s, gx.nodes; lw=2, xlabel="computational coordinate s", ylabel="x",
+ label="x(s), β=2", legend=:topleft, title="radial map (packs x = 0)")
+ scatter!(p1, s, gx.nodes; ms=3, label="nodes")
+ p2 = plot(; xlabel="node index", ylabel="y", title="pitch grid (packs y_c = 1)", legend=:topleft)
+ scatter!(p2, 1:gy.n, gy.nodes; ms=4, label="y nodes, β=2")
+ hline!(p2, [1.0]; ls=:dash, lc=:red, label="y_c (trapped–passing)")
+ p = plot(p1, p2; layout=(1, 2), size=(950, 380), left_margin=12Plots.mm, bottom_margin=6Plots.mm)
+ save!(p, "grids_clustering.png")
+end
+
+# ---------------------------------------------------------------------------
+# F2 — verification ladder A1: MMS convergence, operators + assembled + solve
+# ---------------------------------------------------------------------------
+let
+ mkg(n) = PS.IslandGrid(; nx=n, nxi=8, ny=n, nE=3, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+ ns = [9, 17, 33]
+ p = plot(; xscale=:log10, yscale=:log10, xlabel="radial/pitch resolution n",
+ ylabel="max error", legend=:bottomleft, title="MMS convergence (ladder A1)",
+ size=(700, 480), left_margin=12Plots.mm, bottom_margin=6Plots.mm)
+ for (term, lab) in ((:streaming, "streaming"), (:exb, "E×B bracket"),
+ (:collisions, "collisions"), (:perp, "⊥ transport"))
+ errs = [V.mms_operator_error(mkg(n), term) for n in ns]
+ plot!(p, ns, errs; marker=:circle, lw=2, label=lab)
+ end
+ errs = [V.mms_assembled_error(mkg(n)) for n in ns]
+ plot!(p, ns, errs; marker=:square, lw=3, lc=:black, label="assembled residual")
+ solve_errs = [V.solve_mms(n).err for n in (9, 17, 33)]
+ plot!(p, [9, 17, 33], solve_errs; marker=:diamond, lw=3, ls=:dash, label="converged solve (A1-solve)")
+ guide = errs[1] .* (ns[1] ./ ns) .^ 4
+ plot!(p, ns, guide; ls=:dot, lc=:gray, label="4th order")
+ save!(p, "mms_convergence.png")
+end
+
+# ---------------------------------------------------------------------------
+# F3 — the flattened-electron geometry functions and the A7 identity (01 §2.4)
+# ---------------------------------------------------------------------------
+let
+ Ωout = 1.02:0.02:5.0
+ Ωin = -0.98:0.02:0.98
+ Q = [Fi.Q_omega(Ω) for Ω in Ωout]
+ Qin = [Fi.Q_omega(Ω) for Ω in Ωin]
+ h = [Fi.h_profile(Ω; prefactor=1.0) for Ω in Ωout]
+ p1 = plot(Ωin, Qin; lw=2, label="Q(Ω), inside", xlabel="Ω", ylabel="Q",
+ title="Q(Ω) = (1/2π)∮√(Ω+cos ξ) dξ", legend=:topleft)
+ plot!(p1, Ωout, Q; lw=2, label="Q(Ω), outside")
+ vline!(p1, [1.0]; ls=:dash, lc=:red, label="separatrix")
+ a7 = [abs(Fi.flat_average_d2h_dx2(Ω, 1.0)) for Ω in (1.2, 2.0, 3.0, 5.0)]
+ p2 = plot(Ωout, h; lw=2, label="h(Ω) (unit prefactor)", xlabel="Ω", ylabel="h",
+ title="electron profile h(Ω)", legend=:topleft)
+ vline!(p2, [1.0]; ls=:dash, lc=:red, label="separatrix (h ≡ 0 inside)")
+ annotate!(p2, 1.6, 0.35, text("A7: max |⟨∂²h/∂x²⟩_Ω| = $(round(maximum(a7); sigdigits=2))", 9, :left))
+ p = plot(p1, p2; layout=(1, 2), size=(950, 380), left_margin=12Plots.mm, bottom_margin=6Plots.mm, right_margin=6Plots.mm)
+ save!(p, "hQ_profiles.png")
+end
+
+# ---------------------------------------------------------------------------
+# F4 — preconditioner quality (04 §5): GMRES iterations with/without YBlockJacobi
+# ---------------------------------------------------------------------------
+let
+ g = PS.IslandGrid(; nx=9, nxi=8, ny=9, nE=2, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+ nx, nξ, ny, nE, nσ = PS.nnodes(g)
+ P = @. g.y.nodes * (4.0 - g.y.nodes)
+ K, = Op.conservative_pitch_operator(g.y, P, ones(ny))
+ cstiff = fill(30.0, nx, nξ, nE, nσ)
+ shift = fill(-1.0, nx, nξ, ny, nE, nσ)
+ stack = Op.IslandStack((Op.PitchAngleDiffusion(K, cstiff), Op.RadiationSink(shift)),
+ Op.Quasineutrality(1.3))
+ f0! = So.flat_residual(stack, g)
+ N = Op.statelength(g)
+ b = sin.((1:N) ./ 7)
+ f!(out, u) = (f0!(out, u); out .-= b; out)
+ pc = So.YBlockJacobi(g, (ix, iξ, iE, iσ) -> I(ny) + cstiff[ix, iξ, iE, iσ] .* K; phi_scale=-1.3)
+ s0 = So.newton_krylov(f!, zeros(N); rtol=1e-10, memory=300)
+ s1 = So.newton_krylov(f!, zeros(N); rtol=1e-10, memory=300, precond=pc)
+ p = bar(["unpreconditioned", "y-block Jacobi (TSVD)"], [s0.gmres_iters, s1.gmres_iters];
+ ylabel="total GMRES iterations", legend=false,
+ title="preconditioner: stiff collisional solve",
+ ylims=(0, 1.25 * s0.gmres_iters),
+ size=(600, 430), left_margin=12Plots.mm, bottom_margin=6Plots.mm, top_margin=4Plots.mm)
+ annotate!(p, [(1, s0.gmres_iters + 0.06 * s0.gmres_iters, text("$(s0.gmres_iters)", 10)),
+ (2, s1.gmres_iters + 0.06 * s0.gmres_iters, text("$(s1.gmres_iters)", 10))])
+ save!(p, "preconditioner_gmres.png")
+end
+
+# ---------------------------------------------------------------------------
+# F5 — pseudo-arclength continuation around the toy fold (03 §3)
+# ---------------------------------------------------------------------------
+let
+ ftoy!(out, u, p) = (out[1] = u[1]^2 + p; out)
+ pa = So.pseudo_arclength(ftoy!, [1.0], -1.0; ds=0.15, nsteps=30, rtol=1e-12, atol=1e-12)
+ us = [z[1] for z in pa.us]
+ p = plot(pa.ps, us; marker=:circle, lw=2, xlabel="parameter p", ylabel="u",
+ label="continuation path", legend=:topleft,
+ title="pseudo-arclength steps around the fold (u² + p = 0)",
+ size=(650, 430), left_margin=12Plots.mm, bottom_margin=6Plots.mm)
+ plot!(p, -1.2:0.01:0.0, sqrt.(-(-1.2:0.01:0.0)); ls=:dash, lc=:gray, label="analytic ±√(−p)")
+ plot!(p, -1.2:0.01:0.0, -sqrt.(-(-1.2:0.01:0.0)); ls=:dash, lc=:gray, label="")
+ if !isempty(pa.folds)
+ k = pa.folds[1] + 1
+ scatter!(p, [pa.ps[k]], [us[k]]; ms=8, mc=:red, label="fold detected")
+ end
+ save!(p, "continuation_fold.png")
+end
+
+println("Saved figures:")
+foreach(f -> println(" ", f), saved)
diff --git a/docs/make.jl b/docs/make.jl
index b62bf121e..6940c2c2d 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -37,11 +37,42 @@ makedocs(;
"Forcing Terms" => "forcing_terms.md",
"Perturbed Equilibrium" => "perturbed_equilibrium.md",
"Inner Layer" => "inner_layer.md",
+ "Islands" => "islands.md",
"Analysis" => "analysis.md",
"Utilities" => "utilities.md"
- ],
+ ],
+ "Islands" => [
+ "Overview" => "islands/index.md",
+ "Numerics (as implemented)" => "islands/numerics.md",
+ "State dashboard" => "islands/state/STATE.md",
+ "Derivations" => [
+ "Overview" => "islands/derivations/index.md",
+ "ψ̃ amplitude" => "islands/derivations/psi-tilde-amplitude.md",
+ "ω̂_D drift frequency" => "islands/derivations/omega-D-drift-frequency.md",
+ "Collision operator" => "islands/derivations/collision-operator.md",
+ "Electron closure" => "islands/derivations/electron-closure.md",
+ "Quasineutrality closure" => "islands/derivations/quasineutrality-closure.md",
+ "Δ-moment prefactors" => "islands/derivations/delta-moment-prefactors.md",
+ "Parallel streaming" => "islands/derivations/parallel-streaming.md",
+ "Gradient drive" => "islands/derivations/gradient-drive.md",
+ "Passing fraction" => "islands/derivations/passing-fraction.md"
+ ],
+ "Paper I — figure contract" => "islands/papers/paper-1/OUTLINE.md",
+ "Design documents" => [
+ "00 — Roadmap" => "islands/design/00-roadmap.md",
+ "01 — Level-0 physics" => "islands/design/01-physics-level0.md",
+ "02 — Species and EPs" => "islands/design/02-species-and-eps.md",
+ "03 — Architecture" => "islands/design/03-architecture.md",
+ "04 — Numerics" => "islands/design/04-numerics.md",
+ "05 — Verification ladder" => "islands/design/05-verification.md",
+ "06 — Autonomy and tooling" => "islands/design/06-autonomy-and-tooling.md",
+ "07 — Documentation and papers" => "islands/design/07-documentation-and-papers.md",
+ "08 — Reference library" => "islands/design/08-reference-library.md",
+ "09 — Input manifests" => "islands/design/09-input-manifests.md"
+ ]
+ ],
"Citations" => "citations.md",
- "Developer Notes" => "developer_notes.md",
+ "Developer Notes" => "developer_notes.md"
],
checkdocs=:exports
)
diff --git a/docs/resources/Drift_Kinetic_Island_References/1996-Wilson-Threshold_for_neoclassical_magnetic_islands_in_a_low_collision_frequency_tokamak.pdf b/docs/resources/Drift_Kinetic_Island_References/1996-Wilson-Threshold_for_neoclassical_magnetic_islands_in_a_low_collision_frequency_tokamak.pdf
new file mode 100644
index 000000000..a3d3b2f41
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/1996-Wilson-Threshold_for_neoclassical_magnetic_islands_in_a_low_collision_frequency_tokamak.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2018-Dudkovskaia-Island_Stability_in_Phase_Space.pdf b/docs/resources/Drift_Kinetic_Island_References/2018-Dudkovskaia-Island_Stability_in_Phase_Space.pdf
new file mode 100644
index 000000000..06148fee4
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2018-Dudkovskaia-Island_Stability_in_Phase_Space.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2018-Imada-Drift_kinetic_response_of_ions_to_magnetic_island_perturbation_and_effects_on_NTM_threshold.pdf b/docs/resources/Drift_Kinetic_Island_References/2018-Imada-Drift_kinetic_response_of_ions_to_magnetic_island_perturbation_and_effects_on_NTM_threshold.pdf
new file mode 100644
index 000000000..5259166be
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2018-Imada-Drift_kinetic_response_of_ions_to_magnetic_island_perturbation_and_effects_on_NTM_threshold.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2018-Imada-Nonlinear_Kinetic_Ion_Response_to_Small_Scale_Magnetic_Islands_in_Tokamak_Plasmas.pdf b/docs/resources/Drift_Kinetic_Island_References/2018-Imada-Nonlinear_Kinetic_Ion_Response_to_Small_Scale_Magnetic_Islands_in_Tokamak_Plasmas.pdf
new file mode 100644
index 000000000..094341bc7
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2018-Imada-Nonlinear_Kinetic_Ion_Response_to_Small_Scale_Magnetic_Islands_in_Tokamak_Plasmas.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2019-Dudkovskaia-Modelling_NTMs_in_tokamak_plasmas_PhD_dissertation.pdf b/docs/resources/Drift_Kinetic_Island_References/2019-Dudkovskaia-Modelling_NTMs_in_tokamak_plasmas_PhD_dissertation.pdf
new file mode 100644
index 000000000..fb50a695a
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2019-Dudkovskaia-Modelling_NTMs_in_tokamak_plasmas_PhD_dissertation.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2019-Imada-Finite_ion_orbit_width_effect_on_the_neoclassical_tearing_mode_threshold_in_a_tokamak_plasma.pdf b/docs/resources/Drift_Kinetic_Island_References/2019-Imada-Finite_ion_orbit_width_effect_on_the_neoclassical_tearing_mode_threshold_in_a_tokamak_plasma.pdf
new file mode 100644
index 000000000..83479cbb8
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2019-Imada-Finite_ion_orbit_width_effect_on_the_neoclassical_tearing_mode_threshold_in_a_tokamak_plasma.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2021-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_a_low_collisionality_tokamak_plasma_magnetic_island_threshold_physics.pdf b/docs/resources/Drift_Kinetic_Island_References/2021-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_a_low_collisionality_tokamak_plasma_magnetic_island_threshold_physics.pdf
new file mode 100644
index 000000000..467fc7bef
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2021-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_a_low_collisionality_tokamak_plasma_magnetic_island_threshold_physics.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2023-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_tokamak_plasmas_polarisation_current_and_its_effect_on_magnetic_island_threshold_physics.pdf b/docs/resources/Drift_Kinetic_Island_References/2023-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_tokamak_plasmas_polarisation_current_and_its_effect_on_magnetic_island_threshold_physics.pdf
new file mode 100644
index 000000000..461072467
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2023-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_tokamak_plasmas_polarisation_current_and_its_effect_on_magnetic_island_threshold_physics.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2023-Dudkovskaia-Drift_kinetic_theory_of_the_NTM_magnetic_islands_in_a_finite_beta_general_geometry_tokamak_plasma.pdf b/docs/resources/Drift_Kinetic_Island_References/2023-Dudkovskaia-Drift_kinetic_theory_of_the_NTM_magnetic_islands_in_a_finite_beta_general_geometry_tokamak_plasma.pdf
new file mode 100644
index 000000000..49db982b3
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2023-Dudkovskaia-Drift_kinetic_theory_of_the_NTM_magnetic_islands_in_a_finite_beta_general_geometry_tokamak_plasma.pdf differ
diff --git a/docs/resources/Drift_Kinetic_Island_References/2023-Leigh-Drift_kinetic_simulations_of_Neoclassical_Tearing_Mode_instabilities_in_finite_collisionality_tokamak_plasmas.pdf b/docs/resources/Drift_Kinetic_Island_References/2023-Leigh-Drift_kinetic_simulations_of_Neoclassical_Tearing_Mode_instabilities_in_finite_collisionality_tokamak_plasmas.pdf
new file mode 100644
index 000000000..a7d347163
Binary files /dev/null and b/docs/resources/Drift_Kinetic_Island_References/2023-Leigh-Drift_kinetic_simulations_of_Neoclassical_Tearing_Mode_instabilities_in_finite_collisionality_tokamak_plasmas.pdf differ
diff --git a/docs/src/islands.md b/docs/src/islands.md
new file mode 100644
index 000000000..2d22e176f
--- /dev/null
+++ b/docs/src/islands.md
@@ -0,0 +1,136 @@
+# Islands Module
+
+The Islands module is a steady-state, multi-species drift-kinetic solver for the
+resonant magnetic island/layer region in tokamaks — the nonlinear analog of
+SLAYER, generalizing the Modified Rutherford Equation. It is under active
+development; the module conventions live in `src/Islands/CLAUDE.md`.
+
+This page is the **API reference**. The narrative documentation lives in the
+**Islands** section of this site: the [project overview](islands/index.md),
+the equations-and-figures chapter of what is
+[implemented and verified so far](islands/numerics.md), the
+[Paper I figure contract](islands/papers/paper-1/OUTLINE.md), and the full
+[design document set](islands/design/00-roadmap.md).
+
+## Status — M1 skeleton + M2 Level-0 solve machinery (structure, gated physics)
+
+M1 landed the numerical skeleton and M2 the Level-0 solve machinery — structure
+only, no physics numbers (module `CLAUDE.md`, the `[VERIFY]` policy):
+
+ - `Islands.PhaseSpace` — the `(x, ξ, λ→y, E, σ)` phase-space grids with
+ layer-clustered mappings (design `04 §1`): Fourier spectral `∂ξ`, high-order
+ finite-difference `∂x`/`∂y` on stretched grids, and Gauss energy quadrature.
+ Pure numerics; no physics coefficients.
+ - `Islands.Operators` — the `AbstractTerm` operator stack and residual assembly
+ (design `03 §2`) as allocation-free, AD-compatible structure, including the
+ mimetic (exactly conservative) pitch-angle diffusion and the neoclassical-
+ matching far-field boundary conditions (`01 §3` — never bare Neumann). Every
+ physics coefficient is a supplied data field, never a literal.
+ - `Islands.SpeciesLists` — first-class species arrays (`02 §1`, Decision D3):
+ backgrounds, `Bulk`/`Trace` roles, trace-criteria checks that warn and never
+ silently degrade.
+ - `Islands.Frames` — THE frequency/frame conversion module (`01 §5`): the
+ conversion *forms* with every sign/normalization a NaN-gated
+ `FrameConvention` field until human-cleared (QUESTIONS Q3).
+ - `Islands.Fields` — the Level-0 quasineutrality closure structure: `Q(Ω)`,
+ `h(Ω)` (supplied prefactor), the coefficient-free identity
+ `⟨∂²h/∂x²⟩_Ω = 0` (ladder A7), and the NaN-gated `ElectronClosure` constants.
+ - `Islands.Moments` — `J̄_∥` assembly and the `Δ_cos`/`Δ_sin` Ampère
+ projections (`01 §4`) with **required, gated** prefactors; `Ω`-average and
+ channel-split diagnostics.
+ - `Islands.Solvers` — matrix-free Newton–Krylov (ForwardDiff JVP + GMRES,
+ Eisenstat–Walker forcing), the TSVD-regularized physics-block preconditioner
+ skeleton (`04 §3, §5`), tiny-grid dense debug Jacobian, and pseudo-arclength
+ continuation with fold detection.
+ - `Islands.Configure` — the Level-0 named-configuration assembly (`03 §2`):
+ wires the cleared `Coefficients.*` builders onto the operator stack (`c_D`,
+ the pitch-collision shapes, the `Δ` prefactors) and supplies the still-gated
+ coefficient families (QUESTIONS Q5) as named inputs, producing a runnable
+ `IslandStack` + far-field BCs + `Δ` prefactors.
+ - `Islands.Verify` — MMS/JVP harness (ladder A1/A2), solve-level MMS and
+ zero-drive configurations (A5), and the `y_c`-block conditioning monitor
+ (A8), exercised by `test/runtests_islands_{grids,operators,solve,configure}.jl`.
+
+Structural gates **A1–A5, A7 (coefficient-free part), A8** run in CI. The
+physics numbers (drift-frequency coefficients, collision kernels, closure
+constants, the Δ prefactors, threshold values) remain `[VERIFY]`-gated: the
+B-ladder benchmarks in `benchmarks/islands/` ship skipped, each naming the
+`QUESTIONS.md` entries (Q2–Q4) whose human clearance un-gates it. The
+literature-facing physics gates are **tiered by reproducibility** (Decision D9,
+docs/05): scalings, regime trends, and internal differentials are the primary
+quantitative checks, and absolute literature numbers are audit-gated (reported
+only with input manifests). The Paper-I figure contract is
+`docs/src/islands/papers/paper-1/OUTLINE.md`.
+
+## API Reference
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands]
+```
+
+### Phase-space grids (`Islands.PhaseSpace`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.PhaseSpace]
+```
+
+### Species (`Islands.SpeciesLists`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.SpeciesLists]
+```
+
+### Frames and parameters (`Islands.Frames`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Frames]
+```
+
+### Operator stack (`Islands.Operators`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Operators]
+```
+
+### Field-equation closure structure (`Islands.Fields`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Fields]
+```
+
+### Output moments (`Islands.Moments`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Moments]
+```
+
+### Cleared physics coefficients (`Islands.Coefficients`)
+
+Human-cleared Level-0 coefficient builders (the M2b derivation-lane fill-ins;
+see the Derivations section).
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Coefficients]
+```
+
+### Newton–Krylov solve (`Islands.Solvers`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Solvers]
+```
+
+### Level-0 configuration assembly (`Islands.Configure`)
+
+Wires the cleared `Coefficients.*` builders onto the operator stack and supplies
+the still-gated coefficient families (QUESTIONS Q5) as named inputs; produces the
+runnable `IslandStack` + far-field BCs + `Δ` prefactors.
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Configure]
+```
+
+### Verification harness (`Islands.Verify`)
+
+```@autodocs
+Modules = [GeneralizedPerturbedEquilibrium.Islands.Verify]
+```
diff --git a/docs/src/islands/LOG.md b/docs/src/islands/LOG.md
new file mode 100644
index 000000000..8a5715250
--- /dev/null
+++ b/docs/src/islands/LOG.md
@@ -0,0 +1,348 @@
+# Islands — session LOG
+
+Cross-session memory spine (design doc `06 §2.5`). Read this at session start
+together with `QUESTIONS.md`. Append a short entry before every session end:
+**what moved / what's blocked / next action**. Newest entries at the top.
+Reference `QUESTIONS.md` IDs (`Q`) and ladder IDs (`A1`, `B5a`, …) where
+relevant.
+
+---
+
+## 2026-07-12 — Q5: clear the gradient drive (far-field BC, no frame convention)
+
+- **Moved**: completed the gradient drive by re-reading I19 Eq. 29 first-hand.
+ **Correction**: the earlier draft misread the ratio as `ω_si^T/ω_ci` (⇒ frame
+ convention); it is `ω_si^T/ω_si = 1+(v̂²−3/2)η_i` — a **temperature factor, not
+ a frequency ratio**, so **no frame convention is needed**. The drive is the
+ standard neoclassical `p_φ F'_Mi`, imposed as the **far-field BC** (master eq
+ homogeneous, I19 Formulation A). Cleared: `Operators.GradientDrive = 0` and
+ `Configure.gradient_far_field` builds `g_far = x L̂_{n0}⁻¹[1+(E−3/2)η_i]`
+ (`Φ̂_far = 0`), with a new `Level0Physics.eta_i`. Both `gradient_drive` and
+ `far_field` moved gated→cleared; `drive`/`bc` dropped from `GatedLevel0Inputs`.
+ 1511 islands assertions green (the assembly now solves with the *physical* far
+ field). `gradient-drive.md` signed off; docs/01 §2, QUESTIONS Q5, numerics.md,
+ index/nav updated.
+- **Blocked**: only three kinetic families remain gated (Q5): the `E×B` coupling
+ `c_E`, the collision magnitude `⟨ν̂_ii⟩_u`, and the orbit-averaged pitch
+ measure. Plus the deferred `k ≃ −1.173`.
+- **Next**: E×B coupling (Poisson-bracket normalization) or the collision
+ magnitude `⟨ν̂_ii⟩_u` (needs L23 Eq. 4.1.6). With these three, the L0 solve is
+ fully physical.
+
+## 2026-07-11 — Q3/Q5: clear the passing fraction f_p
+
+- **Moved**: signed off `derivations/passing-fraction.md` and cleared
+ `Coefficients.passing_fraction(ε) = 1 − 1.4624√ε` (the effective
+ trapped-fraction coefficient, derived + numerically confirmed, = the sources'
+ quoted 1.46 to 3 s.f.). Authorizes `Fields.ElectronClosure.f_p`. 1499 islands
+ assertions green (limits + monotonicity + the 1.46 match); docs green.
+ docs/01 §2.4, QUESTIONS Q3/Q5, derivations index/nav updated.
+- **Blocked**: the companion Hirshman–Sigmar `k ≃ −1.173` stays deferred (needs
+ the parallel-viscosity moment problem). Gradient-drive amplitude + frame
+ convention still pending (bundled sign-off, `gradient-drive.md`).
+- **Next**: the gradient-drive amplitude/frame convention is the last structural
+ blocker; then E×B, collision magnitude, pitch measure. `k` its own derivation.
+
+## 2026-07-11 — Q5: gradient-drive structural finding (drive = far-field BC)
+
+- **Moved**: read I19 first-hand (Eqs. 8, 23–32) to derive the gradient drive.
+ **Finding** (`derivations/gradient-drive.md`, draft): the master equation
+ (I19 Eq. 32) is **homogeneous** — no interior source; the drive is the
+ **far-field boundary condition** `Ḡ₀ → p_φ(ω_si^T/ω_ci)(n'/n)F_Mi` (Eq. 29).
+ So `Operators.GradientDrive = 0` at Level 0, and the Q5 `gradient_drive` and
+ `far_field` items **merge** into one object — the diamagnetic far field
+ `g_drive = D_dia·x·[1+(v̂²−3/2)η_i]·F_Mi`. The `x`-linearity, temperature
+ correction, and Maxwellian are cleared structure; the **normalized amplitude
+ `D_dia` bundles the frame convention** `Frames.C_dia` (NaN-gated) — clearing
+ the drive = clearing `C_dia` (ion `ω_dia` normalization). Nothing entered
+ `src/` (draft, docs-only). QUESTIONS Q5 + derivations index/nav updated.
+- **Blocked**: `D_dia` + the frame convention `C_dia`/`sign_omega0`/
+ `C_gradient_shift` (Q3) — a bundled human sign-off; the normalized ion
+ diamagnetic amplitude needs the careful `ω_si/ω_ci` normalization algebra.
+- **Next**: complete `D_dia` (ion `ω_dia` in the code normalization) + sign off
+ the frame convention, then wire `g_far` and set `GradientDrive = 0`. This is
+ the last structural blocker for a real `g` to develop.
+
+## 2026-07-11 — Q5: clear the parallel (island) streaming coefficients
+
+- **Moved**: re-derived the island-streaming channel from the master DKE
+ (I19 Eq. 32) — `derivations/parallel-streaming.md`, **human sign-off**. Key
+ result: the two coefficients factor **exactly** into `{Ω, g}` flux-surface
+ advection, `a_ξ = (L̂_q⁻¹/ρ̂_θi)x Θ`, `a_x = −(L̂_q⁻¹ŵ²/4ρ̂_θi)sinξ Θ` (passing-
+ only via `Θ(y_c−y)`) — a coefficient-free structural check that leaves no
+ freedom. Normalization chosen (÷ −m ρ̂_θi) to keep the cleared `c_D = ω̂_D`
+ untouched. Implemented as `Configure.streaming_coefficients` with a new
+ `Level0Physics.rho_hat_theta_i`; `:streaming` moved from gated to cleared;
+ `a_xi`/`a_x` removed from `GatedLevel0Inputs`. **physics-verifier PASS**; 1494
+ islands assertions green (incl. a per-node `{Ω,g}` structure test);
+ `build_docs_local.jl` green. Doc-first: docs/01 §2, QUESTIONS Q5,
+ numerics.md §2/§8 amended.
+- **Blocked**: nothing new. Cleared now: streaming, drift, collision *shapes*,
+ quasineutrality field, Δ prefactors. Still gated (Q5): `E×B` coupling, gradient
+ drive, collision magnitude `⟨ν̂_ii⟩_u`, orbit-averaged pitch measure, far field.
+- **Next**: the gradient drive + far field are the remaining structural blockers
+ for a real `g` to develop (the drive is the source; the far field is the BC).
+ `f_p` sign-off still pending. Same rhythm: derive → present → sign off → clear.
+
+## 2026-07-11 — Q5 field fix: wire the cleared quasineutrality closure (Φ now driven)
+
+- **Moved**: closed the M2c-surfaced QN **structural gap** (QUESTIONS Q5). The
+ quasineutrality closure was signed off in M2b but the operator carried only
+ `R_Φ = M[g] − αΦ` (no drive), so the Level-0 potential collapsed to zero.
+ Implemented the full cleared closure: `Operators.Quasineutrality` gained an
+ optional `source` field; `Configure.configure_level0` now builds
+ **`α = (τ+1)/τ`** (= `1/quasineutrality_coefficient(τ)` — the reciprocal, =2 at
+ τ=1) and the drive **`S = L̂_{n0}⁻¹(x − ĥ(Ω))`** (`Configure.quasineutrality_source`,
+ from the cleared `h_amplitude`/`h_profile`, one width `w=w_psi` for both `Ω`
+ and the `ĥ` prefactor per `electron-closure.md §3`). Added `inv_Ln0` to
+ `Level0Physics`; removed `alpha` from the gated inputs (`quasineutrality` moved
+ to `cleared`). **Verified: max|Φ| ≈ 5.7 after solve** (was ~0). 194 islands
+ tests green; **physics-verifier PASS** (α reciprocal, source sign, width
+ convention all checked vs the signed-off derivation); `build_docs_local.jl`
+ green. Doc-first: docs/01 §3, `quasineutrality-closure.md §6`, numerics.md,
+ QUESTIONS Q5 all amended.
+- **Blocked**: nothing new. The *kinetic* Q5 families (streaming, E×B, gradient
+ drive, `⟨ν̂_ii⟩_u`, pitch measure, far field) remain gated — the field equation
+ is now the fully cleared closure, but a physics threshold still needs those.
+- **Next**: (human/next lane) the remaining Q5 kinetic clearances — the
+ parallel-streaming coefficients are the highest-leverage next (they + a far
+ field would let a real `g` develop). `f_p` sign-off still pending
+ (`passing-fraction.md`). The B-ladder scaffolding is wired to light up as each
+ clears.
+
+## 2026-07-11 — M2c: L0 configuration assembly + input-completeness audit (autonomous)
+
+- **Moved (M2b lane complete → M2c started)**:
+ - **Derivation lane 6/6 cleared** (earlier this session): ψ̃, ω̂_D + drift
+ toggle, collision operator, h(Ω) closure, quasineutrality, Δ prefactors — all
+ human-signed-off and in `src/` via `Coefficients.*`/`Moments.*` (recorded in
+ docs/01 + `derivations/`). Re-derivation caught the I19 ψ̃ published typo, the
+ collision low-v limit error, and the quasineutrality δn normalization.
+ - **Input-completeness audit** (Decision D9 deliverable): new
+ `docs/09-input-manifests.md` — per-source manifests (I19, D21, D23a/b, L23).
+ Headline: I19's own run collisionality is contradictory (0.01 vs 10⁻³) and its
+ Δ′ unspecified, so B5a's absolute threshold is only a T3 (existence) target;
+ **L23 (thesis) is the only clean T4 candidate**. Itself a reproducibility
+ result (Paper-I C9). Nav-wired.
+ - **M2c goal prompt** authored (`design/M2c-launch-prompt.md`): L0 assembly +
+ audit + docs/07 infra, autonomous-mode (un-gate nothing, escalate to
+ QUESTIONS, never guess).
+ - **L0 configuration assembly** (`src/Islands/configure/Configure.jl`,
+ `configure_level0`): wires the **cleared** coefficients onto the operator
+ stack — `c_D` node-for-node from `magnetic_drift_frequency` (verified Δ=0.0,
+ with the `:improved` toggle and forbidden-region zeroing), the pitch-collision
+ shapes from `pitch_diffusivity`/`deflection_frequency`, the Δ prefactors from
+ `delta_moment_prefactors`. Everything uncleared is a **supplied gated input**
+ (`GatedLevel0Inputs`); `level0_placeholders` gives documented non-physics
+ values so the assembled stack **converges structurally** (verified: 5 Newton
+ iters, ‖F‖=1.3e-9). 184 islands tests green (new `runtests_islands_configure.jl`);
+ the y=0 orbit-average guard was relaxed (`y>0`→`y>=0`, a domain-boundary fix,
+ no y>0 value changes). **Physics-verifier PASS** on the diff.
+ - **docs/07 STATE dashboard** (M2c #3a): `Verify.write_state_dashboard` +
+ `ladder_status` generate `docs/src/islands/state/STATE.md` (auto-gen header,
+ do-not-hand-edit) — the docs/05 ladder as a status table (8 A-ladder rows
+ green, B/C physics rows gated on QUESTIONS). Nav-wired.
+ - **B-ladder scaffolding** (M2c #4): `benchmarks/islands/benchmark_B{2,4,5}*.jl`
+ wired to `configure_level0` with a one-line un-skip (`const UNGATED = true`),
+ kept skipped on QUESTIONS Q3/Q5. B5 carries the full T2 toggle scaffold.
+ - **Anchor-sync check** (M2c #3b, docs/07 §1.1): `Verify.check_anchor_sync`
+ enforces the bidirectional operator↔docs sync — every `AbstractTerm` operator
+ named by an `Implemented by:` marker in `numerics.md` (forward), every marker
+ symbol resolving to a real Islands binding (reverse). numerics.md §8 gained
+ the as-implemented assembly section + `Implemented by:` markers. Tested with
+ negative controls (a missing operator ⇒ undocumented; a bogus symbol ⇒
+ dangling). 189 islands tests green; `build_docs_local.jl` green.
+ - **Deferred-constant draft** (M2c #5): `derivations/passing-fraction.md`
+ `[DERIVED]` derives the electron-closure passing fraction `f_p ≃ 1−1.46√ε`
+ from the effective trapped-fraction integral and **numerically confirms** the
+ coefficient (`1.4624`, = quoted `1.46` to 3 s.f.). **Drafted, awaiting
+ sign-off** — does NOT clear `Fields.ElectronClosure.f_p` (stays NaN-gated).
+ One open reviewer item (I19 Eq. 22's f_p definition). `⟨ν̂_ii⟩_u` and the
+ Hirshman–Sigmar `k` left escalated (need specific source integrands) — not
+ drafted speculatively.
+- **Blocked (escalated → QUESTIONS Q5)**: the L0 assembly surfaced that several
+ operator-stack coefficient families are **not yet cleared** — parallel
+ streaming (`a_xi`/`a_x`), `E×B` `c_E`, gradient drive, the collision magnitude
+ `⟨ν̂_ii⟩_u`/`ν_★`, the orbit-averaged pitch measure, and the neoclassical far
+ field — plus a **structural gap**: the quasineutrality operator lacks the
+ `L̂_{n0}⁻¹(x−ĥ)` field source the cleared closure requires (and its α is the
+ reciprocal of `quasineutrality_coefficient`), so no Level-0 *physics* run is
+ possible until that lands. This is why M2c delivers the assembly **scaffold**,
+ not a physics result. These need a second derivation lane (an "M2d",
+ human-present) run like M2b.
+ Autonomous M2c is now complete (#1 assembly, #2 audit, #3 docs infra [STATE +
+ anchor-sync], #4 B-ladder scaffolding, #6 as-implemented numerics.md; all
+ green, physics-verifier PASS on the assembly).
+- **Next**: (human) work **Q5** — clear the remaining coefficient families and fix
+ the QN operator structure (doc-first: amend docs/01 §3 + docs/03 §2). That is
+ the only thing gating a Level-0 *physics* run; it un-gates the B-ladder T2/T3
+ gates (scaffolding, STATE dashboard, anchor-sync all already wired). Then #5
+ (deferred sub-constants ⟨ν̂_ii⟩_u/k/f_p) is a focused sign-off session like M2b.
+ When the full as-implemented Physics Book chapters (docs/07 §1.1) are scoped,
+ point the operators' anchors there; `Verify.check_anchor_sync` already enforces
+ the sync against `numerics.md` today. The M2c goal prompt is re-entrant.
+
+## 2026-07-11 — Re-scope verification targets: tiered by reproducibility (Decision D9)
+
+- **Moved**: user flagged that absolute literature numbers (w_c ≃ 2.76 ρ_θi ≡
+ 8.73 ρ_bi, 0.45 ρ_θi ≡ 1.46 ρ_bi, the kokuchou 0.440… fit, the −0.89 ω_dia,e
+ reversal, the D23a shaping widths) were quoted as if they were pass/fail
+ targets — but reproducing an absolute number needs *every* input of the
+ source's exact scenario, which the lineage under-specifies (B5a's own
+ collisionality is internally contradictory). Direction: qualitative/scaling
+ checks (the Park 2022 / Burgess 2026 modality) are the real physics gates.
+- **Decision D9** (adopted, docs/00): a **four-tier target taxonomy** written
+ into docs/05 ("Target tiers and reproducibility"): T1 exact math / T2 internal
+ cross-checks & toggle differentials (the sharpest quantitative claims) / T3
+ scalings-trends-existence vs. literature (primary literature-facing gates) /
+ T4 absolute reproduction — **audit-gated**, never pass/fail without an *input
+ manifest*, downgraded to T3 where the source is under-specified. Added a fifth
+ triage outcome ("under-specified source configuration") and three reporting
+ rules (publish the manifest; prefer differentials/ratios; sensitivity scans).
+- **Applied** across docs/05 (every B/C row retagged; A7 constants marked T1),
+ docs/00 (Level-0 gate softened, D9 logged), the Paper-I OUTLINE (C5–C7
+ reframed scaling-first; new C9 = the input-completeness audit as a methods
+ deliverable), the three B-benchmark scripts + README (tier-labeled headers),
+ the M2b prompt (new deliverable: per-source input manifests in a `docs/09`
+ audit; B-ladder DoD = T2 differential + T3 scalings, T4 only with manifests),
+ QUESTIONS (B5a collisionality reframed as the audit type specimen), and the
+ numerics chapter / islands.md status. Docs-only; no `src/` or test changes.
+- **Why it strengthens the project**: T2 internal differentials give *sharper*
+ claims than absolute matches (we control both sides); the input audit is
+ itself publishable reproducibility content; and it aligns the ladder with the
+ SLAYER-validation precedent Islands models itself on.
+- **Next**: unchanged — M2b derivation lane, now with the input-completeness
+ audit folded into its DoD.
+
+## 2026-07-08 — M2 L0 solve machinery: Newton–Krylov + moments + species/frames/fields (structure, gated physics)
+
+- **Contract**: `docs/src/islands/design/M2-launch-prompt.md` (interactive /goal
+ run). Branch `feature/islands-m2`, **PR #324** (stacked on
+ `feature/islands-m1`/PR #320; retargets to `feature/islands` when #320 merges).
+ Full suite green locally.
+- **Moved**: the full L0 solve *structure*, every physics coefficient a supplied
+ `[VERIFY]`-gated parameter (physics-verifier: **PASS**):
+ - `solvers/` — matrix-free Newton–Krylov (Krylov.jl GMRES on a preallocated
+ ForwardDiff JVP; Eisenstat–Walker; line search; convergence on norm AND
+ max-norm per `04 §5`), `YBlockJacobi` physics-block preconditioner with
+ TSVD-regularized pencil solves (the `04 §3` y_c treatment), dense tiny-grid
+ debug Jacobian, pseudo-arclength continuation with fold detection (toy fold
+ found at step 6 of the test problem).
+ - `species/` (D3 plumbing), `frames/` (conversion forms, NaN-gated
+ `FrameConvention`), `fields/` (Q(Ω)/h(Ω) structure + NaN-gated
+ `ElectronClosure`), `moments/` (J̄_∥, Δ projections with required gated
+ prefactors, ⟨·⟩_Ω diagnostics), operators additions (mimetic
+ `PitchAngleDiffusion`, `FarFieldConditions` — never bare Neumann,
+ `weighted_moment!`).
+ - **Structural gates green** (67 new tests in `runtests_islands_solve.jl`):
+ A5 (residual exactly 0 at g≡0), solve-MMS at design order (3.98 observed,
+ nx 17→33), A4 (conservation ≲1e-11, entropy sign exact), A3 parity, A7
+ ⟨∂²h/∂x²⟩_Ω ≈ 1e-16, A8 σ_min monitor + singular detection. Preconditioner
+ cuts a stiff collisional solve from 79.5 s/28 Newton to 0.6 s/7 (GMRES
+ 1700→200-class); all new kernels pass `--check-bounds=yes`.
+ - `benchmarks/islands/` created: B2/B4/B5 scripts **skipped**, each naming its
+ gating QUESTIONS IDs; `regression-harness` case `islands_l0_structural`
+ (solve-MMS err 5.254e-2, 6 Newton/1210 GMRES, A7 8.0e-17, σ_min 0.1139).
+ - Paper-I figure contract: `docs/src/islands/papers/paper-1/OUTLINE.md`
+ (claims C1–C3 green as CI artifacts; C4–C8 gated on Q2–Q4).
+ - **Rendered docs story** (user-flagged gap vs docs/07's M0–M1 intent): new
+ `docs/src/islands/numerics.md` — the equations + figures of everything as
+ implemented — plus the pinned figure script
+ (`benchmarks/islands/figures/make_structural_figures.jl`, five structural
+ figures committed as docs assets) and a full "Islands" site-nav section
+ (overview, numerics chapter, Paper-I contract, design docs 00–08).
+ Remaining docs/07 infra for later milestones: anchor-sync CI check,
+ STATE.md dashboard.
+- **Physics debugging note**: the first solve-MMS attempt failed to converge —
+ the generic `Collisions` (a_y ∂²y) term has no y-BCs, so its BVP
+ discretization is unstable under refinement; the *mimetic* divergence form
+ (degenerate P → 0 endpoints, zero-flux built in) is the correct structure and
+ the far-field x-BCs are what make the advective solve well-posed. Exactly the
+ design's point (`01 §3`, `04 §1`).
+- **Blocked**: the York gates (B5a/b/c, B2, B4) and Paper-I claims C4–C8 — all
+ on the human clearance queue **Q2/Q3/Q4** (unchanged).
+- **Next**: human clears Q2–Q4 → thin run fills the L0 coefficients from the
+ D7 re-derivation and un-skips the B-ladder; independent M2+ work: kinetic-
+ electron toggle (E4), io/ TOML section, trace-species linear pass.
+
+## 2026-07-08 — M1 skeleton: phase-space grids + operator stack + MMS/AD harness
+
+- **PR**: #320 (`feature/islands-m1` → `feature/islands`); full suite green.
+- **Moved**: Landed the M1 core (design `03 §1–2`, `04`, ladder `A1/A2`). Three
+ `src/Islands/` submodules, all structure-only (no `[VERIFY]` physics numbers):
+ - `phasespace/PhaseSpace.jl` — the `(x, ξ, y, E, σ)` grids with layer-clustered
+ maps: Fourier spectral `∂ξ`, Fornberg high-order FD `∂x`/`∂y` on `sinh`-stretched
+ grids (window sized per-derivative so `D1`/`D2` are both 4th-order incl.
+ boundaries), composite-Simpson quadrature weights, Gauss–Laguerre energy nodes.
+ - `operators/Operators.jl` — `AbstractTerm` + `apply!` + `residual!`; the term
+ structs of `03 §2` (`ParallelStreaming`, `MagneticDrift` with the
+ `:original/:improved` toggle, `ExBDrift` as the `(x,ξ)` Poisson bracket,
+ `Collisions`, `GradientDrive`, `PerpTransport`/`RadiationSink` L4 stubs,
+ `Quasineutrality` field residual). Every physics coefficient is a **supplied
+ data field** — no literal in `src/`. Allocation-free, AD-generic.
+ - `verify/Verify.jl` — manufactured-solution + AD-vs-FD JVP harness.
+ - Tests `test/runtests_islands_{grids,operators}.jl` (wired into `runtests.jl`):
+ A1 per-operator MMS → 4th order for `∂x/∂y` terms, machine-precision for the
+ `∂ξ` term; assembled kinetic residual → 4th order; A2 JVP-vs-FD agree to ~6e-9;
+ **allocation regression = 0 bytes** for every `apply!` and `residual!`. All
+ 53 Islands tests green. Added `ForwardDiff` to `Project.toml` (design `04 §9`).
+- **physics-verifier**: PASS — audited all six new/changed files, no
+ `[VERIFY]`-policy violation; the flagged literature numbers (8.73/1.46 ρ_bi,
+ k=−1.173, …) appear only in docstring prose, never assigned to a coefficient.
+- **Blocked**: nothing. **Q1 RESOLVED**: julia is at
+ `/mnt/homes_global/ncl2128/software/julia-1.11.7/bin/julia`; must be run with
+ `env -u LD_LIBRARY_PATH` (OMFIT contamination). Used it to run the suite here.
+- **Next**: M2 — wire moments (`Δ_cos`, `Δ_sin`), `frames/`, `species/`, and the
+ Newton–Krylov solver toward the L0 single-species solve; every physics
+ coefficient stays `[VERIFY]`-gated with a skipped benchmark until cleared.
+
+## 2026-07-08 — Harden Stop hook against OMFIT LD_LIBRARY_PATH contamination
+
+- **Moved**: Diagnosed why the Stop hook's package-load check fails on this box.
+ A loaded OMFIT module (`module load omfit/unstable`) leaks
+ `LD_LIBRARY_PATH=/mnt/codes/atom/mambaforge/envs/omfit/lib:` into the session;
+ those conda libs shadow Julia's bundled artifacts, giving `undefined symbol`
+ errors in CHOLMOD and the Plots/Cairo/GR native stack (and the ubiquitous
+ `libtinfo.so.6` bash warning). Not a code issue — CI is green, and
+ `env -u LD_LIBRARY_PATH julia … using GeneralizedPerturbedEquilibrium` loads
+ clean (exit 0). Fixed `stop-check.sh` to run the build check with
+ `env -u LD_LIBRARY_PATH` (no-op on a clean shell / CI). Repo deps were
+ instantiated here; the shared depot (`/mnt/codes/ncl2128/.julia`) is populated.
+- **Blocked**: nothing new. This is the concrete shape of **Q1** — the
+ automation shell must invoke julia with a clean `LD_LIBRARY_PATH` (unload
+ OMFIT, or unset the var) or the overnight loop's *actual* gpec runs fail the
+ same way, not just the hook.
+- **Next**: (human) launch the loop from a shell without the OMFIT module
+ (`module unload omfit`); hook hardening is defense-in-depth on top of that.
+
+## 2026-07-08 — Fix invalid deny rules in `.claude/settings.json`
+
+- **Moved**: `/doctor` flagged two skipped permission-deny rules —
+ `Bash(git push:* main)` / `Bash(git push:* develop)` — invalid because `:*`
+ (prefix match) is only allowed at the end of a pattern. Rewrote them with a
+ mid-pattern wildcard (`Bash(git push* main)` / `Bash(git push* develop)`) so
+ they load and again deny pushes to `main`/`develop` for any remote/flags.
+- **Blocked**: nothing.
+- **Next**: unchanged — pending items are the Phase A bootstrap **Next** below.
+
+## 2026-07-08 — Phase A bootstrap (supervised)
+
+- **Moved**: Created the `Islands` submodule skeleton (`src/Islands/Islands.jl`,
+ empty `module Islands`) and wired it into `src/GeneralizedPerturbedEquilibrium.jl`
+ (`include` + `import . as` + `export`, last submodule slot before `Rerun.jl`).
+ Stood up this `LOG.md` and `QUESTIONS.md`, the `.claude` unattended-run
+ guardrails, the `physics-verifier` subagent, and the M1 launch prompt.
+- **Landed CI-green** on `feature/islands` (PR #318): both `runtests` jobs pass
+ (the wiring is valid — the package loads and the full suite passes) and the
+ docs build passes. One fix was needed en route: the exported `Islands` module
+ docstring required a manual page under `checkdocs=:exports`, so
+ `docs/src/islands.md` (an `@autodocs` block) was added and wired into
+ `docs/make.jl` (repo-root CLAUDE.md docs-coverage rule).
+- **Blocked**: `julia` is not on the automation shell's PATH (no module, not in
+ `$HOME`) → changes could not be run locally; CI is the only Julia validation
+ here. See **Q1** — the overnight loop's scratch-clone environment must expose
+ `julia` or it cannot run tests / meet M1's definition-of-done.
+- **Next**: (human) resolve Q1 + one supervised `dontAsk` dry-run of the hooks,
+ then launch the overnight loop on milestone **M1** (design `00 §M1`) —
+ phase-space grids + operator-stack skeleton + MMS/AD harness (ladder A1, A2),
+ no `[VERIFY]` physics coefficients.
diff --git a/docs/src/islands/QUESTIONS.md b/docs/src/islands/QUESTIONS.md
new file mode 100644
index 000000000..74f79f4fc
--- /dev/null
+++ b/docs/src/islands/QUESTIONS.md
@@ -0,0 +1,217 @@
+# Islands — blocker queue (QUESTIONS)
+
+Append-only non-blocking escalation queue (design doc `06 §2.2`). When blocked
+on anything the CLAUDE.md forbids guessing — `[VERIFY]` clearances, physics
+coefficients, signs, normalizations, convention/doc contradictions, or a tooling
+prerequisite — write an entry here **and switch to the next unblocked task**.
+Never stall waiting for a human; never resolve silently.
+
+Each entry:
+- **ID**: `Q` (monotonic). Commits/PRs reference the IDs they were blocked on
+ or unblocked by.
+- **Status**: `OPEN` / `RESOLVED (by , )`.
+- **Context**: what you were doing.
+- **Question**: the specific thing a human must decide.
+- **Options**: the alternatives considered.
+- **Recommendation**: your best guess (not acted on until cleared).
+- **Gated work**: what is blocked until this resolves.
+
+The human's recurring job is clearing this queue (and `[VERIFY]` tags), not
+supervising sessions.
+
+---
+
+## Q1 — Julia not on the automation shell PATH — RESOLVED (by Claude, 2026-07-08)
+
+- **Context**: Phase A bootstrap. Verifying the `Islands` module skeleton loads
+ (`using GeneralizedPerturbedEquilibrium`) and running the test suite requires
+ `julia`, but it was not on the non-interactive shell's PATH (no `julia` module,
+ none in `$HOME`; other users have installs under `/mnt/homes*/…/julia*`).
+- **Question**: What is the canonical `julia` invocation for automation on this
+ cluster, and will the overnight loop's scratch-clone environment expose it?
+- **Resolution**: the `ncl2128`-owned install is at
+ `/mnt/homes_global/ncl2128/software/julia-1.11.7/bin/julia` (option (b): an
+ absolute path to a user-owned binary), and it is on this session's PATH. The
+ M1 run used it to build and run `test/runtests.jl` locally. The **only caveat**
+ is the OMFIT `LD_LIBRARY_PATH` contamination already documented in LOG
+ (2026-07-08): the binary must be invoked with a clean loader path —
+ `env -u LD_LIBRARY_PATH /mnt/homes_global/ncl2128/software/julia-1.11.7/bin/julia
+ --project=. …` — or the conda libs shadow Julia's bundled artifacts. The Stop
+ hook already applies `env -u LD_LIBRARY_PATH`; the overnight loop's launch
+ script must do the same for its own gpec runs.
+- **Gated work (now unblocked)**: local verification of every Julia change; the
+ overnight loop's ability to run tests / meet its definition-of-done.
+
+## Q2 — Ratify Decisions D7 and D8 — RESOLVED (by the user, 2026-07-08)
+
+- **Resolution**: both ratified as written (option (a)); recorded in the
+ `docs/00` Decision Log. D7 additionally carries the user's clearance-mode
+ choice for Q3: **re-derivation first** — the L0 coefficient set is cleared by
+ human sign-off of in-repo derivations, not literature transcriptions.
+
+- **Context**: M2 setup. The L0 equation set and benchmark targets rest on two
+ Decision-Log proposals (`docs/00`) that are dated 2026-07-07 and still marked
+ "needs human ratification". Until ratified, M2 cannot fix the L0 physics form or
+ pin its benchmark tolerances.
+- **Question**: Ratify (or amend) **D7** — implement Level-0 physics from an
+ *independent re-derivation* cross-checked against the L23-amended equation set,
+ treating I19 Eq. (A.1) as printed as known-errata (L23 §2.6), with ω_E a scanned
+ input from day one — and **D8** — pin the benchmark grid as the three-code
+ triangle (DK-NTM / RDK-NTM / kokuchou) with B5a/b/c configs, superseding the
+ single "York thresholds" gate item.
+- **Options**: (a) ratify both as written; (b) ratify with amendments; (c) direct a
+ different L0 derivation strategy.
+- **Recommendation**: ratify both — they encode the project's core anti-guessing
+ thesis (published O(1) coefficients in this lineage are demonstrably wrong) and
+ the benchmark structure the ladder already assumes.
+- **Gated work**: pinning any L0 physics coefficient; the York gates B5a/b/c; the
+ Paper-I physics claims.
+
+## Q3 — Clear the Level-0 coefficient set ([CHECKED] → human sign-off) — OPEN (mode decided)
+
+- **Mode decision (user, 2026-07-08, via Q2/D7)**: option (b) — **re-derivation
+ first**. The next milestone (M2b) produces independent derivations of each
+ item below in `docs/src/islands/derivations/` (marked `[DERIVED]`, with a
+ cross-check table against the `[CHECKED]` transcriptions and every
+ discrepancy flagged); the human then signs off the *derivations*, which
+ clears the corresponding coefficients. Items remain individually OPEN until
+ that sign-off.
+
+- **Context**: M2 builds the L0 solve machinery with every physics coefficient a
+ parameterized `[VERIFY]` stub. Reaching the York gates needs these `[CHECKED]`
+ (AI-transcribed, cited, but not human-signed-off) items cleared. `[CHECKED]` is
+ not permission to hardcode (`docs/01` header). Each is implemented structurally;
+ none has a value in `src/`.
+- **Question**: Check each against its source PDF and sign off (record paper + Eq./p.
+ in `docs/01`), or flag a discrepancy:
+ - `ω̂_D` magnetic-drift frequency + the `:original`/`:improved` `L̂_B⁻¹` toggle
+ `[CHECKED: I19 Eq. (32) def.; D21 Eqs. 15, B1; D21 Eq. A2, p. 16]` — drives the
+ 8.73 → 1.46 ρ_bi threshold shift.
+ - Pitch-angle collision kernel `[CHECKED: I19 Eqs. (9)–(12); Diss19 Eqs. 2.25–2.30;
+ WCHH96 Eq. (62)]` + the analytic velocity average
+ `⟨ν̂_ii⟩_u = (4ε^{3/2}ν_★/3√π)(√2 − ln(1+√2))` `[CHECKED: L23 Eq. 4.1.6, p. 88]`
+ + normalization `ν_★ = ν_jj Rq/(ε^{3/2}v_th)` `[CHECKED: L23 Eq. (2.3.40)]`.
+ - Electron-closure constants `k ≃ −1.173` (Hirshman–Sigmar) and `f_p ≃ 1 − 1.46√ε`
+ `[CHECKED: I19 Eq. (22); L23 Eqs. 2.5.5–2.5.8]`.
+ - Quasineutrality closure `e_iΦ̂/T_i = [δn̄_i/n₀ + x − ĥ(Ω)]/(2 L̂_{n0})`
+ `[CHECKED: I19 Eq. (A.11); L23 Eq. (2.4.14)]` and the Picard form
+ `δΦ̂ = (δn̂_i − δn̂_e)/2` `[CHECKED: Diss19 Eq. 2.45]`.
+- **Options**: (a) clear item-by-item after PDF check; (b) require an independent
+ re-derivation first (couples to Q2/D7) before clearing.
+- **Recommendation**: clear via re-derivation (per D7) rather than transcription
+ alone — L23 §2.6 documents concrete errors in the published set.
+- **Gated work**: populating any of these coefficients; the York/large-w/polarization
+ gates (B5a/b/c, B2, B4); the A7 number-bearing identities (`k`, `f_p`, `⟨ν̂_ii⟩`).
+
+## Q4 — Resolve open [VERIFY]s and acquire two missing sources — OPEN
+
+- **Context**: Independent of the `[CHECKED]` clearances above, three genuinely
+ open `[VERIFY]` items block pinning M2's moment normalization and B5a tolerance.
+- **Question**:
+ - `ψ̃` amplitude: is it `(w_ψ²/4)(q_s′/q_s)` (Diss19/D21/L23, dimensional analysis)
+ or `(w_ψ²/4)(q_s/q_s′)` (one I19 extraction)? `[VERIFY: check I19 as printed —
+ possible typo in the paper itself]` — sets the `Δ_cos/Δ_sin` prefactor.
+ - B5a run collisionality: I19 §4.2 states `ν_★ = 0.01`; L23 p. 82 quotes DK-NTM at
+ `ν_★ = 10⁻³`. **Reframed by Decision D9 (2026-07-11)**: this is no longer a
+ tolerance to pin but the **type specimen of the input-completeness problem**
+ — I19 is internally inconsistent about its own run, so its absolute w_c is a
+ T4 (audit-gated) target, not a pass/fail number. It is resolved *in the M2b
+ input manifest* (`docs/09`/audit), recording both values as the source's
+ ambiguity; the primary B5 gates (T2 toggle differential, T3 existence/trend)
+ do not depend on it.
+ - ~~Acquire WCHH96 and Park PoP 29 (2022)~~ **Both resolved (2026-07-09)**:
+ the user added WCHH96 (Wilson, Connor, Hastie & Hegna, PoP **3**, 248
+ (1996), doi:10.1063/1.871830) to the island reference library, and Park
+ 2022 was found already in-repo in the general `docs/resources/` dir (the
+ docs/08 island-subfolder map had missed it; map corrected). The user also
+ flagged **Burgess 2026** (`docs/resources/2026-Burgess-…Two-Fluid Slab
+ Layer.pdf`) as the methodological template: toroidal outer-region Δ′ +
+ SLAYER regime-generalized linear layer — Islands extends that inner region
+ from zero-width linear layers to finite-width islands (recorded in docs/08
+ as **B26**). Remaining open in Q4: the `ψ̃` amplitude check and the B5a run
+ collisionality.
+- **Options**: (a) user resolves from the source PDFs; (b) an independent
+ re-derivation pins `ψ̃` and the collisionality is recorded in the input manifest.
+- **Recommendation**: resolve `ψ̃` by re-derivation (a clean dimensional check, M2b);
+ record the B5a collisionality ambiguity in the input manifest rather than
+ "resolving" it — per D9 the absolute B5a value is audit-gated, not a gate.
+- **Gated work**: the `Δ_cos/Δ_sin` normalization (the `ψ̃` item); the T4 B5a/B2
+ absolute comparisons (the manifest items) — the T2/T3 primary gates are not
+ blocked by these.
+
+**Tier note (Decision D9, 2026-07-11):** verification targets are now tiered by
+reproducibility (docs/05 "Target tiers"). This changes what the Q3/Q4 clearances
+*buy*: a cleared coefficient set un-gates the **primary** T2 (internal
+differentials) and T3 (scalings/trends/existence) physics gates directly; the T4
+absolute literature comparisons additionally require the M2b input-completeness
+audit and are reported only with input manifests + sensitivity scans, never as
+bare pass/fail. The derivation lane inherits this framing.
+
+## Q5 — The remaining un-cleared Level-0 operator coefficient families (a second derivation lane) — OPEN
+
+- **Context**: M2c assembled `Configure.configure_level0` — the Level-0
+ named-configuration builder. Wiring the cleared coefficients onto the operator
+ stack surfaced that the M2b derivation lane cleared the coefficient families
+ that appear as *closures/moments* (`ω̂_D`, the collision `P`/`ν` shapes, the
+ quasineutrality scalar, the `Δ` prefactors), but **several operator-stack
+ coefficients are not yet a cleared family** and were left supplied/gated in
+ `Configure.GatedLevel0Inputs`. `ω̂_D` (`MagneticDrift.c_D`), the pitch
+ diffusivity shape, the deflection-frequency shape, and the `Δ` prefactors *are*
+ wired from cleared `Coefficients.*`; the items below are not.
+- **Question**: clear (by the D7 re-derivation-first route, `docs/derivations/`,
+ human sign-off) each remaining Level-0 operator coefficient:
+ - **Parallel-streaming** `a_xi`, `a_x` (`Operators.ParallelStreaming`):
+ **RESOLVED (2026-07-11)** — re-derived (`parallel-streaming.md`, signed off)
+ from I19 Eq. 32; the coefficients factor exactly into `{Ω, ·}` flux-surface
+ advection (a coefficient-free structural check). Implemented as
+ `Configure.streaming_coefficients` with a new `Level0Physics.rho_hat_theta_i`;
+ normalization chosen to keep the cleared `c_D = ω̂_D` unchanged.
+ - **`E×B` coupling** `c_E` (`Operators.ExBDrift`): the Poisson-bracket
+ normalization; entangled with the frames convention (Frames NaN-gated).
+ - **Gradient drive** `drive` + **far field** `bc` — **RESOLVED (2026-07-11**,
+ `gradient-drive.md`, signed off): reading I19 first-hand (Eqs. 28–32), the
+ master equation is **homogeneous** (no interior source); the drive is the
+ **far-field boundary condition** `Ḡ₀ → p_φ(ω_si^T/ω_si)(n'/n)F_Mi = p_φ F'_Mi`
+ (Eq. 29 — the ratio is `ω_si^T/ω_si = 1+(v̂²−3/2)η_i`, a **temperature factor,
+ not a frequency ratio**; an earlier reading of `ω_ci` was a misread, so **no
+ frame convention is needed**). Implemented: `Operators.GradientDrive = 0` and
+ `Configure.gradient_far_field` builds `g_far = x L̂_{n0}⁻¹[1+(E−3/2)η_i]`
+ (`Φ̂_far = 0` at `ω_E = 0`), with a new `Level0Physics.eta_i`. The
+ `gradient_drive` **and** `far_field` families are both cleared.
+ - **Quasineutrality closure — RESOLVED (2026-07-11).** The cleared closure
+ `Φ̂ = τ/(τ+1)[δn̄_i/n₀ + L̂_{n0}⁻¹(x−ĥ)]` (Q3, `quasineutrality-closure.md`,
+ signed off) is now implemented: `Operators.Quasineutrality` carries a
+ `source` field, and `Configure.configure_level0` builds the residual
+ `R_Φ = M[g] − α Φ̂ + S` with `α = (τ+1)/τ` (= `1/quasineutrality_coefficient(τ)`)
+ and `S = L̂_{n0}⁻¹(x−ĥ)` (`Configure.quasineutrality_source`, from the cleared
+ `h_amplitude`/`h_profile`). The Level-0 potential is now driven (was trivially
+ zero). docs/01 §3 records it; the derivation §6 was the authorization. **No
+ longer gates a Level-0 physics run.**
+ - **Collision magnitude** `nu_tilde`: the `⟨ν̂_ii⟩_u`/`ν_★` normalization scaling
+ the cleared `ν_{jj}(v̂)` shape — the deferred sub-constant already tracked in
+ Q3 (`⟨ν̂_ii⟩_u = (4ε^{3/2}ν_★/3√π)(√2−ln(1+√2))`, L23 Eq. 4.1.6). Still
+ escalated (needs the L23 integrand read in detail).
+ - **Passing fraction** `f_p ≃ 1 − 1.46√ε` (electron closure, Q3): **RESOLVED
+ (2026-07-11)** — `derivations/passing-fraction.md` signed off; cleared as
+ `Coefficients.passing_fraction(ε) = 1 − 1.4624√ε` (the effective
+ trapped-fraction coefficient, = quoted `1.46` to 3 s.f.), authorizes
+ `Fields.ElectronClosure.f_p`. The Hirshman–Sigmar `k ≃ −1.173` remains
+ escalated (needs the parallel-viscosity moment problem).
+ - **Orbit-averaged pitch measure** `B_profile`: the collision operator's `|B|`
+ on the `y`-grid is the *orbit-averaged* field (turning-point structure), not a
+ single local `B`; the cleared `pitch_diffusivity(λ,B)` is the local building
+ block. Clear the orbit-averaged measure form.
+ - **Neoclassical far field** `bc` (`Operators.FarFieldConditions`): the
+ no-island `g_far`/`Φ_far` (never bare Neumann — L23 §5.3), gated physics
+ already flagged under Q3.
+- **Options**: (a) a focused second derivation lane (an "M2d") clearing these
+ item-by-item like M2b, human-present; (b) clear the highest-leverage first
+ (streaming + the QN structural fix un-gate a genuine physics residual).
+- **Recommendation**: (a) — run it exactly like the M2b lane (re-derive →
+ physics-verifier → sign-off → clear into `Coefficients.*` / operator structure).
+ The QN structural gap is the highest priority: without the `(x−ĥ)` source the
+ Level-0 quasineutrality field is trivially `Φ = 0`, so no Level-0 *physics* run
+ is possible until it lands (the M2c assembly runs *structurally* on placeholders
+ only). **This is why M2c delivers the assembly scaffold, not a physics result.**
+- **Gated work**: any Level-0 *physics* solve (as opposed to the structural
+ convergence check); the B-ladder T2/T3 physics gates; the L23/B5c T4 attempt.
diff --git a/docs/src/islands/derivations/collision-operator.md b/docs/src/islands/derivations/collision-operator.md
new file mode 100644
index 000000000..30c7cf7ac
--- /dev/null
+++ b/docs/src/islands/derivations/collision-operator.md
@@ -0,0 +1,226 @@
+# Derivation — the Level-0 pitch-angle collision operator, deflection frequency, and ``\nu_\star`` normalization
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent re-derivation (Decision D7).
+**Clears (on sign-off):** the momentum-conserving pitch-angle (Lorentz)
+collision operator structure, the deflection-frequency velocity dependence
+``\nu_{jj}(v)`` with the Chandrasekhar/``v^{-3}`` sub-toggle, and the ``\nu_\star``
+normalization (`[CHECKED: I19 Eqs. 9–12; Diss19 Eqs. 2.25–2.30; WCHH96 Eq. 62]`,
+QUESTIONS Q3).
+**Deferred (flagged sub-items, §7):** the analytic velocity average
+``\langle\hat\nu_{ii}\rangle_u`` (L23 Eq. 4.1.6 — a separate short derivation),
+and the orbit-averaged/discretized diffusivity profile that feeds
+`PitchAngleDiffusion` (numerics, ties to the conservation gate A4).
+
+**Status:** ✅ **signed off 2026-07-11** (clearance recorded in docs/01 §2.3) for
+the operator structure, deflection frequency, and ``\nu_\star`` normalization —
+implemented as `Coefficients.pitch_diffusivity` and
+`Coefficients.deflection_frequency`. The ``\langle\hat\nu_{ii}\rangle_u``
+constant (§7) and the discretized diffusivity profile remain **deferred /
+gated**.
+
+## 1. Starting point and what must be shown
+
+At Level 0 the collision operator is the momentum-conserving **pitch-angle
+(Lorentz) model** (orderings O6; docs/01 §2.3). From the drift-kinetic equation
+(I19 Eq. 8) the like-species operator is (I19 Eq. 9, verified first-hand,
+print p. 4)
+
+```math
+C_{jj}(f) = 2\nu_{jj}(v)\left[\frac{\sqrt{1-\lambda B}}{B}\,
+ \frac{\partial}{\partial\lambda}\!\Big(\lambda\sqrt{1-\lambda B}\,
+ \frac{\partial f}{\partial\lambda}\Big)
+ \;+\; \frac{v_\parallel\,\bar u_{\parallel j}}{v_{thj}^2}\,F_{Mj}\right],
+```
+
+the ``\lambda``-derivatives taken at fixed ``\psi``, ``\lambda=\mu/\mathcal E`` the
+pitch, ``\mathcal E=v^2/2``. This derivation establishes three things: (i) the
+first bracket **is** the pitch-angle scattering operator, in self-adjoint
+(divergence) form, with diffusivity ``P(\lambda)=\lambda\sqrt{1-\lambda B}``;
+(ii) the velocity dependence ``\nu_{jj}(v)``; (iii) the ``\nu_\star``
+normalization. The second bracket is the momentum-restoring term (§6).
+
+## 2. The pitch-angle operator is Lorentz scattering in self-adjoint form
+
+The full Fokker–Planck test-particle operator, in the small-mass-ratio /
+dominant-deflection limit that defines the Lorentz model, reduces to pure
+pitch-angle scattering — diffusion of the velocity-vector *direction* at fixed
+speed. In the pitch cosine ``\xi_p=v_\parallel/v`` it is the standard Lorentz
+operator ``C=\tfrac{\nu_{jj}}{2}\,\partial_{\xi_p}\!\big[(1-\xi_p^2)\,
+\partial_{\xi_p}f\big]``. Change to the constant-of-motion pitch
+``\lambda=\mu/\mathcal E`` via ``1-\lambda B=\xi_p^2=v_\parallel^2/v^2`` (at
+fixed ``B,v``): then ``\partial_{\xi_p}=-\tfrac{2\sqrt{1-\lambda B}}{B}
+\partial_\lambda`` and ``1-\xi_p^2=\lambda B``, so
+
+```math
+\partial_{\xi_p}\!\big[(1-\xi_p^2)\partial_{\xi_p}f\big]
+ = \frac{4\sqrt{1-\lambda B}}{B}\,\frac{\partial}{\partial\lambda}\!
+ \Big(\lambda\sqrt{1-\lambda B}\,\frac{\partial f}{\partial\lambda}\Big),
+```
+
+and therefore
+
+```math
+C = 2\nu_{jj}\,\frac{\sqrt{1-\lambda B}}{B}\,
+ \frac{\partial}{\partial\lambda}\!\Big(\lambda\sqrt{1-\lambda B}\,
+ \frac{\partial f}{\partial\lambda}\Big) ,
+```
+
+**exactly** the first term of Eq. 9 — the ``2\nu_{jj}`` prefactor is the
+change-of-variables Jacobian, so the coefficient is not free. Two structural
+facts follow directly:
+
+- **Self-adjoint (divergence) form.** The operator is
+ ``w(\lambda)^{-1}\,\partial_\lambda[P(\lambda)\,\partial_\lambda]`` with
+ **diffusivity** ``P(\lambda)=\lambda\sqrt{1-\lambda B}\ge 0`` and **measure**
+ ``w(\lambda)=B/\sqrt{1-\lambda B}`` (so the prefactor is ``1/w``). This is
+ exactly the form the implemented mimetic operator
+ `Operators.conservative_pitch_operator` discretizes
+ (``K=-W_q^{-1}G^{\mathsf T}\mathrm{diag}(P\,w_q)G``), which is why particle
+ conservation and the entropy sign hold *exactly in floating point* (gate A4,
+ already green). This derivation identifies its physics profile:
+ ``P=\lambda\sqrt{1-\lambda B}``.
+- **Zero-flux endpoints.** ``P(\lambda)`` vanishes at ``\lambda=0`` (``v_\parallel
+ =v``, ``P=0``) and at ``\lambda=1/B`` (``v_\parallel=0``, ``\sqrt{1-\lambda B}=0``),
+ so the scattering flux ``P\,\partial_\lambda f`` vanishes at both ends — no
+ pitch boundary conditions are needed, matching the mimetic construction.
+
+## 3. The deflection frequency (velocity dependence)
+
+The speed dependence is carried entirely by ``\nu_{jj}(v)`` (I19 Eq. 11):
+
+```math
+\nu_{jj}(v) = \tilde\nu_{jj}\,\frac{\phi(\hat v)-G(\hat v)}{\hat v^{3}},
+\qquad \hat v = v/v_{thj},\ \ v_{thj}^2=2T_j/m_j ,
+```
+
+with the error function and Chandrasekhar function
+
+```math
+\phi(X)=\frac{2}{\sqrt\pi}\int_0^X e^{-t^2}dt,
+\qquad
+G(X)=\frac{\phi(X)-X\phi'(X)}{2X^2},\quad \phi'=\frac{d\phi}{dX} .
+```
+
+This is the standard test-particle **deflection** (perpendicular-diffusion)
+rate. Two limits fix the structure and the sub-toggle (docs/01 §2.3, ladder E3).
+Expanding with ``\phi(X)=\tfrac{2}{\sqrt\pi}(X-\tfrac{X^3}{3}+\dots)`` and
+``\phi'(X)=\tfrac{2}{\sqrt\pi}e^{-X^2}``:
+
+```math
+G(X)=\frac{\phi-X\phi'}{2X^2}
+ =\frac{2}{\sqrt\pi}\Big(\frac{X}{3}-\frac{X^3}{5}+\dots\Big),
+\qquad
+\phi-G=\frac{2}{\sqrt\pi}\Big(\frac{2X}{3}-\frac{2X^3}{15}+\dots\Big).
+```
+
+- **Low speed** ``\hat v\to 0``: ``\phi-G\to \tfrac{4}{3\sqrt\pi}\hat v``
+ (**linear**, not cubic), so
+ ``\nu_{jj}\to \tfrac{4\tilde\nu_{jj}}{3\sqrt\pi}\,\hat v^{-2}`` — the deflection
+ frequency **diverges as ``\hat v^{-2}``** even in the *full* Chandrasekhar
+ form. This is precisely the "``\tilde\nu\propto u^{-2}`` divergence at low
+ ``u``" of docs/01 §2.3 that spoils a naive velocity quadrature and motivates
+ the analytic ``\langle\hat\nu_{ii}\rangle_u`` (§7).
+- **High speed** ``\hat v\to\infty``: ``\phi-G\to 1``, so ``\nu_{jj}\to
+ \tilde\nu_{jj}/\hat v^{3}`` — the ``v^{-3}`` tail.
+- **Sub-toggle:** I19/L23 use the full ``[\phi-G]/\hat v^3`` (Chandrasekhar,
+ ``\sim\hat v^{-2}`` at low ``\hat v``, needed for neoclassical fidelity);
+ Diss19/D21 use the pure ``\hat v^{-3}`` (a stronger low-``\hat v`` divergence).
+ In both, the divergence is *integrable* in the velocity moments (``\int
+ \hat v^4 e^{-\hat v^2}\nu\,d\hat v`` converges), but it makes naive quadrature
+ inaccurate — hence L23's analytic value (§7).
+
+## 4. The ``\nu_\star`` normalization
+
+The banana-regime collisionality is (docs/01 §2.3; L23 Eq. 2.3.40)
+
+```math
+\nu_\star = \frac{\nu_{jj}\,Rq}{\varepsilon^{3/2} v_{th}},
+\qquad
+\hat\nu_{jj} = \varepsilon^{3/2}\nu_\star\,\tilde\nu_{jj}(\hat v) ,
+```
+
+i.e. ``\nu_\star`` is the ratio of the effective (``\varepsilon^{-1}``
+trapped-boundary-enhanced) collision rate to the bounce rate ``v_{th}/Rq``;
+``\nu_\star\ll 1`` is the banana regime (O6). The ``\varepsilon^{3/2}`` collects
+the trapped fraction ``\sim\sqrt\varepsilon`` and the effective-collisionality
+boundary-layer width ``\sim\varepsilon`` (the ``\propto\nu^{1/2}`` layer of
+docs/04 §2). The dimensionless drift-kinetic equation carries
+``\hat\nu_{jj}=\varepsilon^{3/2}\nu_\star\tilde\nu_{jj}(\hat v)`` as the collision
+coefficient.
+
+## 5. Species coupling (electron–ion)
+
+The electron operator ``C_{ei}`` (I19 Eq. 10) has the identical pitch-angle
+structure with ``\nu_{ei}`` and a momentum-restoring term
+``v_\parallel u_{\parallel i}/v_{the}^2\,F_{Me}`` that drags on the **ion** flow
+``u_{\parallel i}`` — so electrons and ions are coupled through momentum
+conservation (the flattened-electron closure, docs/01 §2.4, is a separate
+derivation). At Level 0 the bulk operator is ``C_{ii}``; multi-species
+field-particle coupling is Level 1 (`Collisions{FokkerPlanckMulti}`), plumbing
+already present.
+
+## 6. The momentum-restoring term
+
+The second bracket of Eq. 9, ``v_\parallel\bar u_{\parallel j}F_{Mj}/v_{thj}^2``,
+restores parallel momentum removed by the pitch-angle scattering (pure Lorentz
+scattering is not momentum-conserving on its own). The restoring flow is
+(I19 Eq. 12)
+
+```math
+\bar u_{\parallel j}(f) = \frac{1}{n\langle\nu_{jj}\rangle_v}\int d^3v\,
+ \nu_{jj}\,v_\parallel f,
+\qquad
+\langle\nu_{jj}\rangle_v = \frac{8}{3\sqrt\pi}\int_0^\infty d\hat v\,
+ \hat v^4 e^{-\hat v^2}\,\nu_{jj}(\hat v),
+```
+
+with the velocity element ``\int d^3v = \pi B\sum_\sigma\int_0^\infty v^2 dv
+\int_0^{B^{-1}} d\lambda/\sqrt{1-\lambda B}`` (I19 Eq. 13). This term is
+structurally fixed here; its **magnitude** enters through
+``\langle\nu_{jj}\rangle_v``, whose closed form is the deferred item §7.
+
+## 7. Deferred sub-items (flagged, not asserted)
+
+- **Analytic velocity average ``\langle\hat\nu_{ii}\rangle_u``.** The low-``\hat v``
+ divergence of ``\tilde\nu`` (``\hat v^{-2}`` Chandrasekhar / ``\hat v^{-3}``
+ reduced, §3) makes the momentum-restoring velocity integral of §6 poorly
+ behaved under naive quadrature; L23 Eq. 4.1.6 (p. 88) gives the closed form
+ ``\langle\hat\nu_{ii}\rangle_u = \tfrac{4\varepsilon^{3/2}\nu_\star}{3\sqrt\pi}
+ (\sqrt2-\ln(1+\sqrt2))``. **This specific constant is not derived here** — it
+ requires L23's exact reduced integrand and is a self-contained follow-up
+ derivation (policy rule 4: not presented as derived until it is). The
+ ``\sqrt2`` reflects the ion self-collision reduced mass; ``\ln(1+\sqrt2)=
+ \operatorname{arcsinh}(1)`` points to a ``\int d\hat v/\sqrt{1+\hat v^2}``-type
+ reduction.
+- **Discretized diffusivity profile for `PitchAngleDiffusion`.** The mapping of
+ ``P(\lambda)=\lambda\sqrt{1-\lambda B}`` and the measure through the
+ orbit-average and onto the ``y=\lambda B_{\max}`` grid (with the ``\theta``-
+ and ``\hat v``-dependence) is numerics that ties to the already-green A4
+ conservation gate; scoped with the L0 collisional solve.
+
+## 8. Cross-check table
+
+| Source | Form | Agrees with `[DERIVED]`? |
+|---|---|---|
+| I19 Eq. (9) (first-hand, print p. 4) | ``C_{jj}=2\nu_{jj}[\tfrac{\sqrt{1-\lambda B}}{B}\partial_\lambda(\lambda\sqrt{1-\lambda B}\partial_\lambda f)+\tfrac{v_\parallel\bar u_{\parallel j}}{v_{thj}^2}F_{Mj}]`` | ✅ operator structure + self-adjoint form + ``P=\lambda\sqrt{1-\lambda B}`` |
+| I19 Eq. (11) (first-hand) | ``\nu_{jj}=\tilde\nu_{jj}[\phi(\hat v)-G(\hat v)]/\hat v^3`` | ✅ deflection frequency + limits |
+| I19 Eqs. (12)–(13) (first-hand) | ``\bar u_{\parallel j}``, ``\langle\nu_{jj}\rangle_v``, ``\int d^3v`` | ✅ structure (constant deferred, §7) |
+| L23 Eq. 2.3.40 | ``\nu_\star=\nu_{jj}Rq/(\varepsilon^{3/2}v_{th})`` | ✅ normalization |
+| L23 Eq. 4.1.6 | ``\langle\hat\nu_{ii}\rangle_u=\tfrac{4\varepsilon^{3/2}\nu_\star}{3\sqrt\pi}(\sqrt2-\ln(1+\sqrt2))`` | ⏳ deferred (§7) |
+
+**Triage:** operator structure, deflection frequency, and normalization agree
+with all first-hand sources — no discrepancy. The one number that carries a
+specific closed form (``\langle\hat\nu_{ii}\rangle_u``) is honestly deferred to
+its own derivation rather than transcribed.
+
+## 9. What sign-off authorizes
+
+On sign-off (recorded in docs/01 §2.3): the pitch-angle **diffusivity profile**
+``P(\lambda)=\lambda\sqrt{1-\lambda B}`` and **measure**
+``w=B/\sqrt{1-\lambda B}`` may be used to build the `PitchAngleDiffusion`
+operator (via `conservative_pitch_operator`, preserving A4); the deflection
+frequency ``\nu_{jj}(\hat v)=\tilde\nu[\phi-G]/\hat v^3`` (with the
+``:chandrasekhar``/``:vcubed`` sub-toggle) and the ``\hat\nu=\varepsilon^{3/2}
+\nu_\star\tilde\nu`` normalization populate the collision coefficient. The
+momentum-restoring magnitude waits on ``\langle\hat\nu_{ii}\rangle_u`` (§7); the
+electron closure constants (``k``, ``f_p``) are a separate derivation.
diff --git a/docs/src/islands/derivations/delta-moment-prefactors.md b/docs/src/islands/derivations/delta-moment-prefactors.md
new file mode 100644
index 000000000..de4016df9
--- /dev/null
+++ b/docs/src/islands/derivations/delta-moment-prefactors.md
@@ -0,0 +1,123 @@
+# Derivation — the ``\Delta_{\cos}``/``\Delta_{\sin}`` moment prefactors
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent derivation of the structure
+plus a documented normalization pin (Decision D7). The sin-moment normalization
+is explicitly a `[DERIVED]` choice (docs/01 §4), made here.
+**Clears (on sign-off):** the ``\Delta_{\cos}``/``\Delta_{\sin}`` output-moment
+prefactors ``\mp\mu_0 R/(2\tilde\psi)`` (`[CHECKED: Diss19 Eq. 4.12 for
+\Delta_{\cos}; sin-normalization DERIVED]`, QUESTIONS Q4), building on the
+already-cleared ``\tilde\psi`` (`island_flux_amplitude`).
+
+**Status:** ✅ **signed off 2026-07-11** (clearance recorded in docs/01 §4).
+Implemented as `Coefficients.delta_moment_prefactors` (returns
+`(cos=-μ₀R/2ψ̃, sin=+μ₀R/2ψ̃)`), feeding `Moments.delta_moments`.
+
+## 1. The two Ampère projections
+
+The Level-0 outputs are the resonant projections of the parallel current
+``\bar J_\parallel(x,\xi)=\langle\sum_j e_j n_j u_{\parallel j}\rangle_\theta``
+through the island (docs/01 §4; O3: Ampère is a diagnostic, not solved). The
+perturbed parallel Ampère law for the resonant helical harmonic, matched to the
+outer-region tearing index ``\Delta'``, and the torque (rotation) condition are
+(Diss19 Eqs. 2.9–2.10)
+
+```math
+\frac{1}{\mu_0 R}\,\Delta'\,\tilde\psi = \int_{\mathbb R} d\psi\oint d\xi\;
+ \bar J_\parallel\cos\xi ,
+\qquad
+0 = \int_{\mathbb R} d\psi\oint d\xi\;\bar J_\parallel\sin\xi .
+```
+
+The ``\cos\xi`` projection matches the growth drive to ``\Delta'``; the
+``\sin\xi`` projection is the torque-balance (steady-rotation) condition. Both
+are the ``m``-harmonic projections of ``\bar J_\parallel`` against the island
+perturbation ``A_\parallel=-(\tilde\psi/R)\cos\xi`` (I19 Eq. 5).
+
+## 2. The growth moment ``\Delta_{\cos}\equiv\Delta_{\rm neo}``
+
+The Modified Rutherford Equation is the amplitude equation for the island
+half-width; its stationarity (marginal island) is the balance of the outer
+drive ``\Delta'`` against the kinetic inner drive. Define the kinetic growth
+moment ``\Delta_{\rm neo}`` so that **stationarity reads ``\Delta'+\Delta_{\rm
+neo}=0``** (docs/01 §4; Diss19 Eq. 4.12):
+
+```math
+\boxed{\;
+\Delta_{\cos}\equiv\Delta_{\rm neo}
+ = -\frac{\mu_0 R}{2\tilde\psi}\int_{\mathbb R} d\psi\oint d\xi\;
+ \bar J_\parallel\cos\xi
+\;}
+```
+
+The prefactor is fixed piece by piece — each piece sourced (the ``1/2`` adopted
+from the Diss19 Eq. 4.12 convention, not re-derived from scratch):
+
+- ``\mu_0 R`` is the geometric factor of parallel Ampère (``\nabla^2 A_\parallel
+ = -\mu_0 J_\parallel``; the ``R`` from the helical/toroidal metric of I19
+ Eq. 5's ``A_\parallel=-\tilde\psi\cos\xi/R``).
+- ``\tilde\psi = \tfrac{w_\psi^2}{4}\,q_s'/q_s`` is the island flux amplitude —
+ **already cleared** (`island_flux_amplitude`, docs/01 §1); dividing by it makes
+ ``\Delta_{\rm neo}`` the drive *per unit island flux*, with units of ``\Delta'``
+ (inverse length), as the MRE requires.
+- The ``1/2`` is the Rutherford matching normalization: the ``\cos\xi``
+ projection of the constant-``\psi`` perturbation carries the standard factor
+ that makes ``\Delta_{\rm neo}`` combine additively with ``\Delta'`` in the
+ amplitude equation (Diss19 Eq. 4.12 convention).
+
+## 3. The torque moment ``\Delta_{\sin}`` — the `[DERIVED]` symmetric pin
+
+The ``\sin\xi`` projection has no external ``\Delta'`` to match against (its
+outer counterpart vanishes — the torque condition is ``0=\int\!\int\bar
+J_\parallel\sin\xi``, §1). Its overall normalization is therefore a **choice**,
+flagged `[DERIVED]` in docs/01 §4. **Pin it symmetrically to** ``\Delta_{\cos}``
+— same magnitude prefactor, opposite sign so that ``\Delta_{\cos}+i\Delta_{\sin}``
+forms the natural complex growth+torque moment that maps onto the linear-layer
+``\Delta(Q)`` in the small-amplitude limit (docs/01 §4; ladder D1):
+
+```math
+\boxed{\;
+\Delta_{\sin}
+ = +\frac{\mu_0 R}{2\tilde\psi}\int_{\mathbb R} d\psi\oint d\xi\;
+ \bar J_\parallel\sin\xi
+\;}
+```
+
+so that
+
+```math
+\Delta_{\cos}+i\,\Delta_{\sin}
+ = \frac{\mu_0 R}{2\tilde\psi}\int d\psi\oint d\xi\;
+ \bar J_\parallel\,(-\cos\xi + i\sin\xi) .
+```
+
+The symmetric choice is what makes ``\Delta_{\sin}=0`` the clean torque-balance
+root (Level-4 ``\omega_E`` closure) and the parity relations of ladder A3
+(``\Delta_{\cos}`` even / ``\Delta_{\sin}`` odd under ``\xi\to-\xi``) hold with
+matched normalization — both already verified structurally (A3 green). This is a
+`[DERIVED: 2026-07-11]` normalization *decision*, recorded as such (policy
+rule 4), not a literature transcription.
+
+## 4. Cross-check table
+
+| Source | Form | Agrees with `[DERIVED]`? |
+|---|---|---|
+| Diss19 Eq. (2.9)–(2.10) (via docs/01 §4) | the ``\cos``/``\sin`` Ampère projections | ✅ structure (§1) |
+| Diss19 Eq. (4.12) | ``\Delta_{\rm neo}=-\tfrac{\mu_0 R}{2\tilde\psi}\int\!\int\bar J_\parallel\cos\xi`` | ✅ (§2) |
+| docs/01 §4 | ``\Delta_{\sin}`` normalization "chosen symmetric — [DERIVED] pin at implementation" | ✅ — pinned here (§3) |
+| A3 gate (green) | ``\Delta_{\cos}`` even / ``\Delta_{\sin}`` odd | ✅ consistent with the matched-normalization pin |
+
+**Triage:** the ``\cos``-moment prefactor is derived structure + the cleared
+``\tilde\psi`` + the standard Rutherford ``1/2`` (Diss19 4.12 convention); no
+discrepancy. The ``\sin``-moment normalization is a `[DERIVED]` symmetric choice,
+now pinned and recorded.
+
+## 5. What sign-off authorizes
+
+On sign-off (recorded in docs/01 §4): the prefactors
+``\text{prefactor\_cos}=-\mu_0 R/(2\tilde\psi)`` and
+``\text{prefactor\_sin}=+\mu_0 R/(2\tilde\psi)`` may be constructed
+(``\tilde\psi`` from the cleared `island_flux_amplitude`, ``\mu_0 R`` from the
+equilibrium) and passed to `Moments.delta_moments`, replacing its required gated
+arguments. This closes the ``\Delta``-prefactor `[VERIFY]`/`[DERIVED]` items; the
+channel decompositions (bootstrap/polarization split, ``\langle\cdot\rangle_\Omega``)
+are already implemented structure.
diff --git a/docs/src/islands/derivations/electron-closure.md b/docs/src/islands/derivations/electron-closure.md
new file mode 100644
index 000000000..ccbe042a7
--- /dev/null
+++ b/docs/src/islands/derivations/electron-closure.md
@@ -0,0 +1,157 @@
+# Derivation — the flattened-electron closure ``h(\Omega)`` and its amplitude
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent re-derivation (Decision D7).
+**Clears (on sign-off):** the flattened-electron profile function ``h(\Omega)``
+and its amplitude ``w_\psi/2\sqrt2`` (`[CHECKED: I19 Eqs. 14–22; WCHH96 §; L23
+§2.4]`, QUESTIONS Q3), and the closure ODE that ties to the already-green A7
+identity.
+**Deferred (flagged, §6):** the Hirshman–Sigmar flow coefficient ``k\simeq
+-1.173`` and the passing-fraction constant in ``f_p\simeq 1-1.46\sqrt\varepsilon``
+— specific neoclassical constants, each its own short derivation.
+
+**Status:** ✅ **signed off 2026-07-11** (clearance recorded in docs/01 §2.4) for
+the ``h(\Omega)`` form and amplitude — implemented as `Coefficients.h_amplitude`
+(``C=w_\psi/2\sqrt2``), feeding `Fields.h_profile`'s prefactor. The flow constants
+``k`` and ``f_p`` (§6) remain **deferred / NaN-gated**.
+
+## 1. Setup
+
+At Level 0 electrons have ``\rho_{\theta e}\ll w`` (O7), so their drift islands
+coincide with the magnetic island and their response is the analytic
+flattened-electron (WCHH96) closure (docs/01 §2.4). The electron distribution is
+(I19 Eq. 17, first-hand)
+
+```math
+f_e = \Big(1-\frac{e\Phi}{T_e}\Big)F_{Mes} + h(\Omega)\,F'_{Mes}
+ - \frac{Iv_\parallel}{\omega_{ce}}F'_{Mes}\frac{\partial h}{\partial\psi}
+ + \bar h_e ,
+```
+
+where ``h(\Omega)`` is the perturbed profile function — **constant on island
+flux surfaces ``\Omega``** (the flattening), exactly flat inside the separatrix
+(``\Omega<1``) and ``\to x`` far outside. This derivation determines ``h`` and
+its amplitude. Convention (module CLAUDE.md): ``\Omega=2x^2/w^2-\cos\xi`` with
+``w=w_\psi``, and the flux-surface average
+``\langle f\rangle_\Omega=\oint f\,(\Omega+\cos\xi)^{-1/2}d\xi\,/\oint(\Omega+
+\cos\xi)^{-1/2}d\xi`` (I19 Eq. 21).
+
+## 2. The closure constraint determines ``h(\Omega)``
+
+Flattening on ``\Omega`` surfaces with quasineutrality requires the
+flux-surface-averaged radial curvature of ``h`` to vanish (I19; the unit target
+``\langle\partial^2 h/\partial x^2\rangle_\Omega=0``, docs/01 §6, ladder A7).
+Compute it for ``h=h(\Omega)``. With ``\partial\Omega/\partial x=4x/w^2`` and
+``x^2=\tfrac{w^2}{2}(\Omega+\cos\xi)``,
+
+```math
+\frac{\partial^2 h}{\partial x^2}
+ = h''(\Omega)\Big(\frac{4x}{w^2}\Big)^2 + h'(\Omega)\frac{4}{w^2}
+ = \frac{8}{w^2}h''(\Omega)(\Omega+\cos\xi) + \frac{4}{w^2}h'(\Omega).
+```
+
+Flux-averaging and setting to zero,
+
+```math
+2\,h''(\Omega)\,\langle\Omega+\cos\xi\rangle_\Omega + h'(\Omega) = 0. \tag{$\star$}
+```
+
+Now relate ``\langle\Omega+\cos\xi\rangle_\Omega`` to the geometry function
+``Q(\Omega)=\tfrac1{2\pi}\oint\sqrt{\Omega+\cos\xi}\,d\xi`` (I19 Eq. 18), whose
+derivative is ``Q'(\Omega)=\tfrac1{4\pi}\oint(\Omega+\cos\xi)^{-1/2}d\xi``:
+
+```math
+\langle\Omega+\cos\xi\rangle_\Omega
+ = \frac{\oint(\Omega+\cos\xi)^{1/2}d\xi}{\oint(\Omega+\cos\xi)^{-1/2}d\xi}
+ = \frac{2\pi Q}{4\pi Q'} = \frac{Q}{2Q'} .
+```
+
+Substituting into ``(\star)``: ``2h''\,\tfrac{Q}{2Q'}+h'=0``, i.e.
+``\dfrac{h''}{h'}=-\dfrac{Q'}{Q}``, which integrates to
+
+```math
+\boxed{\; h'(\Omega)=\frac{C}{Q(\Omega)},\qquad
+ h(\Omega)=\Theta(\Omega-1)\,C\!\int_1^\Omega\frac{d\Omega'}{Q(\Omega')} \;}
+```
+
+flat inside the separatrix (the ``\Theta(\Omega-1)``, since there is no
+flattening gradient on the closed field lines within). This is I19 Eq. 18 up to
+the amplitude ``C``. **The A7 identity ``\langle\partial^2h/\partial x^2\rangle_
+\Omega=0`` is exactly ``(\star)`` with ``h'=C/Q``** — the already-green A7 gate
+*is* this closure constraint, verified for any ``C`` (`Fields.flat_average_d2h_dx2`).
+
+## 3. The amplitude ``C=w_\psi/2\sqrt2`` from far-field matching
+
+Far from the island (``\Omega\to\infty``) the flattened profile must match the
+unperturbed radial coordinate, ``h\to x``. Large-``\Omega`` asymptotics:
+``Q(\Omega)=\tfrac1{2\pi}\oint\sqrt{\Omega+\cos\xi}\,d\xi\to\sqrt\Omega``, so
+
+```math
+h(\Omega)\to C\int^\Omega\frac{d\Omega'}{\sqrt{\Omega'}} = 2C\sqrt\Omega .
+```
+
+Meanwhile ``x=\tfrac{w}{\sqrt2}\sqrt{\Omega+\cos\xi}\to\tfrac{w}{\sqrt2}\sqrt\Omega``
+(from ``x^2=\tfrac{w^2}{2}(\Omega+\cos\xi)``). Matching ``h\to x``:
+
+```math
+2C\sqrt\Omega = \frac{w}{\sqrt2}\sqrt\Omega
+\quad\Longrightarrow\quad
+\boxed{\; C = \frac{w}{2\sqrt2} = \frac{w_\psi}{2\sqrt2} \;}
+```
+
+exactly the I19 Eq. 18 amplitude. So both the *form* and the *amplitude* of
+``h(\Omega)`` are fixed — the former by the flattening constraint, the latter by
+far-field matching.
+
+## 4. Cross-check table (the derived parts)
+
+| Source | Form | Agrees with `[DERIVED]`? |
+|---|---|---|
+| I19 Eq. (18) (first-hand, print p. 4) | ``h=\Theta(\Omega-1)\tfrac{w_\psi}{2\sqrt2}\int_1^\Omega d\Omega'/Q``, ``Q=\tfrac1{2\pi}\oint\sqrt{\Omega+\cos\xi}d\xi`` | ✅ form (§2) + amplitude ``w_\psi/2\sqrt2`` (§3) |
+| I19 §6 / L23 Eq. 4.1.1 | ``\langle\partial^2h/\partial x^2\rangle_\Omega=0`` | ✅ = the closure constraint ``(\star)`` (§2); already the green A7 gate |
+| I19 Eq. (17) | ``f_e`` structure with ``h(\Omega)``, ``-Iv_\parallel/\omega_{ce}F'_{Mes}\partial h/\partial\psi`` | ✅ structure |
+
+**Triage:** the ``h(\Omega)`` form and amplitude agree first-hand; no discrepancy.
+
+## 5. The flux-surface-averaged electron flow (structure)
+
+The electron parallel flow that carries the bootstrap current is (I19 Eq. 22,
+first-hand, print p. 5)
+
+```math
+\frac{\langle\langle Bu_{\parallel e}\rangle_\theta\rangle_\Omega}{B_0 v_{the}}
+ = -\frac{f_t}{1+f_t}\frac{Iv_{the}}{\omega_{ce}}\frac{n'}{n}
+ \Big(1+\eta_e+\tfrac12 k f_c\eta_e\Big)\Big\langle\frac{\partial h}{\partial\psi}\Big\rangle_\Omega
+ + \frac{f_c}{1+f_t}
+ \frac{\langle\langle Bu_{\parallel i}\rangle_\theta\rangle_\Omega}{B_0 v_{thi}} ,
+```
+
+with ``f_t`` the trapped fraction, ``f_c=1-f_t`` passing, ``\eta_e=L_n/L_{Te}``,
+and ``k`` the Hirshman–Sigmar coefficient. The **structure** is derived here (it
+follows from the parallel momentum balance with the pitch-angle operator §2 of
+the collision derivation, and the ``h``-gradient drive of §2–3); the two
+**numerical constants** are §6. Note the flow depends on the *numerically
+computed ion flow* ``u_{\parallel i}`` — the closure is coupled, not one-way
+(the `electrons = :flattened` vs `:kinetic` toggle, E4, is a separate study).
+
+## 6. Deferred constants (flagged, not asserted)
+
+- **``k\simeq -1.173`` (Hirshman–Sigmar).** A specific parallel-viscosity /
+ flow coefficient obtained by solving the Spitzer-problem moment hierarchy with
+ the pitch-angle operator; a self-contained constant, not derived here (policy
+ rule 4). L23 reproduces ``-1.1730`` as a unit test.
+- **``f_p\simeq 1-1.46\sqrt\varepsilon`` (passing fraction).** The trapped
+ fraction ``f_t=1.46\sqrt\varepsilon`` at low ``\varepsilon`` follows from the
+ pitch-angle integral ``f_t=1-\tfrac34\langle B^2\rangle\int_0^{1/B_{\max}}
+ \lambda\,d\lambda/\langle\sqrt{1-\lambda B}\rangle_\theta`` in the
+ large-aspect-ratio limit; the specific ``1.46`` is its own short reduction and
+ is left deferred rather than asserted.
+
+## 7. What sign-off authorizes
+
+On sign-off (recorded in docs/01 §2.4): the ``h(\Omega)`` amplitude
+``C=w_\psi/2\sqrt2`` clears `Fields.h_profile`'s `prefactor` (and hence
+`ElectronClosure.h_prefactor`), with the `Q(\Omega)`/`h(\Omega)` machinery and
+the A7 identity already implemented. The flow-relation *structure* (§5) is
+cleared; its constants ``k`` and ``f_p`` (`ElectronClosure.k_HS`, `.f_p`) stay
+NaN-gated pending their own derivations (§6). The quasineutrality closure
+coefficient (``C_\phi``) is a separate derivation.
diff --git a/docs/src/islands/derivations/gradient-drive.md b/docs/src/islands/derivations/gradient-drive.md
new file mode 100644
index 000000000..50fd66a92
--- /dev/null
+++ b/docs/src/islands/derivations/gradient-drive.md
@@ -0,0 +1,98 @@
+# Derivation — the gradient drive (= the diamagnetic far-field boundary condition)
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent re-derivation (Decision D7)
+of the Level-0 gradient drive, from the ion response of I19 (first-hand,
+Eqs. 8, 23–32).
+**Status:** ✅ **signed off 2026-07-11** — implemented as
+`Configure.gradient_far_field` (the far-field `g_far`) with the Level-0
+`GradientDrive` source set to zero; a new `Level0Physics.eta_i`.
+
+> **Correction (2026-07-11).** An earlier draft of this chapter read I19 Eq. (29)
+> as `p_φ(ω_si^T/ω_ci)(n'/n)F_Mi` and concluded the amplitude was bundled with the
+> frame convention `C_dia`. A first-hand re-read (PDF p. 5) shows the ratio is
+> **`ω_si^T/ω_si`** — a *dimensionless temperature correction*, not a frequency
+> ratio. The drive is the standard neoclassical `p_φ F'_Mi` and needs **no frame
+> convention**. The corrected result is below.
+
+## 1. The finding: the drive is a boundary condition, not an interior source
+
+I19 §4 splits the ion distribution (Eq. 28) as
+`f_i = (1 − Ze\Phi/T_i)F_{Mis} + \bar G_0(p_\phi, \xi, v)`, and the orbit-averaged
+non-adiabatic part is (Eq. **29**, first-hand p. 5)
+
+```math
+\bar G_0 = p_\phi\,\frac{\omega_{si}^T}{\omega_{si}}\,\frac{n'}{n}\,F_{Mi} + \bar h_0 ,
+\qquad
+\frac{\omega_{si}^T}{\omega_{si}} = 1 + \Big(\frac{v^2}{v_{thi}^2} - \tfrac32\Big)\eta_i ,
+```
+
+\noindent
+with `η_i = (T_i'/T_i)/(n'/n)`. The master orbit-averaged equation (I19 **Eq. 32**,
+docs/01 §2) is **homogeneous** in `Ḡ₀` — transport `= ⟨(1/v̂_∥)Ĉ_ii(Ḡ₀)⟩_θ`,
+**no interior source**. The inhomogeneity is imposed through the **far-field
+boundary condition**: I19 seeks "a Maxwellian solution in the vicinity of the
+island (the equilibrium profile assumed far away)" (§4), so `h̄₀ → 0` and
+
+```math
+\boxed{\;
+\bar G_0 \;\to\; g_{\rm drive}
+ \;=\; p_\phi\,\frac{\omega_{si}^T}{\omega_{si}}\,\frac{n'}{n}\,F_{Mi}
+ \;=\; p_\phi\,F'_{Mi}
+ \quad\text{as } |x| \to L_x .
+\;}
+```
+
+The equality `g_drive = p_φ F'_{Mi}` follows from
+`F'_{Mi} = (n'/n)(1+(v̂²−3/2)η_i)F_{Mi}` (differentiate the Maxwellian at fixed
+`v`): the drive is the **standard neoclassical drive** — canonical momentum times
+the equilibrium-gradient Maxwellian — recognizable and coefficient-free.
+
+**Consequence for the operator stack.** In I19's formulation the Level-0
+`Operators.GradientDrive` source is **zero**; the drive is the neoclassical
+far-field state `Operators.FarFieldConditions.g_far`. This merges the two Q5
+"gated" items (`gradient_drive` and `far_field`) into one physical object.
+
+## 2. The normalized far-field (cleared, no frame convention)
+
+Far away `p_φ → ψ_s x` (the orbit-width term `Iv_∥/ω_ci = O(ρ̂_θi)` is a small
+shift at `|x| = L_x`), and `n'/n = L̂_{n0}^{-1}/ψ_s`, so `p_φ(n'/n) → x L̂_{n0}^{-1}`.
+The Maxwellian `F_{Mi} ∝ e^{-v̂^2} = e^{-E}` is carried by the energy-grid measure
+(Gauss–Laguerre, `04 §1`; `velocity_moment!` sums against `e^{-E}`), so the code's
+`g` is the distribution with that factor stripped. Hence
+
+```math
+\boxed{\;
+g_{\rm far}(x{=}\pm L_x,\,\xi,\,y,\,E,\,\sigma)
+ \;=\; x\;\hat L_{n0}^{-1}\;\big[\,1 + (E - \tfrac32)\,\eta_i\,\big]
+\;}
+```
+
+\noindent
+— linear in the boundary `x`, the temperature correction through `E = v̂²`,
+independent of `ξ, y, σ` at leading order (the finite-orbit-width `σ`-dependence
+is an interior effect near `x = 0`, not the far field). `L̂_{n0}^{-1} = inv_Ln0`
+(existing input); `η_i = eta_i` is the only new parameter — a standard scenario
+ratio (`= L_n/L_T`), **not a gated coefficient**. At Level 0 `ω_E = 0`, the
+far-field potential is `Φ̂_far = 0`.
+
+## 3. Cross-check table
+
+| Source | Statement | Agrees with `[DERIVED]`? |
+|---|---|---|
+| I19 Eq. (29) (first-hand p. 5) | `Ḡ₀ = p_φ(ω_si^T/ω_si)(n'/n)F_Mi + h̄₀` | ✅ (§1) — **ω_si^T/ω_si**, not ω_ci |
+| I19 Eq. (32) (first-hand p. 6) | master equation **homogeneous** in `Ḡ₀` | ✅ ⇒ drive is a BC, not a source (§1) |
+| Maxwellian derivative | `F'_Mi = (n'/n)(1+(v̂²−3/2)η_i)F_Mi` | ✅ ⇒ `g_drive = p_φ F'_Mi` (§1) |
+| design docs/01 §3 far-field spec | `g → neoclassical (no-island) solution` | ✅ = `g_drive` |
+
+**Triage:** no discrepancy. The drive is the neoclassical far field `p_φ F'_Mi`
+and the interior source is zero (I19 Formulation A). The only new input is `η_i`;
+no frame convention enters (the corrected reading of Eq. 29).
+
+## 4. What sign-off authorizes — now implemented
+
+On sign-off (recorded in docs/01 §2/§4): (i) `Operators.GradientDrive` source set
+to zero (I19 Formulation A); (ii) the far-field
+`Operators.FarFieldConditions.g_far = x·L̂_{n0}⁻¹·[1+(E−3/2)η_i]` (and `Φ̂_far = 0`)
+built by `Configure.gradient_far_field` from `inv_Ln0` and the new `eta_i`. This
+un-gates the `gradient_drive` **and** `far_field` families. The `E×B` coupling,
+collision magnitude `⟨ν̂_ii⟩_u`, and orbit-averaged pitch measure remain gated.
diff --git a/docs/src/islands/derivations/index.md b/docs/src/islands/derivations/index.md
new file mode 100644
index 000000000..9d41f65a3
--- /dev/null
+++ b/docs/src/islands/derivations/index.md
@@ -0,0 +1,38 @@
+# Derivations
+
+Independent re-derivations of the Level-0 physics coefficients (Decision D7,
+"re-derivation first"): each is marked `[DERIVED: date]`, carries a cross-check
+table against the `[CHECKED]` literature transcriptions (docs/01), and flags
+every discrepancy in the open (policy rule 4, docs/05 triage). **A derivation
+authorizes a coefficient in `src/` only after human sign-off** (recorded in
+docs/01, policy rule 3); until then the corresponding coefficient stays a gated,
+supplied argument.
+
+These pages are *not* literature transcriptions — they derive the result from a
+stated starting point and assumptions, then compare. That distinction is the
+spine of the milestone (policy rule 4: never present a derivation as a
+transcription or vice versa).
+
+## Chapters
+
+| Coefficient | Chapter | Status |
+|---|---|---|
+| Island flux amplitude ``\tilde\psi`` | [ψ̃ amplitude](psi-tilde-amplitude.md) | ✅ signed off 2026-07-11 |
+| Magnetic drift frequency ``\hat\omega_D`` + `:original`/`:improved` toggle | [ω̂_D drift frequency](omega-D-drift-frequency.md) | ✅ signed off 2026-07-11 |
+| Pitch-angle collision operator + deflection frequency + ``\nu_\star`` | [collision operator](collision-operator.md) | ✅ signed off 2026-07-11 (``\langle\hat\nu_{ii}\rangle_u`` deferred) |
+| Flattened-electron closure ``h(\Omega)`` + amplitude | [electron closure](electron-closure.md) | ✅ signed off 2026-07-11 (``k``, ``f_p`` deferred) |
+| Quasineutrality closure ``1/(2\hat L_{n0})`` (arbitrary ``\tau``) | [quasineutrality closure](quasineutrality-closure.md) | ✅ signed off 2026-07-11 |
+| ``\Delta_{\cos}/\Delta_{\sin}`` moment prefactors ``\mp\mu_0 R/2\tilde\psi`` | [Δ-moment prefactors](delta-moment-prefactors.md) | ✅ signed off 2026-07-11 |
+| Parallel (island) streaming ``a_\xi``, ``a_x`` (advection along ``\Omega``) | [parallel streaming](parallel-streaming.md) | ✅ signed off 2026-07-11 |
+| Gradient drive = the diamagnetic far-field BC (I19 Eq. 29) | [gradient drive](gradient-drive.md) | ✅ signed off 2026-07-11 |
+| Passing fraction ``f_p \simeq 1-1.46\sqrt\varepsilon`` (electron closure) | [passing fraction](passing-fraction.md) | ✅ signed off 2026-07-11 |
+
+The six main Q3/Q4 coefficient families are signed off, plus the parallel
+streaming and the passing fraction ``f_p`` (`Coefficients.passing_fraction`).
+The gradient drive's *structure* is found (it is the far-field BC, not a source);
+its normalized amplitude is bundled with the frame convention and awaits
+completion. Of the **deferred numerical sub-constants**, ``f_p`` is now cleared;
+the remaining two — ``\langle\hat\nu_{ii}\rangle_u`` (collision) and the
+Hirshman–Sigmar ``k`` (electron closure) — are escalated in `QUESTIONS.md`
+Q3/Q5 (each needs its specific source integrand) rather than derived
+speculatively.
diff --git a/docs/src/islands/derivations/omega-D-drift-frequency.md b/docs/src/islands/derivations/omega-D-drift-frequency.md
new file mode 100644
index 000000000..3f01a56ea
--- /dev/null
+++ b/docs/src/islands/derivations/omega-D-drift-frequency.md
@@ -0,0 +1,226 @@
+# Derivation — the orbit-averaged magnetic drift frequency ``\hat\omega_D`` and the `:original`/`:improved` toggle
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent re-derivation (Decision D7).
+**Clears (on sign-off):** the ``\hat\omega_D`` expression and the
+`:original`/`:improved` ``\hat L_B^{-1}`` toggle (`[CHECKED: I19 Eq. 32 def.;
+D21 Eqs. 15, B1; D21 Eq. A2, p. 16]`, QUESTIONS Q3). This toggle is the single
+highest-impact physics item — it is the ``\hat\omega_D`` term that produces the
+``\sim\!\times 6`` threshold-width differential between the two drift models
+(the reproducible T2 form of the sources' ``8.73 \to 1.46\,\rho_{bi}`` story;
+Decision D9, docs/05).
+
+**Status:** ✅ **signed off 2026-07-11** (clearance recorded in docs/01 §2.1).
+Implemented as `Coefficients.magnetic_drift_frequency` (both variants); it builds
+`MagneticDrift.c_D` on the phase-space grid.
+
+## 1. What ``\hat\omega_D`` is
+
+In the orbit-averaged (4D) Level-0 kinetic equation (docs/01 §2, I19 Eq. 32 —
+verified first-hand, print p. 6), the ``\partial\bar G_0/\partial\xi``
+coefficient collects three transport channels,
+
+```math
+-m\Big[\underbrace{\tfrac{\hat p}{\hat L_q}\,\Theta(y_c-y)}_{\text{streaming}}
+ + \underbrace{\hat\rho_{\theta i}\,\hat\omega_D}_{\text{magnetic drift}}
+ - \underbrace{\tfrac{\hat\rho_{\theta i}}{2}\big\langle \tfrac{1}{\hat v_\parallel}\tfrac{\partial\hat\Phi}{\partial x}\big\rangle_\theta}_{E\times B}
+ \Big]\frac{\partial\bar G_0}{\partial\xi} ,
+```
+
+so ``\hat\rho_{\theta i}\,\hat\omega_D`` is the **orbit-averaged magnetic-drift
+advection of the distribution in the helical angle ``\xi``**. This derivation
+computes it from the guiding-centre magnetic drift (I19 Eq. 8,
+``\mathbf v_b = -v_\parallel\,\mathbf b\times\nabla(v_\parallel/\omega_{cj})``)
+and the conserved canonical momentum, in the large-aspect-ratio circular
+geometry (orderings O1–O5).
+
+Normalizations (docs/01 §5, I19 p. 6, verified first-hand): ``x=(\psi-\psi_s)/\psi_s``,
+``y=\lambda B_{\max}``, ``\hat v=v/v_{thi}``, ``b=B/B_{\max}=(1-\varepsilon\cos\theta)/(1+\varepsilon)``,
+``\hat L_q^{-1}=(\psi_s/q_s)\,dq/d\psi|_s``, ``\hat L_B^{-1}=(\psi_s/B)\,\partial B/\partial\psi``,
+``\hat\rho_{\theta i}=\rho_{\theta i}/r_s``, ``\sigma=\mathrm{sgn}(v_\parallel)``,
+``v_\parallel=\sigma v\sqrt{1-\lambda B}=\sigma v\sqrt{1-yb}``. The orbit average
+``\langle\cdot\rangle_\theta`` is ``\tfrac{1}{2\pi}\oint d\theta`` (passing) or
+``\tfrac{1}{2\pi}\sum_\sigma\int_{-\theta_b}^{\theta_b} d\theta`` (trapped), at
+fixed ``p_\phi`` (I19 Eq. 31).
+
+## 2. The drift-orbit radial width (from ``p_\phi`` conservation)
+
+The toroidal canonical momentum ``p_\phi = (\psi-\psi_s) - I v_\parallel/\omega_{cj}``
+(I19 Eq. 2) is conserved along orbits, so a particle's flux-surface label
+oscillates about its orbit centre by the **drift-orbit width**
+
+```math
+x - \hat p = \frac{\psi-\psi_s-p_\phi}{\psi_s}
+ = \frac{I\,v_\parallel}{\omega_{cj}\,\psi_s} .
+```
+
+Evaluate it in the normalized variables. With ``I=RB_\phi``,
+``\omega_{cj}=e_jB/m_j``, ``v_\parallel=\sigma v\sqrt{1-yb}``,
+``RB_\phi/B \simeq R_0/b`` (large aspect ratio, ``B=B_{\max}b``,
+``RB_\phi=I\simeq R_0 B_{\phi0}``), and ``\psi_s\simeq r_s R_0 B_\theta``
+(``d\psi/dr=RB_\theta``), together with the poloidal gyroradius
+``\rho_{\theta i}=v_{thi}m_i/(e_iB_\theta)``:
+
+```math
+\boxed{\;
+x_D(\theta;y,\hat v,\sigma) \equiv x-\hat p
+ = \hat\rho_{\theta i}\,\frac{\sigma\hat v}{1+\varepsilon}\,\frac{\sqrt{1-yb}}{b}
+\;}
+```
+
+(the ``1+\varepsilon`` from ``B_{\max}=B_{\phi0}(1+\varepsilon)``). This is the
+finite ion orbit width — the physical heart of the DK-NTM: guiding centres do
+not sit on a flux surface but on a surface shifted by ``x_D``, and this shift is
+``\sigma``-, ``y``-, and ``\hat v``-dependent (docs/01 §2.2, the "drift island").
+
+## 3. Term 1 — the shear-coupled piece (``1/\hat L_q``)
+
+The equilibrium term ``\langle 1-q/q_s\rangle_\theta`` of the orbit-averaged
+equation (I19 Eq. 31) carries the shear. Expanding ``q`` about ``\psi_s``,
+
+```math
+1 - \frac{q(\psi)}{q_s} = -\frac{q_s'}{q_s}(\psi-\psi_s) + \mathcal O((\psi-\psi_s)^2)
+ = -\frac{x}{\hat L_q},
+```
+
+and orbit-averaging at fixed ``\hat p`` using ``x = \hat p + x_D(\theta)`` (§2):
+
+```math
+\big\langle 1-q/q_s\big\rangle_\theta
+ = -\frac{\hat p}{\hat L_q}\,\Theta(y_c-y)\;-\;\frac{1}{\hat L_q}\big\langle x_D\big\rangle_\theta .
+```
+
+The first piece is parallel streaming (nonzero only for passing particles,
+``\Theta(y_c-y)``). The second is a **magnetic-drift** contribution — the shear
+acting on the finite orbit width — and with ``x_D`` from §2 it is
+
+```math
+-\frac{1}{\hat L_q}\big\langle x_D\big\rangle_\theta
+ = -\,\hat\rho_{\theta i}\,\frac{\sigma\hat v}{1+\varepsilon}\,
+ \frac{1}{\hat L_q}\Big\langle \frac{\sqrt{1-yb}}{b}\Big\rangle_\theta .
+```
+
+## 4. Term 2 — the grad-``B`` piece (``1/\hat L_B``)
+
+The explicit magnetic-drift term of I19 Eq. 31 is
+``-\langle I\,\partial(v_\parallel/\omega_{cj})/\partial\psi\rangle_\theta``.
+With ``v_\parallel/\omega_{cj} = (\sigma v m_j/e_j)\sqrt{1-\lambda B}/B`` and
+``\partial/\partial\psi`` acting on ``B`` (fixed ``\lambda,v``),
+
+```math
+\frac{\partial}{\partial\psi}\!\left(\frac{\sqrt{1-\lambda B}}{B}\right)
+ = -\,\frac{\partial B}{\partial\psi}\,\frac{2-\lambda B}{2B^2\sqrt{1-\lambda B}} ,
+```
+
+since ``\dfrac{d}{dB}\!\dfrac{\sqrt{1-\lambda B}}{B}
+ = -\dfrac{2-\lambda B}{2B^2\sqrt{1-\lambda B}}``. Multiplying by ``-I`` and
+normalizing (``\lambda B = yb``, ``\hat L_B^{-1}=(\psi_s/B)\,\partial B/\partial\psi``)
+gives the second magnetic-drift contribution,
+
+```math
+-\Big\langle I\,\frac{\partial}{\partial\psi}\frac{v_\parallel}{\omega_{cj}}\Big\rangle_\theta
+ = -\,\frac{\hat\rho_{\theta i}}{2}\,\frac{\sigma\hat v}{1+\varepsilon}
+ \Big\langle \frac{1}{\hat L_B}\,\frac{2-yb}{b\sqrt{1-yb}}\Big\rangle_\theta .
+```
+
+## 5. Result
+
+Collect the two magnetic-drift contributions. The ``\partial\bar G_0/\partial\xi``
+coefficient of §1 carries the equilibrium term with an **overall minus sign**,
+``-m\langle 1-q/q_s\rangle_\theta`` (this is what makes the streaming piece
+``+\hat p/\hat L_q``, §3); so the §3 second piece enters ``\hat\rho_{\theta i}
+\hat\omega_D`` as ``-\big(-\tfrac1{\hat L_q}\langle x_D\rangle\big)
+= +\tfrac1{\hat L_q}\langle x_D\rangle``, while the §4 grad-``B`` term (already
+written with its sign) adds directly:
+
+```math
+\hat\rho_{\theta i}\hat\omega_D
+ = +\frac{1}{\hat L_q}\big\langle x_D\big\rangle_\theta
+ \;-\;\frac{\hat\rho_{\theta i}}{2}\frac{\sigma\hat v}{1+\varepsilon}
+ \Big\langle\frac{1}{\hat L_B}\frac{2-yb}{b\sqrt{1-yb}}\Big\rangle_\theta .
+```
+
+Substituting ``\langle x_D\rangle`` from §2 and dividing by
+``\hat\rho_{\theta i}``:
+
+```math
+\boxed{\;
+\hat\omega_D = \frac{\sigma\hat v}{1+\varepsilon}
+ \left[\;\frac{1}{\hat L_q}\Big\langle\frac{\sqrt{1-yb}}{b}\Big\rangle_\theta
+ \;-\;\frac{1}{2}\Big\langle\frac{1}{\hat L_B}\,\frac{2-yb}{b\sqrt{1-yb}}\Big\rangle_\theta\;\right]
+\;}
+```
+
+matching I19 Eq. (32) (first-hand) and D21 Eq. (B1) term-for-term (§7). The two
+terms are the shear-coupled orbit-width precession (``1/\hat L_q``) and the
+grad-``B`` drift (``1/\hat L_B``).
+
+## 6. The `:original` / `:improved` toggle — treatment of ``\partial B/\partial\psi``
+
+The toggle is entirely in ``\hat L_B^{-1}=(\psi_s/B)\,\partial B/\partial\psi``.
+In the model circular equilibrium ``R=R_0(1+\varepsilon\cos\theta)``, the radial
+field gradient is (D21 App. A, verified first-hand, print p. 16)
+
+```math
+\frac{\partial B}{\partial\psi} = \frac{I'}{R} - \frac{I}{R^2}\frac{\partial R}{\partial\psi},
+\qquad I' \sim R^2 p'/I \ \Rightarrow\ \frac{I'}{R}\sim \frac{\beta}{r^2}\ (\text{low }\beta,\ \text{drop}),
+```
+
+so, keeping the geometric term with ``\partial R/\partial\psi = \cos\theta/(R_0B_\theta)``,
+
+```math
+\frac{\partial B}{\partial\psi}
+ = -\,\frac{B_\phi}{R_0^2 B_\theta}\cos\theta + \mathcal O(\varepsilon^2/r^2)
+\qquad\text{(D21 Eq. A2).}
+```
+
+The field gradient is **``\propto\cos\theta``** — odd about the outboard
+midplane. This is the crux:
+
+- **`:original`** (I19 / DK-NTM): treat ``\hat L_B^{-1}`` as a *finite constant*
+ (the ``\cos\theta`` structure not resolved). The ``1/\hat L_B`` bracket in
+ ``\hat\omega_D`` survives orbit-averaging with an ``\mathcal O(1)`` value.
+- **`:improved`** (D21 / RDK-NTM): keep the ``\cos\theta``. In the ``1/\hat L_B``
+ bracket the remaining factor ``(2-yb)/(b\sqrt{1-yb})`` is even in ``\theta``
+ and ``\mathcal O(1)`` over a passing orbit, so
+ ``\langle\cos\theta\times(\text{even})\rangle_\theta = \mathcal O(\varepsilon)``:
+ the grad-``B`` term is ``\varepsilon``-small after orbit averaging. The
+ **documented proxy is ``\hat L_B^{-1}=0``** (D21 footnote; its Fig. 8 compares
+ the proxy against the full ``\cos\theta`` form directly).
+
+**Physical consequence (T2 gate, docs/05 D9).** Dropping the ``1/\hat L_B`` term
+removes a drift contribution that, in `:original`, partially cancels the shear
+term over the trapped/barely-passing population; its removal changes the
+drift-island structure and lowers the threshold half-width by a factor of order
+several. Islands measures this **within the code** as the `:original → :improved`
+``w_c`` ratio (the reproducible form of the sources' ``8.73 \to 1.46\,\rho_{bi}``
+absolute pair, which is T4/audit-gated). The absolute numbers are *not* the gate.
+
+## 7. Cross-check table
+
+| Source | Form | Agrees with `[DERIVED]`? |
+|---|---|---|
+| I19 Eq. (32) (first-hand, print p. 6) | ``\hat\omega_D=\tfrac{\sigma\hat v}{1+\varepsilon}[\tfrac1{\hat L_q}\langle\tfrac{\sqrt{1-yb}}{b}\rangle-\tfrac12\langle\tfrac1{\hat L_B}\tfrac{2-yb}{b\sqrt{1-yb}}\rangle]`` | ✅ exact |
+| D21 Eq. (B1) (first-hand, print p. 16) | ``\hat\omega_D=-\tfrac{\hat w}{\hat L_q}\langle\hat V_\parallel\rangle+\langle\tfrac{B^2}{B_\phi^2}\tfrac{\hat w}{\hat L_B}[\hat V_\parallel+\tfrac{\lambda\hat V^2}{2\hat V_\parallel}]B\rangle`` | ✅ (D21 ``w``-normalization; ``\hat V_\parallel+\tfrac{\lambda\hat V^2}{2\hat V_\parallel}=\hat V\tfrac{2-\lambda B}{2\sqrt{1-\lambda B}}`` = the same ``(2-yb)/\sqrt{1-yb}`` structure) |
+| D21 Eq. (A2) (first-hand, print p. 16) | ``\partial B/\partial\psi=-(B_\phi/R_0^2B_\theta)\cos\theta+\mathcal O(\varepsilon^2)`` | ✅ (basis of the `:improved` proxy, §6) |
+| Diss19 §2 / docs/01 §2.1 | same two-term ``\hat\omega_D``; `:original`/`:improved` toggle | ✅ |
+
+**Triage:** all first-hand sources agree; no discrepancy (unlike the ψ̃ typo).
+The one *modelling choice* is the `:improved` ``\hat L_B^{-1}=0`` **proxy** —
+D21 documents it as a proxy for the ``\cos\theta`` form (accurate to
+``\mathcal O(\varepsilon)``), not an identity. Islands should therefore carry
+**both**: `:original` (finite ``\hat L_B^{-1}``) and `:improved`
+(``\hat L_B^{-1}=0``), exactly as the `MagneticDrift.variant` field already
+provides — the toggle *is* the deliverable.
+
+## 8. What sign-off authorizes
+
+On human sign-off (recorded in docs/01 §2.1), the coefficient array
+`MagneticDrift.c_D` may be constructed from the boxed ``\hat\omega_D`` of §5:
+``c_D = -m\,\hat\rho_{\theta i}\,\hat\omega_D`` evaluated on the ``(y,E,\sigma)``
+grid, with the ``\hat L_B^{-1}`` treatment selected by
+`MagneticDrift.variant` (`:original` → finite ``\hat L_B^{-1}``; `:improved`
+→ ``\hat L_B^{-1}=0``). The orbit averages ``\langle\cdot\rangle_\theta`` are
+evaluated with the existing quadrature machinery; the ``\hat L_q``, ``\hat L_B``,
+``\varepsilon`` inputs come from the parameter vector (`Frames.Level0Parameters`
++ the equilibrium). Nothing here authorizes the collision, closure, or E×B
+coefficients.
diff --git a/docs/src/islands/derivations/parallel-streaming.md b/docs/src/islands/derivations/parallel-streaming.md
new file mode 100644
index 000000000..1b31e1ba6
--- /dev/null
+++ b/docs/src/islands/derivations/parallel-streaming.md
@@ -0,0 +1,124 @@
+# Derivation — the parallel (island) streaming coefficients
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent re-derivation (Decision D7)
+of the island-induced parallel-streaming channel of the master orbit-averaged
+drift-kinetic equation.
+**Clears:** the `Operators.ParallelStreaming` coefficients `a_xi`, `a_x`
+(`[VERIFY: I19 Eq. (32) streaming term, with the L23 §2.6 amendments]`,
+QUESTIONS Q5), building on the already-cleared `ω̂_D`
+(`magnetic_drift_frequency`) whose normalization it must match.
+**Status:** ✅ **signed off 2026-07-11** — implemented as
+`Configure.streaming_coefficients` and wired into `Operators.ParallelStreaming`;
+the `{Ω, g}` advection structure (§3) is verified in
+`test/runtests_islands_configure.jl`.
+
+## 1. The channel in the master equation
+
+The Level-0 master equation is I19 Eq. (32) (docs/01 §2), the orbit-averaged DKE
+for `Ḡ₀(p̂, ξ, y; v̂, σ)`:
+
+```math
+-m\Big[\underbrace{\tfrac{p̂}{\hat L_q}\Theta(y_c-y)}_{\text{streaming}}
+ + \hat\rho_{\theta i}\,\hat\omega_D
+ - \tfrac{\hat\rho_{\theta i}}{2}\big\langle\tfrac1{\hat v_\parallel}\partial_x\hat\Phi\big\rangle_\theta\Big]
+ \partial_\xi \bar G_0
++ m\Big[\underbrace{\tfrac{\hat w^2}{4\hat L_q}\sin\xi\,\Theta(y_c-y)}_{\text{streaming}}
+ - \tfrac{\hat\rho_{\theta i}}{2}\big\langle\tfrac1{\hat v_\parallel}\partial_\xi\hat\Phi\big\rangle_\theta\Big]
+ \partial_{p̂} \bar G_0
+= \big\langle\tfrac1{\hat v_\parallel}\hat C_{ii}(\bar G_0)\big\rangle_\theta .
+```
+
+\noindent
+The **island-streaming** channel is the two braced terms — the equilibrium
+magnetic shear (`p̂/L̂_q`) driving the `∂_ξ` transit and the island radial field
+(`ŵ²/4L̂_q · sinξ`, from `A_∥ = −(ψ̃/R)cosξ` → `B̃_r ∝ sinξ`) driving the `∂_{p̂}`
+advection. `Θ(y_c-y)` restricts it to **passing** particles (`y < y_c = 1`):
+trapped particles bounce and carry no net parallel transit, so they do not stream
+along the island (I19 Eq. 32; L23 §2.6). The other braced terms are the drift
+(`ω̂_D`, cleared) and the `E×B` channels (gated).
+
+To leading order in the ``\Delta = w/r`` ordering the canonical momentum equals
+the radial coordinate, ``p̂ = x - I\hat v_\parallel/\omega_c + \dots \to x`` (the
+orbit-width correction is `O(ρ̂_θi)`), so `∂_{p̂} → ∂_x` and `p̂ → x` in the
+operator, whose solve coordinate is `x` (docs/03 §2, Decision D1).
+
+## 2. Normalization — matched to the cleared ``\hat\omega_D``
+
+The operator residual sums `a_xi ∂_ξ g + c_D ∂_ξ g + …` and `a_x ∂_x g + …`, so
+every coefficient must share one normalization. `c_D` is **already cleared** as
+`c_D = ω̂_D` (`magnetic_drift_frequency`), which pins the normalization: divide
+the whole master equation by `−m ρ̂_θi`. The drift term then reads
+`(−m ρ̂_θi ω̂_D)/(−m ρ̂_θi) = ω̂_D = c_D` ✅ (unchanged), and the streaming terms
+become
+
+```math
+a_\xi = \frac{-m\,(x/\hat L_q)\,\Theta}{-m\,\hat\rho_{\theta i}}
+ = \frac{\hat L_q^{-1}\,x}{\hat\rho_{\theta i}}\,\Theta(y_c-y),
+\qquad
+a_x = \frac{+m\,(\hat w^2/4\hat L_q)\sin\xi\,\Theta}{-m\,\hat\rho_{\theta i}}
+ = -\frac{\hat L_q^{-1}\,\hat w^2\,\sin\xi}{4\,\hat\rho_{\theta i}}\,\Theta(y_c-y) .
+```
+
+\noindent
+`m` cancels; `ρ̂_θi` (normalized ion poloidal gyroradius, `~ ŵ` by the O2
+ordering) survives as a genuine parameter because fixing `c_D = ω̂_D` referred the
+whole equation to the drift scale. `ŵ = w_ψ` is the island half-width in `Ω`
+(same `w` as `Ω = 2x²/ŵ² − cosξ` and the `ĥ` amplitude, `electron-closure.md §3`).
+
+```math
+\boxed{\;
+a_\xi = \frac{\hat L_q^{-1}}{\hat\rho_{\theta i}}\,x\,\Theta(y_c-y),
+\qquad
+a_x = -\frac{\hat L_q^{-1}\hat w^2}{4\,\hat\rho_{\theta i}}\,\sin\xi\,\Theta(y_c-y)
+\;}
+```
+
+## 3. The consistency check — streaming *is* advection along `Ω`
+
+Parallel streaming must advect `g` along the island flux surfaces `Ω = const`;
+that advection is the Poisson bracket `\{\Omega, g\} = \partial_x\Omega\,\partial_\xi g - \partial_\xi\Omega\,\partial_x g`. With `Ω = 2x²/ŵ² − cosξ`,
+`∂_xΩ = 4x/ŵ²` and `∂_ξΩ = sinξ`, so
+
+```math
+\{\Omega,g\} = \frac{4x}{\hat w^2}\,\partial_\xi g - \sin\xi\,\partial_x g .
+```
+
+The derived coefficients factor **exactly** into this bracket:
+
+```math
+a_\xi\,\partial_\xi g + a_x\,\partial_x g
+ = \frac{\hat L_q^{-1}\hat w^2}{4\,\hat\rho_{\theta i}}\,\Theta(y_c-y)
+ \Big[\frac{4x}{\hat w^2}\partial_\xi g - \sin\xi\,\partial_x g\Big]
+ = \frac{\hat L_q^{-1}\hat w^2}{4\,\hat\rho_{\theta i}}\,\Theta(y_c-y)\,\{\Omega,g\} .
+```
+
+\noindent
+So the island-streaming operator is `(L̂_q⁻¹ ŵ²/4ρ̂_θi)Θ · {Ω, ·}` — pure
+flux-surface advection, vanishing on `Ω`-contours (`{Ω,Ω}=0`) exactly as parallel
+streaming must. This is a coefficient-free structural check that pins the relative
+sign and magnitude of `a_ξ` and `a_x` with no freedom — the derivation's own
+proof.
+
+## 4. Cross-check table
+
+| Source / check | Statement | Agrees with `[DERIVED]`? |
+|---|---|---|
+| I19 Eq. (32) streaming braces (docs/01 §2) | `(x/L̂_q)Θ ∂_ξ`, `(ŵ²/4L̂_q)sinξ Θ ∂_{p̂}` | ✅ structure (§1) |
+| cleared `c_D = ω̂_D` normalization | divide by `−m ρ̂_θi` ⇒ `c_D` unchanged | ✅ (§2) |
+| Poisson-bracket advection `{Ω, g}` | streaming `∝ {Ω, ·}`, `=0` on `Ω`-contours | ✅ **exact** (§3) |
+| passing/trapped split `Θ(y_c−y)` | trapped carry no net transit | ✅ (§1) |
+
+**Triage:** no discrepancy. The `{Ω, g}` factorization (§3) is a hard internal
+check that leaves no coefficient freedom; the only supplied scale is the physical
+parameter `ρ̂_θi`.
+
+## 5. What sign-off authorizes
+
+On sign-off (recorded in docs/01 §2): `Configure` may build, on the phase-space
+grid, `a_ξ = (inv_Lq/ρ̂_θi)·x·Θ(y_c−y)` and
+`a_x = −(inv_Lq·ŵ²/4ρ̂_θi)·sinξ·Θ(y_c−y)` (with `ŵ = w_psi`, `ρ̂_θi` a new
+`Level0Physics` field, `Θ` from `y < 1`) and wire them into
+`Operators.ParallelStreaming`, replacing the gated `a_xi`/`a_x`. `c_D` is
+unchanged (the normalization was chosen to keep it `= ω̂_D`). This un-gates the
+`:streaming` family (QUESTIONS Q5). The `E×B`, gradient drive, collision
+magnitude, pitch measure, and far field remain gated.
diff --git a/docs/src/islands/derivations/passing-fraction.md b/docs/src/islands/derivations/passing-fraction.md
new file mode 100644
index 000000000..7776fb416
--- /dev/null
+++ b/docs/src/islands/derivations/passing-fraction.md
@@ -0,0 +1,121 @@
+# Derivation — the passing fraction ``f_p \simeq 1 - 1.46\sqrt\varepsilon``
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent derivation of the
+electron-closure passing-fraction constant (Decision D7), one of the deferred
+sub-constants of the flattened-electron closure (QUESTIONS Q3/Q5).
+**Status:** ✅ **signed off 2026-07-11** — implemented as
+`Coefficients.passing_fraction(ε) = 1 − 1.4624√ε`, which may populate
+`Fields.ElectronClosure.f_p`. This chapter derives the constant and cross-checks
+it numerically (§3); the reviewer accepted the `1.4624 ≈ 1.46` match (§4). The
+companion Hirshman–Sigmar `k` remains gated.
+
+## 1. What `f_p` is
+
+The flattened-electron closure (docs/01 §2.4; `electron-closure.md`) carries the
+**passing (circulating) particle fraction** ``f_p`` of a large-aspect-ratio
+circular flux surface — the velocity-space fraction of electrons that complete a
+poloidal circuit rather than mirror-trap in the outboard well.
+The sources quote ``f_p \simeq 1 - 1.46\sqrt\varepsilon`` `[CHECKED: I19
+Eq. (22); L23 Eqs. 2.5.5–2.5.8]`, i.e. ``f_p = 1 - f_t`` with the effective
+trapped fraction ``f_t \simeq 1.46\sqrt\varepsilon``.
+This chapter derives ``f_t`` and hence ``f_p``.
+
+## 2. The effective trapped-fraction integral
+
+Use the model field modulation pinned in `Coefficients.jl` (docs/01 §1),
+
+```math
+b(\theta) \;=\; \frac{B(\theta)}{B_{\max}} \;=\; \frac{1-\varepsilon\cos\theta}{1+\varepsilon},
+\qquad b\in[b_{\min},\,1],\quad b(\pi)=1 ,
+```
+
+\noindent
+so ``B_{\max}`` sits on the inboard side ``\theta=\pi``.
+A particle of pitch ``\lambda = \mu B_{\max}/E`` is trapped when ``\lambda b(\theta)=1`` somewhere on the surface, i.e. for ``\lambda\in(1,\,1/b_{\min})``; it is passing for ``\lambda<1``.
+The neoclassical **effective** trapped fraction is the pitch-space average
+(Lin-Liu & Miller; Wesson, *Tokamaks*)
+
+```math
+f_t \;=\; 1 - \frac{3}{4}\,\langle b^2\rangle
+ \int_0^{1}\frac{\lambda\,d\lambda}{\big\langle\sqrt{1-\lambda b}\,\big\rangle},
+\qquad
+\langle\cdot\rangle \equiv \frac{1}{2\pi}\oint d\theta ,
+```
+
+\noindent
+the same flux-surface average ``\langle\cdot\rangle_\theta`` the drift brackets
+use (`omega-D-drift-frequency.md`).
+
+## 3. The ``\varepsilon\to0`` limit and the ``\sqrt\varepsilon`` coefficient
+
+At ``\varepsilon=0`` the field is uniform (``b\equiv1``, ``\langle b^2\rangle=1``), and
+
+```math
+\int_0^1\frac{\lambda\,d\lambda}{\sqrt{1-\lambda}}
+ \;=\; B(2,\tfrac12) \;=\; \frac{\Gamma(2)\,\Gamma(\tfrac12)}{\Gamma(\tfrac52)}
+ \;=\; \frac{4}{3},
+\qquad\Rightarrow\qquad
+f_t(0) = 1-\tfrac34\cdot1\cdot\tfrac43 = 0 ,
+```
+
+\noindent
+every particle circulates when there is no well — the correct zeroth order.
+The leading correction is ``O(\sqrt\varepsilon)``, not ``O(\varepsilon)``:
+it comes from the boundary layer near ``\lambda=1``, where ``1-\lambda b(\theta)``
+vanishes over part of the circuit (the barely-passing/barely-trapped particles),
+so ``\langle\sqrt{1-\lambda b}\rangle`` acquires a ``\sqrt{\varepsilon}``-scale
+behaviour.
+Writing ``f_t = c_1\sqrt\varepsilon + O(\varepsilon)``, the coefficient ``c_1``
+is the ``\varepsilon\to0`` limit of ``f_t/\sqrt\varepsilon``.
+
+Evaluating the integral numerically in this convention (QuadGK, the same
+machinery as the cleared brackets) gives a clean limit:
+
+| ``\varepsilon`` | ``f_t`` | ``f_t/\sqrt\varepsilon`` |
+|---|---|---|
+| ``10^{-2}`` | ``0.145171`` | ``1.451714`` |
+| ``10^{-3}`` | ``0.046214`` | ``1.461401`` |
+| ``10^{-4}`` | ``0.014623`` | ``1.462324`` |
+| ``10^{-5}`` | ``0.0046246`` | ``1.462415`` |
+
+\noindent
+so ``c_1 = 1.4624\ldots``, and
+
+```math
+\boxed{\;
+f_p \;=\; 1 - f_t \;=\; 1 - c_1\sqrt\varepsilon
+ \;\simeq\; 1 - 1.46\,\sqrt\varepsilon
+\;}
+```
+
+\noindent
+The derived leading coefficient ``1.4624`` matches the sources' quoted ``1.46``
+to three significant figures — the quote is the rounded asymptotic constant, not
+a distinct number.
+
+## 4. Cross-check table and the open sign-off item
+
+| Source | Form | Agrees with `[DERIVED]`? |
+|---|---|---|
+| I19 Eq. (22) / L23 §2.5 (via docs/01 §2.4) | ``f_p \simeq 1-1.46\sqrt\varepsilon`` | ✅ to 3 s.f. (``c_1=1.4624``) |
+| ``\varepsilon\to0`` analytic limit | ``f_t(0)=0`` (all-passing), ``B(2,\tfrac12)=\tfrac43`` | ✅ exact (§3) |
+| numerical asymptotics (this chapter) | ``c_1 = 1.4624\ldots`` | ✅ converged |
+
+**Open for the reviewer (sign-off gate):** this derivation uses the standard
+Lin-Liu–Miller *effective* trapped fraction. Confirm that I19 Eq. (22) /
+L23 §2.5 define ``f_p`` by this same effective fraction (and not a bare
+pitch-boundary fraction, which carries a slightly different ``O(\sqrt\varepsilon)``
+coefficient) before clearing. The ``0.16\%`` gap between ``1.4624`` and the
+quoted ``1.46`` is consistent with rounding, but a definition mismatch would show
+up here — hence the gate.
+
+## 5. What sign-off would authorize
+
+On sign-off (to be recorded in docs/01 §2.4): `f_p = 1 - 1.4624·√ε` (or the
+source's exact constant, if the reviewer pins it) may replace the `NaN`-gated
+`Fields.ElectronClosure.f_p`. Until then `f_p` stays gated (QUESTIONS Q3/Q5) and
+this chapter remains a **draft**, not a clearance. The companion deferred
+constants ``\langle\hat\nu_{ii}\rangle_u`` (collision) and the Hirshman–Sigmar
+``k`` (parallel flow) are **not** drafted here — each needs its specific source
+integrand (L23 Eq. 4.1.6; the parallel-viscosity moment problem) read in detail,
+and are left escalated in QUESTIONS Q3/Q5 rather than derived speculatively.
diff --git a/docs/src/islands/derivations/psi-tilde-amplitude.md b/docs/src/islands/derivations/psi-tilde-amplitude.md
new file mode 100644
index 000000000..981b04236
--- /dev/null
+++ b/docs/src/islands/derivations/psi-tilde-amplitude.md
@@ -0,0 +1,202 @@
+# Derivation — the island flux amplitude ``\tilde\psi``
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent re-derivation (Decision D7).
+**Resolves:** the open `[VERIFY]` on ``\tilde\psi`` (QUESTIONS Q4, docs/01 §1):
+is the amplitude ``\tilde\psi = \tfrac{w_\psi^2}{4}\,\tfrac{q_s'}{q_s}`` or
+``\tfrac{w_\psi^2}{4}\,\tfrac{q_s}{q_s'}``? **First-hand check of I19 (2026-07-11):
+Imada 2019 as printed (print p. 3, text following Eq. 6) shows the second form,
+``\tfrac{w_\psi^2}{4}\,\tfrac{q_s}{q_s'}``.** This derivation shows that form is
+a typo in the published paper — the physical amplitude is ``q_s'/q_s`` — by three
+independent arguments, including I19's *own* internally-inconsistent ``\Omega``
+convention (its Eq. 7).
+
+**Status:** ✅ **signed off 2026-07-11** (clearance recorded in docs/01 §1). The
+relation is implemented as `Moments.island_flux_amplitude`; the
+``\Delta_{\cos}/\Delta_{\sin}`` moment *prefactors* ``\mp\mu_0 R/(2\tilde\psi)``
+remain caller-supplied because the ``\mu_0 R`` normalization and the sin-moment
+normalization pin (docs/01 §4) are separate, still-open items.
+
+## 1. Setup and orderings
+
+Level-0 configuration (docs/01 §1, orderings O1–O3): a single-helicity,
+constant-``\psi`` magnetic island of helicity ``(m, n)`` at the rational surface
+``\psi = \psi_s`` where ``q(\psi_s) = m/n``. Coordinates: ``\psi`` the poloidal
+flux, ``\theta`` poloidal angle, ``\phi`` toroidal angle, and the helical angle
+
+```math
+\xi = m\theta - n\phi ,
+```
+
+equivalent to the docs/01 form ``\xi = m(\theta - \phi/q_s)`` since
+``m/q_s = n``. The island is prescribed and fixed (O3): ``\tilde\psi`` is an
+input amplitude, not solved. The task is purely the **island geometry** — how
+``\tilde\psi`` relates to the island half-width ``w_\psi`` — so no kinetics
+enter.
+
+The perturbation is given in vector-potential form (docs/01 §1):
+
+```math
+A_\parallel = -\frac{\tilde\psi}{R}\cos\xi ,
+```
+
+where ``\tilde\psi`` is the amplitude of the perturbed **helical flux** (the
+factor ``1/R`` is the metric relation between the parallel vector potential and
+the poloidal-flux-like amplitude; ``A_\parallel`` has dimensions of
+flux/length, so ``\tilde\psi`` has dimensions of poloidal flux — used in §5).
+
+## 2. The equilibrium helical flux and its curvature
+
+Define the **helical flux** as the poloidal flux minus the resonant fraction of
+the toroidal flux ``\psi_{\rm tor}`` (with ``d\psi_{\rm tor}/d\psi = q``):
+
+```math
+\chi_0(\psi) \;=\; \psi \;-\; \frac{1}{q_s}\,\psi_{\rm tor}(\psi),
+\qquad
+\frac{d\chi_0}{d\psi} \;=\; 1 - \frac{q(\psi)}{q_s}.
+```
+
+This is the natural flux for the ``(m, n)`` resonance: its contours are the
+equilibrium field lines *projected into the helical frame* (a field line has
+``d\xi/d\theta = m - n\,q(\psi)``, which vanishes at ``\psi_s``), and
+
+```math
+\left.\frac{d\chi_0}{d\psi}\right|_{\psi_s} = 1 - \frac{q_s}{q_s} = 0 ,
+```
+
+so ``\chi_0`` is **stationary at the rational surface** — the defining property
+of a resonant flux. Its curvature there is the load-bearing quantity:
+
+```math
+\boxed{\;
+\chi_0''(\psi_s) \;=\; \frac{d}{d\psi}\!\left(1 - \frac{q}{q_s}\right)_{\psi_s}
+ \;=\; -\,\frac{q_s'}{q_s}
+\;}
+\qquad (q_s' \equiv dq/d\psi|_{\psi_s}).
+```
+
+## 3. The constant-``\psi`` island and its half-width
+
+Add the single-helicity perturbation of constant amplitude (the constant-``\psi``
+approximation, O3) to the equilibrium helical flux and expand about ``\psi_s``
+using ``\chi_0'(\psi_s)=0``, with ``x \equiv \psi - \psi_s``:
+
+```math
+\chi(x, \xi) \;=\; \chi_0(\psi_s) \;+\; \tfrac{1}{2}\,\chi_0''(\psi_s)\,x^2
+ \;+\; \tilde\psi\,\cos\xi .
+```
+
+The contours of ``\chi`` are the perturbed field lines; they form an island.
+The two stationary points on ``x = 0`` are ``(x,\xi) = (0, 0)`` and ``(0, \pi)``
+— one the O-point (elliptic), one the X-point (hyperbolic), their roles set by
+the signs of ``\chi_0''`` and ``\tilde\psi``. The **separatrix** is the contour
+through the X-point. Writing ``\chi_X`` for its value and evaluating the
+separatrix contour at the O-point's poloidal angle gives the maximum radial
+excursion ``x_{\rm sep}``; for either sign assignment,
+
+```math
+\tfrac{1}{2}\,|\chi_0''|\,x_{\rm sep}^2 = 2\,|\tilde\psi|
+\quad\Longrightarrow\quad
+x_{\rm sep} = 2\sqrt{\frac{|\tilde\psi|}{|\chi_0''|}} .
+```
+
+By definition ``w_\psi`` is the island **half-width** in ``\psi``-space (the
+maximum excursion of the separatrix from ``\psi_s``, docs/01 §1), so
+``w_\psi = x_{\rm sep}`` and
+
+```math
+w_\psi = 2\sqrt{\frac{|\tilde\psi|}{|\chi_0''|}}
+\quad\Longleftrightarrow\quad
+|\tilde\psi| = \frac{w_\psi^2}{4}\,|\chi_0''| .
+```
+
+## 4. Result
+
+Substituting ``|\chi_0''| = q_s'/q_s`` from §2:
+
+```math
+\boxed{\;
+\tilde\psi \;=\; \frac{w_\psi^2}{4}\,\frac{q_s'}{q_s}
+\;}
+```
+
+(with ``\tilde\psi \ge 0``, ``w_\psi`` the half-width, and ``q_s'/q_s`` taken as
+its magnitude; the sign is fixed by the pinned ``\Omega`` convention of §5). The
+alternative ``\tfrac{w_\psi^2}{4}\,\tfrac{q_s}{q_s'}`` is **excluded** — see the
+two independent checks below.
+
+## 5. Cross-checks
+
+**(a) Dimensional necessity (kills the ``q_s/q_s'`` form).** ``\chi_0`` is a
+flux, so ``\chi_0'' = d^2\chi_0/d\psi^2`` has dimensions ``[\psi]^{-1}``.
+``w_\psi`` is a half-width in ``\psi``-space, so ``w_\psi^2 \sim [\psi]^2``, and
+``\tilde\psi`` is a flux (``\sim [\psi]``, from ``A_\parallel = -(\tilde\psi/R)
+\cos\xi``, §1). Check each candidate:
+
+| candidate | dimensions | verdict |
+|---|---|---|
+| ``\tfrac{q_s'}{q_s}`` | ``[\psi]^{-1}`` (``q'/q``, since ``q'\sim[\psi]^{-1}``, ``q\sim 1``) | ``\tfrac{w_\psi^2}{4}\tfrac{q_s'}{q_s}\sim[\psi]^2[\psi]^{-1}=[\psi]`` ✓ = ``[\tilde\psi]`` |
+| ``\tfrac{q_s}{q_s'}`` | ``[\psi]`` | ``\tfrac{w_\psi^2}{4}\tfrac{q_s}{q_s'}\sim[\psi]^3`` ✗ |
+
+Only ``q_s'/q_s`` gives ``\tilde\psi`` the dimensions of a flux. The
+``q_s/q_s'`` form is dimensionally impossible.
+
+**(b) Consistency with the pinned island label ``\Omega``.** docs/01 §1 pins
+(``[CHECKED: I19 Eq. 7; Diss19 Eq. 2.7; L23 Eq. 2.1.8]``)
+
+```math
+\Omega(x,\xi) = \frac{2(\psi-\psi_s)^2}{w_\psi^2} - \cos\xi,
+\qquad \Omega = -1 \ (\text{O-point}),\ \ \Omega = +1 \ (\text{separatrix}).
+```
+
+Take the island-supporting branch ``\chi_0'' > 0`` with the O-point at
+``\xi = \pi`` (equivalently, absorb the signs into the orientation of ``\xi`` and
+the sign of ``\tilde\psi`` — this is the convention the ``\Omega`` label *fixes*,
+not an independent assumption). Normalizing the helical flux of §3 by
+``\tilde\psi`` and using the §4 result ``|\chi_0''|/\tilde\psi = 4/w_\psi^2``:
+
+```math
+\frac{\chi - \chi_0(\psi_s)}{\tilde\psi}
+= \frac{|\chi_0''|}{2\tilde\psi}\,x^2 - \cos\xi
+= \frac{2x^2}{w_\psi^2} - \cos\xi
+= \Omega .
+```
+
+(The bare substitution gives ``(\chi_0''/2\tilde\psi)x^2 + \cos\xi``; the O-point
+at ``\Omega=-1`` requires the ``\cos\xi`` term negative, which is exactly the
+sign convention just stated — the ``w_\psi^2/4`` magnitude coefficient, the
+load-bearing result, is the same on either branch.) The island label ``\Omega``
+**is** the ``\tilde\psi``-normalized helical flux,
+reproducing the pinned convention exactly (including the factor of 2 and the
+O-point/separatrix values). This both fixes the sign convention and confirms the
+``w_\psi^2/4`` coefficient self-consistently.
+
+## 6. Cross-check table against the [CHECKED] transcriptions
+
+| Source | Transcribed form (docs/01 §1) | Agrees with `[DERIVED]`? |
+|---|---|---|
+| Diss19 p. 30 | ``\tilde\psi = \tfrac{w_\psi^2}{4}\,q_s'/q_s`` | ✅ |
+| D21 / L23 Eq. (2.1.4) | ``q_s'/q_s`` | ✅ |
+| Ω convention (I19 Eq. 7; Diss19 Eq. 2.7; L23 Eq. 2.1.8) | ``\Omega = 2(\psi-\psi_s)^2/w_\psi^2 - \cos\xi`` | ✅ (reproduced in §5b) |
+| I19 as printed (p. 3, text after Eq. 6) — **first-hand, 2026-07-11** | ``\tilde\psi = \tfrac{w_\psi^2}{4}\,q_s/q_s'`` | ❌ dimensionally impossible (§5a); typo |
+| I19 Eq. (7) — its own ``\Omega`` convention (first-hand) | ``\Omega = 2(\psi-\psi_s)^2/w_\psi^2 - \cos\xi`` | ✅ — and this *requires* ``q_s'/q_s`` (§5b), so I19 is internally inconsistent |
+
+**Triage (docs/05 rule 3) — resolved:** the physics is unambiguous
+(``q_s'/q_s``), agrees with Diss19/D21/L23 and with I19's own ``\Omega``
+convention (Eq. 7), and the alternative is dimensionally impossible. I19 as
+printed shows ``q_s/q_s'`` in the amplitude text, but its Eq. (7) ``\Omega``
+requires ``q_s'/q_s`` — so **I19 is internally inconsistent, and the amplitude
+text is a published typo** (triage outcome: *their published-equation error*,
+the standing docs/05 York-lineage rule; the same class as the L23 §2.6
+amendments). Note I19 defines its shear length ``L_q = q\,(dq/dr)^{-1} = q/q'``
+(p. 2), consistent with ``q'/q`` structure. **The `[VERIFY]` is closed: use
+``q_s'/q_s``.**
+
+## 7. What sign-off authorizes
+
+On human sign-off (recorded in docs/01 with paper/equation/date, policy rule 3),
+the ``\tilde\psi`` amplitude
+``\tilde\psi = \tfrac{w_\psi^2}{4}\,(q_s'/q_s)`` may be used to construct the
+``\Delta_{\cos}/\Delta_{\sin}`` moment prefactors ``\mp\mu_0 R/(2\tilde\psi)``
+(`Moments.delta_moments`), replacing the currently-gated supplied argument. The
+sin-moment normalization pin (docs/01 §4, a separate `[DERIVED]` item) is **not**
+covered here.
diff --git a/docs/src/islands/derivations/quasineutrality-closure.md b/docs/src/islands/derivations/quasineutrality-closure.md
new file mode 100644
index 000000000..87e8a7d36
--- /dev/null
+++ b/docs/src/islands/derivations/quasineutrality-closure.md
@@ -0,0 +1,136 @@
+# Derivation — the Level-0 quasineutrality closure
+
+**Provenance:** `[DERIVED: 2026-07-11]` — independent re-derivation (Decision D7).
+**Clears (on sign-off):** the Level-0 quasineutrality relation and its closure
+coefficient ``1/(2\hat L_{n0})`` (`[CHECKED: I19 Eq. A.11; L23 Eq. 2.4.14;
+Picard form Diss19 Eq. 2.45]`, QUESTIONS Q3), plus the arbitrary-``\tau``
+generalization docs/01 §3 asks for.
+
+**Status:** ✅ **signed off 2026-07-11** (clearance recorded in docs/01 §3),
+after an independent triple-check of the δn normalization — implemented as
+`Coefficients.quasineutrality_coefficient(τ)` (``= τ/(τ+1)``). The code uses the
+raw-moment form, so I19's `δn_i` normalization is a cross-check nuance only.
+
+## 1. Setup
+
+At Level 0 the only field equation is quasineutrality, ``n_i[\Phi;g_i] =
+n_e[\Phi;\text{closure}]`` (docs/01 §3; Ampère is a diagnostic, O3). The two
+species' densities are the velocity moments of their responses (I19 Eqs. 17, 23,
+first-hand). This derivation takes those responses and solves ``n_i=n_e`` for
+``\Phi``, deriving the closure coefficient. Normalizations (docs/01 §5):
+``x=(\psi-\psi_s)/\psi_s``, ``\hat L_{n0}^{-1}=(\psi_s/n_0)\,dn_0/d\psi``,
+``\hat\Phi=e_i\Phi/T_i``, ``\tau=T_e/T_i``, ``\hat h=h/\psi_s`` (so ``\hat h\to x``
+far away, electron-closure derivation §3).
+
+## 2. The species densities
+
+**Ions** (I19 Eq. 23): ``f_i=(1-\tfrac{e_i\Phi}{T_i})F_{Mis}+(\psi-\psi_s)F'_{Mis}
++g_i``. The velocity moment (``e_i=+e``, ``Z_i=1``):
+
+```math
+n_i = n_0\Big(1-\frac{e\Phi}{T_i}\Big) + n_0'\,(\psi-\psi_s) + \delta\bar n_i,
+\qquad \delta\bar n_i=\int g_i\,d^3v ,
+```
+
+using ``\int F_{Mis}d^3v=n_0`` and ``\int F'_{Mis}d^3v=n_0'=dn_0/d\psi``.
+
+**Electrons** (flattened closure, electron-closure derivation; I19 Eq. 17):
+``f_e=(1-\tfrac{e_e\Phi}{T_e})F_{Mes}+h(\Omega)F'_{Mes}-\tfrac{Iv_\parallel}{\omega_{ce}}
+F'_{Mes}\partial_\psi h+\bar h_e``. The ``v_\parallel``-odd term vanishes in the
+density moment; with ``e_e=-e`` and the leading closure (``\bar h_e`` higher
+order),
+
+```math
+n_e = n_0\Big(1+\frac{e\Phi}{T_e}\Big) + n_0'\,h(\Omega) .
+```
+
+The ``n_0' h(\Omega)`` term **is** the electron density perturbation carried by
+the flattening — the flattened electrons pile up/deplete along ``\Omega`` surfaces
+exactly as ``h`` prescribes.
+
+## 3. Quasineutrality → the closure
+
+Impose ``n_i=n_e``. The equilibrium ``n_0`` cancels:
+
+```math
+-\,n_0 e\Phi\Big(\frac{1}{T_i}+\frac{1}{T_e}\Big)
+ = n_0'\big[h(\Omega)-(\psi-\psi_s)\big] - \delta\bar n_i .
+```
+
+The ``(1/T_i+1/T_e)`` is the **sum of the ion and electron adiabatic responses**
+— both species shield the potential — and is the origin of the closure
+denominator. Solving for ``\hat\Phi=e\Phi/T_i`` and using
+``T_i(1/T_i+1/T_e)=1+T_i/T_e=(\tau+1)/\tau``:
+
+```math
+\boxed{\;
+\hat\Phi = \frac{\tau}{\tau+1}\,
+ \Big[\, \frac{\delta\bar n_i}{n_0} + \hat L_{n0}^{-1}\big(x-\hat h(\Omega)\big) \,\Big]
+\;}
+```
+
+using ``n_0'(\psi-\psi_s)/n_0=\hat L_{n0}^{-1}x`` and
+``n_0'h/n_0=\hat L_{n0}^{-1}\hat h``.
+
+**At ``\tau=1``** (the sources' ``T_e=T_i``), ``\tau/(\tau+1)=1/2``:
+
+```math
+\hat\Phi = \frac{1}{2}\Big[\frac{\delta\bar n_i}{n_0}+\hat L_{n0}^{-1}(x-\hat h)\Big]
+ = \frac{1}{2\hat L_{n0}}\Big[\underbrace{\hat L_{n0}\,\frac{\delta\bar n_i}{n_0}}_{\equiv\,\delta n_i/n_0}
+ + x - \hat h\Big],
+```
+
+**exactly I19 Eq. A.11**, ``\hat\Phi=[\delta n_i/n_0+x-\hat h]/(2\hat L_{n0})``,
+provided I19's normalized ion perturbation is
+``\delta n_i/n_0\equiv\hat L_{n0}\,\delta\bar n_i/n_0``. This is a
+**normalization convention, not a discrepancy**, and it is *physically forced*:
+the raw kinetic moment ``\delta\bar n_i=\int g_i\,d^3v`` is gradient-**driven**
+(``g_i`` is sourced by the Maxwellian-gradient terms of the drift-kinetic
+equation, so ``\delta\bar n_i\propto\hat L_{n0}^{-1}`` already), so I19 factors
+that common gradient out for a uniform bracket. The physically-invariant
+statement is the **boxed general-``\tau`` form of §3 with the raw moment
+``\delta\bar n_i``**, whose ``x-\hat h`` piece and coefficient match I19 exactly.
+
+**Implementation note (removes any convention ambiguity from `src`):** the code
+uses the **raw-moment form** — ``\delta\bar n_i`` is the actual velocity moment
+``M[g_i]`` that `velocity_moment!` already computes — so `Operators.Quasineutrality`
+never references I19's ``\delta n_i`` normalization. The un-reverified I19 scaling
+affects only the *cross-check reading* of A.11, not what is built.
+
+## 4. Kinetic-electron (Picard) form
+
+When electrons are solved kinetically (E4 toggle, not the flattened closure), the
+same quasineutrality reads ``\delta\hat\Phi=(\delta\hat n_i-\delta\hat n_e)/2``
+(Diss19 Eq. 2.45) — the ``\hat h`` term is replaced by the kinetic electron
+density perturbation ``\delta\hat n_e``. In Islands both are one residual block
+inside the global Newton system (`Operators.Quasineutrality`); the sources'
+nested Picard loop is what Newton–Krylov replaces (docs/01 §3).
+
+## 5. Cross-check table
+
+| Source | Form | Agrees with `[DERIVED]`? |
+|---|---|---|
+| I19 Eq. (A.11) (first-hand, print p. 11) | ``e_i\Phi/T_i=[\delta n_i/n_0+x-\hat h(\Omega)]/(2\hat L_{n0})`` | ✅ (``\tau=1`` limit of §3; ``\delta n_i`` normalization convention noted) |
+| L23 Eq. (2.4.14) | same closed form | ✅ |
+| Diss19 Eq. (2.45) | Picard form ``\delta\hat\Phi=(\delta\hat n_i-\delta\hat n_e)/2`` | ✅ (§4) |
+| docs/01 §3 (τ general) | "keep ``\tau=T_e/T_i`` general and flag departures" | ✅ — the boxed ``\tau/(\tau+1)`` form delivers it |
+
+**Triage:** no discrepancy. The one subtlety (the ``\delta n_i`` normalization)
+is a definitional convention, resolved in §3; the derivation additionally
+supplies the arbitrary-``\tau`` generalization the sources omit
+(they assume ``T_e=T_i``).
+
+## 6. What sign-off authorizes — now implemented (2026-07-11)
+
+On sign-off (recorded in docs/01 §3): the closure coefficient
+``\tau/(\tau+1)`` (``\to 1/2`` at ``\tau=1``) and the ``\hat L_{n0}^{-1}(x-\hat h)``
+structure populate `Operators.Quasineutrality`'s residual. **Implemented:** the
+field residual is ``R_\Phi = M[g]-\alpha\hat\Phi + S`` with
+``\alpha = (\tau+1)/\tau`` (the reciprocal of the closure coefficient — solving
+the boxed relation for the residual root; `Configure.configure_level0` builds it
+as `1/quasineutrality_coefficient(τ)`) and the drive
+``S = \hat L_{n0}^{-1}(x-\hat h(\Omega))`` from `Configure.quasineutrality_source`
+(the ``\hat h`` amplitude ``w/2\sqrt2`` from the cleared `Coefficients.h_amplitude`,
+the profile from `Fields.h_profile`). The moment machinery (`velocity_moment!`)
+and the ``\hat h``/``Q`` functions were already implemented; nothing here
+authorizes the ``\Delta`` prefactors (a separate, also-cleared derivation).
diff --git a/docs/src/islands/design/00-roadmap.md b/docs/src/islands/design/00-roadmap.md
new file mode 100644
index 000000000..13708023c
--- /dev/null
+++ b/docs/src/islands/design/00-roadmap.md
@@ -0,0 +1,306 @@
+# 00 — Roadmap: the Level structure
+
+Each Level relaxes specific orderings of the Imada 2019 / Dudkovskaia / Leigh
+lineage (reference library: docs/08). The code never branches on "which level"
+— levels are *configurations* of the operator stack (docs/03), so any
+intermediate combination of toggles is legal. A Level is "done" when: (i) its
+verification gate below is green (docs/05), (ii) the Physics Book chapters
+covering its equations are complete and [VERIFY]-cleared, and (iii) its
+manuscript in the paper series is submission-ready, with every claim backed by
+a ladder ID and every figure regenerable from archived data (docs/07). Papers
+I–VI map to gates as defined in docs/07 §3; each level *starts* by writing the
+paper outline (the figure contract), not ends with it.
+
+---
+
+## Level 0 — DK-NTM reproduction (the benchmark configuration)
+
+**Orderings retained** (the I19/L23 set):
+- O1. Large aspect ratio ε ≪ 1, circular concentric surfaces, low β.
+- O2. Radially local: w ≪ r_s, constant background gradients across the domain.
+- O3. Prescribed island: single-harmonic, constant-ψ, fixed w (half-width
+ convention, docs/01 §1). No Ampère solve.
+- O4. Fixed equilibrium-E_r parameter ω_E (≡ −ω₀, the island propagation
+ frequency in the zero-E_r frame; docs/01 §5). The published York
+ thresholds sit at ω_E = 0; Islands treats ω_E as a scanned input from day
+ one (D23b already does), because Δ_pol ∝ ω_E² with a sign reversal near
+ −0.89 ω_dia,e makes single-ω_E polarization values misleading.
+- O5. Timescale ordering ω, ω_*, ω_D ≪ ω_bounce (orbit-averaged leading order
+ at fixed p_φ → 4D).
+- O6. Momentum-conserving pitch-angle collision model, banana regime ν_★ ≪ 1
+ (exact operator + the energy-dependence sub-toggle: docs/01 §2.3).
+- O7. Ions drift-kinetic; electrons via the WCHH96 analytic closure (flattened
+ h(Ω) profile + coupled parallel-flow relation, docs/01 §2.4), with
+ `electrons = :kinetic` (the RDK-NTM treatment) available as the E4
+ toggle.
+- O8. No perpendicular transport operator (w_d physics external).
+- O9. Maxwellian backgrounds, single bulk ion species *in the physics* — but the
+ species list is a first-class array from day one (docs/02). Multi-species
+ *plumbing* is a Level 0 requirement even though multi-species *physics*
+ is Level 1+.
+
+**Critical architectural decision made at Level 0 even though it only pays at
+Level 3:** discretize in (x, ξ), not island coordinates Ω or drift-surface
+coordinates S. Island/drift coordinates presuppose a separatrix and cannot
+represent shielded linear states; (x, ξ) representation makes shielding,
+penetration, and saturated islands points on one solution manifold. Island
+flux-surface averages are *diagnostics*. The RDK S-coordinate solve path
+exists as a cross-check mode (its full coefficient set is published: Diss19
+Eqs. D.60–D.62, D23b Eq. 19 + App. A), never as the primary representation.
+
+**Prior-art baseline to beat (new since the original plan):** kokuchou (L23)
+is a direct 4D implementation of this exact level and documents where it
+breaks: ν_★ floor 5×10⁻³ and ŵ ceiling 0.75 ρ̂_θi set by memory + separatrix
+resolution, Picard non-convergence, a singular trapped-passing matching
+matrix, and a spurious solution branch from Neumann far-field BCs (docs/04
+§§2–3, 6). Islands' architecture (matrix-free Newton–Krylov, adaptive
+layer-packed grids, neoclassical-matching BCs) is chosen point-by-point
+against that failure list. Getting *below* kokuchou's ν_★ floor while matching
+its thresholds is the headline Level-0 numerics deliverable.
+
+**Outputs:** Δ_cos(w, ω_E; p), Δ_sin(w, ω_E; p); flux-surface profiles (n, T,
+Φ, flows) across the island; J_∥(x, ξ) with species/channel partitions.
+
+**Gate** (tiered per Decision D9 — the gate is the tiered B-ladder green, not
+absolute-number matches; docs/05 "Target tiers"): (i) large-w *scalings*
+Δ_bs+Δ_cur ∝ 1/w and Δ_pol ∝ 1/w³ (B2, T3), the WCHH96 Eq. (85) coefficient
+audit-gated; (ii) the drift-model **toggle differential** — the ~×6 w_c shift
+:original → :improved measured within Islands (B5b, T2, the robust form of the
+sources' 8.73 → 1.46 ρ_bi story) — plus threshold *existence* at w_c ~ O(ρ_θi)
+and kokuchou's dw_c/dν_★ > 0 trend (B5a/B5c, T3); the absolute w_c values
+(2.76/0.45 ρ_θi, the 0.440… fit surface) are T4, reportable only with input
+manifests; (iii) separatrix-layer polarization structure and the *existence and
+ω_E²-scaling* of the Δ_pol(ω_E) sign reversal (B4, B6, T3), its −0.89 location
+T4.
+
+---
+
+## Level 1 — Arbitrary collisionality + multi-species collisions
+
+**Relaxes:** O6.
+
+- Full linearized multi-species Fokker–Planck operator (momentum- and
+ energy-conserving; field-particle terms between all species pairs).
+ Bootstrap physics is unforgiving about non-conservative collision operators —
+ this is a correctness requirement, not a nicety.
+- Brute-force numerical resolution of the trapped-passing dissipation layer and
+ the separatrix layer at arbitrary ν (both widths ∝ ν^{1/2} at low ν, with the
+ E×B-dominated regime where the layer width tracks the iterating potential —
+ docs/04 §2). These are the layers RDK-NTM handles analytically and that set
+ kokuchou's operating floor; Level 1 owns beating them numerically.
+- **Tungsten physics lands here** (docs/02 §W): mixed-regime neoclassics (W in
+ Pfirsch–Schlüter/plateau while bulk ions are banana), ion–impurity friction
+ modification of the bootstrap drive, Z_eff effect on electron channel.
+ Deliverable: predicted in-island impurity density asymmetries, comparable to
+ AUG/DIII-D W-accumulation measurements.
+
+**Gate:** (i) in the no-island limit, bootstrap current and multi-species
+neoclassical flows vs. NEO/NCLASS across ν_★ (this doubles as the single most
+powerful global correctness check of the velocity-space discretization);
+(ii) Sauter coefficients recovered in the large-w limit across banana–plateau–PS;
+(iii) smooth connection of Level-0 low-ν results to collisional regimes,
+including closing the gap between kokuchou's ν_★ ≥ 5×10⁻³ window and
+RDK-NTM's ν_★ ≤ 10⁻³ window — the two prior codes never overlapped cleanly
+(ladder B7).
+
+---
+
+## Level 2 — General geometry, drop orbit averaging, energetic particles
+
+**Relaxes:** O1, O5, O2 (partially), O9 (backgrounds).
+
+- Retain poloidal angle: the kinetic problem becomes 5D (x, ξ, θ; λ, E).
+- Geometry arrives in two steps: **Miller analytic parametrization first**
+ (κ, δ, s_κ, s_δ, Shafranov shift — exactly D23a's geometry, giving direct
+ benchmark access to its shaping results), then equilibrium ingested from the
+ same numerical representations DCON/GPEC use (docs/03 §interfaces); shaped,
+ finite-β (the finite-β drift terms are the D23a Eq. 28–31 extension).
+ Analytic circular remains a regression toggle.
+- Widened, potentially nonlocal radial domain (several ρ_θα), with background
+ profile *variation* across the domain as a toggle (relaxing strict locality).
+- **Energetic particles land here** (docs/02 §EP): slowing-down F₀ (touches drive
+ terms and collision drag), alpha finite-orbit-width nonlocality (the drift-island
+ shift is no longer perturbative), and the precession resonance ω ~ ω_D,α — a
+ collisionless polarization-type contribution to Δ with no fluid analog. Highest
+ physics novelty in the program for burning plasmas (ITER/SPARC NTM thresholds
+ with self-consistent alpha kinetics).
+- Because alphas and W are trace in density, their response solves are *linear in
+ the trace species* given the bulk fields — cheap post-processing passes with an
+ additive contribution to Δ_cos/Δ_sin (SpeciesRole mechanism, docs/02 §roles).
+
+**Gate:** (i) general-geometry neoclassics vs. NEO; (ii) orbit frequencies
+(bounce, transit, precession) vs. analytic large-aspect-ratio formulas and a
+standalone orbit integrator; (iii) Level-0 results recovered when the circular
+low-β toggle set is applied; (iv) D23a shaping/finite-β targets with exact
+numbers (ladder C4: triangularity 2w_c 1.82 → 2.90 ρ_bi across δ = +0.42 →
+−0.5; the ε ≈ 0.3 crossover from w_c ∝ ε^{1/2}ρ_θi to ∝ ρ_θi; β_θ trend vs.
+EAST 91972).
+
+---
+
+## Level 3 — Self-consistent electromagnetics: the unification level
+
+**Relaxes:** O3 (and enables retiring O7).
+
+- Solve Ampère's law for the resonant helical harmonic(s) of A_∥ alongside the
+ kinetics. Island width/shape becomes an *output*; the external drive enters as
+ a boundary condition carrying Δ'(w) and/or the error-field amplitude from the
+ outer-region code.
+- Multiple ξ-harmonics; island deformation.
+- Kinetic (or reduced-fluid) electrons become necessary here: shielding currents
+ are carried by electrons; the flattened-electron closure O7 cannot shield.
+ (The RDK-NTM kinetic-electron machinery, already the E4 toggle, is the
+ starting point.)
+- **Small-amplitude limit = linear layer problem.** Verification against SLAYER's
+ Δ(Q) across its drift-MHD regimes. **Dependency note:** the SLAYER Δ(Q)
+ implementation arrives with GPEC's Tearing module work (PR #238,
+ `feature/tearing-growthrates`: `src/Tearing/InnerLayer/SLAYER/` Riccati layer
+ model + GGJ under the same interface, dispersion root-finding, and the
+ `delta_prime_raw` outer-region Δ′). That PR is sequenced to land before Islands
+ work begins (docs/06 §1); ladder D1's in-CI form then calls it directly.
+ The transition regime w ~ δ_layer (penetration bifurcation, kinetic) is the
+ flagship new-physics deliverable.
+- De-risking sub-track (strongly recommended, can start during Level 1): a
+ fluid-electron reduced configuration of Level 3 — essentially a kinetic-ion
+ analog of nonlinear cylindrical two-fluid codes (TM1-class) — to shake out the
+ (x, ξ) electromagnetic solve before full kinetics arrive.
+
+**Gate:** (i) linear limit vs. SLAYER Δ(Q) curves in shared regimes; (ii)
+constant-ψ recovery at small Δ' and w ≫ δ_layer; (iii) fluid-limit toggles vs.
+an established nonlinear cylindrical code (TM1 / XTOR-2F-class case); (iv)
+qualitative reproduction of Fitzpatrick's penetration bifurcation.
+
+---
+
+## Level 4 — Closures: rotation, transport, radiation
+
+**Relaxes:** O4, O8.
+
+- **Torque balance:** ω_E becomes an unknown closed by the Δ_sin = 0 root (the
+ sin ξ Ampère projection *is* the torque-balance condition — Diss19 Eq. 2.10)
+ plus flux-surface-averaged momentum balance against viscous/NTV restoring
+ torques (the in-repo KineticForces module is the natural NTV source),
+ appended to the Newton system — the nonlinear analog of SLAYER's
+ torque-balance closure. Multiple roots exist (Diss19 Fig. 4.18 found ±0.93,
+ ±1.28 ω_dia,e); continuation must track root branches, not assume
+ uniqueness. Quasi-static evolution: dw/dt from the MRE assembly, solved as a
+ sequence of steady states (arclength continuation in time-like parameter).
+- **Perpendicular transport operator:** model χ_⊥ (and D_⊥) as an explicit
+ operator, bringing Fitzpatrick's w_d threshold *inside* the fundamental
+ equation. Documented honestly as a closure knob (turbulence–island interaction
+ is not first-principles here).
+- **Radiative/thermo-resistive channel for W:** energy transport closure with a
+ radiation sink L_Z(T_e) n_W n_e inside the island and η(T_e) coupling — the
+ radiation-driven island / density-limit mechanism (Gates & Delgado-Aparicio
+ class). Requires Level 3 (η enters Ampère/Ohm) + Level 1 W transport.
+
+**Gate:** (i) w_d scaling vs. Fitzpatrick 1995; (ii) penetration thresholds with
+self-consistent torque balance vs. SLAYER-based thresholds in the linear limit
+and vs. empirical scalings in trend (La Haye database context, ladder B9);
+(iii) radiation-driven island growth vs. published thermo-resistive island
+models.
+
+---
+
+## Milestone sequencing (dependency graph, not a schedule)
+
+```
+M0 repo + CLAUDE.md + docs (this) ──┐
+M1 phase-space grids + operator stack skeleton + AD │ Level 0
+M2 L0 single-species solve, Δ moments, York gates │
+ → Paper I ──┘
+M3 FP collision operator + NEO cross-check ──┐ Level 1
+M4 W minority: friction/bootstrap + asymmetry result │
+ → Paper II ──┘
+M5 Miller + general geometry + 5D + orbit benchmarks ──┐ Level 2
+M6 slowing-down F0 + alpha trace response + ω_D res. │
+ → Paper III ──┘
+M7 fluid-electron (x,ξ) EM solve [start after M2] ──┐
+M8 kinetic-electron Ampère; SLAYER-limit gate │ Level 3
+M9 w ~ δ_layer transition study │
+ → Paper IV (flagship) ──┘
+M10 torque balance + χ⊥ + w_d gate ──┐
+M11 radiative W channel │ Level 4
+ → Paper V │
+M12 Δ-surface dataset + emulator release → Paper VI ──┘
+```
+
+Documentation infrastructure (Physics Book skeleton, anchor-sync CI, figure
+pipeline, STATE dashboard — docs/07) is part of M0–M1, not deferred: the first
+operator merged is the first operator anchored.
+
+Parallelism: M3–M4 and M7 are independent of each other; M5–M6 depends on M3
+(collision operator) but not M7.
+
+**Sequencing against GPEC:** the Tearing module PR (#238,
+`feature/tearing-growthrates` — SLAYER + GGJ inner layers, dispersion solver,
+Δ′ machinery) lands **before** M0. If M0 starts while #238 is still open, the
+Islands branch is cut from `feature/tearing-growthrates` rather than `develop`,
+so the SLAYER/Δ′ interfaces Islands consumes are in hand from the first commit.
+
+## Risk register
+
+| Risk | Level | Mitigation |
+|---|---|---|
+| Separatrix + trapped-passing layers unresolvable at low ν without RDK-style analytics — **confirmed by kokuchou hitting a ν_★ = 5×10⁻³ floor** | 0–1 | Mapped/adaptive grids clustered at both layers using the now-known ∝ν^{1/2} width estimates (docs/04 §2); matrix-free removes kokuchou's memory wall; RDK reduction retained as cross-check; accept and document a ν floor per resolution tier |
+| Trapped-passing matching block is intrinsically singular; plain linear algebra yields silent noise, not errors | 0+ | Explicit regularized (TSVD-style) treatment of the y_c block in the preconditioner; smallest-singular-value CI monitor (ladder A8); basis-change spike in M1 (docs/04 §3) |
+| Separatrix-layer width depends on Φ̂ and moves between nonlinear iterations (E×B-dominated regime) | 0–1 | Pack from lower-bound width estimates over the expected Φ̂ range; post-solve validation of layer resolution; re-mesh-and-continue fallback (docs/04 §2) |
+| Far-field BCs admit spurious solution branches (kokuchou's "winged" states under Neumann) | 0+ | Neoclassical-matching far-field BCs, never bare Neumann; continuation warm-starts; spurious-branch detection via far-field flow comparison against no-island neoclassics (docs/01 §3) |
+| Published equation sets in the lineage contain errors (L23 §2.6 amendment list against I19 Eq. A.1) | 0 | Independent re-derivation before implementation ([VERIFY]/[DERIVED] policy); benchmark against L23-amended physics; standing triage category in docs/05 reporting rules |
+| (x, ξ) small-amplitude limit fails to reproduce delicate linear layer structure | 3 | This is *the* physics risk. De-risk via M7 fluid track; verify against SLAYER regime-by-regime; budget the painful months here |
+| In-repo SLAYER not merged yet (on `develop` it is a placeholder; the implementation lives on PR #238 `feature/tearing-growthrates`) | 3 | Sequence #238 before M0; branch Islands from `feature/tearing-growthrates` if starting earlier; fall back to published Park 2022 curves only if that branch stalls |
+| Non-conservative collision discretization poisons bootstrap | 1 | NEO no-island cross-check as a CI-level gate; conservation tests as unit tests |
+| ω_E sensitivity of polarization makes single-point Δ misleading (Δ_pol ∝ ω_E², sign flip near −0.89 ω_dia,e; L23's anomalous electron Δ_pol at ω_E = 0) | 0–4 | Always publish Δ as surfaces over (w, ω_E), never single points, until Level 4 closes ω_E; E4/E6 toggle studies address the open electron-Δ_pol question |
+| 5D cost explosion at Level 2 | 2 | Orbit-averaged (4D) mode retained as toggle; trace-species linear passes; emulator strategy assumes expensive solves |
+| Turbulence–island interaction hiding in χ⊥ | 4 | Explicit closure-knob documentation; sensitivity scans part of every Level-4 result |
+
+## Decision log (append-only)
+
+- D1 (adopted): primary representation (x, ξ), never Ω or S. Rationale: Level-3
+ unification; island coordinates cannot represent shielded states.
+- D2 (adopted): steady-state Newton–Krylov + continuation, not initial-value
+ time-stepping and not nested Picard loops. Rationale: Δ-surface generation is
+ the product; continuation produces it as a byproduct; bifurcation tracking
+ needs it; kokuchou's documented Picard non-convergence (L23 §6.1.1) is the
+ empirical case against the alternative.
+- D3 (adopted): species list first-class at Level 0. Rationale: retrofit cost ≫
+ upfront cost; trace-role machinery needed by both W and EP tracks.
+- D4 (adopted): Julia, as a submodule of the GeneralizedPerturbedEquilibrium
+ package (`src/Islands/`, `module Islands` — no separate Project.toml).
+ Rationale: AD through the operator stack (exact Jacobians + ∂Δ/∂p
+ sensitivities), and GPEC-stack affinity (direct calls to the Δ′/SLAYER/
+ equilibrium machinery, docs/06 §1).
+- D5 (open): velocity coordinates (λ, E) vs. (v_∥, v_⊥) vs. (θ_b-aligned).
+ Default (λ, E) with σ = sgn(v_∥); revisit at Level 2 when θ is retained.
+ Note: prior art all uses y = λB_max with the y_c = 1 boundary; the singular
+ matching block (docs/04 §3) is a point against inheriting it unexamined.
+- D6 (open): kinetic electron treatment at Level 3 — full DKE vs. reduced
+ (parallel-kinetic) electron model. Decide after M7 results. The RDK-NTM
+ kinetic-electron formulation (Diss19 Eq. D.61) is the full-DKE candidate.
+- D7 (adopted; ratified by the user 2026-07-08, QUESTIONS Q2): implement Level-0
+ physics from an independent re-derivation cross-checked against the
+ L23-amended equation set, treating I19 Eq. (A.1) as printed as known-errata;
+ ω_E enters as a scanned input parameter at Level 0 (not deferred to L4).
+ Rationale: L23 §2.6 amendment list; D23b ω_E-parametric formulation.
+ Clearance mode (user, 2026-07-08): **re-derivation first** — the Q3
+ coefficient set is cleared by human sign-off of in-repo derivations
+ (`docs/src/islands/derivations/`), not of literature transcriptions.
+- D8 (adopted; ratified by the user 2026-07-08, QUESTIONS Q2): benchmark grid =
+ the three-code triangle (DK-NTM published numbers, RDK-NTM improved-model
+ numbers, kokuchou finite-ν_★ surface) with the B5a/B5b/B5c configurations
+ pinned in docs/05, superseding the single "York thresholds" gate item.
+- D9 (adopted; user-directed 2026-07-11): **verification targets are tiered by
+ reproducibility** (docs/05 "Target tiers and reproducibility"). Reproducing a
+ published *absolute* number requires every input of the source's exact
+ scenario, and the lineage under-specifies these (B5a's own collisionality is
+ internally contradictory) — so the primary literature-facing physics gates are
+ **scalings, trends, and internal differentials** (T1 exact math / T2 internal
+ cross-checks & toggle ratios / T3 scaling-and-existence vs. literature), and
+ absolute-number reproduction (T4) is aspirational and **audit-gated**: never
+ pass/fail without a published input manifest, and downgraded to T3 where the
+ source is under-specified. Clarifies (does not revoke) D8 — the three-code
+ triangle stays the comparison set, but internal differentials, not absolute
+ matches, are the sharp quantitative claims. Rationale: the SLAYER-validation
+ precedent (Park 2022 / Burgess 2026, docs/08 B26) validates by regime scalings,
+ not single points; and a fifth triage outcome ("under-specified source
+ configuration") is added.
diff --git a/docs/src/islands/design/01-physics-level0.md b/docs/src/islands/design/01-physics-level0.md
new file mode 100644
index 000000000..aa55cc5e6
--- /dev/null
+++ b/docs/src/islands/design/01-physics-level0.md
@@ -0,0 +1,399 @@
+# 01 — Level 0 physics formulation
+
+Scope: the equation set for the benchmark configuration (roadmap O1–O9).
+
+**Tag semantics (updated 2026-07-07).** The full source set now lives in-repo at
+`docs/resources/Drift_Kinetic_Island_References/` (see docs/08 for the library
+map). Expressions below were transcribed from the PDFs and checked against them
+by AI extraction; these carry **[CHECKED: source, Eq./p.]** and still require
+one human sign-off before the corresponding [VERIFY] discipline is considered
+cleared (CLAUDE.md policy). Anything still uncertain carries **[VERIFY: ...]**
+with the specific question stated.
+
+Primary sources: Imada et al. NF 59, 046016 (2019) — **I19** (complete DK-NTM
+reference; the 2018 PRL 121, 175001 and JPCS 1125, 012013 are its compact
+antecedents); Dudkovskaia PhD dissertation, York 2019 — **Diss19** (full RDK
+derivation chain, Appendices C–E); Dudkovskaia et al. PPCF 63, 054001 (2021) —
+**D21**; Dudkovskaia et al. NF 63, 016020 (2023) — **D23a** (finite-β, shaped
+geometry); NF 63, 126040 (2023) — **D23b** (separatrix layer, polarization,
+ω-dependence); Leigh PhD thesis, York Dec 2023 — **L23** (the `kokuchou` code:
+amended DK-NTM equations, finite-ν★ thresholds, numerics forensics); Wilson,
+Connor, Hastie & Hegna, PoP 3, 248 (1996) — **WCHH96** (analytic electron
+closure and large-w limits).
+
+> **Load-bearing warning.** L23 §2.6 (pp. 59–60) documents concrete errors in
+> the *published* I19 equation set (Eq. A.1): a missing ρ̂_θi factor on the
+> ∂²ĝ/∂p̂² diffusion term (making it ∝ ρ̂²_θi), a missing ν̂_ii ρ̂_θi coefficient
+> on ∂ĝ/∂p̂, missing factors on the Maxwellian-gradient drive terms, a corrected
+> momentum-conserving term Û_∥i(ĝ + p̂F̂′_Ms), and a sign fix in the Δ_loc
+> relation. **Islands must implement from an independently re-derived equation
+> set benchmarked against L23's amended form, never from I19 Eq. (A.1) as
+> printed.** This is the empirical justification for the whole [VERIFY]
+> policy: the literature's O(1) coefficients are demonstrably not to be
+> trusted without re-derivation. [CHECKED: L23 §2.6]
+
+---
+
+## 1. Geometry and coordinates
+
+Local region around the rational surface ψ_s (minor radius r_s) of an m/n mode
+in a large-aspect-ratio circular tokamak, B₀ = I(ψ)∇φ + ∇φ×∇ψ, I = RB_φ,
+B ≈ B₀(1 − ε cos θ), ε = r_s/R₀ ≪ 1 [CHECKED: I19 Eq. (3)].
+
+- Radial coordinate: x ∝ ψ − ψ_s. **Pin one normalization and write the map to
+ the others**: I19 uses x = (ψ−ψ_s)/ψ_s with ŵ = w/r_s, ρ̂_θi = ρ_θi/r_s;
+ D21/D23b normalize radial quantities to the island width w_ψ
+ (ρ̂_θj = I V_Tj/(ω_cj w_ψ)); the 2018 PRL normalizes to ψ_s. These
+ inconsistent conventions across the same lineage are a transcription hazard —
+ Islands' own normalization (§5) is r_s-based, with conversion factors in one
+ place. [CHECKED: I19 p. 6; D21 Eq. 19; PRL Eq. (4) note]
+- Helical angle: ξ = m(θ − φ/q_s) (I19 Eq. (6)); Diss19/D21 use ξ = φ − q_s θ
+ with the cos nξ harmonic — same island, different angle multiplicity. Pin
+ Islands' ξ to the I19 form and document the map. **Island rest frame:** all
+ Level-0 solves are steady in this frame.
+- Poloidal angle θ is eliminated at leading order by orbit averaging at fixed
+ p_φ (O5); it reappears at Level 2.
+
+Perturbation (prescribed at Level 0, O3), single-helicity, constant-ψ:
+
+ A_∥ = −(ψ̃/R) cos ξ, ψ̃ = (w_ψ²/4)(q_s′/q_s), q_s′ = dq/dψ|_s
+ [CLEARED: human sign-off 2026-07-11 —
+ derivation docs/src/islands/derivations/psi-tilde-amplitude.md;
+ matches Diss19 p. 30, L23 Eq. (2.1.4), and I19's own Ω (I19 Eq. 7)]
+
+where **w_ψ is the island HALF-width in ψ-space**, w = w_ψ/(RB_θ) the
+half-width in minor radius. **[VERIFY] RESOLVED (2026-07-11):** first-hand check
+confirmed I19 as printed (print p. 3, text after Eq. 6) shows (w_ψ²/4)(q_s/q_s′)
+— a **published typo**. I19 is internally inconsistent: its own Ω convention
+(Eq. 7) requires q_s′/q_s, as do dimensional analysis and Diss19/D21/L23. The
+cleared form is **q_s′/q_s** (independent re-derivation, Decision D7; triage:
+their published-equation error, the docs/05 York-lineage standing rule).
+
+Island label and convention (pinned, matches every source in the lineage):
+
+ Ω(x, ξ) = 2(ψ−ψ_s)²/w_ψ² − cos ξ, Ω = −1 at O-point, Ω = +1 at separatrix
+ [CHECKED: I19 Eq. (7); Diss19 Eq. 2.7; L23 Eq. (2.1.8)]
+
+**All York threshold numbers are HALF-widths** (D23a abstract states
+"threshold magnetic island half-width"; L23 footnote p. 130 notes La Haye's
+experimental fits quote the *full* width w_marg = 2w_c). This half/full-width
+bookkeeping is pinned here and in docs/05.
+
+Even at Level 0, the *stored representation* of the field is A_∥(x, ξ) on the
+(x, ξ) grid (decision D1); Ω is computed, never fundamental.
+
+## 2. Kinetic equation
+
+Per species j, phase space (x, ξ, λ, v, σ) with pitch λ = μ/E (grid variable
+y = λB_max, trapped–passing boundary y_c = 1), v the speed, σ = sgn(v_∥). Split
+
+ f_j = (1 − e_j Φ/T_j) F_Mj(ψ_s) + g_j, [CHECKED: I19 Eq. (28) form; Diss19 Eq. 2.15]
+
+with F_M a Maxwellian carrying background gradients at r_s (strictly local,
+O2). The steady drift-kinetic equation in the island frame [CHECKED: I19 Eq. (8)]:
+
+ v_∥∇_∥f + v_E·∇f + v_b·∇f − (e_j/m_j v)(v_∥∇_∥Φ + v_b·∇Φ) ∂f/∂v = C_j(f)
+
+with v_E = B×∇Φ/B², v_b = −v_∥ b×∇(v_∥/ω_cj). Orderings: Δ = w/r ≪ 1;
+e_jΦ/T_j ~ g_j/F_M ~ Δ; B₁/B₀ ~ εΔ²; collisions O(Δ) below free streaming;
+ions retain ρ_θi ~ w (finite orbit width — the key relaxation), electrons have
+ρ_θe ≪ w. [CHECKED: I19 §1, §4; Diss19 p. 33]
+
+The radial coordinate is traded for the canonical momentum
+
+ p_φ = (ψ − ψ_s) − I v_∥/ω_cj [CHECKED: I19 Eq. (2)]
+
+and θ is annihilated by orbit averaging at fixed p_φ (passing: (1/2π)∮dθ;
+trapped: (1/2π)Σ_σ σ∫_{−θ_b}^{θ_b}dθ) [CHECKED: I19 Eq. (31); Diss19 Eq. 2.24].
+The master 4D equation for the orbit-averaged distribution Ḡ₀(p̂, ξ, y; v̂, σ)
+is **I19 Eq. (32)** (structure confirmed; coefficients subject to the L23 §2.6
+amendments — implement from re-derivation):
+
+ −m[ (p̂/L̂_q)Θ(y_c−y) + ρ̂_θi ω̂_D − (ρ̂_θi/2)⟨(1/v̂_∥)∂Φ̂/∂x⟩_θ ] ∂Ḡ₀/∂ξ|_p̂
+ + m[ (ŵ²/4L̂_q) sin ξ Θ(y_c−y) − (ρ̂_θi/2)⟨(1/v̂_∥)∂Φ̂/∂ξ⟩_θ ] ∂Ḡ₀/∂p̂
+ = ⟨(1/v̂_∥)Ĉ_ii(Ḡ₀)⟩_θ
+
+The three transport channels of the design (island-induced streaming, magnetic
+drift, E×B) are the three bracketed frequencies above; they map one-to-one onto
+the operator stack (docs/03 §2).
+
+**Island-streaming [CLEARED: human sign-off 2026-07-11 — derivation
+docs/src/islands/derivations/parallel-streaming.md]:** the streaming braces
+`(x/L̂_q)Θ(y_c−y) ∂_ξ` and `(ŵ²/4L̂_q) sinξ Θ(y_c−y) ∂_p̂` give, in the
+`c_D = ω̂_D` normalization (divide by −m ρ̂_θi), `a_ξ = (L̂_q⁻¹/ρ̂_θi) x Θ` and
+`a_x = −(L̂_q⁻¹ŵ²/4ρ̂_θi) sinξ Θ` — which factor exactly into
+`(L̂_q⁻¹ŵ²/4ρ̂_θi)Θ·{Ω, ·}`, advection along the island flux surfaces (a
+coefficient-free structural check). Implemented as
+`Configure.streaming_coefficients` → `Operators.ParallelStreaming`, passing-only
+(`Θ`), leaving the cleared `c_D` unchanged.
+
+**Gradient drive [CLEARED: human sign-off 2026-07-11 — derivation
+gradient-drive.md]:** the master equation (Eq. 32) is **homogeneous** — I19's
+`Ḡ₀ = p_φ(ω_si^T/ω_si)(n'/n)F_Mi + h̄₀` (Eq. 29, `ω_si^T/ω_si = 1+(v̂²−3/2)η_i`
+a temperature factor, *not* a frequency ratio) is the standard neoclassical drive
+`p_φ F'_Mi`, imposed as the **far-field boundary condition**, not an interior
+source. So `Operators.GradientDrive = 0` and the far field is
+`g_far = x L̂_{n0}⁻¹[1+(E−3/2)η_i]` (`Φ̂_far = 0` at `ω_E = 0`), built by
+`Configure.gradient_far_field`. No frame convention enters (Level-0, `ω_E = 0`).
+
+### 2.1 Magnetic drift frequency: the original/improved toggle (now precise)
+
+Orbit-averaged precession [CLEARED: human sign-off 2026-07-11 — derivation
+docs/src/islands/derivations/omega-D-drift-frequency.md; first-hand agreement
+with I19 Eq. (32), D21 Eqs. (B1), (A2), Diss19; no discrepancy]:
+
+ ω̂_D = [σv̂/(1+ε)] [ (1/L̂_q)⟨√(1−yb)/b⟩_θ − (1/2)(1/L̂_B)⟨(2−yb)/(b√(1−yb))⟩_θ ]
+
+The two terms are the shear-coupled drift-orbit-width precession (1/L̂_q, from the
+finite orbit width x_D = ρ̂_θi(σv̂/(1+ε))√(1−yb)/b) and the grad-B drift (1/L̂_B).
+
+- **:original** (I19/DK-NTM): finite constant L̂_B⁻¹ = (ψ_s/B)∂B/∂ψ — retains a
+ non-vanishing ∇B term after orbit averaging.
+- **:improved** (D21/RDK-NTM): Appendix A of D21 shows (from
+ ∂B/∂ψ = I′/R − (I/R²)∂R/∂ψ, low-β) ∂B/∂ψ = −(B_φ/(R₀²B_θ)) cos θ + O(ε²) — the
+ cos θ modulation makes ⟨cos θ·even⟩_θ = O(ε), so the term is ε-small after
+ orbit averaging; **L̂_B⁻¹ = 0 is the documented proxy** (D21 footnote 10, Fig. 8
+ compares proxy vs full cos θ form directly). [CLEARED 2026-07-11, same
+ derivation; the toggle is carried by MagneticDrift.variant.]
+
+This single toggle is what moved the threshold half-width 8.73 ρ_bi → 1.46 ρ_bi
+(D23a abstract). It is the archetype of the toggle-impact studies (docs/05 E1).
+
+### 2.2 The drift-island structure (must emerge from the solve, not be assumed)
+
+The exact drift-surface label [CHECKED: I19 Eq. (33); D21 Eq. 21; Diss19 Eq. 2.37]:
+
+ S = (ŵ²/4L̂_q)[ 2(p̂ − ρ̂_θi ω̂_D L̂_q)²/ŵ² − cos ξ ] Θ(y_c−y)
+ − p̂ ρ̂_θi ω̂_D Θ(y−y_c) − (1/2)⟨(ρ̂_θi/v̂_∥) Φ̂⟩_θ
+
+Passing particles: constant-S surfaces are the magnetic island **radially
+shifted by x_D = ρ̂_θi ω̂_D(y, v̂; σ) L̂_q** — σ-dependent, equal and opposite
+for v_∥ ≷ 0, pitch/energy-dependent through ω̂_D. (There is no separate
+"h(λ,E)" shift function in the sources; the shift *is* ρ̂_θi ω̂_D L̂_q. The
+symbol h(Ω) is reserved for the electron profile function, §2.4.) Flattening
+of f on drift islands rather than the magnetic island sustains pressure
+gradients across small islands (w ~ ρ_θi) and weakens the bootstrap drive —
+the kinetic threshold mechanism, carried by **passing** particles (D21 §7:
+passing-particle physics, not banana-orbit physics; ρ_bi is merely the natural
+unit at ε = 0.1). Trapped particles: S ∝ p̂ (no island structure); response
+tied to the magnetic island.
+
+In DK (4D direct) mode Islands does **not** impose S-structure; it must *emerge*.
+The RDK reduction — solve the 1D collisional constraint ⟨Ĉ/𝒜⟩_ξ^S g^(0,0) = 0
+per S-contour [CHECKED: D21 Eqs. 23–24; explicit coefficient forms Diss19
+Eqs. D.60–D.62 and D23b Eq. 19 + Appendix A] — is retained as a cross-check
+mode valid for δ_j = ν_j/(εω_b) ≪ 1.
+
+### 2.3 Collision operator (Level 0)
+
+Momentum-conserving pitch-angle (Lorentz) model [CLEARED: human sign-off
+2026-07-11 — derivation docs/src/islands/derivations/collision-operator.md;
+operator structure, deflection frequency, and ν_★ normalization agree first-hand
+with I19 Eqs. (9)–(12), L23 Eq. (2.3.40); no discrepancy]:
+
+ C_jj(f) = 2ν_jj(v)[ (√(1−λB)/B) ∂_λ( λ√(1−λB) ∂_λ f ) + v_∥ ū_∥j f /v²_thj · F_Mj-normalized ]
+ ū_∥j(f) = (1/(n⟨ν_jj⟩_v)) ∫d³v ν_jj v_∥ f (momentum restoring)
+ C_ei drags on the ION flow u_∥i (species coupling)
+
+The pitch-angle bracket is the Lorentz operator in self-adjoint form
+w⁻¹∂_λ(P∂_λ) with diffusivity P(λ) = λ√(1−λB) and measure w = B/√(1−λB) (the
+change of variables from the pitch cosine fixes the 2ν_jj prefactor exactly).
+λ-derivatives at **fixed ψ**, not fixed p_φ (a classic transcription trap).
+Energy dependence: two variants exist in the lineage and become a documented
+sub-toggle — I19/L23 use the full ν_jj(v) = ν̃_jj[φ(v̂) − G(v̂)]/v̂³ (Chandrasekhar
+G; needed for neoclassical fidelity), while Diss19/D21 use the simpler
+ν(V) ∝ V⁻³. Both diverge as v̂ → 0 (φ − G → (4/3√π)v̂ linear ⟹ ν̃ ~ v̂⁻² for the
+Chandrasekhar form, v̂⁻³ for the reduced), motivating the analytic velocity
+average L23 additionally derives:
+⟨ν̂_ii⟩_u = (4ε^{3/2}ν_★/3√π)(√2 − ln(1+√2)). **[This ⟨ν̂_ii⟩_u constant remains
+[CHECKED]-uncleared — its own short derivation is a deferred M2b sub-item;
+L23 Eq. 4.1.6, p. 88.]**
+
+Collisionality normalization [CLEARED 2026-07-11, same derivation]:
+ν_★ = ν_jj Rq/(ε^{3/2} v_th) (banana regime
+ν_★ ≪ 1); ν̂_jj = ε^{3/2}ν_★ ν̃_jj(u). [CHECKED: L23 Eq. (2.3.40); Diss19
+footnote 26]
+
+Replaced wholesale at Level 1 by the multi-species Fokker–Planck operator.
+
+### 2.4 Electrons at Level 0 (O7) — closure now exact
+
+ρ_θe ≪ w ⇒ electron drift islands coincide with the magnetic island. The
+analytic closure is WCHH96's, as used by I19/L23. The h(Ω) profile and its
+amplitude are [CLEARED: human sign-off 2026-07-11 — derivation
+docs/src/islands/derivations/electron-closure.md; the closure constraint
+⟨∂²h/∂x²⟩_Ω = 0 gives h′ = C/Q, far-field matching h → x gives C = w_ψ/2√2,
+matching I19 Eq. 18]:
+
+ f_e = (1 − e_eΦ/T_e) F_Mes + h(Ω) F′_Mes − (Iv_∥/ω_ce) F′_Mes ∂h/∂ψ + h̄_e
+ h(Ω) = Θ(Ω−1) (w_ψ/2√2) ∫₁^Ω dΩ′/Q(Ω′), Q(Ω) = (1/2π)∮√(Ω+cos ξ) dξ
+
+h(Ω) is exactly flat inside the separatrix, → x far away, and satisfies
+⟨∂²h/∂x²⟩_Ω = 0 (the closure constraint itself; unit-test target, L23 Eq. 4.1.1,
+green as ladder A7). Flux-surface-averaged electron flow — **structure**
+[CLEARED 2026-07-11], **constants k, f_p deferred** [CHECKED: I19 Eq. (22);
+L23 Eqs. 2.5.5–2.5.8]:
+
+ ⟨⟨Bu_∥e⟩_θ⟩_Ω/(B₀v_the) = −[f_t/(1+f_t)](Iv_the/ω_ce)(n′/n)(1 + η_e + ½ k f_c η_e)⟨∂h/∂ψ⟩_Ω
+ + [f_c/(1+f_t)] ⟨⟨Bu_∥i⟩_θ⟩_Ω/(B₀v_thi)
+
+with k ≃ −1.173 (Hirshman–Sigmar; **[CHECKED, uncleared — deferred, own
+derivation]**; unit-test: L23 reproduces −1.1730) and f_p ≃ 1 − 1.46√ε
+**[CLEARED: human sign-off 2026-07-11 — derivation passing-fraction.md;
+`Coefficients.passing_fraction(ε) = 1 − 1.4624√ε`, the effective trapped-fraction
+coefficient, = 1.46 to 3 s.f.]**. Note the electron current depends on the
+*numerically computed ion flow* (momentum conservation) — the closure is coupled, not
+one-way. Toggle `electrons = :flattened | :kinetic`: the `:kinetic` option is
+exactly RDK-NTM's defining feature (electrons solved with the same drift-island
+machinery as ions, Diss19 Eq. D.61 / D21 §5) and is *required* at Level 3
+(shielding); running it at Level 0 against `:flattened` is toggle study E4.
+
+## 3. Field equation (Level 0: quasineutrality only)
+
+ n_i[Φ; g_i] = n_e[Φ; closure] → Φ(x, ξ)
+
+Exact Level-0 closed form with flattened electrons [CLEARED: human sign-off
+2026-07-11 — derivation docs/src/islands/derivations/quasineutrality-closure.md;
+derived from the ion/electron density moments, matches I19 Eq. (A.11) exactly at
+τ=1; the general-τ form is the new result]:
+
+ e_iΦ̂/T_i = (τ/(τ+1)) [ δn̄_i/n₀ + L̂_{n0}⁻¹ (x − ĥ(Ω)) ] (arbitrary τ)
+ = [ δn̄_i/n₀·L̂_{n0} + x − ĥ(Ω) ] / (2 L̂_{n0}) (τ=1, I19 A.11 form)
+
+(T_e = T_i assumed in the sources; Islands keeps τ = T_e/T_i general — the
+τ/(τ+1) closure coefficient is the sum of ion+electron adiabatic responses. The
+code uses the raw-moment form with δn̄_i = ∫g_i d³v the actual velocity moment,
+so I19's δn_i normalization convention is a cross-check nuance only, not a code
+dependency.) With kinetic electrons, the Picard form δΦ̂ = (δn̂_i − δn̂_e)/2
+[CHECKED: Diss19 Eq. 2.45]. In Islands both reduce to one quasineutrality
+residual inside the global Newton system (docs/03) — the sources' nested
+Picard loops (Φ outer, ū_∥i inner; I19 fig. A1) are precisely the fragile
+iteration structure Newton–Krylov replaces; L23 §6.1.1 reports the Picard
+convergence criterion was *never met* in production (Φ̂ array-max residuals
+> 100%/iteration at large ŵ) even as Δ stabilized — treat that as the
+cautionary tale motivating D2.
+
+**Implemented (2026-07-11):** `Operators.Quasineutrality` carries the full closure
+— the residual is `R_Φ = M[g] − α Φ̂ + S`, with `α = (τ+1)/τ` (the reciprocal of
+the `τ/(τ+1)` closure coefficient, from `Coefficients.quasineutrality_coefficient`)
+and the field source `S = L̂_{n0}⁻¹(x − ĥ(Ω))` built by
+`Configure.quasineutrality_source` from the cleared `ĥ` profile
+(`Coefficients.h_amplitude`, `Fields.h_profile`). This closes the earlier gap
+where the operator carried only `M[g] − α Φ`: without the `(x − ĥ)` source the
+Level-0 potential was trivially zero (QUESTIONS Q5, field term now resolved).
+
+Boundary conditions: g → neoclassical (no-island) solution and Φ̂ → background
+E_r potential as |x| → L_x; periodic in ξ. **Do not use bare Neumann
+∂ĝ/∂p̂ = 0**: L23 §5.3/§7.1 traces its non-physical "winged" solution branch
+(flows extending 8–10 island widths, disagreeing with neoclassical theory) to
+the Neumann condition admitting multiple numerically-valid solutions, and
+recommends matching to the analytic far-field limit — which is exactly Islands'
+neoclassical-matching BC. [CHECKED: L23 pp. 113–115, 141]
+
+Ampère is **not** solved at Level 0 (O3). The Ampère residual is evaluated as
+a diagnostic from day one; its resonant moments are the Δ outputs:
+
+## 4. Output moments and MRE assembly (normalization now exact)
+
+Parallel current J̄_∥ = θ-average of Σ_j e_j n_j u_∥j. The two projections of
+parallel Ampère through the island [CHECKED: Diss19 Eqs. 2.9–2.10; D21
+Eqs. 7–8, 32]:
+
+ (1/μ₀R) Δ′ ψ̃ = ∫_ℝ dψ ∮ dξ J̄_∥ cos ξ (growth: matching to Δ′)
+ 0 = ∫_ℝ dψ ∮ dξ J̄_∥ sin ξ (torque balance / rotation)
+
+so the kinetic drive and torque moments are
+
+ Δ_cos ≡ Δ_neo = −(μ₀R/2ψ̃) ∫ dψ ∮ dξ J̄_∥ cos ξ, stationarity: Δ′ + Δ_neo = 0
+ Δ_sin = (μ₀R/2ψ̃) ∫ dψ ∮ dξ J̄_∥ sin ξ [CLEARED: human sign-off 2026-07-11 —
+ derivation docs/src/islands/derivations/delta-moment-prefactors.md;
+ Δ_cos matches Diss19 Eq. 4.12, ψ̃ cleared (§1), μ₀R geometry;
+ sin-normalization pinned symmetric ([DERIVED: 2026-07-11])]
+
+with ψ̃ = (w_ψ²/4)(q_s′/q_s), and the Rutherford LHS (2τ_R/r_s²) dw/dt (w =
+half-width). Decomposition diagnostics [CHECKED: Diss19 Eqs. 4.13–4.15; D21
+Eqs. 33–34; D23b §4]:
+
+- **Bootstrap+curvature part**: the Ω-flux-surface-constant part of J̄_∥,
+ ⟨J̄_∥⟩_Ω with ⟨·⟩_Ω = ∮·(Ω+cosξ)^{−1/2}dξ / ∮(Ω+cosξ)^{−1/2}dξ.
+- **Polarization part**: Δ_pol = Δ_neo − (Δ_bs+Δ_cur) — the piece that
+ flux-surface-averages to zero. (L23 Eq. 2.5.3 flags this split as
+ approximate bookkeeping — "could comprise similar contributions from other
+ sources" — which is the design's position: partition is diagnostic, the
+ solve never separates channels.)
+- Species partition (ion vs electron) alongside: L23 finds the *electron*
+ channel dominates both Δ_bs and (unexpectedly, at ω_E = 0) the stabilizing
+ Δ_pol — an open physics question Islands can settle with the ω_E scan.
+
+**Analytic large-w limits to recover** (ladder B2): Δ_bs+Δ_cur ∝ 1/w matching
+WCHH96 Eq. (85) — with the caveat that Eq. (85) is derived in the E_r = 0
+frame while the island-frame calculation must be mapped before comparison
+(Diss19 p. 86) — generic scaling Δ_bs ~ ε^{1/2}(L_q/L_p)(β_θ/w); and
+Δ_pol ∝ 1/w³ at large w. [CHECKED: Diss19 pp. 84–86; D21 p. 2]
+
+In the linear limit, (Δ_cos + iΔ_sin) ↔ the complex layer Δ(Q) (SLAYER
+convention map still [VERIFY: Park PoP 29 (2022) — paper not yet in the
+reference library; acquire]).
+
+## 5. Nondimensionalization and frames (the input parameter vector p)
+
+Normalizations (r_s-based, following I19): x = (ψ−ψ_s)/ψ_s, ŵ = w/r_s,
+ρ̂_θj = ρ_θj/r_s, v̂ = v/v_thj, y = λB_max, b = B/B_max, L̂_q⁻¹ = (ψ_s/q)dq/dψ,
+L̂_n⁻¹ = (ψ_s/n)dn/dψ, Φ̂ = e_jΦ/T_j, ν̂ = ε^{3/2}ν_★ν̃(v̂). Conversion maps to
+the D21 (w_ψ-based) and PRL (ψ_s-based) conventions live in `src/frames/`
+alongside the frequency maps. [CHECKED: I19 p. 6; L23 Eqs. 2.3.40–2.3.46]
+
+**Frame identities (now source-confirmed, the frames-module spec):**
+
+- ω_dia,e = m T_e n₀′/(−e q_s n₀); ω_E ≡ m Φ′_eqm/q_s; ω̂_E = ω_E/ω_dia,e.
+ [CHECKED: Diss19 p. 46]
+- The combination **ω − ω_E is frame-independent**; with ω₀ the island
+ propagation frequency in the frame where E_r → 0 far from the island,
+ **ω₀ = −ω_E** (island-rest-frame calculation at equilibrium-potential
+ gradient ω_E ⇔ island rotating at −ω_E in the zero-E_r frame).
+ [CHECKED: Diss19 pp. 47–48]
+- The effective density gradient shifts with frame:
+ L_n⁻¹ = L_{n0}⁻¹(1 + Z_j ω_E/ω_dia,e). [CHECKED: Diss19 p. 46]
+- Level-0 sources' published thresholds are at ω_E = 0 (no equilibrium E_r);
+ D23b treats ω_E as an input parameter — exactly Islands' O4. Torque-balance
+ roots (Δ_sin = 0) exist at discrete ω̂_E (Diss19 benchmark: ω₀ = −0.93
+ ω_dia,e selected among ±0.93, ±1.28); Δ_pol ∝ ω_E² away from zero and
+ **reverses sign at ω_E ≈ −0.89 ω_dia,e** (D23b Fig. 8) — the modern, frame-
+ pinned statement of the polarization sign controversy. These are ladder-B4
+ targets.
+
+Level-0 input vector:
+
+ p = ( ŵ = w/ρ_θi (half-width),
+ ω̂_E = ω_E/ω_dia,e (≡ −ω₀/ω_dia,e; SLAYER Q-map [VERIFY: Park 2022]),
+ ν̂_j = ν_★j per species,
+ ε, ŝ (via L̂_q), q_s, τ = T_e/T_i,
+ η_j = L_n/L_Tj,
+ species list: {Z_j, m_j/m_i, n_j/n_e, T_j/T_i, gradients, F0 type, role} )
+
+Frequency bookkeeping owns its own unit tests; the polarization-sign disputes
+in the literature are largely frame disputes, and the identities above make
+the conversions mechanical. One module (`src/frames/`) owns them.
+
+## 6. Symmetries and conserved checks (unit-test targets)
+
+- Parity: Δ_cos even / Δ_sin odd under the appropriate (ξ, σ, ω_E) reflection
+ [derive at implementation and record as [DERIVED]; consistency targets:
+ Δ_pol(ω_E) parabolic/even to leading order away from the linear-in-ω_E
+ region near zero, D23b Fig. 8].
+- Zero-gradient, zero-Φ̃ Maxwellian: g = 0 exactly; residual = machine zero.
+- No island (ψ̃ → 0), gradients on: recover standard local neoclassics
+ (bootstrap J_∥ vs. Sauter/NEO) — the most powerful global check (docs/05 B1).
+- Electron-closure identities: ⟨∂²h/∂x²⟩_Ω = 0; k → −1.173; f_p → 1 − 1.46√ε;
+ ⟨ν̂_ii⟩_u analytic value (§2.3). [CHECKED: L23 Ch. 4]
+- Collision operator: particle conservation (L0); +momentum/energy per pair
+ (L1); discrete entropy sign ∫ g C[g]/F_M ≤ 0.
+
+## 7. Explicitly out of Level-0 scope (recorded to prevent creep)
+
+Ampère & multi-harmonic (L3); torque-balance closure of ω_E (L4 — but ω_E is
+an input *parameter scan* from day one; publishing single-ω_E Δ values is
+forbidden per the risk register); χ_⊥/w_d (L4); general geometry & 5D (L2);
+slowing-down F₀ (L2); radiation (L4); gyroaveraging beyond drift order (out of
+program scope — documented limitation vs. gyrokinetic island studies; L23
+p. 142 draws the same boundary: ŵ approaching ρ_i needs gyrokinetics).
diff --git a/docs/src/islands/design/02-species-and-eps.md b/docs/src/islands/design/02-species-and-eps.md
new file mode 100644
index 000000000..abc6b72c4
--- /dev/null
+++ b/docs/src/islands/design/02-species-and-eps.md
@@ -0,0 +1,146 @@
+# 02 — Species abstraction, tungsten, and energetic particles
+
+## 1. Species as a first-class dimension (Level 0 requirement, D3)
+
+Every kinetic object in Islands is indexed by species. The solve is per-species DKEs
+coupled through (i) quasineutrality (and Ampère at L3), (ii) the collision
+operator's field-particle terms (L1+), and (iii) the output moments, which sum
+over species. Designing for N species at Level 0 costs ~nothing (the L0 test is a
+trace deuterium copy of the bulk); retrofitting costs a rewrite.
+
+### 1.1 Species definition
+
+```julia
+abstract type AbstractBackground end
+struct Maxwellian <: AbstractBackground # n, T, dlnn/dr, dlnT/dr at r_s
+struct SlowingDown <: AbstractBackground # S0, v_birth, v_crit(T_e, composition),
+ # dln(source)/dr ; isotropic at L2 entry
+struct Species{B<:AbstractBackground}
+ name::Symbol
+ Z::Float64 # charge number (allow Float for mean-charge-state W)
+ m::Float64 # mass ratio to reference ion
+ background::B
+ role::SpeciesRole
+ collisional_coupling::Bool # participate in field-particle terms? (see §1.3)
+end
+```
+
+### 1.2 Roles: the trace-species economy
+
+```
+@enum SpeciesRole Bulk Trace
+```
+
+- **Bulk**: full nonlinear participant. In quasineutrality (+ Ampère at L3);
+ its g enters the Newton state vector.
+- **Trace** (n_j Z_j ≪ n_e for charge, n_j ≪ n_e for current — check both): the
+ trace DKE is *linear in g_j* given the converged bulk fields (Φ̃, island, bulk
+ flows for friction). Solved as a post-processing pass — one linear solve, no
+ Newton coupling — with an additive contribution to Δ_cos/Δ_sin and to profiles.
+ This is the computational backbone of both the W and alpha tracks: parameter
+ scans over trace-species properties reuse one bulk solve.
+- Promotion rule: any species violating trace criteria (e.g. W at high
+ concentration where Z n_W is non-negligible, or when friction back-reaction on
+ bulk flows matters — see §2) must be run as Bulk; the code checks the criteria
+ and warns, never silently degrades.
+
+### 1.3 Collisional coupling matrix
+
+Friction is directional: a trace species always *feels* the bulk (drag,
+pitch-angle scattering off bulk); whether the bulk feels the trace
+(field-particle back-reaction) is the `collisional_coupling` flag. W at reactor-
+relevant concentrations: back-reaction ON (Z² n_W friction on bulk ions is not
+small even when charge-trace holds — this is exactly the mixed case the analytic
+MRE cannot do). Dilute alphas: back-reaction OFF is usually safe; verify with the
+flag flip (a one-line toggle — the whole point of the architecture).
+
+---
+
+## 2. Tungsten (physics gate: Level 1; radiative channel: Level 4)
+
+### 2.1 Why W breaks the analytic MRE terms
+
+Collisionality scales ~ Z²(?) with low v_th: W sits in Pfirsch–Schlüter or plateau
+while bulk D and electrons are banana — a *mixed-regime* multi-species problem.
+Analytic bootstrap terms in the MRE assume a per-species regime; the friction
+between a PS impurity and banana bulk ions modifies the bootstrap current (and
+hence Δ_bs) in ways only a full multi-species collision operator captures.
+Additional channels: Z_eff shift of electron collisionality (electron bootstrap
+and, at L3, resistive layer physics); impurity contribution to polarization is
+small (tiny ρ_θW) — a prediction to *verify*, not assume.
+
+### 2.2 Level-1 deliverables
+
+- Δ_bs(w; n_W, Z_W, ν̂) surfaces with W friction — quantified departure from
+ Sauter-based MRE terms.
+- **In-island impurity asymmetry**: n_W(Ω, ξ) structure (parallel compression +
+ friction with flattened bulk flows inside the island). Directly comparable to
+ AUG/DIII-D measurements of W behavior at islands, and the input to the L4
+ radiative channel.
+- Charge state: single mean-Z at L1 (Z̄(T_e) from coronal tables as a parameter);
+ multi-charge-state bundle only if asymmetry results prove sensitive.
+
+### 2.3 Level-4 radiative/thermo-resistive channel
+
+Energy closure with radiation sink Q_rad = n_e n_W L_W(T_e) inside the island,
+temperature-dependent resistivity η(T_e) entering the L3 Ohm/Ampère system →
+radiation-driven island growth (Gates & Delgado-Aparicio-class mechanism,
+density-limit relevance). Gate: reproduce published thermo-resistive island model
+trends (docs/05 D3). Depends on: L1 (W transport into island) + L3 (η in field
+equation) + L4 energy closure. This is deliberately the *last* W milestone.
+
+---
+
+## 3. Energetic particles (physics gate: Level 2)
+
+### 3.1 Why EPs are the reason Level 2 exists
+
+Alphas violate the orderings W leaves intact:
+
+- **ρ_θα ≳ w** (and possibly ≳ L_x): the drift-island shift is not a perturbative
+ O(ρ_θ) displacement — it is the dominant structure. Radially local, constant-
+ gradient assumptions fail; the domain must span several ρ_θα with profile
+ variation toggles.
+- **Orbit-average survives, resonance does not**: ω_bα is still fast (O5 holds for
+ alphas), but island rotation can satisfy **ω ~ ω_D,α** (precession) — a
+ collisionless, resonant polarization-type contribution to Δ with no fluid
+ analog and no regime formula. Flagship EP physics target: ITER/SPARC NTM
+ thresholds with self-consistent alpha kinetics.
+- **Non-Maxwellian F₀**: slowing-down background changes the drive terms
+ (∂F₀/∂r structure, no temperature gradient in the usual sense) and the
+ collision physics (drag on electrons + bulk ions rather than self-collisions;
+ self-collisions negligible). Mechanically modest once L2 orbits are right;
+ pointless before.
+
+### 3.2 Implementation sequence within Level 2
+
+1. Trace Maxwellian "hot ion" with artificially large ρ_θ — isolates finite-orbit
+ nonlocality from F₀ shape.
+2. SlowingDown F₀, isotropic — adds drive/drag changes.
+3. ω-scan through ω_D,α — the precession-resonance study. Requires the L2
+ orbit machinery to deliver accurate ω_D(λ, E) (benchmark vs. standalone orbit
+ integrator, docs/05 C2). Methodological antecedent: Dudkovskaia et al. JPCS
+ 1125, 012009 (2018) — *phase-space* island stability for EP-driven modes;
+ its bounce/angle-variable and separatrix-layer machinery is the same toolkit
+ (docs/08), though its physics (bump-on-tail secondary modes) is not part of
+ this study.
+4. (Optional/later) anisotropic F₀ for NBI/RF fast ions — same machinery,
+ different B; parked unless a collaborator needs it.
+
+### 3.3 Division of labor with the outer region
+
+EP pressure also modifies Δ′ (outer-region kinetic corrections — same physics
+class as GPEC/PENT stability integrals). That stays on the perturbed-equilibrium
+side and arrives through the Δ′(w) input. Islands owns only resonant/orbit-width EP
+physics at the island. State this in every EP paper to preempt double-counting
+questions, and define the split precisely: outer kinetic Δ′ evaluated with the
+island region excised at the matching radius |x| = L_x [interface spec in
+docs/03 §5].
+
+### 3.4 Alpha–W interplay (free deliverable)
+
+Both tracks live in the same species list; an L2 run with {D bulk, e bulk/model,
+W trace, α trace} costs one bulk solve + two linear passes. Alpha drag heating
+asymmetries vs. W radiative cooling inside islands is an unexplored combination —
+cheap to look at once both tracks exist, potentially interesting for burning-
+plasma island stability. Not a milestone; an opportunity.
diff --git a/docs/src/islands/design/03-architecture.md b/docs/src/islands/design/03-architecture.md
new file mode 100644
index 000000000..0efe26b49
--- /dev/null
+++ b/docs/src/islands/design/03-architecture.md
@@ -0,0 +1,158 @@
+# 03 — Architecture (Julia)
+
+Design principle: **the physics levels are configurations, not code paths.** One
+discretization, one Newton solver, one state vector; orderings are swappable
+operators and flags. If implementing a new level requires touching the solver
+loop, the architecture has failed.
+
+## 1. Package layout
+
+Islands is a GPEC submodule (`module Islands`, no separate `Project.toml` — it
+shares the GeneralizedPerturbedEquilibrium package environment). Its files are
+distributed across the repo's existing trees, not a self-contained package dir:
+
+```
+src/Islands/ # the module (module Islands)
+├── CLAUDE.md # module conventions (nested; Claude Code auto-loads)
+├── Islands.jl # module entry
+├── geometry/ # AbstractEquilibrium: AnalyticCircular (L0),
+│ # MillerAnalytic (L2 entry; the D23a geometry),
+│ # NumericalEquilibrium (L2; DCON/gEQDSK ingest)
+├── phasespace/ # grids (x, ξ[, θ]; λ, E, σ), maps, quadrature,
+│ # layer-clustered mappings (docs/04)
+├── species/ # Species, backgrounds, roles (docs/02)
+├── frames/ # THE frequency/frame conversion module (docs/01 §5)
+├── operators/ # the stack (see §2)
+├── fields/ # Φ̃ quasineutrality residual; A_∥ Ampère residual (L3)
+├── closures/ # torque balance, χ⊥ transport, radiation (L4)
+├── moments/ # Δ_cos, Δ_sin, profiles, channel decompositions
+├── solvers/ # Newton–Krylov, continuation, trace-species linear pass
+├── io/ # config (TOML), results (HDF5/JLD2), provenance
+└── verify/ # benchmark harness callable from tests AND scripts
+
+docs/src/islands/ # docs (rendered by the GPEC Documenter site)
+├── index.md # overview / landing page
+├── design/ # these design documents (normative, aspirational)
+├── (Physics Book chapters) # as-implemented equations (docs/07)
+├── derivations/ papers/ state/ notes/ LOG.md QUESTIONS.md
+
+test/runtests_islands_*.jl # unit + symmetry + conservation tests (fast),
+ # included from the repo's test/runtests.jl
+benchmarks/islands/ # the docs/05 ladder (slow; CI-gated subsets)
+└── figures/ # surface generation + paper/gallery figure scripts
+regression-harness/ # islands cases integrated with the rest (islands_*)
+```
+
+## 2. The operator stack
+
+The state is `U = (g_1, …, g_Nbulk, Φ̃ [, A_∥ at L3] [, ω, E_r at L4])`. The
+residual is assembled as a sum of operator applications:
+
+```julia
+abstract type AbstractTerm end
+# each implements: apply!(R, term, U, cache), and is either matrix-free or
+# provides a local stencil for preconditioning
+
+struct ParallelStreaming <: AbstractTerm end # includes island B̃_r ∂x
+struct MagneticDrift <: AbstractTerm # variant = :original (finite L̂_B⁻¹, I19)
+ # | :improved (L̂_B⁻¹ → 0 proxy of
+ # the cosθ ∂B/∂ψ structure, D21
+ # Eq. A2) — the 8.73→1.46 ρ_bi
+ # toggle, docs/01 §2.1
+struct ExBDrift <: AbstractTerm end
+struct Collisions{M} <: AbstractTerm # M = PitchAngle (L0; energy-dependence
+ # sub-toggle :chandrasekhar (I19) |
+ # :vcubed (D21), docs/01 §2.3) |
+ # FokkerPlanckMulti (L1)
+struct GradientDrive <: AbstractTerm end # (v_E+v_D+v_ψ̃)·∇F₀ ; dispatches
+ # on background type (Maxwellian /
+ # SlowingDown)
+struct PerpTransport <: AbstractTerm end # χ⊥, D⊥ (L4)
+struct RadiationSink <: AbstractTerm end # (L4, energy closure)
+```
+
+Configuration = list of terms per species + field-equation set + closure set,
+read from a TOML config. **Named configurations are pinned in `src/Islands/verify/`**:
+`:imada2019` (B5a; note it targets the L23-amended physics, not I19 Eq. A.1 as
+printed — docs/01 header), `:dudkovskaia2021` (B5b), `:leigh2023` (B5c),
+`:sauter_limit`, `:slayer_limit`, … — the toggle-comparison studies the
+project exists to do are then config diffs, and every published figure names
+its configuration.
+
+Rules:
+- No term may inspect which other terms are active (no hidden coupling).
+- Every term carries its own verification hook (an analytic limit or manufactured
+ solution registered in `src/Islands/verify/`).
+- Orderings that *remove* structure (e.g. orbit averaging O5) are implemented as
+ alternative phase-space configurations, not operators: `phase = :orbit_averaged
+ (4D)` vs `:full (5D)`. Terms are written against an abstract phase-space
+ interface so both share implementations where possible.
+
+## 3. Solver strategy
+
+- **Steady-state Newton–Krylov** (D2): matrix-free JVPs via AD; GMRES; physics-
+ block preconditioner (docs/04 §4).
+- **AD policy**: JVPs and small-parameter sensitivities via ForwardDiff duals
+ through the operator stack (write terms generically over `eltype`); evaluate
+ Enzyme for reverse-mode ∂Δ/∂p over the full parameter vector when surface
+ generation begins. AD-compatibility is a CI test per term (no
+ `Float64`-hardcoded buffers — reuse the per-thread preallocation patterns from
+ GPEC/QuadGK work, but typed generically).
+- **Continuation**: pseudo-arclength in any component of p (and in ψ̃₀/w).
+ Purposes: (i) Newton globalization by homotopy from analytic-limit solutions,
+ (ii) Δ-surface generation as a byproduct, (iii) bifurcation tracking — the
+ penetration bifurcation at Level 3 *is* a fold in the continuation curve, so the
+ solver must detect/step around folds from the start.
+- **Trace pass**: after bulk convergence, each Trace species is one linear solve
+ with frozen bulk fields (docs/02 §1.2); reuses the same operator stack and
+ Krylov machinery with a linear RHS.
+- **ω closure (L4)**: ω, E_r appended to U; torque-balance residual appended to R.
+ Structurally identical Newton system — this is why the closure is cheap *if*
+ the architecture holds.
+
+## 4. Data and provenance
+
+- Config: TOML in, full resolved config (all defaults expanded, git SHA, term
+ list, grid spec) stored inside every output file. A result that can't
+ regenerate itself is a bug.
+- Output: HDF5/JLD2 with (p, Δ_cos, Δ_sin, channel decompositions, convergence
+ metadata, profiles on demand). Surface datasets are append-only stores keyed by
+ p; the emulator (M12) trains from these.
+
+## 5. External interfaces
+
+> **Superseded where in-repo (docs/06 §1):** with Islands living inside the GPEC
+> repository, the Δ′ and SLAYER interfaces below become direct Julia calls
+> against GPEC's implementations, exercised in CI. The file-based forms remain
+> the spec for any standalone/external consumers.
+
+- **Δ′(w) input**: file-based interface (resistive DCON/STRIDE output → a small
+ table Δ′ vs. w at the rational surface, with the matching radius L_x recorded).
+ Keep it dumb and versioned; no live coupling until the physics settles.
+- **Equilibrium input (L2)**: gEQDSK + profiles, mapped through the same
+ representations the GPEC stack uses; `AnalyticCircular` remains permanently
+ available for regression.
+- **SLAYER (L3 verification)**: comparison harness that maps Islands' linear-limit
+ (Δ_cos + iΔ_sin) onto SLAYER's Δ(Q) convention — the frames module owns the Q
+ mapping [VERIFY: Park 2022 conventions — paper not yet in the reference
+ library]. **Status:** the in-repo SLAYER arrives with the Tearing module PR
+ (#238, `src/Tearing/InnerLayer/SLAYER/`), sequenced before Islands starts
+ (docs/06 §1); code against the Tearing-module layout, not `develop`'s old
+ `src/InnerLayer` placeholder.
+- **NEO/NCLASS (L1 verification)**: no-island neoclassical cross-check driver
+ (export local parameters → run → compare bootstrap/flows).
+
+## 6. Performance posture (sizing, not optimization)
+
+L0 4D: N_x × N_ξ × N_λ × N_E × σ ~ 400×64×128×32×2 ≈ 2×10⁸ dof upper bound
+per bulk species before layer-adapted grids reduce N_x, N_λ needs — matrix-free
+mandatory. The prior-art cost baseline makes the case concrete: kokuchou's
+dense-block shooting method needed ≈16.6 GB *per energy grid point* and
+O(100 GB) total at n_ξ = 30, n_p = 145, and memory (not physics) set its
+accessible parameter window (docs/04 §6). Single-node multithread first (Julia
+threads; the GC-contention lessons from parallel QuadGK apply: preallocate
+per-thread caches). MPI only if
+L2 5D demands it; do not architect for MPI speculatively, but keep halo-friendly
+array layouts (x outermost) so the option stays open. Every solve logs a cost
+model entry (dof, iterations, wall time) — the emulator strategy depends on
+knowing what a point costs.
diff --git a/docs/src/islands/design/04-numerics.md b/docs/src/islands/design/04-numerics.md
new file mode 100644
index 000000000..e04f20b9d
--- /dev/null
+++ b/docs/src/islands/design/04-numerics.md
@@ -0,0 +1,190 @@
+# 04 — Numerics
+
+The numerics posture is now informed by three generations of prior-art
+implementation experience: DK-NTM (I19 Appendix A — shooting method, nested
+Picard loops), RDK-NTM (Diss19 Appendix E — S-space reduction, analytic
+layers), and kokuchou (L23 Chs. 3–6 — the most complete forensic record of
+where the direct 4D approach actually breaks). Citations per docs/01 header.
+
+## 1. Discretization by coordinate
+
+- **ξ (helical angle)**: Fourier pseudo-spectral, periodic. Nonlinear terms
+ (v_E·∇g, Φ̃ coupling) via dealiased transforms. At L0 the field has one
+ harmonic but g does not — keep the full spectrum from the start. Harmonic
+ content of the residual doubles as a resolution diagnostic. (kokuchou used
+ 2nd-order FD with n_ξ = 30 and flagged accuracy limits; spectral ξ is a
+ deliberate upgrade.)
+- **x (radial)**: mapped collocation or high-order FD on a stretched grid.
+ **Packing must target the drift-island separatrices, not the magnetic
+ island**: the layer follows contours shifted by ±ρ̂_θi ω̂_D L̂_q per (y, v̂, σ)
+ (docs/01 §2.2), so the packed envelope is max over the σ-shifts of the Ω=1
+ curve — L23 §3.1.6 identifies the rectilinear-mesh/shifted-round-island
+ mismatch as kokuchou's dominant accuracy limiter, and proposes a mapped
+ radial coordinate absorbing the θ-dependent orbit shift (its Eq. 7.1.1,
+ p̃ = ψ − I(v_∥(θ)/ω_ci(θ) − v_∥(0)/ω_ci(0))) — adopt as a candidate *grid
+ map* x(s; y, v̂, σ-envelope), never as a solve-coordinate change (D1 stands).
+ Far-field BCs at |x| = L_x: g → neoclassical (no-island) solution, Φ̃ →
+ background E_r potential. **Never bare Neumann ∂g/∂x = 0**: L23 traced its
+ spurious "winged" solution branch to Neumann non-uniqueness (docs/01 §3).
+ L_x is a convergence parameter reported with every result (and is the Δ′
+ excision radius, docs/03 §5).
+- **λ (pitch)**: collocation with clustering at the trapped-passing boundary
+ y_c = 1. Layer width **in pitch angle: ε_λ ≈ [(2ν̂_j/v̂) a(λ_c, v̂; σ)]^{1/2}
+ ~ √ν_★** with a(λ) = ⟨σλ(1−λB)^{1/2}R/B_φ⟩_θ — now a confirmed scaling
+ [CHECKED: Diss19 p. 58; D23b §3.1], electron layer wider by (m_i/m_e)^{1/4}.
+ Packing parameter set adaptively from input ν̂. Internal boundary
+ conditions at y_c (the DK-NTM/kokuchou matching set):
+ Σ_σ σg^p = 0, Σ_σ g^p = 2g^t, Σ_σ ∂_y g^p = 2 ∂_y g^t
+ [CHECKED: I19 Eqs. A.7–A.9; L23 Eqs. 2.3.52–2.3.54]. σ = ± sheets joined at
+ bounce points for trapped particles.
+- **E (energy)**: Gauss-type quadrature nodes on a mapped semi-infinite domain
+ weighted by F₀ — Maxwellian weights at L0; slowing-down weights (L2) change
+ the map, not the machinery. Mind the integrable divergences the sources hit:
+ ν̃(v̂) ∝ v̂⁻² at low v̂ (use the analytic ⟨ν̂⟩_v, docs/01 §2.3), (1−yb)^{−1/2}
+ at bounce points, and the y → 1/b flow integrals — analytic correction
+ terms/asymptotics, not naive quadrature [CHECKED: L23 pp. 69, 87–88, App. 8.4].
+ Collision-operator energy diffusion (L1) prefers modest-order collocation
+ over pure spectral here; decide with a convergence study on the Sauter
+ benchmark.
+- **θ (poloidal, L2)**: Fourier. The 4D orbit-averaged mode must be recoverable
+ as the θ-average of the 5D solve — a built-in cross-check of both.
+
+## 2. The two internal layers (the numerical cliff — now quantified)
+
+Both layers are collisional with **width ∝ ν^{1/2}** [CHECKED: D23b §3.1,
+footnote 11]:
+
+- **Trapped-passing dissipation layer** at y_c: ε_λ ~ [(2ν̂/v̂)a(λ_c)]^{1/2}.
+- **Drift-island separatrix layer** at S = S_c: ε_S ~ [(2ν̂/v̂)C_SS(S_c)]^{1/2}
+ in the S variable; in kokuchou's (p, ξ) variables the equivalent widths are
+ Δ_p,pass ~ √(ν̂L̂_q/ŵ)·ρ̂_θi and Δ_p,trap ~ √(ν̂L̂_q/ŵ)·√(ŵρ̂_θi)
+ [CHECKED: L23 §6.1.2].
+
+Two prior-art failure modes to design against:
+
+1. **The layer moves during the solve.** When E×B dominates the layer balance
+ (ν̂/(ŵL̂_q) < 1 — satisfied over part of the velocity grid in *every* L23
+ production run), the width becomes Δ_p,E×B ~ ν̂ρ̂_θi/ŵ and **depends on Φ̂,
+ so it shifts between nonlinear iterations** — a mesh packed for the initial
+ iterate can be unresolved at the converged one [CHECKED: L23 Eqs. 6.1.1–6.1.2].
+ Mitigation: pack from the layer-width *lower bound* over the expected Φ̂
+ range; validate the estimate against measured solution structure post-solve
+ (estimates logged); re-mesh-and-continue as a fallback.
+2. **The accessible parameter window shrinks with correct physics.** L23's
+ corrected ∂²ĝ/∂p̂² coefficient (∝ ρ̂²_θi, per the §2.6 amendments) makes
+ separatrix gradients *steeper* than in the original DK-NTM runs — kokuchou
+ could not reach DK-NTM's ν_★ = 10⁻³ operating point (floor at 5×10⁻³) nor
+ ŵ > 0.75ρ̂_θi at that floor, with memory as the binding constraint
+ [CHECKED: L23 §5.3, §6.1.2, p. 116]. Islands' matrix-free posture removes
+ kokuchou's specific memory wall (§6 below) but not the resolution demand.
+
+Posture (unchanged in spirit, now with numbers): adaptive packing driven by
+the ε_λ, ε_S, Δ_p,E×B estimates from input parameters; a documented ν̂ floor
+per resolution tier; below the floor, results flagged `layer_unresolved` and
+the RDK cross-check mode (analytic layer treatment per Diss19 §3/D23b §3.1.1 —
+Fourier-matched layer solutions) is the reference. Disagreement between DK and
+RDK modes above the floor is a release-blocking bug; below it, it's the
+measured cost of the RDK ordering — a publishable toggle-impact result.
+Grid-convergence studies are first-class benchmark artifacts.
+
+## 3. The trapped-passing boundary is *singular* — regularize deliberately
+
+The hardest-won lesson in the reference set [CHECKED: L23 §4.2, pp. 94–97]:
+the linear system coupling the two sides of y_c through the matching
+conditions is **intrinsically singular/ill-conditioned** (kokuchou measured
+rcond ≈ 10⁻¹⁶–10⁻¹⁹, det = 0 or ±Inf, at every energy grid point; plain LU
+gave machine-dependent noise in ĝ(v̂); the same latent defect was reproduced in
+DK-NTM once other bugs were fixed). kokuchou's fix: truncated SVD (cutoff
+10⁻⁷; exactly one singular value truncated) applied only at the boundary
+solve.
+
+Implications for Islands' Newton–Krylov (no y-sweep, one global residual):
+
+- The same near-null-space will reappear as **Jacobian ill-conditioning
+ localized at the y_c block**. The physics-block preconditioner must treat
+ the y_c matching rows explicitly (small dense block per (x-locality, v̂):
+ factor with SVD/complete pivoting, truncate/regularize below a documented
+ cutoff), so GMRES never has to resolve the near-singular directions itself.
+- Add a CI-level diagnostic: monitor the smallest singular value of the y_c
+ matching block and the GMRES convergence stagnation signature; a silent
+ regression here produced noise, not crashes, in the prior art — it must be
+ *tested for*, not observed.
+- Root cause is the asymptotic v_∥⁻¹ structure at the boundary; any basis
+ change that removes the 1/v_∥ divergence from the matched unknowns (e.g.
+ solving for flux-like variables across y_c) is worth a design spike in M1.
+
+## 4. Manufactured solutions
+
+Before any physics benchmark: MMS per operator and for the assembled L0 system
+(source terms chosen so a prescribed smooth g*, Φ̃* solve the equations).
+Verifies discretization order and the AD-generated JVPs simultaneously
+(JVP checked against finite differences of the residual). MMS configs live in
+`src/Islands/verify/` and run in CI at low resolution. Supplement with the sources' cheap
+analytic unit targets (docs/01 §6: h(Ω) identities, k = −1.173, ⟨ν̂⟩_v,
+f_p = 1 − 1.46√ε) — L23 Ch. 4 demonstrates these catch inherited bugs that
+integration tests miss.
+
+## 5. Newton–Krylov details
+
+- Inexact Newton (Eisenstat–Walker forcing), line search + continuation
+ globalization (docs/03 §3). Expect and handle fold points (penetration
+ bifurcation at L3): pseudo-arclength with tangent monitoring from day one.
+- The prior art's nested Picard loops (Φ outer / ū_∥i inner) are the explicit
+ anti-pattern: kokuchou's production runs *never met* their Picard convergence
+ criterion (Φ̂ array-max residual >100%/iteration at large ŵ) even as Δ_loc
+ stabilized, and the array-averaged residual hid locally-divergent regions
+ [CHECKED: L23 §3.1.5, §6.1.1]. Islands solves (g_j, Φ̃) as one Newton system;
+ convergence is measured by the global residual norm *and* its spatial max,
+ both archived.
+- **Preconditioning** (the make-or-break): physics-block preconditioner —
+ approximate inverse built from the ξ-averaged, drift-free operator
+ (streaming + collisions per species: block-tridiagonal-ish in x per (λ, E)),
+ a Schur-type block for Φ̃ (and A_∥ at L3), plus the explicit y_c matching
+ block of §3. Cheap to factor, captures the stiff parallel/collisional
+ physics; Krylov handles drifts and nonlinear coupling. Iteration counts vs.
+ p logged; preconditioner quality is a tracked metric, not folklore.
+- Krylov: GMRES (restarted) via Krylov.jl or LinearSolve.jl; matrix-free JVP
+ through the operator stack (ForwardDiff duals). No global sparse Jacobian is
+ ever formed except in tiny-grid debug mode (also useful for eigenvalue
+ diagnostics near folds and for the y_c singular-value monitor).
+
+## 6. Cost model and why matrix-free is mandatory (prior-art data point)
+
+kokuchou's y-sweep shooting method stores dense (n_ξn_p)² recursion blocks:
+at n_ξ = 30, n_p = 145 that is ≈16.6 GB *per energy grid point* and
+O(0.67 N³) ≈ 5.5×10¹⁰ flops per y-point (0.4 hr/energy-point on ARCHER2,
+O(100 GB) RAM total) — and memory, not physics, set its ν_★ floor and ŵ
+ceiling [CHECKED: L23 pp. 80–84]. Islands' matrix-free Newton–Krylov stores
+O(#dof) vectors instead; the trade is preconditioner engineering (§5). Every
+solve logs a cost-model entry (dof, iterations, wall time) — the emulator
+strategy depends on knowing what a point costs, and the L23 numbers are the
+baseline to beat.
+
+## 7. Trace-species linear pass
+
+One GMRES solve per trace species with frozen bulk fields; same preconditioner
+with the trace species' collision blocks. Sensitivities of Δ w.r.t. trace
+parameters (n_W, Z̄_W, alpha source) are nearly free here (linear problem +
+AD) — the W/EP parameter scans (docs/02) should exploit this before any bulk
+rescans.
+
+## 8. Surface generation & emulator posture (M12)
+
+- Continuation walks generate curves; a scheduler tiles (ŵ, ω̂_E) planes per
+ remaining-parameter grid point, warm-starting from nearest neighbors. (L23's
+ practical trick — warm-starting Φ̂ from a stable neighboring run to avoid the
+ spurious branch — is the same mechanism; continuation makes it systematic.)
+- Store everything (docs/03 §4); train emulator (GP or small NN — decide later)
+ on (p → Δ_cos, Δ_sin) with AD sensitivities as extra supervision.
+- Publish surfaces with the named configuration and grid tier; emulator
+ uncertainty must exceed measured grid-convergence error or the tier is bumped.
+
+## 9. Language/tooling specifics
+
+Julia ≥ LTS current at project start. Key packages: Krylov.jl / LinearSolve.jl,
+ForwardDiff.jl (+ Enzyme.jl evaluation), FFTW.jl, Interpolations.jl (equilibrium
+ingest; mind boundary conditions), HDF5.jl/JLD2.jl, TOML stdlib. Threading with
+per-thread preallocated caches (no allocation in `apply!` hot paths — enforce
+with an allocation regression test). Revise.jl workflow assumed; keep
+world-age-safe (no runtime `eval` in the stack). All hot kernels `@inbounds`-safe
+with explicit bounds-check test coverage first.
diff --git a/docs/src/islands/design/05-verification.md b/docs/src/islands/design/05-verification.md
new file mode 100644
index 000000000..605254da4
--- /dev/null
+++ b/docs/src/islands/design/05-verification.md
@@ -0,0 +1,168 @@
+# 05 — Verification ladder
+
+The project's credibility is this document. Every claim of the form "Islands
+generalizes X" is backed by "Islands reproduces X in its limit." Benchmarks are
+code in `benchmarks/islands/`, each with: named configuration, target (formula/number/
+dataset), tolerance, grid-convergence requirement, and status. CI runs a fast
+subset; full ladder runs before any tagged release or paper submission.
+
+Source abbreviations and the [CHECKED]/[VERIFY] tag semantics per docs/01
+header; the reference-library map is docs/08. Targets below marked [CHECKED]
+have been transcribed from the in-repo PDFs with equation/page cites and await
+one human sign-off; remaining [VERIFY] items state exactly what is missing.
+
+## Target tiers and reproducibility (normative — Decision D9)
+
+Reproducing an *absolute* number from a publication requires every input of
+that publication's exact scenario — profiles/gradients, ν★, ω_E, τ, q_s, ŝ,
+domain size L_x, resolution, boundary treatment, and even the
+threshold-extraction procedure — and publications under-specify these (the
+type specimen is B5a below: I19 is *internally inconsistent about its own run
+collisionality*). Following the SLAYER-validation precedent (Park 2022 /
+Burgess 2026, docs/08 B26), the primary literature-facing physics checks are
+**scaling and regime behavior**, not single-number matches. Every target
+carries a tier:
+
+- **T1 — exact (mathematical).** Scenario-independent: MMS orders,
+ conservation/entropy identities, closure *constants* that are well-defined
+ integrals (k, f_p, ⟨ν̂_ii⟩_u). Tight pass/fail.
+- **T2 — internal cross-checks (we control all inputs).** DK vs RDK
+ cross-check mode, 4D vs 5D, and **differential/toggle results measured
+ within Islands itself** (e.g. the E1 drift-model toggle ratio). Ratios and
+ differentials cancel shared input uncertainty — they are the sharpest
+ quantitative statements available and are the **primary quantitative
+ physics gates**. Pass/fail.
+- **T3 — scaling/regime checks vs literature (primary literature-facing
+ gates).** Power-law exponents, trend signs, regime boundaries, landmark
+ existence, curve morphology. Pass/fail on exponent/sign/existence within a
+ stated window.
+- **T4 — quantitative reproduction attempts (aspirational, audit-gated).**
+ Absolute literature numbers. **Never pass/fail** until an
+ **input-completeness audit** of the source yields a full *input manifest*
+ (template below): every required input either cited to the paper or
+ recorded as "unspecified → assumption + sensitivity scan required". An
+ incomplete manifest permanently downgrades the target to T3
+ (order-of-magnitude + trend), and any residual gap is reported together
+ with an input-sensitivity scan (∂(result)/∂(input) over plausible input
+ ranges) quantifying how much of the discrepancy input uncertainty can
+ explain.
+
+**Input-manifest template** (per T4 target, filled during the audit and
+published with any comparison): ε; q_s and ŝ (or L̂_q); density/temperature
+gradients (L̂_n, L̂_T or η_j); τ = T_e/T_i; ν★ per species; ω_E; m/n; domain
+half-width L_x; grid resolution / convergence statement; far-field boundary
+treatment; threshold-extraction procedure (which Δ crosses zero, at what
+assumed Δ′); Δ′ convention; frame convention; species list. Each entry: value
++ where the source states it, or "unspecified".
+
+**Standing triage rule for York-lineage targets**: L23 §2.6 documents errors
+in the published I19 equation set (docs/01 header warning). Where Islands
+disagrees with a published DK-NTM number but agrees with the L23-amended
+physics, the triage outcome "their published equation set" is available — but
+only after the [VERIFY] resolution is logged with the specific amended term.
+
+## A. Structural (pre-physics)
+
+| ID | Target |
+|---|---|
+| A1 | MMS: per-operator and assembled-system convergence at design order |
+| A2 | JVP vs. finite-difference residual directional derivatives |
+| A3 | Symmetry/parity relations of Δ_cos, Δ_sin (docs/01 §6) |
+| A4 | Conservation: particles (L0); +momentum/energy per collision pair (L1); discrete entropy sign |
+| A5 | Zero-drive null test: g ≡ 0, residual = machine zero |
+| A6 | 4D orbit-averaged mode = θ-average of 5D mode (once L2 exists) |
+| A7 | Closure identities (**T1 — scenario-independent mathematical constants/identities, tight pass/fail**): ⟨∂²h/∂x²⟩_Ω = 0; k → −1.173; f_p → 1−1.46√ε; analytic ⟨ν̂_ii⟩_v [CHECKED: L23 Ch. 4 — kokuchou's unit set, which caught inherited DK-NTM bugs] |
+| A8 | Trapped-passing block conditioning monitor: smallest singular value of the y_c matching block tracked; regression = silent-noise failure mode of L23 §4.2 |
+
+## B. Level 0–1 physics
+
+| ID | Target | Source / configuration |
+|---|---|---|
+| B1 | **No-island limit**: bootstrap current & neoclassical flows vs. NEO/NCLASS across ν_★; single- and multi-species | NEO; Sauter PoP 6, 2834 (1999) |
+| B2 | Large-w limit — **T3 (primary)**: the Δ_bs+Δ_cur ∝ 1/w and Δ_pol ∝ 1/w³ scalings, and the parametric trend ε^{1/2}(L_q/L_p)(β_θ/w). **T4 (audit-gated)**: the 1/w *coefficient* against WCHH96 Eq. (85) mapped to the island frame (Diss19 p. 86 frame caveat) | [CHECKED: Diss19 pp. 84–86; D21 Fig. 8-class curves] |
+| B3 | Curvature: GGJ/D_R contribution in the appropriate fluid-ish limit; note Δ_cur = O(ε²), negligible in the York configs — pick a configuration where it isn't [VERIFY accessible configuration — may require L4 transport toggle] | Glasser–Greene–Johnson 1975; in-repo GGJ inner-layer model (src/InnerLayer/GGJ) as cross-check |
+| B4 | Polarization — **T3 (primary)**: (i) Wilson–Connor collisionless and Smolyakov collisional *scalings*; (ii) Δ_pol ∝ ω_E² away from zero with **a sign reversal existing** at an ω_E of order −ω_dia,e, reversal location insensitive to w/ρ_θi; (iii) torque-balance roots (Δ_sin = 0) *existing* at discrete ω̂_E. **T4 (audit-gated)**: the reversal location (sources: ≈ −0.89 ω_dia,e) and root values (sources: ±0.93, ±1.28 ω_dia,e) | WCHH96; Waelbroeck & Fitzpatrick PRL 78 (1997); [CHECKED: D23b Fig. 8; Diss19 Fig. 4.18] |
+| B5a | **DK-NTM threshold comparison** — **T3 (primary)**: a finite threshold *exists* at w_c ~ O(ρ_θi) in the `:original`-drift configuration (circular, ε = 0.1, L̂_q = 1, m/n = 2/1, T_e = T_i, ω_E = 0). **T4 (audit-gated)**: the absolute value (sources: w_c ≃ 2.76 ρ_θi half-width ≡ 8.73 ρ_bi at ε = 0.1; the "8.73" is a unit conversion, not printed in I19). The audit's type specimen: [VERIFY: I19 is internally inconsistent on its own run collisionality — §4.2 states ν_★ = 0.01; L23 p. 82 quotes DK-NTM at ν_★ = 10⁻³] — an *absolute* match is not meaningful until the input manifest resolves this | [CHECKED: I19 Fig. 9; PRL p. 4] |
+| B5b | **RDK-NTM improved-model comparison** — **T2 (primary, promoted)**: the **drift-model toggle differential** measured within Islands — `:original` → `:improved` (L̂_B⁻¹ = 0 proxy) reduces w_c by a factor ~6 in an otherwise identical configuration (the robust form of the sources' "8.73 → 1.46 ρ_bi" story; shared input uncertainties cancel in the ratio). **T4 (audit-gated)**: the absolute value (sources: w_c ≈ 0.45 ρ_θi ≡ 1.41–1.47 ρ_bi half-width; config as B5a with ν_i★ = 10⁻³–10⁻⁴, Φ′_eqm = 0, η_j = 1) | [CHECKED: D21 abstract + Fig. 8; D23a abstract] |
+| B5c | **kokuchou finite-ν_★ comparison** — **T3 (primary)**: the threshold *grows with ν_★* (dw_c/dν_★ > 0, roughly linear over ν_★ ∈ [5, 20]×10⁻³) and scales ∝ ρ̂_θi — the ν_★-dependence is the new physics. **T4 (audit-gated; best audit candidate — a thesis documents its numerics most fully)**: the fitted surface w_c[r_s] ≈ 0.440 ρ̂_θi + 0.0178 ν_★ − 7.54×10⁻⁵ (2D OLS, R² = 0.9916; validity ρ̂_θi ∈ [1,5]×10⁻³, ε = 0.1, ω_E = 0, m/n = 2/1, **L23-amended equation set**) | [CHECKED: L23 Eqs. 6.3.1–6.3.2, Figs. 6.10–6.12] |
+| B6 | **T3 (morphology — qualitative by nature)**: Δ vs. w curves and separatrix-layer structure vs. published RDK-NTM figures: D23b Fig. 3 (layer-resolved g at both σ), Fig. 4 (g(p_φ) separatrix zoom), Fig. 6 (u_∥i contours with/without layer), Fig. 7 (δΦ contours), Fig. 9 (Δ_neo, Δ*_bs, Δ_pol vs. w against 1/w and 1/w³). **T2 (internal)**: the layer-on/layer-off threshold *differential* measured within Islands (sources report w_c 0.78 → 0.52 ρ_θi at ω_E = 0, ν_i = 10⁻³ — the ~⅓ reduction is the comparable, the absolutes are T4) | [CHECKED: D23b §3–4] |
+| B7 | **T2 (primary quantitative gate — we control all inputs on both sides)**: DK mode vs. RDK cross-check mode agreement above the documented ν̂ floor; prior-art agreement window ν_★ ~ 10⁻³–10⁻⁴ (D21 Appendix C benchmarked DK-NTM against RDK-NTM there); note kokuchou could not operate below ν_★ = 5×10⁻³ — beating that floor is an Islands numerics deliverable | internal; [CHECKED: D21 App. C; L23 §5.3] |
+| B8 | W minority: multi-species neoclassical fluxes & bootstrap modification vs. NEO multi-species; PS-impurity/banana-bulk mixed regime | NEO |
+| B9 | Threshold-vs-experiment context check (not pass/fail): La Haye NSTX+DIII-D fit w_c = 0.26 ρ̂_θi ≈ 0.955 ρ_bi half-width (quoted as full width 1.91 ρ_bi in the source), vs. B5b/B5c — the residual gap (rotation, shaping, finite ε) is the Level 2–4 motivation | [CHECKED: L23 pp. 131–132, Fig. 6.12; La Haye 2012] |
+
+## C. Level 2 physics
+
+| ID | Target |
+|---|---|
+| C1 | General-geometry no-island neoclassics vs. NEO on a shaped numerical equilibrium |
+| C2 | Orbit frequencies ω_b, ω_t, ω_D(λ, E) vs. standalone guiding-center integrator and large-ε analytic formulas; includes the :original/:improved ω̂_D forms (docs/01 §2.1) as analytic checks |
+| C3 | Circular/low-β toggle set reproduces Level 0–1 results on the same equilibria |
+| C4 | Shaping/finite-β vs. D23a — **T3 (primary)**: (i) triangularity is *destabilizing* (w_c grows as δ decreases; trend direction also seen in the DIII-D 2/1 onset database); (ii) the aspect-ratio *crossover* — w_c ∝ ε^{1/2}ρ_θi at small ε turning to ∝ ρ_θi beyond ε ≈ 0.3; (iii) w_c *grows with β_θ* (trend vs. EAST 91972 context). **T4 (audit-gated)**: the absolute widths (sources: 2w_c 1.82 ρ_bi at δ = +0.42 → 2.90 ρ_bi at δ = −0.5, at ν_★ = 10⁻⁴, m/n = 2/1, Miller geometry) | [CHECKED: D23a §6.3, Figs. 5–9] |
+| C5 | Slowing-down F₀: analytic slowing-down flux/current limits in no-island geometry [identify best analytic target — candidate: classical alpha-driven current / electron shielding results] |
+| C6 | Precession resonance: trace-EP Δ contribution vs. a reduced analytic resonant-response model in a controlled limit [derive companion analytic limit as part of the study — publishable on its own; the bounce/angle-variable + separatrix-layer machinery of Dudkovskaia JPCS 1125 012009 (2018) is the methodological antecedent] |
+
+## D. Level 3–4 physics
+
+| ID | Target |
+|---|---|
+| D1 | **Linear limit vs. SLAYER**: (Δ_cos + iΔ_sin)(Q) across drift-MHD regimes; frame/Q mapping documented. **Prerequisite**: the in-repo SLAYER Δ(Q) lands with the Tearing module PR (#238, `src/Tearing/InnerLayer/SLAYER/`), sequenced before Islands M0 (docs/00, docs/06 §1); this benchmark then calls it directly in CI. Published Park 2022 curves remain the independent cross-check. [VERIFY: Park PoP 29 (2022) conventions — paper not in the reference library; acquire] |
+| D2 | Constant-ψ recovery: prescribed-island results re-derived as the small-Δ′, w ≫ δ_layer limit of the self-consistent solve |
+| D3 | Fluid-limit toggles vs. an established nonlinear cylindrical two-fluid code (TM1-class case) for island growth and penetration |
+| D4 | Penetration bifurcation: fold structure and hysteresis qualitatively vs. Fitzpatrick 1998; thresholds with L4 torque balance vs. SLAYER-derived thresholds in the linear limit |
+| D5 | w_d: threshold island width scaling vs. Fitzpatrick PoP 2, 825 (1995) with the χ⊥ operator on [VERIFY target formula] |
+| D6 | Radiative island: growth/threshold trends vs. published thermo-resistive island model (Gates–Delgado-Aparicio class) [select specific reference case] |
+| D7 | Torque moment: linear-limit Δ_sin ↔ SLAYER torque; NTV-side consistency check against the in-repo KineticForces (PENTRC) module in the appropriate limit [scope carefully — may be a paper, not a benchmark] |
+
+## E. The toggle-impact studies (deliverable science, run on the ladder)
+
+Not pass/fail — measured differences, each a figure or paper section:
+
+- E1: drift-frequency model :original vs. :improved — the **T2 toggle
+ differential** (the ~×6 w_c reduction measured within Islands, the robust
+ form of the sources' 8.73 → 1.46 ρ_bi finding; B5a/B5b configs), then extend
+ the *ratio* across parameter space. The toggle is one term (L̂_B⁻¹ handling,
+ docs/01 §2.1) — the cheapest high-impact study in the program, and a ratio
+ so it needs no absolute-input manifest.
+- E2: orbit-averaged 4D vs. full 5D (the O5 ordering's cost).
+- E3: pitch-angle vs. full FP collisions across ν̂ (the O6 cost); plus the
+ energy-dependence sub-toggle ν(v) Chandrasekhar-form vs. V⁻³ (I19 vs. D21
+ operator variants, docs/01 §2.3) — quantifies a known inconsistency *within*
+ the York lineage.
+- E4: flattened vs. kinetic electrons (O7) — the DK-NTM/kokuchou closure vs.
+ the RDK-NTM kinetic treatment; mandatory before trusting anything at small w
+ at L3. Includes reproducing (or refuting) L23's unexplained stabilizing
+ electron Δ_pol at ω_E = 0 (L23 §6.2.2/§7) — a live physics question the
+ ω_E scan can settle.
+- E5: local vs. profile-variation domains for EPs (O2 cost at large ρ_θα).
+- E6: fixed-ω_E vs. torque-balance ω_E (O4) — the polarization-term
+ sensitivity study; publish Δ surfaces both ways. D23b Fig. 8 (sign reversal
+ at ω_E ≈ −0.89 ω_dia,e) is the anchor curve.
+
+## Reporting rules
+
+1. No benchmark "passes" on a single grid: convergence demonstrated, tolerance
+ stated, both archived with the result.
+2. Every figure in every paper names its configuration (docs/03 §2) and git SHA.
+3. Disagreements with published targets are triaged as {our bug, their
+ approximation, **their published-equation error** (see standing triage
+ rule), transcription error, **under-specified or irreproducible source
+ configuration** (T4 targets with incomplete input manifests)} — with
+ [VERIFY] resolution logged in this file's history before the triage
+ concludes anything other than "our bug."
+4. Every ladder benchmark ships with a figure script per the docs/07 §2
+ pipeline; the same script feeds CI artifacts, the state gallery, and paper
+ panels. Ladder status renders automatically into docs/src/islands/state/STATE.md — this
+ file defines targets; the dashboard reports reality.
+5. Threshold numbers are always reported as **half-widths with the unit
+ stated** (ρ_θi and ρ_bi = ε^{1/2}ρ_θi both given at the run's ε), because
+ the literature mixes half/full widths and both gyroradius units (docs/01 §1).
+6. Any T4 comparison publishes its **input manifest** (template above)
+ alongside the result — a comparison without a manifest is not a comparison.
+7. **Prefer differential/ratio comparisons over absolute ones** wherever the
+ sources permit: toggle differentials, trend slopes, and dimensionless ratios
+ cancel shared input uncertainty and are the project's strongest quantitative
+ claims (Decision D9).
+8. Any claimed quantitative agreement *or* disagreement with a T4 target is
+ accompanied by an **input-sensitivity scan** over the manifest's
+ unspecified/assumed entries, so the reader can judge how much of the
+ residual input uncertainty explains.
diff --git a/docs/src/islands/design/06-autonomy-and-tooling.md b/docs/src/islands/design/06-autonomy-and-tooling.md
new file mode 100644
index 000000000..bfdc7b33c
--- /dev/null
+++ b/docs/src/islands/design/06-autonomy-and-tooling.md
@@ -0,0 +1,286 @@
+# 06 — Autonomy, tooling, and GPEC-repo integration
+
+Audience: (a) the human setting up the environment once, (b) every Claude Code /
+Fable session working this project. Sections marked **[HUMAN SETUP]** are
+one-time actions; everything else is standing guidance for agent sessions.
+
+---
+
+## 1. Living inside the GPEC repo (supersedes docs/03 §5 where in-repo)
+
+Islands develops inside the OpenFUSIONToolkit GPEC repository as a **submodule of
+the GeneralizedPerturbedEquilibrium package** (`src/Islands/`, `module Islands` —
+no separate `Project.toml`; it shares the repo's environment and CI). Layout:
+code in `src/Islands/`, docs in `docs/src/islands/` (see `src/Islands/CLAUDE.md`
+for the full map):
+
+- **The interfaces become function calls.** docs/03 §5 specified file-based
+ interfaces to Δ′ and SLAYER because a standalone repo needed them. In-repo,
+ everything Islands consumes arrives with the Tearing module work (PR #238,
+ `feature/tearing-growthrates`): SLAYER Δ(Q) (Fitzpatrick Riccati layer model,
+ `src/Tearing/InnerLayer/SLAYER/`), GGJ under the same `InnerLayer` interface,
+ the dispersion/root-finding layer (`src/Tearing/Dispersion/`), and the
+ outer-region Δ′ exposed as the full 2m×2m matrix (`delta_prime_raw` /
+ `pest3_decompose` from ForceFreeStates, fed by the new `Riccati.jl` ideal
+ solver) — all as direct Julia calls, no digitization, no format drift.
+ Equilibrium representations are reused, not re-ingested. This is the single
+ biggest win of colocating. **Sequencing:** #238 lands before Islands M0; if
+ Islands starts earlier, branch from `feature/tearing-growthrates` (docs/00
+ milestone sequencing) so the interfaces are in hand from the first commit.
+ On `develop` today the old `src/InnerLayer/SLAYER/Slayer.jl` is still a
+ placeholder — do not code against `develop`'s module layout; #238 also moves
+ GGJ under `src/Tearing/`. `src/KineticForces` (PENTRC/NTV) exists on
+ `develop` and is the natural Level-4 torque-balance counterpart.
+- **Namespace discipline**: Islands imports GPEC; GPEC never imports Islands until
+ Islands is stable enough to be a documented feature. One-way dependency,
+ enforced by CI (a test that greps GPEC sources for `using .Islands`-type
+ references).
+- **CLAUDE.md layering**: Claude Code loads nested CLAUDE.md files; the repo
+ root keeps GPEC-wide conventions (including the existing merge-conflict
+ synthesis policy), and `src/Islands/CLAUDE.md` applies within the module
+ subtree. On conflict, the more specific file governs for work inside
+ `src/Islands/`; flag genuine contradictions to the human rather than
+ resolving silently.
+- **CI**: Islands tests/benchmarks run as separate CI jobs so a red Islands ladder
+ never blocks unrelated GPEC merges (and vice versa), but the SLAYER/DCON
+ cross-checks run in *both* suites once they exist — they protect the
+ interface from both sides.
+- **Branch discipline for autonomous work**: `main` protected; all agent work
+ on `feature/islands--` branches via PR (GPEC GitFlow
+ convention, repo-root CLAUDE.md); parallel milestones (e.g.
+ M3–M4 alongside M7, per docs/00) in separate git worktrees so concurrent
+ sessions never share a working tree.
+
+## 2. The operating model: autonomy without babysitting
+
+The goal is milestone-sized unattended runs with human attention concentrated
+at a few high-leverage points. Three mechanisms make this safe:
+
+### 2.1 Machine-checkable definition of done
+
+Every autonomous run is launched against a milestone (docs/00) whose exit
+criterion is a set of ladder IDs (docs/05) going green plus CI passing. "Done"
+is never a judgment call. Launch prompts follow this template:
+
+> Work milestone M per docs/00-roadmap.md. Definition of done: ladder IDs
+> green with convergence artifacts archived, full test suite passing,
+> PR opened with the docs/05 reporting requirements. If blocked, follow the
+> escalation protocol (docs/06 §2.2) and continue with the next parallelizable
+> task. Do not weaken a benchmark tolerance or re-baseline a target to reach
+> done; that is a blocker, not a fix.
+
+### 2.2 Non-blocking escalation: `docs/src/islands/QUESTIONS.md`
+
+Autonomous runs must never stall waiting for a human, and must never guess on
+the things CLAUDE.md forbids guessing (coefficients, signs, normalizations,
+[VERIFY] clearances). The resolution: an append-only `QUESTIONS.md` queue.
+When blocked, the agent writes an entry — context, the specific question,
+options considered, its recommendation, and what work is gated — then switches
+to the next unblocked task. The human's recurring job is clearing this queue
+(and [VERIFY] tags), not supervising sessions. Entries get IDs; commits/PRs
+reference the IDs they were blocked on or unblocked by.
+
+### 2.3 Guardrails in tooling, not vigilance
+
+**[HUMAN SETUP]** in the repo-root `.claude/settings.json` (the checked-in
+project settings shared across GPEC; add Islands-specific rules here):
+
+- `permissions`: allow the routine loop (edit within repo, `julia --project`
+ test/benchmark commands, git branch/commit/push, `gh pr` on this repo); deny
+ destructive git (force-push, `push` to main), package registry publishes,
+ and credential paths (extend the existing personal deny rules — keep those
+ global, add repo-specific ones here so collaborators inherit them).
+- Truly unattended runs (`--dangerously-skip-permissions` or equivalent
+ auto modes) only inside a container/devcontainer or on a cluster node with a
+ scratch clone — never on a laptop checkout with credentials in reach. The
+ existing tmux-on-cluster workflow is the natural home for long runs: one
+ tmux window per worktree per milestone.
+- **Hooks** (checked in with the project): PostToolUse on Edit/Write → run the
+ fast test subset for the touched module (seconds, not the ladder); a Stop
+ hook that blocks session completion if the working tree has uncommitted
+ changes or failing fast tests; PreToolUse on Bash → deny-pattern for
+ destructive commands as a second layer under permissions.
+- **Checkpointing** stays on (default) so exploratory refactors are cheaply
+ reversible.
+
+Consult https://code.claude.com/docs/en/ (docs map) when configuring — hook
+names, permission syntax, and flags change; verify against the installed
+version rather than this document.
+
+### 2.4 GitHub Actions layer
+
+**[HUMAN SETUP]**, building on the prior GPEC GitHub-App exploration (org
+permission constraints noted there still apply — may need org-owner action):
+
+- `anthropics/claude-code-action` for PR review on `src/Islands/**` and
+ `docs/src/islands/**` paths, with a
+ review prompt pointing at CLAUDE.md and docs/: check [VERIFY] policy
+ compliance, no regime-branches in operators, ladder IDs addressed.
+- Issue-triggered autonomous work: label `claude-task` on an issue containing
+ a milestone-template prompt → action runs headless (`claude -p`) in a
+ container, opens a PR. This is the "assign work from a phone" channel.
+- Scheduled (cron) workflows: nightly fast-ladder regression + allocation
+ regression; weekly full ladder on `main`. Failures open issues automatically
+ (which can themselves be `claude-task`-labeled — closing the loop on
+ maintenance without human dispatch).
+- Secrets: `ANTHROPIC_API_KEY` as a repo/org secret. Local-config gotcha from
+ prior experience: if Claude Code ignores a correctly set key and shows the
+ login menu, check `~/.claude.json` for a stale `customApiKeyResponses.rejected`
+ entry.
+
+### 2.5 Session-level habits (standing guidance for agents)
+
+- Start milestone work in plan mode; write the plan against docs/00 before
+ editing. Persist the session objective (goal/directive mechanisms) so long
+ sessions don't drift from the milestone definition of done.
+- Delegate to subagents (§4) for review/verification passes so the main
+ context stays on implementation; summarize subagent findings into the PR.
+- Commit granularly with ladder/QUESTIONS references; a session that ends
+ without a pushed branch and status note has failed its exit criteria.
+- Post-session, append a short entry to `docs/src/islands/LOG.md`: what moved, what's
+ blocked, next action. This file is the cross-session memory spine — read it
+ at session start along with QUESTIONS.md.
+
+## 3. Get Physics Done (GPD) — assessment and setup
+
+**What it is**: open-source (Apache-2.0) agentic physics-research command pack
+from Physical Superintelligence PBC, released March 2026; installs into Claude
+Code (also Codex/Gemini/OpenCode) via `npx -y get-physics-done`, adding a
+command ladder (`/gpd:help` → `start` → `tour` → `new-project` /
+`map-research` → `resume-work`) plus an autopilot mode for directed autonomous
+research. It targets exactly this project's genre — long-horizon problems
+needing rigorous verification, structured research memory, multi-step
+analytical work, and manuscript preparation — with a stated bias toward rigor
+over agreeability. Plasma physics is among its supported subfields.
+Repo: https://github.com/psi-oss/get-physics-done
+
+**Recommendation: install and trial it, scoped to a specific lane.** The
+division of labor:
+
+- **GPD lane — derivation and verification work**: clearing [VERIFY] tags by
+ independent re-derivation (its verification discipline is exactly the
+ [VERIFY] workflow's counterpart); deriving the companion analytic limits the
+ ladder needs (e.g. docs/05 C6 resonant-EP limit); literature mapping
+ (`map-research`) for the polarization-current sign genealogy before touching
+ B4; eventually manuscript drafting. GPD derivations feed
+ `docs/src/islands/derivations/` as `[DERIVED]` artifacts per CLAUDE.md — they *propose*
+ [VERIFY] clearances; a human still signs off.
+- **Native lane — implementation**: code, numerics, CI, benchmarks stay under
+ the Islands module's CLAUDE.md + docs. GPD is a research harness, not a
+ software-engineering harness; don't let two workflow systems fight over the
+ same task.
+
+**[HUMAN SETUP]** cautions: install project-local first (not global), read
+what it injects before granting — a command pack is prompt-layer software and
+should be audited and version-pinned like any dependency. Confirm its
+project-artifact directories (`GPD/`, `~/.gpd`) are gitignored or deliberately
+committed, and that its instructions don't contradict CLAUDE.md (if they do,
+CLAUDE.md wins inside `src/Islands/`; note conflicts in QUESTIONS.md).
+
+Its sibling GSD (general-purpose "Get Shit Done" workflow, which GPD is
+modeled on) is an optional trial for milestone execution on the native lane;
+adopt only if the plain milestone-prompt + hooks + ladder setup proves
+insufficient — more workflow machinery is not automatically better.
+
+## 4. Skills and subagents
+
+### 4.1 Public skills **[HUMAN SETUP]**
+
+Baseline installs from Anthropic's official marketplace
+(`/plugin marketplace add anthropics/claude-plugins-official`, and
+`anthropics/skills` as a second marketplace):
+
+- **skill-creator** — the important one. It scaffolds skills interactively and
+ runs eval loops (test cases in `evals/evals.json`, isolated subagent runs,
+ graded assertions). All custom skills below get built and *evaluated* with
+ it rather than hand-written.
+- **code-simplifier** — Anthropic's internal cleanup pass (behavior-preserving
+ simplification); run it at the end of implementation sessions to counter
+ agent-accumulated complexity.
+- Document skills (pdf/docx/pptx/xlsx) as needed for reports; low priority.
+
+Community directories (skills.sh, claudeskills.info, curated lists) are worth
+a periodic browse, but the ecosystem is flooded and physics-specific offerings
+are thin: expect nothing that knows Julia plasma physics. Adopt sparingly,
+pin versions, and audit anything that runs scripts. A community Julia skill,
+if a well-maintained one exists at setup time, can seed §4.2's `julia-conventions`
+skill; verify currency (Julia ecosystem skills go stale fast) and strip
+anything conflicting with project conventions.
+
+### 4.2 Custom project skills (the real leverage; build with skill-creator)
+
+These live in-repo (`.claude/skills/` at the appropriate level) so every
+session and CI agent inherits them. They exist to make *subagents and fresh
+sessions* cheap to orient — progressive disclosure of exactly the context that
+would otherwise be re-explained:
+
+1. **gpec-map** (repo root): GPEC/OFT architecture — where DCON Δ′, SLAYER,
+ equilibrium representations, and coordinate machinery live; module naming;
+ how to run each test suite; SFL coordinate conventions (Hamada/Boozer/PEST)
+ and Jacobian gotchas. Mostly distilled from existing GPEC docs + the
+ maintainer's head; this skill is the highest-value few hours of human
+ dictation in the whole setup.
+2. **julia-conventions** (repo root): project Julia idioms — Revise workflow,
+ per-thread preallocation patterns, allocation-test policy, `@inbounds`
+ policy, OrdinaryDiffEq-vs-QuadGK division of labor, Interpolations boundary
+ conditions. Encodes the lessons already learned so no session relearns them.
+3. **islands-conventions** (repo-root `.claude/skills/`): the load-bearing
+ distillation of docs/01–05 — half-width convention, frames module rule,
+ [VERIFY]/[DERIVED] workflow, operator-stack rules, escalation protocol.
+ Keeps subagents aligned without loading the full docs.
+4. **benchmark-ladder** (repo-root `.claude/skills/`): how to run
+ `src/Islands/verify/` and `benchmarks/islands/`, where reference data lives,
+ how to read convergence artifacts, what re-baselining requires. Paired with
+ the ladder from day one.
+5. **paper-figures** (later): publication figure conventions (Makie/matplotlib
+ styles, the editable-SVG lessons), once results exist.
+
+Maintain skills with the same [VERIFY]-grade discipline as docs: they are
+normative context, and a stale skill is worse than none. skill-creator's eval
+loop is the regression test.
+
+### 4.3 Project subagents
+
+Defined in-repo so they're versioned. Minimal set:
+
+- **physics-verifier** (read-only tools): audits diffs against docs/01
+ conventions and the [VERIFY] policy; adversarial by instruction ("find the
+ sign error" posture). Runs before every PR.
+- **numerics-reviewer** (read-only): convergence-artifact and
+ allocation-regression review; checks that "passing" benchmarks meet the
+ docs/05 reporting rules.
+- **literature-scout** (read + web): given a [VERIFY] tag, retrieves the source
+ (arXiv/DOI), extracts the exact equation context, and drafts the clearance
+ proposal for human sign-off. Pairs with an arXiv/paper-search MCP server if
+ one is connected **[HUMAN SETUP — optional]**; audit any third-party MCP
+ server before connecting, same rules as command packs.
+
+## 5. Human attention budget (what "not babysitting" costs instead)
+
+Steady state, the human's recurring surface is: (1) the QUESTIONS.md queue,
+(2) [VERIFY]/[DERIVED] sign-offs, (3) PR review of milestone branches (with
+the action + subagents having pre-reviewed), (4) gate sign-offs and Decision
+Log entries at level boundaries. Everything else — implementation, tests,
+regression triage, benchmark bookkeeping, nightly maintenance — is delegated.
+If any other category starts consuming attention, that's a tooling bug: fix
+the hook/skill/prompt, don't absorb the load manually.
+
+## 6. Setup checklist (condensed)
+
+**[HUMAN SETUP]**, in order:
+1. Create the `src/Islands/` module skeleton (submodule of
+ GeneralizedPerturbedEquilibrium — no separate `Project.toml`); the design
+ docs already live under `docs/src/islands/`. Commit docs before any code.
+2. Project settings: permissions allow/deny, hooks, checked into the repo-root
+ `.claude/`.
+3. Branch protection on main; worktree convention documented in LOG.md.
+4. GitHub Actions: claude-code-action PR review; `claude-task` issue workflow;
+ nightly/weekly ladder crons; API key secret.
+5. Plugin marketplaces + skill-creator + code-simplifier.
+6. Build gpec-map and julia-conventions skills (human-dictated, skill-creator
+ evaluated); islands-conventions and benchmark-ladder skills alongside M0–M1.
+7. Define the three subagents.
+8. GPD: project-local install, audit, trial on one [VERIFY] clearance and one
+ derivation task; decide lane adoption after two weeks of use.
+9. First autonomous run: M1 (operator-stack skeleton + MMS harness) with the
+ §2.1 template — deliberately low-physics-risk to shake out the tooling.
diff --git a/docs/src/islands/design/07-documentation-and-papers.md b/docs/src/islands/design/07-documentation-and-papers.md
new file mode 100644
index 000000000..725b0e352
--- /dev/null
+++ b/docs/src/islands/design/07-documentation-and-papers.md
@@ -0,0 +1,147 @@
+# 07 — Documentation and the paper series
+
+Principle: **documentation is a build artifact, not a chore.** The repository
+always contains an accurate, current statement of (a) what physics is
+implemented, as equations, (b) what state the code is in, and (c) what has been
+verified, as figures. Level gates are redefined so that each level concludes
+with a journal-grade manuscript whose equations are the implemented equations
+and whose figures are the passing verification tests. If the docs and the code
+disagree, the build is broken.
+
+---
+
+## 1. The four documentation layers
+
+### 1.1 The Physics Book — `docs/src/islands/`
+
+A chaptered, continuously-maintained derivation-and-implementation reference
+(Markdown + LaTeX math; rendered with the API docs). One chapter per physics
+component: coordinates & conventions, the DKE and each operator, collision
+operators, field equations, moments & the MRE assembly, species/backgrounds,
+closures. Rules:
+
+- **As-implemented, not as-aspired.** Every equation in the Physics Book is the
+ equation the code solves, in the code's normalization, with the code's sign
+ conventions. Aspirational/planned physics lives in docs/00–02, never here.
+- **Bidirectional anchors.** Every operator/term in `src/` carries a comment
+ citing its Physics Book anchor (`# physics: dke.md#magnetic-drift-improved`),
+ and every equation block in the Book carries the implementing symbol
+ (`Implemented by: MagneticDrift{:improved}`). A CI script checks both
+ directions: no operator without an anchor, no `Implemented by:` pointing at
+ a nonexistent symbol. Orphans fail CI.
+- **[VERIFY]/[DERIVED] tags live here** (migrating from docs/01 as
+ implementation proceeds); the tag state is part of the rendered page, so the
+ epistemic status of every equation is always visible.
+- **Change discipline:** any PR that changes physics behavior must change the
+ corresponding Physics Book section *in the same PR* (see §4).
+
+### 1.2 API documentation — Documenter.jl
+
+Standard Julia docstrings on all public API, built with Documenter.jl in CI
+(doctest-checked) and deployed with the GPEC docs. Docstrings for physics
+functions are thin — one-line purpose + the Physics Book anchor — so the
+physics prose has a single home. Worked examples via Literate.jl scripts that
+double as smoke tests.
+
+### 1.3 The State Dashboard — `docs/src/islands/state/`
+
+The always-current answer to "what works right now":
+
+- `STATE.md` — auto-generated (script in `src/Islands/verify/`, run in CI on main):
+ a table of levels × orderings showing each toggle's status
+ (implemented / partial / planned), and the full docs/05 ladder as a status
+ table (ID, target, status, tolerance achieved, grid tier, date, commit).
+ Hand-editing this file is forbidden; it regenerates from benchmark artifacts.
+- `LOG.md` and `QUESTIONS.md` (docs/06) — session memory and blocker queue.
+- The **figure gallery** (§2) index.
+
+### 1.4 The paper series — `docs/src/islands/papers/`
+
+Each level's gate deliverable (§3). In-repo LaTeX, figures generated only by
+pinned scripts from archived benchmark data.
+
+## 2. One figure pipeline for tests, gallery, and papers
+
+The same figure serves three masters — CI verification artifact, gallery page,
+paper panel — so there is exactly one implementation:
+
+- Every ladder benchmark (docs/05) ships with a figure script in
+ `benchmarks/islands/figures/` that reads *archived benchmark output* (HDF5/JLD2 with
+ embedded config + git SHA, per docs/03 §4), never re-runs physics. Output:
+ publication-grade PDF/SVG + a PNG for the gallery.
+- Standard verification-figure grammar, enforced by a shared plotting module:
+ Islands result (points/solid) vs. analytic limit (dashed) vs. published
+ reference data (symbols, digitized where unavoidable — in-repo SLAYER/DCON
+ comparisons are computed live in CI, one of the payoffs of colocating);
+ inset or companion panel showing grid-convergence; caption footer
+ auto-stamped with configuration name, commit, and ladder ID.
+- The gallery (`docs/src/islands/state/gallery/`) is rebuilt in the nightly/weekly ladder
+ runs — so "are we still meeting the limits we expect" is a page you (or a
+ collaborator, or a referee) can look at any day, not a claim in a README.
+- Papers `\includegraphics` from `benchmarks/islands/figures/` output directly. A paper
+ figure that cannot be regenerated by `make figures` from archived data is a
+ release-blocking bug. This is the reproducibility posture stated in
+ docs/05's reporting rules, made mechanical.
+
+## 3. The paper series: gates as manuscripts
+
+A level gate is now: **ladder IDs green + Physics Book chapters complete and
+[VERIFY]-cleared for the level's equations + a submission-ready manuscript.**
+The manuscript is not overhead on top of the gate; it *is* the gate review —
+writing it is how gaps get found. Provisional series (titles indicative;
+venues by convention of the field):
+
+| Paper | Gate | Content | Indicative venue |
+|---|---|---|---|
+| **I** | Level 0 | Formulation & numerics of the generalized island drift-kinetic solver; verification figures: MMS convergence, no-island neoclassics, large-w bootstrap limit, three-code threshold reproduction (DK-NTM 8.73 ρ_bi → RDK-NTM 1.46 ρ_bi drift-model story + kokuchou's finite-ν_★ w_c(ρ̂_θi, ν_★) surface, docs/05 B5a–c), Δ_pol(ω_E) sign-reversal curve; resolution of the L23 open question (stabilizing electron Δ_pol at ω_E = 0) is a candidate headline result | Phys. Plasmas (methods) |
+| **II** | Level 1 | Arbitrary-collisionality unification of bootstrap & polarization MRE terms; figures: Δ vs. ν̂ sweeping banana→PS with analytic corners marked; W minority: friction-modified Δ_bs and in-island impurity asymmetry | Nucl. Fusion |
+| **III** | Level 2 | Shaped/finite-β geometry; energetic-particle island response: slowing-down alphas, finite-orbit nonlocality, precession resonance ω ~ ω_D,α; burning-plasma NTM threshold implications | Nucl. Fusion (PRL letter if the resonance result warrants) |
+| **IV** | Level 3 | **Flagship:** unification of linear layer (SLAYER limit) and nonlinear island response; the w ~ δ_layer kinetic penetration-to-NTM transition; figures: Δ(w, Q) surface with SLAYER curves recovered on the small-w edge and MRE terms on the large-w edge | PRL/letter + long-form companion |
+| **V** | Level 4 | Self-consistent thresholds: torque-balance ω, w_d with explicit χ⊥, radiative/W thermo-resistive islands; confrontation with empirical penetration & onset databases | Nucl. Fusion |
+| **VI** | M12 | Δ-surface dataset + emulator; code/data release paper | Comput. Phys. Commun. or NF |
+
+Rules:
+
+- **Paper = milestone structure.** Each paper gets `docs/src/islands/papers/paper-N/` with
+ OUTLINE.md (claims → figure list → ladder IDs backing each claim) created at
+ level *start*, not level end. The outline is the level's figure contract:
+ agents implementing benchmarks know which figures are paper figures from day
+ one. Outline claims lacking a ladder ID are flagged — every claim is backed
+ by a test.
+- **Companion derivations** (e.g. the C6 resonant-EP analytic limit) are
+ developed in `docs/src/islands/derivations/` (GPD lane, docs/06 §3) and either fold into
+ the paper or spin out — decided at outline review.
+- **Authorship/collaboration** is a human matter; agents draft methods,
+ verification sections, and figure captions (which they can ground in the
+ archived artifacts), humans own claims, framing, and submission. No
+ submission without every [VERIFY] tag in the paper's equation set cleared.
+- Interim results that don't warrant a paper still get the same treatment at
+ milestone scale: a short technical note in `docs/src/islands/notes/` with gallery
+ figures — the habit is uniform, only the polish varies.
+
+## 4. Enforcement: docs change when code changes
+
+Mechanisms, weakest to strongest:
+
+1. **CLAUDE.md policy** (amended): physics-behavior PRs must update the
+ Physics Book section and, if outputs change, regenerate affected figures.
+ The PR description lists doc anchors touched, or states `docs-not-needed:`
+ with justification (reviewer-visible, greppable, audited).
+2. **Anchor-sync CI check** (§1.1): orphaned operators or dangling
+ `Implemented by:` references fail CI.
+3. **State regeneration**: STATE.md and the gallery rebuild on main; a PR that
+ turns a ladder ID red turns the dashboard red — visible drift, fast.
+4. **physics-verifier subagent** (docs/06 §4.3) gains an explicit doc-sync
+ audit: diff the operator changes against the Physics Book diff and flag
+ semantic mismatches (the thing CI string checks can't catch).
+5. **Doctest/Literate examples** run in CI, so API docs can't silently rot.
+
+## 5. Division of documentation labor
+
+Agents maintain: docstrings, Physics Book edits accompanying their own PRs,
+figure scripts, STATE regeneration, gallery, first drafts of methods/
+verification prose, technical notes. Humans own: [VERIFY] clearances, Physics
+Book review at gates, paper claims/framing/submission, and the outline reviews
+that start each level. The recurring human documentation surface is therefore
+the same queue-shaped work as docs/06 §5 — review and sign-off, not authorship
+from scratch.
diff --git a/docs/src/islands/design/08-reference-library.md b/docs/src/islands/design/08-reference-library.md
new file mode 100644
index 000000000..137de853a
--- /dev/null
+++ b/docs/src/islands/design/08-reference-library.md
@@ -0,0 +1,65 @@
+# 08 — Reference library
+
+The primary sources live in-repo at
+`docs/resources/Drift_Kinetic_Island_References/`. This file maps each PDF to
+its role in the project, its abbreviation used across docs/00–05, and the
+load-bearing content a reader (human or agent) should pull from it. Equation
+transcriptions from these sources into docs/01 carry [CHECKED]/[VERIFY] tags
+per the docs/01 header semantics.
+
+## The DK-NTM / RDK-NTM / kokuchou lineage (core Level-0/1 sources)
+
+| Abbrev. | File | What it is | Load-bearing content |
+|---|---|---|---|
+| **WCHH96** | `1996-Wilson-Threshold_for_neoclassical_magnetic_islands_in_a_low_collision_frequency_tokamak.pdf` | Wilson, Connor, Hastie & Hegna, PoP **3**, 248 (1996), doi:10.1063/1.871830. The foundational analytic NTM-threshold paper of the lineage (added 2026-07-09) | The analytic flattened-electron closure (the h(Ω) construction and coupled electron-flow relation — the primary source behind docs/01 §2.4, previously cited only via I19/L23/Diss19 transcriptions); the large-w bootstrap limit **Eq. (85)** (ladder B2 target); the collisional polarization discussion. First-hand source for the M2b electron-closure derivation cross-checks |
+| **PRL18** | `2018-Imada-Nonlinear_Kinetic_Ion_Response_to_Small_Scale_Magnetic_Islands_in_Tokamak_Plasmas.pdf` | Imada et al., PRL 121, 175001 (2018). First announcement of DK-NTM | Compact statement of the drift-island result (w_c ≃ 2.7 ρ_θi); **caution: uses ψ_s-based normalizations, different from I19's r_s-based ones** — see docs/01 §1 |
+| **JPCS18** | `2018-Imada-Drift_kinetic_response_of_ions_to_magnetic_island_perturbation_and_effects_on_NTM_threshold.pdf` | Imada et al., Varenna proceedings (2018) | Condensed DK-NTM derivation; explicit electron-flow and h(Ω) formulas; renames Δ′_bs → Δ′_loc; MRE context incl. Δ_pol ∝ 1/w³ discussion |
+| **I19** | `2019-Imada-Finite_ion_orbit_width_effect_on_the_neoclassical_tearing_mode_threshold_in_a_tokamak_plasma.pdf` | Imada et al., NF 59, 046016 (2019). The complete DK-NTM reference paper | Full equation hierarchy (Eqs. 23–34); master 4D equation Eq. (32); S-function Eq. (33); collision operator Eqs. (9)–(12); electron closure §3 (Eqs. 14–22); numerics appendix (shooting method, y_c matching Eqs. A.7–A.10, Picard loops, Eq. A.11 quasineutrality); w_c ≃ 2.76 ρ_θi (Fig. 9). **Known errata: see L23 §2.6 amendment list** (docs/01 header warning) |
+| **Diss19** | `2019-Dudkovskaia-Modelling_NTMs_in_tokamak_plasmas_PhD_dissertation.pdf` | Dudkovskaia PhD dissertation, York 2019 | The full RDK derivation chain: island geometry & Ω convention (Ch. 2), S-coordinate reduction, trapped-passing layer analytics (Ch. 3, width √(ν/εω) in λ), Δ_neo normalization (Eq. 4.12) and bootstrap/polarization split (Eqs. 4.13–4.15), frame identities ω₀ = −ω_E (pp. 47–48), torque-balance roots (Fig. 4.18), **complete solver coefficient sets in Appendices C–E (Eqs. D.60–D.62)** — the RDK cross-check mode's spec |
+| **D21** | `2021-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_a_low_collisionality_tokamak_plasma_magnetic_island_threshold_physics.pdf` | Dudkovskaia et al., PPCF 63, 054001 (2021). RDK-NTM v.1 | The improved magnetic-drift model (App. A, Eq. A2: cos θ structure of ∂B/∂ψ; L̂_B⁻¹ = 0 proxy, footnote 10) → w_c ≈ 0.45 ρ_θi ≡ 1.41–1.47 ρ_bi half-width; DK-NTM benchmark in App. C (agreement window ν_★ ~ 10⁻³–10⁻⁴); threshold-mechanism statement (§7: passing-particle physics) |
+| **D23a** | `2023-Dudkovskaia-Drift_kinetic_theory_of_the_NTM_magnetic_islands_in_a_finite_beta_general_geometry_tokamak_plasma.pdf` | NF 63, 016020 (2023). RDK-NTM v.2: finite β, shaped (Miller) geometry | Authoritative "8.73 → 1.46 ρ_bi half-width" statement (abstract); finite-β drift terms (Eqs. 28–31); Miller parametrization (Eq. 33); shaping results: triangularity 2w_c = 1.82 ρ_bi (δ=+0.42) → 2.90 ρ_bi (δ=−0.5); ε ≈ 0.3 crossover of the w_c scaling; β_θ trend vs. EAST 91972 (ladder C4) |
+| **D23b** | `2023-Dudkovskaia-Drift_kinetic_theory_of_neoclassical_tearing_modes_in_tokamak_plasmas_polarisation_current_and_its_effect_on_magnetic_island_threshold_physics.pdf` | NF 63, 126040 (2023). RDK-NTM v.3: separatrix layer + polarization + ω dependence | **Table 1** = the code-family comparison (DK-NTM vs RDK v.1/v.2/v.3); both layer widths ∝ ν^{1/2} (§3.1, footnote 11); ω_E as input parameter; Δ_pol(ω_E) sign reversal at ≈ −0.89 ω_dia,e (Fig. 8); layer effect on threshold 0.78 → 0.52 ρ_θi (Fig. 9); the B6 figure set (Figs. 3, 4, 6, 7, 9, 11, 13) |
+| **L23** | `2023-Leigh-Drift_kinetic_simulations_of_Neoclassical_Tearing_Mode_instabilities_in_finite_collisionality_tokamak_plasmas.pdf` | Leigh PhD thesis, York Dec 2023. The `kokuchou` code (DK-NTM successor, finite ν_★) | **§2.6 amendment list against I19 Eq. (A.1)** (the [VERIFY] policy's empirical justification); WCHH96 electron closure spelled out (§2.4–2.5, k = −1.173, f_p = 1−1.46√ε); TSVD treatment of the singular y_c matching matrix (§4.2); separatrix-layer width scalings incl. the iteration-dependent E×B regime (§6.1.2); Picard non-convergence forensics (§6.1.1); spurious "winged" Neumann branch (§5.3, §7.1); memory/cost data for the dense shooting method (pp. 80–84); **w_c ≈ 0.440 ρ̂_θi + 0.0178 ν_★ − 7.54×10⁻⁵** (Eq. 6.3.2, ν_★ ∈ [0.005, 0.020]); future-work list §7.1 (mapped p̃ coordinate, analytic far-field BC) |
+
+## Background / adjacent
+
+| Abbrev. | File | What it is | Role |
+|---|---|---|---|
+| **JOP18** | `2018-Dudkovskaia-Island_Stability_in_Phase_Space.pdf` | Dudkovskaia, Garbet, Lesur, Wilson — JPCS 1125, 012009 (2018) | **Not about magnetic islands.** Bump-on-tail *phase-space* island stability (Vlasov–Fokker-Planck–Poisson secondary modes). Relevant only as (a) the methodological antecedent of the RDK bounce/angle-variable and separatrix-layer machinery, (b) EP-physics background for the Level-2 precession-resonance study (ladder C6). Do not cite it as an NTM threshold source |
+| **B26** | `../2026-Burgess-Tearing Stability Prediction Combining Toroidal Calculations With a Two-Fluid Slab Layer.pdf` (general `docs/resources/` dir) | Burgess et al. (2026): tearing stability prediction combining toroidal outer-region calculations with the Park 2022 SLAYER two-fluid slab layer | **The methodological template Islands generalizes** (user-flagged, 2026-07-09): outer-region toroidal Δ′ matched to a regime-generalized *linear, zero-width* inner layer (SLAYER Δ(Q)) gives classical-tearing stability across drift-MHD regimes. Islands is the same architecture with the inner region extended from the zero-width linear layer to finite-width islands (NTMs) — the D1/L3 unification target and the `Δ_cos + iΔ_sin ↔ Δ(Q)` small-amplitude limit (ladder D1). Read alongside Park 2022 for the Q-convention and the outer/inner matching interface (`docs/03 §5`) |
+
+## Referenced but not yet in the library (acquire)
+
+- ~~WCHH96~~ **Acquired** (2026-07-09): now in the library table above.
+- ~~Park, Phys. Plasmas 29 (2022) — SLAYER. Needed for the D1 Q-convention
+ map.~~ **Found in-repo** (2026-07-09): it lives in the general GPEC library,
+ `docs/resources/2022-Park-Parametric dependencies of resonant layer responses
+ across linear, two-fluid, drift-MHD regimes.pdf` — outside this island
+ subfolder, which is why this map missed it. The D1 Q-convention `[VERIFY]`
+ can be worked from that file; see also **B26** above for the SLAYER-based
+ outer/inner matching workflow Islands generalizes.
+- La Haye et al. (2012 NSTX/DIII-D scaling; 2006) — experimental threshold fits
+ behind ladder B9.
+- Sauter et al., PoP 6, 2834 (1999); Glasser–Greene–Johnson 1975; Fitzpatrick
+ 1993/1995/1998; Cole & Fitzpatrick 2006; Rutherford 1973; Waelbroeck &
+ Fitzpatrick 1997; Smolyakov — the classical MRE-term and penetration
+ literature (ladder B1–B4, D4–D5 targets).
+
+## Known cross-source inconsistencies (pinned so nobody re-trips on them)
+
+1. **Normalization drift within the lineage**: PRL18 normalizes to ψ_s, I19/L23
+ to r_s, D21/D23b to w_ψ. Islands pins r_s-based forms with maps in
+ `src/frames/` (docs/01 §5).
+2. **Helical angle**: I19/L23 use ξ = m(θ − φ/q_s); Diss19/D21 use
+ ξ = φ − q_s θ with cos nξ. Same island, different angle multiplicity.
+3. **Collision-frequency energy dependence**: I19/L23 use the Chandrasekhar
+ form; Diss19/D21 use V⁻³ (docs/05 E3 sub-toggle).
+4. **ψ̃ amplitude**: one I19 extraction rendered ψ̃ = (w_ψ²/4)(q_s/q_s′);
+ dimensional analysis and Diss19/D21/L23 give (w_ψ²/4)(q_s′/q_s)
+ [VERIFY against I19 as printed — possible typo in the paper].
+5. **DK-NTM run collisionality**: I19 §4.2 states ν_★ = 0.01; L23 p. 82 quotes
+ DK-NTM at ν_★ = 10⁻³; D21 App. C benchmarks at ν_★ ~ 10⁻³–10⁻⁴
+ [VERIFY before pinning B5a tolerances].
+6. **Half vs. full width**: all York w_c values are half-widths; La Haye
+ experimental fits are quoted as full widths (w_marg = 2w_c). Ladder
+ reporting rule 5 exists because of this.
diff --git a/docs/src/islands/design/09-input-manifests.md b/docs/src/islands/design/09-input-manifests.md
new file mode 100644
index 000000000..4c1260097
--- /dev/null
+++ b/docs/src/islands/design/09-input-manifests.md
@@ -0,0 +1,143 @@
+# 09 — Input-completeness audit (the source input manifests)
+
+**Decision D9 (docs/05 "Target tiers"), Paper-I claim C9.** Reproducing an
+*absolute* threshold number from a publication (a T4 target) requires **every
+input** of that publication's exact scenario. This file audits what each
+DK-NTM/RDK-NTM/kokuchou source actually pins down. The rule (docs/05): a T4
+comparison is attemptable only where the manifest is complete; where a required
+input is **unspecified**, the target permanently downgrades to T3 (scaling +
+trend) and any residual gap is reported with an input-sensitivity scan.
+
+**This is itself a reproducibility result** — it documents that the published
+NTM-threshold configurations are, in several places, under-specified or
+internally contradictory (the type specimen: I19's own run collisionality).
+
+## Manifest template
+
+Each required input, per source: **value + where the paper states it**, or
+**"unspecified → assumption + sensitivity needed"**. Fields:
+`ε` · `q_s` · shear `ŝ`/`L̂_q` · density gradient `L̂_n` · temperature gradient
+`L̂_T`/`η_j` · `τ=T_e/T_i` · `ν_★` per species · `ω_E` · `m/n` · domain `L_x` ·
+resolution / convergence · far-field BC · threshold-extraction procedure ·
+`Δ′` convention · frame · species list.
+
+---
+
+## I19 — Imada et al., NF 59, 046016 (2019) — DK-NTM (B5a)
+
+Audited first-hand (print pp. 2–6, 10–11). The flagship manifest.
+
+| Input | Value | Source | Status |
+|---|---|---|---|
+| `ε = r_s/R₀` | 0.1 | §4 (figures) | ✅ specified |
+| `m/n` | 2/1 | §4 | ✅ |
+| `q_s` | 2 (= m/n) | Eq. 6 def. | ✅ |
+| shear `L̂_q` | 1 (`L̂_q⁻¹ = (ψ_s/q_s)dq/dψ = 1`) | §4 normalization | ✅ |
+| `τ = T_e/T_i` | 1 (`T_e = T_i`) | §3 ("hydrogenic, quasi-neutral") | ✅ |
+| density gradient `L̂_n` | present (`F'_M` drive), value **unspecified** | §2 | ⚠️ unspecified |
+| temperature gradient `η_j` | **unspecified** (η appears in Eq. 22 but run value not given) | §3 | ⚠️ unspecified |
+| **`ν_★`** | **CONTRADICTORY: §4.2 states `ν_★ = 0.01`; L23 p. 82 quotes this same DK-NTM run at `ν_★ = 10⁻³`** | §4.2 vs L23 | ❌ **contradictory** |
+| `ω_E` | 0 (island rest frame, no equilibrium E_r) | §2 | ✅ |
+| domain `L_x` | **unspecified** (shooting method; far-field "away from the island") | Appendix | ⚠️ unspecified |
+| resolution / convergence | shooting (`n_ξ`, `n_p`, `n_y` grids, Fig. A1); no convergence table | Appendix, Eq. A.2 | ⚠️ partial |
+| far-field BC | localized: `ĥ` radial gradient → 0 away; equilibrium gradient present | Appendix (after A.1) | ✅ described |
+| threshold-extraction | `w_c` where `dw/dt = 0` (MRE Eq. 1 with a given `Δ′`); the `Δ′` value assumed is not stated | Eq. 1, §5 | ⚠️ `Δ′` value unspecified |
+| `Δ′` convention | standard tearing index, jump in `A_∥` log-derivative | Eq. 1 | ✅ convention / ⚠️ value |
+| frame | island rest frame | §2 | ✅ |
+| species | 1 bulk ion (DK) + flattened electrons (WCHH96) | §2–3 | ✅ |
+| **Reported T4 number** | `w_c ≃ 2.76 ρ_θi` half-width (`≡ 8.73 ρ_bi` at ε=0.1) | Fig. 9 | — |
+
+**Verdict (I19 / B5a): T4 NOT cleanly attemptable.** Two required inputs are
+missing or contradictory: the run **`ν_★` is internally inconsistent** (0.01 vs
+10⁻³ — a factor of 10), and the **`Δ′` value** behind the `dw/dt=0` threshold is
+not stated. B5a therefore stays **T3** (threshold *exists* at `w_c ~ O(ρ_θi)`);
+any absolute comparison must scan `ν_★ ∈ [10⁻³, 10⁻²]` and report the sensitivity
+`∂w_c/∂ν_★` (which is exactly what kokuchou's B5c surface quantifies).
+
+## D21 — Dudkovskaia et al., PPCF 63, 054001 (2021) — RDK-NTM v.1 (B5b)
+
+Audited: abstract + App. A/B (first-hand); config from the abstract/§7.
+
+| Input | Value | Source | Status |
+|---|---|---|---|
+| `ε` | 0.1 (ρ_bi = ε^{1/2}ρ_θi context) | abstract | ✅ |
+| `m/n`, `q_s`, `L̂_q` | 2/1, 2, 1 (as B5a) | §7 | ✅ |
+| `τ` | 1 | (lineage) | ✅ |
+| `η_j` | `η_j = 1` | (D23a config; carried) | ⚠️ confirm per-run |
+| `ν_i★` | `10⁻³–10⁻⁴` (a *range*, not a point) | abstract/App. C | ⚠️ range not point |
+| `Φ′_eqm` (`ω_E`) | 0 | abstract | ✅ |
+| drift model | **:improved** (`L̂_B⁻¹ = 0` proxy) | App. A, footnote 10 | ✅ |
+| domain / resolution | S-space reduction; analytic layers | §3 | ⚠️ different method |
+| **Reported T4 number** | `w_c ≈ 0.45 ρ_θi ≡ 1.41–1.47 ρ_bi` half-width | abstract, Fig. 8 | — |
+
+**Verdict (D21 / B5b): T4 partial (ν_★ a range).** The **T2 primary gate is the
+`:original→:improved` toggle differential** — a within-code ratio that needs no
+absolute manifest and is the reproducible form of the `8.73→1.46` story. The
+absolute `0.45 ρ_θi` is a *range* over `ν_i★`, so report it as a T3 band, not a
+point.
+
+## D23a — Dudkovskaia et al., NF 63, 016020 (2023) — finite-β, shaped (C4)
+
+| Input | Value | Source | Status |
+|---|---|---|---|
+| `ν_★` | `10⁻⁴` | §6.3 | ✅ |
+| `m/n` | 2/1 | §6.3 | ✅ |
+| geometry | Miller (κ, δ, s_κ, s_δ, Shafranov) | Eq. 33 | ✅ parametrized |
+| triangularity `δ` scan | +0.42 → −0.5 | Figs. 5–9 | ✅ |
+| other Miller shape params (κ, s_κ, s_δ) at each δ | **unspecified in the transcription** — need first-hand read | §6.3 | ⚠️ unaudited |
+| `β_θ` | scanned; EAST 91972 context | §6.3 | ⚠️ partial |
+| **Reported T4 numbers** | `2w_c` full width 1.82 ρ_bi (δ=+0.42) → 2.90 ρ_bi (δ=−0.5) | §6.3 | — |
+
+**Verdict (D23a / C4): Level-2 milestone; T3 primary** (triangularity
+*destabilizing trend*, the ε-crossover, β_θ trend). Absolute widths T4, pending a
+first-hand audit of the full Miller parameter set at each δ.
+
+## D23b — Dudkovskaia et al., NF 63, 126040 (2023) — separatrix/polarization (B4, B6)
+
+| Input | Value | Source | Status |
+|---|---|---|---|
+| `ε`, `m/n`, `ν_i` | 0.1, 2/1, `10⁻³` | §3–4 | ✅ |
+| `ω_E` | **scanned** (the point — Δ_pol vs ω_E) | §4, Fig. 8 | ✅ (scan) |
+| layer resolution | separatrix-layer resolved (Figs. 3–4) | §3 | ✅ described |
+| **Reported T4** | reversal at `ω_E ≈ −0.89 ω_dia,e`; layer effect `w_c 0.78→0.52 ρ_θi` | Fig. 8–9 | — |
+
+**Verdict (D23b / B4): T3 primary** (ω_E² scaling + reversal *existence*; layer
+threshold *reduction ratio* ~⅓ is T2). The −0.89 location is T4 (a curve
+feature, comparable in morphology).
+
+## L23 — Leigh PhD thesis, York (2023) — kokuchou (B5c)
+
+The most fully-documented source (a thesis) — the **best T4 candidate**.
+
+| Input | Value | Source | Status |
+|---|---|---|---|
+| `ε` | 0.1 | §6.3 | ✅ |
+| `m/n`, `ω_E` | 2/1, 0 | §6.3 | ✅ |
+| `ρ̂_θi` range | `[1,5]×10⁻³` | Eq. 6.3.2 validity | ✅ |
+| `ν_★` grid | `{5,10,15,20}×10⁻³` | Figs. 6.10–12 | ✅ (grid) |
+| equation set | **L23-amended** (the §2.6 corrections) | §2.6 | ✅ (explicit) |
+| resolution / `ν_★` floor | `ν_★ ≥ 5×10⁻³` (memory-bound); `ŵ ≤ 0.75 ρ̂_θi` | §5.3, §6.1.2 | ✅ documented |
+| far-field BC | neoclassical-matching (not Neumann — §5.3 forensics) | §5.3, §7.1 | ✅ |
+| threshold-extraction | `w_c[r_s]` 2D OLS fit, R²=0.9916 | Eq. 6.3.1–2 | ✅ (with fit quality) |
+| **Reported T4 surface** | `w_c ≈ 0.440 ρ̂_θi + 0.0178 ν_★ − 7.54×10⁻⁵` | Eq. 6.3.2 | — |
+
+**Verdict (L23 / B5c): T4 attemptable** — the manifest is essentially complete
+(a thesis documents its numerics + fit quality + the amended equation set). This
+is the source Islands should target for a genuine absolute comparison **after the
+deferred constants clear**, reporting grid-convergence + the `∂w_c/∂ν_★` slope
+against the fitted `0.0178`.
+
+## Summary — what the audit changes
+
+| Ladder | Reported number | Manifest verdict | Gate tier |
+|---|---|---|---|
+| B5a (I19) | `2.76 ρ_θi` | contradictory `ν_★`, `Δ′` unspecified | **T3** (existence) — absolute needs ν_★ scan |
+| B5b (D21) | `0.45 ρ_θi` | `ν_★` a range | **T2** toggle ratio primary; absolute a T3 band |
+| B5c (L23) | `0.440 ρ̂_θi + …` | essentially complete | **T4 attemptable** (best candidate) |
+| B4 (D23b) | reversal `−0.89` | scan documented | **T3** (existence/scaling); location T4 |
+| C4 (D23a) | `1.82→2.90 ρ_bi` | Miller params unaudited | **T3** trend (L2 milestone) |
+
+The primary quantitative physics gates remain the **T2 internal differentials**
+(the drift-model toggle) and **T3 scalings/trends** — none of which need a
+complete input manifest. Absolute comparisons are attempted only for L23/B5c
+(complete manifest) with a sensitivity scan, per D9.
diff --git a/docs/src/islands/design/M1-launch-prompt.md b/docs/src/islands/design/M1-launch-prompt.md
new file mode 100644
index 000000000..1ea55cfe9
--- /dev/null
+++ b/docs/src/islands/design/M1-launch-prompt.md
@@ -0,0 +1,74 @@
+# M1 launch prompt (Islands overnight autonomous run)
+
+> This file is fed verbatim to the overnight loop:
+> `claude --permission-mode dontAsk --continue -p "$(cat docs/src/islands/design/M1-launch-prompt.md)"`.
+> It is the milestone contract (design doc `06 §2.1`). Keep it stable across
+> relaunches so `--continue` resumes the same objective.
+
+You are working milestone **M1** of the Islands module (`src/Islands/`), a
+steady-state drift-kinetic island/layer solver, autonomously and unattended.
+
+## Read first (every session)
+
+`src/Islands/CLAUDE.md` (module conventions + the [VERIFY] policy),
+`docs/src/islands/LOG.md` and `docs/src/islands/QUESTIONS.md` (session memory +
+open blockers), then the design docs
+`docs/src/islands/design/{00-roadmap,03-architecture,04-numerics,05-verification}.md`.
+The repo-root `CLAUDE.md` governs GPEC-wide conventions.
+
+## Goal (set this as your `/goal` completion condition)
+
+**M1 is done when all of the following hold:**
+1. `test/runtests_islands_*.jl` exist and are included in `test/runtests.jl`, and
+ they implement ladder **A1** (MMS: per-operator + assembled-system convergence
+ at design order) and **A2** (JVP vs. finite-difference residual), plus an
+ allocation-regression test for the `apply!` hot paths — all **green**.
+2. The full suite passes: `julia --project=. test/runtests.jl`.
+3. A PR is open onto `feature/islands` with the changes.
+4. **No `[VERIFY]` coefficient has been assigned a value** without a
+ `docs/src/islands/QUESTIONS.md` entry.
+
+## Scope
+
+- **M1 core** (design `03 §1–2`, `04`): the phase-space grids `(x, ξ, λ, E, σ)`
+ with the layer-clustered mappings; the operator-stack skeleton (`AbstractTerm`,
+ `apply!`, and the term structs from `03 §2`) as **AD-compatible,
+ allocation-free stubs**; the MMS harness and per-term AD-vs-FD JVP checks
+ (`04 §3`). Build the structure and the tests, not the physics numbers.
+- **Early-M2 structure, best-effort after M1 core is green**: wire the
+ term/moment/field structure toward the L0 solve — but **every physics
+ coefficient stays a parameterized, `[VERIFY]`-tagged stub with a skipped
+ benchmark** (CLAUDE.md rule 1).
+
+## The hard rule (non-negotiable)
+
+**Never assign a value to a `[VERIFY]` physics coefficient, sign, or
+normalization.** The moment you would need a specific number/sign from the
+literature that isn't already `[CHECKED]`-cleared: (a) implement the *structure*
+with the coefficient as a named parameter, (b) add a skipped benchmark
+referencing the `[VERIFY]` tag, (c) write a `QUESTIONS.md` entry (context /
+question / options / recommendation / gated work), and (d) **switch to the next
+unblocked task**. Guessing a coefficient is the exact failure this project
+exists to prevent.
+
+## Working discipline
+
+- Before committing any physics-adjacent change, run the **`physics-verifier`**
+ subagent; if it returns BLOCK, fix or escalate — do not commit.
+- Commit granularly to `feature/islands` with messages in the repo format
+ (`ISLANDS - - `); reference `QUESTIONS.md` IDs where relevant.
+ Push only to `feature/islands` (never `develop`/`main`; the hooks enforce this).
+- Never weaken a tolerance or re-baseline a target to reach "done" — that is a
+ blocker, not a fix.
+- Append a `LOG.md` entry (what moved / blocked / next) before ending each
+ session. The `Stop` hook will keep you from ending with a dirty tree or a
+ broken build.
+- If you exhaust the milestone's unblocked work (everything remaining is gated on
+ `QUESTIONS.md`), commit, log, and let the session end — the outer loop and the
+ human will pick it up.
+
+## Definition of NOT done (do not stop early)
+
+Do not declare M1 done if any A1/A2/allocation test is failing or skipped-as-a-
+shortcut, if the suite is red, if the tree is dirty, or if any coefficient was
+guessed. Those are blockers, not completion.
diff --git a/docs/src/islands/design/M2-launch-prompt.md b/docs/src/islands/design/M2-launch-prompt.md
new file mode 100644
index 000000000..2b406ce1b
--- /dev/null
+++ b/docs/src/islands/design/M2-launch-prompt.md
@@ -0,0 +1,169 @@
+# M2 milestone contract (Islands)
+
+> The milestone contract for Islands M2 (design doc `06 §2.1`). Set it as the
+> `/goal` completion condition when working M2.
+
+You are working milestone **M2** of the Islands module (`src/Islands/`), a
+steady-state drift-kinetic island/layer solver. M1 (phase-space grids +
+operator-stack skeleton + MMS/AD harness) is complete (PR #320); M2 builds the
+**Level-0 solve machinery** on top of it.
+
+## Read first (every session)
+
+`src/Islands/CLAUDE.md` (module conventions + the [VERIFY] policy),
+`docs/src/islands/LOG.md` and `docs/src/islands/QUESTIONS.md` (session memory +
+open blockers), then the design docs
+`docs/src/islands/design/{00-roadmap,01-physics-level0,02-species-and-eps,03-architecture,04-numerics,05-verification,06-autonomy-and-tooling,07-documentation-and-papers}.md`.
+The repo-root `CLAUDE.md` governs GPEC-wide conventions.
+
+## The gating reality (read this before you plan)
+
+M2's roadmap headline is "L0 single-species solve, Δ moments, **York gates** →
+Paper I." **The York gates are NOT reachable in this run** and reaching one is a
+policy violation, not completion. Every L0 physics coefficient the gates need
+(the `ω̂_D`/`L̂_B` toggle, the collision kernel, `k=−1.173`, `f_p=1−1.46√ε`,
+`⟨ν̂_ii⟩_u`, the quasineutrality closure, the York numbers `8.73`/`1.46` ρ_bi) is
+at most **`[CHECKED]`** — none is human-cleared — and Decisions **D7** (implement
+L0 from an independent re-derivation vs the L23-amended set) and **D8** (pin the
+B5a/b/c benchmark triangle) are **unratified** (`docs/00` Decision Log). `[CHECKED]`
+is *not* permission to hardcode a number (`docs/01` header; `CLAUDE.md` policy).
+
+So M2 here = **build the full L0 solve *structure* with every physics coefficient
+a `[VERIFY]`-gated parameter, close the physics-free structural gates, and escalate
+a prioritized clearance queue** for the human. A thin follow-up run fills the
+numbers and hits the York gates *after* a human clears the tags.
+
+## Goal (set this as your `/goal` completion condition)
+
+**M2 is done when all of the following hold:**
+
+1. **The L0 solve machinery exists** as AD-compatible, allocation-free structure,
+ with **every physics coefficient a supplied `[VERIFY]`-gated parameter (no
+ literal in `src/`)** — new submodules per design `03 §1`:
+ - `solvers/` — matrix-free **Newton–Krylov** (GMRES on the M1 ForwardDiff JVP;
+ inexact-Newton / Eisenstat–Walker forcing; line search; a pseudo-arclength
+ continuation scaffold that detects folds from day one; a physics-block
+ preconditioner that treats the `y_c` matching block explicitly), `04 §3, §5`.
+ - `moments/` — `Δ_cos`/`Δ_sin` assembly: species-charge-weighted velocity moment
+ of `g` → `J̄_∥(x, ξ)` → the `∮ J̄_∥ {cos, sin} ξ dξ ∫ dψ` projections (`01 §4`),
+ plus the bootstrap/polarization channel decomposition *structure*. The `μ₀R/2ψ̃`
+ prefactor and `ψ̃` stay **gated** (open `[VERIFY]`); the projection + quadrature
+ is pure numerics.
+ - `frames/` — THE `ω`/normalization conversion module (`01 §5`): the conversion
+ *forms* with every sign and normalization flagged and gated. This module owns
+ all `ω`-sign conventions (the polarization sign disputes are frame disputes —
+ do not reproduce them elsewhere). Unit-test only the mechanical, sign-free
+ identities.
+ - `fields/` — formalize the quasineutrality residual (already stubbed in
+ `operators/`); the flattened-electron closure *structure* gated.
+ - `species/` — `Species`, `AbstractBackground`, `SpeciesRole {Bulk, Trace}`
+ plumbing (`02 §1`, Decision D3) — pure data structures, fully unblocked. The L0
+ test is a single bulk ion + a trace-deuterium copy.
+ - **neoclassical-matching far-field BCs** (`01 §3`, `04 §1`): structure only; the
+ no-island neoclassical far-field solution is gated. **Never bare Neumann** (the
+ L23 "winged" spurious-branch failure).
+
+2. **Physics-free structural gates green** (`05 §A`), in `test/runtests_islands_*.jl`,
+ passing in the full suite:
+ - **A5** zero-drive null: gradients and `Φ̃` off ⇒ `g ≡ 0` exactly, residual =
+ machine zero, Newton converges trivially.
+ - **Assembled solve-MMS**: a manufactured `g*` with `GradientDrive` set so `g*` is
+ the exact Newton solution; the solver recovers `g*` at design order (extends
+ M1's residual-MMS to a full converged solve).
+ - **A8** `y_c` matching-block smallest-singular-value conditioning monitor
+ (regression = the silent-noise failure mode of L23 §4.2).
+ - **A4** (L0): particle conservation + discrete entropy sign `∫ g C[g]/F_M ≤ 0`
+ of the discretized collision operator (structural — holds for any valid
+ discretization, independent of the physical `ν` value).
+ - **A3** parity: `Δ_cos` even / `Δ_sin` odd under the appropriate `(ξ, σ, ω_E)`
+ reflection, tested on a manufactured `J̄_∥` (coefficient-free).
+ - **A7** the coefficient-free identity `⟨∂²h/∂x²⟩_Ω = 0` (defer the number-bearing
+ `k`, `f_p`, `⟨ν̂_ii⟩` identities to a post-clearance run).
+
+3. The full suite passes: `julia --project=. test/runtests.jl`.
+
+4. **`docs/src/islands/papers/paper-1/OUTLINE.md`** written as the Paper-I figure
+ contract (`07 §3`): claims → figures → ladder IDs (B5a/b/c, B2, B4). This is the
+ level-*start* deliverable, not the level-*gate*.
+
+5. **The clearance queue** (the parallel-human deliverable): a consolidated,
+ prioritized set of `QUESTIONS.md` entries covering every coefficient the York
+ gates need + D7/D8 ratification + the open `[VERIFY]`s, **each paired with a
+ skipped B-series benchmark** in `benchmarks/islands/` that references its tag — so
+ the human can clear in parallel and the follow-up run fills numbers and hits gates.
+
+6. A PR is open onto `feature/islands` (a sub-branch → `feature/islands`, as M1's
+ PR #320 did).
+
+7. **No `[VERIFY]`/uncleared-`[CHECKED]` coefficient has been assigned a value**
+ without a `docs/src/islands/QUESTIONS.md` entry.
+
+**Explicitly NOT in the M2 DoD (gated on human clearance — do not attempt):** the
+York gates B5a/b/c, B2, B4; B1 (needs an external NEO/NCLASS run); any physics
+number. A "green York gate" reached by hardcoding is the exact failure this project
+exists to prevent.
+
+## Scope
+
+- **Reuse existing GPEC machinery; do not reimplement** (repo-root CLAUDE.md
+ minimal-change discipline): `FastInterpolations.integrate` /
+ `cumulative_integrate` for the ψ/flux integrals (`src/Equilibrium/Equilibrium.jl`,
+ `src/ForceFreeStates/Resist.jl`); the `src/KineticForces/BounceAveraging.jl`
+ velocity-space λ-averaging pattern for the `(y, E, σ)` moments; the allocation-free
+ `QuadGK.quadgk!` pattern (`src/ForceFreeStates/Galerkin/GalerkinAssembly.jl`) for
+ hot quadrature; the `EquilibriumConfig` / `build_inputs_from_toml` TOML-config
+ pattern (`src/Equilibrium/EquilibriumTypes.jl`,
+ `src/GeneralizedPerturbedEquilibrium.jl`) for an `[Islands]` `gpec.toml` section in
+ a new `io/`.
+- **Add `Krylov.jl`** to `Project.toml` `[deps]` + `[compat]` (matrix-free GMRES;
+ `04 §9` names Krylov.jl / LinearSolve.jl — prefer Krylov.jl: lighter and
+ purpose-built). The JVP is the M1 ForwardDiff operator; form **no** global sparse
+ Jacobian except in a tiny-grid debug mode.
+- Create `benchmarks/islands/` (+ `figures/`) and at least one `islands_*` regression
+ case integrated with `regression-harness/` — every physics benchmark ships
+ **skipped**, referencing its `[VERIFY]`/`QUESTIONS` id.
+
+## The hard rule (non-negotiable)
+
+**Never assign a value to a `[VERIFY]` or uncleared-`[CHECKED]` physics coefficient,
+sign, or normalization.** The moment you would need a specific number/sign from the
+literature that isn't human-cleared: (a) implement the *structure* with the
+coefficient as a named parameter, (b) add a skipped benchmark referencing the tag,
+(c) write a `QUESTIONS.md` entry (context / question / options / recommendation /
+gated work), and (d) **ask the user to clear it if they are available, otherwise
+move to the next unblocked task**. Guessing a coefficient is the exact failure this
+project exists to prevent. Manufactured, order-unity test
+coefficients in the MMS/verification harness are legitimate (they test numerics, not
+physics) and carry no tag — do not confuse the two.
+
+## Working discipline
+
+- Before committing any physics-adjacent change, run the **`physics-verifier`**
+ subagent; if it returns BLOCK, fix or escalate — do not commit.
+- **Run every new kernel once under `--check-bounds=yes`** before trusting it. (M1
+ lesson: an `@inbounds` index-swap corrupted memory and passed silently until a
+ forced bounds-checked run caught it. Assume new nested-index kernels are guilty
+ until a bounds-checked run clears them.)
+- Invoke julia with a clean loader path:
+ `env -u LD_LIBRARY_PATH /mnt/homes_global/ncl2128/software/julia-1.11.7/bin/julia
+ --project=. …` (Q1 resolution: the OMFIT `LD_LIBRARY_PATH` shadows Julia's
+ artifacts otherwise).
+- Commit granularly to a milestone sub-branch with messages in the repo format
+ (`ISLANDS - - `); reference `QUESTIONS.md`/ladder IDs where relevant.
+ Push only Islands branches (never `develop`/`main`; the hooks enforce this).
+- Never weaken a tolerance or re-baseline a target to reach "done" — that is a
+ blocker, not a fix.
+- Append a `LOG.md` entry (what moved / blocked / next) before wrapping up a working
+ session, and keep the tree clean and the build green (`06 §2.5`).
+- If the remaining work is all gated on human clearance, surface the blockers to the
+ user (they can clear a tag or ratify D7/D8 live) rather than stalling.
+
+## Definition of NOT done (do not stop early)
+
+Do not declare M2 done if: any structural gate (A5, solve-MMS, A8, A4, A3, A7's
+`⟨∂²h/∂x²⟩=0`) is failing or skipped-as-a-shortcut; the L0 machinery submodules are
+absent; the suite is red; the tree is dirty; the Paper-I OUTLINE or the clearance
+queue is missing; or any coefficient was guessed. Those are blockers, not
+completion. Conversely, do **not** keep working past a green structural ladder in an
+attempt to reach the York gates — those are gated on human clearance and out of
+scope for this run.
diff --git a/docs/src/islands/design/M2b-launch-prompt.md b/docs/src/islands/design/M2b-launch-prompt.md
new file mode 100644
index 000000000..0740553d6
--- /dev/null
+++ b/docs/src/islands/design/M2b-launch-prompt.md
@@ -0,0 +1,139 @@
+# M2b milestone contract (Islands) — Level-0 physics via the derivation lane
+
+> The milestone contract for Islands M2b (design doc `06 §2.1`). Set it as the
+> `/goal` completion condition when working M2b. Prerequisites are in place:
+> M1 (PR #320) and M2 (PR #324) are merged — the full L0 solve *structure*
+> exists with every physics coefficient gated — and the user has ratified
+> **D7/D8** and chosen the **re-derivation-first** clearance mode (QUESTIONS
+> Q2 resolved, Q3 mode pinned, 2026-07-08).
+
+You are working milestone **M2b** of the Islands module (`src/Islands/`):
+filling in the Level-0 physics that M2 deliberately gated, by the route the
+project's own thesis demands — **independent re-derivation, human sign-off,
+then implementation** — ending with the B-ladder physics benchmarks running.
+
+## Read first (every session)
+
+`src/Islands/CLAUDE.md` (the [VERIFY]/[DERIVED]/[CHECKED] policy — rules 3 and
+4 are the spine of this milestone), `docs/src/islands/LOG.md` and
+`QUESTIONS.md` (Q3 items and mode, Q4), then design docs
+`docs/src/islands/design/{00-roadmap,01-physics-level0,03-architecture,04-numerics,05-verification}.md`
+— **especially docs/05 "Target tiers and reproducibility" and Decision D9
+(00): the physics gates are scalings/differentials, absolute numbers are
+audit-gated** — and the as-implemented chapter `docs/src/islands/numerics.md`.
+The source PDFs are in the `docs/08` reference library. The repo-root
+`CLAUDE.md` governs
+GPEC-wide conventions.
+
+## Goal (set this as your `/goal` completion condition)
+
+**M2b is done when all of the following hold:**
+
+1. **The derivation set exists** in `docs/src/islands/derivations/`, one
+ chapter per Q3 item, each marked `[DERIVED: date]` and structured as:
+ stated starting point (the drift-kinetic equation and orderings O1–O9,
+ cited to `01 §2`), explicit assumptions, the derivation with no skipped
+ sign-bearing steps, a boxed final coefficient form, and a **cross-check
+ table** against the `[CHECKED]` transcriptions (I19/Diss19/D21/L23,
+ honoring the L23 §2.6 amendments). Items:
+ - (a) the orbit-averaged drift frequency `ω̂_D` including the
+ `:original`/`:improved` `L̂_B⁻¹` treatment;
+ - (b) the pitch-angle collision kernel, the `ν_★` normalization, and the
+ analytic `⟨ν̂_ii⟩_u` velocity average;
+ - (c) the flattened-electron closure: the `h(Ω)` prefactor, the coupled
+ flow relation, and the `k`/`f_p` constants;
+ - (d) the quasineutrality closure coefficient;
+ - (e) the `ψ̃` island amplitude — deriving it settles the Q4
+ `q_s′/q_s` vs `q_s/q_s′` question by dimensional necessity;
+ - (f) the `Δ_cos`/`Δ_sin` prefactors, including pinning the sin-moment
+ normalization (a `[DERIVED]` pin per `01 §4`).
+ **Every discrepancy against a transcription is flagged in the table and in
+ `QUESTIONS.md` — never resolved silently** (policy rule 2).
+2. **Human sign-off, item by item.** Present each derivation to the user (they
+ are working interactively; ask). A signed-off derivation clears the
+ corresponding coefficients: record the clearance in `docs/01` (source,
+ equation, date, derivation link) per policy rule 3. **You never sign off
+ your own derivation.**
+3. **Implementation fill-in for signed-off items only**: a single Level-0
+ coefficient/configuration builder in `src/Islands` (the named-configuration
+ mechanism of `03 §2` — e.g. `:dkntm_original`, `:rdkntm_improved`) that
+ populates the M2 gated parameters, each value annotated with its derivation
+ anchor. Un-gate progressively: partial sign-off ⇒ partial fill-in; anything
+ unsigned stays NaN/supplied.
+4. **The input-completeness audit** (Decision D9, docs/05 "Target tiers"): a
+ per-source *input manifest* (using the docs/05 template) for each threshold
+ source — I19, D21, D23a/b, L23 — written into a new
+ `docs/src/islands/design/09-input-manifests.md` (or a docs/05 appendix;
+ executor's choice), each required input either cited to where the paper
+ states it or recorded as "unspecified → assumption + sensitivity scan
+ needed". This audit is what decides, per target, whether a T4 absolute
+ comparison is even attemptable or the target stays T3. It is itself a
+ Paper-I methods deliverable (OUTLINE claim C9). B5a's collisionality
+ contradiction is the type specimen — resolve it *in the manifest*, not by
+ picking a number.
+5. **The B-ladder starts running — tiered (D9)**: for cleared configurations,
+ un-skip the `benchmarks/islands/` scripts. The DoD is the **primary-tier
+ gates running with archived artifacts**: the T2 drift-model **toggle
+ differential** (:original → :improved w_c ratio, measured within Islands),
+ and the T3 **scaling/existence/trend** sweeps (1/w and 1/w³, layer widths
+ ∝ ν^{1/2}, ω_E² with a sign reversal existing, threshold existence at
+ w_c ~ O(ρ_θi), dw_c/dν_★ > 0) with fitted exponents/signs. **T4 absolute
+ comparisons are attempted only for manifests the audit completes, and
+ reported only with the manifest + an input-sensitivity scan** (docs/05
+ reporting rules 6–8) — never as bare pass/fail. Follow the docs/05 reporting
+ rules (grid-convergence archived; half-widths with both ρ_θi and ρ_bi
+ stated); triage disagreements per docs/05 rule 3 (now including
+ "under-specified source configuration"). B5 absolute *agreement* is never
+ this milestone's precondition.
+6. Q4 source status: WCHH96 and Park 2022 are both in the library (resolved
+ 2026-07-09; docs/08). Nothing to acquire — proceed with the in-repo sources.
+7. The full suite passes; the Physics Book chapter
+ (`docs/src/islands/numerics.md` or a new physics chapter) is updated **in
+ the same PR** for every equation that becomes as-implemented (docs/07
+ policy), with figures regenerated via the pinned script where outputs
+ change.
+8. A PR is open onto `feature/islands`, and `physics-verifier` has passed on
+ every physics-adjacent commit — its job here is checking **provenance**:
+ derivations marked `[DERIVED]`, transcriptions never silently promoted,
+ no unsigned coefficient in `src/`.
+
+## The hard rules (non-negotiable, sharpened for this milestone)
+
+- **Never present a derivation as a literature transcription or vice versa**
+ (policy rule 4). The provenance tag is part of the result.
+- **Never assign an unsigned coefficient in `src/`** — sign-off happens in the
+ conversation and is recorded in `docs/01` before the value lands in code.
+- **Never tune a derived coefficient to make a benchmark pass** (policy rule
+ 2). If a benchmark disagrees, the triage path is docs/05 rule 3, in the open.
+- If a derivation stalls (a step you cannot justify), flag it in
+ `QUESTIONS.md` with the specific step and move to the next item — a partial
+ derivation set with honest gaps beats a complete one with a glossed step.
+
+## Working discipline
+
+- Derivation chapters are Documenter pages: one-sentence-per-line source, LaTeX
+ `math` blocks, cross-check tables as Markdown tables; wire new pages into the
+ Islands nav section in `docs/make.jl` and verify with a local docs build
+ (`env -u LD_LIBRARY_PATH …/julia --project=. build_docs_local.jl`).
+- Run every new kernel once under `--check-bounds=yes`; keep `apply!` paths
+ allocation-free (regression-tested); julia via
+ `env -u LD_LIBRARY_PATH /mnt/homes_global/ncl2128/software/julia-1.11.7/bin/julia --project=. …`.
+- Commit granularly to a milestone sub-branch (`ISLANDS - - …`,
+ referencing Q3 items and ladder IDs); update the `islands_l0_structural`
+ regression case if tracked numbers legitimately move (with justification —
+ never re-baseline to reach green); append a `LOG.md` entry per session.
+- The user is present: surface each completed derivation for sign-off rather
+ than batching everything to the end.
+
+## Definition of NOT done
+
+Any coefficient in `src/` without a recorded human sign-off; a derivation
+presented without its cross-check table; a discrepancy resolved silently; a
+benchmark "passing" via tuned coefficients or weakened tolerance; a **T4
+absolute comparison reported without its input manifest and sensitivity scan**
+(Decision D9); the suite red; docs not updated with the as-implemented
+equations; or the tree dirty. Conversely: a T4 absolute *disagreement* after
+honest triage — including "the source is under-specified" — is a reportable
+result, not a failure. **Do not chase absolute agreement**; the primary gates
+are the T2 differentials and T3 scalings, and pursuing an absolute number past
+what the derivations and input manifests support is itself out of bounds.
diff --git a/docs/src/islands/design/M2c-launch-prompt.md b/docs/src/islands/design/M2c-launch-prompt.md
new file mode 100644
index 000000000..3bcedc3e9
--- /dev/null
+++ b/docs/src/islands/design/M2c-launch-prompt.md
@@ -0,0 +1,107 @@
+# M2c milestone contract (Islands) — Level-0 assembly + audit + docs infrastructure (autonomous)
+
+> The milestone contract for Islands M2c. Fed to the autonomous loop
+> (`claude --permission-mode dontAsk --continue -p "$(cat …/M2c-launch-prompt.md)"`)
+> or set as a `/goal` completion condition. Keep it stable across relaunches.
+
+You are working milestone **M2c** of the Islands module, **autonomously and
+unattended**. The M2b derivation lane is complete: all six main Level-0
+coefficient families (`ψ̃`, `ω̂_D` + drift toggle, collision operator, `h(Ω)`
+closure, quasineutrality, `Δ` prefactors) are human-signed-off and cleared into
+`Coefficients`/`Moments`. M2c **assembles** that cleared physics into a runnable
+Level-0 configuration, produces the input-completeness audit, and stands up the
+`docs/07` infrastructure — all **without any new human sign-off**.
+
+## Read first (every session)
+
+`src/Islands/CLAUDE.md`, `docs/src/islands/LOG.md` + `QUESTIONS.md`, then design
+docs `00-roadmap`, `01-physics-level0`, `03-architecture`, `04-numerics`,
+`05-verification` (**especially "Target tiers", Decision D9**), `06-autonomy-and-tooling`,
+`07-documentation-and-papers`, and the derivations `docs/src/islands/derivations/`
+(the cleared physics). Repo-root `CLAUDE.md` governs GPEC conventions.
+
+## The hard constraint (you are unattended)
+
+**Human sign-off is unavailable this milestone.** Therefore:
+
+- **Un-gate nothing new.** Only the six already-cleared coefficient families may
+ enter `src/` (via `Coefficients.*`). The deferred sub-constants —
+ `⟨ν̂_ii⟩_u`, the Hirshman–Sigmar `k ≃ −1.173`, the `1.46` in `f_p` — stay
+ `[CHECKED]`-gated. Anywhere the assembly needs them, use a **named, supplied
+ parameter (or NaN-gate)** and record a `QUESTIONS.md` entry; never guess.
+- **Draft, don't clear.** You may *draft* the deferred-constant derivations
+ (`⟨ν̂_ii⟩_u`, `k`, `f_p`) as `[DERIVED]` chapters *awaiting sign-off* — but only
+ if you can derive them rigorously from the in-repo sources (read L23 Eq. 4.1.6
+ for `⟨ν̂_ii⟩_u`; the trapped-fraction integral for `f_p`; the parallel-viscosity
+ moment problem for `k`). **If a derivation is not rigorous, do not fake it** —
+ write a `QUESTIONS.md` entry stating exactly what's missing and move on
+ (policy rule 4). Physics-verifier every draft.
+- **Never weaken a tolerance or re-baseline to reach "done."**
+
+## Goal (set this as your `/goal` completion condition)
+
+**M2c is done when all of the following hold:**
+
+1. **Level-0 coefficient assembly.** A named-configuration builder in `src/Islands`
+ (design `03 §2`, e.g. `configure_level0(grid, params, species; variant)` →
+ an `IslandStack` + far-field BCs + `Δ`-prefactors) that populates every
+ operator-stack coefficient from the **cleared** `Coefficients.*` functions on
+ the phase-space grid (orbit-averaged `ω̂_D` per `(y,E,σ)`; the mimetic
+ collision `P(y)` from `pitch_diffusivity` + `deflection_frequency`; the
+ `Quasineutrality` coefficient; the `GradientDrive` from the cleared closure;
+ the `Δ` prefactors). The deferred `⟨ν̂_ii⟩_u`/`k`/`f_p` enter as clearly-named
+ gated parameters. Allocation-free, AD-compatible (the M1 discipline). **Tests:**
+ the assembly builds; the assembled residual/`newton_krylov` **runs and
+ converges structurally** on the `:imada2019`/`:dudkovskaia2021` configs (with
+ the deferred constants set to documented placeholder values *for the
+ structural run only*, flagged in the test as non-physics); no cleared
+ coefficient is a literal in `src/` (all via `Coefficients.*`).
+2. **Input-completeness audit** (Decision D9, `docs/05` "Target tiers"): per-source
+ input manifests (I19, D21, D23a/b, L23) in a new
+ `docs/src/islands/design/09-input-manifests.md`, each required input cited to
+ where the paper states it or marked "unspecified → assumption + sensitivity
+ needed". This is the D9/Paper-I C9 deliverable and pins which B5/C4 numbers are
+ even T4-attemptable.
+3. **`docs/07` infrastructure:** the auto-generated **STATE dashboard**
+ (`docs/src/islands/state/STATE.md`, ladder status from test/benchmark
+ artifacts — never hand-edited) and the **anchor-sync CI check** (every operator
+ cites its Physics-Book section; every equation names its implementing symbol).
+4. **B-ladder scaffolding wired to the assembly:** the skipped `benchmarks/islands/`
+ scripts updated so that, the moment the deferred constants clear, the **T2/T3
+ primary gates** (the `:original→:improved` `w_c` toggle differential; the
+ `1/w`, `1/w³`, `ν^{1/2}`, `ω_E²` scalings) run against the assembled solve. Keep
+ them skipped (gated on QUESTIONS) but make un-skipping a one-line change.
+5. **Deferred-constant derivations** drafted where rigorously possible (§ hard
+ constraint), each `[DERIVED]`, physics-verifier PASS, **awaiting sign-off** (not
+ cleared); the rest escalated in `QUESTIONS.md` with the specific missing piece.
+6. Full suite green (`julia --project=. test/runtests.jl`); the Physics-Book /
+ `numerics.md` updated for the assembly (docs/07); a PR open onto
+ `feature/islands`; physics-verifier PASS on every physics-adjacent commit.
+
+**Explicitly NOT in the M2c DoD (needs human sign-off / clearance):** un-gating
+`⟨ν̂_ii⟩_u`/`k`/`f_p`; the B-ladder physics gates; any York number. A physics
+threshold result is *not* reachable until a human clears the last constants.
+
+## Working discipline (autonomous)
+
+- Reuse existing GPEC machinery (`FastInterpolations.integrate`, `QuadGK.quadgk!`,
+ the `KineticForces/BounceAveraging` λ-averaging, the `EquilibriumConfig` TOML
+ pattern) — don't reimplement (repo-root CLAUDE.md).
+- Run every new kernel once under `--check-bounds=yes` (the M1 corruption lesson).
+- Invoke julia with `env -u LD_LIBRARY_PATH /mnt/homes_global/ncl2128/software/julia-1.11.7/bin/julia --project=. …`.
+- Any new Islands submodule needs its `@autodocs` block in `docs/src/islands.md`
+ (checkdocs=:exports — this silently reddened docs CI once; verify with a local
+ `build_docs_local.jl`).
+- Commit granularly (`ISLANDS - - …`), reference QUESTIONS/ladder IDs; append
+ a `LOG.md` entry per session; end with the branch pushed.
+- When blocked on anything requiring human judgment (a sign-off, a coefficient, a
+ convention), write a `QUESTIONS.md` entry and switch to the next unblocked task —
+ never stall, never guess.
+
+## Definition of NOT done
+
+Any deferred constant un-gated without sign-off; any coefficient guessed; a
+deferred derivation faked (presented as rigorous when it isn't); the assembly not
+running structurally; the suite red; the tree dirty; the audit or `docs/07` infra
+missing. A "physics result" reached by placeholder constants is a structural
+check, not a milestone — label it so.
diff --git a/docs/src/islands/design/REVISION-2026-07-07.md b/docs/src/islands/design/REVISION-2026-07-07.md
new file mode 100644
index 000000000..4021ae879
--- /dev/null
+++ b/docs/src/islands/design/REVISION-2026-07-07.md
@@ -0,0 +1,119 @@
+# Plan revision — 2026-07-07
+
+The original design bundle for the Islands module (drafted in Claude chat from
+memory of the literature, under the working acronym "ISLET") was revised against
+a full read of the nine PDFs in `docs/resources/Drift_Kinetic_Island_References/`
+plus a reality check of the GPEC repo's actual module status. It was then placed
+in-repo: module conventions at `src/Islands/CLAUDE.md`, overview at
+`docs/src/islands/index.md`, and these design docs at
+`docs/src/islands/design/`. (The "ISLET" acronym was subsequently retired in
+favour of the plain module name `Islands`, per the repo module-naming
+convention.) This note records the physics/scope revision; the later in-repo
+relocation and rename are tracked in git history.
+
+## What changed and why
+
+### 1. A whole prior-art code was missing from the plan
+The Leigh thesis (York, Dec 2023) describes **kokuchou**, a direct 4D
+drift-kinetic NTM threshold code — effectively a working prototype of Islands'
+Level 0 — with:
+- a **finite-collisionality threshold surface**
+ w_c ≈ 0.440 ρ̂_θi + 0.0178 ν_★ − 7.54×10⁻⁵ (new ladder target B5c);
+- an **amendment list (§2.6) documenting errors in the published Imada 2019
+ equation set** (missing ρ̂_θi factors, missing collision coefficients, a
+ sign) — now a load-bearing warning in docs/01 and a new [VERIFY] policy
+ rule 6 + proposed decision D7;
+- forensic documentation of the numerical failure modes: intrinsically
+ **singular trapped-passing matching matrix** (TSVD required; new ladder A8
+ monitor and docs/04 §3), **separatrix-layer width that moves between
+ nonlinear iterations** in the E×B-dominated regime, **Picard
+ non-convergence** in production, **spurious "winged" branches under Neumann
+ far-field BCs**, and a **memory wall** (16.6 GB/energy-point) that set its
+ ν_★ ≥ 5×10⁻³ floor. All entered into docs/04 and the docs/00 risk register;
+ each maps onto an Islands architecture choice (matrix-free Newton–Krylov,
+ neoclassical-matching BCs, adaptive layer packing).
+
+### 2. Threshold numbers and conventions pinned exactly (former [VERIFY]s)
+All York thresholds are **half-widths**; ρ_bi = ε^{1/2}ρ_θi:
+- DK-NTM: w_c ≃ 2.76 ρ_θi ≡ 8.73 ρ_bi at ε = 0.1 ("8.73" is a unit
+ conversion, not printed in I19) — B5a.
+- RDK-NTM improved drift model: w_c ≈ 0.45 ρ_θi ≡ 1.46 ρ_bi (2.85 ρ_bi full
+ width); the 8.73→1.46 pair is authoritative in the NF 016020 abstract — B5b.
+- The improved-vs-original drift model is now concrete: the cos θ structure of
+ ∂B/∂ψ (D21 Eq. A2), proxied by L̂_B⁻¹ = 0 — a one-term toggle
+ (`MagneticDrift` variants in docs/03).
+- Exact Ω convention, ψ̃ = (w_ψ²/4)(q_s′/q_s), Δ_neo normalization
+ (Diss19 Eq. 4.12), bootstrap/polarization split, and the WCHH96 large-w
+ limits (Δ_bs ∝ 1/w, Δ_pol ∝ 1/w³) are transcribed into docs/01 with
+ equation/page cites.
+
+### 3. Frames and polarization physics grounded
+docs/01 §5 now carries the source-confirmed identities: ω − ω_E is
+frame-invariant; ω₀ = −ω_E; L_n⁻¹ shifts with frame; Δ_pol ∝ ω_E² with a
+**sign reversal at ω_E ≈ −0.89 ω_dia,e** (D23b Fig. 8) and discrete
+torque-balance roots (Diss19 Fig. 4.18). Consequence: ω_E is a scanned Level-0
+input parameter (roadmap O4 reworded; proposed decision D7), and single-ω_E
+polarization values are forbidden in publications (risk register). L23's
+unexplained stabilizing electron Δ_pol at ω_E = 0 is recorded as an open
+physics question Islands can settle (E4/E6; Paper I candidate result).
+
+### 4. Electron closure and collision operator now exact
+docs/01 §2.3–2.4: the WCHH96 flattened-electron closure with h(Ω), the coupled
+u_∥e(u_∥i) relation (k = −1.173, f_p = 1 − 1.46√ε), the momentum-conserving
+pitch-angle operator with the Chandrasekhar-form vs. V⁻³ energy-dependence
+sub-toggle (an inconsistency *within* the York lineage, now toggle study E3),
+and the analytic ⟨ν̂_ii⟩_v average. The `:kinetic` electron option is
+identified with the RDK-NTM treatment (full coefficient sets in Diss19
+Appendix D).
+
+### 5. Verification ladder rewritten with exact targets
+docs/05: B5 split into B5a/B5b/B5c (three-code triangle, proposed decision
+D8); B4/B6 get the D23b figure list and sign-reversal curve; B7 gets the
+prior-art agreement window and the "beat kokuchou's ν_★ floor" deliverable;
+B9 (new) places the La Haye experimental fits as context; C4 gets D23a's exact
+shaping numbers (triangularity 1.82→2.90 ρ_bi full width, ε ≈ 0.3 scaling
+crossover, EAST 91972); A7/A8 (new) add kokuchou's analytic unit-test set and
+the singular-block conditioning monitor. New reporting rule 5: thresholds
+always as half-widths with the unit stated. New standing triage category:
+"their published-equation error."
+
+### 6. Repo reality check (amended after review)
+On `develop`, `src/InnerLayer/SLAYER/Slayer.jl` is a placeholder — but the
+SLAYER implementation is in flight on **PR #238**
+(`feature/tearing-growthrates`): the `src/Tearing/` umbrella with SLAYER
+(Fitzpatrick Riccati Δ(Q)) and GGJ inner-layer models, the dispersion
+root-finding layer, and the outer-region Δ′ as a full 2m×2m matrix
+(`delta_prime_raw` + the new ForceFreeStates `Riccati.jl` solver).
+**Sequencing decision:** #238 lands before Islands M0; if Islands starts earlier,
+the Islands branch is cut from `feature/tearing-growthrates` rather than
+`develop`, so the SLAYER/Δ′ interfaces are in hand from the first commit.
+README, docs/00 (milestone sequencing + risk register), docs/03, docs/05 D1,
+and docs/06 §1 encode this; docs/06 also warns not to code against `develop`'s
+old `src/InnerLayer` layout since #238 moves GGJ under `src/Tearing/`.
+KineticForces (NTV, the Level-4 torque-balance counterpart) exists on
+`develop` already.
+
+### 7. Reference hygiene
+New `docs/08-reference-library.md`: per-PDF role map, abbreviations
+(PRL18/JPCS18/I19/Diss19/D21/D23a/D23b/L23/JOP18), and six pinned
+cross-source inconsistencies (normalization drift, helical-angle conventions,
+collision-frequency forms, a suspected ψ̃ typo, the DK-NTM run-collisionality
+discrepancy, half/full widths). **Dudkovskaia 2018 JOP reclassified**: it is
+about *phase-space* (bump-on-tail) islands, not magnetic islands —
+methodological antecedent and EP background only. Missing sources flagged for
+acquisition: WCHH96 (load-bearing), Park 2022 (SLAYER), La Haye fits, and the
+classical MRE-term papers.
+
+### 8. Tag semantics extended
+New `[CHECKED: source, Eq./p.]` state in the Islands module CLAUDE.md: transcribed from
+an in-repo PDF with exact cite and machine-checked, pending the human sign-off
+that rule 3 still requires. Applied throughout docs/01/04/05.
+
+## Items needing human ratification
+- Decision D7 (implement from re-derivation vs. L23-amended set; ω_E as
+ Level-0 scan parameter) and D8 (three-code benchmark triangle) in the
+ docs/00 decision log.
+- All [CHECKED] tags (one sign-off pass over docs/01 against the PDFs).
+- Open [VERIFY] items: the I19 ψ̃ possible typo; the DK-NTM run collisionality
+ (0.01 vs 10⁻³) before pinning B5a tolerances; Park 2022 Q-convention;
+ B3 curvature configuration; D5 w_d target formula.
diff --git a/docs/src/islands/figures/continuation_fold.png b/docs/src/islands/figures/continuation_fold.png
new file mode 100644
index 000000000..6431920b1
Binary files /dev/null and b/docs/src/islands/figures/continuation_fold.png differ
diff --git a/docs/src/islands/figures/grids_clustering.png b/docs/src/islands/figures/grids_clustering.png
new file mode 100644
index 000000000..06c1890f8
Binary files /dev/null and b/docs/src/islands/figures/grids_clustering.png differ
diff --git a/docs/src/islands/figures/hQ_profiles.png b/docs/src/islands/figures/hQ_profiles.png
new file mode 100644
index 000000000..1c13c1785
Binary files /dev/null and b/docs/src/islands/figures/hQ_profiles.png differ
diff --git a/docs/src/islands/figures/mms_convergence.png b/docs/src/islands/figures/mms_convergence.png
new file mode 100644
index 000000000..5b2a4dd67
Binary files /dev/null and b/docs/src/islands/figures/mms_convergence.png differ
diff --git a/docs/src/islands/figures/preconditioner_gmres.png b/docs/src/islands/figures/preconditioner_gmres.png
new file mode 100644
index 000000000..d885a9b62
Binary files /dev/null and b/docs/src/islands/figures/preconditioner_gmres.png differ
diff --git a/docs/src/islands/index.md b/docs/src/islands/index.md
new file mode 100644
index 000000000..bb3f44c87
--- /dev/null
+++ b/docs/src/islands/index.md
@@ -0,0 +1,129 @@
+# Islands — drift-kinetic island/layer solver
+
+> GPEC submodule (`src/Islands/`, `module Islands`). The name is a plain
+> description of the domain — the resonant magnetic island/layer region — per the
+> repo module-naming convention (simple, intuitive names, not standalone-code
+> acronyms; repo-root CLAUDE.md). Earlier drafts used the working acronym
+> "ISLET"; that has been retired.
+
+## One-paragraph pitch
+
+The Modified Rutherford Equation (MRE) is assembled, by decades-long practice, from
+regime-specific analytic terms (Rutherford resistive drive, Sauter/Hegna–Callen
+bootstrap, GGJ curvature, Wilson–Connor/Smolyakov polarization, Fitzpatrick's
+transport threshold w_d), each valid only in its asymptotic corner of parameter
+space. This mirrors the pre-SLAYER state of linear error-field penetration theory,
+where Fitzpatrick's and Cole & Fitzpatrick's regime-specific thresholds coexisted
+until Park's SLAYER (Phys. Plasmas 29, 2022) solved the underlying layer equations
+numerically for arbitrary parameters and recovered every analytic limit. Islands is
+the nonlinear analog: a steady-state, multi-species drift-kinetic solver for the
+resonant island/layer region that returns the growth moment Δ_cos(w, ω; p) and
+torque moment Δ_sin(w, ω; p) for arbitrary parameters, replacing the sum of
+regime-specific MRE terms with a single calculation — and, in its small-amplitude
+limit, reducing to the linear layer response so that error-field shielding,
+penetration, seeded-NTM onset, and island saturation become faces of one solution
+manifold.
+
+## Key deliverables (priority order)
+
+1. **Unification of small (SLAYER) and large (MRE) island response.** One inner-region
+ framework spanning shielded linear response → penetration bifurcation → saturated
+ island. The transition regime w ~ δ_layer has no kinetic treatment in the
+ literature; it is the flagship result.
+2. **Toggleable ordering stack.** Every approximation in the Imada 2019 /
+ Dudkovskaia 2021–2023 / Leigh 2023 (DK-NTM / RDK-NTM / kokuchou) lineage is a
+ runtime toggle, so their theory is a *benchmark configuration* of Islands, and
+ the impact of each ordering is measurable all the way up to the unreduced
+ problem.
+3. **Multi-species physics.** High-Z minority impurities (W: mixed-collisionality
+ bootstrap/friction physics, radiative island destabilization) and energetic
+ particles (alphas: finite-orbit-width nonlocality, slowing-down backgrounds,
+ precession resonance with island rotation).
+4. **Δ(w, ω; p) surfaces + emulator.** Precomputed response surfaces over
+ nondimensional parameter space for integrated modeling, the way SLAYER is used
+ for penetration thresholds.
+
+## Relationship to the existing toolchain
+
+Islands develops **inside the OpenFUSIONToolkit GPEC repository** as a
+subdirectory Julia package (see docs/06 §1). Status of the GPEC-side assets
+(checked 2026-07-07):
+
+- **Outer region + linear layer:** everything Islands consumes arrives with the
+ Tearing module work (PR #238, `feature/tearing-growthrates`, sequenced to
+ land before Islands starts): SLAYER Δ(Q) and GGJ inner-layer models
+ (`src/Tearing/InnerLayer/`), the dispersion/root-finding layer, and the
+ outer-region Δ′ as a full 2m×2m matrix (`delta_prime_raw` from
+ ForceFreeStates with the new Riccati ideal solver). Islands never recomputes
+ global ideal-MHD physics; the SLAYER Δ(Q) is a Level-3 *verification target*
+ called directly in CI (docs/05 D1). If Islands work begins before #238 merges,
+ branch from `feature/tearing-growthrates`, not `develop`.
+- **EP corrections to Δ'** (fast-ion pressure in the outer region) stay on the
+ GPEC side (`src/KineticForces`, the PENTRC/NTV machinery — also the natural
+ NTV restoring-torque source for Level 4 torque balance); Islands handles
+ resonant/orbit-width EP physics at the island.
+
+## Document map
+
+Design docs live in `docs/src/islands/design/`; module conventions in
+`src/Islands/CLAUDE.md`. The `docs/NN` shorthand used throughout means design
+doc `NN` (`docs/src/islands/design/NN-*.md`).
+
+| File | Contents |
+|---|---|
+| `src/Islands/CLAUDE.md` | Module conventions for Claude Code: layout map, style, testing gates, [VERIFY] policy, merge policy |
+| `design/00-roadmap.md` | Level 0–4 plan, milestones, risk register, decision log |
+| `design/01-physics-level0.md` | Level 0 equation set: coordinates, DKE, quasineutrality, moments, nondimensionalization |
+| `design/02-species-and-eps.md` | Species abstraction; tungsten (Level 1) and energetic particles (Level 2) physics specs |
+| `design/03-architecture.md` | In-repo module layout, operator stack, toggles, AD strategy, coupling interfaces |
+| `design/04-numerics.md` | Discretization, boundary layers, Newton–Krylov, continuation, performance model |
+| `design/05-verification.md` | The benchmark ladder: every analytic limit and published number Islands must recover, per level |
+| `design/06-autonomy-and-tooling.md` | GPEC-repo integration, autonomous Claude Code workflows, GPD, skills, subagents, setup checklist |
+| `design/07-documentation-and-papers.md` | Living documentation system (Physics Book, State Dashboard, figure pipeline) and the paper series; level gates are manuscripts |
+| `design/08-reference-library.md` | Map of the in-repo PDF sources (`docs/resources/Drift_Kinetic_Island_References/`), per-source load-bearing content, and known cross-source inconsistencies |
+
+## Reading order for a new contributor (human or Claude)
+
+`00-roadmap` → `01-physics-level0` → `03-architecture` → `05-verification`, then
+`02` and `04` as needed. Nothing in `src/Islands/` may contradict these documents;
+when it must, the document is amended *first* (doc-first workflow, see
+`src/Islands/CLAUDE.md`).
+
+## Canonical references
+
+The York drift-kinetic lineage is in-repo as PDFs
+(`docs/resources/Drift_Kinetic_Island_References/`) — see **docs/08** for the
+per-source map, abbreviations, and known cross-source inconsistencies:
+
+- K. Imada et al., PRL 121, 175001 (2018); JPCS 1125, 012013 (2018); **Nucl.
+ Fusion 59, 046016 (2019)** — DK-NTM: 4D nonlinear drift-kinetic island
+ theory. The 2019 paper is the complete reference; **its published equation
+ set carries known errata (see Leigh 2023 §2.6)**.
+- A. V. Dudkovskaia, PhD dissertation, York (2019) — full RDK derivation chain
+ and solver coefficient sets (Appendices C–E).
+- A. V. Dudkovskaia et al., PPCF 63, 054001 (2021); Nucl. Fusion 63, 016020
+ (2023); Nucl. Fusion 63, 126040 (2023) — RDK-NTM v.1–v.3: improved drift
+ model, shaping/finite-β, separatrix layer & polarization / ω-dependence.
+- S. Leigh, PhD thesis, York (Dec 2023) — `kokuchou`: amended DK-NTM equation
+ set, finite-ν★ threshold surface w_c(ρ̂_θi, ν_★), and the most complete
+ forensic record of the numerical failure modes (singular trapped-passing
+ matching, separatrix-layer resolution, Picard non-convergence, spurious
+ Neumann branches).
+- A. V. Dudkovskaia et al., JPCS 1125, 012009 (2018) — *phase-space* island
+ stability (bump-on-tail); methodological antecedent and EP background only,
+ not an NTM threshold source.
+
+Classical MRE-term and penetration literature (not yet in the PDF library):
+
+- R. Fitzpatrick, Nucl. Fusion 33, 1049 (1993); Phys. Plasmas 5, 3325 (1998) — linear penetration thresholds, torque balance.
+- A. Cole & R. Fitzpatrick, Phys. Plasmas 13, 032503 (2006) — drift-MHD regime-specific penetration thresholds.
+- J.-K. Park, Phys. Plasmas 29 (2022) — SLAYER: parametric layer responses across linear two-fluid drift-MHD regimes.
+- P. H. Rutherford, Phys. Fluids 16, 1903 (1973) — nonlinear island evolution.
+- O. Sauter et al., Phys. Plasmas 6, 2834 (1999) — bootstrap coefficients (arbitrary ν*).
+- A. H. Glasser, J. M. Greene, J. L. Johnson, Phys. Fluids 18, 875 (1975) — curvature (GGJ).
+- H. R. Wilson, J. W. Connor, R. J. Hastie & C. C. Hegna, Phys. Plasmas 3, 248 (1996) — the analytic electron closure and large-w limits (load-bearing for Level 0; acquire the PDF); F. L. Waelbroeck & R. Fitzpatrick, PRL 78, 1703 (1997); A. I. Smolyakov — polarization current (regime-dependent).
+- R. Fitzpatrick, Phys. Plasmas 2, 825 (1995) — transport threshold w_d.
+
+Equation transcriptions from the in-repo PDFs carry [CHECKED: source, Eq./p.]
+tags (AI-verified against the PDF, one human sign-off pending); everything
+else carries [VERIFY] until the source is in hand (see CLAUDE.md).
diff --git a/docs/src/islands/numerics.md b/docs/src/islands/numerics.md
new file mode 100644
index 000000000..31efcb230
--- /dev/null
+++ b/docs/src/islands/numerics.md
@@ -0,0 +1,362 @@
+# Islands — numerics as implemented (M1–M2)
+
+This chapter documents the Islands machinery **as it exists in the code today**
+— the discretization, operator stack, solver, and output-moment assembly landed
+by milestones M1 and M2, with the verification evidence behind each piece. It
+is the "Physics Book" companion to the aspirational [design documents](design/00-roadmap.md):
+the design docs say what Islands *will* compute; this page says what is
+*implemented and verified now*, equation by equation.
+
+!!! warning "Where the physics is (and isn't)"
+ Everything on this page is **structure and numerics**. Under the module's
+ `[VERIFY]` policy (`src/Islands/CLAUDE.md`), no physics coefficient, sign,
+ or normalization from the drift-kinetic literature has been assigned a
+ value anywhere in `src/`: every such quantity enters as a *supplied,
+ gated parameter* (many deliberately default to `NaN` so an un-cleared
+ convention poisons results rather than guessing). The human clearance
+ queue that un-gates the physics is `QUESTIONS.md` entries **Q2–Q4**; until
+ then the physics benchmarks (`benchmarks/islands/`) ship skipped by design.
+
+## 1. Phase space and discretization
+
+The Level-0 solve lives on the orbit-averaged phase space
+``(x, \xi;\, y, E, \sigma)`` — radial distance from the rational surface,
+helical angle, pitch ``y = \lambda B_{\max}``, energy, and the parallel-velocity
+sign ``\sigma = \pm 1`` (design `03 §1`). Implemented in `Islands.PhaseSpace`:
+
+**Helical angle ``\xi``** — Fourier pseudo-spectral on the periodic domain. The
+dense spectral derivative on an even number ``n`` of uniform nodes is
+
+```math
+(D_1)_{jk} \;=\; \frac{(-1)^{j-k}}{2}\,\cot\!\Big(\frac{(j-k)\,h}{2}\Big),
+\qquad j \neq k,\quad h = \tfrac{2\pi}{n},
+```
+
+exact for bandlimited data (verified to ``6\times10^{-15}`` in the tests).
+
+**Radial ``x`` and pitch ``y``** — high-order finite differences on
+layer-clustered grids. A uniform computational coordinate ``s \in [-1, 1]`` maps
+to the physical coordinate through a monotone ``\sinh`` stretching that packs
+nodes at the internal layers the drift-kinetic problem develops (`04 §1–2`):
+
+```math
+x(s) \;=\; x_c + L\,\frac{\sinh(\beta s)}{\sinh(\beta)}
+\qquad\Longrightarrow\qquad
+\frac{d}{dx} = \frac{1}{x'(s)}\frac{d}{ds},\quad
+\frac{d^2}{dx^2} = \frac{1}{x'(s)^2}\frac{d^2}{ds^2} - \frac{x''(s)}{x'(s)^3}\frac{d}{ds}.
+```
+
+The radial grid packs toward the rational surface ``x = 0``; the pitch grid
+toward the trapped–passing boundary ``y_c`` — the two layers whose widths scale
+as ``\nu^{1/2}`` and set the prior art's operating floor (`04 §2`).
+Derivative matrices use Fornberg weights on windows of ``\mathrm{order}+d``
+points for the ``d``-th derivative, so ``D_1`` **and** ``D_2`` hold the design
+order uniformly, including at boundary rows. Composite-Simpson weights on the
+same nodes (pushed through the map Jacobian) give quadrature at matching order.
+
+**Energy ``E``** — Gauss–Laguerre nodes and weights: the Level-0 Maxwellian
+weight ``\int_0^\infty f(E)\, e^{-E}\, dE = \sum_i w_i f(E_i)`` (a slowing-down
+background at Level 2 changes the map, not the machinery).
+
+
+
+*Implementing symbols:* `PhaseSpace.FourierGrid`, `PhaseSpace.MappedFDGrid`,
+`PhaseSpace.GaussGrid`, `PhaseSpace.IslandGrid`, `PhaseSpace.fd_weights`.
+
+## 2. The operator stack
+
+The unknowns are ``U = (g,\, \tilde\Phi)`` — the orbit-averaged distribution
+``g(x, \xi, y, E, \sigma)`` per species and the electrostatic potential
+``\tilde\Phi(x, \xi)`` — and the steady-state residual is assembled as a sum of
+independent operator applications (design `03 §2`; no term inspects which
+others are active, no regime branches anywhere):
+
+```math
+R_g(U) \;=\; \sum_{\text{terms } T} T[U],
+\qquad
+R_\Phi(U) \;=\; M[g] - \alpha\,\tilde\Phi + S_\Phi ,
+```
+
+with the Level-0 term structures (each coefficient below is **supplied data**,
+its physics value gated):
+
+| Term | Structure | Gated coefficient |
+|---|---|---|
+| `ParallelStreaming` | ``a_\xi\, \partial_\xi g + a_x\, \partial_x g`` | **cleared** — ``(\hat L_q^{-1}\hat w^2/4\hat\rho_{\theta i})\Theta\,\{\Omega,\cdot\}`` advection along island surfaces (§8) |
+| `MagneticDrift` | ``c_D\, \partial_\xi g`` (with the `:original`/`:improved` ``\hat L_B^{-1}`` toggle) | the precession frequency ``\hat\omega_D(y, E; \sigma)`` |
+| `ExBDrift` | ``c_E \left( \partial_\xi\tilde\Phi\, \partial_x g - \partial_x\tilde\Phi\, \partial_\xi g \right)`` — the ``(x,\xi)`` Poisson bracket, the one state-nonlinear Level-0 term | the ``E\times B`` coupling |
+| `PitchAngleDiffusion` | ``c\,(K g)`` along ``y`` (mimetic form, §3) | ``\hat\nu(E)`` and the pitch diffusivity profile |
+| `GradientDrive` | additive source | the ``(\mathbf v_E + \mathbf v_D + \mathbf v_{\tilde\psi})\cdot\nabla F_0`` drive |
+| `Quasineutrality` | ``M[g] - \alpha\tilde\Phi + S_\Phi`` | **cleared** — ``\alpha=(\tau+1)/\tau`` and the drive ``S_\Phi=\hat L_{n0}^{-1}(x-\hat h)`` (§8) |
+
+Every `apply!` kernel is allocation-free (a CI regression test holds this at
+**0 bytes**) and generic over the element type, so ForwardDiff dual numbers
+flow through the entire stack — that is what makes the solver's Jacobian exact
+(§5).
+
+Implemented by: `Operators.ParallelStreaming`, `Operators.MagneticDrift`,
+`Operators.ExBDrift`, `Operators.Collisions`, `Operators.PitchAngleDiffusion`,
+`Operators.GradientDrive`, `Operators.PerpTransport`, `Operators.RadiationSink`,
+`Operators.Quasineutrality`.
+
+The last two are Level-4 closure stubs, and `Collisions` is the non-mimetic
+collision slot superseded at Level 0 by `PitchAngleDiffusion` (§3); the stack is
+assembled by `Operators.residual!` over an `Operators.IslandStack`. The
+anchor-sync check `Verify.check_anchor_sync` holds this list in step with the
+`AbstractTerm` subtypes so a new operator without documentation, or a doc naming
+a deleted symbol, fails the check.
+
+## 3. The conservative collision structure
+
+Bootstrap physics is unforgiving about non-conservative collision operators
+(design `00`, risk register), so the pitch-angle operator is implemented in
+**mimetic divergence form**. With gradient matrix ``G`` (the ``y``-grid ``D_1``),
+quadrature weights ``w_q``, a supplied non-negative diffusivity profile ``P(y)``
+and measure ``w(y)``:
+
+```math
+K \;=\; -\,W_q^{-1}\, G^{\mathsf T}\, \mathrm{diag}(P \circ w_q)\, G,
+\qquad W_q = \mathrm{diag}(w \circ w_q),
+```
+
+which enjoys the two Level-0 conservation properties **exactly in floating
+point**, not just asymptotically:
+
+```math
+\mathbf 1^{\mathsf T} W_q K g
+ = -\,(G\mathbf 1)^{\mathsf T}\,\mathrm{diag}(P \circ w_q)\,(G g) = 0
+\quad\text{(particles; } G\mathbf 1 = 0\text{)},
+```
+```math
+g^{\mathsf T} W_q K g
+ = -\,(G g)^{\mathsf T}\,\mathrm{diag}(P \circ w_q)\,(G g) \;\le\; 0
+\quad\text{(entropy sign)}.
+```
+
+Verified to ``10^{-14}`` (ladder **A4**). A physically-profiled ``P`` vanishes
+at the pitch-domain endpoints, so zero-flux boundary behavior is built into the
+operator — no artificial ``y`` boundary conditions. (This mattered in practice:
+the generic ``a_y \partial_y^2`` form *without* boundary conditions is an
+unstable BVP discretization under refinement; the mimetic degenerate form is
+the correct structure.)
+
+*Implementing symbols:* `Operators.conservative_pitch_operator`,
+`Operators.PitchAngleDiffusion`.
+
+## 4. Boundary conditions
+
+Far-field rows at ``|x| = L_x`` are replaced by matching conditions
+``g - g_\infty`` and ``\tilde\Phi - \tilde\Phi_\infty`` (Dirichlet-type against
+supplied far-field states). **Never bare Neumann** ``\partial_x g = 0``: the
+prior art traced a spurious "winged" solution branch directly to Neumann
+non-uniqueness (`01 §3`). The physical far field — the no-island neoclassical
+solution — is gated physics, so the code takes it as supplied data; the tests
+use manufactured far fields. These conditions are also what make the
+first-order-in-``x`` advective solve well-posed.
+
+*Implementing symbols:* `Operators.FarFieldConditions`, `Operators.apply_farfield!`.
+
+## 5. The Newton–Krylov solve
+
+Decision D2: steady-state Newton–Krylov, never time-stepping and never the
+sources' nested Picard loops (which, per the prior-art forensics, *never met*
+their own convergence criterion in production). The pieces (design `04 §5`):
+
+**Exact matrix-free Jacobian.** The directional derivative comes from one dual-
+number sweep of the residual — no finite-difference Jacobian, no global sparse
+matrix:
+
+```math
+J(u)\,v \;=\; \left.\frac{d}{d\varepsilon}\right|_{\varepsilon=0} F(u + \varepsilon v)
+\quad\text{via ForwardDiff duals through the stack.}
+```
+
+**Inexact Newton with Eisenstat–Walker forcing.** Each step solves
+``J\,\delta u = -F`` by GMRES only to the tolerance the outer iteration needs,
+
+```math
+\eta_k = \gamma \left( \frac{\lVert F_k \rVert}{\lVert F_{k-1} \rVert} \right)^{2},
+```
+
+with a backtracking line search on ``\lVert F \rVert``. Convergence is declared
+on the norm **and** the pointwise maximum of the residual — the array-averaged
+residual famously hid locally divergent regions in the prior art.
+
+**Physics-block preconditioning with explicit regularization.** The stiff
+pitch-direction blocks are factored per pencil by SVD and truncated below
+``\epsilon\,\sigma_{\max}`` — the deliberate treatment of the intrinsically
+near-singular trapped–passing matching block (`04 §3`; the prior art measured
+``\mathrm{rcond} \sim 10^{-16}`` there and got machine-dependent *noise, not
+crashes*, under plain LU). On a collision-dominated test solve the block-Jacobi
+preconditioner cuts the work by an order of magnitude:
+
+
+
+A companion diagnostic tracks the smallest singular value of the ``y_c``-block
+of the (tiny-grid, debug) dense Jacobian — ladder **A8** — so a silent
+conditioning regression is *tested for*, not observed.
+
+*Implementing symbols:* `Solvers.newton_krylov`, `Solvers.JVPOperator`,
+`Solvers.YBlockJacobi`, `Solvers.dense_jacobian`, `Verify.yc_block_sigma_min`.
+
+## 6. Continuation and fold detection
+
+Δ-surface generation, Newton globalization, and (at Level 3) the penetration
+bifurcation all ride on pseudo-arclength continuation, so fold handling is in
+from day one. The corrector solves the extended system
+
+```math
+G(z) = \begin{pmatrix} F(u, p) \\ t \cdot (z - z_{\text{pred}}) \end{pmatrix} = 0,
+\qquad z = (u, p),
+```
+
+with a secant tangent ``t`` and fold detection via sign reversal of the
+tangent's parameter component ``t_p``:
+
+
+
+*Implementing symbol:* `Solvers.pseudo_arclength`.
+
+## 7. Island geometry, the electron-closure functions, and the Δ moments
+
+The island flux-surface label is the pinned module convention (half-width
+``w``):
+
+```math
+\Omega(x, \xi) = \frac{2x^2}{w^2} - \cos\xi,
+\qquad \Omega = -1 \text{ at the O-point},\quad \Omega = +1 \text{ at the separatrix},
+```
+
+with the flux-surface average
+``\langle f \rangle_\Omega = \oint f\,(\Omega + \cos\xi)^{-1/2} d\xi \,/\,
+\oint (\Omega + \cos\xi)^{-1/2} d\xi`` — a *diagnostic* only, never a solve
+coordinate (Decision D1). The flattened-electron closure geometry is
+implemented as structure with a supplied amplitude:
+
+```math
+Q(\Omega) = \frac{1}{2\pi} \oint \sqrt{\Omega + \cos\xi}\; d\xi,
+\qquad
+h(\Omega) = \Theta(\Omega - 1)\; C \int_1^{\Omega} \frac{d\Omega'}{Q(\Omega')},
+```
+
+so ``h`` is exactly flat inside the separatrix. Because ``h'(\Omega) = C/Q``,
+the chain rule gives the **coefficient-free consistency identity** (ladder
+**A7** — the unit target that historically caught inherited bugs in this
+lineage):
+
+```math
+\Big\langle \frac{\partial^2 h}{\partial x^2} \Big\rangle_{\!\Omega}
+ = \frac{4}{w^2}\left[ h''(\Omega)\, \frac{Q}{Q'} \; +\; h'(\Omega) \right]
+ \;\equiv\; 0 ,
+```
+
+verified to ``10^{-16}`` for arbitrary amplitude ``C``:
+
+
+
+The output moments are the two Ampère projections of the species-summed
+parallel current ``\bar J_\parallel = \sum_j Z_j \int W_j\, g_j`` (`01 §4`):
+
+```math
+\Delta_{\cos} = C_{\cos} \int dx \oint d\xi\; \bar J_\parallel \cos\xi,
+\qquad
+\Delta_{\sin} = C_{\sin} \int dx \oint d\xi\; \bar J_\parallel \sin\xi,
+```
+
+where the ``\xi``-projection is spectrally exact on the periodic grid and the
+prefactors ``C_{\cos}, C_{\sin}`` (physically ``\mp\mu_0 R / 2\tilde\psi``) are
+**required, gated arguments** — ``\tilde\psi`` carries an open `[VERIFY]` and
+the sin normalization is `[DERIVED]`-unpinned (QUESTIONS Q4). The parity
+structure ``\Delta_{\cos}`` even / ``\Delta_{\sin}`` odd under ``\xi``-reflection
+is verified exactly (ladder **A3**).
+
+*Implementing symbols:* `Moments.parallel_current!`, `Moments.delta_moments`,
+`Moments.omega_average`, `Fields.Q_omega`, `Fields.h_profile`,
+`Fields.flat_average_d2h_dx2`.
+
+## 8. The Level-0 configuration assembly (M2c)
+
+`Islands.Configure.configure_level0(grid, phys, species; gated)` assembles a
+Level-0 named configuration — the `IslandStack` + far-field conditions + `Δ`
+prefactors the solver consumes — by wiring the **human-cleared** coefficient
+builders (§7, the M2b derivation lane) onto the operator stack:
+
+- the magnetic drift `c_D[ix, iξ, iy, iE, iσ]` from `magnetic_drift_frequency`,
+ evaluated on the phase-space grid (``\hat v = \sqrt E``, the ``:original`` /
+ ``:improved`` toggle, and the forbidden pitch region ``y \ge (1+\varepsilon)/
+ (1-\varepsilon)`` zeroed since it carries no particles);
+- the **island-streaming** coefficients ``a_\xi``, ``a_x`` from
+ `streaming_coefficients` — the passing-particle (`Θ(y_c−y)`) advection along
+ island flux surfaces, ``(\hat L_q^{-1}\hat w^2/4\hat\rho_{\theta i})\Theta\,\{\Omega,\cdot\}``
+ (`parallel-streaming.md`; normalized to leave `c_D = ω̂_D` unchanged);
+- the pitch-collision diffusivity ``P`` and the energy-dependent deflection
+ coefficient from `pitch_diffusivity` / `deflection_frequency`, fed to the
+ mimetic `conservative_pitch_operator` (§3);
+- the ``\Delta_{\cos}`` / ``\Delta_{\sin}`` prefactors from
+ `delta_moment_prefactors` (§7);
+- the **quasineutrality field term** — ``\alpha=(\tau+1)/\tau`` from
+ `quasineutrality_coefficient` and the drive
+ ``S_\Phi=\hat L_{n0}^{-1}(x-\hat h(\Omega))`` from `quasineutrality_source`
+ (the ``\hat h`` amplitude ``w/2\sqrt2`` from `h_amplitude`, the profile from
+ `Fields.h_profile`), closing the Level-0 potential (§2; the drive whose absence
+ had left ``\Phi`` trivially zero).
+
+The gradient drive is cleared as I19 Formulation A — a **zero** interior
+`GradientDrive` source plus the neoclassical far field
+``g_{\rm far} = x\hat L_{n0}^{-1}[1+(E-\tfrac32)\eta_i]`` (`gradient_far_field`;
+`Φ̂_far = 0` at `ω_E = 0`). The families that remain **not yet cleared** — the
+``E\times B`` coupling, the collision magnitude ``\langle\hat\nu_{ii}\rangle_u``,
+and the orbit-averaged pitch measure — are **supplied** through
+`GatedLevel0Inputs`, never assigned a physics value here (QUESTIONS Q5). So the
+assembly is still a **scaffold** for those three *kinetic* pieces, even though
+the streaming, drift, gradient drive, far field, and field equation are now
+cleared: with `level0_placeholders`
+(documented non-physics values for the gated kinetic inputs) the assembled
+residual is well-formed, ``\Phi`` is genuinely driven, and Newton–Krylov
+converges. A physics threshold still awaits the remaining Q5 kinetic clearances.
+Implemented by: `Configure.configure_level0`.
+
+## 9. Verification evidence (the A-ladder, all green in CI)
+
+The manufactured-solution ladder verifies discretization order and the AD
+plumbing simultaneously — per operator, for the assembled residual, and through
+a full converged Newton solve forced by the analytic source:
+
+
+
+| Gate | Statement | Result |
+|---|---|---|
+| A1 | per-operator + assembled MMS at design order | 4th order (``\xi`` spectral to ``10^{-15}``) |
+| A1-solve | converged Newton solve recovers the manufactured state | observed order **3.98** |
+| A2 | AD Jacobian–vector product vs. central finite differences | agree to ``\sim 10^{-9}`` |
+| A3 | ``\Delta_{\cos}`` even / ``\Delta_{\sin}`` odd parity | exact |
+| A4 | particle conservation + entropy sign of the collision operator | exact (``10^{-14}`` / definite) |
+| A5 | zero-drive null: ``g \equiv 0 \Rightarrow R = 0`` | **exactly** machine zero |
+| A7 | ``\langle \partial^2 h / \partial x^2 \rangle_\Omega = 0`` | ``10^{-16}`` |
+| A8 | ``y_c``-block ``\sigma_{\min}`` monitor + singular detection | active |
+| M2c | L0 assembly builds; cleared ``c_D`` faithful; placeholder solve converges | exact / 5 Newton iters |
+| — | allocation regression on every hot kernel | 0 bytes |
+
+Everything above regenerates from one pinned script
+(`benchmarks/islands/figures/make_structural_figures.jl`) and runs in the test
+suite (`test/runtests_islands_{grids,operators,solve,configure}.jl`); the
+`islands_l0_structural` regression case tracks the headline numbers across
+commits. The always-current ladder status is the
+[State dashboard](state/STATE.md).
+
+## 10. What comes next
+
+The physics story — the drift-kinetic coefficients, the drift-model threshold
+*scalings and toggle differentials*, the ``\Delta_{\text{pol}}(\omega_E)``
+sign-reversal *behavior* — is written as the
+[Paper I figure contract](papers/paper-1/OUTLINE.md) and un-gates claim by claim
+as the derivation lane and human clearances (`QUESTIONS.md` Q2–Q5) are worked
+through — Q5 being the remaining Level-0 coefficient families the M2c assembly
+surfaced as still-gated. Following the SLAYER-validation precedent (Park 2022 / Burgess 2026),
+the physics gates are **tiered by reproducibility** (Decision D9, docs/05):
+scalings, regime trends, and internally-controlled differentials are the primary
+quantitative checks; absolute literature numbers (e.g. the "``8.73 \to 1.46\,
+\rho_{bi}``" drift-model shift) are reported only alongside an input manifest and
+sensitivity scan, because reproducing an absolute threshold requires every input
+of the source's exact scenario — which the lineage under-specifies. The
+[design documents](design/00-roadmap.md) hold the full eight-milestone program.
diff --git a/docs/src/islands/papers/paper-1/OUTLINE.md b/docs/src/islands/papers/paper-1/OUTLINE.md
new file mode 100644
index 000000000..a4e8cc0ed
--- /dev/null
+++ b/docs/src/islands/papers/paper-1/OUTLINE.md
@@ -0,0 +1,55 @@
+# Paper I — OUTLINE (the Level-0 figure contract)
+
+> Created at level *start* per design doc `07 §3`: claims → figures → ladder
+> IDs. This outline is the figure contract — agents implementing benchmarks
+> know which figures are paper figures from day one. Every claim must be backed
+> by a ladder ID; claims lacking one are flagged. Status reflects the
+> `[VERIFY]` gating of `docs/src/islands/QUESTIONS.md` (Q2–Q4) — **no
+> submission until every tag in the paper's equation set is cleared**.
+
+**Working title:** Formulation and verification of a generalized drift-kinetic
+solver for magnetic-island stability (Islands, Level 0).
+
+**Indicative venue:** Phys. Plasmas (methods paper). **Gate:** Level 0
+(design `00`), i.e. ladder A + B1/B2/B4/B5a–c green with convergence artifacts.
+
+## Claims and figures
+
+| # | Claim | Figure(s) | Ladder ID(s) | Status |
+|---|---|---|---|---|
+| C1 | The `(x, ξ; y, E, σ)` discretization converges at design order: spectral in `ξ`, 4th-order FD on layer-clustered `x`/`y` grids, per operator and for the assembled solve | F1: MMS convergence panels (per-operator + assembled residual + assembled solve error vs. resolution) | A1, A2 | **green** (M1/M2; CI artifacts) |
+| C2 | The steady-state Newton–Krylov solve is exact-Jacobian (AD), globally convergent from zero states, and its conditioning is monitored at the trapped–passing boundary (no silent-noise regime, contra L23 §4.2) | F2: Newton/GMRES convergence histories with and without the physics-block preconditioner; `σ_min(y_c)` track | A5, A8 (+ solver gates) | **green** (M2) |
+| C3 | The discretized collision operator conserves particles exactly and has definite entropy sign — bootstrap-relevant structure holds discretely, not just asymptotically | F3: conservation/entropy residuals vs. resolution and profile | A4 | **green** (L0 parts, M2) |
+| C4 | In the no-island limit the solver reproduces standard local neoclassics (bootstrap current vs. Sauter/NEO) — the strongest global check of the velocity-space discretization | F4: `J_bs` vs. `ν_★` against NEO/Sauter | B1 | **gated** — needs cleared L0 coefficient set (Q2, Q3) + external NEO runs |
+| C5 | **(T3, primary)** At large `w` the solver recovers the analytic MRE *scalings* `Δ_bs + Δ_cur ∝ 1/w` and `Δ_pol ∝ 1/w³`; **(T4)** the 1/w coefficient vs. WCHH96 Eq. 85 (frame-mapped) is audit-gated | F5: `Δ` channels vs. `w` with fitted exponents and analytic asymptotes | B2 | **gated** — Q2, Q3, Q4 (`ψ̃` [VERIFY]); WCHH96 acquired |
+| C6 | **(T2, robust headline)** The `:original → :improved` drift-model **toggle differential** — a ~×6 reduction in `w_c` in an otherwise identical configuration, measured within Islands (the reproducible form of the sources' `8.73 → 1.46 ρ_bi` story) — plus **(T3)** threshold *existence* at `w_c ~ O(ρ_θi)` and kokuchou's `dw_c/dν_★ > 0` trend. **(T4, audit-gated)** the absolute triangle values (`2.76`/`0.45 ρ_θi`, the `0.440…` fit surface) reported only with input manifests | F6: `w_c` vs. configuration with tier labels; F7: the E1 toggle *ratio* scan | B5a, B5b, B5c (E1) | **gated** — Q2 (D7/D8), Q3, Q4 |
+| C7 | **(T3, primary)** The polarization `Δ_pol ∝ ω_E²` away from zero with a sign reversal *existing* at an `ω_E` of order `−ω_dia,e`, reversal location insensitive to `w/ρ_θi`; single-`ω_E` `Δ` values are misleading (surfaces over `(w, ω_E)` are the deliverable). **(T4)** the reversal location (`≈ −0.89 ω_dia,e`) audit-gated | F8: `Δ_pol(ω_E)` reversal curve (morphology vs. D23b Fig. 8); F9: `Δ(w, ω_E)` surface | B4 | **gated** — Q2, Q3 (frame-convention signs) |
+| C8 (candidate headline) | **(T2, internal)** Resolution of L23's open question: the stabilizing *electron* `Δ_pol` at `ω_E = 0` — reproduced or refuted by the `ω_E` scan with kinetic vs. flattened electrons (E4), a channel-decomposition comparison we control end-to-end | F10: electron-channel `Δ_pol(ω_E)` decomposition | B4 + E4 | **gated** — Q2, Q3; kinetic-electron toggle is M2+/E4 work |
+| C9 (methods) | **Input-completeness audit** of the DK-NTM/RDK-NTM/kokuchou configurations: a per-source manifest of what the published NTM-threshold scenarios actually pin down — itself a reproducibility contribution that frames every T4 comparison and pre-empts benchmark-provenance questions | Table: input manifests per source (docs/05 "input-manifest" template) | — (methods) | **in progress** (M2b deliverable) |
+
+## Verification-artifact rules (docs/05 reporting)
+
+- Every figure names its configuration (docs/03 §2) and git SHA; no benchmark
+ "passes" on a single grid — convergence + tolerance archived with the result.
+- Threshold numbers are reported as **half-widths** with both `ρ_θi` and
+ `ρ_bi = ε^{1/2} ρ_θi` stated at the run's `ε` (docs/05 rule 5).
+- Disagreements with published targets are triaged per the standing rule
+ (docs/05: our bug / their approximation / their published-equation error /
+ transcription error / **under-specified source configuration**) with
+ `[VERIFY]` resolution logged first.
+- **Targets are tiered (Decision D9, docs/05).** The paper's quantitative
+ physics claims are T1 (exact math), T2 (internal differentials/cross-checks —
+ the sharpest), and T3 (scalings/trends/existence vs. literature). Absolute
+ literature numbers (T4) appear only with their input manifests and
+ sensitivity scans; where the source is under-specified they are reported as
+ order-of-magnitude + trend, not agreement claims.
+
+## Dependencies for un-gating (the human clearance queue)
+
+`QUESTIONS.md` **Q2** (ratify D7/D8 — done), **Q3** (clear the L0 `[CHECKED]`
+coefficient set via re-derivation), **Q4** (resolve the `ψ̃` and
+B5a-collisionality `[VERIFY]`s; sources acquired). C1–C3 are already green as
+CI artifacts. The physics claims un-gate as the M2b derivation lane and the
+input-completeness audit (C9) proceed — the T2/T3 scaling gates need only the
+cleared coefficients, while the T4 absolute comparisons additionally need the
+per-source input manifests.
diff --git a/docs/src/islands/state/STATE.md b/docs/src/islands/state/STATE.md
new file mode 100644
index 000000000..b85b32b2b
--- /dev/null
+++ b/docs/src/islands/state/STATE.md
@@ -0,0 +1,36 @@
+# Islands — State Dashboard
+
+!!! warning "Auto-generated — do not hand-edit"
+ Generated by `Islands.Verify.write_state_dashboard` (docs/07 §1.3).
+ Regenerate rather than edit; it refreshes from the ladder spec and,
+ as they land, archived benchmark artifacts.
+
+Snapshot: commit `03fee6bb` — 2026-07-11.
+
+The docs/05 verification ladder. The **A-ladder** (structural, pre-physics)
+is green via `test/runtests_islands_*.jl`. The **B/C physics ladder** is
+gated on the `QUESTIONS.md` clearances — no physics result is claimed until
+human sign-off (the `[VERIFY]` policy, module CLAUDE.md).
+
+| ID | Tier | Target | Status | Gate |
+|---|---|---|---|---|
+| A1 | structural | MMS convergence (ξ spectral; x, y order-4) | ✅ green | test suite |
+| A2 | structural | AD-vs-FD JVP agreement | ✅ green | test suite |
+| A3 | structural | Δ_cos even / Δ_sin odd under ξ-reflection | ✅ green | test suite |
+| A4 | structural | mimetic pitch operator: exact discrete conservation + entropy sign | ✅ green | test suite |
+| A5 | structural | zero-drive null: g≡0 ⇒ residual machine-zero | ✅ green | test suite |
+| A7 | T1 | coefficient-free closure identity ⟨∂²h/∂x²⟩_Ω = 0 | ✅ green | test suite |
+| A8 | structural | y_c matching-block conditioning monitor | ✅ green | test suite |
+| M2c | structural | L0 assembly builds + solves structurally (placeholders) | ✅ green | test suite |
+| B2 | T3 | large-w scalings Δ_bs+Δ_cur ∝ 1/w, Δ_pol ∝ 1/w³ | 🔒 gated | QUESTIONS Q5 |
+| B4 | T3 | Δ_pol ∝ ω_E² + sign-reversal existence | 🔒 gated | QUESTIONS Q3, Q5 |
+| B5a | T3 | threshold existence w_c ~ O(ρ_θi), :original | 🔒 gated | QUESTIONS Q5 |
+| B5b/E1 | T2 | :original→:improved w_c toggle differential (~×6) | 🔒 gated | QUESTIONS Q5 |
+| B5c | T3 | ν_★ trend dw_c/dν_★ > 0; w_c ∝ ρ̂_θi | 🔒 gated | QUESTIONS Q5 |
+| B7 | T2 | DK vs RDK cross-check mode | 🔒 gated | QUESTIONS Q5 |
+| C4 | T3 | finite-β/shaping triangularity trend + ε-crossover | ⏳ planned | Level 2 |
+
+Summary: **8 green** (structural), 6 gated (physics, awaiting clearance), 1 planned.
+
+Tiers (Decision D9, docs/05 "Target tiers"): T1 exact math · T2 internal
+differentials · T3 scalings/trends/existence · T4 absolute (audit-gated).
diff --git a/regression-harness/cases/islands_l0_structural.toml b/regression-harness/cases/islands_l0_structural.toml
new file mode 100644
index 000000000..091b49491
--- /dev/null
+++ b/regression-harness/cases/islands_l0_structural.toml
@@ -0,0 +1,60 @@
+# Regression case: Islands L0 solve machinery, structural (pre-physics) quantities.
+# A "computed" case (kind = "computed", no example_dir) tracking the M2 structural
+# gates: the solve-level MMS error and solver iteration counts (Newton-Krylov on the
+# manufactured advective stack at nx = 17), the coefficient-free flattened-electron
+# closure identity |_Omega| at Omega = 2, and the y_c-block smallest singular
+# value (the L23 SS4.2 silent-noise tripwire). The physics B-ladder stays skipped until
+# [VERIFY] clearance (docs/src/islands/QUESTIONS.md Q2-Q4); these are the numbers that
+# silently move if the discretization, solver, or quadratures drift.
+[case]
+name = "islands_l0_structural"
+description = "Islands L0 structural: solve-MMS error, solver iterations, A7 identity, y_c sigma_min"
+kind = "computed"
+
+[quantities.solve_mms_err]
+h5path = "islands/solve_mms_err"
+type = "real_scalar"
+extract = "value"
+label = "solve-MMS max error (nx=17)"
+noise_threshold = 1e-8
+order = 10
+
+[quantities.newton_iters]
+h5path = "islands/newton_iters"
+type = "real_scalar"
+extract = "value"
+label = "Newton iterations"
+noise_threshold = 0.0
+order = 11
+
+[quantities.gmres_iters]
+h5path = "islands/gmres_iters"
+type = "real_scalar"
+extract = "value"
+label = "GMRES iterations (total)"
+noise_threshold = 0.0
+order = 12
+
+[quantities.a7_identity]
+h5path = "islands/a7_identity"
+type = "real_scalar"
+extract = "value"
+label = "|_Omega| (A7)"
+noise_threshold = 1e-12
+order = 13
+
+[quantities.yc_sigma_min]
+h5path = "islands/yc_sigma_min"
+type = "real_scalar"
+extract = "value"
+label = "y_c block sigma_min (A8)"
+noise_threshold = 1e-10
+order = 14
+
+[quantities.runtime]
+h5path = ""
+type = "runtime"
+extract = "value"
+label = "Runtime (s)"
+noise_threshold = 0.0
+order = 90
diff --git a/regression-harness/src/runner.jl b/regression-harness/src/runner.jl
index 8705cb4d5..d6aa9b217 100644
--- a/regression-harness/src/runner.jl
+++ b/regression-harness/src/runner.jl
@@ -2,7 +2,9 @@
Runner: orchestrates checking out commits, running GPEC, and extracting results.
"""
-"""Read the GPEC-only runtime from the timing file written by the subprocess."""
+"""
+Read the GPEC-only runtime from the timing file written by the subprocess.
+"""
function _read_timing_file(path::String)::Float64
if isfile(path)
return parse(Float64, strip(read(path, String)))
@@ -29,7 +31,7 @@ function _materialize_rundir(example_path::String, overrides::Dict{String,Any})
for (dotted, val) in overrides
ks = split(dotted, ".")
d = cfg
- for k in ks[1:(end - 1)]
+ for k in ks[1:(end-1)]
d = get!(d, k, Dict{String,Any}())
end
d[ks[end]] = val
@@ -109,28 +111,60 @@ open(ARGS[2], "w") do f
end
"""
+# Islands L0 structural regression: the solve-level MMS (Newton-Krylov recovers
+# the manufactured state), the coefficient-free A7 closure identity, and the A8
+# y_c-block conditioning monitor. Tracks the structural (pre-physics) numbers
+# that would silently move if the discretization, solver, or quadratures drift;
+# the physics B-ladder stays skipped until [VERIFY] clearance (QUESTIONS Q2-Q4).
+const COMPUTED_ISLANDS_SCRIPT_TEMPLATE = """
+using Pkg
+%INSTANTIATE%
+using GeneralizedPerturbedEquilibrium
+using HDF5
+const Isl = GeneralizedPerturbedEquilibrium.Islands
+t_start = time()
+r = Isl.Verify.solve_mms(17)
+a7 = Isl.Fields.flat_average_d2h_dx2(2.0, 1.0)
+grid = Isl.PhaseSpace.IslandGrid(nx=7, nxi=8, ny=7, nE=2, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+setup = Isl.Verify.zero_drive_setup(grid)
+J = Isl.Solvers.dense_jacobian(setup.f, zeros(setup.N))
+mon = Isl.Verify.yc_block_sigma_min(J, grid)
+elapsed = time() - t_start
+h5open(ARGS[1], "w") do fid
+ fid["islands/solve_mms_err"] = r.err
+ fid["islands/newton_iters"] = Float64(r.iterations)
+ fid["islands/gmres_iters"] = Float64(r.gmres_iters)
+ fid["islands/a7_identity"] = abs(a7)
+ fid["islands/yc_sigma_min"] = mon.sigma_min
+end
+open(ARGS[2], "w") do f
+ println(f, elapsed)
+end
+"""
+
"""
Run GPEC for a single commit/ref and case. Dispatches to run_local for
the working tree or run_at_commit for a git ref.
"""
function run_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
- case_spec::CaseSpec, repo_root::String;
- force::Bool=false, verbose::Bool=false,
- no_instantiate::Bool=false)
+ case_spec::CaseSpec, repo_root::String;
+ force::Bool=false, verbose::Bool=false,
+ no_instantiate::Bool=false)
if case_spec.kind == "computed"
if commit_hash == LOCAL_REF
return run_computed_local(db, case_spec, repo_root;
- verbose=verbose, no_instantiate=no_instantiate)
+ verbose=verbose, no_instantiate=no_instantiate)
end
return run_computed_at_commit(db, commit_hash, ref_name, case_spec, repo_root;
- force=force, verbose=verbose, no_instantiate=no_instantiate)
+ force=force, verbose=verbose, no_instantiate=no_instantiate)
end
if commit_hash == LOCAL_REF
return run_local(db, case_spec, repo_root;
- force=force, verbose=verbose, no_instantiate=no_instantiate)
+ force=force, verbose=verbose, no_instantiate=no_instantiate)
end
return run_at_commit(db, commit_hash, ref_name, case_spec, repo_root;
- force=force, verbose=verbose, no_instantiate=no_instantiate)
+ force=force, verbose=verbose, no_instantiate=no_instantiate)
end
"""
@@ -141,6 +175,8 @@ function _computed_script_template(case_spec::CaseSpec)
return COMPUTED_GGJ_SCRIPT_TEMPLATE
elseif case_spec.name == "efit_fixedbdy_separatrix"
return COMPUTED_SEPARATRIX_SCRIPT_TEMPLATE
+ elseif case_spec.name == "islands_l0_structural"
+ return COMPUTED_ISLANDS_SCRIPT_TEMPLATE
end
error("No computed-script template registered for case '$(case_spec.name)'")
end
@@ -153,11 +189,11 @@ import GeneralizedPerturbedEquilibrium), reads the resulting tempfile h5 with
so callers can handle store_failed_run uniformly.
"""
function _execute_computed(case_spec::CaseSpec, project_root::String;
- verbose::Bool, no_instantiate::Bool,
- stderr_buf::IO)
+ verbose::Bool, no_instantiate::Bool,
+ stderr_buf::IO)
instantiate_line = no_instantiate ? "" : "Pkg.instantiate()"
script_content = replace(_computed_script_template(case_spec),
- "%INSTANTIATE%" => instantiate_line)
+ "%INSTANTIATE%" => instantiate_line)
tmpscript = tempname() * ".jl"
h5path = tempname() * ".h5"
timingfile = tempname() * ".timing"
@@ -166,8 +202,8 @@ function _execute_computed(case_spec::CaseSpec, project_root::String;
if verbose
run(pipeline(`julia --project=$project_root $tmpscript $h5path $timingfile`))
else
- run(pipeline(`julia --project=$project_root $tmpscript $h5path $timingfile`,
- stdout=devnull, stderr=stderr_buf))
+ run(pipeline(`julia --project=$project_root $tmpscript $h5path $timingfile`;
+ stdout=devnull, stderr=stderr_buf))
end
runtime_s = _read_timing_file(timingfile)
if !isfile(h5path)
@@ -186,18 +222,18 @@ end
Run a kind="computed" case against the working tree.
"""
function run_computed_local(db::SQLite.DB, case_spec::CaseSpec, repo_root::String;
- verbose::Bool=false, no_instantiate::Bool=false)
+ verbose::Bool=false, no_instantiate::Bool=false)
delete_cached(db, LOCAL_REF, case_spec.name)
date = Dates.format(Dates.now(), "yyyy-mm-ddTHH:MM:SS")
@info "Running: $(case_spec.name) @ local (working tree, computed)"
stderr_buf = IOBuffer()
try
extracted, runtime_s = _execute_computed(case_spec, repo_root;
- verbose=verbose,
- no_instantiate=no_instantiate,
- stderr_buf=stderr_buf)
+ verbose=verbose,
+ no_instantiate=no_instantiate,
+ stderr_buf=stderr_buf)
store_run(db, LOCAL_REF, "local", date, "working tree", case_spec.name,
- runtime_s, extracted)
+ runtime_s, extracted)
@info " Completed in $(round(runtime_s, digits=3))s — $(length(extracted)) quantities extracted"
catch e
err_msg = if e isa ProcessFailedException
@@ -212,7 +248,7 @@ function run_computed_local(db::SQLite.DB, case_spec::CaseSpec, repo_root::Strin
err_msg_short = length(err_msg) > 2000 ? "..." * last(err_msg, 2000) : err_msg
@warn "Run failed (local computed): $(first(err_msg_short, 200))"
store_failed_run(db, LOCAL_REF, "local", date, "working tree", case_spec.name,
- err_msg_short)
+ err_msg_short)
end
end
@@ -220,9 +256,9 @@ end
Run a kind="computed" case at a specific git commit via worktree.
"""
function run_computed_at_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
- case_spec::CaseSpec, repo_root::String;
- force::Bool=false, verbose::Bool=false,
- no_instantiate::Bool=false)
+ case_spec::CaseSpec, repo_root::String;
+ force::Bool=false, verbose::Bool=false,
+ no_instantiate::Bool=false)
if !force && is_cached(db, commit_hash, case_spec.name)
info = get_run_info(db, commit_hash, case_spec.name)
if info !== nothing
@@ -243,11 +279,11 @@ function run_computed_at_commit(db::SQLite.DB, commit_hash::String, ref_name::St
try
worktree_path = create_worktree(commit_hash, repo_root)
extracted, runtime_s = _execute_computed(case_spec, worktree_path;
- verbose=verbose,
- no_instantiate=no_instantiate,
- stderr_buf=stderr_buf)
+ verbose=verbose,
+ no_instantiate=no_instantiate,
+ stderr_buf=stderr_buf)
store_run(db, commit_hash, commit_info.short, commit_info.date,
- commit_info.msg, case_spec.name, runtime_s, extracted)
+ commit_info.msg, case_spec.name, runtime_s, extracted)
@info " Completed in $(round(runtime_s, digits=3))s — $(length(extracted)) quantities extracted"
catch e
err_msg = if e isa ProcessFailedException
@@ -262,7 +298,7 @@ function run_computed_at_commit(db::SQLite.DB, commit_hash::String, ref_name::St
err_msg_short = length(err_msg) > 2000 ? "..." * last(err_msg, 2000) : err_msg
@warn "Run failed (computed) for $(commit_info.short): $(first(err_msg_short, 200))"
store_failed_run(db, commit_hash, commit_info.short, commit_info.date,
- commit_info.msg, case_spec.name, err_msg_short)
+ commit_info.msg, case_spec.name, err_msg_short)
finally
if worktree_path !== nothing
remove_worktree(worktree_path, repo_root)
@@ -275,8 +311,8 @@ Run GPEC in the current working tree (uncommitted changes included).
Always re-runs (local results are never cached since the working tree is mutable).
"""
function run_local(db::SQLite.DB, case_spec::CaseSpec, repo_root::String;
- force::Bool=false, verbose::Bool=false,
- no_instantiate::Bool=false)
+ force::Bool=false, verbose::Bool=false,
+ no_instantiate::Bool=false)
# Always delete previous local results and re-run
delete_cached(db, LOCAL_REF, case_spec.name)
@@ -287,7 +323,7 @@ function run_local(db::SQLite.DB, case_spec::CaseSpec, repo_root::String;
if !isdir(example_path)
@warn "Example directory not found: $(case_spec.example_dir)"
store_failed_run(db, LOCAL_REF, "local", date, "working tree", case_spec.name,
- "Example directory not found: $(case_spec.example_dir)")
+ "Example directory not found: $(case_spec.example_dir)")
return
end
@@ -309,8 +345,8 @@ function run_local(db::SQLite.DB, case_spec::CaseSpec, repo_root::String;
if verbose
run(pipeline(`julia --project=$repo_root $tmpscript $rundir $timingfile`))
else
- run(pipeline(`julia --project=$repo_root $tmpscript $rundir $timingfile`,
- stdout=devnull, stderr=stderr_buf))
+ run(pipeline(`julia --project=$repo_root $tmpscript $rundir $timingfile`;
+ stdout=devnull, stderr=stderr_buf))
end
runtime_s = _read_timing_file(timingfile)
@@ -318,13 +354,13 @@ function run_local(db::SQLite.DB, case_spec::CaseSpec, repo_root::String;
if !isfile(h5path)
@warn "gpec.h5 not produced"
store_failed_run(db, LOCAL_REF, "local", date, "working tree", case_spec.name,
- "gpec.h5 not produced after successful run")
+ "gpec.h5 not produced after successful run")
return
end
extracted = extract_quantities(h5path, case_spec.quantities, runtime_s)
store_run(db, LOCAL_REF, "local", date, "working tree", case_spec.name,
- runtime_s, extracted)
+ runtime_s, extracted)
@info " Completed in $(round(runtime_s, digits=1))s — $(length(extracted)) quantities extracted"
@@ -341,7 +377,7 @@ function run_local(db::SQLite.DB, case_spec::CaseSpec, repo_root::String;
err_msg_short = length(err_msg) > 2000 ? "..." * last(err_msg, 2000) : err_msg
@warn "Run failed (local): $(first(err_msg_short, 200))"
store_failed_run(db, LOCAL_REF, "local", date, "working tree", case_spec.name,
- err_msg_short)
+ err_msg_short)
finally
if tmpscript !== nothing
rm(tmpscript; force=true)
@@ -360,9 +396,9 @@ Run GPEC for a specific git commit via worktree. Stores results in the database.
Skips if already cached (unless force=true).
"""
function run_at_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
- case_spec::CaseSpec, repo_root::String;
- force::Bool=false, verbose::Bool=false,
- no_instantiate::Bool=false)
+ case_spec::CaseSpec, repo_root::String;
+ force::Bool=false, verbose::Bool=false,
+ no_instantiate::Bool=false)
# Check cache
if !force && is_cached(db, commit_hash, case_spec.name)
info = get_run_info(db, commit_hash, case_spec.name)
@@ -398,8 +434,8 @@ function run_at_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
if !isdir(example_path)
@warn "Example directory not found at commit $(commit_info.short): $(case_spec.example_dir)"
store_failed_run(db, commit_hash, commit_info.short, commit_info.date,
- commit_info.msg, case_spec.name,
- "Example directory not found: $(case_spec.example_dir)")
+ commit_info.msg, case_spec.name,
+ "Example directory not found: $(case_spec.example_dir)")
return
end
@@ -418,8 +454,8 @@ function run_at_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
if verbose
run(pipeline(`julia --project=$project_root $tmpscript $rundir $timingfile`))
else
- run(pipeline(`julia --project=$project_root $tmpscript $rundir $timingfile`,
- stdout=devnull, stderr=stderr_buf))
+ run(pipeline(`julia --project=$project_root $tmpscript $rundir $timingfile`;
+ stdout=devnull, stderr=stderr_buf))
end
runtime_s = _read_timing_file(timingfile)
@@ -428,8 +464,8 @@ function run_at_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
if !isfile(h5path)
@warn "gpec.h5 not produced at $(commit_info.short)"
store_failed_run(db, commit_hash, commit_info.short, commit_info.date,
- commit_info.msg, case_spec.name,
- "gpec.h5 not produced after successful run")
+ commit_info.msg, case_spec.name,
+ "gpec.h5 not produced after successful run")
return
end
@@ -438,7 +474,7 @@ function run_at_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
# Store in database
store_run(db, commit_hash, commit_info.short, commit_info.date,
- commit_info.msg, case_spec.name, runtime_s, extracted)
+ commit_info.msg, case_spec.name, runtime_s, extracted)
@info " Completed in $(round(runtime_s, digits=1))s — $(length(extracted)) quantities extracted"
@@ -456,7 +492,7 @@ function run_at_commit(db::SQLite.DB, commit_hash::String, ref_name::String,
err_msg_short = length(err_msg) > 2000 ? "..." * last(err_msg, 2000) : err_msg
@warn "Run failed for $(commit_info.short): $(first(err_msg_short, 200))"
store_failed_run(db, commit_hash, commit_info.short, commit_info.date,
- commit_info.msg, case_spec.name, err_msg_short)
+ commit_info.msg, case_spec.name, err_msg_short)
finally
# Clean up
if tmpscript !== nothing
diff --git a/src/GeneralizedPerturbedEquilibrium.jl b/src/GeneralizedPerturbedEquilibrium.jl
index e7ecef8a7..26750e78f 100755
--- a/src/GeneralizedPerturbedEquilibrium.jl
+++ b/src/GeneralizedPerturbedEquilibrium.jl
@@ -48,6 +48,10 @@ include("Analysis/Analysis.jl")
import .Analysis as Analysis
export Analysis
+include("Islands/Islands.jl")
+import .Islands as Islands
+export Islands
+
include("Rerun.jl")
# Import ForceFreeStates types and functions needed for main
diff --git a/src/Islands/CLAUDE.md b/src/Islands/CLAUDE.md
new file mode 100644
index 000000000..61ce2cc75
--- /dev/null
+++ b/src/Islands/CLAUDE.md
@@ -0,0 +1,156 @@
+# CLAUDE.md — Islands module conventions
+
+Islands is a steady-state, multi-species drift-kinetic solver for the resonant
+island/layer region in tokamaks, generalizing the Modified Rutherford Equation
+the way SLAYER generalized linear layer theory. It is a GPEC submodule
+(`src/Islands/`, `module Islands`), not a standalone package.
+
+**Where things live** (in-repo layout, reconciled from the original standalone
+design bundle):
+
+- Module source: `src/Islands/` (this file governs work here).
+- Design docs (normative, aspirational): `docs/src/islands/design/00-roadmap.md`
+ … `08-reference-library.md`. Throughout the docs and code, the shorthand
+ `docs/NN §X` (e.g. `docs/03 §2`) means design doc `NN` — read it as
+ `docs/src/islands/design/NN-*.md`.
+- Physics Book (as-implemented equations, rendered by Documenter):
+ `docs/src/islands/` chapters. Derivations: `docs/src/islands/derivations/`.
+ Papers: `docs/src/islands/papers/`. State dashboard + gallery:
+ `docs/src/islands/state/`. Notes: `docs/src/islands/notes/`.
+- Session memory / blocker queue: `docs/src/islands/LOG.md`,
+ `docs/src/islands/QUESTIONS.md`.
+- Tests: `test/runtests_islands_.jl`, included from `test/runtests.jl`.
+- Benchmarks + figures: `benchmarks/islands/`, `benchmarks/islands/figures/`.
+- Regression cases: integrated with the rest under `regression-harness/`.
+
+Read `docs/src/islands/index.md`, then `docs/src/islands/design/00-roadmap.md`.
+The design docs are **normative**: code must not contradict them; when physics
+or design must change, amend the doc in the same PR (doc-first workflow) and
+append to the Decision Log in `docs/src/islands/design/00-roadmap.md`.
+
+## The [VERIFY] policy (most important rule)
+
+Equations and numeric targets transcribed from literature carry `[VERIFY: source]`
+tags in docs and in code comments. Rules:
+
+1. Never implement physics against a [VERIFY]-tagged expression as if it were
+ confirmed. Implement the *structure*, parameterize the uncertain coefficient,
+ and add a failing/skipped benchmark referencing the tag.
+2. Never silently "fix" a coefficient to make a benchmark pass. Flag the
+ discrepancy for human review with the source citation.
+3. Only a human clears a [VERIFY] tag, after checking the source paper. Record
+ the clearance (paper, equation number) in the doc.
+4. If you (Claude) derive an expression yourself, mark it `[DERIVED: date]` with
+ the derivation in `docs/src/islands/derivations/` — never present a
+ derivation as a literature transcription or vice versa.
+5. `[CHECKED: source, Eq./p.]` is the intermediate state: the expression was
+ transcribed from a PDF in the in-repo reference library (docs/08) with an
+ exact equation/page cite and machine-checked against it, but has not yet
+ received the human sign-off of rule 3. [CHECKED] expressions may guide
+ design but are implemented under the same rule-1 discipline as [VERIFY].
+6. **The policy is not paranoia — it is calibrated to this literature.** Leigh
+ 2023 §2.6 documents concrete coefficient/sign errors in the *published*
+ Imada 2019 equation set (docs/01 header). Published equations in this
+ lineage are re-derived before implementation, full stop.
+
+## Physics conventions (pinned; changing any of these is a Decision Log entry)
+
+- Island width `w` = **half**-width; Ω = 2x²/w² − cos ξ; O-point Ω = −1,
+ separatrix Ω = +1. (Matches every source in the York lineage — [CHECKED:
+ I19 Eq. 7; Diss19 Eq. 2.7; L23 Eq. 2.1.8]. Thresholds are always reported
+ as half-widths with the gyroradius unit stated; docs/05 reporting rule 5.)
+- Primary field representation: A_∥(x, ξ) on the grid. Island coordinates Ω are
+ diagnostics only (Decision D1). Any PR introducing Ω as a solve coordinate
+ outside the RDK cross-check mode is rejected.
+- All frame/frequency conversions live in `src/Islands/frames/`. No other part
+ of the module may contain an ω sign convention. The polarization-current sign
+ disputes in the literature are largely frame disputes; we will not reproduce
+ them internally.
+- Normalizations per `docs/01-physics-level0.md §5`. SI only at I/O boundaries.
+- Species lists are first-class everywhere; no function may assume a single ion
+ species (Decision D3). Trace species go through the linear post-pass
+ (`docs/02 §1.2`), with trace-criteria checks that warn, never silently degrade.
+
+## Code conventions
+
+- Julia. Style: 4-space indent, explicit imports, no `using X` wildcard in
+ `src/Islands/`. Public API documented with docstrings; physics functions cite
+ the doc section they implement (`# docs/01 §2.1`).
+- Operator stack rules (`docs/03 §2`): terms are independent (no term inspects
+ other terms), generic over `eltype` (AD compatibility is tested), and
+ allocation-free in `apply!` hot paths (allocation regression test in CI).
+- No regime-specific branches in physics code. If you find yourself writing
+ `if collisional ... else banana ...` inside an operator, stop: that is the
+ disease this project treats. Regime logic is allowed only in
+ `src/Islands/verify/` (choosing analytic comparison targets) and in
+ preconditioners (approximations there are legitimate).
+- Threading: per-thread preallocated caches; no shared mutable state in kernels.
+
+## Testing gates
+
+- `test/runtests_islands_*.jl` (included from `test/runtests.jl`): fast unit +
+ symmetry + conservation + MMS-at-low-resolution + AD checks. Must pass on
+ every commit.
+- `benchmarks/islands/`: the docs/05 ladder. CI runs the fast subset; the full
+ ladder runs before any tag/paper. A PR that changes physics must state which
+ ladder IDs it affects and show them green (or explicitly re-baselined with
+ justification).
+- New operators/terms ship with: MMS test, at least one analytic-limit hook in
+ `src/Islands/verify/`, and an AD-compatibility test.
+
+## Merge conflict policy
+
+Synthesize both branch sides rather than choosing one; preserve current-branch
+naming conventions; flag ambiguous cases for human review rather than guessing.
+Conflicts touching the design docs or physics conventions always go to human
+review.
+
+## Documentation policy (docs/07)
+
+Documentation is a build artifact; stale docs are broken builds.
+
+- Any PR changing physics behavior updates the corresponding Physics Book
+ section (`docs/src/islands/`) **in the same PR**, and regenerates affected
+ verification figures if outputs change. Otherwise the PR description carries
+ `docs-not-needed:` with justification.
+- Bidirectional anchors are mandatory: every operator cites its Physics Book
+ section (`# physics: #`); every Physics Book equation block
+ names its implementing symbol. The anchor-sync CI check must stay green.
+- The Physics Book documents equations **as implemented** (code normalization,
+ code sign conventions). Aspirational physics belongs in the design docs
+ (`docs/src/islands/design/`, docs 00–02).
+- Never hand-edit `docs/src/islands/state/STATE.md` or gallery pages — they
+ regenerate from benchmark artifacts.
+- Paper figures are generated only by pinned scripts in
+ `benchmarks/islands/figures/` reading archived benchmark data; a figure that
+ can't be regenerated by `make figures` is a release-blocking bug.
+- Each level begins with the paper OUTLINE.md (claims → figures → ladder IDs);
+ implementing agents treat it as the figure contract.
+
+## Autonomous-session protocol (docs/06)
+
+- Definition of done for any milestone run = its docs/05 ladder IDs green with
+ convergence artifacts + CI passing + PR opened. Never weaken a tolerance or
+ re-baseline a target to reach done — that is a blocker.
+- When blocked on anything CLAUDE.md forbids guessing ([VERIFY] clearances,
+ coefficients, signs, conventions, doc contradictions): append an entry to
+ `docs/src/islands/QUESTIONS.md` (context, question, options, recommendation,
+ gated work) and continue with the next unblocked task. Never stall waiting for
+ a human; never resolve silently.
+- Read `docs/src/islands/LOG.md` and `docs/src/islands/QUESTIONS.md` at session
+ start; append a LOG.md entry (what moved / blocked / next) before session end.
+ End every session with the branch pushed.
+- This file governs work inside `src/Islands/` (code) and `docs/src/islands/`
+ (docs); the repo-root CLAUDE.md governs GPEC-wide conventions. On conflict the
+ repo-root file wins; flag genuine contradictions via QUESTIONS.md.
+- GPD (`/gpd:*` commands, if installed) is for derivation/verification/
+ literature tasks feeding `docs/src/islands/derivations/` and [VERIFY]
+ proposals — not for implementation work, which follows this file and
+ docs/03–05.
+
+## What to do when uncertain
+
+Prefer: (a) parameterize and flag, (b) add a skipped test documenting the
+uncertainty, (c) ask — in that order. Do not guess coefficients, sign
+conventions, or normalizations; in this project those are the entire failure
+mode of the field we're trying to fix.
diff --git a/src/Islands/Islands.jl b/src/Islands/Islands.jl
new file mode 100644
index 000000000..993ce8a07
--- /dev/null
+++ b/src/Islands/Islands.jl
@@ -0,0 +1,48 @@
+"""
+ Islands
+
+Steady-state, multi-species drift-kinetic solver for the resonant island/layer
+region in tokamaks. Generalizes the Modified Rutherford Equation the way SLAYER
+generalized linear layer theory: it returns the growth moment `Δ_cos(w, ω; p)`
+and torque moment `Δ_sin(w, ω; p)` for arbitrary parameters, and in its
+small-amplitude limit reduces to the linear layer response.
+
+Design docs live in `docs/src/islands/` (module conventions in
+`src/Islands/CLAUDE.md`, design docs in `docs/src/islands/design/`). Physics
+equations transcribed from the York drift-kinetic lineage carry `[VERIFY]` /
+`[CHECKED]` tags until human-cleared — see the module CLAUDE.md.
+
+Status: skeleton. The operator stack, phase-space grids, field/moment assembly,
+solvers, and verification harness (design doc `03-architecture.md §1`) land as
+milestone M1 proceeds.
+"""
+module Islands
+
+# Submodule layout follows docs/src/islands/design/03-architecture.md §1.
+# M1 landed the discretization + operator stack + MMS/AD harness; M2 lands the
+# L0 solve machinery (species, frames, solve, fields, moments) as gated
+# structure. Remaining design dirs (geometry/, closures/, io/) arrive with the
+# milestones that need them (L2/L4/M2-io).
+include("phasespace/PhaseSpace.jl") # grids (x, ξ, λ→y, E, σ), layer-clustered maps
+include("species/Species.jl") # Species, backgrounds, roles (D3)
+include("frames/Frames.jl") # THE frequency/frame conversion module (gated forms)
+include("operators/Operators.jl") # the AbstractTerm stack + residual assembly
+include("fields/Fields.jl") # quasineutrality closure structure, h(Ω)/Q(Ω)
+include("moments/Moments.jl") # J̄_∥, Δ_cos/Δ_sin projections, ⟨·⟩_Ω diagnostics
+include("coefficients/Coefficients.jl") # human-cleared L0 physics coefficient builders (M2b, D7)
+include("solvers/Solvers.jl") # Newton–Krylov, preconditioner, continuation
+include("configure/Configure.jl") # L0 named-config assembly (cleared coeffs wired, gated pieces supplied)
+include("verify/Verify.jl") # MMS + AD-vs-FD JVP harness, y_c monitor
+
+import .PhaseSpace
+import .SpeciesLists
+import .Frames
+import .Operators
+import .Fields
+import .Moments
+import .Coefficients
+import .Solvers
+import .Configure
+import .Verify
+
+end # module Islands
diff --git a/src/Islands/coefficients/Coefficients.jl b/src/Islands/coefficients/Coefficients.jl
new file mode 100644
index 000000000..a2a361c8f
--- /dev/null
+++ b/src/Islands/coefficients/Coefficients.jl
@@ -0,0 +1,250 @@
+"""
+ Coefficients
+
+Home for the **human-cleared** Level-0 physics coefficient builders (the M2b
+derivation-lane fill-ins). Every function here computes a coefficient whose form
+was independently re-derived (Decision D7) and signed off by a human, with the
+clearance recorded in `docs/01` and the derivation in
+`docs/src/islands/derivations/`. Uncleared coefficients stay gated in their home
+modules (`Frames`, `Fields`, `Operators`, `Moments`); nothing is promoted here
+without that paper trail.
+
+Cleared so far:
+
+ - [`magnetic_drift_frequency`](@ref) — the orbit-averaged drift frequency
+ ``\\hat\\omega_D`` and the `:original`/`:improved` ``\\hat L_B^{-1}`` toggle
+ (sign-off 2026-07-11; derivation `omega-D-drift-frequency.md`, docs/01 §2.1).
+ - [`pitch_diffusivity`](@ref), [`deflection_frequency`](@ref) — the Lorentz
+ collision operator's self-adjoint diffusivity ``P(\\lambda)=\\lambda\\sqrt{1-\\lambda B}``
+ and the velocity dependence ``\\nu_{jj}(\\hat v)=\\tilde\\nu[\\phi-G]/\\hat v^3``
+ (sign-off 2026-07-11; derivation `collision-operator.md`, docs/01 §2.3). The
+ ``\\langle\\hat\\nu_{ii}\\rangle_u`` momentum-restoring constant is a deferred
+ sub-item and stays gated.
+"""
+module Coefficients
+
+import QuadGK
+
+export magnetic_drift_frequency, orbit_average_drift_brackets
+export pitch_diffusivity, deflection_frequency
+export h_amplitude, passing_fraction
+export quasineutrality_coefficient
+export delta_moment_prefactors
+
+# Model circular equilibrium field modulation (docs/01 §1, I19 p. 6):
+# b(θ) = B/B_max = (1 − ε cos θ)/(1 + ε); b ∈ [b_min, 1], b_min = b(0), b(π)=1.
+@inline _b(θ, ε) = (1 - ε * cos(θ)) / (1 + ε)
+
+"""
+ orbit_average_drift_brackets(; y, epsilon, rtol=1e-8)
+
+The two poloidal-orbit integrals that appear in ``\\hat\\omega_D`` (docs/01 §2.1;
+derivation `omega-D-drift-frequency.md` §5), in I19's ``\\langle\\cdot\\rangle_\\theta = \\tfrac{1}{2\\pi}\\oint`` form:
+
+```math
+A(y) = \\Big\\langle \\frac{\\sqrt{1-yb}}{b} \\Big\\rangle_\\theta,
+\\qquad
+G(y) = \\Big\\langle \\frac{2-yb}{b\\sqrt{1-yb}} \\Big\\rangle_\\theta ,
+```
+
+with `b(θ) = (1−ε cos θ)/(1+ε)`. Returns `(A, G)`.
+
+**Passing** particles (`y < y_c = 1`): the full poloidal circuit, ``\\tfrac1{2\\pi} \\int_0^{2\\pi}``. **Trapped** particles (`1 < y < (1+ε)/(1−ε)`): the bounce
+integral ``\\tfrac1{2\\pi}\\sum_\\sigma\\int_{-\\theta_b}^{\\theta_b}`` between the
+turning points ``\\cos\\theta_b = [1-(1+ε)/y]/ε``. `G`'s integrand has an
+integrable ``1/\\sqrt{1-yb}`` singularity at the turning points, handled by the
+adaptive quadrature. The signed-off derivation covers the passing drift-island
+mechanism; the trapped brackets follow I19's stated ``\\langle\\cdot\\rangle_\\theta``.
+"""
+function orbit_average_drift_brackets(; y::Real, epsilon::Real, rtol::Real=1e-8)
+ ε = float(epsilon)
+ 0 < ε < 1 || throw(ArgumentError("epsilon must be in (0, 1) (got $epsilon)"))
+ # y = 0 (deeply-passing endpoint) is physical: A=⟨1/b⟩, G=⟨2/b⟩ are finite; the
+ # guard admits the closed pitch domain [0, 1/b_min) the grid samples.
+ y >= 0 || throw(ArgumentError("y must be nonnegative"))
+ y_forbidden = (1 + ε) / (1 - ε) # 1/b_min: no particle beyond this
+ y < y_forbidden || throw(ArgumentError("y = $y exceeds 1/b_min = $y_forbidden (forbidden region)"))
+
+ fA(θ) = sqrt(max(1 - y * _b(θ, ε), 0.0)) / _b(θ, ε)
+ fG(θ) = (2 - y * _b(θ, ε)) / (_b(θ, ε) * sqrt(max(1 - y * _b(θ, ε), 0.0)))
+
+ if y < 1 # passing: full circuit
+ A, _ = QuadGK.quadgk(fA, 0.0, 2π; rtol=rtol)
+ G, _ = QuadGK.quadgk(fG, 0.0, 2π; rtol=rtol)
+ return (A / (2π), G / (2π))
+ else # trapped: bounce integral between turning points
+ cosθb = (1 - (1 + ε) / y) / ε
+ θb = acos(clamp(cosθb, -1.0, 1.0))
+ # (1/2π) Σ_σ ∫_{-θb}^{θb} = (1/π) ∫_{-θb}^{θb} for σ-even integrands; split at 0
+ A, _ = QuadGK.quadgk(fA, -θb, 0.0, θb; rtol=rtol)
+ G, _ = QuadGK.quadgk(fG, -θb, 0.0, θb; rtol=rtol)
+ return (A / π, G / π)
+ end
+end
+
+"""
+ magnetic_drift_frequency(; y, v_hat, sigma, epsilon, inv_Lq, inv_LB, variant=:original, rtol=1e-8)
+
+The cleared orbit-averaged magnetic drift frequency (docs/01 §2.1; derivation
+`omega-D-drift-frequency.md`, sign-off 2026-07-11):
+
+```math
+\\hat\\omega_D = \\frac{\\sigma\\hat v}{1+\\varepsilon}
+ \\Big[\\; \\hat L_q^{-1}\\,A(y) \\;-\\; \\tfrac12\\,\\hat L_B^{-1}\\,G(y) \\;\\Big],
+```
+
+with `A(y)`, `G(y)` the orbit brackets of
+[`orbit_average_drift_brackets`](@ref). The **drift-model toggle** is `variant`:
+
+ - `:original` — finite `inv_LB` (the I19/DK-NTM ∇B term is retained);
+ - `:improved` — `inv_LB` is forced to `0` (the D21/RDK-NTM proxy: ∂B/∂ψ ∝ cos θ
+ orbit-averages to O(ε); derivation §6). This is the ~×6 threshold-width
+ toggle (measured as the internal `:original`/`:improved` ratio — the D9 T2
+ gate; the absolute 8.73→1.46 ρ_bi pair is T4/audit-gated).
+
+`inv_Lq = L̂_q⁻¹`, `inv_LB = L̂_B⁻¹`, `epsilon = ε`, `v_hat = v/v_thi`,
+`sigma = ±1`, `y = λ B_max`.
+"""
+function magnetic_drift_frequency(; y::Real, v_hat::Real, sigma::Real, epsilon::Real,
+ inv_Lq::Real, inv_LB::Real, variant::Symbol=:original, rtol::Real=1e-8)
+ variant in (:original, :improved) || throw(ArgumentError("variant must be :original or :improved (got $variant)"))
+ LB = variant === :improved ? zero(float(inv_LB)) : float(inv_LB)
+ A, G = orbit_average_drift_brackets(; y=y, epsilon=epsilon, rtol=rtol)
+ return (sigma * v_hat / (1 + epsilon)) * (inv_Lq * A - 0.5 * LB * G)
+end
+
+# ---------------------------------------------------------------------------
+# Collision operator (cleared 2026-07-11; derivation collision-operator.md)
+# ---------------------------------------------------------------------------
+"""
+ pitch_diffusivity(λ, B)
+
+The Lorentz pitch-angle operator's self-adjoint **diffusivity**
+``P(\\lambda) = \\lambda\\sqrt{1-\\lambda B} \\ge 0`` (docs/01 §2.3; derivation
+`collision-operator.md` §2), for the operator ``w^{-1}\\partial_\\lambda(P \\partial_\\lambda)`` with measure ``w = B/\\sqrt{1-\\lambda B}``. `P` vanishes at
+`λ=0` and the trapped–passing edge `λ=1/B` (zero-flux endpoints), so it feeds
+`conservative_pitch_operator` directly and preserves the A4 conservation gate.
+"""
+function pitch_diffusivity(λ::Real, B::Real)
+ B > 0 || throw(ArgumentError("B must be positive"))
+ (0 <= λ <= 1 / B) || throw(ArgumentError("λ must be in [0, 1/B]"))
+ return λ * sqrt(max(1 - λ * B, 0.0))
+end
+
+# error function φ(X) = (2/√π)∫₀ˣ e^{−t²}dt and its derivative φ'(X) = (2/√π)e^{−X²}
+_phi(X; rtol=1e-10) = (2 / sqrt(π)) * first(QuadGK.quadgk(t -> exp(-t^2), 0.0, X; rtol=rtol))
+_dphi(X) = (2 / sqrt(π)) * exp(-X^2)
+# Chandrasekhar function G(X) = [φ − Xφ']/(2X²)
+_chandrasekhar_G(X; rtol=1e-10) = (_phi(X; rtol=rtol) - X * _dphi(X)) / (2 * X^2)
+
+"""
+ deflection_frequency(v_hat; nu_tilde=1.0, model=:chandrasekhar, rtol=1e-10)
+
+The velocity dependence of the deflection frequency (docs/01 §2.3; derivation
+`collision-operator.md` §3):
+
+```math
+\\nu_{jj}(\\hat v) = \\tilde\\nu\\,\\frac{\\phi(\\hat v) - G(\\hat v)}{\\hat v^3}
+\\quad(\\texttt{:chandrasekhar}),
+\\qquad
+\\nu_{jj}(\\hat v) = \\tilde\\nu\\,\\hat v^{-3}\\quad(\\texttt{:vcubed}),
+```
+
+with ``\\phi`` the error function and ``G`` the Chandrasekhar function. The
+`:chandrasekhar` form ``\\to \\tfrac{4\\tilde\\nu}{3\\sqrt\\pi}\\hat v^{-2}`` at low
+speed (the derived linear ``\\phi-G`` limit) and ``\\to \\tilde\\nu\\hat v^{-3}`` at
+high speed; the `:vcubed` form is the reduced Diss19/D21 model. The energy-
+dependence sub-toggle (ladder E3).
+"""
+function deflection_frequency(v_hat::Real; nu_tilde::Real=1.0, model::Symbol=:chandrasekhar, rtol::Real=1e-10)
+ v_hat > 0 || throw(ArgumentError("v_hat must be positive"))
+ if model === :vcubed
+ return nu_tilde / v_hat^3
+ elseif model === :chandrasekhar
+ return nu_tilde * (_phi(v_hat; rtol=rtol) - _chandrasekhar_G(v_hat; rtol=rtol)) / v_hat^3
+ else
+ throw(ArgumentError("model must be :chandrasekhar or :vcubed (got $model)"))
+ end
+end
+
+# ---------------------------------------------------------------------------
+# Flattened-electron closure (cleared 2026-07-11; derivation electron-closure.md)
+# ---------------------------------------------------------------------------
+"""
+ h_amplitude(w_psi)
+
+The flattened-electron profile amplitude ``C = w_\\psi/(2\\sqrt2)`` (docs/01 §2.4;
+derivation `electron-closure.md` §3), i.e. the prefactor of
+``h(\\Omega)=\\Theta(\\Omega-1)\\,C\\int_1^\\Omega d\\Omega'/Q(\\Omega')``. Derived
+from the flattening constraint ``\\langle\\partial^2 h/\\partial x^2\\rangle_\\Omega=0``
+(``\\Rightarrow h'=C/Q``) plus far-field matching ``h\\to x``. Feeds
+`Fields.h_profile`'s `prefactor` (and `Fields.ElectronClosure.h_prefactor`); the
+closure constants ``k`` and ``f_p`` remain gated.
+"""
+h_amplitude(w_psi::Real) = w_psi / (2 * sqrt(2))
+
+"""
+ passing_fraction(epsilon)
+
+The flattened-electron closure **passing (circulating) particle fraction**
+``f_p = 1 - f_t \\simeq 1 - 1.4624\\,\\sqrt\\varepsilon`` (docs/01 §2.4; derivation
+`passing-fraction.md`, human sign-off 2026-07-11). The leading coefficient
+`1.4624` is the ``\\varepsilon\\to0`` limit of the effective trapped-fraction
+integral (Lin-Liu–Miller / Wesson) derived and numerically confirmed in the
+derivation; it matches the sources' quoted ``1.46`` (I19 Eq. 22) to three
+significant figures. Clears `Fields.ElectronClosure.f_p`; the Hirshman–Sigmar
+`k` constant of the same closure remains gated (QUESTIONS Q3/Q5).
+"""
+function passing_fraction(epsilon::Real)
+ epsilon >= 0 || throw(ArgumentError("epsilon must be nonnegative"))
+ return 1 - 1.4624 * sqrt(epsilon)
+end
+
+# ---------------------------------------------------------------------------
+# Quasineutrality closure (cleared 2026-07-11; derivation quasineutrality-closure.md)
+# ---------------------------------------------------------------------------
+"""
+ quasineutrality_coefficient(tau)
+
+The Level-0 quasineutrality closure coefficient ``\\tau/(\\tau+1)`` (docs/01 §3;
+derivation `quasineutrality-closure.md`), where ``\\tau=T_e/T_i``:
+
+```math
+\\hat\\Phi = \\frac{\\tau}{\\tau+1}\\Big[\\frac{\\delta\\bar n_i}{n_0}
+ + \\hat L_{n0}^{-1}\\,(x-\\hat h)\\Big].
+```
+
+The ``\\tau/(\\tau+1)`` factor is the sum of the ion and electron adiabatic
+shielding responses (``\\to 1/2`` at ``\\tau=1``, the sources' ``T_e=T_i``). The
+raw ion-density moment ``\\delta\\bar n_i=M[g_i]`` (`velocity_moment!`) enters
+directly — no ``\\delta n_i`` normalization convention — so this coefficient plus
+the ``\\hat L_{n0}^{-1}(x-\\hat h)`` drive populate `Operators.Quasineutrality`.
+"""
+function quasineutrality_coefficient(tau::Real)
+ tau > 0 || throw(ArgumentError("tau = T_e/T_i must be positive"))
+ return tau / (tau + 1)
+end
+
+# ---------------------------------------------------------------------------
+# Δ-moment prefactors (cleared 2026-07-11; derivation delta-moment-prefactors.md)
+# ---------------------------------------------------------------------------
+"""
+ delta_moment_prefactors(; mu0_R, w_psi, dq_dpsi, q_s)
+
+The ``\\Delta_{\\cos}``/``\\Delta_{\\sin}`` output-moment prefactors (docs/01 §4;
+derivation `delta-moment-prefactors.md`): returns
+`(cos=-\\mu_0 R/(2\\tilde\\psi), sin=+\\mu_0 R/(2\\tilde\\psi))` for
+`Moments.delta_moments`, with ``\\tilde\\psi`` the cleared island flux
+amplitude `Moments.island_flux_amplitude`. ``\\Delta_{\\cos}=\\Delta_{\\rm neo}``
+matches Diss19 Eq. 4.12 (stationarity ``\\Delta'+\\Delta_{\\rm neo}=0``); the sin
+normalization is the symmetric `[DERIVED]` pin so that
+``\\Delta_{\\cos}+i\\Delta_{\\sin}`` maps onto the linear-layer ``\\Delta(Q)``.
+"""
+function delta_moment_prefactors(; mu0_R::Real, w_psi::Real, dq_dpsi::Real, q_s::Real)
+ ψ̃ = (w_psi^2 / 4) * (dq_dpsi / q_s) # = island_flux_amplitude (docs/01 §1)
+ ψ̃ != 0 || throw(ArgumentError("ψ̃ must be nonzero (check w_psi, dq_dpsi)"))
+ pref = mu0_R / (2 * ψ̃)
+ return (cos=-pref, sin=+pref)
+end
+
+end # module Coefficients
diff --git a/src/Islands/configure/Configure.jl b/src/Islands/configure/Configure.jl
new file mode 100644
index 000000000..e11efaa31
--- /dev/null
+++ b/src/Islands/configure/Configure.jl
@@ -0,0 +1,464 @@
+"""
+ Configure
+
+Level-0 **named-configuration assembly** (design `03 §2`): turns a physics
+parameter set + a species list into a runnable `Operators.IslandStack`, its
+far-field boundary conditions, and the `Δ`-moment prefactors — the object the
+solver consumes.
+
+**What this module does and does not do (the honest state, M2c).** The M2b
+derivation lane cleared six Level-0 coefficient families. Only some of those map
+onto operator-stack coefficients *cleanly*; the assembly wires exactly those and
+**gates the rest**:
+
+ - **Cleared, wired here** (populated by `Coefficients.*`, never literals):
+ the orbit-averaged magnetic drift `c_D` ([`drift_coefficient_table`], from
+ `Coefficients.magnetic_drift_frequency`); the **island-streaming**
+ coefficients `a_xi`/`a_x` ([`streaming_coefficients`], the `{Ω, ·}`
+ flux-surface advection, `01 §2`); the pitch-collision
+ *shapes* (`Coefficients.pitch_diffusivity` →
+ `Operators.conservative_pitch_operator`, and
+ `Coefficients.deflection_frequency` → the energy-dependent collision
+ coefficient); the **quasineutrality field term** — `α = (τ+1)/τ` from
+ `Coefficients.quasineutrality_coefficient` and the `L̂_{n0}⁻¹(x − ĥ)` drive
+ ([`quasineutrality_source`], from the cleared `ĥ` profile), closing the
+ Level-0 potential (`01 §3`); the **gradient drive** — zero interior source
+ plus the neoclassical far field `g_far = x L̂_{n0}⁻¹[1+(E−3/2)η_i]`
+ ([`gradient_far_field`], I19 Formulation A, `01 §2`); and the `Δ`-moment
+ prefactors (`Coefficients.delta_moment_prefactors`).
+ - **Not yet a cleared coefficient family → supplied, gated** (QUESTIONS Q5):
+ the `E×B` coupling `c_E`, the orbit-averaged pitch measure/field `B_profile`,
+ and the collision magnitude `nu_tilde` (carries the deferred `⟨ν̂_ii⟩_u`).
+ These enter through [`GatedLevel0Inputs`]; nothing here assigns a physics
+ value to them.
+
+The gated pieces are the subject of a future derivation lane (QUESTIONS Q5): the
+assembly *surfaces* exactly what Level-0 physics is still uncleared rather than
+papering over it with guesses. [`level0_placeholders`](@ref) supplies documented
+**non-physics** values for the gated inputs so the assembled residual/solve can
+be exercised *structurally* (that the stack is well-formed and solvable) — never
+for a physics result.
+"""
+module Configure
+
+using LinearAlgebra
+import ..PhaseSpace: IslandGrid, nnodes, MappedFDGrid
+import ..Operators: IslandStack, FarFieldConditions,
+ ParallelStreaming, MagneticDrift, ExBDrift, PitchAngleDiffusion,
+ GradientDrive, Quasineutrality, conservative_pitch_operator
+import ..Coefficients:
+ magnetic_drift_frequency, orbit_average_drift_brackets,
+ pitch_diffusivity, deflection_frequency, delta_moment_prefactors,
+ h_amplitude, quasineutrality_coefficient
+import ..Fields: h_profile
+import ..SpeciesLists: Species, Maxwellian, Bulk, validate_species
+
+export Level0Physics, GatedLevel0Inputs, configure_level0, level0_placeholders
+export drift_coefficient_table, collision_coefficient, pitch_diffusivity_profile
+export quasineutrality_source, streaming_coefficients, gradient_far_field
+
+# ---------------------------------------------------------------------------
+# Physics parameter carrier (the cleared inputs)
+# ---------------------------------------------------------------------------
+"""
+ Level0Physics(; epsilon, inv_Lq, inv_LB, q_s, dq_dpsi, w_psi, mu0_R,
+ tau=1.0, variant=:original, collision_model=:chandrasekhar)
+
+The Level-0 physics parameters that feed the **cleared** coefficient builders
+(`01 §1`–`§4`). These are ordinary scenario inputs, not gated coefficients — the
+gated (uncleared) pieces live in [`GatedLevel0Inputs`].
+
+## Fields
+
+ - `epsilon` — inverse aspect ratio `ε = r_s/R₀`.
+ - `inv_Lq` — `L̂_q⁻¹ = (ψ_s/q_s) dq/dψ` at `r_s` (drift, `01 §2.1`).
+ - `inv_LB` — `L̂_B⁻¹` (∇B drift term; forced to `0` when
+ `variant = :improved`, `01 §2.1`).
+ - `q_s`, `dq_dpsi` — safety factor and its `ψ`-derivative at `r_s`
+ (`ψ̃` and `Δ`-prefactor, `01 §1`, `§4`).
+ - `w_psi` — island **half**-width (the `w` in `Ω = 2x²/w² − cosξ`
+ and the `ĥ` amplitude `w/2√2`, `01 §1`, `§2.4`).
+ - `mu0_R` — `μ₀R` geometric factor of the `Δ`-moment prefactor
+ (`01 §4`).
+ - `inv_Ln0` — inverse density scale length `L̂_{n0}⁻¹` at `r_s`, the
+ quasineutrality drive amplitude (`01 §3`).
+ - `rho_hat_theta_i` — normalized ion poloidal gyroradius `ρ̂_θi` (`~ ŵ` by the
+ O2 ordering); sets the island-streaming scale (`01 §2`, `parallel-streaming.md`).
+ - `eta_i` — `η_i = L_n/L_{T_i} = (T_i'/T_i)/(n'/n)`, the
+ temperature-gradient ratio in the gradient-drive far field (`01 §2`,
+ `gradient-drive.md`); `0` = flat temperature.
+ - `tau` — `T_e/T_i` (quasineutrality closure, `01 §3`).
+ - `variant` — `:original`/`:improved` drift-model toggle (`01 §2.1`).
+ - `collision_model` — `:chandrasekhar`/`:vcubed` deflection-frequency energy
+ dependence (`01 §2.3`).
+"""
+Base.@kwdef struct Level0Physics
+ epsilon::Float64
+ inv_Lq::Float64
+ inv_LB::Float64
+ q_s::Float64
+ dq_dpsi::Float64
+ w_psi::Float64
+ mu0_R::Float64
+ inv_Ln0::Float64
+ rho_hat_theta_i::Float64
+ eta_i::Float64 = 0.0
+ tau::Float64 = 1.0
+ variant::Symbol = :original
+ collision_model::Symbol = :chandrasekhar
+end
+
+# ---------------------------------------------------------------------------
+# Gated inputs carrier (the uncleared pieces — supplied, never guessed here)
+# ---------------------------------------------------------------------------
+"""
+ GatedLevel0Inputs(; c_E, nu_tilde, B_profile)
+
+The Level-0 operator coefficients that are **not yet a cleared coefficient
+family** and are therefore *supplied* to [`configure_level0`], not derived from
+`Coefficients.*` (QUESTIONS Q5). Nothing in this module assigns them a physics
+value; a caller either supplies cleared physics (once available) or the
+documented non-physics placeholders of [`level0_placeholders`](@ref).
+
+## Fields
+
+ - `c_E` — `E×B` coupling scalar (`Operators.ExBDrift`; the frame/
+ normalization is uncleared, QUESTIONS Q3/Q5).
+ - `nu_tilde` — collision magnitude scaling the cleared `ν_{jj}(v̂)` shape;
+ carries the deferred `⟨ν̂_ii⟩_u`/`ν_★` normalization (QUESTIONS Q3).
+ - `B_profile` — orbit-averaged `|B|/B_max` on the `y`-grid, feeding the
+ cleared `pitch_diffusivity` shape and the collision measure (the orbit
+ average is gated, QUESTIONS Q5).
+"""
+Base.@kwdef struct GatedLevel0Inputs{S,V}
+ c_E::S
+ nu_tilde::S
+ B_profile::V
+end
+
+# ---------------------------------------------------------------------------
+# Cleared coefficient wiring
+# ---------------------------------------------------------------------------
+"""
+ drift_coefficient_table(grid, phys) -> Array{Float64,5}
+
+Build the orbit-averaged magnetic-drift coefficient `c_D[ix, iξ, iy, iE, iσ]` for
+`Operators.MagneticDrift` by evaluating the **cleared**
+`Coefficients.magnetic_drift_frequency` on the phase-space grid
+(`01 §2.1`). `ω̂_D` depends on `(y, E, σ)` only (through `v̂ = √E` and the orbit
+brackets `A(y)`, `G(y)`), so the `(y, E, σ)` table is broadcast over `(x, ξ)`.
+The orbit brackets are computed once per `y` (they depend only on `y`, `ε`) and
+reused across `E`, `σ` — the drift is linear in `σ v̂`. Uses `phys.variant` for
+the `:original`/`:improved` `L̂_B⁻¹` toggle. Grid nodes in the forbidden pitch
+region `y ≥ 1/b_min = (1+ε)/(1−ε)` carry no particles, so `c_D ≡ 0` there (a
+grid may extend past the physical pitch edge; the deeply-trapped limit is
+unambiguous).
+"""
+# Cleared orbit-average with a graceful miss at the near-separatrix y_c layer:
+# returns (A, G), or `nothing` if the bounce-average quadrature cannot converge
+# (the gated y_c layer, handled by the caller). Only the singular layer misses;
+# every well-defined node returns the exact cleared value.
+function _try_drift_brackets(y::Real, ε::Real)
+ try
+ return orbit_average_drift_brackets(; y=y, epsilon=ε)
+ catch err
+ err isa Union{ErrorException,DomainError} || rethrow(err)
+ return nothing
+ end
+end
+
+function drift_coefficient_table(grid::IslandGrid, phys::Level0Physics)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ ε = phys.epsilon
+ LB = phys.variant === :improved ? 0.0 : phys.inv_LB
+ y_forbidden = (1 + ε) / (1 - ε) # 1/b_min: no particle beyond (01 §2.1)
+ cD = Array{Float64}(undef, nx, nξ, ny, nE, nσ)
+ @inbounds for iy in 1:ny
+ y = grid.y.nodes[iy]
+ if y >= y_forbidden # forbidden region carries no particles ⇒ ω̂_D ≡ 0
+ @views cD[:, :, iy, :, :] .= 0.0
+ continue
+ end
+ # The trapped-passing boundary y = y_c = 1 is the near-separatrix pitch
+ # layer: θ_b → π and the bounce average develops the integrable
+ # 1/√(1−yb) turning-point singularity, so the cleared orbit-average
+ # quadrature cannot reach tolerance there. That layer's drift is part of
+ # the gated y_c-matching treatment (04 §3, ladder A8, QUESTIONS Q5); the
+ # node gets a documented gated placeholder (0), not a guessed value.
+ brackets = _try_drift_brackets(y, ε)
+ if brackets === nothing
+ @views cD[:, :, iy, :, :] .= 0.0
+ continue
+ end
+ A, G = brackets
+ bracket = phys.inv_Lq * A - 0.5 * LB * G # the [·] of ω̂_D (01 §2.1)
+ for iσ in 1:nσ
+ σ = grid.σ[iσ]
+ for iE in 1:nE
+ v̂ = sqrt(grid.E.nodes[iE]) # E = v̂² (Maxwellian energy)
+ val = (σ * v̂ / (1 + ε)) * bracket
+ for iξ in 1:nξ, ix in 1:nx
+ cD[ix, iξ, iy, iE, iσ] = val
+ end
+ end
+ end
+ end
+ return cD
+end
+
+"""
+ pitch_diffusivity_profile(grid, B_profile) -> (P, wmeas)
+
+Evaluate the **cleared** Lorentz pitch diffusivity
+`Coefficients.pitch_diffusivity` `P(λ) = λ√(1−λB)` and the collision
+measure `w = B/√(1−λB)` on the `y`-grid (`01 §2.3`), with `λ = y` in the
+`B_max = 1` normalization and `B = B_profile[iy]` the (gated) orbit-averaged
+field. Returns `(P, wmeas)` for `Operators.conservative_pitch_operator`, which
+builds the mimetic operator preserving the A4 conservation gate for any `P ≥ 0`.
+The caller must supply a `B_profile` keeping `0 ≤ y·B ≤ 1` on the grid (the
+turning-point structure is part of the gated orbit average, QUESTIONS Q5).
+"""
+function pitch_diffusivity_profile(grid::IslandGrid, B_profile::AbstractVector)
+ ny = grid.y.n
+ length(B_profile) == ny || throw(ArgumentError("B_profile must have length ny = $ny"))
+ P = Vector{Float64}(undef, ny)
+ wmeas = Vector{Float64}(undef, ny)
+ @inbounds for iy in 1:ny
+ λ = grid.y.nodes[iy]
+ B = B_profile[iy]
+ arg = 1 - λ * B
+ (0 <= λ * B <= 1) || throw(ArgumentError("λB = $(λ * B) at iy=$iy outside [0,1]; supply a valid orbit-averaged B_profile"))
+ P[iy] = pitch_diffusivity(λ, B) # cleared: λ√(1−λB)
+ # collision measure w = B/√(1−λB); regularize the zero-flux edge (√arg→0)
+ wmeas[iy] = B / sqrt(max(arg, 1e-12))
+ end
+ return P, wmeas
+end
+
+"""
+ collision_coefficient(grid, phys, nu_tilde) -> Array{Float64,4}
+
+Build the energy-dependent collision coefficient `c[ix, iξ, iE, iσ]` for
+`Operators.PitchAngleDiffusion` from the **cleared**
+`Coefficients.deflection_frequency` `ν_{jj}(v̂)` (`01 §2.3`), scaled by the
+gated magnitude `nu_tilde` (QUESTIONS Q3, carries `⟨ν̂_ii⟩_u`/`ν_★`). It is
+**`y`-independent by construction** (dimensions `(x, ξ, E, σ)`), as
+`PitchAngleDiffusion` requires so the mimetic `K`'s exact conservation is
+preserved; the physical velocity dependence lives entirely in the `E`-axis via
+`v̂ = √E`. Uses `phys.collision_model` for the `:chandrasekhar`/`:vcubed` toggle.
+"""
+function collision_coefficient(grid::IslandGrid, phys::Level0Physics, nu_tilde::Real)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ c = Array{Float64}(undef, nx, nξ, nE, nσ)
+ @inbounds for iE in 1:nE
+ v̂ = sqrt(grid.E.nodes[iE])
+ ν = deflection_frequency(v̂; nu_tilde=nu_tilde, model=phys.collision_model)
+ for iσ in 1:nσ, iξ in 1:nξ, ix in 1:nx
+ c[ix, iξ, iE, iσ] = ν
+ end
+ end
+ return c
+end
+
+"""
+ streaming_coefficients(grid, phys) -> (a_xi, a_x)
+
+Build the **cleared** island-streaming coefficients for
+`Operators.ParallelStreaming` (`01 §2`; derivation `parallel-streaming.md`,
+sign-off 2026-07-11):
+
+```math
+a_\\xi = \\frac{\\hat L_q^{-1}}{\\hat\\rho_{\\theta i}}\\,x\\,\\Theta(y_c-y),
+\\qquad
+a_x = -\\frac{\\hat L_q^{-1}\\hat w^2}{4\\,\\hat\\rho_{\\theta i}}\\,\\sin\\xi\\,\\Theta(y_c-y),
+```
+
+with `ŵ = phys.w_psi`, `ρ̂_θi = phys.rho_hat_theta_i`, and `Θ(y_c − y)` the
+passing-particle mask (`1` for `y < grid.y_c`, `0` for trapped). Together they
+are `(L̂_q⁻¹ ŵ²/4ρ̂_θi)Θ · {Ω, ·}` — advection along the island flux surfaces
+(derivation §3). Depends on `(x, ξ, y)` only, broadcast over `(E, σ)`. The
+normalization is chosen so the cleared drift `c_D = ω̂_D` is unchanged (§2).
+Returns `(a_xi, a_x)`, each shaped like `g`.
+"""
+function streaming_coefficients(grid::IslandGrid, phys::Level0Physics)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ w = phys.w_psi
+ ρ = phys.rho_hat_theta_i
+ ρ != 0 || throw(ArgumentError("rho_hat_theta_i must be nonzero"))
+ a_xi = Array{Float64}(undef, nx, nξ, ny, nE, nσ)
+ a_x = Array{Float64}(undef, nx, nξ, ny, nE, nσ)
+ @inbounds for iy in 1:ny
+ Θ = grid.y.nodes[iy] < grid.y_c ? 1.0 : 0.0 # passing-only (01 §2)
+ for iξ in 1:nξ
+ sξ = sin(grid.ξ.nodes[iξ])
+ for ix in 1:nx
+ x = grid.x.nodes[ix]
+ v_xi = (phys.inv_Lq / ρ) * x * Θ # (L̂_q⁻¹/ρ̂_θi) x Θ
+ v_x = -(phys.inv_Lq * w^2 / (4 * ρ)) * sξ * Θ # −(L̂_q⁻¹ŵ²/4ρ̂_θi) sinξ Θ
+ for iσ in 1:nσ, iE in 1:nE
+ a_xi[ix, iξ, iy, iE, iσ] = v_xi
+ a_x[ix, iξ, iy, iE, iσ] = v_x
+ end
+ end
+ end
+ end
+ return a_xi, a_x
+end
+
+"""
+ gradient_far_field(grid, phys) -> FarFieldConditions
+
+Build the **cleared** neoclassical far-field boundary state — the Level-0
+gradient drive (`01 §2`; derivation `gradient-drive.md`, sign-off 2026-07-11).
+I19's master equation is homogeneous (no interior source); the gradients enter
+as the far field `Ḡ₀ → g_drive = p_φ F'_{Mi}`, which in the code normalization is
+
+```math
+g_{\\rm far}(x{=}\\pm L_x, \\xi, y, E, \\sigma) = x\\,\\hat L_{n0}^{-1}\\,[\\,1 + (E-\\tfrac32)\\eta_i\\,]
+```
+
+\\noindent
+— linear in the boundary `x = ±halfwidth`, the temperature correction through
+`E = v̂²`, isotropic in `ξ, y, σ` at leading order (the Maxwellian `e^{-E}` is
+carried by the energy-grid measure). `L̂_{n0}⁻¹ = phys.inv_Ln0`,
+`η_i = phys.eta_i`. At Level 0 `ω_E = 0`, so `Φ̂_far = 0`. Returns the
+`Operators.FarFieldConditions` (the companion `Operators.GradientDrive` source is
+zero — I19 Formulation A).
+"""
+function gradient_far_field(grid::IslandGrid, phys::Level0Physics)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ x_left = grid.x.nodes[1]
+ x_right = grid.x.nodes[nx]
+ g_left = Array{Float64}(undef, nξ, ny, nE, nσ)
+ g_right = Array{Float64}(undef, nξ, ny, nE, nσ)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny, iξ in 1:nξ
+ temp = 1 + (grid.E.nodes[iE] - 1.5) * phys.eta_i # 1 + (E − 3/2)η_i
+ g_left[iξ, iy, iE, iσ] = x_left * phys.inv_Ln0 * temp
+ g_right[iξ, iy, iE, iσ] = x_right * phys.inv_Ln0 * temp
+ end
+ return FarFieldConditions(g_left, g_right, zeros(nξ), zeros(nξ)) # Φ̂_far = 0 (ω_E = 0)
+end
+
+"""
+ quasineutrality_source(grid, phys) -> Matrix{Float64}
+
+Build the **cleared** flattened-electron drive `S[ix, iξ] = L̂_{n0}⁻¹(x − ĥ(Ω))`
+for `Operators.Quasineutrality` (`01 §3`; derivation `quasineutrality-closure.md`,
+sign-off 2026-07-11). `Ω = 2x²/w² − cosξ` with `w = phys.w_psi`, and
+`ĥ(Ω) = Coefficients.h_amplitude(w) · ∫₁^Ω dΩ′/Q(Ω′)` (`Fields.h_profile`, the
+cleared far-field-matched profile: `ĥ = 0` inside the separatrix, `ĥ → x`
+outside). `L̂_{n0}⁻¹ = phys.inv_Ln0`. This is the drive whose absence left the
+Level-0 potential trivially zero (QUESTIONS Q5, now closed for the field term).
+"""
+function quasineutrality_source(grid::IslandGrid, phys::Level0Physics)
+ nx, nξ = grid.x.n, grid.ξ.n
+ w = phys.w_psi
+ C = h_amplitude(w) # cleared ĥ amplitude w/(2√2)
+ S = Array{Float64}(undef, nx, nξ)
+ @inbounds for iξ in 1:nξ
+ cξ = cos(grid.ξ.nodes[iξ])
+ for ix in 1:nx
+ x = grid.x.nodes[ix]
+ Ω = 2 * x^2 / w^2 - cξ # island label (Moments.omega_label)
+ ĥ = h_profile(Ω; prefactor=C) # cleared: 0 inside, →x outside
+ S[ix, iξ] = phys.inv_Ln0 * (x - ĥ)
+ end
+ end
+ return S
+end
+
+# ---------------------------------------------------------------------------
+# The assembly
+# ---------------------------------------------------------------------------
+"""
+ configure_level0(grid, phys, species; gated)
+
+Assemble the Level-0 named configuration (`03 §2`): returns a NamedTuple
+`(stack, bc, delta_prefactors, cleared, gated)` where
+
+ - `stack::Operators.IslandStack` — the operator stack (streaming, magnetic
+ drift, `E×B`, mimetic pitch collisions, gradient drive) + the
+ quasineutrality field term;
+ - `bc` — the cleared far-field `Operators.FarFieldConditions` (the gradient
+ drive, from [`gradient_far_field`]);
+ - `delta_prefactors` — the cleared `(cos, sin)` `Δ`-moment prefactors
+ (`Coefficients.delta_moment_prefactors`);
+ - `cleared`, `gated` — the provenance tuples naming which coefficients came
+ from cleared `Coefficients.*` builders vs. supplied gated inputs.
+
+The magnetic drift `c_D`, the island streaming `a_xi`/`a_x`, the pitch-collision
+`P`/`K` and `c`, the quasineutrality field term (`α` + the `L̂_{n0}⁻¹(x − ĥ)`
+drive), the gradient drive (zero source + the far field `bc`), and the `Δ`
+prefactors are populated from the **cleared** coefficient builders. The `E×B`,
+collision magnitude, and pitch measure are **supplied** through
+`gated::GatedLevel0Inputs` (QUESTIONS Q5) — this function assigns no physics
+value to them.
+
+`species` is validated (a Level-0 config must have a bulk ion); its
+per-species roles/backgrounds drive the gated builders that are not yet cleared.
+"""
+function configure_level0(grid::IslandGrid, phys::Level0Physics, species::AbstractVector{<:Species};
+ gated::GatedLevel0Inputs)
+ validate_species(species)
+
+ # cleared coefficient wiring
+ c_D = drift_coefficient_table(grid, phys)
+ P, wmeas = pitch_diffusivity_profile(grid, gated.B_profile)
+ K, _ = conservative_pitch_operator(grid.y, P, wmeas)
+ c_coll = collision_coefficient(grid, phys, gated.nu_tilde)
+ Δpref = delta_moment_prefactors(; mu0_R=phys.mu0_R, w_psi=phys.w_psi, dq_dpsi=phys.dq_dpsi, q_s=phys.q_s)
+
+ # cleared island streaming (advection along Ω; parallel-streaming.md)
+ a_xi, a_x = streaming_coefficients(grid, phys)
+ # cleared quasineutrality field term (α + drive from the signed-off closure)
+ α = 1 / quasineutrality_coefficient(phys.tau) # (τ+1)/τ, adiabatic shielding
+ S_Φ = quasineutrality_source(grid, phys) # L̂_{n0}⁻¹(x − ĥ)
+
+ # cleared gradient drive (I19 Formulation A, gradient-drive.md): the master
+ # equation is homogeneous — zero interior source; the drive is the far field.
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ drive0 = zeros(Float64, nx, nξ, ny, nE, nσ)
+ bc = gradient_far_field(grid, phys)
+
+ kinetic = (
+ ParallelStreaming(a_xi, a_x), # cleared (01 §2)
+ MagneticDrift(c_D; variant=phys.variant), # cleared
+ ExBDrift(gated.c_E), # gated
+ PitchAngleDiffusion(K, c_coll), # cleared shape (magnitude gated)
+ GradientDrive(drive0) # cleared: zero source (drive is the far field)
+ )
+ stack = IslandStack(kinetic, Quasineutrality(α, S_Φ)) # cleared closure (01 §3)
+
+ return (stack=stack, bc=bc, delta_prefactors=Δpref,
+ cleared=(:magnetic_drift, :streaming, :pitch_diffusivity, :deflection_frequency, :delta_prefactors, :quasineutrality, :gradient_drive, :far_field),
+ gated=(:exb, :nu_tilde, :pitch_measure))
+end
+
+# ---------------------------------------------------------------------------
+# Documented non-physics placeholders (structural runs only)
+# ---------------------------------------------------------------------------
+"""
+ level0_placeholders(grid; c_E=0.0, nu_tilde=1.0, B_edge=0.999)
+
+Build a [`GatedLevel0Inputs`] of **documented non-physics placeholder** values so
+the assembled stack can be exercised *structurally* — that `configure_level0`
+produces a well-formed, solvable `IslandStack` — **never for a physics result**
+(the remaining gated coefficients are uncleared, QUESTIONS Q5). Choices:
+
+ - `c_E = 0` (drop the `E×B` nonlinearity so the structural residual is linear);
+ - `nu_tilde = 1` (order-unity);
+ - a `B_profile` that keeps `y·B ≤ 1` on the grid (`B = min(1, B_edge/y)`), so
+ the cleared `pitch_diffusivity` stays in-domain.
+
+Every value is a structural stand-in; a physics run supplies cleared inputs. The
+gradient drive and far field are now *cleared* (built by `configure_level0` from
+`phys`), so they are no longer placeholders here.
+"""
+function level0_placeholders(grid::IslandGrid; c_E::Real=0.0, nu_tilde::Real=1.0, B_edge::Real=0.999)
+ ny = grid.y.n
+ # B_profile ≤ 1/y keeps the cleared pitch_diffusivity in [0,1]; B ≤ 1 (B_max norm)
+ B_profile = [min(1.0, B_edge / max(grid.y.nodes[iy], eps())) for iy in 1:ny]
+ return GatedLevel0Inputs(; c_E=Float64(c_E), nu_tilde=Float64(nu_tilde), B_profile=B_profile)
+end
+
+end # module Configure
diff --git a/src/Islands/fields/Fields.jl b/src/Islands/fields/Fields.jl
new file mode 100644
index 000000000..7724cf6fc
--- /dev/null
+++ b/src/Islands/fields/Fields.jl
@@ -0,0 +1,144 @@
+"""
+ Fields
+
+The Level-0 field-equation layer (design `01 §3`, `03 §1`): quasineutrality is
+the only field equation at Level 0 (Ampère arrives at Level 3). The residual
+term itself lives in `Operators.Quasineutrality`; this module provides the
+closure *structure* around it:
+
+ - the island-geometry functions `Q(Ω)`, `h(Ω)` of the flattened-electron
+ (WCHH96-class) closure — implemented as **structure with a supplied
+ prefactor** (the physics prefactor `w_ψ/2√2` is `[CHECKED]`-uncleared,
+ QUESTIONS Q3);
+ - the coefficient-free consistency identity `⟨∂²h/∂x²⟩_Ω = 0` (ladder A7 —
+ kokuchou's unit set, which caught inherited DK-NTM bugs);
+ - [`ElectronClosure`](@ref) — the gated constant set of the closure, NaN-
+ poisoned until human-cleared.
+"""
+module Fields
+
+import QuadGK
+
+export Q_omega, dQ_domega, h_profile, dh_domega, d2h_domega2
+export flat_average_d2h_dx2, ElectronClosure, is_cleared
+
+"""
+ Q_omega(Ω; rtol=1e-10)
+
+`Q(Ω) = (1/2π) ∮ √(Ω + cos ξ) dξ` over the region where the radicand is
+positive (`01 §2.4` structure). For `Ω > 1` the integral covers the full
+period; for `|Ω| < 1` it runs between the turning points `cos ξ_b = −Ω`.
+"""
+function Q_omega(Ω; rtol::Real=1e-10)
+ if Ω > 1
+ val, _ = QuadGK.quadgk(ξ -> sqrt(Ω + cos(ξ)), 0.0, 2π; rtol=rtol)
+ return val / (2π)
+ elseif -1 < Ω <= 1
+ ξb = acos(-Ω)
+ val, _ = QuadGK.quadgk(ξ -> sqrt(max(Ω + cos(ξ), 0.0)), -ξb, ξb; rtol=rtol)
+ return val / (2π)
+ else
+ throw(DomainError(Ω, "Q_omega needs Ω > −1"))
+ end
+end
+
+"""
+ dQ_domega(Ω; rtol=1e-10)
+
+`dQ/dΩ = (1/4π) ∮ (Ω + cos ξ)^{−1/2} dξ` (differentiating `Q_omega` under the
+integral; the inside-separatrix endpoint singularity is integrable).
+"""
+function dQ_domega(Ω; rtol::Real=1e-10)
+ if Ω > 1
+ val, _ = QuadGK.quadgk(ξ -> 1.0 / sqrt(Ω + cos(ξ)), 0.0, 2π; rtol=rtol)
+ return val / (4π)
+ elseif -1 < Ω < 1
+ ξb = acos(-Ω)
+ val, _ = QuadGK.quadgk(ξ -> 1.0 / sqrt(Ω + cos(ξ)), -ξb, ξb; rtol=rtol)
+ return val / (4π)
+ else
+ throw(DomainError(Ω, "dQ_domega needs Ω ∈ (−1, 1) ∪ (1, ∞)"))
+ end
+end
+
+"""
+ h_profile(Ω; prefactor, rtol=1e-10)
+
+The flattened-electron profile function *structure* `h(Ω) = Θ(Ω − 1) · prefactor · ∫₁^Ω dΩ′/Q(Ω′)` (`01 §2.4`): exactly flat inside the separatrix,
+`→ x` far outside. The physics prefactor (`w_ψ/2√2` in the sources) is
+`[CHECKED]`-uncleared (QUESTIONS Q3) and therefore **supplied**; the A7
+identity below is prefactor-independent.
+"""
+function h_profile(Ω; prefactor, rtol::Real=1e-10)
+ Ω <= 1 && return zero(float(Ω))
+ val, _ = QuadGK.quadgk(Ωp -> 1.0 / Q_omega(Ωp; rtol=rtol), 1.0, Ω; rtol=rtol)
+ return prefactor * val
+end
+
+"""
+ dh_domega(Ω; prefactor, rtol=1e-10)
+
+`h′(Ω) = prefactor / Q(Ω)` for `Ω > 1`, zero inside.
+"""
+dh_domega(Ω; prefactor, rtol::Real=1e-10) = Ω > 1 ? prefactor / Q_omega(Ω; rtol=rtol) : zero(float(Ω))
+
+"""
+ d2h_domega2(Ω; prefactor, rtol=1e-10)
+
+`h″(Ω) = −prefactor · Q′(Ω)/Q(Ω)²` for `Ω > 1`, zero inside.
+"""
+function d2h_domega2(Ω; prefactor, rtol::Real=1e-10)
+ Ω <= 1 && return zero(float(Ω))
+ Q = Q_omega(Ω; rtol=rtol)
+ return -prefactor * dQ_domega(Ω; rtol=rtol) / Q^2
+end
+
+"""
+ flat_average_d2h_dx2(Ω, w; prefactor=1.0, rtol=1e-10)
+
+The ladder-A7 identity integrand: `⟨∂²h/∂x²⟩_Ω` assembled from the chain rule
+`∂²h/∂x² = h″(Ω)(∂Ω/∂x)² + h′(Ω) ∂²Ω/∂x²` with `∂Ω/∂x = 4x/w²` on the surface
+(`(∂Ω/∂x)² = 8(Ω + cos ξ)/w²`), averaged with the `(Ω + cos ξ)^{−1/2}` weight.
+Analytically **exactly zero** for any prefactor — a coefficient-free
+consistency check of the `Q`, `h` quadratures and the `⟨·⟩_Ω` machinery
+(L23 Eq. 4.1.1-class unit target). Valid for `Ω > 1`.
+"""
+function flat_average_d2h_dx2(Ω, w; prefactor::Real=1.0, rtol::Real=1e-10)
+ Ω > 1 || throw(DomainError(Ω, "the A7 identity applies outside the separatrix (Ω > 1)"))
+ hp = dh_domega(Ω; prefactor=prefactor, rtol=rtol)
+ hpp = d2h_domega2(Ω; prefactor=prefactor, rtol=rtol)
+ num, _ = QuadGK.quadgk(ξ -> (hpp * 8 * (Ω + cos(ξ)) / w^2 + hp * 4 / w^2) / sqrt(Ω + cos(ξ)), 0.0, 2π; rtol=rtol)
+ den, _ = QuadGK.quadgk(ξ -> 1.0 / sqrt(Ω + cos(ξ)), 0.0, 2π; rtol=rtol)
+ return num / den
+end
+
+"""
+ ElectronClosure(; k_HS=NaN, f_p=NaN, h_prefactor=NaN, C_phi=NaN)
+
+The gated constant set of the Level-0 flattened-electron closure (`01 §2.4`,
+`§3`), all `[CHECKED]`-uncleared (QUESTIONS Q3) and **NaN-defaulted** so an
+un-cleared closure poisons results instead of committing to a guess:
+
+## Fields
+
+ - `k_HS` — the Hirshman–Sigmar flow coefficient (sources: `≃ −1.173`).
+ - `f_p` — the passing fraction (**cleared** 2026-07-11:
+ `Coefficients.passing_fraction(ε) = 1 − 1.4624√ε`; may populate this field).
+ - `h_prefactor` — the `h(Ω)` amplitude (sources: `w_ψ/2√2`).
+ - `C_phi` — the quasineutrality closure coefficient (sources: `1/2L̂_{n0}`).
+"""
+Base.@kwdef struct ElectronClosure
+ k_HS::Float64 = NaN
+ f_p::Float64 = NaN
+ h_prefactor::Float64 = NaN
+ C_phi::Float64 = NaN
+end
+
+"""
+ is_cleared(ec::ElectronClosure)
+
+`true` only when every gated closure constant has been assigned (no NaN).
+"""
+is_cleared(ec::ElectronClosure) = !(isnan(ec.k_HS) || isnan(ec.f_p) || isnan(ec.h_prefactor) || isnan(ec.C_phi))
+
+end # module Fields
diff --git a/src/Islands/frames/Frames.jl b/src/Islands/frames/Frames.jl
new file mode 100644
index 000000000..079167d9c
--- /dev/null
+++ b/src/Islands/frames/Frames.jl
@@ -0,0 +1,119 @@
+"""
+ Frames
+
+THE frequency/frame conversion module (design `01 §5`, module CLAUDE.md): no
+other part of Islands may contain an ω sign convention. The polarization-current
+sign disputes in the literature are largely frame disputes; this module owns the
+conversions so they cannot be reproduced inconsistently elsewhere.
+
+**Milestone-M2 status: forms only, signs gated.** The frame identities of
+`01 §5` are `[CHECKED: Diss19 pp. 46–48]` but not human-cleared (QUESTIONS Q3),
+so every sign/normalization here is a `FrameConvention` field with a **NaN
+default**: using an un-cleared convention poisons every downstream number
+instead of silently committing to a guess. Only the mechanical, sign-free
+bookkeeping (`frame_shift`, round-trips, parameter validation) is active.
+"""
+module Frames
+
+export Level0Parameters, FrameConvention
+export frame_shift, omega_dia_form, effective_dlnn_form, is_cleared
+
+"""
+ Level0Parameters(; w_hat, omega_E_hat, epsilon, inv_Lq_hat, q_s, tau, nu_star)
+
+The Level-0 input parameter vector `p` (`01 §5`). Species-resolved quantities
+(gradients, `η_j`, backgrounds, roles) live on the species list; this struct
+carries the scalars.
+
+## Fields
+
+ - `w_hat` — island **half**-width `w/ρ_θi` (half-width convention pinned
+ in the module CLAUDE.md; thresholds are always reported as half-widths).
+ - `omega_E_hat` — `ω_E/ω_dia,e` (`≡ −ω₀/ω_dia,e`; a scanned input from day one,
+ ordering O4).
+ - `epsilon` — inverse aspect ratio `r_s/R₀`.
+ - `inv_Lq_hat` — `L̂_q⁻¹ = (ψ_s/q) dq/dψ` at `r_s`.
+ - `q_s` — safety factor at the rational surface.
+ - `tau` — `T_e/T_i`.
+ - `nu_star` — per-species banana collisionality `ν_★j`, keyed by species name.
+"""
+struct Level0Parameters
+ w_hat::Float64
+ omega_E_hat::Float64
+ epsilon::Float64
+ inv_Lq_hat::Float64
+ q_s::Float64
+ tau::Float64
+ nu_star::Dict{Symbol,Float64}
+ function Level0Parameters(w_hat, omega_E_hat, epsilon, inv_Lq_hat, q_s, tau, nu_star)
+ w_hat > 0 || throw(ArgumentError("w_hat must be positive (half-width)"))
+ epsilon > 0 || throw(ArgumentError("epsilon must be positive"))
+ q_s > 0 || throw(ArgumentError("q_s must be positive"))
+ tau > 0 || throw(ArgumentError("tau must be positive"))
+ all(v -> v >= 0, values(nu_star)) || throw(ArgumentError("nu_star entries must be nonnegative"))
+ return new(w_hat, omega_E_hat, epsilon, inv_Lq_hat, q_s, tau, Dict{Symbol,Float64}(nu_star))
+ end
+end
+
+function Level0Parameters(; w_hat, omega_E_hat, epsilon, inv_Lq_hat, q_s, tau=1.0, nu_star=Dict{Symbol,Float64}())
+ return Level0Parameters(w_hat, omega_E_hat, epsilon, inv_Lq_hat, q_s, tau, nu_star)
+end
+
+"""
+ FrameConvention(; C_dia=NaN, sign_omega0=NaN, C_gradient_shift=NaN)
+
+The gated sign/normalization set of the frame identities (`01 §5`,
+`[CHECKED: Diss19 pp. 46–48]`, awaiting human clearance — QUESTIONS **Q3**).
+All fields default to **NaN** so an un-cleared convention poisons results
+rather than silently committing to a guess; `is_cleared` tests for this.
+
+## Fields
+
+ - `C_dia` — prefactor (incl. sign) of the electron diamagnetic
+ frequency form `ω_dia,e = C_dia · m T̂_e L̂_n⁻¹ / q_s`.
+ - `sign_omega0` — the island-propagation relation `ω₀ = sign_omega0 · ω_E`.
+ - `C_gradient_shift` — prefactor of the frame shift of the effective density
+ gradient, `L_n⁻¹ = L_{n0}⁻¹ (1 + C_gradient_shift · Z_j ω_E/ω_dia,e)`.
+"""
+Base.@kwdef struct FrameConvention
+ C_dia::Float64 = NaN
+ sign_omega0::Float64 = NaN
+ C_gradient_shift::Float64 = NaN
+end
+
+"""
+ is_cleared(conv::FrameConvention)
+
+`true` only when every gated field has been assigned (no NaN). Downstream code
+must check this before producing physics numbers.
+"""
+is_cleared(conv::FrameConvention) = !(isnan(conv.C_dia) || isnan(conv.sign_omega0) || isnan(conv.C_gradient_shift))
+
+"""
+ frame_shift(omega, omega_E)
+
+The frame-invariant combination `ω − ω_E` (`01 §5`): mechanical bookkeeping,
+no sign convention (the invariance is what *defines* the shift).
+"""
+frame_shift(omega, omega_E) = omega - omega_E
+
+"""
+ omega_dia_form(m_mode, T_hat, inv_Ln_hat, q_s, conv)
+
+The electron diamagnetic frequency *form* `C_dia · m T̂ L̂_n⁻¹ / q_s`
+(structure of `01 §5`; value gated on `conv.C_dia`, QUESTIONS Q3). Returns NaN
+until the convention is cleared.
+"""
+omega_dia_form(m_mode, T_hat, inv_Ln_hat, q_s, conv::FrameConvention) = conv.C_dia * m_mode * T_hat * inv_Ln_hat / q_s
+
+"""
+ effective_dlnn_form(inv_Ln0_hat, Z, omega_E_hat, conv)
+
+The frame shift of the effective density gradient *form*
+`L̂_n⁻¹ = L̂_{n0}⁻¹ (1 + C_gradient_shift · Z ω̂_E)` (structure of `01 §5`; value
+gated on `conv.C_gradient_shift`, QUESTIONS Q3). `omega_E_hat` is already
+normalized to `ω_dia,e`. Returns NaN until the convention is cleared.
+"""
+effective_dlnn_form(inv_Ln0_hat, Z, omega_E_hat, conv::FrameConvention) = inv_Ln0_hat * (1 + conv.C_gradient_shift * Z * omega_E_hat)
+
+end # module Frames
diff --git a/src/Islands/moments/Moments.jl b/src/Islands/moments/Moments.jl
new file mode 100644
index 000000000..bd4c3db8c
--- /dev/null
+++ b/src/Islands/moments/Moments.jl
@@ -0,0 +1,161 @@
+"""
+ Moments
+
+Output-moment assembly (design `01 §4`, `03 §1`): the parallel current
+`J̄_∥(x, ξ)` from species-summed velocity moments, its `cos ξ`/`sin ξ` Ampère
+projections `Δ_cos`/`Δ_sin`, and the island flux-surface-average diagnostics
+(`Ω` label, `⟨·⟩_Ω`, bootstrap/polarization channel split).
+
+**Gating:** the projection and quadrature machinery here is pure numerics. The
+physics enters through (i) the per-species velocity-space weights `W_j` (the
+`v̂_∥`-structure — `[VERIFY]`-gated, QUESTIONS Q3), and (ii) the `Δ` moment
+prefactors (`±μ₀R/2ψ̃`). The `ψ̃` amplitude is **cleared** — see
+[`island_flux_amplitude`](@ref) (human sign-off 2026-07-11; derivation
+`docs/src/islands/derivations/psi-tilde-amplitude.md`, docs/01 §1) — but the
+`μ₀R` normalization and the sin-moment normalization pin (`[DERIVED]`,
+docs/01 §4) are not, so `delta_moments`' prefactors remain **required
+caller-supplied arguments** with no defaults; nothing here assigns them.
+
+The island label convention is the module-CLAUDE.md pin: `Ω = 2x²/w² − cos ξ`,
+O-point `Ω = −1`, separatrix `Ω = +1`, `w` = **half**-width.
+"""
+module Moments
+
+using LinearAlgebra
+import QuadGK
+import ..PhaseSpace: IslandGrid, nnodes
+import ..Operators: weighted_moment!
+import ..SpeciesLists: Species
+
+export parallel_current!, delta_moments, omega_label, omega_average, channel_split
+export island_flux_amplitude
+
+"""
+ island_flux_amplitude(; w_psi, dq_dpsi, q_s)
+
+The prescribed single-helicity island flux amplitude (`01 §1`, ordering O3):
+
+```math
+\\tilde\\psi = \\frac{w_\\psi^2}{4}\\,\\frac{q_s'}{q_s},
+\\qquad q_s' = dq/d\\psi|_s ,
+```
+
+with `w_psi` the island **half**-width in `ψ`-space and `dq_dpsi`, `q_s` the
+safety-factor derivative and value at the rational surface. This is a **cleared
+physics relation** — human sign-off 2026-07-11, independent re-derivation
+(Decision D7): `docs/src/islands/derivations/psi-tilde-amplitude.md`. It
+resolves the former `q_s'/q_s` vs `q_s/q_s'` `[VERIFY]` (I19 as printed has a
+typo; the physical form is `q_s'/q_s`, consistent with I19's own Ω convention
+and Diss19/D21/L23). Feeds the `Δ`-moment prefactor `∓μ₀R/(2 ψ̃)`
+([`delta_moments`](@ref); the `μ₀R` and sin normalization remain gated).
+"""
+function island_flux_amplitude(; w_psi::Real, dq_dpsi::Real, q_s::Real)
+ q_s != 0 || throw(ArgumentError("q_s must be nonzero"))
+ return (w_psi^2 / 4) * (dq_dpsi / q_s)
+end
+
+"""
+ parallel_current!(Jpar, gs, species, weights, grid)
+
+Assemble `J̄_∥(x, ξ) = Σ_j Z_j ∫ W_j g_j` into `Jpar[ix, iξ]` (`01 §4`): one
+`weighted_moment!` per species, charge-scaled and accumulated. `gs`, `species`
+and `weights` are aligned vectors; each `W_j` is the supplied `(ny, nE, nσ)`
+parallel-flow velocity weight (gated physics, QUESTIONS Q3).
+"""
+function parallel_current!(Jpar, gs::AbstractVector, species::AbstractVector{<:Species}, weights::AbstractVector, grid::IslandGrid)
+ length(gs) == length(species) == length(weights) ||
+ throw(ArgumentError("gs, species, weights must be aligned (got $(length(gs)), $(length(species)), $(length(weights)))"))
+ fill!(Jpar, zero(eltype(Jpar)))
+ for (g, sp, W) in zip(gs, species, weights)
+ weighted_moment!(Jpar, g, W, grid; scale=sp.Z, accumulate=true)
+ end
+ return Jpar
+end
+
+"""
+ delta_moments(Jpar, grid; prefactor_cos, prefactor_sin)
+
+The two Ampère projections of `J̄_∥` through the island (`01 §4`):
+
+ Δ_cos = prefactor_cos · ∫dx ∮dξ J̄_∥ cos ξ
+ Δ_sin = prefactor_sin · ∫dx ∮dξ J̄_∥ sin ξ
+
+`ξ`-integration is the uniform-grid trapezoid (spectrally exact on the periodic
+grid); `x`-integration uses the grid's Simpson weights. The prefactors
+(`∓μ₀R/2ψ̃`, `01 §4`) are **gated** — `ψ̃` carries an open `[VERIFY]` and the
+sin normalization is `[DERIVED]`-unpinned (QUESTIONS Q4) — so both are required
+arguments. Returns `(Δcos, Δsin)`.
+"""
+function delta_moments(Jpar, grid::IslandGrid; prefactor_cos, prefactor_sin)
+ nx, nξ = size(Jpar)
+ (nx, nξ) == (grid.x.n, grid.ξ.n) || throw(ArgumentError("Jpar must be (nx, nξ) = $((grid.x.n, grid.ξ.n))"))
+ wξ = grid.ξ.L / grid.ξ.n
+ pc = zero(eltype(Jpar))
+ ps = zero(eltype(Jpar))
+ @inbounds for iξ in 1:nξ
+ c = cos(grid.ξ.nodes[iξ])
+ s = sin(grid.ξ.nodes[iξ])
+ for ix in 1:nx
+ w = grid.x.wq[ix] * wξ * Jpar[ix, iξ]
+ pc += w * c
+ ps += w * s
+ end
+ end
+ return (Δcos=prefactor_cos * pc, Δsin=prefactor_sin * ps)
+end
+
+"""
+ omega_label(x, ξ, w)
+
+The island flux-surface label `Ω = 2x²/w² − cos ξ` (pinned convention, module
+CLAUDE.md; O-point `Ω = −1`, separatrix `Ω = +1`, `w` = half-width). A
+diagnostic label only — never a solve coordinate (Decision D1).
+"""
+omega_label(x, ξ, w) = 2 * x^2 / w^2 - cos(ξ)
+
+# x on the Ω surface at helical angle ξ (branch = ±1 selects the x-sign).
+_x_on_surface(Ω, ξ, w, branch) = branch * (w / sqrt(2)) * sqrt(max(Ω + cos(ξ), 0.0))
+
+"""
+ omega_average(f, Ω, w; branch=+1, rtol=1e-10)
+
+Island flux-surface average `⟨f⟩_Ω = ∮ f (Ω + cos ξ)^{−1/2} dξ / ∮ (Ω + cos ξ)^{−1/2} dξ`
+(`01 §4` decomposition weight) of a callable `f(x, ξ)`:
+
+ - `Ω > 1` (outside the separatrix): the `±x` branches are distinct surfaces;
+ `branch` selects one, integrating over the full `ξ ∈ [0, 2π)`.
+ - `−1 < Ω < 1` (inside): the surface closes through both branches between the
+ turning points `cos ξ_b = −Ω`; the endpoint weight singularity is integrable
+ and handled by the adaptive quadrature.
+"""
+function omega_average(f, Ω, w; branch::Int=1, rtol::Real=1e-10)
+ if Ω > 1
+ num, _ = QuadGK.quadgk(ξ -> f(_x_on_surface(Ω, ξ, w, branch), ξ) / sqrt(Ω + cos(ξ)), 0.0, 2π; rtol=rtol)
+ den, _ = QuadGK.quadgk(ξ -> 1.0 / sqrt(Ω + cos(ξ)), 0.0, 2π; rtol=rtol)
+ return num / den
+ elseif -1 < Ω < 1
+ ξb = acos(-Ω)
+ integrand(ξ) = (f(_x_on_surface(Ω, ξ, w, +1), ξ) + f(_x_on_surface(Ω, ξ, w, -1), ξ)) / sqrt(Ω + cos(ξ))
+ num, _ = QuadGK.quadgk(integrand, -ξb, ξb; rtol=rtol)
+ den, _ = QuadGK.quadgk(ξ -> 2.0 / sqrt(Ω + cos(ξ)), -ξb, ξb; rtol=rtol)
+ return num / den
+ else
+ throw(DomainError(Ω, "omega_average needs Ω ∈ (−1, 1) ∪ (1, ∞) (O-point/separatrix excluded)"))
+ end
+end
+
+"""
+ channel_split(Jfun, Ω, w; branch=+1)
+
+The `01 §4` bootstrap/polarization bookkeeping at one `Ω` surface: returns
+`(bs, pol)` where `bs = ⟨J⟩_Ω` (the flux-surface-constant "bootstrap+curvature"
+part) and `pol(x, ξ) = Jfun(x, ξ) − bs` (the piece that `Ω`-averages to zero).
+Diagnostic only — the solve never separates channels; the split is approximate
+bookkeeping (L23 Eq. 2.5.3 caveat).
+"""
+function channel_split(Jfun, Ω, w; branch::Int=1)
+ bs = omega_average(Jfun, Ω, w; branch=branch)
+ return (bs=bs, pol=(x, ξ) -> Jfun(x, ξ) - bs)
+end
+
+end # module Moments
diff --git a/src/Islands/operators/Operators.jl b/src/Islands/operators/Operators.jl
new file mode 100644
index 000000000..8501fcff5
--- /dev/null
+++ b/src/Islands/operators/Operators.jl
@@ -0,0 +1,657 @@
+"""
+ Operators
+
+The Islands operator stack (`03 §2`): the state vector, the residual assembly,
+and the `AbstractTerm` family whose sum is the drift-kinetic residual.
+
+**Milestone-M1 status: allocation-free, AD-compatible *structural* stubs.** Each
+term implements its discretized differential/algebraic *structure* — which
+derivatives act, in which coordinate, with which sign pattern — but takes every
+physics coefficient as *supplied data* (`term.a_xi`, `term.c_D`, …). No literal
+physics coefficient, sign, or normalization is written here: those carry
+`[VERIFY]`/`[CHECKED]` tags (module `CLAUDE.md`) and are populated only after
+human clearance. The verification harness (`Verify`) exercises the discretization
+with arbitrary manufactured test coefficients, which is legitimate — it tests
+the numerics, not the physics.
+
+Operator-stack rules enforced here (`03 §2`, `CLAUDE.md`):
+
+ - terms are independent — no term inspects which others are active;
+ - `apply!` is generic over `eltype(U)` (ForwardDiff duals flow through) and
+ allocation-free on the hot path (regression-tested);
+ - orderings that *remove* structure are phase-space configurations, not terms.
+
+Term ↔ physics map (`03 §2`, `01 §2`): `ParallelStreaming` (island-induced
+streaming, `∂ξ` + `∂x`), `MagneticDrift` (precession, `∂ξ`, `:original`/`:improved`
+toggle), `ExBDrift` (the `E×B` Poisson bracket in `(x, ξ)`), `Collisions`
+(pitch-angle diffusion in `y`), `GradientDrive` (`(v·∇F₀)` source),
+`PerpTransport`/`RadiationSink` (Level-4 stubs), and `Quasineutrality` (the
+Level-0 field residual, `01 §3`).
+"""
+module Operators
+
+using LinearAlgebra
+import ..PhaseSpace: IslandGrid, nnodes
+
+export IslandState, IslandCache, IslandStack, AbstractTerm
+export ParallelStreaming, MagneticDrift, ExBDrift, Collisions, GradientDrive,
+ PerpTransport, RadiationSink, Quasineutrality
+export PitchAngleDiffusion, conservative_pitch_operator
+export FarFieldConditions, apply_farfield!
+export apply!, residual!, velocity_moment!, weighted_moment!, statelength,
+ flatten!, unflatten!, g_flat_index, Φ_flat_index
+
+# ---------------------------------------------------------------------------
+# State, cache
+# ---------------------------------------------------------------------------
+"""
+ IslandState(g, Φ)
+
+The Level-0 unknowns (`03 §2`): the orbit-averaged distribution
+`g[ix, iξ, iy, iE, iσ]` per grid point and the electrostatic potential
+`Φ[ix, iξ]`. Parametric in the element type so ForwardDiff duals flow through.
+
+## Fields
+
+ - `g` — 5D array over `(x, ξ, y, E, σ)`.
+ - `Φ` — 2D array over `(x, ξ)`.
+"""
+struct IslandState{T,A5<:AbstractArray{T,5},A2<:AbstractArray{T,2}}
+ g::A5
+ Φ::A2
+end
+
+function IslandState{T}(grid::IslandGrid) where {T}
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ return IslandState(zeros(T, nx, nξ, ny, nE, nσ), zeros(T, nx, nξ))
+end
+IslandState(grid::IslandGrid) = IslandState{Float64}(grid)
+
+Base.eltype(::IslandState{T}) where {T} = T
+
+function Base.similar(U::IslandState{T}) where {T}
+ return IslandState(similar(U.g), similar(U.Φ))
+end
+
+function fill_state!(U::IslandState, v)
+ fill!(U.g, v)
+ fill!(U.Φ, v)
+ return U
+end
+
+"""
+ IslandCache{T}(grid)
+
+Per-solve scratch buffers, typed to the state element type `T` so the hot path
+allocates nothing (and so AD duals get dual-typed scratch). Currently holds the
+two potential-gradient fields consumed by the `E×B` bracket.
+"""
+struct IslandCache{T}
+ dΦdx::Matrix{T}
+ dΦdξ::Matrix{T}
+end
+
+function IslandCache{T}(grid::IslandGrid) where {T}
+ nx, nξ, = nnodes(grid)
+ return IslandCache{T}(zeros(T, nx, nξ), zeros(T, nx, nξ))
+end
+IslandCache(grid::IslandGrid) = IslandCache{Float64}(grid)
+
+# ---------------------------------------------------------------------------
+# Term family
+# ---------------------------------------------------------------------------
+"""
+ AbstractTerm
+
+Supertype of every operator-stack term (`03 §2`). Each concrete term implements
+`apply!(R, term, U, grid, cache)` to accumulate its contribution to the residual.
+Terms are independent (no term inspects which others are active), generic over
+`eltype(U)`, and allocation-free on the hot path.
+"""
+abstract type AbstractTerm end
+
+"""
+ ParallelStreaming(a_xi, a_x)
+
+Island-induced parallel streaming (`01 §2`, the `∂ξ`/`∂x` channel): adds
+`a_xi ∂g/∂ξ + a_x ∂g/∂x` to the residual. `a_xi`, `a_x` are supplied coefficient
+arrays shaped like `g` (physics values `[VERIFY]`-gated; never literals here).
+"""
+struct ParallelStreaming{A} <: AbstractTerm
+ a_xi::A
+ a_x::A
+end
+
+"""
+ MagneticDrift(c_D; variant=:original)
+
+Orbit-averaged magnetic (precession) drift (`01 §2.1`): adds `c_D ∂g/∂ξ`. The
+`variant` selects the `:original` (finite `L̂_B⁻¹`, I19) vs `:improved`
+(`L̂_B⁻¹ → 0` proxy, D21) drift-frequency structure — the 8.73→1.46 ρ_bi toggle
+(`docs/05 E1`). Structure only: `c_D` (the `ω̂_D` coefficient over `(y, E, σ)`)
+is supplied data.
+"""
+struct MagneticDrift{A} <: AbstractTerm
+ c_D::A
+ variant::Symbol
+end
+MagneticDrift(c_D; variant::Symbol=:original) = MagneticDrift(c_D, variant)
+
+"""
+ ExBDrift(c_E)
+
+`E×B` advection as the Poisson bracket of `Φ` and `g` in `(x, ξ)`
+(`01 §2`): adds `c_E (∂Φ/∂ξ · ∂g/∂x − ∂Φ/∂x · ∂g/∂ξ)`. This is the one Level-0
+kinetic term nonlinear in the state (couples `g` and `Φ`); `c_E` is a supplied
+scalar coupling.
+"""
+struct ExBDrift{S} <: AbstractTerm
+ c_E::S
+end
+
+"""
+ Collisions(a_y, b_y; model=:pitch_angle)
+
+Pitch-angle (Lorentz) collision operator (`01 §2.3`) as a second-order operator
+in `y`: adds `a_y ∂²g/∂y² + b_y ∂g/∂y`. `model = :pitch_angle` is the Level-0
+form; `:fokker_planck` (Level 1) reuses the same slot. `a_y`, `b_y` are supplied
+coefficient arrays (the `ν̂`-weighted diffusion structure, `[VERIFY]`-gated).
+"""
+struct Collisions{A} <: AbstractTerm
+ a_y::A
+ b_y::A
+ model::Symbol
+end
+Collisions(a_y, b_y; model::Symbol=:pitch_angle) = Collisions(a_y, b_y, model)
+
+"""
+ PitchAngleDiffusion(K, c)
+
+Pitch-angle collision operator in **discretely conservative (mimetic) form**
+(`01 §2.3` structure; ladder A4): adds `c ⋅ (K g)` along `y`, where `K` is the
+divergence-form matrix built by [`conservative_pitch_operator`](@ref) and `c` is
+a supplied coefficient array over `(x, ξ, E, σ)` — **`y`-independent by
+construction**, so the exact discrete particle conservation and entropy-sign
+properties of `K` are preserved (the physical `ν̂(v̂)` energy dependence lives in
+`c`'s `E`-dependence and is `[VERIFY]`-gated). The momentum-restoring
+field-particle piece of the Level-0 operator is gated physics (QUESTIONS Q3)
+and is not part of this structure.
+"""
+struct PitchAngleDiffusion{M<:AbstractMatrix,A} <: AbstractTerm
+ K::M
+ c::A
+end
+
+"""
+ conservative_pitch_operator(ygrid, P, wmeas)
+
+Build the mimetic divergence-form pitch operator on `ygrid`
+(`C[g] = (1/w) d/dy(P w dg/dy)` discretized as `K = −Wq⁻¹ Gᵀ diag(P .* wq) G`
+with `G = ygrid.D1` and `Wq = wmeas .* ygrid.wq`), returning `(K, Wq)`.
+
+Because `G` differentiates constants exactly and the quadrature weights are
+positive, `K` satisfies **exactly** (to machine precision, ladder A4):
+
+ - particle conservation: `Wqᵀ (K g) = 0` for any `g` (zero-flux built in);
+ - entropy sign: `gᵀ diag(Wq) K g = −(Gg)ᵀ diag(P .* wq)(Gg) ≤ 0` for `P ≥ 0`.
+
+`P` (the pitch-space diffusivity profile, physically the `λ√(1−λB)`-structure)
+and `wmeas` (the velocity-space measure) are **supplied** profiles — their
+Level-0 physics forms are `[VERIFY]`-gated (QUESTIONS Q3); any positive test
+profiles exercise the conservation structure.
+"""
+function conservative_pitch_operator(ygrid, P::AbstractVector, wmeas::AbstractVector)
+ length(P) == ygrid.n || throw(ArgumentError("P must have length $(ygrid.n)"))
+ length(wmeas) == ygrid.n || throw(ArgumentError("wmeas must have length $(ygrid.n)"))
+ all(>=(0), P) || throw(ArgumentError("diffusivity profile P must be nonnegative"))
+ all(>(0), wmeas) || throw(ArgumentError("measure weights wmeas must be positive"))
+ G = ygrid.D1
+ Wq = wmeas .* ygrid.wq
+ K = -Diagonal(1.0 ./ Wq) * (G' * Diagonal(P .* ygrid.wq) * G)
+ return Matrix(K), Wq
+end
+
+"""
+ GradientDrive(drive)
+
+The `(v_E + v_D + v_ψ̃)·∇F₀` gradient drive (`03 §2`, `01 §2`): a state-independent
+source, adds `drive` to the residual. `drive` is a supplied array shaped like
+`g`; at Level 0 it is built from the background Maxwellian gradients (`[VERIFY]`).
+"""
+struct GradientDrive{A} <: AbstractTerm
+ drive::A
+end
+
+"""
+ PerpTransport(χ)
+
+Perpendicular transport (Level-4 closure stub, `03 §2`): adds `χ ∂²g/∂x²`.
+Present as structure only; the closure value `χ` is a supplied knob.
+"""
+struct PerpTransport{S} <: AbstractTerm
+ χ::S
+end
+
+"""
+ RadiationSink(κ)
+
+Radiative energy sink (Level-4 closure stub, `03 §2`): adds `-κ g`. Structure
+only; `κ` is a supplied coefficient array.
+"""
+struct RadiationSink{A} <: AbstractTerm
+ κ::A
+end
+
+"""
+ Quasineutrality(α)
+ Quasineutrality(α, source)
+
+The Level-0 field residual (`01 §3`): `R_Φ = M[g] − α Φ + source`, where `M[g]`
+is the velocity moment `∫dy ∫dE Σ_σ g` (Gauss in `E`, Simpson in `y`), `α` is
+the adiabatic-shielding coefficient `(τ+1)/τ`, and `source` is the
+`L̂_{n0}⁻¹(x − ĥ(Ω))` flattened-electron drive of the cleared closure (`01 §3`,
+`quasineutrality-closure.md`). Both come from the **human-cleared** closure
+(sign-off 2026-07-11) — built by `Configure.configure_level0` from
+`Coefficients.quasineutrality_coefficient` and the `ĥ` profile. `source`
+defaults to `nothing` (a pure `M[g] − α Φ` residual) for the manufactured-test
+and pre-drive configurations. This closes `Φ(x, ξ)` inside the global Newton
+system rather than the sources' fragile nested Picard loop (`01 §3`, `03 §3`).
+
+## Fields
+
+ - `α` — adiabatic-shielding coefficient multiplying `Φ` (`(τ+1)/τ`).
+ - `source` — the `(nx, nξ)` `L̂_{n0}⁻¹(x − ĥ)` field drive, or `nothing`.
+"""
+struct Quasineutrality{S,A} <: AbstractTerm
+ α::S
+ source::A
+end
+Quasineutrality(α) = Quasineutrality(α, nothing)
+
+# ---------------------------------------------------------------------------
+# Discrete kernels (allocation-free, generic over eltype)
+#
+# Directional-derivative kernels accumulate `coef · ∂g/∂dir` into `Rg`. The
+# dense matrices `D` are Float64; `D·g` with `g::Dual` promotes correctly, so
+# the whole stack is AD-transparent.
+# ---------------------------------------------------------------------------
+# adds coef .* (∂g/∂ξ) along dim 2
+@inline function _add_dxi!(Rg, coef, g, D)
+ nx, nξ, ny, nE, nσ = size(g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny, ix in 1:nx
+ for a in 1:nξ
+ acc = zero(eltype(Rg))
+ for b in 1:nξ
+ acc += D[a, b] * g[ix, b, iy, iE, iσ]
+ end
+ Rg[ix, a, iy, iE, iσ] += coef[ix, a, iy, iE, iσ] * acc
+ end
+ end
+ return Rg
+end
+
+# adds coef .* (∂g/∂x) along dim 1
+@inline function _add_dx!(Rg, coef, g, D)
+ nx, nξ, ny, nE, nσ = size(g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny, iξ in 1:nξ
+ for a in 1:nx
+ acc = zero(eltype(Rg))
+ for b in 1:nx
+ acc += D[a, b] * g[b, iξ, iy, iE, iσ]
+ end
+ Rg[a, iξ, iy, iE, iσ] += coef[a, iξ, iy, iE, iσ] * acc
+ end
+ end
+ return Rg
+end
+
+# adds coef .* (Dⁿ g along dim 3, y). D is D1 or D2.
+@inline function _add_dy!(Rg, coef, g, D)
+ nx, nξ, ny, nE, nσ = size(g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iξ in 1:nξ, ix in 1:nx
+ for a in 1:ny
+ acc = zero(eltype(Rg))
+ for b in 1:ny
+ acc += D[a, b] * g[ix, iξ, b, iE, iσ]
+ end
+ Rg[ix, iξ, a, iE, iσ] += coef[ix, iξ, a, iE, iσ] * acc
+ end
+ end
+ return Rg
+end
+
+# scalar-coefficient ∂²/∂x² (PerpTransport): adds χ .* D2x g along dim 1
+@inline function _add_dx2_scalar!(Rg, χ, g, D)
+ nx, nξ, ny, nE, nσ = size(g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny, iξ in 1:nξ
+ for a in 1:nx
+ acc = zero(eltype(Rg))
+ for b in 1:nx
+ acc += D[a, b] * g[b, iξ, iy, iE, iσ]
+ end
+ Rg[a, iξ, iy, iE, iσ] += χ * acc
+ end
+ end
+ return Rg
+end
+
+# ∂Φ/∂x and ∂Φ/∂ξ into cache buffers (allocation-free)
+@inline function _potential_gradients!(cache::IslandCache, Φ, Dx, Dξ)
+ nx, nξ = size(Φ)
+ dΦdx, dΦdξ = cache.dΦdx, cache.dΦdξ
+ @inbounds for iξ in 1:nξ, ix in 1:nx
+ ax = zero(eltype(Φ))
+ for b in 1:nx
+ ax += Dx[ix, b] * Φ[b, iξ]
+ end
+ dΦdx[ix, iξ] = ax
+ end
+ @inbounds for ix in 1:nx, iξ in 1:nξ
+ aξ = zero(eltype(Φ))
+ for b in 1:nξ
+ aξ += Dξ[iξ, b] * Φ[ix, b]
+ end
+ dΦdξ[ix, iξ] = aξ
+ end
+ return cache
+end
+
+# ---------------------------------------------------------------------------
+# apply!(R, term, U, grid, cache) — accumulate the term's contribution.
+# ---------------------------------------------------------------------------
+"""
+ apply!(R, term, U, grid, cache)
+
+Accumulate `term`'s contribution to the residual `R` at state `U` on `grid`,
+using `cache` for scratch. Each `AbstractTerm` defines a method; the assembly in
+[`residual!`](@ref) walks the stack. Allocation-free and generic over `eltype(U)`
+so ForwardDiff duals flow through (verification ladder A2).
+"""
+function apply!(R::IslandState, t::ParallelStreaming, U::IslandState, grid::IslandGrid, ::IslandCache)
+ _add_dxi!(R.g, t.a_xi, U.g, grid.ξ.D1)
+ _add_dx!(R.g, t.a_x, U.g, grid.x.D1)
+ return R
+end
+
+function apply!(R::IslandState, t::MagneticDrift, U::IslandState, grid::IslandGrid, ::IslandCache)
+ _add_dxi!(R.g, t.c_D, U.g, grid.ξ.D1)
+ return R
+end
+
+function apply!(R::IslandState, t::Collisions, U::IslandState, grid::IslandGrid, ::IslandCache)
+ _add_dy!(R.g, t.a_y, U.g, grid.y.D2)
+ _add_dy!(R.g, t.b_y, U.g, grid.y.D1)
+ return R
+end
+
+function apply!(R::IslandState, t::PitchAngleDiffusion, U::IslandState, grid::IslandGrid, ::IslandCache)
+ K = t.K
+ c = t.c
+ g = U.g
+ nx, nξ, ny, nE, nσ = size(g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iξ in 1:nξ, ix in 1:nx
+ cv = c[ix, iξ, iE, iσ]
+ for a in 1:ny
+ acc = zero(eltype(R.g))
+ for b in 1:ny
+ acc += K[a, b] * g[ix, iξ, b, iE, iσ]
+ end
+ R.g[ix, iξ, a, iE, iσ] += cv * acc
+ end
+ end
+ return R
+end
+
+function apply!(R::IslandState, t::GradientDrive, U::IslandState, ::IslandGrid, ::IslandCache)
+ @inbounds @. R.g += t.drive
+ return R
+end
+
+function apply!(R::IslandState, t::PerpTransport, U::IslandState, grid::IslandGrid, ::IslandCache)
+ _add_dx2_scalar!(R.g, t.χ, U.g, grid.x.D2)
+ return R
+end
+
+function apply!(R::IslandState, t::RadiationSink, U::IslandState, ::IslandGrid, ::IslandCache)
+ @inbounds @. R.g += -t.κ * U.g
+ return R
+end
+
+function apply!(R::IslandState, t::ExBDrift, U::IslandState, grid::IslandGrid, cache::IslandCache)
+ _potential_gradients!(cache, U.Φ, grid.x.D1, grid.ξ.D1)
+ dΦdx, dΦdξ = cache.dΦdx, cache.dΦdξ
+ g = U.g
+ Dx, Dξ = grid.x.D1, grid.ξ.D1
+ cE = t.c_E
+ nx, nξ, ny, nE, nσ = size(g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny, iξ in 1:nξ, ix in 1:nx
+ dgdx = zero(eltype(R.g))
+ for b in 1:nx
+ dgdx += Dx[ix, b] * g[b, iξ, iy, iE, iσ]
+ end
+ dgdξ = zero(eltype(R.g))
+ for b in 1:nξ
+ dgdξ += Dξ[iξ, b] * g[ix, b, iy, iE, iσ]
+ end
+ R.g[ix, iξ, iy, iE, iσ] += cE * (dΦdξ[ix, iξ] * dgdx - dΦdx[ix, iξ] * dgdξ)
+ end
+ return R
+end
+
+function apply!(R::IslandState, t::Quasineutrality, U::IslandState, grid::IslandGrid, ::IslandCache)
+ velocity_moment!(R.Φ, U.g, grid; accumulate=true)
+ @inbounds @. R.Φ += -t.α * U.Φ
+ t.source === nothing || @inbounds(@. R.Φ += t.source)
+ return R
+end
+
+# ---------------------------------------------------------------------------
+# Velocity moment M[g](x,ξ) = ∫dy ∫dE Σ_σ g (Simpson in y, Gauss in E).
+# ---------------------------------------------------------------------------
+"""
+ velocity_moment!(M, g, grid; accumulate=false)
+
+Accumulate the phase-space velocity moment `∫dy ∫dE Σ_σ g` into `M[ix, iξ]`
+using the Simpson `y`-weights and Gauss `E`-weights of `grid` (`03 §2`). Set
+`accumulate=true` to add into `M` (residual assembly); otherwise `M` is zeroed
+first.
+"""
+function velocity_moment!(M, g, grid::IslandGrid; accumulate::Bool=false)
+ accumulate || fill!(M, zero(eltype(M)))
+ wy = grid.y.wq
+ wE = grid.E.weights
+ nx, nξ, ny, nE, nσ = size(g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny
+ w = wy[iy] * wE[iE]
+ for iξ in 1:nξ, ix in 1:nx
+ M[ix, iξ] += w * g[ix, iξ, iy, iE, iσ]
+ end
+ end
+ return M
+end
+
+"""
+ weighted_moment!(M, g, W, grid; scale=1, accumulate=false)
+
+Accumulate the weighted velocity moment `scale · ∫dy ∫dE Σ_σ W(y, E, σ) g` into
+`M[ix, iξ]` (`03 §2`, moments). `W` is a supplied `(ny, nE, nσ)` velocity-space
+weight — e.g. the `v̂_∥`-structure of the parallel-flow moment, whose Level-0
+physics form is `[VERIFY]`-gated (QUESTIONS Q3). `scale` carries a per-species
+factor (e.g. the charge `Z_j`). `velocity_moment!` is the `W ≡ 1` special case.
+"""
+function weighted_moment!(M, g, W, grid::IslandGrid; scale=1, accumulate::Bool=false)
+ accumulate || fill!(M, zero(eltype(M)))
+ wy = grid.y.wq
+ wE = grid.E.weights
+ nx, nξ, ny, nE, nσ = size(g)
+ size(W) == (ny, nE, nσ) || throw(ArgumentError("weight W must have size (ny, nE, nσ) = $((ny, nE, nσ))"))
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny
+ w = scale * wy[iy] * wE[iE] * W[iy, iE, iσ]
+ for iξ in 1:nξ, ix in 1:nx
+ M[ix, iξ] += w * g[ix, iξ, iy, iE, iσ]
+ end
+ end
+ return M
+end
+
+# ---------------------------------------------------------------------------
+# Far-field boundary conditions (01 §3, 04 §1): match g and Φ to supplied
+# far-field states at |x| = L_x. NEVER bare Neumann — L23 traced its spurious
+# "winged" solution branch to Neumann non-uniqueness; the physical far field is
+# the neoclassical (no-island) solution, which is [VERIFY]-gated physics and
+# therefore *supplied* here, not computed.
+# ---------------------------------------------------------------------------
+"""
+ FarFieldConditions(g_left, g_right, Φ_left, Φ_right)
+
+Dirichlet-type far-field matching data at the two radial boundaries (`01 §3`):
+`g → g_far` and `Φ̂ → Φ̂_far` as `|x| → L_x`. The residual rows at `ix = 1, nx`
+are replaced by `(U − far)` so the Newton solve pins the far field. The
+neoclassical no-island `g_far` is gated physics (QUESTIONS Q3) — callers supply
+it (tests use manufactured far fields).
+
+## Fields
+
+ - `g_left`, `g_right` — `(nξ, ny, nE, nσ)` far-field distributions at `ix = 1`, `ix = nx`.
+ - `Φ_left`, `Φ_right` — length-`nξ` far-field potentials at `ix = 1`, `ix = nx`.
+"""
+struct FarFieldConditions{T,A4<:AbstractArray{T,4},V<:AbstractVector{T}}
+ g_left::A4
+ g_right::A4
+ Φ_left::V
+ Φ_right::V
+end
+
+"""
+ apply_farfield!(R, U, bc, grid)
+
+Replace the residual rows at the radial boundaries with the far-field matching
+conditions `U − far` (`01 §3`). Called after the operator stack in
+[`residual!`](@ref); allocation-free and AD-transparent.
+"""
+function apply_farfield!(R::IslandState, U::IslandState, bc::FarFieldConditions, grid::IslandGrid)
+ nx, nξ, ny, nE, nσ = size(U.g)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny, iξ in 1:nξ
+ R.g[1, iξ, iy, iE, iσ] = U.g[1, iξ, iy, iE, iσ] - bc.g_left[iξ, iy, iE, iσ]
+ R.g[nx, iξ, iy, iE, iσ] = U.g[nx, iξ, iy, iE, iσ] - bc.g_right[iξ, iy, iE, iσ]
+ end
+ @inbounds for iξ in 1:nξ
+ R.Φ[1, iξ] = U.Φ[1, iξ] - bc.Φ_left[iξ]
+ R.Φ[nx, iξ] = U.Φ[nx, iξ] - bc.Φ_right[iξ]
+ end
+ return R
+end
+
+# ---------------------------------------------------------------------------
+# Stack + residual assembly
+# ---------------------------------------------------------------------------
+"""
+ IslandStack(kinetic, field)
+
+A named configuration (`03 §2`): the tuple of kinetic terms acting on `R.g` and
+the `field` term (`Quasineutrality`) closing `R.Φ`. Stored as a tuple for type
+stability so `residual!` stays allocation-free.
+
+## Fields
+
+ - `kinetic` — tuple of `AbstractTerm`s contributing to the kinetic residual.
+ - `field` — the field-equation term.
+"""
+struct IslandStack{K<:Tuple,F<:AbstractTerm}
+ kinetic::K
+ field::F
+end
+
+"""
+ residual!(R, U, stack, grid, cache)
+
+Assemble the full residual `R = (R_g, R_Φ)` in place: zero `R`, apply every
+kinetic term, then the field term. Allocation-free and AD-transparent.
+"""
+function residual!(R::IslandState, U::IslandState, stack::IslandStack, grid::IslandGrid, cache::IslandCache)
+ fill_state!(R, zero(eltype(R)))
+ _apply_kinetic!(R, stack.kinetic, U, grid, cache)
+ apply!(R, stack.field, U, grid, cache)
+ return R
+end
+
+"""
+ residual!(R, U, stack, grid, cache, bc)
+
+Residual assembly with far-field boundary conditions: assemble the stack, then
+replace the radial-boundary rows with the `FarFieldConditions` matching
+residual (`01 §3`).
+"""
+function residual!(R::IslandState, U::IslandState, stack::IslandStack, grid::IslandGrid, cache::IslandCache, bc::FarFieldConditions)
+ residual!(R, U, stack, grid, cache)
+ apply_farfield!(R, U, bc, grid)
+ return R
+end
+
+# recursive tuple walk keeps the loop type-stable (no dynamic dispatch, no alloc)
+@inline _apply_kinetic!(R, ::Tuple{}, U, grid, cache) = R
+@inline function _apply_kinetic!(R, terms::Tuple, U, grid, cache)
+ apply!(R, terms[1], U, grid, cache)
+ return _apply_kinetic!(R, Base.tail(terms), U, grid, cache)
+end
+
+# ---------------------------------------------------------------------------
+# Flatten / unflatten for the Newton–Krylov / JVP interface
+# ---------------------------------------------------------------------------
+"""
+ statelength(grid)
+
+Number of scalar unknowns in the flattened state (`g` plus `Φ`).
+"""
+function statelength(grid::IslandGrid)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ return nx * nξ * ny * nE * nσ + nx * nξ
+end
+
+"""
+ flatten!(v, U)
+
+Copy state `U` into the flat vector `v` (`g` block then `Φ` block).
+"""
+function flatten!(v, U::IslandState)
+ ng = length(U.g)
+ copyto!(view(v, 1:ng), vec(U.g))
+ copyto!(view(v, (ng+1):(ng+length(U.Φ))), vec(U.Φ))
+ return v
+end
+
+"""
+ unflatten!(U, v)
+
+Copy the flat vector `v` back into state `U`.
+"""
+function unflatten!(U::IslandState, v)
+ ng = length(U.g)
+ copyto!(vec(U.g), view(v, 1:ng))
+ copyto!(vec(U.Φ), view(v, (ng+1):(ng+length(U.Φ))))
+ return U
+end
+
+"""
+ g_flat_index(grid, ix, iξ, iy, iE, iσ)
+
+Flat-state-vector index of `g[ix, iξ, iy, iE, iσ]` under the `flatten!` layout
+(`g` block column-major, then `Φ`). Used by the `y_c`-block conditioning
+monitor (ladder A8) to address pitch-pencil sub-blocks of the dense Jacobian.
+"""
+function g_flat_index(grid::IslandGrid, ix::Int, iξ::Int, iy::Int, iE::Int, iσ::Int)
+ nx, nξ, ny, nE, = nnodes(grid)
+ return ix + nx * ((iξ - 1) + nξ * ((iy - 1) + ny * ((iE - 1) + nE * (iσ - 1))))
+end
+
+"""
+ Φ_flat_index(grid, ix, iξ)
+
+Flat-state-vector index of `Φ[ix, iξ]` under the `flatten!` layout.
+"""
+function Φ_flat_index(grid::IslandGrid, ix::Int, iξ::Int)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ return nx * nξ * ny * nE * nσ + ix + nx * (iξ - 1)
+end
+
+end # module Operators
diff --git a/src/Islands/phasespace/PhaseSpace.jl b/src/Islands/phasespace/PhaseSpace.jl
new file mode 100644
index 000000000..025132fa8
--- /dev/null
+++ b/src/Islands/phasespace/PhaseSpace.jl
@@ -0,0 +1,342 @@
+"""
+ PhaseSpace
+
+Phase-space grids and discretization operators for the Islands drift-kinetic
+solver: the `(x, ξ, λ→y, E, σ)` coordinates of design doc `03 §1` with the
+layer-clustered mappings of `04 §1`.
+
+This module is **pure numerics** — grid node placement, spectral/finite-difference
+differentiation matrices, and quadrature weights. It contains no physics
+coefficients: nothing here carries a `[VERIFY]`/`[CHECKED]` tag, and the
+milestone-M1 discipline (build the discretization, not the physics numbers)
+lives one layer up in `Operators`.
+
+Design-order summary (verified by the MMS ladder A1, `Verify`):
+
+ - `ξ` (helical angle): Fourier pseudo-spectral on the periodic domain `[0, L)`
+ — exponential convergence for smooth periodic data (`04 §1`).
+ - `x` (radial) and `y` (pitch): high-order finite differences on a stretched,
+ layer-clustered grid — algebraic convergence at the requested `order`
+ (`04 §1`; the layer packing targets `x = 0` and `y = y_c = 1`).
+ - `E` (energy): Gauss quadrature on the `F₀`-weighted semi-infinite domain
+ (`04 §1`, Maxwellian weight at Level 0 via Gauss–Laguerre).
+ - `σ = ±1`: the two `sgn(v_∥)` sheets.
+"""
+module PhaseSpace
+
+using LinearAlgebra
+import FastGaussQuadrature
+
+export FourierGrid, MappedFDGrid, GaussGrid, IslandGrid
+export nnodes, differentiate_fourier, fd_weights
+
+# ---------------------------------------------------------------------------
+# Finite-difference weights (Fornberg 1988, Math. Comp. 51, 699) — weights for
+# derivatives 0..m of a function sampled at arbitrary `nodes`, evaluated at x0.
+# Returns `w[j+1, d+1]` = weight of node j for the d-th derivative.
+# ---------------------------------------------------------------------------
+"""
+ fd_weights(m, nodes, x0)
+
+Finite-difference weights for derivatives `0:m` of a function sampled at
+`nodes`, evaluated at `x0`, via Fornberg's recurrence. `w[j, d+1]` multiplies
+`f(nodes[j])` in the approximation of the `d`-th derivative. Exact for
+polynomials up to degree `length(nodes) - 1`.
+"""
+function fd_weights(m::Int, nodes::AbstractVector{<:Real}, x0::Real)
+ n = length(nodes)
+ @assert m < n "need at least m+1 nodes for the m-th derivative"
+ w = zeros(Float64, n, m + 1)
+ c1 = 1.0
+ c4 = nodes[1] - x0
+ w[1, 1] = 1.0
+ for i in 2:n
+ mn = min(i, m + 1)
+ c2 = 1.0
+ c5 = c4
+ c4 = nodes[i] - x0
+ for j in 1:(i-1)
+ c3 = nodes[i] - nodes[j]
+ c2 *= c3
+ if j == i - 1
+ for k in mn:-1:2
+ w[i, k] = c1 * ((k - 1) * w[i-1, k-1] - c5 * w[i-1, k]) / c2
+ end
+ w[i, 1] = -c1 * c5 * w[i-1, 1] / c2
+ end
+ for k in mn:-1:2
+ w[j, k] = (c4 * w[j, k] - (k - 1) * w[j, k-1]) / c3
+ end
+ w[j, 1] = c4 * w[j, 1] / c3
+ end
+ c1 = c2
+ end
+ return w
+end
+
+# ---------------------------------------------------------------------------
+# ξ: Fourier pseudo-spectral, periodic on [0, L).
+# ---------------------------------------------------------------------------
+"""
+ FourierGrid(n; L=2π)
+
+Uniform periodic grid of `n` nodes on `[0, L)` with the Fourier spectral
+first-derivative matrix `D1` (`04 §1`, `ξ` coordinate). `n` must be even.
+
+## Fields
+
+ - `n` — number of nodes.
+ - `L` — period length.
+ - `nodes` — node positions `j·L/n`, `j = 0:n-1`.
+ - `D1` — `n×n` spectral first-derivative matrix.
+"""
+struct FourierGrid
+ n::Int
+ L::Float64
+ nodes::Vector{Float64}
+ D1::Matrix{Float64}
+end
+
+function FourierGrid(n::Int; L::Real=2π)
+ iseven(n) || throw(ArgumentError("FourierGrid needs an even node count (got $n)"))
+ h = 2π / n # computational grid spacing on [0, 2π)
+ nodes = collect((0:(n-1)) .* (L / n))
+ D1 = zeros(Float64, n, n)
+ @inbounds for i in 1:n, j in 1:n
+ if i != j
+ k = i - j
+ D1[i, j] = 0.5 * (-1.0)^k / tan(k * h / 2)
+ end
+ end
+ D1 .*= (2π / L) # rescale d/dξ from [0,2π) to [0,L)
+ return FourierGrid(n, Float64(L), nodes, D1)
+end
+
+"""
+ differentiate_fourier(g, grid)
+
+Spectral first derivative of a periodic sample vector `g` on `grid` (allocating
+convenience wrapper around `grid.D1 * g`; hot paths use the matrix directly).
+"""
+differentiate_fourier(g::AbstractVector, grid::FourierGrid) = grid.D1 * g
+
+# ---------------------------------------------------------------------------
+# x, y: high-order finite differences on a layer-clustered stretched grid.
+#
+# A uniform computational coordinate s ∈ [-1, 1] is mapped to the physical
+# coordinate by a smooth, monotonic, layer-clustering map; physical derivative
+# matrices follow by the chain rule so the FD order is preserved (`04 §1`).
+# ---------------------------------------------------------------------------
+"""
+ MappedFDGrid(n; halfwidth, clustering=0.0, center=0.0, domain=:symmetric, order=4)
+
+High-order finite-difference grid of `n` nodes with layer clustering.
+
+The uniform computational coordinate `s ∈ [-1, 1]` is mapped to the physical
+coordinate by `sinh` stretching (`clustering = β`): points cluster toward the
+map center as `β` grows, and the map degenerates to uniform as `β → 0`. This is
+the design-doc packing that targets the internal layers — `x = 0` (rational
+surface) and `y = y_c = 1` (trapped–passing boundary), `04 §1–2`.
+
+`domain = :symmetric` gives `[center - halfwidth, center + halfwidth]` with
+clustering at `center`; `domain = :half` gives `[0, halfwidth]` with clustering
+at `center` (used for `y ∈ [0, y_max]` packed at `y_c`).
+
+Physical first/second-derivative matrices `D1`, `D2` are built with Fornberg
+weights on windows sized per derivative (`order + d` points for the `d`-th
+derivative, shifted one-sided near the boundaries) so both reach `order`-th
+order accuracy *uniformly*, including at the boundary rows; the chain rule uses
+the analytic map Jacobian `x'(s)` and `x''(s)`.
+
+`wq` holds composite-Simpson quadrature weights on the same nodes (built on the
+uniform computational grid and pushed through the map Jacobian), so
+`∫ f dx ≈ Σ wq[j] f(nodes[j])` at fourth order — matching the FD order for the
+velocity-moment integrals (`03 §2`, moments). Requires an odd `n`.
+
+## Fields
+
+ - `n`, `order` — node count and nominal FD order.
+ - `nodes` — physical node positions.
+ - `D1`, `D2` — `n×n` physical first/second-derivative matrices.
+ - `wq` — composite-Simpson quadrature weights on `nodes`.
+"""
+struct MappedFDGrid
+ n::Int
+ order::Int
+ nodes::Vector{Float64}
+ D1::Matrix{Float64}
+ D2::Matrix{Float64}
+ wq::Vector{Float64}
+end
+
+function MappedFDGrid(n::Int; halfwidth::Real, clustering::Real=0.0, center::Real=0.0,
+ domain::Symbol=:symmetric, order::Int=4)
+ isodd(n) || throw(ArgumentError("MappedFDGrid needs odd n for composite Simpson (got $n)"))
+ # widest window is for D2 (order+2 points); need enough nodes for it.
+ n > order + 2 || throw(ArgumentError("need n > order+2 ($n ≤ $(order + 2))"))
+
+ s = collect(range(-1.0, 1.0; length=n)) # uniform computational grid
+ hw = Float64(halfwidth)
+ β = Float64(clustering)
+
+ # Map s → physical, with analytic first/second derivatives of the map.
+ if domain === :symmetric
+ # x(s) = center + hw·sinh(β s)/sinh(β): monotone, clusters at s=0 ↔ x=center.
+ if abs(β) < 1e-12
+ xs = center .+ hw .* s
+ dxds = fill(hw, n)
+ d2xds2 = zeros(n)
+ else
+ sb = sinh(β)
+ xs = center .+ hw .* sinh.(β .* s) ./ sb
+ dxds = hw .* β .* cosh.(β .* s) ./ sb
+ d2xds2 = hw .* β^2 .* sinh.(β .* s) ./ sb
+ end
+ elseif domain === :half
+ # Map [-1,1] → [0, hw] with clustering at s* ↔ x=center via sinh about s*.
+ # u(s) = sinh(β(s - s*)); x = hw·(u - u(-1))/(u(1) - u(-1)).
+ sstar = clamp(2 * (center / hw) - 1, -1.0, 1.0)
+ if abs(β) < 1e-12
+ xs = hw .* (s .+ 1) ./ 2
+ dxds = fill(hw / 2, n)
+ d2xds2 = zeros(n)
+ else
+ u(z) = sinh(β * (z - sstar))
+ um1 = u(-1.0)
+ span = u(1.0) - um1
+ xs = hw .* (u.(s) .- um1) ./ span
+ dxds = hw .* β .* cosh.(β .* (s .- sstar)) ./ span
+ d2xds2 = hw .* β^2 .* sinh.(β .* (s .- sstar)) ./ span
+ end
+ else
+ throw(ArgumentError("unknown domain $domain (use :symmetric or :half)"))
+ end
+
+ D1s = _fd_matrix(s, 1, order)
+ D2s = _fd_matrix(s, 2, order)
+
+ # Chain rule: d/dx = (1/x')·d/ds ; d²/dx² = (1/x'²)·d²/ds² − (x''/x'³)·d/ds.
+ invp = 1.0 ./ dxds
+ D1 = Diagonal(invp) * D1s
+ D2 = Diagonal(invp .^ 2) * D2s - Diagonal(d2xds2 .* invp .^ 3) * D1s
+
+ # Composite Simpson on uniform s (ds = 2/(n-1)), times the map Jacobian dx/ds.
+ ds = 2.0 / (n - 1)
+ wq = similar(dxds)
+ @inbounds for j in 1:n
+ c = (j == 1 || j == n) ? 1.0 : (iseven(j) ? 4.0 : 2.0)
+ wq[j] = c * ds / 3 * dxds[j]
+ end
+
+ return MappedFDGrid(n, order, xs, Matrix(D1), Matrix(D2), wq)
+end
+
+# Dense derivative matrix for the `deriv`-th derivative on an arbitrary 1D node
+# set at the requested `order`, using windows of `order + deriv` points (the
+# minimum for `order`-th accuracy of the `deriv`-th derivative), rounded up to
+# an odd width so the interior window is centered; near the ends the window is
+# shifted one-sided. Weights are Fornberg's, exact for polynomials up to the
+# window degree.
+function _fd_matrix(nodes::AbstractVector{<:Real}, deriv::Int, order::Int)
+ n = length(nodes)
+ stencil = order + deriv
+ isodd(stencil) || (stencil += 1) # centered interior window needs odd width
+ stencil = min(stencil, n)
+ D = zeros(Float64, n, n)
+ half = stencil ÷ 2
+ @inbounds for i in 1:n
+ lo = clamp(i - half, 1, n - stencil + 1)
+ hi = lo + stencil - 1
+ w = fd_weights(deriv, @view(nodes[lo:hi]), nodes[i])
+ for (col, j) in enumerate(lo:hi)
+ D[i, j] = w[col, deriv+1]
+ end
+ end
+ return D
+end
+
+# ---------------------------------------------------------------------------
+# E: Gauss quadrature on the F₀-weighted semi-infinite energy domain.
+# ---------------------------------------------------------------------------
+"""
+ GaussGrid(n; kind=:laguerre)
+
+Gauss quadrature nodes/weights for velocity-space (energy) integrals over the
+`F₀`-weighted semi-infinite domain (`04 §1`). `kind = :laguerre` gives the
+Level-0 Maxwellian weight `∫₀^∞ f(E) e^{-E} dE ≈ Σ wᵢ f(Eᵢ)` (Gauss–Laguerre);
+the slowing-down weight (Level 2) only changes `kind`, not the machinery.
+
+## Fields
+
+ - `n` — number of quadrature nodes.
+ - `nodes` — abscissae `Eᵢ`.
+ - `weights` — quadrature weights `wᵢ` (the `F₀` weight is folded in).
+"""
+struct GaussGrid
+ n::Int
+ nodes::Vector{Float64}
+ weights::Vector{Float64}
+end
+
+function GaussGrid(n::Int; kind::Symbol=:laguerre)
+ if kind === :laguerre
+ nodes, weights = FastGaussQuadrature.gausslaguerre(n)
+ elseif kind === :legendre
+ nodes, weights = FastGaussQuadrature.gausslegendre(n)
+ else
+ throw(ArgumentError("unknown quadrature kind $kind"))
+ end
+ return GaussGrid(n, collect(nodes), collect(weights))
+end
+
+# ---------------------------------------------------------------------------
+# Bundle: the full Level-0 orbit-averaged 4D phase space (x, ξ, y, E) plus σ.
+# ---------------------------------------------------------------------------
+"""
+ IslandGrid(; nx, nxi, ny, nE, halfwidth_x, clustering_x, y_max, y_c,
+ clustering_y, xi_period=2π, energy_kind=:laguerre, order=4)
+
+The assembled Level-0 phase-space grid: radial `x`, helical `ξ`, pitch `y`,
+energy `E`, and the two `σ = ±1` sheets (`03 §1`). Grids are packed at the
+internal layers (`x = 0`, `y = y_c`) per `04 §1`.
+
+## Fields
+
+ - `x` — radial `MappedFDGrid` (clustered at `x = 0`).
+ - `ξ` — helical `FourierGrid`.
+ - `y` — pitch `MappedFDGrid` on `[0, y_max]` (clustered at `y_c`).
+ - `E` — energy `GaussGrid`.
+ - `σ` — `[+1.0, -1.0]`.
+ - `y_c` — trapped–passing boundary location in `y` (the layer the pitch grid
+ packs toward; consumed by the `y_c`-block conditioning monitor, ladder A8).
+"""
+struct IslandGrid
+ x::MappedFDGrid
+ ξ::FourierGrid
+ y::MappedFDGrid
+ E::GaussGrid
+ σ::Vector{Float64}
+ y_c::Float64
+end
+
+function IslandGrid(; nx::Int, nxi::Int, ny::Int, nE::Int,
+ halfwidth_x::Real, clustering_x::Real=0.0,
+ y_max::Real, y_c::Real=1.0, clustering_y::Real=0.0,
+ xi_period::Real=2π, energy_kind::Symbol=:laguerre, order::Int=4)
+ x = MappedFDGrid(nx; halfwidth=halfwidth_x, clustering=clustering_x, center=0.0,
+ domain=:symmetric, order=order)
+ ξ = FourierGrid(nxi; L=xi_period)
+ y = MappedFDGrid(ny; halfwidth=y_max, clustering=clustering_y, center=y_c,
+ domain=:half, order=order)
+ E = GaussGrid(nE; kind=energy_kind)
+ return IslandGrid(x, ξ, y, E, [1.0, -1.0], Float64(y_c))
+end
+
+"""
+ nnodes(grid::IslandGrid)
+
+Tuple `(nx, nξ, ny, nE, nσ)` of the phase-space grid dimensions.
+"""
+nnodes(g::IslandGrid) = (g.x.n, g.ξ.n, g.y.n, g.E.n, length(g.σ))
+
+end # module PhaseSpace
diff --git a/src/Islands/solvers/Solvers.jl b/src/Islands/solvers/Solvers.jl
new file mode 100644
index 000000000..2b4799cf8
--- /dev/null
+++ b/src/Islands/solvers/Solvers.jl
@@ -0,0 +1,331 @@
+"""
+ Solvers
+
+The Islands steady-state solve (design `03 §3`, `04 §5`, Decision D2):
+matrix-free **Newton–Krylov** — never initial-value time-stepping, never nested
+Picard loops (kokuchou's Picard criterion was *never met* in production; the
+sources' Φ-outer/ū_∥i-inner iteration is the explicit anti-pattern).
+
+Pieces:
+
+ - [`flat_residual`](@ref) — wrap an operator stack (+ optional far-field BCs)
+ as a flat-vector residual `f!(out, u)`, generic over `eltype` with
+ preallocated real and dual workspaces.
+ - [`JVPOperator`](@ref) — the exact Jacobian–vector product via ForwardDiff
+ duals through the stack (`04 §5`); no global sparse Jacobian is ever formed
+ except in [`dense_jacobian`](@ref) tiny-grid debug mode.
+ - [`YBlockJacobi`](@ref) — the physics-block preconditioner skeleton: per-pitch-
+ pencil blocks with **TSVD-regularized** solves, the explicit `y_c`-matching
+ treatment of `04 §3` (GMRES must never resolve the near-singular directions
+ itself).
+ - [`newton_krylov`](@ref) — inexact Newton with Eisenstat–Walker forcing and
+ a backtracking line search.
+ - [`pseudo_arclength`](@ref) — the continuation scaffold with fold detection
+ from day one (`03 §3`: the Level-3 penetration bifurcation is a fold; the
+ solver must step around folds, not fail at them).
+
+Pure numerics — no physics coefficients (the stack it solves carries the gated
+parameters).
+"""
+module Solvers
+
+using LinearAlgebra
+import Krylov
+import ForwardDiff
+import ..PhaseSpace: IslandGrid, nnodes
+import ..Operators: IslandState, IslandCache, IslandStack, FarFieldConditions,
+ residual!, flatten!, unflatten!, statelength
+
+export flat_residual, JVPOperator, dense_jacobian
+export YBlockJacobi, newton_krylov, pseudo_arclength
+
+# Tag for the solver's ForwardDiff duals (one directional derivative).
+struct JVPTag end
+const JVPDual = ForwardDiff.Dual{JVPTag,Float64,1}
+
+# ---------------------------------------------------------------------------
+# Flat residual wrapper
+# ---------------------------------------------------------------------------
+"""
+ flat_residual(stack, grid; bc=nothing)
+
+Wrap `residual!` for `stack` on `grid` (with optional `FarFieldConditions`) as
+a flat-vector function `f!(out, u)` usable by [`newton_krylov`](@ref) and
+[`JVPOperator`](@ref). Real (`Float64`) and dual workspaces are preallocated so
+repeated evaluations allocate nothing.
+"""
+function flat_residual(stack::IslandStack, grid::IslandGrid; bc::Union{Nothing,FarFieldConditions}=nothing)
+ U = IslandState(grid)
+ R = IslandState(grid)
+ cache = IslandCache(grid)
+ Ud = IslandState{JVPDual}(grid)
+ Rd = IslandState{JVPDual}(grid)
+ cached = IslandCache{JVPDual}(grid)
+ function f!(out, u)
+ if eltype(u) === Float64
+ unflatten!(U, u)
+ bc === nothing ? residual!(R, U, stack, grid, cache) : residual!(R, U, stack, grid, cache, bc)
+ flatten!(out, R)
+ else
+ unflatten!(Ud, u)
+ bc === nothing ? residual!(Rd, Ud, stack, grid, cached) : residual!(Rd, Ud, stack, grid, cached, bc)
+ flatten!(out, Rd)
+ end
+ return out
+ end
+ return f!
+end
+
+# ---------------------------------------------------------------------------
+# Matrix-free Jacobian-vector product
+# ---------------------------------------------------------------------------
+"""
+ JVPOperator(f!, u)
+
+Matrix-free linear operator for the Jacobian of `f!` at the point `u` (which is
+**aliased**, so updating `u` in place moves the linearization point): `mul!(y, J, v)` computes the exact directional derivative via one dual-mode evaluation
+of `f!` (`04 §5`). Satisfies the `mul!`/`size`/`eltype` interface Krylov.jl
+expects.
+"""
+struct JVPOperator{F}
+ f!::F
+ u::Vector{Float64}
+ udual::Vector{JVPDual}
+ rdual::Vector{JVPDual}
+end
+
+function JVPOperator(f!, u::Vector{Float64})
+ n = length(u)
+ return JVPOperator(f!, u, Vector{JVPDual}(undef, n), Vector{JVPDual}(undef, n))
+end
+
+Base.size(J::JVPOperator) = (length(J.u), length(J.u))
+Base.size(J::JVPOperator, d::Int) = d <= 2 ? length(J.u) : 1
+Base.eltype(::JVPOperator) = Float64
+
+function LinearAlgebra.mul!(y::AbstractVector, J::JVPOperator, v::AbstractVector)
+ @inbounds for i in eachindex(J.u)
+ J.udual[i] = JVPDual(J.u[i], ForwardDiff.Partials((v[i],)))
+ end
+ J.f!(J.rdual, J.udual)
+ @inbounds for i in eachindex(y)
+ y[i] = ForwardDiff.partials(J.rdual[i])[1]
+ end
+ return y
+end
+
+"""
+ dense_jacobian(f!, u)
+
+Dense Jacobian of `f!` at `u` by column-wise dual sweeps — **tiny-grid debug
+mode only** (`04 §5`): used for eigenvalue/conditioning diagnostics (the
+`y_c`-block singular-value monitor, ladder A8), never in the production solve.
+"""
+function dense_jacobian(f!, u::Vector{Float64})
+ n = length(u)
+ J = zeros(n, n)
+ ud = Vector{JVPDual}(undef, n)
+ rd = Vector{JVPDual}(undef, n)
+ for j in 1:n
+ @inbounds for i in 1:n
+ ud[i] = JVPDual(u[i], ForwardDiff.Partials((i == j ? 1.0 : 0.0,)))
+ end
+ f!(rd, ud)
+ @inbounds for i in 1:n
+ J[i, j] = ForwardDiff.partials(rd[i])[1]
+ end
+ end
+ return J
+end
+
+# ---------------------------------------------------------------------------
+# Physics-block preconditioner skeleton (04 §3, §5)
+# ---------------------------------------------------------------------------
+"""
+ YBlockJacobi(grid, block; svd_cutoff=1e-10, phi_scale=1.0)
+
+Block-Jacobi preconditioner over pitch pencils — the skeleton of the `04 §5`
+physics-block preconditioner. `block(ix, iξ, iE, iσ)` returns the `ny × ny`
+stiff-direction block for that pencil (typically the identity-plus-collisions
+operator); each block is factored by SVD and **truncated below
+`svd_cutoff · σ_max`** — the explicit, regularized `y_c`-matching treatment of
+`04 §3` (kokuchou's rcond ≈ 1e-16 block gave machine-dependent noise under
+plain LU; TSVD is the documented fix). `phi_scale` is the diagonal applied to
+the `Φ` rows. Apply via `ldiv!` (pass as `N=...` with `ldiv=true` to GMRES).
+"""
+struct YBlockJacobi
+ grid::IslandGrid
+ pinvs::Array{Matrix{Float64},4}
+ phi_scale::Float64
+end
+
+function YBlockJacobi(grid::IslandGrid, block; svd_cutoff::Real=1e-10, phi_scale::Real=1.0)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ pinvs = Array{Matrix{Float64},4}(undef, nx, nξ, nE, nσ)
+ for iσ in 1:nσ, iE in 1:nE, iξ in 1:nξ, ix in 1:nx
+ B = Matrix{Float64}(block(ix, iξ, iE, iσ))
+ size(B) == (ny, ny) || throw(ArgumentError("block must be ny×ny = ($ny, $ny)"))
+ F = svd(B)
+ smax = F.S[1]
+ sinv = [s > svd_cutoff * smax ? 1.0 / s : 0.0 for s in F.S]
+ pinvs[ix, iξ, iE, iσ] = F.V * Diagonal(sinv) * F.U'
+ end
+ return YBlockJacobi(grid, pinvs, Float64(phi_scale))
+end
+
+function LinearAlgebra.ldiv!(y::AbstractVector, P::YBlockJacobi, x::AbstractVector)
+ nx, nξ, ny, nE, nσ = nnodes(P.grid)
+ ng = nx * nξ * ny * nE * nσ
+ xg = reshape(view(x, 1:ng), nx, nξ, ny, nE, nσ)
+ yg = reshape(view(y, 1:ng), nx, nξ, ny, nE, nσ)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iξ in 1:nξ, ix in 1:nx
+ Pi = P.pinvs[ix, iξ, iE, iσ]
+ for a in 1:ny
+ acc = 0.0
+ for b in 1:ny
+ acc += Pi[a, b] * xg[ix, iξ, b, iE, iσ]
+ end
+ yg[ix, iξ, a, iE, iσ] = acc
+ end
+ end
+ @inbounds for i in (ng+1):length(x)
+ y[i] = x[i] / P.phi_scale
+ end
+ return y
+end
+
+# ---------------------------------------------------------------------------
+# Inexact Newton-Krylov
+# ---------------------------------------------------------------------------
+"""
+ newton_krylov(f!, u0; rtol=1e-8, atol=1e-10, max_iter=25, precond=nothing,
+ eta0=0.5, eta_max=0.9, gamma=0.9, ls_max=10, memory=30,
+ verbose=false)
+
+Matrix-free inexact Newton–Krylov solve of `f!(out, u) = 0` (design `03 §3`,
+`04 §5`, Decision D2):
+
+ - GMRES (Krylov.jl) on the ForwardDiff [`JVPOperator`](@ref), with the
+ **Eisenstat–Walker** (choice-2) forcing term `η_k = γ(‖F_k‖/‖F_{k−1}‖)²` so
+ early Newton steps do not over-solve the linear system;
+ - a backtracking (Armijo-on-`‖F‖`) line search;
+ - optional right preconditioner `precond` applied via `ldiv!` (e.g.
+ [`YBlockJacobi`](@ref)).
+
+Convergence is declared on **both** the global residual norm and its max-norm
+(`04 §5`: the array-averaged residual hid locally divergent regions in the
+prior art). Returns a NamedTuple `(u, converged, iterations, residual_norms, residual_max, gmres_iters)` — the total Krylov iteration count is a tracked
+preconditioner-quality metric (`04 §5`), not folklore.
+"""
+function newton_krylov(f!, u0::AbstractVector{Float64};
+ rtol::Real=1e-8, atol::Real=1e-10, max_iter::Int=25, precond=nothing,
+ eta0::Real=0.5, eta_max::Real=0.9, gamma::Real=0.9, ls_max::Int=10,
+ memory::Int=30, verbose::Bool=false)
+ u = collect(u0)
+ n = length(u)
+ F = similar(u)
+ f!(F, u)
+ nF = norm(F)
+ nF0 = max(nF, eps())
+ tol = max(atol, rtol * nF0)
+ hist = Float64[nF]
+ J = JVPOperator(f!, u) # aliases u: updates move the linearization point
+ Ftrial = similar(u)
+ utrial = similar(u)
+ eta = float(eta0)
+ nF_prev = nF
+ k = 0
+ gmres_total = 0
+ while nF > tol && k < max_iter
+ k += 1
+ k > 1 && (eta = clamp(gamma * (nF / nF_prev)^2, 1e-4, eta_max))
+ rhs = -F
+ du, stats =
+ precond === nothing ?
+ Krylov.gmres(J, rhs; rtol=eta, atol=0.1 * atol, memory=memory, itmax=10n) :
+ Krylov.gmres(J, rhs; rtol=eta, atol=0.1 * atol, memory=memory, itmax=10n, N=precond, ldiv=true)
+ gmres_total += stats.niter
+ verbose && @info "newton iter $k: ‖F‖=$nF, η=$eta, gmres iters=$(stats.niter)"
+ # backtracking line search on ‖F‖
+ λ = 1.0
+ nFtrial = Inf
+ for _ in 1:ls_max
+ @. utrial = u + λ * du
+ f!(Ftrial, utrial)
+ nFtrial = norm(Ftrial)
+ nFtrial <= (1 - 1e-4 * λ) * nF && break
+ λ /= 2
+ end
+ nF_prev = nF
+ if nFtrial < nF
+ u .= utrial
+ F .= Ftrial
+ nF = nFtrial
+ else
+ verbose && @warn "newton line search stagnated at iter $k"
+ break
+ end
+ push!(hist, nF)
+ end
+ return (u=u, converged=(nF <= tol), iterations=k, residual_norms=hist, residual_max=maximum(abs, F), gmres_iters=gmres_total)
+end
+
+# ---------------------------------------------------------------------------
+# Pseudo-arclength continuation scaffold (03 §3)
+# ---------------------------------------------------------------------------
+"""
+ pseudo_arclength(f!, u0, p0; ds=0.1, nsteps=10, newton_kwargs...)
+
+Pseudo-arclength continuation scaffold for a parameterized residual
+`f!(out, u, p)` (`03 §3`): predictor along the (secant) tangent, corrector =
+[`newton_krylov`](@ref) on the extended system `[F(u, p); t·(z − z_pred)]`, and
+**fold detection from day one** via sign changes of the tangent's `p`-component
+(the Level-3 penetration bifurcation is a fold in this curve). Returns
+`(us, ps, folds)` where `folds` lists the step indices at which `dp/ds`
+reversed.
+"""
+function pseudo_arclength(f!, u0::AbstractVector{Float64}, p0::Real;
+ ds::Real=0.1, nsteps::Int=10, newton_kwargs...)
+ n = length(u0)
+ # converge onto the curve at fixed p0
+ sol = newton_krylov((out, u) -> f!(out, u, p0), collect(u0); newton_kwargs...)
+ sol.converged || error("pseudo_arclength: initial solve at p0 did not converge")
+ us = [copy(sol.u)]
+ ps = [float(p0)]
+ folds = Int[]
+ # initial tangent from du/dp: J du = -dF/dp (finite difference in p)
+ Fp = zeros(n)
+ Fm = zeros(n)
+ hp = max(1e-7, 1e-7 * abs(p0))
+ f!(Fp, sol.u, p0 + hp)
+ f!(Fm, sol.u, p0 - hp)
+ dFdp = (Fp .- Fm) ./ (2hp)
+ Jop = JVPOperator((out, u) -> f!(out, u, p0), copy(sol.u))
+ dudp, _ = Krylov.gmres(Jop, -dFdp; rtol=1e-10)
+ t = vcat(dudp, 1.0)
+ t ./= norm(t)
+ tp_prev = t[end]
+ for step in 1:nsteps
+ zpred = vcat(us[end], ps[end]) .+ ds .* t
+ tloc = copy(t) # freeze tangent for this corrector
+ function ext!(out, z)
+ f!(view(out, 1:n), view(z, 1:n), z[n+1])
+ out[n+1] = LinearAlgebra.dot(tloc, z) - LinearAlgebra.dot(tloc, zpred)
+ return out
+ end
+ sol = newton_krylov(ext!, zpred; newton_kwargs...)
+ sol.converged || break
+ push!(us, sol.u[1:n])
+ push!(ps, sol.u[n+1])
+ # secant tangent for the next step; fold when dp/ds changes sign
+ t = vcat(us[end] .- us[end-1], ps[end] - ps[end-1])
+ t ./= norm(t)
+ if t[end] * tp_prev < 0
+ push!(folds, step)
+ end
+ tp_prev = t[end]
+ end
+ return (us=us, ps=ps, folds=folds)
+end
+
+end # module Solvers
diff --git a/src/Islands/species/Species.jl b/src/Islands/species/Species.jl
new file mode 100644
index 000000000..f19481507
--- /dev/null
+++ b/src/Islands/species/Species.jl
@@ -0,0 +1,170 @@
+"""
+ SpeciesLists
+
+Species as a first-class dimension of the Islands solve (design `02 §1`,
+Decision D3): every kinetic object is indexed by species, and the species list
+is an array from day one even though Level-0 *physics* is single-bulk-ion.
+
+Pure data structures and bookkeeping — no physics coefficients live here.
+Quantities follow the Level-0 normalization (`01 §5`): densities to `n_e`,
+temperatures to `T_i`, masses to the reference bulk ion, gradients as inverse
+scale lengths at `r_s`.
+"""
+module SpeciesLists
+
+export AbstractBackground, Maxwellian, SlowingDown
+export SpeciesRole, Bulk, Trace
+export Species, is_bulk, is_trace, bulk_species, trace_species
+export validate_species, check_trace_criteria
+
+# ---------------------------------------------------------------------------
+# Backgrounds
+# ---------------------------------------------------------------------------
+"""
+ AbstractBackground
+
+Supertype of species background distributions (`02 §1.1`). `Maxwellian` is the
+Level-0 background; `SlowingDown` enters at Level 2 (energetic particles) and
+changes the energy-grid weight map, not the machinery.
+"""
+abstract type AbstractBackground end
+
+"""
+ Maxwellian(; n, T, dlnn_dr, dlnT_dr)
+
+Maxwellian background at the rational surface `r_s` (`02 §1.1`), carrying the
+strictly-local constant gradients of ordering O2.
+
+## Fields
+
+ - `n` — density normalized to `n_e`.
+ - `T` — temperature normalized to `T_i`.
+ - `dlnn_dr` — inverse density scale length `L̂_n⁻¹` at `r_s`.
+ - `dlnT_dr` — inverse temperature scale length `L̂_T⁻¹` at `r_s`.
+"""
+Base.@kwdef struct Maxwellian <: AbstractBackground
+ n::Float64
+ T::Float64
+ dlnn_dr::Float64 = 0.0
+ dlnT_dr::Float64 = 0.0
+end
+
+"""
+ SlowingDown(; S0, v_birth, v_crit, dlnS_dr)
+
+Slowing-down background (isotropic at Level-2 entry, `02 §3`). Present now so
+the background abstraction is exercised from day one; the Level-2 physics that
+consumes it is out of Level-0 scope.
+
+## Fields
+
+ - `S0` — source rate normalization.
+ - `v_birth` — birth speed (normalized to the reference thermal speed).
+ - `v_crit` — critical speed (from `T_e` and composition).
+ - `dlnS_dr` — inverse source scale length at `r_s`.
+"""
+Base.@kwdef struct SlowingDown <: AbstractBackground
+ S0::Float64
+ v_birth::Float64
+ v_crit::Float64
+ dlnS_dr::Float64 = 0.0
+end
+
+# ---------------------------------------------------------------------------
+# Roles and the species struct
+# ---------------------------------------------------------------------------
+"""
+ SpeciesRole
+
+`Bulk` = full nonlinear participant (its `g` enters the Newton state vector).
+`Trace` = linear post-processing pass with frozen bulk fields (`02 §1.2`) —
+one linear solve, an additive contribution to `Δ_cos`/`Δ_sin`.
+"""
+@enum SpeciesRole Bulk Trace
+
+"""
+ Species(; name, Z, m, background, role, collisional_coupling=false)
+
+One species of the Islands solve (`02 §1.1`).
+
+## Fields
+
+ - `name` — identifying symbol (unique within a list).
+ - `Z` — charge number (`Float64` to allow mean-charge-state impurities).
+ - `m` — mass ratio to the reference bulk ion.
+ - `background` — an `AbstractBackground` (Maxwellian at Level 0).
+ - `role` — `Bulk` or `Trace` (`02 §1.2`).
+ - `collisional_coupling` — whether the bulk feels this species' field-particle
+ back-reaction (`02 §1.3`; L1+ physics, plumbing present from day one).
+"""
+Base.@kwdef struct Species{B<:AbstractBackground}
+ name::Symbol
+ Z::Float64
+ m::Float64
+ background::B
+ role::SpeciesRole = Bulk
+ collisional_coupling::Bool = false
+end
+
+is_bulk(sp::Species) = sp.role == Bulk
+is_trace(sp::Species) = sp.role == Trace
+
+"""
+ bulk_species(list) / trace_species(list)
+
+Filter a species list by role.
+"""
+bulk_species(list::AbstractVector{<:Species}) = filter(is_bulk, list)
+trace_species(list::AbstractVector{<:Species}) = filter(is_trace, list)
+
+# ---------------------------------------------------------------------------
+# Validation and the trace-criteria check
+# ---------------------------------------------------------------------------
+"""
+ validate_species(list)
+
+Structural validation of a species list: unique names, positive `Z`-magnitude,
+`m`, density and temperature, and at least one `Bulk` species. Throws
+`ArgumentError` on violation; returns the list for chaining.
+"""
+function validate_species(list::AbstractVector{<:Species})
+ isempty(list) && throw(ArgumentError("species list is empty"))
+ names = [sp.name for sp in list]
+ length(unique(names)) == length(names) || throw(ArgumentError("duplicate species names: $names"))
+ any(is_bulk, list) || throw(ArgumentError("species list needs at least one Bulk species"))
+ for sp in list
+ sp.m > 0 || throw(ArgumentError("species $(sp.name): mass ratio must be positive"))
+ abs(sp.Z) > 0 || throw(ArgumentError("species $(sp.name): charge must be nonzero"))
+ if sp.background isa Maxwellian
+ sp.background.n > 0 || throw(ArgumentError("species $(sp.name): density must be positive"))
+ sp.background.T > 0 || throw(ArgumentError("species $(sp.name): temperature must be positive"))
+ end
+ end
+ return list
+end
+
+"""
+ check_trace_criteria(list; threshold=0.05)
+
+Check every `Trace` species against both trace criteria (`02 §1.2`): charge
+trace (`n_j Z_j ≪ n_e`) and current trace (`n_j ≪ n_e`), with densities already
+normalized to `n_e`. A violating species gets an `@warn` recommending `Bulk`
+promotion — **warn, never silently degrade** (`02 §1.2` promotion rule).
+`threshold` is a diagnostic heuristic knob for "≪", not a physics coefficient.
+Returns the vector of violating species names (empty when clean).
+"""
+function check_trace_criteria(list::AbstractVector{<:Species}; threshold::Real=0.05)
+ violators = Symbol[]
+ for sp in trace_species(list)
+ sp.background isa Maxwellian || continue
+ n = sp.background.n
+ charge_frac = n * abs(sp.Z)
+ if charge_frac > threshold || n > threshold
+ push!(violators, sp.name)
+ @warn "species $(sp.name) violates trace criteria (n Z = $(charge_frac), n = $(n) vs threshold $(threshold)); run it as Bulk" maxlog = 1
+ end
+ end
+ return violators
+end
+
+end # module SpeciesLists
diff --git a/src/Islands/verify/Verify.jl b/src/Islands/verify/Verify.jl
new file mode 100644
index 000000000..a7274e9f4
--- /dev/null
+++ b/src/Islands/verify/Verify.jl
@@ -0,0 +1,616 @@
+"""
+ Verify
+
+The Islands verification harness (`04 §4`, `05 §A`): manufactured-solution (MMS)
+convergence checks and AD-vs-finite-difference JVP checks for the operator
+stack. Callable from the test suite (`test/runtests_islands_*.jl`) and from
+scripts.
+
+**These are structural (pre-physics) checks — ladder A1/A2.** They exercise the
+*discretization order* and the *AD plumbing* using arbitrary, order-unity
+manufactured coefficients. That is deliberate and policy-clean: nothing here is
+a physics coefficient, so nothing here carries a `[VERIFY]` tag. Physics
+benchmarks (ladder B+) live in `benchmarks/islands/` and stay `[VERIFY]`-gated
+until human-cleared.
+
+Design orders checked:
+
+ - `ξ` derivatives — Fourier spectral (a bandlimited manufactured `ξ`-profile is
+ differentiated to machine precision).
+ - `x`, `y` derivatives — high-order finite differences at the grid `order`
+ (default 4): halving the mesh cuts the error by `2^order`.
+ - assembled kinetic residual — the min of the above (algebraic, set by `x`/`y`).
+"""
+module Verify
+
+using LinearAlgebra
+import ForwardDiff
+import ..PhaseSpace: IslandGrid, MappedFDGrid, GaussGrid, nnodes
+import ..Operators: IslandState, IslandCache, IslandStack, residual!, velocity_moment!,
+ apply!, statelength, flatten!, unflatten!, g_flat_index,
+ ParallelStreaming, MagneticDrift, ExBDrift, Collisions, GradientDrive,
+ PerpTransport, RadiationSink, Quasineutrality, FarFieldConditions
+import ..Solvers: flat_residual, newton_krylov
+
+export manufactured_state,
+ test_coefficients, build_stack,
+ mms_operator_error, mms_assembled_error, estimate_order,
+ jvp_fd_maxerror, moment_selfconvergence,
+ term_allocations, residual_allocations,
+ yc_block_sigma_min, solve_mms, zero_drive_setup
+export ladder_status, write_state_dashboard
+export check_anchor_sync
+
+# ---------------------------------------------------------------------------
+# Manufactured solution: smooth, separable, ξ-periodic and bandlimited.
+# Each factor's analytic first/second derivatives are known in closed form,
+# so the continuous value of every operator is exact at the nodes.
+# ---------------------------------------------------------------------------
+_Gx(x) = exp(-x^2 / 2)
+_Gx′(x) = -x * _Gx(x)
+_Gx″(x) = (x^2 - 1) * _Gx(x)
+_Gξ(ξ) = sin(ξ) + 0.5 * cos(2ξ)
+_Gξ′(ξ) = cos(ξ) - sin(2ξ)
+_Gy(y) = exp(-(y - 1)^2 / 2)
+_Gy′(y) = -(y - 1) * _Gy(y)
+_Gy″(y) = ((y - 1)^2 - 1) * _Gy(y)
+_GE(E) = exp(-0.3 * E)
+_Gσ(σ) = 1 + 0.25 * σ
+
+_Px(x) = exp(-x^2 / 4)
+_Px′(x) = -(x / 2) * _Px(x)
+_Pξ(ξ) = cos(ξ)
+_Pξ′(ξ) = -sin(ξ)
+
+# Fill a 5D array over (x, ξ, y, E, σ) from a scalar function of the node coords.
+function _fill5(grid::IslandGrid, f)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ A = Array{Float64}(undef, nx, nξ, ny, nE, nσ)
+ @inbounds for iσ in 1:nσ, iE in 1:nE, iy in 1:ny, iξ in 1:nξ, ix in 1:nx
+ A[ix, iξ, iy, iE, iσ] = f(grid.x.nodes[ix], grid.ξ.nodes[iξ], grid.y.nodes[iy],
+ grid.E.nodes[iE], grid.σ[iσ])
+ end
+ return A
+end
+
+function _fill2(grid::IslandGrid, f)
+ nx, nξ, = nnodes(grid)
+ A = Array{Float64}(undef, nx, nξ)
+ @inbounds for iξ in 1:nξ, ix in 1:nx
+ A[ix, iξ] = f(grid.x.nodes[ix], grid.ξ.nodes[iξ])
+ end
+ return A
+end
+
+"""
+ manufactured_state(grid)
+
+Return `(U, deriv)` where `U::IslandState` holds the manufactured `g*`, `Φ*` on
+`grid` and `deriv` is a NamedTuple of the analytic partial-derivative arrays
+(`dgdx`, `dgdξ`, `dgdy`, `d2gdy2`, `d2gdx2`, `dΦdx`, `dΦdξ`) used to form exact
+continuous operator values.
+"""
+function manufactured_state(grid::IslandGrid)
+ g = _fill5(grid, (x, ξ, y, E, σ) -> _Gx(x) * _Gξ(ξ) * _Gy(y) * _GE(E) * _Gσ(σ))
+ Φ = _fill2(grid, (x, ξ) -> _Px(x) * _Pξ(ξ))
+ U = IslandState(g, Φ)
+ deriv = (
+ dgdx=_fill5(grid, (x, ξ, y, E, σ) -> _Gx′(x) * _Gξ(ξ) * _Gy(y) * _GE(E) * _Gσ(σ)),
+ dgdξ=_fill5(grid, (x, ξ, y, E, σ) -> _Gx(x) * _Gξ′(ξ) * _Gy(y) * _GE(E) * _Gσ(σ)),
+ dgdy=_fill5(grid, (x, ξ, y, E, σ) -> _Gx(x) * _Gξ(ξ) * _Gy′(y) * _GE(E) * _Gσ(σ)),
+ d2gdy2=_fill5(grid, (x, ξ, y, E, σ) -> _Gx(x) * _Gξ(ξ) * _Gy″(y) * _GE(E) * _Gσ(σ)),
+ d2gdx2=_fill5(grid, (x, ξ, y, E, σ) -> _Gx″(x) * _Gξ(ξ) * _Gy(y) * _GE(E) * _Gσ(σ)),
+ dΦdx=_fill2(grid, (x, ξ) -> _Px′(x) * _Pξ(ξ)),
+ dΦdξ=_fill2(grid, (x, ξ) -> _Px(x) * _Pξ′(ξ))
+ )
+ return U, deriv
+end
+
+# ---------------------------------------------------------------------------
+# Test coefficients: arbitrary, smooth, order-unity. NOT physics — they exercise
+# the discretization. `a_y > 0` keeps the pitch operator a sensible diffusion.
+# ---------------------------------------------------------------------------
+"""
+ test_coefficients(grid)
+
+Build a NamedTuple of arbitrary smooth manufactured coefficient arrays/scalars
+for every term (`a_xi`, `a_x`, `c_D`, `a_y`, `b_y`, `drive`, `c_E`, `χ`, `α`).
+These are MMS test values, not physics coefficients.
+"""
+function test_coefficients(grid::IslandGrid)
+ return (
+ a_xi=_fill5(grid, (x, ξ, y, E, σ) -> 1.0 + 0.2 * cos(ξ) + 0.1 * x),
+ a_x=_fill5(grid, (x, ξ, y, E, σ) -> 0.8 + 0.1 * sin(2ξ) + 0.05 * y),
+ c_D=_fill5(grid, (x, ξ, y, E, σ) -> 0.5 * σ * (1 + 0.1 * E)),
+ a_y=_fill5(grid, (x, ξ, y, E, σ) -> 0.3 + 0.05 * x^2),
+ b_y=_fill5(grid, (x, ξ, y, E, σ) -> 0.1 * (y - 1)),
+ drive=_fill5(grid, (x, ξ, y, E, σ) -> 0.2 * exp(-x^2) * cos(ξ)),
+ c_E=0.7,
+ χ=0.15,
+ α=1.3
+ )
+end
+
+# ---------------------------------------------------------------------------
+# Continuous (exact) operator values from the analytic manufactured derivatives.
+# ---------------------------------------------------------------------------
+function _continuous(term::Symbol, deriv, c)
+ if term === :streaming
+ return c.a_xi .* deriv.dgdξ .+ c.a_x .* deriv.dgdx
+ elseif term === :drift
+ return c.c_D .* deriv.dgdξ
+ elseif term === :collisions
+ return c.a_y .* deriv.d2gdy2 .+ c.b_y .* deriv.dgdy
+ elseif term === :perp
+ return c.χ .* deriv.d2gdx2
+ elseif term === :drive
+ return c.drive
+ elseif term === :exb
+ # broadcast the 2D potential gradients over (y, E, σ)
+ dΦdx = reshape(deriv.dΦdx, size(deriv.dΦdx, 1), size(deriv.dΦdx, 2), 1, 1, 1)
+ dΦdξ = reshape(deriv.dΦdξ, size(deriv.dΦdξ, 1), size(deriv.dΦdξ, 2), 1, 1, 1)
+ return c.c_E .* (dΦdξ .* deriv.dgdx .- dΦdx .* deriv.dgdξ)
+ else
+ throw(ArgumentError("unknown term $term"))
+ end
+end
+
+function _term_object(term::Symbol, c)
+ if term === :streaming
+ return ParallelStreaming(c.a_xi, c.a_x)
+ elseif term === :drift
+ return MagneticDrift(c.c_D; variant=:original)
+ elseif term === :collisions
+ return Collisions(c.a_y, c.b_y; model=:pitch_angle)
+ elseif term === :perp
+ return PerpTransport(c.χ)
+ elseif term === :drive
+ return GradientDrive(c.drive)
+ elseif term === :exb
+ return ExBDrift(c.c_E)
+ else
+ throw(ArgumentError("unknown term $term"))
+ end
+end
+
+# ---------------------------------------------------------------------------
+# MMS error drivers
+# ---------------------------------------------------------------------------
+"""
+ mms_operator_error(grid, term)
+
+Max-norm error between the discrete `apply!` of a single kinetic `term`
+(`:streaming`, `:drift`, `:exb`, `:collisions`, `:perp`, `:drive`) on the
+manufactured state and its exact continuous value on `grid`.
+"""
+function mms_operator_error(grid::IslandGrid, term::Symbol)
+ U, deriv = manufactured_state(grid)
+ c = test_coefficients(grid)
+ R = IslandState(grid)
+ cache = IslandCache(grid)
+ apply!(R, _term_object(term, c), U, grid, cache)
+ exact = _continuous(term, deriv, c)
+ return maximum(abs, R.g .- exact)
+end
+
+"""
+ mms_assembled_error(grid; terms=(:streaming,:drift,:exb,:collisions,:perp,:drive))
+
+Max-norm error between the assembled discrete kinetic residual (sum of `terms`)
+on the manufactured state and the summed continuous values.
+"""
+function mms_assembled_error(grid::IslandGrid;
+ terms=(:streaming, :drift, :exb, :collisions, :perp, :drive))
+ U, deriv = manufactured_state(grid)
+ c = test_coefficients(grid)
+ R = IslandState(grid)
+ cache = IslandCache(grid)
+ exact = zeros(size(R.g))
+ for t in terms
+ apply!(R, _term_object(t, c), U, grid, cache)
+ exact .+= _continuous(t, deriv, c)
+ end
+ return maximum(abs, R.g .- exact)
+end
+
+"""
+ estimate_order(errors, refine)
+
+Observed convergence order from a sequence of `errors` at mesh-refinement
+factors `refine` (ratio of successive node counts): `log(eₖ/eₖ₊₁)/log(rₖ)`.
+Returns the vector of per-step orders.
+"""
+function estimate_order(errors::AbstractVector, refine::AbstractVector)
+ return [log(errors[k] / errors[k+1]) / log(refine[k]) for k in 1:(length(errors)-1)]
+end
+
+# ---------------------------------------------------------------------------
+# Assembled stack for the JVP check (includes the E×B nonlinearity + field).
+# ---------------------------------------------------------------------------
+"""
+ build_stack(grid; coeffs=test_coefficients(grid))
+
+Assemble an `IslandStack` with the full Level-0-shaped term set (streaming,
+drift, E×B, collisions, drive) plus the quasineutrality field term, using the
+supplied (manufactured) coefficients. Used by the JVP check.
+"""
+function build_stack(grid::IslandGrid; coeffs=test_coefficients(grid))
+ kinetic = (
+ ParallelStreaming(coeffs.a_xi, coeffs.a_x),
+ MagneticDrift(coeffs.c_D; variant=:original),
+ ExBDrift(coeffs.c_E),
+ Collisions(coeffs.a_y, coeffs.b_y; model=:pitch_angle),
+ GradientDrive(coeffs.drive)
+ )
+ return IslandStack(kinetic, Quasineutrality(coeffs.α))
+end
+
+"""
+ jvp_fd_maxerror(grid; h=1e-6, seed=1)
+
+Compare the forward-mode-AD Jacobian–vector product of the assembled residual
+against a central finite-difference directional derivative, at the manufactured
+state in a deterministic direction. Returns the max-norm difference (ladder A2).
+The residual is nonlinear (E×B bracket + g/Φ coupling), so this is a genuine
+check of both the AD plumbing and its correctness.
+"""
+function jvp_fd_maxerror(grid::IslandGrid; h::Float64=1e-6, seed::Int=1)
+ stack = build_stack(grid)
+ cache_f = IslandCache{Float64}(grid)
+ U, = manufactured_state(grid)
+
+ N = statelength(grid)
+ u0 = Vector{Float64}(undef, N)
+ flatten!(u0, U)
+ # deterministic direction (no RNG dependence)
+ v = Float64[0.5 + 0.5 * sin(seed * k) for k in 1:N]
+ v ./= norm(v)
+
+ # residual as a flat-vector function, generic over eltype
+ function resid_vec!(out, uvec)
+ T = eltype(uvec)
+ Ut = IslandState(reshape(view(uvec, 1:length(U.g)), size(U.g)),
+ reshape(view(uvec, (length(U.g)+1):length(uvec)), size(U.Φ)))
+ Rt = IslandState(reshape(view(out, 1:length(U.g)), size(U.g)),
+ reshape(view(out, (length(U.g)+1):length(out)), size(U.Φ)))
+ cache = IslandCache{T}(grid)
+ residual!(Rt, Ut, stack, grid, cache)
+ return out
+ end
+
+ # AD JVP: d/dε residual(u0 + ε v) |_{ε=0}
+ jvp_ad = ForwardDiff.derivative(0.0) do ε
+ out = Vector{typeof(ε)}(undef, N)
+ resid_vec!(out, u0 .+ ε .* v)
+ end
+
+ # central-difference JVP
+ rp = similar(u0)
+ rm = similar(u0)
+ resid_vec!(rp, u0 .+ h .* v)
+ resid_vec!(rm, u0 .- h .* v)
+ jvp_fd = (rp .- rm) ./ (2h)
+
+ return maximum(abs, jvp_ad .- jvp_fd)
+end
+
+# ---------------------------------------------------------------------------
+# Velocity-moment quadrature self-convergence (Simpson in y, at grid order).
+# ---------------------------------------------------------------------------
+"""
+ moment_selfconvergence(grid_coarse, grid_fine)
+
+Max-norm difference between the velocity moment of the manufactured `g*` on two
+`y`-resolutions. The two grids must share `(nx, nξ)` so the moments live on the
+same `(x, ξ)` grid and compare pointwise; refine `ny` (and/or `nE`) between them
+to probe the `y`-quadrature order.
+"""
+function moment_selfconvergence(grid_coarse::IslandGrid, grid_fine::IslandGrid)
+ nnodes(grid_coarse)[1:2] == nnodes(grid_fine)[1:2] ||
+ throw(ArgumentError("moment_selfconvergence needs grids sharing (nx, nξ)"))
+ Uc, = manufactured_state(grid_coarse)
+ Uf, = manufactured_state(grid_fine)
+ Mc = zeros(nnodes(grid_coarse)[1], nnodes(grid_coarse)[2])
+ Mf = zeros(nnodes(grid_fine)[1], nnodes(grid_fine)[2])
+ velocity_moment!(Mc, Uc.g, grid_coarse)
+ velocity_moment!(Mf, Uf.g, grid_fine)
+ return maximum(abs, Mc .- Mf)
+end
+
+# ---------------------------------------------------------------------------
+# Allocation probes (hot-path allocation regression, `04 §9`).
+# ---------------------------------------------------------------------------
+"""
+ term_allocations(grid, term)
+
+Bytes allocated by a single warmed `apply!` of kinetic `term` on `grid`. Should
+be zero for the allocation-free hot path.
+"""
+function term_allocations(grid::IslandGrid, term::Symbol)
+ c = test_coefficients(grid)
+ U, = manufactured_state(grid)
+ R = IslandState(grid)
+ cache = IslandCache(grid)
+ t = _term_object(term, c)
+ apply!(R, t, U, grid, cache) # warm up compilation
+ return @allocated apply!(R, t, U, grid, cache)
+end
+
+"""
+ residual_allocations(grid; coeffs=test_coefficients(grid))
+
+Bytes allocated by a single warmed full-`residual!` assembly on `grid`. Should
+be zero for the allocation-free hot path.
+"""
+function residual_allocations(grid::IslandGrid; coeffs=test_coefficients(grid))
+ stack = build_stack(grid; coeffs=coeffs)
+ U, = manufactured_state(grid)
+ R = IslandState(grid)
+ cache = IslandCache(grid)
+ residual!(R, U, stack, grid, cache) # warm up compilation
+ return @allocated residual!(R, U, stack, grid, cache)
+end
+
+# ---------------------------------------------------------------------------
+# Solve-level MMS configurations (ladder A1-solve, A5) — the manufactured
+# configurations live here per 04 §4 so tests and benchmark scripts share them.
+# ---------------------------------------------------------------------------
+"""
+ zero_drive_setup(grid)
+
+The ladder-A5 configuration: the manufactured advective stack with **all drives
+off** (no `GradientDrive`, no source), so `g ≡ 0, Φ ≡ 0` is the exact solution
+and the residual there is machine zero (`01 §6`). Returns `(f, N)` — the flat
+residual function and state length — for the null test and the trivial-Newton
+check. The `RadiationSink(−1)` entry is a **unit relaxation shift** (a
+manufactured test coefficient, not physics) making the zero state locally unique.
+"""
+function zero_drive_setup(grid::IslandGrid)
+ c = test_coefficients(grid)
+ shift = fill(-1.0, size(c.drive))
+ kin = (ParallelStreaming(c.a_xi, c.a_x), MagneticDrift(c.c_D; variant=:original),
+ ExBDrift(c.c_E), Collisions(c.a_y, c.b_y; model=:pitch_angle), RadiationSink(shift))
+ stack = IslandStack(kin, Quasineutrality(c.α))
+ return (f=flat_residual(stack, grid), N=statelength(grid))
+end
+
+"""
+ solve_mms(nx; nxi=8, ny=9, nE=2, rtol=1e-10, memory=300)
+
+The assembled **solve-level** MMS (the ladder-A1 extension to a full converged
+Newton–Krylov solve): the advective manufactured stack (streaming + drift +
+E×B + unit relaxation shift + quasineutrality) with far-field matching BCs
+taken from the manufactured state, forced by the *analytic* continuous source
+so the discrete solution differs from `g*` by the discretization error. The
+first-order-in-`x` stack needs only the `x` far-field conditions (the pitch
+operator's degenerate zero-flux structure is exercised separately, ladder A4).
+Returns `(err, converged, iterations, gmres_iters)` where `err` is the max-norm
+solution error against the manufactured state — refining `nx` must show the
+design order.
+"""
+function solve_mms(nx::Int; nxi::Int=8, ny::Int=9, nE::Int=2, rtol::Real=1e-10, memory::Int=300)
+ grid = IslandGrid(; nx=nx, nxi=nxi, ny=ny, nE=nE, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+ c = test_coefficients(grid)
+ shift = fill(-1.0, size(c.drive))
+ kin = (ParallelStreaming(c.a_xi, c.a_x), MagneticDrift(c.c_D; variant=:original),
+ ExBDrift(c.c_E), RadiationSink(shift))
+ stack = IslandStack(kin, Quasineutrality(c.α))
+ Ustar, deriv = manufactured_state(grid)
+ bc = FarFieldConditions(Ustar.g[1, :, :, :, :], Ustar.g[nx, :, :, :, :], Ustar.Φ[1, :], Ustar.Φ[nx, :])
+ f0! = flat_residual(stack, grid; bc=bc)
+ N = statelength(grid)
+ # continuous source: the analytic values of every active term at (g*, Φ*)
+ Sg = c.a_xi .* deriv.dgdξ .+ c.a_x .* deriv.dgdx .+ c.c_D .* deriv.dgdξ .+ Ustar.g
+ dPx = reshape(deriv.dΦdx, nx, nxi, 1, 1, 1)
+ dPξ = reshape(deriv.dΦdξ, nx, nxi, 1, 1, 1)
+ Sg .+= c.c_E .* (dPξ .* deriv.dgdx .- dPx .* deriv.dgdξ)
+ Sg[1, :, :, :, :] .= 0.0
+ Sg[nx, :, :, :, :] .= 0.0 # BC rows carry no source
+ MΦ = zeros(nx, nxi)
+ velocity_moment!(MΦ, Ustar.g, grid)
+ SΦ = MΦ .- c.α .* Ustar.Φ # field rows: discretely consistent source
+ SΦ[1, :] .= 0.0
+ SΦ[nx, :] .= 0.0
+ S = zeros(N)
+ flatten!(S, IslandState(Sg, SΦ))
+ f!(out, u) = (f0!(out, u); out .-= S; out)
+ sol = newton_krylov(f!, zeros(N); rtol=rtol, atol=1e-13, max_iter=30, memory=memory)
+ ustar = zeros(N)
+ flatten!(ustar, Ustar)
+ return (err=maximum(abs, sol.u .- ustar), converged=sol.converged,
+ iterations=sol.iterations, gmres_iters=sol.gmres_iters)
+end
+
+# ---------------------------------------------------------------------------
+# y_c matching-block conditioning monitor (ladder A8, 04 §3).
+# ---------------------------------------------------------------------------
+"""
+ yc_block_sigma_min(J, grid; window=3)
+
+Ladder-A8 conditioning monitor: the smallest singular value of the pitch-pencil
+sub-block of the (tiny-grid debug) Jacobian `J` nearest the trapped–passing
+boundary `y_c`, minimized over all `(x, ξ, E, σ)` pencils. The prior art's
+`y_c` matching system was intrinsically near-singular (rcond ≈ 1e-16, L23 §4.2)
+and produced machine-dependent **noise, not crashes** — so this must be
+*tested for*, not observed. Returns `(sigma_min, pencil)` where `pencil` is the
+`(ix, iξ, iE, iσ)` of the minimizing block.
+"""
+function yc_block_sigma_min(J::AbstractMatrix, grid::IslandGrid; window::Int=3)
+ nx, nξ, ny, nE, nσ = nnodes(grid)
+ window <= ny || throw(ArgumentError("window ($window) exceeds ny ($ny)"))
+ # contiguous y-index window centered on the node nearest y_c
+ ic = argmin(abs.(grid.y.nodes .- grid.y_c))
+ lo = clamp(ic - window ÷ 2, 1, ny - window + 1)
+ iys = lo:(lo+window-1)
+ σmin = Inf
+ pencil = (0, 0, 0, 0)
+ idx = Vector{Int}(undef, window)
+ for iσ in 1:nσ, iE in 1:nE, iξ in 1:nξ, ix in 1:nx
+ for (k, iy) in enumerate(iys)
+ idx[k] = g_flat_index(grid, ix, iξ, iy, iE, iσ)
+ end
+ s = svdvals(J[idx, idx])[end]
+ if s < σmin
+ σmin = s
+ pencil = (ix, iξ, iE, iσ)
+ end
+ end
+ return (sigma_min=σmin, pencil=pencil)
+end
+
+# ---------------------------------------------------------------------------
+# State dashboard generator (docs/07 §1.3): the auto-generated status table.
+# ---------------------------------------------------------------------------
+# The docs/05 ladder as a declarative status spec. `status` is the current
+# reality: the A-ladder (structural) is green via the islands test suite; the
+# B/C physics ladder is gated on the QUESTIONS clearances (never a physics
+# result until human-cleared). This is the artifact the dashboard renders; when
+# benchmark runs archive convergence artifacts, they refine the B/C rows.
+const _LADDER = (
+ (id="A1", tier="structural", target="MMS convergence (ξ spectral; x, y order-4)", status="green", gate="test suite"),
+ (id="A2", tier="structural", target="AD-vs-FD JVP agreement", status="green", gate="test suite"),
+ (id="A3", tier="structural", target="Δ_cos even / Δ_sin odd under ξ-reflection", status="green", gate="test suite"),
+ (id="A4", tier="structural", target="mimetic pitch operator: exact discrete conservation + entropy sign", status="green", gate="test suite"),
+ (id="A5", tier="structural", target="zero-drive null: g≡0 ⇒ residual machine-zero", status="green", gate="test suite"),
+ (id="A7", tier="T1", target="coefficient-free closure identity ⟨∂²h/∂x²⟩_Ω = 0", status="green", gate="test suite"),
+ (id="A8", tier="structural", target="y_c matching-block conditioning monitor", status="green", gate="test suite"),
+ (id="M2c", tier="structural", target="L0 assembly builds + solves structurally (placeholders)", status="green", gate="test suite"),
+ (id="B2", tier="T3", target="large-w scalings Δ_bs+Δ_cur ∝ 1/w, Δ_pol ∝ 1/w³", status="gated", gate="QUESTIONS Q5"),
+ (id="B4", tier="T3", target="Δ_pol ∝ ω_E² + sign-reversal existence", status="gated", gate="QUESTIONS Q3, Q5"),
+ (id="B5a", tier="T3", target="threshold existence w_c ~ O(ρ_θi), :original", status="gated", gate="QUESTIONS Q5"),
+ (id="B5b/E1", tier="T2", target=":original→:improved w_c toggle differential (~×6)", status="gated", gate="QUESTIONS Q5"),
+ (id="B5c", tier="T3", target="ν_★ trend dw_c/dν_★ > 0; w_c ∝ ρ̂_θi", status="gated", gate="QUESTIONS Q5"),
+ (id="B7", tier="T2", target="DK vs RDK cross-check mode", status="gated", gate="QUESTIONS Q5"),
+ (id="C4", tier="T3", target="finite-β/shaping triangularity trend + ε-crossover", status="planned", gate="Level 2")
+)
+
+_status_badge(s) = s == "green" ? "✅ green" : s == "gated" ? "🔒 gated" : s == "planned" ? "⏳ planned" : s
+
+"""
+ ladder_status()
+
+Return the docs/05 verification-ladder status as a vector of NamedTuples
+`(id, tier, target, status, gate)` — the declarative spec the State Dashboard
+renders (docs/07 §1.3). The A-ladder rows are `green` (structural, via the
+islands test suite); the B/C physics rows are `gated`/`planned` on the QUESTIONS
+clearances (no physics result is claimed until human-cleared).
+"""
+ladder_status() = collect(_LADDER)
+
+"""
+ write_state_dashboard(io=stdout; commit="unknown", date="unknown")
+
+Emit the Islands **State Dashboard** (docs/07 §1.3) as Markdown to `io`: the
+docs/05 ladder as a status table (ID, tier, target, status, gate) stamped with
+`commit` and `date`. This is the generator for
+`docs/src/islands/state/STATE.md`; that file is **auto-generated — never
+hand-edit it** (docs/07 §1.3), it regenerates from this function (and, as they
+land, from archived benchmark artifacts). Deterministic given its arguments.
+"""
+function write_state_dashboard(io::IO=stdout; commit::AbstractString="unknown", date::AbstractString="unknown")
+ println(io, "# Islands — State Dashboard")
+ println(io)
+ println(io, "!!! warning \"Auto-generated — do not hand-edit\"")
+ println(io, " Generated by `Islands.Verify.write_state_dashboard` (docs/07 §1.3).")
+ println(io, " Regenerate rather than edit; it refreshes from the ladder spec and,")
+ println(io, " as they land, archived benchmark artifacts.")
+ println(io)
+ println(io, "Snapshot: commit `", commit, "` — ", date, ".")
+ println(io)
+ println(io, "The docs/05 verification ladder. The **A-ladder** (structural, pre-physics)")
+ println(io, "is green via `test/runtests_islands_*.jl`. The **B/C physics ladder** is")
+ println(io, "gated on the `QUESTIONS.md` clearances — no physics result is claimed until")
+ println(io, "human sign-off (the `[VERIFY]` policy, module CLAUDE.md).")
+ println(io)
+ println(io, "| ID | Tier | Target | Status | Gate |")
+ println(io, "|---|---|---|---|---|")
+ for r in _LADDER
+ println(io, "| ", r.id, " | ", r.tier, " | ", r.target, " | ", _status_badge(r.status), " | ", r.gate, " |")
+ end
+ println(io)
+ ngreen = count(r -> r.status == "green", _LADDER)
+ ngated = count(r -> r.status == "gated", _LADDER)
+ nplan = count(r -> r.status == "planned", _LADDER)
+ println(io, "Summary: **", ngreen, " green** (structural), ", ngated, " gated (physics, awaiting clearance), ", nplan, " planned.")
+ println(io)
+ println(io, "Tiers (Decision D9, docs/05 \"Target tiers\"): T1 exact math · T2 internal")
+ println(io, "differentials · T3 scalings/trends/existence · T4 absolute (audit-gated).")
+ return io
+end
+
+# ---------------------------------------------------------------------------
+# Anchor-sync check (docs/07 §1.1, §4.2): the bidirectional operator ↔ docs
+# consistency the CI enforces.
+# ---------------------------------------------------------------------------
+# Resolve a dotted symbol (e.g. "Operators.MagneticDrift") from `root` (Islands),
+# walking submodules. Returns true iff every component is defined.
+function _resolve_symbol(qualified::AbstractString, root::Module)
+ m = root
+ parts = split(qualified, '.')
+ for (i, p) in enumerate(parts)
+ sym = Symbol(p)
+ isdefined(m, sym) || return false
+ i == length(parts) && return true
+ v = getfield(m, sym)
+ v isa Module || return false
+ m = v
+ end
+ return true
+end
+
+# Extract the backtick-quoted symbols from every `Implemented by:` block of a
+# doc — the block runs from the marker to the next blank line, so a wrapped
+# multi-line list is captured whole.
+function _implemented_by_symbols(docfile::AbstractString)
+ text = read(docfile, String)
+ syms = String[]
+ for block in eachmatch(r"Implemented by:(.*?)(?:\n\n|\z)"s, text)
+ for m in eachmatch(r"`([^`]+)`", block.captures[1])
+ push!(syms, String(m.captures[1]))
+ end
+ end
+ return syms
+end
+
+# The AbstractTerm operator names declared in the Operators source. The `<:
+# AbstractTerm` must sit *outside* the type-parameter braces, so a struct that
+# merely carries an `F<:AbstractTerm` type parameter (e.g. `IslandStack`) is not
+# a false match.
+function _operator_names(operators_file::AbstractString)
+ text = read(operators_file, String)
+ return [String(m.captures[1]) for m in eachmatch(r"struct\s+(\w+)(?:\{[^}]*\})?\s*<:\s*AbstractTerm\b", text)]
+end
+
+"""
+ check_anchor_sync(root=parentmodule(@__MODULE__); operators_file, docfiles)
+
+The docs/07 §1.1 bidirectional anchor-sync check, as a pure function CI can gate
+on. Returns `(undocumented, dangling)`:
+
+ - `undocumented` — `AbstractTerm` operators declared in `operators_file` whose
+ name is **not** claimed by any `Implemented by:` marker in `docfiles` (an
+ operator with no as-implemented documentation);
+ - `dangling` — backtick-quoted symbols on an `Implemented by:` line that do
+ **not** resolve to a defined name under `root` (a doc pointing at a
+ nonexistent/renamed symbol).
+
+Both empty ⇒ the operator stack and the as-implemented docs are in sync. `root`
+defaults to the `Islands` module; `operators_file`/`docfiles` default to the
+in-repo paths relative to this source file.
+"""
+function check_anchor_sync(root::Module=parentmodule(@__MODULE__);
+ operators_file::AbstractString=normpath(joinpath(@__DIR__, "..", "operators", "Operators.jl")),
+ docfiles=[normpath(joinpath(@__DIR__, "..", "..", "..", "docs", "src", "islands", "numerics.md"))])
+ implemented = String[]
+ for f in docfiles
+ isfile(f) && append!(implemented, _implemented_by_symbols(f))
+ end
+ implemented_leaves = Set(last(split(s, '.')) for s in implemented)
+ ops = _operator_names(operators_file)
+ undocumented = [op for op in ops if !(op in implemented_leaves)]
+ dangling = [s for s in implemented if !_resolve_symbol(s, root)]
+ return (undocumented=undocumented, dangling=dangling)
+end
+
+end # module Verify
diff --git a/test/runtests.jl b/test/runtests.jl
index 2ae4125ea..a1e642284 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -36,4 +36,8 @@ else
include("./runtests_coils.jl")
include("./runtests_imas.jl")
include("./runtests_rerun_from_h5.jl")
+ include("./runtests_islands_grids.jl")
+ include("./runtests_islands_operators.jl")
+ include("./runtests_islands_solve.jl")
+ include("./runtests_islands_configure.jl")
end
diff --git a/test/runtests_islands_configure.jl b/test/runtests_islands_configure.jl
new file mode 100644
index 000000000..527aa6eef
--- /dev/null
+++ b/test/runtests_islands_configure.jl
@@ -0,0 +1,235 @@
+# runtests_islands_configure.jl
+#
+# Islands M2c — the Level-0 configuration-assembly gates
+# (docs/src/islands/design/03 §2; M2c milestone contract, deliverable #1):
+# - configure_level0 builds a well-formed IslandStack + far-field BCs + Δ prefactors;
+# - the CLEARED coefficients are wired faithfully onto the operator stack
+# (c_D ≡ Coefficients.magnetic_drift_frequency node-for-node; the :improved
+# toggle; the pitch diffusivity/deflection shapes; the Δ prefactors);
+# - the still-gated coefficient families (QUESTIONS Q5) are the supplied inputs;
+# - the assembled residual runs and Newton–Krylov converges STRUCTURALLY on the
+# documented non-physics placeholder config (never a physics result — the
+# gated coefficients are uncleared).
+#
+# STRUCTURAL check: the placeholder gated inputs are explicitly non-physics
+# (Configure.level0_placeholders); a physics run supplies cleared inputs once the
+# Q5 derivation lane clears the remaining coefficient families.
+
+using LinearAlgebra
+using Test
+
+const IslC = GeneralizedPerturbedEquilibrium.Islands
+const PSc = IslC.PhaseSpace
+const Opc = IslC.Operators
+const Soc = IslC.Solvers
+const Coc = IslC.Coefficients
+const Spc = IslC.SpeciesLists
+const Cfg = IslC.Configure
+
+# small physical grid: y_max spans the full pitch domain (forbidden region +
+# y_c layer) so the assembly's robustness there is exercised.
+_grid() = PSc.IslandGrid(; nx=9, nxi=8, ny=9, nE=3, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+
+# ρ̂_θi is order-unity here so the island-streaming a_xi = (inv_Lq/ρ̂_θi)·x stays
+# commensurate with the (structural, non-physics) test domain and the naive
+# Newton–Krylov converges without the physics preconditioner; the cleared
+# coefficient *structure* (the {Ω,g} advection) is ρ̂_θi-independent in form.
+_phys(; variant=:original, model=:chandrasekhar) = Cfg.Level0Physics(; epsilon=0.1,
+ inv_Lq=1.0, inv_LB=1.0, q_s=2.0, dq_dpsi=0.5, w_psi=0.05, mu0_R=1.0, inv_Ln0=1.0,
+ rho_hat_theta_i=1.0, eta_i=0.5, tau=1.0, variant=variant, collision_model=model)
+
+_ion() = [Spc.Species(; name=:i, Z=1.0, m=1.0, background=Spc.Maxwellian(; n=1.0, T=1.0), role=Spc.Bulk)]
+
+@testset "Islands Configure — Level-0 assembly (M2c)" begin
+ grid = _grid()
+ phys = _phys()
+ species = _ion()
+ gated = Cfg.level0_placeholders(grid)
+ cfg = Cfg.configure_level0(grid, phys, species; gated=gated)
+
+ @testset "well-formed stack + provenance" begin
+ @test cfg.stack isa Opc.IslandStack
+ @test length(cfg.stack.kinetic) == 5 # streaming, drift, E×B, collisions, drive
+ @test cfg.stack.field isa Opc.Quasineutrality
+ @test cfg.bc isa Opc.FarFieldConditions
+ # provenance tuples name exactly which coefficients are cleared vs gated
+ @test :magnetic_drift in cfg.cleared
+ @test :delta_prefactors in cfg.cleared
+ @test :quasineutrality in cfg.cleared # closure now wired (01 §3)
+ @test !(:quasineutrality_alpha in cfg.gated) # no longer a structural gap
+ @test :streaming in cfg.cleared # island streaming now wired (01 §2)
+ @test :gradient_drive in cfg.cleared # far-field drive, zero source (01 §2)
+ @test :far_field in cfg.cleared
+ @test :exb in cfg.gated # E×B still gated
+ end
+
+ @testset "gradient drive = zero source + diamagnetic far field (01 §2)" begin
+ nx, nξ, ny, nE, nσ = PSc.nnodes(grid)
+ # I19 Formulation A: the interior GradientDrive source is zero
+ gd = cfg.stack.kinetic[5]
+ @test gd isa Opc.GradientDrive
+ @test all(iszero, gd.drive)
+ # far field g_far = x·L̂_n0⁻¹·[1+(E−3/2)η_i] at the boundaries, Φ_far = 0
+ bc = Cfg.gradient_far_field(grid, phys)
+ xL, xR = grid.x.nodes[1], grid.x.nodes[nx]
+ for iE in 1:nE
+ temp = 1 + (grid.E.nodes[iE] - 1.5) * phys.eta_i
+ @test bc.g_left[2, 3, iE, 1] ≈ xL * phys.inv_Ln0 * temp atol = 1e-12
+ @test bc.g_right[2, 3, iE, 1] ≈ xR * phys.inv_Ln0 * temp atol = 1e-12
+ end
+ @test all(iszero, bc.Φ_left) && all(iszero, bc.Φ_right) # ω_E = 0
+ # isotropic in ξ, y, σ (leading order)
+ @test bc.g_left[1, 1, 2, 1] == bc.g_left[nξ, ny, 2, nσ]
+ end
+
+ @testset "CLEARED c_D wired faithfully vs magnetic_drift_frequency" begin
+ cD = Cfg.drift_coefficient_table(grid, phys)
+ nx, nξ, ny, nE, nσ = PSc.nnodes(grid)
+ y_forbidden = (1 + 0.1) / (1 - 0.1)
+ # every well-defined node equals the direct cleared call, node-for-node
+ for iσ in 1:nσ, iE in 1:nE, iy in 1:ny
+ y = grid.y.nodes[iy]
+ (y >= y_forbidden) && continue # forbidden region ⇒ 0 (no particles)
+ (abs(y - 1.0) < 5e-2) && continue # skip the gated y_c layer
+ v̂ = sqrt(grid.E.nodes[iE])
+ σ = grid.σ[iσ]
+ direct = Coc.magnetic_drift_frequency(; y=y, v_hat=v̂, sigma=σ, epsilon=0.1,
+ inv_Lq=1.0, inv_LB=1.0, variant=:original)
+ @test cD[4, 3, iy, iE, iσ] ≈ direct atol = 1e-12
+ end
+ # forbidden region carries no particles ⇒ c_D ≡ 0
+ for iy in 1:ny
+ if grid.y.nodes[iy] >= y_forbidden
+ @test all(iszero, @view cD[:, :, iy, :, :])
+ end
+ end
+ # σ-odd: reversing the sign of v_∥ flips the drift
+ @test cD[4, 3, 2, 2, 1] ≈ -cD[4, 3, 2, 2, 2] atol = 1e-12
+ end
+
+ @testset ":improved drift toggle zeroes the ∇B term" begin
+ cD_orig = Cfg.drift_coefficient_table(grid, _phys(; variant=:original))
+ cD_imp = Cfg.drift_coefficient_table(grid, _phys(; variant=:improved))
+ iy, iE, iσ = 2, 2, 1
+ y = grid.y.nodes[iy]
+ v̂ = sqrt(grid.E.nodes[iE])
+ σ = grid.σ[iσ]
+ A, G = Coc.orbit_average_drift_brackets(; y=y, epsilon=0.1)
+ @test cD_imp[4, 3, iy, iE, iσ] ≈ (σ * v̂ / 1.1) * (1.0 * A) atol = 1e-10 # LB → 0
+ @test cD_orig[4, 3, iy, iE, iσ] ≈ (σ * v̂ / 1.1) * (1.0 * A - 0.5 * 1.0 * G) atol = 1e-10
+ @test cD_imp[4, 3, iy, iE, iσ] != cD_orig[4, 3, iy, iE, iσ] # the toggle bites
+ end
+
+ @testset "CLEARED collision shapes wired from Coefficients" begin
+ # c is y-independent, energy-dependent = nu_tilde · deflection_frequency(√E)
+ c = Cfg.collision_coefficient(grid, phys, 1.0)
+ nx, nξ, nE, nσ = size(c)
+ for iE in 1:nE
+ v̂ = sqrt(grid.E.nodes[iE])
+ ν = Coc.deflection_frequency(v̂; nu_tilde=1.0, model=:chandrasekhar)
+ @test c[3, 2, iE, 1] ≈ ν atol = 1e-12
+ end
+ # P profile from the cleared pitch_diffusivity, in-domain and nonnegative
+ P, wmeas = Cfg.pitch_diffusivity_profile(grid, gated.B_profile)
+ @test all(>=(0), P)
+ @test all(>(0), wmeas)
+ iy = 4
+ λ = grid.y.nodes[iy]
+ @test P[iy] ≈ Coc.pitch_diffusivity(λ, gated.B_profile[iy]) atol = 1e-12
+ end
+
+ @testset "CLEARED Δ prefactors (symmetric, from delta_moment_prefactors)" begin
+ direct = Coc.delta_moment_prefactors(; mu0_R=1.0, w_psi=0.05, dq_dpsi=0.5, q_s=2.0)
+ @test cfg.delta_prefactors.cos ≈ direct.cos atol = 1e-9
+ @test cfg.delta_prefactors.sin ≈ direct.sin atol = 1e-9
+ @test cfg.delta_prefactors.cos ≈ -cfg.delta_prefactors.sin atol = 1e-9 # symmetric pin
+ end
+
+ @testset "structural solve converges on the placeholder config" begin
+ f! = Soc.flat_residual(cfg.stack, grid; bc=cfg.bc)
+ N = Opc.statelength(grid)
+ sol = Soc.newton_krylov(f!, zeros(N); rtol=1e-8, atol=1e-12, max_iter=30, memory=200)
+ @test sol.converged
+ @test sol.residual_norms[end] < 1e-7
+ # residual is finite everywhere at a nonzero state
+ U = Opc.IslandState(grid)
+ Opc.fill_state!(U, 0.3)
+ R = Opc.IslandState(grid)
+ cache = Opc.IslandCache(grid)
+ Opc.residual!(R, U, cfg.stack, grid, cache, cfg.bc)
+ @test all(isfinite, R.g) && all(isfinite, R.Φ)
+ end
+
+ @testset "island streaming = advection along Ω (the {Ω,g} structure)" begin
+ a_xi, a_x = Cfg.streaming_coefficients(grid, phys)
+ nx, nξ, ny, nE, nσ = PSc.nnodes(grid)
+ w = phys.w_psi
+ pref = phys.inv_Lq * w^2 / (4 * phys.rho_hat_theta_i)
+ for iy in 1:ny
+ Θ = grid.y.nodes[iy] < grid.y_c ? 1.0 : 0.0
+ for iξ in 1:nξ, ix in 1:nx
+ x = grid.x.nodes[ix]
+ ξ = grid.ξ.nodes[iξ]
+ # a_xi must equal pref·Θ·∂ₓΩ = pref·Θ·(4x/w²); a_x = pref·Θ·(−∂_ξΩ) = pref·Θ·(−sinξ)
+ @test a_xi[ix, iξ, iy, 1, 1] ≈ pref * Θ * (4 * x / w^2) atol = 1e-12
+ @test a_x[ix, iξ, iy, 1, 1] ≈ pref * Θ * (-sin(ξ)) atol = 1e-12
+ end
+ end
+ # passing-only: trapped nodes (y ≥ y_c) carry zero streaming
+ itrap = findfirst(>=(grid.y_c), grid.y.nodes)
+ if itrap !== nothing
+ @test all(iszero, @view a_xi[:, :, itrap, :, :])
+ @test all(iszero, @view a_x[:, :, itrap, :, :])
+ end
+ # E, σ independence (broadcast)
+ @test a_xi[3, 2, 2, 1, 1] == a_xi[3, 2, 2, nE, nσ]
+ end
+
+ @testset "quasineutrality drive makes Φ nonzero (the Q5 field fix)" begin
+ # the cleared L̂_{n0}⁻¹(x−ĥ) source drives Φ; without it Φ collapses to 0.
+ S = Cfg.quasineutrality_source(grid, phys)
+ @test any(!iszero, S) # the drive is nontrivial
+ @test cfg.stack.field.source !== nothing # wired into the operator
+ @test cfg.stack.field.α ≈ (phys.tau + 1) / phys.tau # α = (τ+1)/τ, not τ/(τ+1)
+ # solved Φ is nonzero in the interior (boundaries pinned by the far field)
+ f! = Soc.flat_residual(cfg.stack, grid; bc=cfg.bc)
+ N = Opc.statelength(grid)
+ sol = Soc.newton_krylov(f!, zeros(N); rtol=1e-9, atol=1e-13, max_iter=40, memory=200)
+ Usol = Opc.IslandState(grid)
+ Opc.unflatten!(Usol, sol.u)
+ @test maximum(abs, Usol.Φ) > 1e-6 # Φ no longer trivially zero
+ end
+
+ @testset "assembly validates the species list" begin
+ @test_throws ArgumentError Cfg.configure_level0(grid, phys, Spc.Species[]; gated=gated)
+ end
+end
+
+@testset "Islands anchor-sync (docs/07 §1.1)" begin
+ Vfy = IslC.Verify
+ ops_file = normpath(joinpath(@__DIR__, "..", "src", "Islands", "operators", "Operators.jl"))
+
+ @testset "the operator stack and the as-implemented docs are in sync" begin
+ r = Vfy.check_anchor_sync()
+ @test isempty(r.undocumented) # every AbstractTerm operator is documented
+ @test isempty(r.dangling) # every `Implemented by:` symbol resolves
+ end
+
+ @testset "the check CATCHES drift (negative controls)" begin
+ # a doc missing an operator ⇒ that operator is flagged undocumented
+ missing_doc = tempname() * ".md"
+ write(missing_doc, "Implemented by: `Operators.MagneticDrift`.\n")
+ r1 = Vfy.check_anchor_sync(; docfiles=[missing_doc])
+ @test "ParallelStreaming" in r1.undocumented
+ @test isempty(r1.dangling) # the one symbol it names does resolve
+ rm(missing_doc; force=true)
+
+ # a doc naming a nonexistent symbol ⇒ that symbol is flagged dangling
+ bogus_doc = tempname() * ".md"
+ write(bogus_doc, "Implemented by: `Operators.NotARealOperator`.\n")
+ r2 = Vfy.check_anchor_sync(; docfiles=[bogus_doc])
+ @test "Operators.NotARealOperator" in r2.dangling
+ rm(bogus_doc; force=true)
+ end
+end
diff --git a/test/runtests_islands_grids.jl b/test/runtests_islands_grids.jl
new file mode 100644
index 000000000..10dd38336
--- /dev/null
+++ b/test/runtests_islands_grids.jl
@@ -0,0 +1,93 @@
+# runtests_islands_grids.jl
+#
+# Islands module — phase-space grid / discretization unit tests (src/Islands/phasespace).
+# Pure numerics: spectral and finite-difference differentiation and quadrature.
+# No physics coefficients here (nothing [VERIFY]-tagged); these back the MMS
+# ladder A1 checks in runtests_islands_operators.jl.
+
+const PS = GeneralizedPerturbedEquilibrium.Islands.PhaseSpace
+
+@testset "Islands phase-space grids" begin
+
+ @testset "Fourier spectral ∂ξ is exact for bandlimited data" begin
+ fg = PS.FourierGrid(16; L=2π)
+ x = fg.nodes
+ g = @. sin(3x) + cos(2x) - 0.5 * sin(x)
+ dg_exact = @. 3cos(3x) - 2sin(2x) - 0.5cos(x)
+ @test maximum(abs, fg.D1 * g .- dg_exact) < 1e-12
+ # odd node count is rejected
+ @test_throws ArgumentError PS.FourierGrid(15)
+ end
+
+ @testset "Mapped FD converges at design order (uniform grid)" begin
+ # smooth decaying test function on [-6, 6]
+ f(x) = exp(-x^2 / 2)
+ f′(x) = -x * exp(-x^2 / 2)
+ f″(x) = (x^2 - 1) * exp(-x^2 / 2)
+ err1 = Float64[]
+ err2 = Float64[]
+ ns = [17, 33, 65]
+ for n in ns
+ g = PS.MappedFDGrid(n; halfwidth=6.0, order=4)
+ xn = g.nodes
+ push!(err1, maximum(abs, g.D1 * f.(xn) .- f′.(xn)))
+ push!(err2, maximum(abs, g.D2 * f.(xn) .- f″.(xn)))
+ end
+ # 4th-order: halving h cuts error by ≳ 2^4 (allow margin for pre-asymptotics)
+ @test log(err1[2] / err1[3]) / log(ns[3] / ns[2]) > 3.7
+ @test log(err2[2] / err2[3]) / log(ns[3] / ns[2]) > 3.7
+ @test err1[end] < 1e-3
+ @test err2[end] < 1e-3
+ end
+
+ @testset "Layer-clustered map preserves order and packs the center" begin
+ # a strongly clustered grid still differentiates a smooth function accurately
+ g = PS.MappedFDGrid(65; halfwidth=6.0, clustering=2.0, order=4)
+ # node spacing is smallest near the clustering center (x = 0)
+ Δ = diff(g.nodes)
+ icenter = argmin(abs.(g.nodes[1:(end-1)] .+ g.nodes[2:end]) ./ 2)
+ @test Δ[icenter] < Δ[1]
+ @test Δ[icenter] < Δ[end]
+ f(x) = sin(x) * exp(-x^2 / 8)
+ f′(x) = (cos(x) - x / 4 * sin(x)) * exp(-x^2 / 8)
+ @test maximum(abs, g.D1 * f.(g.nodes) .- f′.(g.nodes)) < 1e-3
+ end
+
+ @testset "Half-domain grid packs at y_c and spans [0, y_max]" begin
+ g = PS.MappedFDGrid(17; halfwidth=4.0, clustering=1.0, center=1.0, domain=:half, order=4)
+ @test g.nodes[1] ≈ 0.0 atol = 1e-12
+ @test g.nodes[end] ≈ 4.0 atol = 1e-12
+ @test issorted(g.nodes)
+ end
+
+ @testset "Simpson quadrature weights integrate at design order" begin
+ # ∫_0^4 exp(-(y-1)^2/2) dy — self-convergence (no closed form needed):
+ # successive-refinement differences must shrink at ≳ 4th order.
+ quad(n) =
+ let g = PS.MappedFDGrid(n; halfwidth=4.0, clustering=1.0, center=1.0, domain=:half, order=4)
+ sum(g.wq .* exp.(-(g.nodes .- 1) .^ 2 ./ 2))
+ end
+ q = quad.([17, 33, 65, 129])
+ d1, d2, d3 = abs(q[2] - q[1]), abs(q[3] - q[2]), abs(q[4] - q[3])
+ @test log(d1 / d2) / log(2) > 3.3
+ @test log(d2 / d3) / log(2) > 3.3
+ end
+
+ @testset "Gauss–Laguerre quadrature is exact on polynomials × e^{-E}" begin
+ gg = PS.GaussGrid(6; kind=:laguerre)
+ # ∫_0^∞ E^k e^{-E} dE = k!
+ for (k, want) in ((0, 1.0), (1, 1.0), (2, 2.0), (3, 6.0), (4, 24.0))
+ @test sum(gg.weights .* gg.nodes .^ k) ≈ want rtol = 1e-10
+ end
+ end
+
+ @testset "IslandGrid assembles all five coordinates" begin
+ ig = PS.IslandGrid(; nx=17, nxi=16, ny=9, nE=4, halfwidth_x=6.0, clustering_x=1.5,
+ y_max=4.0, y_c=1.0, clustering_y=1.0)
+ @test PS.nnodes(ig) == (17, 16, 9, 4, 2)
+ @test ig.σ == [1.0, -1.0]
+ @test length(ig.x.wq) == 17
+ # even nx is rejected (Simpson needs odd)
+ @test_throws ArgumentError PS.MappedFDGrid(16; halfwidth=6.0)
+ end
+end
diff --git a/test/runtests_islands_operators.jl b/test/runtests_islands_operators.jl
new file mode 100644
index 000000000..007a3ee77
--- /dev/null
+++ b/test/runtests_islands_operators.jl
@@ -0,0 +1,97 @@
+# runtests_islands_operators.jl
+#
+# Islands operator-stack skeleton — verification ladder A1/A2 (docs/src/islands/design/05).
+# A1 MMS: per-operator and assembled-system convergence at design order.
+# A2 JVP (forward-mode AD) vs. finite-difference residual directional derivative.
+# + allocation regression for the `apply!` / `residual!` hot paths (docs/04 §9).
+#
+# These are STRUCTURAL (pre-physics) checks. The manufactured coefficients they
+# use are arbitrary order-unity test values — not physics — so nothing here is
+# [VERIFY]-gated. Physics benchmarks (ladder B+) stay skipped until human-cleared.
+
+const Isl = GeneralizedPerturbedEquilibrium.Islands
+const V = Isl.Verify
+const PSg = Isl.PhaseSpace
+const Op = Isl.Operators
+
+# nx = ny refined together; ξ bandlimited (nxi small is exact); nE small.
+_grid(n) = PSg.IslandGrid(; nx=n, nxi=8, ny=n, nE=3, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+_order(e_coarse, e_fine, n_coarse, n_fine) = log(e_coarse / e_fine) / log(n_fine / n_coarse)
+
+@testset "Islands operator stack (A1/A2)" begin
+
+ @testset "A1 — per-operator MMS convergence at design order" begin
+ # x/y differential operators: fourth-order finite differences.
+ for term in (:streaming, :exb, :collisions, :perp)
+ e17 = V.mms_operator_error(_grid(17), term)
+ e33 = V.mms_operator_error(_grid(33), term)
+ @test _order(e17, e33, 17, 33) > 3.3
+ @test e33 < e17
+ end
+ # ξ-only operator (magnetic drift): Fourier spectral → machine precision
+ # on the bandlimited manufactured ξ-profile.
+ @test V.mms_operator_error(_grid(17), :drift) < 1e-10
+ # state-independent source is reproduced exactly (no discretization).
+ @test V.mms_operator_error(_grid(17), :drive) < 1e-12
+ end
+
+ @testset "A1 — assembled kinetic-residual MMS convergence" begin
+ e17 = V.mms_assembled_error(_grid(17))
+ e33 = V.mms_assembled_error(_grid(33))
+ @test _order(e17, e33, 17, 33) > 3.3
+ @test e33 < 5e-3
+ end
+
+ @testset "A1 — velocity-moment quadrature convergence (refine y)" begin
+ mky(ny) = PSg.IslandGrid(; nx=9, nxi=8, ny=ny, nE=3, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+ d1 = V.moment_selfconvergence(mky(17), mky(33))
+ d2 = V.moment_selfconvergence(mky(33), mky(65))
+ @test _order(d1, d2, 33, 65) > 3.3
+ # grids must share (nx, nξ)
+ @test_throws ArgumentError V.moment_selfconvergence(mky(17), _grid(17))
+ end
+
+ @testset "A2 — AD JVP matches finite-difference directional derivative" begin
+ # residual is nonlinear (E×B bracket couples g and Φ), so this exercises
+ # the AD plumbing and its correctness together.
+ @test V.jvp_fd_maxerror(_grid(9)) < 1e-6
+ @test V.jvp_fd_maxerror(_grid(9); seed=7) < 1e-6
+ end
+
+ @testset "allocation regression — apply!/residual! are allocation-free" begin
+ g = _grid(9)
+ for term in (:streaming, :drift, :exb, :collisions, :perp, :drive)
+ @test V.term_allocations(g, term) == 0
+ end
+ @test V.residual_allocations(g) == 0
+ end
+
+ @testset "structural — state, stack, flatten/unflatten" begin
+ g = _grid(9)
+ U, = V.manufactured_state(g)
+ @test eltype(U) == Float64
+ @test size(U.g) == PSg.nnodes(g)
+ @test size(U.Φ) == (9, 8)
+
+ # flatten/unflatten round-trips
+ n = Op.statelength(g)
+ @test n == prod(PSg.nnodes(g)) + 9 * 8
+ v = zeros(n)
+ Op.flatten!(v, U)
+ U2 = Op.IslandState(g)
+ Op.unflatten!(U2, v)
+ @test U2.g == U.g && U2.Φ == U.Φ
+
+ # residual! runs and produces a finite, correctly-shaped result
+ stack = V.build_stack(g)
+ R = Op.IslandState(g)
+ cache = Op.IslandCache(g)
+ Op.residual!(R, U, stack, g, cache)
+ @test all(isfinite, R.g) && all(isfinite, R.Φ)
+
+ # the magnetic-drift variant toggle is carried on the term
+ @test Op.MagneticDrift(zeros(1, 1, 1, 1, 1); variant=:improved).variant == :improved
+ end
+end
diff --git a/test/runtests_islands_solve.jl b/test/runtests_islands_solve.jl
new file mode 100644
index 000000000..d8b105965
--- /dev/null
+++ b/test/runtests_islands_solve.jl
@@ -0,0 +1,312 @@
+# runtests_islands_solve.jl
+#
+# Islands M2 — Level-0 solve machinery, structural verification gates
+# (docs/src/islands/design/05 §A; the M2 milestone contract):
+# A5 zero-drive null: g ≡ 0 ⇒ residual = machine zero; Newton converges trivially.
+# A1+ assembled solve-MMS: Newton–Krylov recovers the manufactured state at design order.
+# A8 y_c matching-block smallest-singular-value conditioning monitor.
+# A4 (L0) exact discrete particle conservation + entropy sign of the mimetic
+# pitch-angle collision operator.
+# A3 Δ_cos even / Δ_sin odd parity under ξ-reflection (manufactured J̄_∥).
+# A7 the coefficient-free closure identity ⟨∂²h/∂x²⟩_Ω = 0.
+# + preconditioner quality (GMRES iteration reduction), far-field BC rows,
+# pseudo-arclength fold detection, species/frames plumbing.
+#
+# STRUCTURAL (pre-physics) checks: manufactured order-unity coefficients only.
+# Every physics coefficient in src/ is a [VERIFY]-gated supplied parameter
+# (QUESTIONS Q2–Q4); the York-gate physics benchmarks stay skipped in
+# benchmarks/islands/ until human clearance.
+
+using LinearAlgebra
+
+const IslM2 = GeneralizedPerturbedEquilibrium.Islands
+const PS2 = IslM2.PhaseSpace
+const Op2 = IslM2.Operators
+const So2 = IslM2.Solvers
+const V2 = IslM2.Verify
+const Mo2 = IslM2.Moments
+const Fi2 = IslM2.Fields
+const Sp2 = IslM2.SpeciesLists
+const Fr2 = IslM2.Frames
+
+_sgrid(n; ny=n) = PS2.IslandGrid(; nx=n, nxi=8, ny=ny, nE=2, halfwidth_x=6.0, clustering_x=1.0,
+ y_max=4.0, y_c=1.0, clustering_y=0.8, order=4)
+
+@testset "Islands L0 solve machinery (M2)" begin
+
+ @testset "species plumbing (02 §1, D3)" begin
+ bg = Sp2.Maxwellian(; n=1.0, T=1.0, dlnn_dr=-1.0)
+ ion = Sp2.Species(; name=:D, Z=1.0, m=1.0, background=bg, role=Sp2.Bulk)
+ trc = Sp2.Species(; name=:Dtrace, Z=1.0, m=1.0, background=Sp2.Maxwellian(; n=1e-4, T=1.0), role=Sp2.Trace)
+ @test Sp2.validate_species([ion, trc]) == [ion, trc]
+ @test Sp2.is_bulk(ion) && Sp2.is_trace(trc)
+ @test Sp2.bulk_species([ion, trc]) == [ion]
+ # the L0 test pair: trace deuterium copy of the bulk passes the criteria
+ @test isempty(Sp2.check_trace_criteria([ion, trc]))
+ # a heavy trace at bulk-like density violates and is flagged (warn, never degrade)
+ w = Sp2.Species(; name=:W, Z=40.0, m=92.0, background=Sp2.Maxwellian(; n=0.01, T=1.0), role=Sp2.Trace)
+ @test Sp2.check_trace_criteria([ion, w]) == [:W]
+ # structural validation
+ @test_throws ArgumentError Sp2.validate_species([trc]) # no Bulk
+ @test_throws ArgumentError Sp2.validate_species([ion, ion]) # duplicate names
+ end
+
+ @testset "frames: gated conventions poison, mechanics work (01 §5)" begin
+ conv = Fr2.FrameConvention() # all NaN until human-cleared (Q3)
+ @test !Fr2.is_cleared(conv)
+ @test isnan(Fr2.omega_dia_form(2, 1.0, -1.0, 2.0, conv))
+ @test isnan(Fr2.effective_dlnn_form(-1.0, 1.0, 0.5, conv))
+ # the frame-invariant combination is pure bookkeeping
+ @test Fr2.frame_shift(1.7, 0.4) ≈ 1.3
+ # parameter-vector validation
+ p = Fr2.Level0Parameters(; w_hat=1.0, omega_E_hat=0.0, epsilon=0.1, inv_Lq_hat=1.0, q_s=2.0)
+ @test p.tau == 1.0
+ @test_throws ArgumentError Fr2.Level0Parameters(-1.0, 0.0, 0.1, 1.0, 2.0, 1.0, Dict{Symbol,Float64}())
+ end
+
+ @testset "A4 — mimetic pitch operator: exact conservation + entropy sign" begin
+ g = _sgrid(9)
+ P = @. g.y.nodes * (4.0 - g.y.nodes) + 0.1 # arbitrary positive test profile
+ wm = @. 1.0 + 0.1 * g.y.nodes
+ K, Wq = Op2.conservative_pitch_operator(g.y, P, wm)
+ for gv in (sin.(g.y.nodes) .+ 0.3 .* g.y.nodes .^ 2, exp.(-g.y.nodes), g.y.nodes)
+ @test abs(dot(Wq, K * gv)) < 1e-11 # particle conservation, machine level
+ @test dot(gv .* Wq, K * gv) <= 1e-13 # entropy sign (0 only for constants)
+ end
+ @test abs(dot(ones(g.y.n) .* Wq, K * ones(g.y.n))) < 1e-12 # constants in the null space
+ @test_throws ArgumentError Op2.conservative_pitch_operator(g.y, -P, wm)
+ # the term applies c ⋅ (K g) and is allocation-free
+ nx, nξ, ny, nE, nσ = PS2.nnodes(g)
+ c4 = fill(2.0, nx, nξ, nE, nσ)
+ term = Op2.PitchAngleDiffusion(K, c4)
+ U, = V2.manufactured_state(g)
+ R = Op2.IslandState(g)
+ cache = Op2.IslandCache(g)
+ Op2.apply!(R, term, U, g, cache)
+ @test R.g[3, 2, :, 1, 1] ≈ 2.0 .* (K * U.g[3, 2, :, 1, 1])
+ @test (@allocated Op2.apply!(R, term, U, g, cache)) == 0
+ end
+
+ @testset "A5 — zero-drive null test" begin
+ g = _sgrid(7; ny=7)
+ setup = V2.zero_drive_setup(g)
+ r = ones(setup.N)
+ setup.f(r, zeros(setup.N))
+ @test maximum(abs, r) == 0.0 # residual is EXACTLY machine zero
+ # Newton from a small perturbation falls back to the zero state
+ sol = So2.newton_krylov(setup.f, 1e-3 .* sin.(1:setup.N); rtol=1e-12, atol=1e-12)
+ @test sol.converged
+ @test norm(sol.u) < 1e-9
+ end
+
+ @testset "A1 — assembled solve-MMS at design order" begin
+ r17 = V2.solve_mms(17)
+ r33 = V2.solve_mms(33)
+ @test r17.converged && r33.converged
+ # solution error against the manufactured state converges at design order
+ @test log(r17.err / r33.err) / log(33 / 17) > 3.3
+ @test r33.err < 5e-3
+ end
+
+ @testset "preconditioner: y-block Jacobi with TSVD cuts GMRES iterations (04 §5)" begin
+ g = _sgrid(9)
+ nx, nξ, ny, nE, nσ = PS2.nnodes(g)
+ P = @. g.y.nodes * (4.0 - g.y.nodes) # degenerate endpoints: zero-flux built in
+ K, = Op2.conservative_pitch_operator(g.y, P, ones(ny))
+ cstiff = fill(30.0, nx, nξ, nE, nσ) # stiff collisional pencil
+ shift = fill(-1.0, nx, nξ, ny, nE, nσ)
+ stack = Op2.IslandStack((Op2.PitchAngleDiffusion(K, cstiff), Op2.RadiationSink(shift)),
+ Op2.Quasineutrality(1.3))
+ f0! = So2.flat_residual(stack, g)
+ N = Op2.statelength(g)
+ b = sin.((1:N) ./ 7)
+ f!(out, u) = (f0!(out, u); out .-= b; out)
+ pc = So2.YBlockJacobi(g, (ix, iξ, iE, iσ) -> I(ny) + cstiff[ix, iξ, iE, iσ] .* K; phi_scale=-1.3)
+ s0 = So2.newton_krylov(f!, zeros(N); rtol=1e-10, memory=300)
+ s1 = So2.newton_krylov(f!, zeros(N); rtol=1e-10, memory=300, precond=pc)
+ @test s0.converged && s1.converged
+ @test maximum(abs, s0.u .- s1.u) < 1e-8 # same solution
+ @test s1.gmres_iters < s0.gmres_iters ÷ 2 # preconditioner earns its keep
+ end
+
+ @testset "far-field BCs replace the boundary residual rows (01 §3)" begin
+ g = _sgrid(7; ny=7)
+ nx, nξ, ny, nE, nσ = PS2.nnodes(g)
+ U, = V2.manufactured_state(g)
+ bc = Op2.FarFieldConditions(0.1 .+ zeros(nξ, ny, nE, nσ), 0.2 .+ zeros(nξ, ny, nE, nσ),
+ fill(0.3, nξ), fill(0.4, nξ))
+ stack = V2.build_stack(g)
+ R = Op2.IslandState(g)
+ cache = Op2.IslandCache(g)
+ Op2.residual!(R, U, stack, g, cache, bc)
+ @test R.g[1, 2, 3, 1, 1] ≈ U.g[1, 2, 3, 1, 1] - 0.1
+ @test R.g[nx, 2, 3, 1, 2] ≈ U.g[nx, 2, 3, 1, 2] - 0.2
+ @test R.Φ[1, 5] ≈ U.Φ[1, 5] - 0.3
+ @test R.Φ[nx, 5] ≈ U.Φ[nx, 5] - 0.4
+ # interior rows are untouched by the BC application
+ R2 = Op2.IslandState(g)
+ Op2.residual!(R2, U, stack, g, cache)
+ @test R.g[2:(nx-1), :, :, :, :] == R2.g[2:(nx-1), :, :, :, :]
+ end
+
+ @testset "A8 — y_c matching-block conditioning monitor" begin
+ g = _sgrid(7; ny=7)
+ setup = V2.zero_drive_setup(g)
+ J = So2.dense_jacobian(setup.f, zeros(setup.N))
+ mon = V2.yc_block_sigma_min(J, g)
+ @test isfinite(mon.sigma_min) && mon.sigma_min > 0
+ @test all(mon.pencil .>= 1)
+ # the monitor detects an artificially singularized pencil block (the
+ # silent-noise regression of L23 §4.2 must be *tested for*)
+ idx = [Op2.g_flat_index(g, 1, 1, iy, 1, 1) for iy in 3:5]
+ Jsing = copy(J)
+ Jsing[idx, :] .= 0.0
+ @test V2.yc_block_sigma_min(Jsing, g).sigma_min < 1e-14
+ end
+
+ @testset "A3 — Δ_cos even / Δ_sin odd under ξ-reflection" begin
+ g = _sgrid(9)
+ J = [exp(-x^2) * (2.0 + 1.5 * cos(ξ) + 0.7 * sin(ξ)) for x in g.x.nodes, ξ in g.ξ.nodes]
+ Jr = [exp(-x^2) * (2.0 + 1.5 * cos(-ξ) + 0.7 * sin(-ξ)) for x in g.x.nodes, ξ in g.ξ.nodes]
+ d = Mo2.delta_moments(J, g; prefactor_cos=1.0, prefactor_sin=1.0)
+ dr = Mo2.delta_moments(Jr, g; prefactor_cos=1.0, prefactor_sin=1.0)
+ @test d.Δcos ≈ dr.Δcos atol = 1e-12 # even
+ @test d.Δsin ≈ -dr.Δsin atol = 1e-12 # odd
+ @test abs(d.Δsin) > 0.1 # the projection actually sees the sin part
+ # ξ-projection is spectrally exact: a pure cos ξ current has zero sin moment
+ Jc = [cos(ξ) for x in g.x.nodes, ξ in g.ξ.nodes]
+ @test abs(Mo2.delta_moments(Jc, g; prefactor_cos=1.0, prefactor_sin=1.0).Δsin) < 1e-13
+ end
+
+ @testset "moments: J̄_∥ assembly and Ω-average diagnostics" begin
+ g = _sgrid(9)
+ nx, nξ, ny, nE, nσ = PS2.nnodes(g)
+ U, = V2.manufactured_state(g)
+ W = ones(ny, nE, nσ)
+ ion = Sp2.Species(; name=:D, Z=1.0, m=1.0, background=Sp2.Maxwellian(; n=1.0, T=1.0), role=Sp2.Bulk)
+ anti = Sp2.Species(; name=:A, Z=-1.0, m=1.0, background=Sp2.Maxwellian(; n=1.0, T=1.0), role=Sp2.Bulk)
+ Jp = zeros(nx, nξ)
+ # equal & opposite charges with identical g cancel exactly
+ Mo2.parallel_current!(Jp, [U.g, U.g], [ion, anti], [W, W], g)
+ @test maximum(abs, Jp) < 1e-14
+ # single species with W ≡ 1 reduces to the plain velocity moment
+ Mo2.parallel_current!(Jp, [U.g], [ion], [W], g)
+ Mref = zeros(nx, nξ)
+ Op2.velocity_moment!(Mref, U.g, g)
+ @test Jp ≈ Mref
+ # ⟨1⟩_Ω = 1 outside and inside the separatrix; constant J has no polarization part
+ @test Mo2.omega_average((x, ξ) -> 1.0, 1.5, 1.0) ≈ 1.0 rtol = 1e-8
+ @test Mo2.omega_average((x, ξ) -> 1.0, 0.2, 1.0) ≈ 1.0 rtol = 1e-6
+ cs = Mo2.channel_split((x, ξ) -> 3.0, 2.0, 1.0)
+ @test cs.bs ≈ 3.0 rtol = 1e-8
+ @test abs(cs.pol(0.5, 1.0)) < 1e-8
+ @test Mo2.omega_label(0.0, 0.0, 1.0) ≈ -1.0 # O-point
+ @test Mo2.omega_label(1.0, float(π), 1.0) ≈ 3.0
+ end
+
+ @testset "island_flux_amplitude — cleared ψ̃ = (w_ψ²/4)(q_s'/q_s) relation" begin
+ # cleared physics relation (sign-off 2026-07-11; derivations/psi-tilde-amplitude.md)
+ w, dq, q = 0.3, 0.8, 1.2
+ ψ̃ = Mo2.island_flux_amplitude(; w_psi=w, dq_dpsi=dq, q_s=q)
+ @test ψ̃ ≈ (w^2 / 4) * (dq / q)
+ # round-trip: ψ̃ inverts the half-width relation w_ψ = 2√(ψ̃/|χ₀''|), χ₀'' = q_s'/q_s
+ χ0pp = dq / q
+ @test 2 * sqrt(ψ̃ / χ0pp) ≈ w # recovers the input half-width
+ # scales as w²
+ @test Mo2.island_flux_amplitude(; w_psi=2w, dq_dpsi=dq, q_s=q) ≈ 4 * ψ̃
+ @test_throws ArgumentError Mo2.island_flux_amplitude(; w_psi=w, dq_dpsi=dq, q_s=0.0)
+ end
+
+ @testset "magnetic_drift_frequency — cleared ω̂_D + :original/:improved toggle" begin
+ Co = IslM2.Coefficients
+ # cleared physics (sign-off 2026-07-11; derivations/omega-D-drift-frequency.md)
+ # ε→0 analytic limit: b→1 ⟹ A→√(1−y), G→(2−y)/√(1−y)
+ for y in (0.2, 0.5, 0.8)
+ A, G = Co.orbit_average_drift_brackets(; y=y, epsilon=1e-5)
+ @test A ≈ sqrt(1 - y) rtol = 1e-3
+ @test G ≈ (2 - y) / sqrt(1 - y) rtol = 1e-3
+ end
+ # the :improved toggle forces the L̂_B term to zero
+ kw = (; y=0.5, v_hat=1.2, sigma=1.0, epsilon=0.1, inv_Lq=1.0)
+ ωimp = Co.magnetic_drift_frequency(; kw..., inv_LB=0.7, variant=:improved)
+ ωlb0 = Co.magnetic_drift_frequency(; kw..., inv_LB=0.0, variant=:original)
+ @test ωimp ≈ ωlb0 # :improved == :original with L̂_B⁻¹=0
+ # σ-odd, v̂-linear (the σv̂/(1+ε) prefactor)
+ ωp = Co.magnetic_drift_frequency(; kw..., inv_LB=0.7, variant=:original)
+ ωm = Co.magnetic_drift_frequency(; y=0.5, v_hat=1.2, sigma=-1.0, epsilon=0.1, inv_Lq=1.0, inv_LB=0.7)
+ @test ωp ≈ -ωm
+ @test Co.magnetic_drift_frequency(; y=0.5, v_hat=2.4, sigma=1.0, epsilon=0.1, inv_Lq=1.0, inv_LB=0.7) ≈ 2 * ωp
+ # the toggle is a real, large effect here (grad-B nearly cancels the shear term)
+ @test !isapprox(ωp, ωimp; rtol=0.5)
+ # trapped particles (1 < y < (1+ε)/(1−ε)) give finite brackets; forbidden y rejected
+ At, Gt = Co.orbit_average_drift_brackets(; y=1.1, epsilon=0.1)
+ @test isfinite(At) && isfinite(Gt) && At > 0 && Gt > 0
+ @test_throws ArgumentError Co.orbit_average_drift_brackets(; y=1.5, epsilon=0.1)
+ @test_throws ArgumentError Co.magnetic_drift_frequency(; kw..., inv_LB=0.7, variant=:bogus)
+ end
+
+ @testset "collision operator — cleared diffusivity P(λ) + deflection frequency ν(v̂)" begin
+ Co = IslM2.Coefficients
+ # P(λ) = λ√(1−λB) ≥ 0, vanishing at both endpoints (zero-flux)
+ @test Co.pitch_diffusivity(0.0, 2.0) == 0.0
+ @test Co.pitch_diffusivity(0.5, 2.0) == 0.0 # λ = 1/B endpoint
+ @test Co.pitch_diffusivity(0.25, 2.0) ≈ 0.25 * sqrt(1 - 0.5)
+ @test Co.pitch_diffusivity(0.3, 2.0) > 0
+ @test_throws ArgumentError Co.pitch_diffusivity(0.6, 2.0) # λ > 1/B
+ # deflection frequency: high-v → ν̃/v̂³ (φ−G → 1 − 1/2v̂² + …); low-v → (4/3√π)/v̂²
+ @test Co.deflection_frequency(6.0) * 6.0^3 ≈ 1.0 rtol = 2e-2 # φ−G → 1 (slow 1/2v̂² tail)
+ @test Co.deflection_frequency(30.0) * 30.0^3 ≈ 1.0 rtol = 1e-3 # tighter at larger v̂
+ @test Co.deflection_frequency(0.02) * 0.02^2 ≈ 4 / (3 * sqrt(π)) rtol = 1e-2 # 1/v̂² divergence
+ # the Chandrasekhar form diverges slower than the reduced v̂⁻³ at low v̂
+ @test Co.deflection_frequency(0.05; model=:chandrasekhar) < Co.deflection_frequency(0.05; model=:vcubed)
+ @test Co.deflection_frequency(1.0; model=:vcubed) == 1.0
+ @test_throws ArgumentError Co.deflection_frequency(1.0; model=:bogus)
+ @test_throws ArgumentError Co.deflection_frequency(-1.0)
+ end
+
+ @testset "A7 — flattened-electron identity ⟨∂²h/∂x²⟩_Ω = 0 (coefficient-free)" begin
+ for Ω in (1.2, 2.0, 5.0), pref in (1.0, 3.7) # prefactor-independent
+ @test abs(Fi2.flat_average_d2h_dx2(Ω, 1.0; prefactor=pref)) < 1e-10
+ end
+ @test Fi2.h_profile(0.5; prefactor=1.0) == 0.0 # exactly flat inside the separatrix
+ @test Fi2.h_profile(2.0; prefactor=1.0) > 0.0
+ @test Fi2.Q_omega(3.0) > Fi2.Q_omega(1.5) # Q grows with Ω
+ @test !Fi2.is_cleared(Fi2.ElectronClosure()) # closure constants (k, f_p) stay NaN-gated (Q3)
+ @test isnan(Fi2.ElectronClosure().k_HS)
+ # the h(Ω) amplitude C = w_ψ/2√2 is cleared (sign-off 2026-07-11); feeds h_profile's prefactor
+ Co = IslM2.Coefficients
+ @test Co.h_amplitude(0.3) ≈ 0.3 / (2 * sqrt(2))
+ # far-field: with C = w_ψ/2√2 and Q → √Ω, h = C ∫₁^Ω dΩ'/Q → 2C(√Ω − 1)
+ # = (w_ψ/√2)(√Ω − 1), approaching x = (w_ψ/√2)√Ω (derivation §3)
+ w = 0.4
+ Ω = 400.0
+ @test Fi2.h_profile(Ω; prefactor=Co.h_amplitude(w)) ≈ (w / sqrt(2)) * (sqrt(Ω) - 1) rtol = 2e-2
+ # quasineutrality closure coefficient τ/(τ+1) → 1/2 at τ=1 (cleared 2026-07-11)
+ @test Co.quasineutrality_coefficient(1.0) ≈ 0.5
+ @test Co.quasineutrality_coefficient(2.0) ≈ 2 / 3
+ @test Co.quasineutrality_coefficient(1e6) ≈ 1.0 rtol = 1e-5 # τ → ∞ (cold ions)
+ @test_throws ArgumentError Co.quasineutrality_coefficient(0.0)
+ # passing fraction f_p = 1 − 1.4624√ε (cleared 2026-07-11; = quoted 1.46 to 3 s.f.)
+ @test Co.passing_fraction(0.0) == 1.0 # no trapping at ε=0
+ @test Co.passing_fraction(0.1) ≈ 1 - 1.4624 * sqrt(0.1)
+ @test Co.passing_fraction(0.01) < Co.passing_fraction(0.001) # f_p decreases with ε
+ @test isapprox(1 - Co.passing_fraction(0.1), 1.46 * sqrt(0.1); rtol=2e-3) # matches 1.46
+ @test_throws ArgumentError Co.passing_fraction(-0.1)
+ # Δ-moment prefactors ∓μ₀R/2ψ̃ (cleared 2026-07-11), ψ̃ = (w²/4)(q'/q)
+ pf = Co.delta_moment_prefactors(; mu0_R=3.0, w_psi=0.3, dq_dpsi=0.8, q_s=1.2)
+ ψt = Mo2.island_flux_amplitude(; w_psi=0.3, dq_dpsi=0.8, q_s=1.2)
+ @test pf.cos ≈ -3.0 / (2 * ψt)
+ @test pf.sin ≈ +3.0 / (2 * ψt)
+ @test pf.sin ≈ -pf.cos # symmetric [DERIVED] pin
+ end
+
+ @testset "pseudo-arclength continuation detects the toy fold (03 §3)" begin
+ ftoy!(out, u, p) = (out[1] = u[1]^2 + p; out)
+ pa = So2.pseudo_arclength(ftoy!, [1.0], -1.0; ds=0.3, nsteps=15, rtol=1e-12, atol=1e-12)
+ @test length(pa.ps) > 10 # stepped through, no stall at the fold
+ @test !isempty(pa.folds) # the fold at p = 0 is detected
+ @test maximum(pa.ps) < 0.05 # never steps past the fold parameter
+ # both branches visited: u > 0 before the fold, u < 0 after
+ @test any(z -> z[1] > 0.5, pa.us) && any(z -> z[1] < -0.5, pa.us)
+ end
+end