//! Chapter 4 tuning-resolution vocabulary and the resolver
//! (`core_spec.tex` §"Tuning Systems", `sec:tuning:system`, `:3279` onward),
//! Push 4b tranche 2 (`spec/CONTRACT_PUSH4B_RESOLVER.md`).
//!
//! Like [`crate::pitch_space`] this is a **vertical slice that stays in
//! memory**: [`TuningSystem`], [`TuningResolution`], [`TuningOverride`],
//! [`TuningScope`], the built-in catalog, and [`resolve_pitch_frequency`]
//! (the resolver) land together, with a behavioural proof of life (real
//! frequencies asserted, not `is_ok()`) rather than as three separate
//! passes. **No `Codec` impl exists, or may be added, for anything in this
//! module** (Ruling C, `spec/PLAN_PUSH4B_TUNING.md`): these types are
//! referenced only by id from canonical score state (`ScoreTuningContext`'s
//! wire form stays the three fields it always had — see the hand-written
//! codec in `codec.rs`), so they stay free to change once a later tranche
//! discovers they are wrong.
//!
//! ## What resolves, and what still does not
//!
//! [`TuningResolution`] is a **six**-variant enum in the specification
//! (`core_spec.tex:3309`); this module defines four —
//! [`TuningResolution::EqualTemperament`], [`TuningResolution::PerPositionRatios`],
//! [`TuningResolution::Function`] (Push 4b tranche 2b), and, since Push 4b's
//! adaptive tranche, [`TuningResolution::Adaptive`] (plus [`PositionRatio`],
//! the marker [`TuningParameters`], and the minimal [`HarmonicContext`]). The
//! other two are transcribed only when a built-in needs them, so their
//! unconstructed payload subtrees (`ImportedTuningData`, …) never become an
//! unconsumed type surface (the `NOTEHEAD_ANCHORS` failure): `Overlay` and
//! `Imported` wait on a built-in that needs a split-accidental keyboard or an
//! imported `.scl`/MTS tuning respectively — nothing in the twenty-item
//! catalog constructs either.
//!
//! [`built_in_tuning_system`] resolves **all twenty** catalog identifiers
//! (`req:tuning:builtin-tuning-catalog`): the six `tet-*` equal temperaments,
//! the three `ji-static-5limit-*` just-intonation systems, the ten historical
//! temperaments (`pythagorean`, the three `meantone-*`,
//! `werckmeister-iii`/`-iv`, `vallotti`, `kirnberger-ii`/`-iii`, `young-ii`,
//! Push 4b tranche 2b) — each built from its ratified fifth-tempering
//! construction (`core_spec.tex` §"Temperament Constructions",
//! `:3696`-`4011`) by `temperament_ratios`, never from a pasted cents table —
//! and, finally, `ji-adaptive-5limit`: [`TuningResolution::Adaptive`] bound to
//! the one registered [`crate::pitch::AdaptiveTuningFunctionId`],
//! `"default-v1"`, which is *exactly* `ji_static_5limit_ratios` re-anchored to
//! the harmonic context's tonal centre (C, chromatic position 0, when none is
//! supplied) — the same lattice construction the three static-JI built-ins
//! use, never a second construction or a transcribed cents table. (An earlier
//! draft of this module claimed adaptive resolution "needs `HarmonicContext`,
//! which does not exist in Rust" — that was wrong even at the time: version 1
//! is a pure function of position and anchor pitch class alone, per
//! `req:tuning:adaptive-default-version`, and needed only the one-field
//! `HarmonicContext` this module now defines.)
//!
//! ## The compatibility check, narrowed the same way tranche 1 narrowed it
//!
//! `req:tuning:tuning-system-compatibility` (`:3581`) allows a resolved tuning
//! system's `pitch_space` to differ from the resolved pitch space when a
//! *registered compatibility mapping* declares them compatible. **No such
//! registry exists** in this tranche (matching how tranche 1 left the
//! pitch-space-mapping registry unbuilt, `spec/PLAN_PUSH4B_TUNING.md` Ruling
//! C): [`resolve_pitch_frequency`] accepts only exact `pitch_space` equality
//! and fails closed on any mismatch, a deliberate deferral, not an oversight.

use core::num::NonZeroU32;

use crate::graph::{KeySignature, Score, ScoreTuningContext, StaffInstance};
use crate::ids::{RegionId, StaffId, VoiceId};
use crate::pitch::{
    AcousticRealization, AdaptiveTuningFunctionId, ChromaticPitchClass, Pitch, PitchSpaceId,
    PitchSpacePosition, ReferencePitch, TuningFunctionId, TuningReference, TuningSystemId,
    VoiceSelector,
};
use crate::pitch_space::{built_in_position_structure, JiRatio, PositionStructure};
use crate::time::{EventPosition, MusicalPosition, TimeAnchor};

// ===========================================================================
// Types (Chapter 4 §"Tuning Systems" / §"Score Tuning Context and
// Hierarchical Resolution").
// ===========================================================================

/// One entry of a [`TuningResolution::PerPositionRatios`] catalog
/// (`core_spec.tex:3314-3316`): "Explicit per-position ratios. Each entry is
/// a ratio relative to the reference position." The specification's own
/// listing writes only `PerPositionRatios(Vec<PositionRatio>)` and never
/// spells out `PositionRatio`'s fields — nothing in `core_spec.tex`
/// constructs one but the three `ji-static-5limit-*` built-ins — so this
/// tranche defines it as narrowly as those three need: a chromatic position
/// plus the exact ratio-to-anchor at that position, reusing
/// [`JiRatio`](crate::pitch_space::JiRatio) rather than inventing a second
/// rational type.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct PositionRatio {
    /// The chromatic position this ratio governs: `0..divisions_per_octave`
    /// of the enclosing pitch space's chromatic layer (for the built-ins
    /// below, always `0..12`, `cmn-12`'s chromatic layer).
    pub position: i32,
    /// The exact frequency ratio of `position` relative to the tuning's own
    /// 1/1 (its anchor), octave-reduced into `[1, 2)`.
    pub ratio: JiRatio,
}

/// Placeholder for [`TuningResolution::Function`]'s parameter schema
/// (`core_spec.tex:3318-3324`). The specification never gives this type's
/// shape — Chapter 4's own forward-reference list (`:4083`) singles out the
/// sibling `AdaptiveTuningParameters` as "likewise undefined," for the same
/// reason: no built-in in the twenty-item catalog parameterizes a `Function`
/// resolution, since each of the ten historical temperaments this module
/// builds is fixed entirely by its [`TuningFunctionId`] alone. The same
/// discipline as [`SpellingParameters`](crate::pitch_space::SpellingParameters),
/// one level down: a documented zero-field marker so
/// `TuningResolution::Function`'s field list matches the specification's own
/// listing, carrying no state and inventing no schema.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub struct TuningParameters;

/// Harmonic context supplied to adaptive tuning resolution (`core_spec.tex`
/// §"Adaptive Tuning", `:3397-3428`). **Minimal**, not the four-field
/// listing at `core_spec.tex:3411`
/// (`concurrent: Vec<PitchId>`, `recent: Vec<(PitchId, f64)>`,
/// `key_context: Option<KeyContext>`, `hints: Vec<ContextHint>`). Two of
/// those fields are unimplementable by construction: `key_context` and
/// `hints` are typed on `KeyContext` and `ContextHint`, which the
/// specification leaves undefined on purpose (Forward References,
/// `core_spec.tex:4111`: "defining them now would freeze a type surface on a
/// chapter with no consumer"). The other two, `concurrent` and `recent`, are
/// ignored by version 1 of the one adaptive function this module registers
/// (`req:tuning:adaptive-default-version`: "`concurrent`, `recent`, `hints`,
/// `parameters`, and mode are ignored by version 1") — carrying them here
/// would mint exactly the unconsumed type surface the module doc's
/// `NOTEHEAD_ANCHORS` note warns against. Each field arrives with the first
/// function that actually consumes it.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub struct HarmonicContext {
    /// The active tonal centre as a chromatic pitch class (`0..=11`), if
    /// known. `None` when no tonal centre could be determined (e.g.
    /// [`derive_tonal_centre`] found no applicable key-signature change) —
    /// the resolver then defaults to C (`req:tuning:adaptive-default-version`),
    /// a defined default, never an error.
    pub tonal_centre: Option<ChromaticPitchClass>,
}

/// How a tuning system resolves pitch-space positions to frequencies
/// (`core_spec.tex:3309-3348`). **Deliberately partial**: the specification
/// names six variants; this module defines four. See the module doc for
/// which tranche completes each of the other two.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum TuningResolution {
    /// N-tone equal temperament: each step is the Nth root of the octave
    /// ratio. Includes 12-TET when `divisions_per_octave == 12`.
    EqualTemperament { divisions_per_octave: u16 },
    /// Explicit per-position ratios, each relative to the reference
    /// position.
    PerPositionRatios(Vec<PositionRatio>),
    /// Procedural definition: a registered tuning function that computes
    /// frequencies from a reference (`core_spec.tex:3318-3324`: "Historical
    /// temperaments (Werckmeister, Vallotti, meantone variants) live here").
    /// The ten historical temperaments are reserved built-in
    /// [`TuningFunctionId`]s, each re-derived from its ratified
    /// fifth-tempering construction (`core_spec.tex` §"Temperament
    /// Constructions", `:3696`-`4011`) by `temperament_ratios` rather than
    /// transcribed from a cents table. No parameter schema exists yet
    /// ([`TuningParameters`] is a documented marker); an unreserved
    /// `TuningFunctionId` has no registry to resolve against and fails
    /// closed (`coordinate_ratio` returns `None`) — `Function` is an
    /// extension point, not a registry this module builds.
    Function {
        function: TuningFunctionId,
        parameters: TuningParameters,
    },
    /// Adaptive resolution: frequency from position *plus* [`HarmonicContext`]
    /// (`core_spec.tex:3397-3472`). The one built-in,
    /// `"default-v1"` (`req:tuning:adaptive-default-version`), is a pure
    /// function of (position, anchor pitch class): the position resolves
    /// through `ji_static_5limit_ratios`, transposed so the harmonic
    /// context's tonal centre takes the role of `1/1` (C, chromatic position
    /// 0, when no context or no tonal centre is supplied — a defined
    /// default, not a fallback). An unregistered `function` is a hard error
    /// with no silent fallback, exactly as an unreserved `Function`
    /// [`TuningFunctionId`] fails closed above.
    Adaptive { function: AdaptiveTuningFunctionId },
}

/// A tuning system: a map from pitch-space positions to frequencies, given a
/// reference pitch (`core_spec.tex:3287-3303`).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TuningSystem {
    pub id: TuningSystemId,
    /// Human-readable name.
    pub name: String,
    /// The pitch space whose positions this tuning resolves.
    pub pitch_space: PitchSpaceId,
    /// How the tuning resolves positions to frequencies, given a reference
    /// pitch.
    pub resolution: TuningResolution,
    /// Optional historical or provenance notes.
    pub description: Option<String>,
}

/// The scope a [`TuningOverride`] applies to (`core_spec.tex:3534-3539`).
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum TuningScope {
    Voice(VoiceId),
    Staff(StaffId),
    Region(RegionId),
    Range {
        start: TimeAnchor,
        end: TimeAnchor,
        voices: VoiceSelector,
    },
}

/// A per-scope override of one or more tuning components
/// (`core_spec.tex:3527-3532`). `None` fields inherit from the next-outer
/// scope, per `req:tuning:tuning-resolution-order`.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TuningOverride {
    pub scope: TuningScope,
    pub pitch_space: Option<PitchSpaceId>,
    pub tuning_system: Option<TuningSystemId>,
    pub reference: Option<ReferencePitch>,
}

// ===========================================================================
// The built-in catalog, as data (partial, honestly) — item 2.
// ===========================================================================

/// A built-in catalog lookup result: a real, resolved [`TuningSystem`], or a
/// real catalog identifier (`req:tuning:builtin-tuning-catalog` still
/// requires it to resolve *eventually*) whose resolution some tranche has
/// not yet built. Distinguishing this from "not a built-in identifier at
/// all" ([`built_in_tuning_system`] returning `None`) is what lets
/// [`resolve_pitch_frequency`] report a genuinely unknown identifier and a
/// known-but-deferred one differently, per the contract's "a clear 'not yet
/// supported' error, never a fallback frequency." As of Push 4b's adaptive
/// tranche, [`built_in_tuning_system`] constructs no `Deferred` entry — all
/// twenty catalog identifiers resolve — but the variant stays: a future
/// built-in (the 21st) could still land as a real, honestly-deferred entry
/// rather than a silent guess.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum TuningCatalogEntry {
    Resolved(TuningSystem),
    /// Why this identifier's resolution is deferred, and to what.
    Deferred(&'static str),
}

/// Looks up a built-in [`TuningSystem`] (Chapter 4 §"Built-in Catalog",
/// `core_spec.tex:3656-3694`, `req:tuning:builtin-tuning-catalog`).
///
/// All twenty resolve:
///
/// * the six `tet-*` — [`TuningResolution::EqualTemperament`] from the
///   identifier's divisions. `tet-12` pairs with `cmn-12` (the default
///   pairing, `core_spec.tex:4058-4068`); `tet-19/22/31/53/72` pair with the
///   built-in `edo-19/22/31/53/72` pitch spaces — the only built-in pitch
///   spaces whose [`PositionStructure::Chromatic`] cardinality matches, so
///   the pairing is forced by the catalog rather than chosen;
/// * the three `ji-static-5limit-{C,G,D}` — [`TuningResolution::PerPositionRatios`]
///   computed by `ji_static_5limit_ratios` from the lattice block
///   (`req:tuning:ji-static-construction`), over `cmn-12`'s twelve chromatic
///   positions (`core_spec.tex:4019-4021`: "assigned in ascending order to
///   the twelve chromatic positions of `cmn-12`");
/// * the ten historical temperaments (`pythagorean`, three `meantone-*`,
///   `werckmeister-iii`/`-iv`, `vallotti`, `kirnberger-ii`/`-iii`,
///   `young-ii`, Push 4b tranche 2b) — [`TuningResolution::Function`], each
///   built by `temperament_ratios` from its ratified fifth-tempering
///   construction (`core_spec.tex` §"Temperament Constructions",
///   `:3696`-`4011`), over `cmn-12`'s twelve chromatic positions exactly as
///   the static-JI systems are;
/// * `ji-adaptive-5limit` — [`TuningResolution::Adaptive`] bound to the one
///   registered [`AdaptiveTuningFunctionId`], `"default-v1"`
///   (`req:tuning:adaptive-default-version`), over `cmn-12`'s twelve
///   chromatic positions exactly as the static-JI systems. Resolving it to a
///   frequency needs a harmonic context (or defaults to C without one) — see
///   [`resolve_pitch_frequency`].
///
/// `None` for any other identifier: not one of the twenty at all.
pub fn built_in_tuning_system(id: &TuningSystemId) -> Option<TuningCatalogEntry> {
    fn tet(
        name: &'static str,
        pitch_space: &'static str,
        divisions: u16,
        desc: &'static str,
    ) -> TuningCatalogEntry {
        TuningCatalogEntry::Resolved(TuningSystem {
            id: TuningSystemId::new(name),
            name: name.to_owned(),
            pitch_space: PitchSpaceId::new(pitch_space),
            resolution: TuningResolution::EqualTemperament {
                divisions_per_octave: divisions,
            },
            description: Some(desc.to_owned()),
        })
    }
    fn ji_static(
        name: &'static str,
        anchor_chromatic_degree: i32,
        tonic: &'static str,
    ) -> TuningCatalogEntry {
        TuningCatalogEntry::Resolved(TuningSystem {
            id: TuningSystemId::new(name),
            name: name.to_owned(),
            pitch_space: PitchSpaceId::new("cmn-12"),
            resolution: TuningResolution::PerPositionRatios(ji_static_5limit_ratios(
                anchor_chromatic_degree,
            )),
            description: Some(format!(
                "Static 5-limit just intonation anchored to {tonic} tonic."
            )),
        })
    }
    /// A historical temperament's catalog entry: [`TuningResolution::Function`]
    /// naming `name` as its own [`TuningFunctionId`] (item 3 of
    /// `spec/CONTRACT_PUSH4B_TEMPERAMENTS.md`: "`pitch_space` = `cmn-12`").
    /// `desc` is transcribed from the built-in catalog table
    /// (`core_spec.tex:3669-3679`).
    fn temperament(name: &'static str, desc: &'static str) -> TuningCatalogEntry {
        TuningCatalogEntry::Resolved(TuningSystem {
            id: TuningSystemId::new(name),
            name: name.to_owned(),
            pitch_space: PitchSpaceId::new("cmn-12"),
            resolution: TuningResolution::Function {
                function: TuningFunctionId::new(name),
                parameters: TuningParameters,
            },
            description: Some(desc.to_owned()),
        })
    }
    /// `ji-adaptive-5limit`'s catalog entry: [`TuningResolution::Adaptive`]
    /// bound to the one registered [`AdaptiveTuningFunctionId`],
    /// `"default-v1"` (`req:tuning:adaptive-default-version`), over
    /// `cmn-12`'s twelve chromatic positions exactly as the static-JI
    /// systems.
    fn ji_adaptive() -> TuningCatalogEntry {
        TuningCatalogEntry::Resolved(TuningSystem {
            id: TuningSystemId::new("ji-adaptive-5limit"),
            name: "ji-adaptive-5limit".to_owned(),
            pitch_space: PitchSpaceId::new("cmn-12"),
            resolution: TuningResolution::Adaptive {
                function: AdaptiveTuningFunctionId::new("default-v1"),
            },
            description: Some(
                "Adaptive 5-limit just intonation: the static 5-limit construction, \
                 re-anchored per resolution to the prevailing tonal centre (C by default)."
                    .to_owned(),
            ),
        })
    }
    match id.as_str() {
        "tet-12" => Some(tet(
            "tet-12",
            "cmn-12",
            12,
            "12-tone equal temperament. The default.",
        )),
        "tet-19" => Some(tet("tet-19", "edo-19", 19, "19-tone equal temperament.")),
        "tet-22" => Some(tet("tet-22", "edo-22", 22, "22-tone equal temperament.")),
        "tet-31" => Some(tet("tet-31", "edo-31", 31, "31-tone equal temperament.")),
        "tet-53" => Some(tet("tet-53", "edo-53", 53, "53-tone equal temperament.")),
        "tet-72" => Some(tet("tet-72", "edo-72", 72, "72-tone equal temperament.")),
        "ji-static-5limit-C" => Some(ji_static("ji-static-5limit-C", 0, "C")),
        "ji-static-5limit-G" => Some(ji_static("ji-static-5limit-G", 7, "G")),
        "ji-static-5limit-D" => Some(ji_static("ji-static-5limit-D", 2, "D")),
        "pythagorean" => Some(temperament(
            "pythagorean",
            "Pure-fifth (3:2) Pythagorean tuning.",
        )),
        "meantone-1/4-comma" => Some(temperament(
            "meantone-1/4-comma",
            "Quarter-comma meantone (pure major thirds).",
        )),
        "meantone-1/6-comma" => Some(temperament("meantone-1/6-comma", "Sixth-comma meantone.")),
        "meantone-1/5-comma" => Some(temperament("meantone-1/5-comma", "Fifth-comma meantone.")),
        "werckmeister-iii" => Some(temperament(
            "werckmeister-iii",
            "Werckmeister III well temperament.",
        )),
        "werckmeister-iv" => Some(temperament(
            "werckmeister-iv",
            "Werckmeister IV well temperament.",
        )),
        "vallotti" => Some(temperament("vallotti", "Vallotti well temperament.")),
        "kirnberger-ii" => Some(temperament(
            "kirnberger-ii",
            "Kirnberger II well temperament.",
        )),
        "kirnberger-iii" => Some(temperament(
            "kirnberger-iii",
            "Kirnberger III well temperament.",
        )),
        "young-ii" => Some(temperament(
            "young-ii",
            "Thomas Young's second temperament.",
        )),
        "ji-adaptive-5limit" => Some(ji_adaptive()),
        _ => None,
    }
}

/// The greatest common divisor of two positive integers (Euclid's
/// algorithm), used to keep [`ji_static_5limit_ratios`]'s fractions in
/// lowest terms.
fn gcd(a: i64, b: i64) -> i64 {
    if b == 0 {
        a
    } else {
        gcd(b, a % b)
    }
}

/// Computes the twelve, anchor-relative ratios of the static 5-limit
/// construction (`req:tuning:ji-static-construction`, `core_spec.tex:4015-4024`,
/// read and verified before citing): the lattice block
/// $\{3^a 5^b \mid a \in [-1,2],\ b \in [-1,1]\}$ — twelve cells, generated
/// by its bounds, nothing selected or discarded — octave-reduced into
/// `[1, 2)` and assigned in ascending order starting from the anchor, which
/// takes the role of `1/1`. Computed here in code, in exact integer
/// arithmetic (never a pasted cents or ratio table), exactly the same
/// construction the specification states in prose.
///
/// `anchor_chromatic_degree` is the `cmn-12` chromatic position (0 = C,
/// 7 = G, 2 = D, …) playing the role of `1/1`; the returned table's
/// `position` fields are `(anchor_chromatic_degree + step) mod 12` for the
/// construction's ascending step order, so indexing the result by
/// *chromatic position* (not by lattice step) gives each position's
/// ratio-to-anchor directly.
fn ji_static_5limit_ratios(anchor_chromatic_degree: i32) -> Vec<PositionRatio> {
    let mut cells: Vec<(i64, i64)> = Vec::with_capacity(12);
    for a in -1..=2i32 {
        for b in -1..=1i32 {
            let mut num: i64 = 1;
            let mut den: i64 = 1;
            if a >= 0 {
                num *= 3i64.pow(a as u32);
            } else {
                den *= 3i64.pow((-a) as u32);
            }
            if b >= 0 {
                num *= 5i64.pow(b as u32);
            } else {
                den *= 5i64.pow((-b) as u32);
            }
            // Octave-reduce into [1, 2) by exact integer doubling/halving —
            // never a float comparison, so the reduction cannot introduce
            // rounding error of its own.
            while num >= 2 * den {
                den *= 2;
            }
            while num < den {
                num *= 2;
            }
            let g = gcd(num, den);
            cells.push((num / g, den / g));
        }
    }
    // Sort ascending by value, comparing by cross-multiplication so the
    // ordering is exact (no float division).
    cells.sort_by(|(n1, d1), (n2, d2)| (n1 * d2).cmp(&(n2 * d1)));
    cells
        .into_iter()
        .enumerate()
        .map(|(step, (num, den))| PositionRatio {
            position: (anchor_chromatic_degree + step as i32).rem_euclid(12),
            ratio: JiRatio {
                numerator: num as i32,
                denominator: NonZeroU32::new(den as u32)
                    .expect("an octave-reduced denominator is a positive power of two times an odd factor, never zero"),
            },
        })
        .collect()
}

// ===========================================================================
// The ten historical temperaments (item 2 of
// `spec/CONTRACT_PUSH4B_TEMPERAMENTS.md`): the circle-of-fifths walk and the
// ten ratified constructions it is walked over.
// ===========================================================================

/// The pure 3/2 fifth, the Pythagorean comma, the syntonic comma, and the
/// schisma, each as `1200 · log2(exact ratio)` — never a hardcoded rounded
/// cents value (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 2 and "Do not":
/// "Hardcode a rounded comma value"). `f64` throughout is correct here:
/// these feed only non-canonical frequencies
/// (`req:determinism:canonical-floating-point` binds *stored* canonical
/// floats, not values computed and discarded in memory — read and confirmed
/// before citing), and several fifths in the walk below (meantone's 1/5- and
/// 1/6-comma variants) are irrational by construction, so there is no
/// exact-rational alternative to begin with.
fn pure_fifth_cents() -> f64 {
    1200.0 * (3.0_f64 / 2.0).log2()
}
/// The Pythagorean comma, `531441/524288 ≈ 23.460` c (`core_spec.tex:3713`).
fn pythagorean_comma_cents() -> f64 {
    1200.0 * (531441.0_f64 / 524288.0).log2()
}
/// The syntonic comma, `81/80 ≈ 21.506` c (`core_spec.tex:3765`).
fn syntonic_comma_cents() -> f64 {
    1200.0 * (81.0_f64 / 80.0).log2()
}
/// The schisma, `32805/32768 ≈ 1.954` c (`core_spec.tex:3896`) — the
/// Pythagorean comma minus the syntonic comma exactly: `syntonic + schisma =
/// pythagorean`, which is why a Kirnberger construction's regular
/// syntonic-tempered fifths plus its one schisma-tempered closing fifth
/// reach exactly one Pythagorean comma of total tempering. Computed here
/// directly from `32805/32768`, independently of [`pythagorean_comma_cents`]
/// and [`syntonic_comma_cents`], so the closure tests below *prove* that
/// identity rather than assume it.
fn schisma_cents() -> f64 {
    1200.0 * (32805.0_f64 / 32768.0).log2()
}

/// How one of the twelve fifths in the circle-of-fifths chain
/// `C–G–D–A–E–B–F♯–C♯–G♯–E♭–B♭–F–(C)` is tempered by a historical
/// construction (`core_spec.tex` §"Temperament Constructions",
/// `:3696`-`4011`). This — which fifths, by what fraction of which comma —
/// *is* the construction; the derived cents tables the specification also
/// gives are checked against the walk below, never pasted in as data
/// (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 2).
#[derive(Copy, Clone, Debug)]
enum FifthTempering {
    /// The untempered 3/2 ratio.
    Pure,
    /// Narrowed by `fraction` of the *Pythagorean* comma (Werckmeister
    /// III/IV, Vallotti, Young II — the well temperaments).
    NarrowPythagorean(f64),
    /// Widened by `fraction` of the Pythagorean comma (Werckmeister IV's two
    /// wide fifths, `G♯–E♭` and `E♭–B♭`).
    WidePythagorean(f64),
    /// Narrowed by `fraction` of the *syntonic* comma (meantone; Kirnberger's
    /// regular tempered fifths).
    NarrowSyntonic(f64),
    /// Narrowed by exactly one schisma — Kirnberger's closing `F♯–D♭` fifth,
    /// "the single most commonly omitted element of these ... constructions"
    /// (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md`'s schisma-fifth trap).
    NarrowSchisma,
    /// The closing/residual fifth: given no fraction of its own, but
    /// whatever cents value brings the twelve-fifth chain to exactly seven
    /// octaves (8400 c) — the wolf, for the four non-circulating
    /// constructions (`pythagorean`, the three `meantone-*`).
    Residual,
}

/// A temperament's construction: the twelve fifths of the circle
/// `C–G–D–A–E–B–F♯–C♯–G♯–E♭–B♭–F–(C)`
/// (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 2), in that fixed arc order —
/// index 0 is `C–G`, index 1 is `G–D`, …, index 8 is `G♯–E♭` (the
/// conventional wolf/closing position), index 11 is the final `F–C`.
type Construction = [FifthTempering; 12];

/// The `cmn-12` chromatic degree reached after each of the twelve chain
/// arcs, in chain order starting from `C` itself: `C`(0), `G`(1), `D`(2),
/// `A`(3), `E`(4), `B`(5), `F♯`(6), `C♯`(7), `G♯`(8), `E♭`(9), `B♭`(10),
/// `F`(11) — i.e. `CHAIN_CHROMATIC_DEGREE[k]` is chain position `k`'s
/// `cmn-12` degree (`C=0, C♯=1, D=2, …, B=11`).
const CHAIN_CHROMATIC_DEGREE: [usize; 12] = [0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5];

/// The result of walking a construction: the deliverable the closure tests
/// below recompute from, never a hardcoded constant
/// (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md`'s "closure invariant, recomputed
/// in code").
struct TemperamentWalk {
    /// The twelve ratios relative to `C` (`1/1`), indexed by `cmn-12`
    /// chromatic degree (`0..12`). The one field production code
    /// ([`temperament_ratios`]) consumes.
    ratios: [f64; 12],
    /// The twelve individual fifths' actual cents, in chain order (index 0
    /// is `C–G`, …, index 8 is `G♯–E♭`, index 11 is `F–C`) — read only by
    /// the closure/wolf tests below, hence `#[cfg(test)]`: no production
    /// code needs a single fifth's cents in isolation.
    #[cfg(test)]
    fifth_cents: [f64; 12],
    /// The *raw* (unreduced) cumulative cents after walking all twelve
    /// fifths forward from `C` — exactly `8400.0` (seven octaves) if and
    /// only if the construction closes. Also test-only, for the same reason.
    #[cfg(test)]
    raw_closure_cents: f64,
}

/// Walks the circle-of-fifths chain under `construction`
/// (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 2: "one walk that places the
/// twelve notes and derives their ratios"). At most one arc may be
/// [`FifthTempering::Residual`] (the four non-circulating constructions);
/// its cents are computed as whatever value brings the other eleven
/// arcs' sum to seven octaves (`core_spec.tex:3749-3759`'s own framing:
/// "any assignment of twelve distinct pitch classes must sum to
/// [8400 c] ... by construction").
fn walk_temperament(construction: &Construction) -> TemperamentWalk {
    let pure = pure_fifth_cents();

    // Pass 1: every fixed (non-residual) arc's actual cents, plus the index
    // of the one residual (wolf) arc, if any.
    let mut fifth_cents = [0.0f64; 12];
    let mut residual_index: Option<usize> = None;
    for (i, tempering) in construction.iter().enumerate() {
        let deviation = match *tempering {
            FifthTempering::Pure => 0.0,
            FifthTempering::NarrowPythagorean(fraction) => fraction * pythagorean_comma_cents(),
            FifthTempering::WidePythagorean(fraction) => -fraction * pythagorean_comma_cents(),
            FifthTempering::NarrowSyntonic(fraction) => fraction * syntonic_comma_cents(),
            FifthTempering::NarrowSchisma => schisma_cents(),
            FifthTempering::Residual => {
                debug_assert!(
                    residual_index.is_none(),
                    "a construction may have at most one residual (wolf) fifth"
                );
                residual_index = Some(i);
                continue;
            }
        };
        fifth_cents[i] = pure - deviation;
    }
    // Pass 2: the residual (wolf) arc, if any, closes the chain to exactly
    // seven octaves.
    if let Some(i) = residual_index {
        let sum_of_others: f64 = fifth_cents
            .iter()
            .enumerate()
            .filter(|(j, _)| *j != i)
            .map(|(_, c)| *c)
            .sum();
        fifth_cents[i] = 8400.0 - sum_of_others;
    }

    // Walk forward from C, accumulating *raw* (unreduced) cents; reducing
    // only when reading off each chain note's pitch class below is what
    // lets the wolf (for the non-circulating four) fall out of the same
    // walk as everything else, rather than needing separate handling.
    let mut cumulative = [0.0f64; 13];
    for i in 0..12 {
        cumulative[i + 1] = cumulative[i] + fifth_cents[i];
    }

    let mut ratios = [0.0f64; 12];
    for (chain_pos, &degree) in CHAIN_CHROMATIC_DEGREE.iter().enumerate() {
        let pitch_class_cents = cumulative[chain_pos].rem_euclid(1200.0);
        ratios[degree] = 2f64.powf(pitch_class_cents / 1200.0);
    }

    TemperamentWalk {
        ratios,
        #[cfg(test)]
        fifth_cents,
        #[cfg(test)]
        raw_closure_cents: cumulative[12],
    }
}

/// `pythagorean` (`core_spec.tex:3709-3759`): eleven pure fifths; the
/// conventional `E♭–G♯` cut leaves the wolf at `G♯–E♭` (chain arc 8).
fn pythagorean_construction() -> Construction {
    use FifthTempering::{Pure, Residual};
    [
        Pure, Pure, Pure, Pure, Pure, Pure, Pure, Pure, Residual, Pure, Pure, Pure,
    ]
}

/// `meantone-1/n-comma` (`core_spec.tex:3761-3812`): all eleven non-wolf
/// fifths narrowed uniformly by `1/n` of the *syntonic* comma; same
/// `E♭–G♯` cut as `pythagorean`, so the wolf is again chain arc 8.
fn meantone_construction(comma_fraction: f64) -> Construction {
    use FifthTempering::{NarrowSyntonic, Residual};
    let n = NarrowSyntonic(comma_fraction);
    [n, n, n, n, n, n, n, n, Residual, n, n, n]
}

/// `werckmeister-iii` (`core_spec.tex:3814-3838`): `C–G, G–D, D–A, B–F♯`
/// narrowed 1/4 Pythagorean comma; the other eight pure. Circulating — no
/// residual arc.
fn werckmeister_iii_construction() -> Construction {
    use FifthTempering::{NarrowPythagorean, Pure};
    let quarter = NarrowPythagorean(0.25);
    [
        quarter, // 0: C-G
        quarter, // 1: G-D
        quarter, // 2: D-A
        Pure,    // 3: A-E
        Pure,    // 4: E-B
        quarter, // 5: B-F#
        Pure,    // 6: F#-C#
        Pure,    // 7: C#-G#
        Pure,    // 8: G#-Eb
        Pure,    // 9: Eb-Bb
        Pure,    // 10: Bb-F
        Pure,    // 11: F-C
    ]
}

/// `werckmeister-iv` (`core_spec.tex:3840-3866`): `C–G, D–A, E–B, F♯–C♯,
/// B♭–F` narrowed 1/3 Pythagorean comma; `G♯–E♭, E♭–B♭` widened 1/3
/// Pythagorean comma; the other five pure. Circulating.
fn werckmeister_iv_construction() -> Construction {
    use FifthTempering::{NarrowPythagorean, Pure, WidePythagorean};
    let narrow = NarrowPythagorean(1.0 / 3.0);
    let wide = WidePythagorean(1.0 / 3.0);
    [
        narrow, // 0: C-G
        Pure,   // 1: G-D
        narrow, // 2: D-A
        Pure,   // 3: A-E
        narrow, // 4: E-B
        Pure,   // 5: B-F#
        narrow, // 6: F#-C#
        Pure,   // 7: C#-G#
        wide,   // 8: G#-Eb
        wide,   // 9: Eb-Bb
        narrow, // 10: Bb-F
        Pure,   // 11: F-C
    ]
}

/// `vallotti` (`core_spec.tex:3868-3888`): `F–C, C–G, G–D, D–A, A–E, E–B`
/// (six consecutive) narrowed 1/6 Pythagorean comma; the other six pure.
/// Circulating.
fn vallotti_construction() -> Construction {
    use FifthTempering::{NarrowPythagorean, Pure};
    let sixth = NarrowPythagorean(1.0 / 6.0);
    [
        sixth, // 0: C-G
        sixth, // 1: G-D
        sixth, // 2: D-A
        sixth, // 3: A-E
        sixth, // 4: E-B
        Pure,  // 5: B-F#
        Pure,  // 6: F#-C#
        Pure,  // 7: C#-G#
        Pure,  // 8: G#-Eb
        Pure,  // 9: Eb-Bb
        Pure,  // 10: Bb-F
        sixth, // 11: F-C
    ]
}

/// `young-ii` (`core_spec.tex:3985-4010`): `C–G, G–D, D–A, A–E, E–B, B–F♯`
/// (six consecutive) narrowed 1/6 Pythagorean comma; the other six pure —
/// the same construction as `vallotti`, rotated to start at `C` instead of
/// `F` (compare this construction's tempered run, arcs 0-5, against
/// `vallotti`'s, arcs 11,0-4). Circulating.
fn young_ii_construction() -> Construction {
    use FifthTempering::{NarrowPythagorean, Pure};
    let sixth = NarrowPythagorean(1.0 / 6.0);
    [
        sixth, // 0: C-G
        sixth, // 1: G-D
        sixth, // 2: D-A
        sixth, // 3: A-E
        sixth, // 4: E-B
        sixth, // 5: B-F#
        Pure,  // 6: F#-C#
        Pure,  // 7: C#-G#
        Pure,  // 8: G#-Eb
        Pure,  // 9: Eb-Bb
        Pure,  // 10: Bb-F
        Pure,  // 11: F-C
    ]
}

/// `kirnberger-ii` (`core_spec.tex:3890-3937`): `D–A, A–E` narrowed 1/2
/// syntonic comma; the closing fifth (named `F♯–D♭` in the specification,
/// since Kirnberger's own chain is built outward from `D♭` — the *same
/// physical arc* as chain position 6, `F♯–C♯`, `D♭` and `C♯` being one
/// enharmonic pitch class) narrowed one schisma — the trap
/// `spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` calls out by name; the other nine
/// pure. Circulating.
fn kirnberger_ii_construction() -> Construction {
    use FifthTempering::{NarrowSchisma, NarrowSyntonic, Pure};
    let half = NarrowSyntonic(0.5);
    [
        Pure,          // 0: C-G
        Pure,          // 1: G-D
        half,          // 2: D-A
        half,          // 3: A-E
        Pure,          // 4: E-B
        Pure,          // 5: B-F#
        NarrowSchisma, // 6: F#-C# (the closing F#-Db fifth)
        Pure,          // 7: C#-G#
        Pure,          // 8: G#-Eb
        Pure,          // 9: Eb-Bb
        Pure,          // 10: Bb-F
        Pure,          // 11: F-C
    ]
}

/// `kirnberger-iii` (`core_spec.tex:3939-3983`): `C–G, G–D, D–A, A–E` (four
/// consecutive) narrowed 1/4 syntonic comma; the same closing `F♯–D♭`
/// (chain position 6) narrowed one schisma as `kirnberger-ii`; the other
/// seven pure. Circulating.
fn kirnberger_iii_construction() -> Construction {
    use FifthTempering::{NarrowSchisma, NarrowSyntonic, Pure};
    let quarter = NarrowSyntonic(0.25);
    [
        quarter,       // 0: C-G
        quarter,       // 1: G-D
        quarter,       // 2: D-A
        quarter,       // 3: A-E
        Pure,          // 4: E-B
        Pure,          // 5: B-F#
        NarrowSchisma, // 6: F#-C# (the closing F#-Db fifth)
        Pure,          // 7: C#-G#
        Pure,          // 8: G#-Eb
        Pure,          // 9: Eb-Bb
        Pure,          // 10: Bb-F
        Pure,          // 11: F-C
    ]
}

/// The twelve ratios (indexed by `cmn-12` chromatic degree) of the
/// reserved-built-in temperament named by `function`, or `None` for any
/// other id — the extension point's fail-closed path
/// (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 1: "An unknown
/// `TuningFunctionId` returns `None` ... `Function` is an extension point and
/// no registry exists"). Never memoized: each of the ten constructions is
/// cheap arithmetic on twelve `f64`s, so there is no cache to keep coherent.
fn temperament_ratios(function: &TuningFunctionId) -> Option<[f64; 12]> {
    let construction = match function.as_str() {
        "pythagorean" => pythagorean_construction(),
        "meantone-1/4-comma" => meantone_construction(0.25),
        "meantone-1/5-comma" => meantone_construction(0.2),
        "meantone-1/6-comma" => meantone_construction(1.0 / 6.0),
        "werckmeister-iii" => werckmeister_iii_construction(),
        "werckmeister-iv" => werckmeister_iv_construction(),
        "vallotti" => vallotti_construction(),
        "kirnberger-ii" => kirnberger_ii_construction(),
        "kirnberger-iii" => kirnberger_iii_construction(),
        "young-ii" => young_ii_construction(),
        _ => return None,
    };
    Some(walk_temperament(&construction).ratios)
}

// ===========================================================================
// The frequency resolver (item 3): (position, TuningSystem, ReferencePitch)
// -> Hz.
// ===========================================================================

/// Why a tuning could not be resolved to a frequency. Every variant is a
/// closed failure, never a fallback frequency
/// (`req:tuning:tuning-resolution-determinism`).
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum TuningResolutionError {
    /// `voice` is not the id of any voice reachable from the score's
    /// `canvas.regions` — a caller error (an orphaned or foreign
    /// [`VoiceId`]), not a tuning failure.
    VoiceNotFound(VoiceId),
    /// The resolved tuning-system identifier is not in the built-in catalog
    /// at all (no registry for score-defined tuning systems exists, Ruling C
    /// of `spec/PLAN_PUSH4B_TUNING.md`).
    UnknownTuningSystem(TuningSystemId),
    /// The resolved tuning-system identifier is a real catalog entry whose
    /// resolution this tranche defers (see [`built_in_tuning_system`]); the
    /// reason names which tranche completes it.
    NotYetSupported {
        id: TuningSystemId,
        reason: &'static str,
    },
    /// The resolved pitch space does not resolve structurally at all
    /// ([`built_in_position_structure`] returned `None`) — an unknown
    /// identifier or one of the six built-in spaces the specification names
    /// but does not structurally determine (`crate::pitch_space`).
    UnresolvedPitchSpace(PitchSpaceId),
    /// `req:tuning:tuning-system-compatibility`: the resolved tuning
    /// system's declared `pitch_space` differs from the resolved pitch
    /// space, and no compatibility-mapping registry exists this tranche —
    /// a deliberate deferral (see the module doc), not a guess.
    IncompatiblePitchSpace {
        pitch_space: PitchSpaceId,
        tuning_system_pitch_space: PitchSpaceId,
    },
    /// The pitch's (or the reference's) position could not be placed on the
    /// tuning system's coordinate frame: a structural mismatch between the
    /// resolved pitch space's [`PositionStructure`] and the
    /// [`PitchSpacePosition`] variant in play, or between its chromatic
    /// cardinality and the tuning system's own divisions/table length.
    PositionUnavailable,
    /// The resolved tuning system is [`TuningResolution::Adaptive`] naming an
    /// [`AdaptiveTuningFunctionId`] other than the one registered built-in,
    /// `"default-v1"` (`req:tuning:adaptive-default-version`'s final clause:
    /// "An unregistered or unknown `AdaptiveTuningFunctionId` MUST be a hard
    /// error; there is no silent fallback"). Never falls back to the C
    /// default.
    UnregisteredAdaptiveFunction(AdaptiveTuningFunctionId),
    /// A [`crate::graph::KeySignatureChange`]'s anchor could not be ordered
    /// against a pitch's onset while deriving an adaptive tonal centre
    /// ([`derive_tonal_centre`], `req:tuning:adaptive-anchor-derivation`) —
    /// either the two live on clocks that cannot be compared (a wall-clock
    /// anchor against a musical onset, or vice versa), or the anchor is an
    /// indirect form (`Event`/`Measure`/`Region`) the caller's injected
    /// resolver could not place. A **deliberate deferral**, like
    /// [`Self::IncompatiblePitchSpace`]: it says "this score uses an anchor
    /// form the adaptive resolver does not order yet," never "your key
    /// signature is wrong" — so it is never confused with a malformed score.
    AnchorNotOrderable {
        anchor: TimeAnchor,
        reason: &'static str,
    },
}

impl core::fmt::Display for TuningResolutionError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::VoiceNotFound(v) => write!(f, "voice {v:?} is not reachable from the score graph"),
            Self::UnknownTuningSystem(id) => write!(f, "'{id}' is not a built-in tuning system"),
            Self::NotYetSupported { id, reason } => {
                write!(f, "'{id}' is not yet supported: {reason}")
            }
            Self::UnresolvedPitchSpace(id) => {
                write!(f, "'{id}' does not resolve to a structurally determined pitch space")
            }
            Self::IncompatiblePitchSpace {
                pitch_space,
                tuning_system_pitch_space,
            } => write!(
                f,
                "resolved pitch space '{pitch_space}' is incompatible with the tuning system's declared \
                 pitch space '{tuning_system_pitch_space}' (no compatibility mapping registry exists)"
            ),
            Self::PositionUnavailable => {
                f.write_str("the pitch space position could not be placed on the tuning system's coordinate frame")
            }
            Self::UnregisteredAdaptiveFunction(id) => write!(
                f,
                "'{id}' is not a registered adaptive tuning function (only \"default-v1\" is built in)"
            ),
            Self::AnchorNotOrderable { anchor, reason } => write!(
                f,
                "cannot order the key-signature-change anchor {anchor:?} against the pitch's onset: {reason}"
            ),
        }
    }
}

impl std::error::Error for TuningResolutionError {}

/// The absolute tuning-coordinate of `position`, in the coordinate frame a
/// tuning system with `divisions` positions-per-octave uses — the same
/// absolute chromatic-coordinate idea [`Pitch::twelve_tet_semitone`]
/// (`crate::pitch`) uses for `cmn-12`, generalized from a fixed 12 to any
/// `N` and to [`PitchSpacePosition::Integer`] (the EDO pitch spaces'
/// position kind) as well as [`PitchSpacePosition::Cmn`].
fn absolute_coordinate(
    position: &PitchSpacePosition,
    structure: &PositionStructure,
    divisions: u32,
) -> Option<i64> {
    match (position, structure) {
        (
            PitchSpacePosition::Cmn {
                nominal,
                alteration,
                octave,
            },
            PositionStructure::DiatonicOverChromatic {
                chromatic_positions_per_octave,
                nominal_to_chromatic,
                ..
            },
        ) if u32::from(*chromatic_positions_per_octave) == divisions => {
            let degree = i64::from(nominal_to_chromatic[*nominal as usize]);
            Some(i64::from(*octave) * i64::from(divisions) + degree + i64::from(*alteration))
        }
        (
            PitchSpacePosition::Integer { space_size, index },
            PositionStructure::Chromatic {
                positions_per_octave,
            },
        ) if u32::from(*space_size) == divisions
            && u32::from(*positions_per_octave) == divisions =>
        {
            Some(i64::from(*index))
        }
        _ => None,
    }
}

/// The chromatic cardinality of `structure` — the "positions per octave"
/// figure [`TuningResolution::Function`] borrows from the pitch space
/// rather than carrying itself (the variant has no division-count field,
/// `core_spec.tex:3318-3324`). Both `Chromatic` and `DiatonicOverChromatic`
/// name one; `JiLattice` and `Registered` do not (a lattice or a grammar
/// plugin has no single "positions per octave" scalar), so `None` there is
/// a structural refusal, not an oversight.
fn chromatic_cardinality(structure: &PositionStructure) -> Option<u32> {
    match structure {
        PositionStructure::Chromatic {
            positions_per_octave,
        } => Some(u32::from(*positions_per_octave)),
        PositionStructure::DiatonicOverChromatic {
            chromatic_positions_per_octave,
            ..
        } => Some(u32::from(*chromatic_positions_per_octave)),
        PositionStructure::JiLattice { .. } | PositionStructure::Registered(_) => None,
    }
}

/// The full-register frequency ratio of absolute coordinate `s`, relative to
/// coordinate `0` under `resolution`.
fn coordinate_ratio(resolution: &TuningResolution, s: i64) -> Option<f64> {
    match resolution {
        TuningResolution::EqualTemperament {
            divisions_per_octave,
        } => {
            if *divisions_per_octave == 0 {
                return None;
            }
            Some(2f64.powf(s as f64 / f64::from(*divisions_per_octave)))
        }
        TuningResolution::PerPositionRatios(table) => {
            let n = i64::try_from(table.len()).ok().filter(|n| *n > 0)?;
            let degree = i32::try_from(s.rem_euclid(n)).ok()?;
            let octave = i32::try_from(s.div_euclid(n)).ok()?;
            let entry = table.iter().find(|pr| pr.position == degree)?;
            let base = f64::from(entry.ratio.numerator) / f64::from(entry.ratio.denominator.get());
            Some(base * 2f64.powi(octave))
        }
        TuningResolution::Function { function, .. } => {
            // `degree = s.rem_euclid(12)`, `octave = s.div_euclid(12)`,
            // `ratio = temperament_ratios[degree] · 2^octave`
            // (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 1). An unknown
            // `TuningFunctionId` has no registry to consult, so
            // `temperament_ratios` returns `None` and this fails closed —
            // never a fallback frequency.
            let ratios = temperament_ratios(function)?;
            let degree = i32::try_from(s.rem_euclid(12)).ok()?;
            let octave = i32::try_from(s.div_euclid(12)).ok()?;
            Some(ratios[degree as usize] * 2f64.powi(octave))
        }
        TuningResolution::Adaptive { .. } => {
            // An `Adaptive` resolution carries no anchor of its own: the
            // anchor comes from a `HarmonicContext` this function is never
            // given. `resolve_pitch_frequency` always converts `Adaptive` to
            // a concrete `PerPositionRatios` (via `ji_static_5limit_ratios`)
            // before reaching this layer; one arriving here unconverted (a
            // caller invoking `frequency_for_position` directly) has no
            // anchor to consult, so this fails closed exactly like an
            // unregistered `Function` id, never guessing C.
            None
        }
    }
}

/// Computes the frequency in Hz at which `position` sounds under `system`,
/// anchored by `reference` (Chapter 4 §"Tuning Systems" / §"Reference
/// Pitch"; `req:tuning:tuning-resolution-determinism`).
///
/// **Anchoring** — the one subtlety `spec/CONTRACT_PUSH4B_RESOLVER.md`
/// singles out: a [`TuningResolution`]'s ratios are relative to the
/// tuning's own 1/1 (its anchor), but `reference` fixes a *different*
/// position's absolute frequency. Both `position` and `reference.position`
/// are placed on the same absolute coordinate frame
/// (`absolute_coordinate`), and the frequency is
/// `reference.frequency_hz() * ratio(position) / ratio(reference.position)`.
/// The arbitrary choice of which position the construction calls "1/1"
/// cancels out of that quotient, so this is correct regardless of anchor —
/// for [`TuningResolution::EqualTemperament`] it reduces exactly to
/// `ref_freq · 2^((position − ref_position)/N)`.
pub fn frequency_for_position(
    position: &PitchSpacePosition,
    system: &TuningSystem,
    reference: &ReferencePitch,
) -> Result<f64, TuningResolutionError> {
    let structure = built_in_position_structure(&system.pitch_space)
        .ok_or_else(|| TuningResolutionError::UnresolvedPitchSpace(system.pitch_space.clone()))?;
    let divisions =
        match &system.resolution {
            TuningResolution::EqualTemperament {
                divisions_per_octave,
            } => u32::from(*divisions_per_octave),
            TuningResolution::PerPositionRatios(table) => u32::try_from(table.len())
                .map_err(|_| TuningResolutionError::PositionUnavailable)?,
            // `Function` carries no division count of its own (unlike the other
            // two variants); divisions comes from the *pitch space*'s chromatic
            // cardinality instead — 12 for `cmn-12`
            // (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 1).
            TuningResolution::Function { .. } => chromatic_cardinality(&structure)
                .ok_or(TuningResolutionError::PositionUnavailable)?,
            // Same reasoning as `Function`: `Adaptive` borrows the pitch
            // space's chromatic cardinality rather than carrying its own.
            // (In practice `resolve_pitch_frequency` never leaves `Adaptive`
            // unconverted this far — see `coordinate_ratio`.)
            TuningResolution::Adaptive { .. } => chromatic_cardinality(&structure)
                .ok_or(TuningResolutionError::PositionUnavailable)?,
        };
    let s = absolute_coordinate(position, &structure, divisions)
        .ok_or(TuningResolutionError::PositionUnavailable)?;
    let s_ref = absolute_coordinate(&reference.position, &structure, divisions)
        .ok_or(TuningResolutionError::PositionUnavailable)?;
    let ratio_p = coordinate_ratio(&system.resolution, s)
        .ok_or(TuningResolutionError::PositionUnavailable)?;
    let ratio_ref = coordinate_ratio(&system.resolution, s_ref)
        .ok_or(TuningResolutionError::PositionUnavailable)?;
    Ok(reference.frequency_hz() * ratio_p / ratio_ref)
}

// ===========================================================================
// The five-scope resolution walk (item 4).
// ===========================================================================

/// The independently-resolved pitch space, tuning system, and reference
/// pitch that govern a pitch at a given location (`req:tuning:tuning-resolution-order`).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ResolvedTuning {
    pub pitch_space: PitchSpaceId,
    pub tuning_system: TuningSystemId,
    pub reference: ReferencePitch,
}

/// The first value `field` supplies from an override whose scope matches
/// `voice`, else `staff`, else `region`, in that priority order (scopes 2-4
/// of `req:tuning:tuning-resolution-order`) — the same lookup used
/// identically for all three components the walk resolves. Within a tier,
/// overrides are consulted in `context.overrides`' own declared order ("in
/// the order they apply", `core_spec.tex:3523`), skipping past a
/// scope-matching override that leaves this particular component `None`
/// rather than stopping at it, so a later override at the same scope can
/// still supply the value this one didn't.
fn override_value<T: Clone>(
    context: &ScoreTuningContext,
    voice: VoiceId,
    staff: StaffId,
    region: RegionId,
    field: impl Fn(&TuningOverride) -> &Option<T>,
) -> Option<T> {
    let tier = |matches_scope: &dyn Fn(&TuningScope) -> bool| -> Option<T> {
        context
            .overrides
            .iter()
            .filter(|o| matches_scope(&o.scope))
            .find_map(|o| field(o).clone())
    };
    tier(&|s| matches!(s, TuningScope::Voice(v) if *v == voice))
        .or_else(|| tier(&|s| matches!(s, TuningScope::Staff(st) if *st == staff)))
        .or_else(|| {
            // Step 4: "each region enclosing the pitch, innermost to
            // outermost". In this data model a `Voice` is owned by exactly
            // one `StaffInstance`, owned by exactly one `Region`
            // (containment, not a derived time-range query) — there is no
            // nested-region concept, so "innermost to outermost" is exactly
            // this one region.
            //
            // `TuningScope::Range` is part of the type (Chapter 4 defines
            // it) but `req:tuning:tuning-resolution-order` enumerates
            // exactly five steps and does not include it; this tranche's
            // walk never matches it (see the module doc's scope note).
            tier(&|s| matches!(s, TuningScope::Region(r) if *r == region))
        })
}

/// The five-scope walk of `req:tuning:tuning-resolution-order`
/// (`core_spec.tex:3549`, read and verified before citing): resolves each of
/// `pitch_space`, `tuning_system`, and `reference` independently, walking
/// from the pitch's own [`crate::pitch::AcousticPitch`] outward through
/// voice, staff, and region overrides to the score default.
///
/// Step 1 supplies a value only for `tuning_system` (an explicit
/// [`TuningReference::Explicit`] short-circuits) — [`AcousticPitch`] carries
/// no pitch-space or reference field of its own, so those two components
/// always proceed to step 2. (A pitch's [`AcousticRealization::AbsoluteHz`]
/// short-circuits the *whole* frequency, bypassing this walk entirely; see
/// [`resolve_pitch_frequency`].)
///
/// [`AcousticPitch`]: crate::pitch::AcousticPitch
pub fn resolve_tuning_scope(
    pitch: &Pitch,
    voice: VoiceId,
    staff: StaffId,
    region: RegionId,
    context: &ScoreTuningContext,
) -> ResolvedTuning {
    let tuning_system = match &pitch.acoustic.tuning {
        TuningReference::Explicit(id) => id.clone(),
        TuningReference::Inherit => {
            override_value(context, voice, staff, region, |o| &o.tuning_system)
                .unwrap_or_else(|| context.default_tuning_system.clone())
        }
    };
    let pitch_space = override_value(context, voice, staff, region, |o| &o.pitch_space)
        .unwrap_or_else(|| context.default_pitch_space.clone());
    let reference = override_value(context, voice, staff, region, |o| &o.reference)
        .unwrap_or_else(|| context.reference.clone());
    ResolvedTuning {
        pitch_space,
        tuning_system,
        reference,
    }
}

// ===========================================================================
// The top-level pipeline: walk, catalog lookup, compatibility check (item
// 5), frequency.
// ===========================================================================

/// Locates the region, staff, and **staff instance** that structurally own
/// `voice`: a `Voice` belongs to exactly one `StaffInstance`, which belongs
/// to exactly one `Region` (Chapter 5's containment tree — ownership, not a
/// derived time-range query). `None` if no region in `score.canvas.regions`
/// owns a voice with this id.
///
/// Returns the instance itself, not just its id: `key_sequence`
/// (`req:tuning:adaptive-anchor-derivation`) lives on the *instance*
/// (`StaffInstance::key_sequence`), not on the underlying `Staff`. An earlier
/// version of this function returned only `(RegionId, StaffId)`, discarding
/// the instance it already had in hand — the one structural blocker that
/// made adaptive resolution impossible before [`derive_tonal_centre`] existed.
fn locate_voice(score: &Score, voice: VoiceId) -> Option<(RegionId, StaffId, &StaffInstance)> {
    for region in &score.canvas.regions {
        for instance in region.staff_instances() {
            if instance.voices.iter().any(|v| v.id == voice) {
                return Some((region.id, instance.staff, instance));
            }
        }
    }
    None
}

/// A [`TimeAnchor`] or [`EventPosition`] placed on one common, directly
/// comparable timeline — either absolute wall-clock nanoseconds or a
/// region-relative [`MusicalPosition`]. [`derive_tonal_centre`] only ever
/// compares two `Coordinate`s built from the *same* onset (see
/// `anchor_coordinate`), so the two variants are never compared against each
/// other in practice.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum Coordinate {
    WallClock(i64),
    Musical(MusicalPosition),
}

/// Places `anchor` on the same clock as `onset`, or fails closed with a
/// distinct, reported [`TuningResolutionError::AnchorNotOrderable`] — never
/// a guessed ordering (`req:tuning:adaptive-anchor-derivation`).
///
/// Two shapes are unambiguously determinable: a [`TimeAnchor::WallClock`]
/// anchor against a [`EventPosition::WallClock`] onset compares directly by
/// nanosecond; any other anchor against a [`EventPosition::Musical`] onset is
/// placed via the caller-injected `resolve` — the same `impl
/// Fn(&TimeAnchor) -> Option<MusicalPosition>` shape
/// `TempoMap::musical_to_wallclock_with` injects (`tempo.rs:331`), reused
/// here rather than reinvented so this module stays out of the time-model
/// business. Everything else — a wall-clock anchor against a musical onset
/// or vice versa (cross-clock), or an indirect anchor the injected resolver
/// declines to place — fails closed rather than guessing.
fn anchor_coordinate(
    anchor: &TimeAnchor,
    onset: &EventPosition,
    resolve: &impl Fn(&TimeAnchor) -> Option<MusicalPosition>,
) -> Result<Coordinate, TuningResolutionError> {
    match (anchor, onset) {
        (TimeAnchor::WallClock { time }, EventPosition::WallClock(_)) => {
            Ok(Coordinate::WallClock(time.0))
        }
        (TimeAnchor::WallClock { .. }, EventPosition::Musical(_)) => {
            Err(TuningResolutionError::AnchorNotOrderable {
                anchor: anchor.clone(),
                reason: "a wall-clock key-signature anchor cannot be ordered against a \
                         musical-time onset (cross-clock comparison)",
            })
        }
        (_, EventPosition::WallClock(_)) => Err(TuningResolutionError::AnchorNotOrderable {
            anchor: anchor.clone(),
            reason: "a non-wall-clock key-signature anchor cannot be ordered against a \
                     wall-clock onset (cross-clock comparison)",
        }),
        (_, EventPosition::Musical(_)) => {
            resolve(anchor).map(Coordinate::Musical).ok_or_else(|| {
                TuningResolutionError::AnchorNotOrderable {
                    anchor: anchor.clone(),
                    reason: "the key-signature anchor is an indirect form (Event/Measure/Region) \
                         the injected resolver could not place on the musical timeline",
                }
            })
        }
    }
}

/// Derives the adaptive tonal centre from the score graph
/// (`req:tuning:adaptive-anchor-derivation`, `core_spec.tex:3474-3489`): the
/// prevailing key signature on the staff instance containing `voice` —
/// specifically, the *latest* `KeySignatureChange` in that instance's
/// `key_sequence` whose anchor is at or before `onset` — converted to a
/// chromatic pitch class by `anchor_pc = (7 * fifths).rem_euclid(12)` (never
/// `%`: `fifths` runs negative for flat keys, `KeySignature::MIN_FIFTHS ==
/// -7`, and `%` would yield a negative pitch class). Mode is **not**
/// consulted: `KeySignature` carries no mode field at all, so a signature of
/// 0 anchors at C whether the prevailing key is C major or A minor —
/// structurally guaranteed, not merely by omission.
///
/// `resolve` places any non-wall-clock `KeySignatureChange` anchor on the
/// musical timeline for comparison against a musical `onset` — see
/// `anchor_coordinate`, which this function uses for every entry in
/// `key_sequence`; if *any* entry's anchor cannot be ordered against
/// `onset`, this fails closed rather than risk skipping past the one that
/// would have been the true prevailing signature.
///
/// `Ok(None)` when no key-signature change applies at or before `onset` —
/// the resolver then defaults to C (`req:tuning:adaptive-default-version`);
/// that default is applied in [`resolve_pitch_frequency`], not here, since a
/// missing tonal centre is a defined default, not this function's failure to
/// report. `Err` only for a caller error (`voice` unreachable from the score
/// graph) or an anchor this function cannot order.
pub fn derive_tonal_centre(
    score: &Score,
    voice: VoiceId,
    onset: &EventPosition,
    resolve: impl Fn(&TimeAnchor) -> Option<MusicalPosition>,
) -> Result<Option<ChromaticPitchClass>, TuningResolutionError> {
    let (_, _, instance) =
        locate_voice(score, voice).ok_or(TuningResolutionError::VoiceNotFound(voice))?;
    let onset_coordinate = match onset {
        EventPosition::WallClock(t) => Coordinate::WallClock(t.0),
        EventPosition::Musical(p) => Coordinate::Musical(p.clone()),
    };
    let mut prevailing: Option<(Coordinate, KeySignature)> = None;
    for change in &instance.key_sequence {
        let coordinate = anchor_coordinate(&change.anchor, onset, &resolve)?;
        if coordinate <= onset_coordinate {
            let is_later = match &prevailing {
                Some((best, _)) => coordinate > *best,
                None => true,
            };
            if is_later {
                prevailing = Some((coordinate, change.key));
            }
        }
    }
    Ok(prevailing.map(|(_, key)| {
        let anchor_pc = (7 * i32::from(key.fifths())).rem_euclid(12);
        ChromaticPitchClass::new(anchor_pc as u8)
            .expect("(7 * fifths).rem_euclid(12) is always in 0..=11")
    }))
}

/// The anchor pitch class for adaptive resolution: a hard error if `function`
/// is not the one reserved built-in, `"default-v1"`
/// (`req:tuning:adaptive-default-version`'s final clause — no silent
/// fallback to C), else `context`'s tonal centre when supplied, else C
/// (chromatic position 0 — `core_spec.tex:3452-3453`'s spec-mandated
/// default, not a fail-closed case).
fn adaptive_anchor(
    function: &AdaptiveTuningFunctionId,
    context: Option<&HarmonicContext>,
) -> Result<i32, TuningResolutionError> {
    if function.as_str() != "default-v1" {
        return Err(TuningResolutionError::UnregisteredAdaptiveFunction(
            function.clone(),
        ));
    }
    Ok(context
        .and_then(|c| c.tonal_centre)
        .map(|pc| i32::from(pc.get()))
        .unwrap_or(0))
}

/// The full resolver: walks the five scopes, checks compatibility, and
/// computes the frequency in Hz at which `pitch` sounds, given its location
/// (`voice`) in `score`. `context` supplies the harmonic context adaptive
/// tuning consumes (`core_spec.tex:3426-3428`: "The harmonic context is
/// constructed by the audio engine ... and passed to the tuning resolution
/// function"); static systems ignore it entirely.
///
/// Step 1's other short-circuit — [`AcousticRealization::AbsoluteHz`] —
/// bypasses everything else: the frequency is already fixed, so neither the
/// scope walk nor the built-in catalog is consulted at all.
/// [`AcousticRealization::CentsOffset`] is applied multiplicatively on top
/// of the resolved base frequency, per its own documented semantics ("an
/// explicit offset in cents from the tuning system's result").
///
/// Resolution of [`TuningResolution::Adaptive`]: an unregistered `function`
/// is a hard error (see `adaptive_anchor`); otherwise the anchor is
/// `context`'s tonal centre, or C when none is supplied, and the position
/// resolves through `ji_static_5limit_ratios(anchor)` — *exactly* the call
/// the three static `ji-static-5limit-*` built-ins make, transposed to a
/// runtime anchor rather than one of the three fixed ones. No second lattice
/// construction, no cents table.
pub fn resolve_pitch_frequency(
    score: &Score,
    pitch: &Pitch,
    voice: VoiceId,
    context: Option<&HarmonicContext>,
) -> Result<f64, TuningResolutionError> {
    if let AcousticRealization::AbsoluteHz(hz) = pitch.acoustic.realization {
        return Ok(hz.get());
    }
    let (region, staff, _instance) =
        locate_voice(score, voice).ok_or(TuningResolutionError::VoiceNotFound(voice))?;
    let resolved = resolve_tuning_scope(pitch, voice, staff, region, &score.tuning_context);
    let entry = built_in_tuning_system(&resolved.tuning_system).ok_or_else(|| {
        TuningResolutionError::UnknownTuningSystem(resolved.tuning_system.clone())
    })?;
    let mut system = match entry {
        TuningCatalogEntry::Resolved(system) => system,
        TuningCatalogEntry::Deferred(reason) => {
            return Err(TuningResolutionError::NotYetSupported {
                id: resolved.tuning_system,
                reason,
            });
        }
    };
    // req:tuning:tuning-system-compatibility (`:3581`): accept only exact
    // equality this tranche — see the module doc's compatibility note.
    if system.pitch_space != resolved.pitch_space {
        return Err(TuningResolutionError::IncompatiblePitchSpace {
            pitch_space: resolved.pitch_space,
            tuning_system_pitch_space: system.pitch_space,
        });
    }
    if let TuningResolution::Adaptive { function } = &system.resolution {
        let anchor = adaptive_anchor(function, context)?;
        // Reuse the static-JI construction verbatim, re-anchored at runtime
        // — see `ji_static_5limit_ratios`'s own doc for why this is the same
        // call the three static built-ins make, not a second construction.
        system.resolution = TuningResolution::PerPositionRatios(ji_static_5limit_ratios(anchor));
    }
    let base =
        frequency_for_position(&pitch.scale_position.position, &system, &resolved.reference)?;
    match pitch.acoustic.realization {
        AcousticRealization::Implicit => Ok(base),
        AcousticRealization::CentsOffset(c) => Ok(base * 2f64.powf(c.get() / 1200.0)),
        AcousticRealization::AbsoluteHz(_) => unreachable!("handled by the early return above"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::{
        Canvas, KeySignatureChange, MetricTimeModel, Region, RegionContent, RegionTimeModel,
        StaffBasedContent, StaffExtent, StaffInstance, TimeExtent, Voice,
    };
    use crate::ids::{IdentityContext, ReplicaId, StaffInstanceId};
    use crate::pitch::{AcousticPitch, CmnNominal, ScalePosition};
    use crate::time::WallClockTime;
    use epiphany_determinism::{Tolerance, ToleranceClass, ToleranceGovernance};

    fn cents(c: f64) -> Tolerance {
        Tolerance::absolute(
            ToleranceClass::AcousticCents,
            c,
            ToleranceGovernance::Validation,
        )
        .unwrap()
    }

    /// Absolute cents between two frequencies — the metric the proof-of-life
    /// tests assert against, per the contract's own instruction ("a
    /// cents-level check is right").
    fn cents_between(a: f64, b: f64) -> f64 {
        1200.0 * (a / b).log2().abs()
    }

    fn cmn_pitch(space: &str, nominal: CmnNominal, alteration: i8, octave: i8) -> Pitch {
        Pitch {
            scale_position: ScalePosition {
                space: PitchSpaceId::new(space),
                position: PitchSpacePosition::Cmn {
                    nominal,
                    alteration,
                    octave,
                },
            },
            acoustic: AcousticPitch {
                tuning: TuningReference::Inherit,
                realization: AcousticRealization::Implicit,
            },
        }
    }

    fn wc_extent(a: i64, b: i64) -> TimeExtent {
        TimeExtent {
            start: TimeAnchor::WallClock {
                time: WallClockTime(a),
            },
            end: TimeAnchor::WallClock {
                time: WallClockTime(b),
            },
        }
    }

    /// A `KeySignatureChange` anchored at wall-clock nanosecond `t`, for the
    /// adaptive-anchor-derivation tests below.
    fn key_change(t: i64, fifths: i8) -> KeySignatureChange {
        KeySignatureChange {
            anchor: TimeAnchor::WallClock {
                time: WallClockTime(t),
            },
            key: KeySignature::new(fifths).expect("fifths within -7..=7"),
        }
    }

    /// A resolver that never places a `TimeAnchor` on the musical timeline --
    /// sufficient for the wall-clock-only fixtures below, where
    /// `derive_tonal_centre` never needs it.
    fn no_musical_resolve(_: &TimeAnchor) -> Option<MusicalPosition> {
        None
    }

    /// A minimal score: one region, one staff instance, two voices — enough
    /// for `locate_voice` and the scope walk, nothing more.
    struct Fixture {
        score: Score,
        voice_a: VoiceId,
        voice_b: VoiceId,
    }

    fn fixture() -> Fixture {
        let r = ReplicaId(1);
        let voice_a = VoiceId::new(r, 1);
        let voice_b = VoiceId::new(r, 2);
        let staff = StaffId::new(r, 1);
        let mut instance = StaffInstance::new(StaffInstanceId::new(r, 1), staff);
        instance.voices = vec![Voice::user(voice_a), Voice::user(voice_b)];
        let region = Region {
            id: RegionId::new(r, 1),
            time_model: RegionTimeModel::Metric(MetricTimeModel::default()),
            content: RegionContent::StaffBased(StaffBasedContent {
                staff_instances: vec![instance],
                ..Default::default()
            }),
            time_extent: wc_extent(0, 1000),
            staff_extent: StaffExtent {
                staves: vec![staff],
            },
            local_tempo_map: None,
            permits_spanning_slurs: false,
        };
        let mut score = Score::empty(IdentityContext::new(r));
        score.canvas = Canvas {
            regions: vec![region],
            ..Default::default()
        };
        Fixture {
            score,
            voice_a,
            voice_b,
        }
    }

    // -- Proof of life 1: tet-12, A4 = 440 Hz -> C5. --------------------------

    #[test]
    fn tet12_a4_440_resolves_c5_to_523_2511_hz() {
        let f = fixture();
        let c5 = cmn_pitch("cmn-12", CmnNominal::C, 0, 5);
        let freq =
            resolve_pitch_frequency(&f.score, &c5, f.voice_a, None).expect("tet-12 resolves");
        assert!(
            cents(0.01).within(cents_between(freq, 523.2511), 0.0),
            "expected ~523.2511 Hz, got {freq}"
        );
    }

    // -- Proof of life 2: ji-static-5limit-C's major third differs from --
    // -- tet-12's by the syntonic comma (~13.7 c).                      --

    #[test]
    fn ji_static_5limit_c_major_third_distinct_from_tet12() {
        // Anchor the reference *at the tonic itself* (C4), so both systems'
        // "1/1" and the comparison point coincide: the ratio this measures
        // is then exactly each system's C-to-E interval, with no confound
        // from how the two systems otherwise retune A differently (both
        // being referenced against the *same* absolute A4 would silently
        // mix "how A retunes" into "how E retunes" — a trap this test's
        // first draft fell into).
        let f = fixture();
        let c4_ref = ReferencePitch::new(
            PitchSpacePosition::Cmn {
                nominal: CmnNominal::C,
                alteration: 0,
                octave: 4,
            },
            264.0,
        )
        .unwrap();
        let mut ji_score = f.score.clone();
        ji_score.tuning_context.default_tuning_system = TuningSystemId::new("ji-static-5limit-C");
        ji_score.tuning_context.reference = c4_ref.clone();
        let mut tet_score = f.score.clone();
        tet_score.tuning_context.reference = c4_ref;

        let e4 = cmn_pitch("cmn-12", CmnNominal::E, 0, 4);
        let ji_freq = resolve_pitch_frequency(&ji_score, &e4, f.voice_a, None)
            .expect("ji-static-5limit-C resolves");
        let tet_freq =
            resolve_pitch_frequency(&tet_score, &e4, f.voice_a, None).expect("tet-12 resolves");
        // Just major third 5/4 (386.31 c) vs equal-tempered (400 c): the just
        // third is *flatter*, by the syntonic comma (~13.7 c).
        assert!(
            ji_freq < tet_freq,
            "the just major third ({ji_freq} Hz) must be flatter than the equal-tempered one ({tet_freq} Hz)"
        );
        let diff = cents_between(ji_freq, tet_freq);
        assert!(
            cents(0.05).within(diff, 13.6864),
            "expected a ~13.6864 c syntonic-comma difference, got {diff}"
        );
    }

    #[test]
    fn ji_static_5limit_lattice_matches_the_published_construction() {
        // `core_spec.tex:4034-4046`'s table, spot-checked at all three
        // anchors: this proves the *code's* lattice-block construction
        // reproduces the specification's own worked values, rather than the
        // production code and the spec merely agreeing to look similar.
        let ratio_at = |system: &str, degree: i32| -> (i32, u32) {
            let TuningCatalogEntry::Resolved(sys) =
                built_in_tuning_system(&TuningSystemId::new(system)).unwrap()
            else {
                panic!("{system} must resolve")
            };
            let TuningResolution::PerPositionRatios(table) = sys.resolution else {
                panic!("{system} must be PerPositionRatios")
            };
            let entry = table.iter().find(|pr| pr.position == degree).unwrap();
            (entry.ratio.numerator, entry.ratio.denominator.get())
        };
        // ji-static-5limit-C: anchor at C (chromatic 0). C=1/1, E=5/4 (step 4), G=3/2 (step 7).
        assert_eq!(ratio_at("ji-static-5limit-C", 0), (1, 1));
        assert_eq!(ratio_at("ji-static-5limit-C", 4), (5, 4));
        assert_eq!(ratio_at("ji-static-5limit-C", 7), (3, 2));
        // ji-static-5limit-G: anchor at G (chromatic 7), so G itself is 1/1.
        assert_eq!(ratio_at("ji-static-5limit-G", 7), (1, 1));
        // ji-static-5limit-D: anchor at D (chromatic 2), so D itself is 1/1.
        assert_eq!(ratio_at("ji-static-5limit-D", 2), (1, 1));
    }

    // -- Proof of life 3: scope precedence. -----------------------------------

    #[test]
    fn voice_scope_override_changes_resolution_for_its_voice_but_not_others() {
        let mut f = fixture();
        f.score.tuning_context.overrides.push(TuningOverride {
            scope: TuningScope::Voice(f.voice_a),
            pitch_space: None,
            tuning_system: None,
            reference: Some(
                ReferencePitch::new(
                    PitchSpacePosition::Cmn {
                        nominal: CmnNominal::A,
                        alteration: 0,
                        octave: 4,
                    },
                    415.0,
                )
                .unwrap(),
            ),
        });
        let a4 = cmn_pitch("cmn-12", CmnNominal::A, 0, 4);
        let in_voice_a = resolve_pitch_frequency(&f.score, &a4, f.voice_a, None)
            .expect("resolves under the override");
        let in_voice_b = resolve_pitch_frequency(&f.score, &a4, f.voice_b, None)
            .expect("resolves under the default");
        assert!(
            cents(0.01).within(cents_between(in_voice_a, 415.0), 0.0),
            "voice A's own reference override must apply: got {in_voice_a}"
        );
        assert!(
            cents(0.01).within(cents_between(in_voice_b, 440.0), 0.0),
            "voice B must still see the score default (440 Hz): got {in_voice_b}"
        );
    }

    // -- Proof of life 4: compatibility rejects a mismatch. -------------------

    #[test]
    fn compatibility_check_rejects_a_pitch_space_mismatch() {
        let mut f = fixture();
        // tet-19's declared pitch_space is edo-19; the score's default pitch
        // space stays cmn-12 (unchanged) — a genuine, catchable mismatch.
        f.score.tuning_context.default_tuning_system = TuningSystemId::new("tet-19");
        let c5 = cmn_pitch("cmn-12", CmnNominal::C, 0, 5);
        let err = resolve_pitch_frequency(&f.score, &c5, f.voice_a, None)
            .expect_err("must reject the mismatch");
        assert!(
            matches!(err, TuningResolutionError::IncompatiblePitchSpace { .. }),
            "expected IncompatiblePitchSpace, got {err:?}"
        );
    }

    // -- Proof of life 5: `ji-adaptive-5limit` now resolves; a genuinely -----
    // -- unknown identifier still fails closed, distinctly.               ---

    #[test]
    fn ji_adaptive_5limit_resolves_and_unknown_ids_still_fail_closed() {
        // Push 4b's adaptive tranche: `ji-adaptive-5limit` is no longer
        // deferred (`TuningCatalogEntry::Deferred`) -- it resolves like every
        // other built-in, closing the last entry of the twenty-item catalog.
        // This test used to prove the opposite (a `NotYetSupported` error);
        // it inverts rather than being deleted, per the contract.
        let f = fixture();
        let c5 = cmn_pitch("cmn-12", CmnNominal::C, 0, 5);
        let mut score = f.score.clone();
        score.tuning_context.default_tuning_system = TuningSystemId::new("ji-adaptive-5limit");
        let freq = resolve_pitch_frequency(&score, &c5, f.voice_a, None)
            .expect("ji-adaptive-5limit must now resolve to a frequency");
        assert!(
            freq.is_finite() && freq > 0.0,
            "expected a real, positive frequency, got {freq}"
        );

        // A genuinely unknown identifier still reports differently, so the
        // two failure modes this test used to distinguish don't blur now
        // that the catalog's one deferred entry is gone.
        let mut score = f.score.clone();
        score.tuning_context.default_tuning_system = TuningSystemId::new("not-a-built-in-system");
        let err = resolve_pitch_frequency(&score, &c5, f.voice_a, None)
            .expect_err("unknown id must not resolve");
        assert!(matches!(err, TuningResolutionError::UnknownTuningSystem(_)));
    }

    // -- Extra: the whole-walk AbsoluteHz short-circuit. ----------------------

    #[test]
    fn absolute_hz_realization_short_circuits_the_whole_walk() {
        let mut f = fixture();
        // An unresolvable tuning system: if the shortcut were skipped, this
        // would return an error, not 500.0.
        f.score.tuning_context.default_tuning_system = TuningSystemId::new("not-a-built-in-system");
        let mut pinned = cmn_pitch("cmn-12", CmnNominal::C, 0, 5);
        pinned.acoustic.realization = AcousticRealization::absolute_hz(500.0).unwrap();
        let freq = resolve_pitch_frequency(&f.score, &pinned, f.voice_a, None)
            .expect("AbsoluteHz must resolve without consulting the tuning system at all");
        assert_eq!(freq, 500.0);
    }

    // -- Extra: an EDO built-in resolves through Integer positions. -----------

    #[test]
    fn tet_19_resolves_edo_integer_positions() {
        let mut f = fixture();
        f.score.tuning_context.default_pitch_space = PitchSpaceId::new("edo-19");
        f.score.tuning_context.default_tuning_system = TuningSystemId::new("tet-19");
        f.score.tuning_context.reference = ReferencePitch::new(
            PitchSpacePosition::Integer {
                space_size: 19,
                index: 0,
            },
            440.0,
        )
        .unwrap();
        let one_step = Pitch {
            scale_position: ScalePosition {
                space: PitchSpaceId::new("edo-19"),
                position: PitchSpacePosition::Integer {
                    space_size: 19,
                    index: 1,
                },
            },
            acoustic: AcousticPitch {
                tuning: TuningReference::Inherit,
                realization: AcousticRealization::Implicit,
            },
        };
        let freq =
            resolve_pitch_frequency(&f.score, &one_step, f.voice_a, None).expect("tet-19 resolves");
        let expected = 440.0 * 2f64.powf(1.0 / 19.0);
        assert!(
            cents(0.01).within(cents_between(freq, expected), 0.0),
            "expected ~{expected} Hz, got {freq}"
        );
    }

    // =========================================================================
    // Push 4b tranche 2b: the ten historical temperaments. This is the
    // tranche's reason to exist, per `spec/CONTRACT_PUSH4B_TEMPERAMENTS.md`'s
    // "Proof of life" section — every assertion below recomputes its expected
    // value from the walk (or from an exact comma ratio), never from a
    // hardcoded cents constant copied out of the spec's tables.
    // =========================================================================

    /// `1200 · log2(ratio)` for chromatic `degree` in `walk` — the cents this
    /// module's own walk assigns that degree, relative to C.
    fn cents_of(walk: &TemperamentWalk, degree: usize) -> f64 {
        1200.0 * walk.ratios[degree].log2()
    }

    // -- Closure 1: the six circulating temperaments sum to one Pythagorean --
    // -- comma, and none of their twelve fifths is a wolf. -------------------

    #[test]
    fn circulating_temperaments_close_to_one_pythagorean_comma_with_no_wolf() {
        let comma = pythagorean_comma_cents();
        let cases: [(&str, Construction); 6] = [
            ("werckmeister-iii", werckmeister_iii_construction()),
            ("werckmeister-iv", werckmeister_iv_construction()),
            ("vallotti", vallotti_construction()),
            ("kirnberger-ii", kirnberger_ii_construction()),
            ("kirnberger-iii", kirnberger_iii_construction()),
            ("young-ii", young_ii_construction()),
        ];
        for (name, construction) in cases {
            let walk = walk_temperament(&construction);
            // The sum of the twelve fifths' deviations from pure equals one
            // Pythagorean comma (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md`'s
            // closure invariant for the circulating six), recomputed from
            // the walk's raw (unreduced) closing cents rather than asserted
            // directly against a constant.
            let total_deviation = 12.0 * pure_fifth_cents() - walk.raw_closure_cents;
            assert!(
                (total_deviation - comma).abs() < 1e-9,
                "{name}: twelve fifths should deviate from pure by exactly one \
                 Pythagorean comma ({comma} c), computed {total_deviation} c"
            );
            // No fifth is a wolf: every one of the twelve stays within 15 c
            // of a pure fifth. 15 c comfortably exceeds this construction's
            // largest single tempering (kirnberger-ii's half-syntonic-comma
            // fifths, ~10.75 c) while comfortably staying below any real
            // historical wolf (pythagorean's is ~23.5 c narrow; meantone's
            // are 16-36 c wide).
            for (i, &fifth) in walk.fifth_cents.iter().enumerate() {
                let deviation = (fifth - pure_fifth_cents()).abs();
                assert!(
                    deviation < 15.0,
                    "{name}: chain arc {i} deviates from pure by {deviation} c -- too large, looks like a wolf"
                );
            }
        }
    }

    // -- Closure 2: the four non-circulating temperaments' residual wolf ------
    // -- matches the spec's ratified value.                                --

    #[test]
    fn noncirculating_temperaments_wolf_matches_the_ratified_spec_value() {
        // Chain arc 8 (`G♯–E♭`) is the one `Residual` arc in each of these
        // four constructions -- see each `*_construction` function's own
        // comment.
        const WOLF_ARC: usize = 8;
        let cases: [(&str, Construction, f64); 4] = [
            ("pythagorean", pythagorean_construction(), 678.495),
            ("meantone-1/4-comma", meantone_construction(0.25), 737.637),
            ("meantone-1/5-comma", meantone_construction(0.2), 725.809),
            (
                "meantone-1/6-comma",
                meantone_construction(1.0 / 6.0),
                717.923,
            ),
        ];
        for (name, construction, expected_wolf) in cases {
            let walk = walk_temperament(&construction);
            let wolf = walk.fifth_cents[WOLF_ARC];
            assert!(
                (wolf - expected_wolf).abs() < 0.001,
                "{name}: expected the wolf at {expected_wolf} c (`core_spec.tex`'s ratified value), computed {wolf} c"
            );
        }
    }

    // -- Discriminators: spot values the walk must reproduce against the -----
    // -- spec's own independently-derived cents tables.                   ----

    #[test]
    fn pythagorean_e_and_fsharp_match_the_published_cents() {
        let walk = walk_temperament(&pythagorean_construction());
        let e = cents_of(&walk, CmnNominal::E.chromatic() as usize);
        let fsharp = cents_of(&walk, 6); // F# = chromatic degree 6, no plain CmnNominal for it
        assert!(
            (e - 407.820).abs() < 0.001,
            "pythagorean E: expected 407.820 c, got {e}"
        );
        assert!(
            (fsharp - 611.730).abs() < 0.001,
            "pythagorean F#: expected 611.730 c, got {fsharp}"
        );
    }

    #[test]
    fn meantone_quarter_comma_major_third_c_to_e_is_the_just_5_4() {
        let walk = walk_temperament(&meantone_construction(0.25));
        let c = cents_of(&walk, CmnNominal::C.chromatic() as usize);
        let e = cents_of(&walk, CmnNominal::E.chromatic() as usize);
        let third = e - c;
        assert!(
            (third - 386.31).abs() < 0.01,
            "expected the just 5/4 major third (386.31 c), got {third} c"
        );
    }

    #[test]
    fn kirnberger_ii_and_iii_d_differ_though_the_chain_skeleton_is_the_same() {
        // Same chain skeleton (D-A/A-E tempered, F#-Db schisma-tempered),
        // different comma fraction -- a test that passes both proves the two
        // are not secretly the same construction.
        let ii = walk_temperament(&kirnberger_ii_construction());
        let iii = walk_temperament(&kirnberger_iii_construction());
        let d = CmnNominal::D.chromatic() as usize;
        let d_ii = cents_of(&ii, d);
        let d_iii = cents_of(&iii, d);
        assert!(
            (d_ii - 203.910).abs() < 0.001,
            "kirnberger-ii D: expected 203.910 c, got {d_ii}"
        );
        assert!(
            (d_iii - 193.157).abs() < 0.001,
            "kirnberger-iii D: expected 193.157 c, got {d_iii}"
        );
        assert!(
            (d_ii - d_iii).abs() > 1.0,
            "kirnberger-ii and -iii should disagree audibly on D, got {d_ii} vs {d_iii}"
        );
    }

    // -- Resolver-level: all ten resolve to a real frequency. -----------------

    #[test]
    fn all_ten_temperaments_resolve_and_produce_a_real_frequency() {
        const TEN: [&str; 10] = [
            "pythagorean",
            "meantone-1/4-comma",
            "meantone-1/5-comma",
            "meantone-1/6-comma",
            "werckmeister-iii",
            "werckmeister-iv",
            "vallotti",
            "kirnberger-ii",
            "kirnberger-iii",
            "young-ii",
        ];
        let f = fixture();
        let c5 = cmn_pitch("cmn-12", CmnNominal::C, 0, 5);
        for id in TEN {
            let mut score = f.score.clone();
            score.tuning_context.default_tuning_system = TuningSystemId::new(id);
            let freq = resolve_pitch_frequency(&score, &c5, f.voice_a, None)
                .unwrap_or_else(|e| panic!("{id} must resolve to a frequency, got error: {e}"));
            assert!(
                freq.is_finite() && freq > 0.0,
                "{id}: expected a real, positive frequency, got {freq}"
            );
        }
    }

    #[test]
    fn werckmeister_iii_c_sharp_resolves_distinctly_from_tet_12_c_sharp() {
        let f = fixture();
        let c_sharp = cmn_pitch("cmn-12", CmnNominal::C, 1, 5);
        let mut wm_score = f.score.clone();
        wm_score.tuning_context.default_tuning_system = TuningSystemId::new("werckmeister-iii");
        // `f.score` already defaults to tet-12.
        let wm_freq = resolve_pitch_frequency(&wm_score, &c_sharp, f.voice_a, None)
            .expect("werckmeister-iii resolves");
        let tet_freq =
            resolve_pitch_frequency(&f.score, &c_sharp, f.voice_a, None).expect("tet-12 resolves");
        let diff = cents_between(wm_freq, tet_freq);
        assert!(
            diff > 0.5,
            "expected werckmeister-iii's C# to differ audibly from tet-12's, diff was only {diff} c"
        );
    }

    // -- Fail-closed: an unreserved TuningFunctionId never yields a frequency. -

    #[test]
    fn unknown_tuning_function_id_fails_closed() {
        // Unit level: `coordinate_ratio` returns `None` directly -- the
        // extension point has no registry
        // (`spec/CONTRACT_PUSH4B_TEMPERAMENTS.md` item 1).
        let bogus_resolution = TuningResolution::Function {
            function: TuningFunctionId::new("not-a-real-temperament"),
            parameters: TuningParameters,
        };
        assert_eq!(coordinate_ratio(&bogus_resolution, 0), None);

        // Full-pipeline level: a `TuningSystem` carrying that resolution
        // never produces a frequency, only an error -- `PositionUnavailable`,
        // since `coordinate_ratio`'s `None` is exactly what
        // `frequency_for_position` reports that way for.
        let bogus_system = TuningSystem {
            id: TuningSystemId::new("bogus"),
            name: "bogus".to_owned(),
            pitch_space: PitchSpaceId::new("cmn-12"),
            resolution: bogus_resolution,
            description: None,
        };
        let c5_position = PitchSpacePosition::Cmn {
            nominal: CmnNominal::C,
            alteration: 0,
            octave: 5,
        };
        let err = frequency_for_position(&c5_position, &bogus_system, &ReferencePitch::a440())
            .expect_err("an unknown TuningFunctionId must never resolve to a frequency");
        assert!(
            matches!(err, TuningResolutionError::PositionUnavailable),
            "expected PositionUnavailable, got {err:?}"
        );
    }

    // =========================================================================
    // Push 4b's adaptive tranche: `ji-adaptive-5limit`, `HarmonicContext`, and
    // `derive_tonal_centre`. Every assertion below either recomputes its
    // expected value from the arithmetic (`req:tuning:adaptive-anchor-derivation`'s
    // `(7 * fifths).rem_euclid(12)`) or cross-checks against the already-tested
    // static-JI built-ins, never a hardcoded frequency copied from nowhere.
    // =========================================================================

    /// An `Adaptive` `frequency_for_position` reached directly (never through
    /// `resolve_pitch_frequency`, which always converts `Adaptive` to a
    /// concrete `PerPositionRatios` first) has no anchor to consult and must
    /// fail closed, not guess C -- the same shape as
    /// `unknown_tuning_function_id_fails_closed`.
    #[test]
    fn adaptive_resolution_reaching_frequency_for_position_directly_fails_closed() {
        assert_eq!(
            coordinate_ratio(
                &TuningResolution::Adaptive {
                    function: AdaptiveTuningFunctionId::new("default-v1"),
                },
                0,
            ),
            None
        );

        let system = TuningSystem {
            id: TuningSystemId::new("bogus-adaptive"),
            name: "bogus-adaptive".to_owned(),
            pitch_space: PitchSpaceId::new("cmn-12"),
            resolution: TuningResolution::Adaptive {
                function: AdaptiveTuningFunctionId::new("default-v1"),
            },
            description: None,
        };
        let c5_position = PitchSpacePosition::Cmn {
            nominal: CmnNominal::C,
            alteration: 0,
            octave: 5,
        };
        let err = frequency_for_position(&c5_position, &system, &ReferencePitch::a440())
            .expect_err("an unconverted Adaptive resolution must never resolve to a frequency");
        assert!(
            matches!(err, TuningResolutionError::PositionUnavailable),
            "expected PositionUnavailable, got {err:?}"
        );
    }

    /// `req:tuning:adaptive-default-version`'s final clause: an unregistered
    /// `AdaptiveTuningFunctionId` is a hard error, never a silent fallback to
    /// the C default.
    #[test]
    fn unregistered_adaptive_function_id_fails_closed_with_no_fallback_to_c() {
        let bogus = AdaptiveTuningFunctionId::new("not-default-v1");
        let err = adaptive_anchor(&bogus, None)
            .expect_err("an unregistered adaptive function id must be a hard error");
        assert!(
            matches!(err, TuningResolutionError::UnregisteredAdaptiveFunction(_)),
            "expected UnregisteredAdaptiveFunction, got {err:?}"
        );
    }

    /// The spec-mandated default (`core_spec.tex:3452-3453`): no context, or
    /// a context whose `tonal_centre` is `None`, resolves at anchor C --
    /// bit-identical to `ji-static-5limit-C`'s result for the same position.
    /// This pins the default against a future "fail closed when context is
    /// missing" refactor -- a missing tonal centre is not an error.
    #[test]
    fn adaptive_with_no_context_defaults_to_c_matching_ji_static_5limit_c() {
        let f = fixture();
        let e4 = cmn_pitch("cmn-12", CmnNominal::E, 0, 4);

        let mut adaptive_score = f.score.clone();
        adaptive_score.tuning_context.default_tuning_system =
            TuningSystemId::new("ji-adaptive-5limit");
        let mut static_score = f.score.clone();
        static_score.tuning_context.default_tuning_system =
            TuningSystemId::new("ji-static-5limit-C");

        let no_context = resolve_pitch_frequency(&adaptive_score, &e4, f.voice_a, None)
            .expect("adaptive resolves with no context at all");
        let empty_tonal_centre = resolve_pitch_frequency(
            &adaptive_score,
            &e4,
            f.voice_a,
            Some(&HarmonicContext { tonal_centre: None }),
        )
        .expect("adaptive resolves with an empty tonal centre");
        let static_c = resolve_pitch_frequency(&static_score, &e4, f.voice_a, None)
            .expect("ji-static-5limit-C resolves");

        assert_eq!(
            no_context.to_bits(),
            static_c.to_bits(),
            "no context: {no_context} != ji-static-5limit-C's {static_c}"
        );
        assert_eq!(
            empty_tonal_centre.to_bits(),
            static_c.to_bits(),
            "empty tonal centre: {empty_tonal_centre} != ji-static-5limit-C's {static_c}"
        );
    }

    /// The transposition identity the reviewer will check by hand: adaptive
    /// at anchor 0 (C) matches `ji-static-5limit-C` position-for-position,
    /// and at anchor 7 (G) matches `ji-static-5limit-G` -- because adaptive
    /// resolution *is* `ji_static_5limit_ratios` called with a runtime
    /// anchor, not a second construction.
    #[test]
    fn adaptive_transposition_identity_matches_static_c_and_g() {
        let f = fixture();
        let naturals = [
            CmnNominal::C,
            CmnNominal::D,
            CmnNominal::E,
            CmnNominal::F,
            CmnNominal::G,
            CmnNominal::A,
            CmnNominal::B,
        ];
        for (anchor_pc, static_id) in [(0u8, "ji-static-5limit-C"), (7u8, "ji-static-5limit-G")] {
            let mut adaptive_score = f.score.clone();
            adaptive_score.tuning_context.default_tuning_system =
                TuningSystemId::new("ji-adaptive-5limit");
            let mut static_score = f.score.clone();
            static_score.tuning_context.default_tuning_system = TuningSystemId::new(static_id);
            let ctx = HarmonicContext {
                tonal_centre: ChromaticPitchClass::new(anchor_pc),
            };
            for nominal in naturals {
                let pitch = cmn_pitch("cmn-12", nominal, 0, 4);
                let adaptive_freq =
                    resolve_pitch_frequency(&adaptive_score, &pitch, f.voice_a, Some(&ctx))
                        .unwrap_or_else(|e| panic!("adaptive must resolve {nominal:?}: {e}"));
                let static_freq = resolve_pitch_frequency(&static_score, &pitch, f.voice_a, None)
                    .unwrap_or_else(|e| panic!("{static_id} must resolve {nominal:?}: {e}"));
                assert_eq!(
                    adaptive_freq.to_bits(),
                    static_freq.to_bits(),
                    "anchor {anchor_pc}, {nominal:?}: adaptive {adaptive_freq} != {static_id} {static_freq}"
                );
            }
        }
    }

    /// `req:tuning:adaptive-default-version`'s closing clause: "No adjustment
    /// is ever carried forward from a previous resolution." Resolve the same
    /// position repeatedly, interleaved with other resolutions in between --
    /// a stateful (comma-drifting) implementation would let those leak in;
    /// this one never does, by construction (every call recomputes from the
    /// anchor and reference alone).
    #[test]
    fn adaptive_resolution_is_comma_drift_free_by_shape() {
        let f = fixture();
        let mut score = f.score.clone();
        score.tuning_context.default_tuning_system = TuningSystemId::new("ji-adaptive-5limit");
        let e4 = cmn_pitch("cmn-12", CmnNominal::E, 0, 4);
        let c4 = cmn_pitch("cmn-12", CmnNominal::C, 0, 4);
        let g4 = cmn_pitch("cmn-12", CmnNominal::G, 0, 4);
        let ctx = HarmonicContext {
            tonal_centre: ChromaticPitchClass::new(0),
        };

        let first =
            resolve_pitch_frequency(&score, &e4, f.voice_a, Some(&ctx)).expect("adaptive resolves");
        let _ =
            resolve_pitch_frequency(&score, &c4, f.voice_a, Some(&ctx)).expect("adaptive resolves");
        let _ =
            resolve_pitch_frequency(&score, &g4, f.voice_a, Some(&ctx)).expect("adaptive resolves");
        let second =
            resolve_pitch_frequency(&score, &e4, f.voice_a, Some(&ctx)).expect("adaptive resolves");
        let _ =
            resolve_pitch_frequency(&score, &g4, f.voice_a, Some(&ctx)).expect("adaptive resolves");
        let _ =
            resolve_pitch_frequency(&score, &c4, f.voice_a, Some(&ctx)).expect("adaptive resolves");
        let third =
            resolve_pitch_frequency(&score, &e4, f.voice_a, Some(&ctx)).expect("adaptive resolves");

        assert_eq!(first.to_bits(), second.to_bits());
        assert_eq!(second.to_bits(), third.to_bits());
    }

    /// `req:tuning:adaptive-anchor-derivation`: mode is not consulted.
    /// `KeySignature` carries no mode field at all, so `fifths = 0` anchors
    /// at C whether the prevailing key is conceived as C major or A minor --
    /// there is no mode input `derive_tonal_centre` could consult even if it
    /// tried. This pins the arithmetic for that no-mode-information case.
    #[test]
    fn key_signature_zero_fifths_anchors_at_c_regardless_of_mode() {
        let mut f = fixture();
        {
            let region = &mut f.score.canvas.regions[0];
            let instances = region.content.staff_instances_mut().unwrap();
            instances[0].key_sequence = vec![key_change(0, 0)];
        }
        let onset = EventPosition::WallClock(WallClockTime(500));
        let pc = derive_tonal_centre(&f.score, f.voice_a, &onset, no_musical_resolve)
            .expect("must resolve")
            .expect("a prevailing key exists");
        assert_eq!(
            pc.get(),
            0,
            "fifths=0 must anchor at C (pc 0), got {}",
            pc.get()
        );
    }

    /// `req:tuning:adaptive-anchor-derivation`'s sign-bug trap: `fifths = -1`
    /// (one flat) gives `(7 * -1).rem_euclid(12) = 5` (F). A buggy `%`
    /// implementation would yield `-7`, an invalid pitch class -- this test
    /// dies under that mutation.
    #[test]
    fn flat_key_signature_anchor_uses_rem_euclid_not_percent() {
        let mut f = fixture();
        {
            let region = &mut f.score.canvas.regions[0];
            let instances = region.content.staff_instances_mut().unwrap();
            instances[0].key_sequence = vec![key_change(0, -1)];
        }
        let onset = EventPosition::WallClock(WallClockTime(100));
        let pc = derive_tonal_centre(&f.score, f.voice_a, &onset, no_musical_resolve)
            .expect("must resolve")
            .expect("a prevailing key exists");
        assert_eq!(pc.get(), 5, "expected F (pc 5), got {}", pc.get());
    }

    /// `req:tuning:adaptive-anchor-derivation`'s "prevailing" selection: with
    /// two `KeySignatureChange`s on a staff, a pitch between them takes the
    /// earlier; a pitch after both takes the later.
    #[test]
    fn prevailing_key_signature_selects_the_latest_at_or_before_onset() {
        let mut f = fixture();
        {
            let region = &mut f.score.canvas.regions[0];
            let instances = region.content.staff_instances_mut().unwrap();
            instances[0].key_sequence = vec![
                key_change(0, 0),   // C major/A minor at t=0.
                key_change(500, 1), // G major/E minor at t=500.
            ];
        }

        let onset_between = EventPosition::WallClock(WallClockTime(250));
        let pc = derive_tonal_centre(&f.score, f.voice_a, &onset_between, no_musical_resolve)
            .expect("must resolve")
            .expect("a prevailing key exists");
        assert_eq!(
            pc.get(),
            0,
            "a pitch between the two changes takes the earlier (C)"
        );

        let onset_after = EventPosition::WallClock(WallClockTime(600));
        let pc = derive_tonal_centre(&f.score, f.voice_a, &onset_after, no_musical_resolve)
            .expect("must resolve")
            .expect("a prevailing key exists");
        assert_eq!(
            pc.get(),
            7,
            "a pitch after both changes takes the later (G)"
        );
    }

    /// No key-signature change at all ⇒ no tonal centre -- `Ok(None)`, never
    /// an error. `resolve_pitch_frequency` is the one that defaults to C;
    /// `derive_tonal_centre` just reports what it found.
    #[test]
    fn no_key_signature_change_yields_no_tonal_centre() {
        let f = fixture();
        let onset = EventPosition::WallClock(WallClockTime(0));
        let centre = derive_tonal_centre(&f.score, f.voice_a, &onset, no_musical_resolve)
            .expect("must resolve (absence of a tonal centre is not an error)");
        assert_eq!(centre, None);
    }

    /// Cross-clock and indirect anchors fail closed with a distinct,
    /// reported error rather than a guessed ordering.
    #[test]
    fn anchor_ordering_fails_closed_on_cross_clock_and_indirect_anchors() {
        let mut f = fixture();
        {
            let region = &mut f.score.canvas.regions[0];
            let instances = region.content.staff_instances_mut().unwrap();
            // A wall-clock-anchored key change, but the pitch's onset is
            // musical: cross-clock, unorderable.
            instances[0].key_sequence = vec![key_change(0, 0)];
        }
        let musical_onset = EventPosition::Musical(MusicalPosition::origin());
        let err = derive_tonal_centre(&f.score, f.voice_a, &musical_onset, no_musical_resolve)
            .expect_err("a wall-clock anchor against a musical onset must not silently order");
        assert!(
            matches!(err, TuningResolutionError::AnchorNotOrderable { .. }),
            "expected AnchorNotOrderable, got {err:?}"
        );

        // An indirect (Event-anchored) key change, with a musical onset and
        // a resolver that declines to place it.
        let mut f2 = fixture();
        {
            let region = &mut f2.score.canvas.regions[0];
            let instances = region.content.staff_instances_mut().unwrap();
            instances[0].key_sequence = vec![KeySignatureChange {
                anchor: TimeAnchor::Event {
                    id: crate::ids::EventId::new(ReplicaId(1), 1),
                    offset: crate::time::AnchorOffset::Zero,
                },
                key: KeySignature::new(0).unwrap(),
            }];
        }
        let err = derive_tonal_centre(&f2.score, f2.voice_a, &musical_onset, no_musical_resolve)
            .expect_err("an indirect anchor the resolver declines must not silently order");
        assert!(
            matches!(err, TuningResolutionError::AnchorNotOrderable { .. }),
            "expected AnchorNotOrderable, got {err:?}"
        );
    }

    /// `derive_tonal_centre` reports the same caller error
    /// (`VoiceNotFound`) as `resolve_pitch_frequency` for an orphaned voice.
    #[test]
    fn derive_tonal_centre_reports_voice_not_found_for_an_orphaned_voice() {
        let f = fixture();
        let orphan = VoiceId::new(ReplicaId(1), 999);
        let onset = EventPosition::WallClock(WallClockTime(0));
        let err = derive_tonal_centre(&f.score, orphan, &onset, no_musical_resolve)
            .expect_err("an orphaned voice must not resolve");
        assert!(matches!(err, TuningResolutionError::VoiceNotFound(_)));
    }
}
