G-pass follow-up: unsupported pre-pass algorithm ids error

Post-commit review finding: req:pitch:spelling-algorithm /
req:time:decomposition-algorithm ratified MUST-error for unregistered
algorithm ids, but derive_annotations kept the pre-ratification
derive-nothing-under-honest-profile behavior (and a test locked it).
The spec text stands; the code moves: derive_annotations returns
Result<DerivedAnnotations, PrePassError>, rejecting unregistered ids
up front — a silently-empty derivation is indistinguishable from a
legitimately empty score, and would silently disagree with an
implementation that does support the requested id. All production
callers use the default profile (.expect); the stale lock test is
rewritten as unknown_algorithm_ids_error; PrePassError re-exported.

Full gate green: fmt, clippy -D warnings, rustdoc -D warnings, 30
workspace suites, conformance scale 1 (8/8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
This commit is contained in:
Levi Neuwirth 2026-07-07 11:32:05 -04:00
parent e4edea6a3e
commit 93f3dfde93
10 changed files with 191 additions and 88 deletions

View File

@ -438,3 +438,22 @@ code tranche (authored-only resolution paths + distinct taxonomy buckets in
both pre-passes). Also ratified here: system-derived intrinsic content is
immutable under reduction (P12-K3; core Ch5 states it, the catalog pins the
precondition, `epiphany-ops` implements).
### G-pass follow-up (2026-07-07): unsupported algorithm ids now ERROR
A post-commit review found the ratified requirement and the implementation
disagreeing: `req:pitch:spelling-algorithm` / `req:time:decomposition-algorithm`
say a profile requesting an unregistered id MUST **error**, but
`derive_annotations` still implemented the pre-ratification behavior (that
pre-pass "derives nothing" while the requested id stays in the result profile
— the honest-cache-key rationale), and a test locked it. The MUST-error
contract is the right one and stands: a silently-empty derivation is
indistinguishable from a legitimately empty score, and an implementation that
*does* support the requested algorithm would produce real annotations while
this one quietly produced none — two "successful" results that disagree.
Fixed: `derive_annotations` returns `Result<DerivedAnnotations, PrePassError>`
(`UnsupportedSpellingAlgorithm` / `UnsupportedDecompositionAlgorithm`,
rejected up front); every production caller uses the default profile and
`.expect`s; the stale test is rewritten as `unknown_algorithm_ids_error`.
CONFORMANCE.md's long-standing "errors" claim is now true rather than
aspirational.

View File

@ -87,7 +87,8 @@ pub use pitch::{
pub use prepass::{
derive_annotations, resolve_decomposition, resolve_spelling, simplest_spelling,
DerivedAnnotations, PrePassProfile, ResolvedSpelling, SpellingProvenance, TaxonomyReport,
DerivedAnnotations, PrePassError, PrePassProfile, ResolvedSpelling, SpellingProvenance,
TaxonomyReport,
};
pub use event::{

View File

@ -245,11 +245,71 @@ impl DerivedAnnotations {
// Entry point
// ===========================================================================
/// Why a derivation was refused: the profile names an algorithm this
/// implementation does not provide. Ratified Pass 12
/// (`req:pitch:spelling-algorithm`, `req:time:decomposition-algorithm`): a
/// profile requesting an unregistered identifier MUST **error** — neither
/// silently substituting the default nor returning an empty derivation a
/// caller cannot distinguish from a legitimately empty score. Erroring also
/// fails loudly where silence would diverge: an implementation that *does*
/// support the requested algorithm produces real annotations, so a quietly
/// empty result here would disagree with it while both looked successful.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum PrePassError {
/// The profile's spelling algorithm id is not a registered algorithm.
UnsupportedSpellingAlgorithm(SpellingAlgorithmId),
/// The profile's decomposition algorithm id is not a registered algorithm.
UnsupportedDecompositionAlgorithm(DecompositionAlgorithmId),
}
impl std::fmt::Display for PrePassError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PrePassError::UnsupportedSpellingAlgorithm(id) => {
write!(f, "unsupported spelling algorithm id {:?}", id.as_str())
}
PrePassError::UnsupportedDecompositionAlgorithm(id) => {
write!(
f,
"unsupported decomposition algorithm id {:?}",
id.as_str()
)
}
}
}
}
impl std::error::Error for PrePassError {}
/// Computes the [`DerivedAnnotations`] for `score` under `profile`. Pure and
/// deterministic: byte-identical `(score, profile)` yields byte-identical
/// output, independent of iteration incidentals (the derivation walks the graph
/// in canonical id/position order).
pub fn derive_annotations(score: &Score, profile: &PrePassProfile) -> DerivedAnnotations {
///
/// # Errors
///
/// A profile naming an algorithm this implementation does not register errors
/// with [`PrePassError`] (ratified Pass 12; see the type's documentation) —
/// nothing is silently substituted, and no empty-but-successful derivation is
/// produced. The default profile always succeeds.
pub fn derive_annotations(
score: &Score,
profile: &PrePassProfile,
) -> Result<DerivedAnnotations, PrePassError> {
// The pre-passes implement exactly one algorithm each (the registered
// `"default"` id); any other id refuses up front per the ratified
// requirement.
if profile.spelling_algorithm != SpellingAlgorithmId::default_id() {
return Err(PrePassError::UnsupportedSpellingAlgorithm(
profile.spelling_algorithm.clone(),
));
}
if profile.decomposition_algorithm != DecompositionAlgorithmId::default_id() {
return Err(PrePassError::UnsupportedDecompositionAlgorithm(
profile.decomposition_algorithm.clone(),
));
}
let mut taxonomy = TaxonomyReport::default();
// Index regions: event -> (is the owning region metric?), and the event's
@ -257,19 +317,9 @@ pub fn derive_annotations(score: &Score, profile: &PrePassProfile) -> DerivedAnn
// position-sorted (invariant 3), so the per-voice order is canonical.
let layout = ScoreLayout::build(score);
// The pre-passes implement exactly one algorithm each (the registered
// `"default"` id). A profile requesting any other id is **not honored**:
// rather than return default output labeled as the requested algorithm —
// which would let an unimplemented/future algorithm silently alias the
// default in a derivation cache — that pre-pass derives nothing. The
// requested id stays in the result's `profile`, so the cache key is honest.
let spelling_supported = profile.spelling_algorithm == SpellingAlgorithmId::default_id();
let decomposition_supported =
profile.decomposition_algorithm == DecompositionAlgorithmId::default_id();
// --- Spelling pre-pass. ---
let mut spellings = BTreeMap::new();
if spelling_supported {
{
let inferred = infer_spellings(score, &layout, &mut taxonomy);
let precedence = &score.spelling_precedence;
for (pid, inferred_spelling) in inferred {
@ -313,7 +363,7 @@ pub fn derive_annotations(score: &Score, profile: &PrePassProfile) -> DerivedAnn
// --- Decomposition pre-pass. ---
let mut decompositions = BTreeMap::new();
if decomposition_supported {
{
let inferred = infer_decompositions(score, &layout, &mut taxonomy);
for (eid, inferred_attachment) in inferred {
let resolved = resolve_decomposition(score, eid, inferred_attachment);
@ -342,12 +392,12 @@ pub fn derive_annotations(score: &Score, profile: &PrePassProfile) -> DerivedAnn
}
}
DerivedAnnotations {
Ok(DerivedAnnotations {
spellings,
decompositions,
taxonomy,
profile: profile.clone(),
}
})
}
// ===========================================================================

View File

@ -564,7 +564,8 @@ fn derive_counts_sub_sixtyfourth_event_as_ungriddable() {
(vec![ev], vec![])
});
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(ann.spellings.len(), 1, "pitch still spells");
assert_eq!(
ann.decompositions.len(),
@ -598,7 +599,8 @@ fn off_grid_position_is_ungriddable_not_downbeat_aligned() {
(vec![ev], vec![])
});
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(ann.spellings.len(), 1, "pitch still spells");
assert_eq!(
ann.decompositions.len(),
@ -653,7 +655,8 @@ fn derive_spells_and_decomposes_a_simple_metric_score() {
(events, vec![])
});
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
// Every pitch got a spelling; every event a single-quarter decomposition.
assert_eq!(ann.spellings.len(), 4);
assert_eq!(ann.decompositions.len(), 4);
@ -705,7 +708,8 @@ fn taxonomy_classifies_each_kind_and_counts_ineligible_explicitly() {
(vec![p, rst, unp, ji], vec![])
});
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
let t = &ann.taxonomy;
assert_eq!(t.pitched_events, 2);
assert_eq!(t.rest_events, 1);
@ -793,7 +797,8 @@ fn nonmetric_region_defers_decomposition_but_still_spells() {
..Default::default()
};
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(ann.spellings.len(), 1, "pitch is still spelled");
assert_eq!(
ann.decompositions.len(),
@ -962,8 +967,10 @@ fn derivation_is_deterministic_across_runs() {
})
};
// Same identity seed → identical scores → identical annotations, twice.
let a = derive_annotations(&build(), &PrePassProfile::default());
let b = derive_annotations(&build(), &PrePassProfile::default());
let a = derive_annotations(&build(), &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
let b = derive_annotations(&build(), &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(a, b);
}
@ -999,7 +1006,8 @@ fn tuplet_member_event_decomposes_with_tuplet_membership() {
(events, vec![tuplet])
});
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(ann.decompositions.len(), 3);
for dec in ann.decompositions.values() {
assert_eq!(dec.components.len(), 1);
@ -1110,7 +1118,8 @@ fn authored_decomposition_outranks_the_inferred_default() {
let authored = tied_quarters(e1, DecompositionSource::UserChosen);
score.decomposition_attachments.push(authored.clone());
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
// The authored attachment is the effective decomposition for its event; no
// derived (single-half-note) decomposition shadows it.
assert_eq!(ann.decompositions[&e1], authored, "authored override wins");
@ -1139,7 +1148,8 @@ fn inferred_source_attachment_does_not_outrank_the_prepass() {
.decomposition_attachments
.push(tied_quarters(e1, DecompositionSource::Inferred));
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
let dec = &ann.decompositions[&e1];
assert_eq!(dec.components.len(), 1, "the pre-pass's half note stands");
assert_eq!(dec.components[0].base_value, NoteValue::Half);
@ -1184,7 +1194,8 @@ fn authored_decomposition_surfaces_for_an_ungriddable_event_p12_h7() {
};
score.decomposition_attachments.push(authored.clone());
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(
ann.decompositions[&eid], authored,
"the authored attachment surfaces on its own"
@ -1243,7 +1254,8 @@ fn inferred_source_attachment_does_not_surface_for_ineligible_events_p12_h7() {
source: DecompositionSource::Inferred,
});
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert!(
!ann.decompositions.contains_key(&eid),
"an Inferred-source attachment does not surface"
@ -1300,7 +1312,8 @@ fn authored_spelling_surfaces_for_an_unavailable_pitch_p12_h7() {
layer: None,
});
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
let resolved = ann
.spellings
.get(&pid)
@ -1319,7 +1332,8 @@ fn authored_spelling_surfaces_for_an_unavailable_pitch_p12_h7() {
!ann.spellings.contains_key(&dangling),
"an attachment on an absent pitch surfaces nothing"
);
let again = derive_annotations(&score, &PrePassProfile::default());
let again = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(
ann.canonical_fingerprint(),
again.canonical_fingerprint(),
@ -1341,7 +1355,8 @@ fn decomposition_precedence_ranks_sources_then_canonical_order() {
let user = tied_quarters(e1, DecompositionSource::UserChosen);
score.decomposition_attachments.push(imported); // listed first — must lose
score.decomposition_attachments.push(user.clone());
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(
ann.decompositions[&e1], user,
"UserChosen outranks Imported regardless of attachment order"
@ -1355,7 +1370,8 @@ fn decomposition_precedence_ranks_sources_then_canonical_order() {
let second = dotted_quarter_eighth(f1, DecompositionSource::UserChosen);
score2.decomposition_attachments.push(first.clone());
score2.decomposition_attachments.push(second);
let ann2 = derive_annotations(&score2, &PrePassProfile::default());
let ann2 = derive_annotations(&score2, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(
ann2.decompositions[&f1], first,
"rank ties keep the earlier attachment in canonical order"
@ -1374,13 +1390,16 @@ fn derivation_with_authored_decomposition_is_deterministic() {
.push(tied_quarters(e1, DecompositionSource::UserChosen));
score
};
let a = derive_annotations(&build(), &PrePassProfile::default());
let b = derive_annotations(&build(), &PrePassProfile::default());
let a = derive_annotations(&build(), &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
let b = derive_annotations(&build(), &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(a, b);
assert_eq!(a.canonical_fingerprint(), b.canonical_fingerprint());
let (plain, _, _) = two_half_note_score();
let c = derive_annotations(&plain, &PrePassProfile::default());
let c = derive_annotations(&plain, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_ne!(
a.canonical_fingerprint(),
c.canonical_fingerprint(),
@ -1417,7 +1436,8 @@ fn authored_attachment_on_an_ungriddable_event_surfaces_p12_h7() {
let authored = tied_quarters(ids[0], DecompositionSource::UserChosen);
score.decomposition_attachments.push(authored.clone());
let ann = derive_annotations(&score, &PrePassProfile::default());
let ann = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert_eq!(
ann.decompositions.get(&ids[0]),
Some(&authored),
@ -1430,10 +1450,14 @@ fn authored_attachment_on_an_ungriddable_event_surfaces_p12_h7() {
}
#[test]
fn unknown_algorithm_ids_are_not_honored() {
// A profile requesting an algorithm the pre-pass does not implement must not
// receive default output labeled as that algorithm; the unhonored pre-pass
// derives nothing while the requested id stays in the result profile.
fn unknown_algorithm_ids_error() {
// Ratified Pass 12 (`req:pitch:spelling-algorithm`,
// `req:time:decomposition-algorithm`): a profile requesting an algorithm
// this implementation does not register MUST error — neither silently
// substituting the default nor returning an empty-but-successful
// derivation a caller cannot tell apart from a legitimately empty score.
// (This supersedes the pre-ratification "derives nothing under the
// requested profile" behavior this test previously locked.)
let score = metric_score(|idc, voice| {
let eid = idc.mint();
let pid = idc.mint::<PitchId>();
@ -1450,54 +1474,35 @@ fn unknown_algorithm_ids_are_not_honored() {
(vec![ev], vec![])
});
let default = derive_annotations(&score, &PrePassProfile::default());
let default = derive_annotations(&score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
assert!(!default.spellings.is_empty(), "the default profile spells");
assert!(
!default.decompositions.is_empty(),
"the default profile decomposes"
);
// Unknown spelling algorithm: no spellings; decomposition (still default) runs.
let unknown_spelling = PrePassProfile {
spelling_algorithm: SpellingAlgorithmId::new("future-v2"),
decomposition_algorithm: DecompositionAlgorithmId::default_id(),
};
let a = derive_annotations(&score, &unknown_spelling);
assert!(
a.spellings.is_empty(),
"an unknown spelling algorithm is not honored (no default output)"
);
assert_eq!(
a.decompositions.len(),
default.decompositions.len(),
"the supported decomposition pre-pass still runs"
);
assert_eq!(
a.profile.spelling_algorithm,
SpellingAlgorithmId::new("future-v2"),
"the requested id is preserved in the result profile"
);
// The canonical fingerprint distinguishes differing derivations (it is not a
// degenerate constant the determinism gate would pass vacuously).
assert_ne!(
a.canonical_fingerprint(),
default.canonical_fingerprint(),
"the canonical fingerprint discriminates differing annotations"
derive_annotations(&score, &unknown_spelling),
Err(PrePassError::UnsupportedSpellingAlgorithm(
SpellingAlgorithmId::new("future-v2")
)),
"an unknown spelling algorithm errors; nothing is substituted"
);
// Unknown decomposition algorithm: no decompositions; spelling runs.
let unknown_decomp = PrePassProfile {
spelling_algorithm: SpellingAlgorithmId::default_id(),
decomposition_algorithm: DecompositionAlgorithmId::new("future-v2"),
};
let b = derive_annotations(&score, &unknown_decomp);
assert!(
b.decompositions.is_empty(),
"an unknown decomposition algorithm is not honored"
);
assert_eq!(
b.spellings.len(),
default.spellings.len(),
"the supported spelling pre-pass still runs"
derive_annotations(&score, &unknown_decomp),
Err(PrePassError::UnsupportedDecompositionAlgorithm(
DecompositionAlgorithmId::new("future-v2")
)),
"an unknown decomposition algorithm errors; nothing is substituted"
);
}

View File

@ -1458,7 +1458,8 @@ impl EditorSession {
/// inferred respelling is accounted for. Falls back to a note's raw pitch position
/// when it has no resolved CMN spelling. `None` if the event has no CMN note.
fn rendered_top_of_event(&self, event: EventId) -> Option<(Pitch, i64)> {
let annotations = derive_annotations(&self.score, &PrePassProfile::default());
let annotations = derive_annotations(&self.score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
let mut buf: Vec<&IdentifiedPitch> = Vec::new();
self.score
.events

View File

@ -409,7 +409,8 @@ pub fn to_logical(score: &Score) -> LogicalLayoutIR {
// (Agent H's pre-pass): which notehead a note draws, where its pitches sit,
// and which accidentals its spelling carries. Recomputed deterministically
// from the score with the default profile.
let annotations = derive_annotations(score, &PrePassProfile::default());
let annotations = derive_annotations(score, &PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
// The last region index that manifests each staff, so a measure can tell a
// mid-staff region boundary (continuation) from the true final barline.
let mut staff_last_region: BTreeMap<StaffId, usize> = BTreeMap::new();

View File

@ -3031,7 +3031,8 @@ fn a_mid_region_meter_change_reduces_cleanly_p12_c5() {
assert!(check_invariants(&result.score).is_empty());
// The pre-pass tolerates the multi-meter grid (P12-H4 owns honoring it).
let _ =
epiphany_core::derive_annotations(&result.score, &epiphany_core::PrePassProfile::default());
epiphany_core::derive_annotations(&result.score, &epiphany_core::PrePassProfile::default())
.expect("the default pre-pass algorithms are supported");
}
#[test]

View File

@ -1231,7 +1231,8 @@ pub fn classify_corpus() -> CorpusReport {
);
// 2. Derive H's annotations + taxonomy.
let ann = derive_annotations(&score, &profile);
let ann = derive_annotations(&score, &profile)
.expect("the default pre-pass algorithms are supported");
let t = &ann.taxonomy;
// 3. Cross-check H's event-kind counts against an independent walk.

View File

@ -56,8 +56,10 @@ pub fn fingerprint(ann: &DerivedAnnotations) -> Vec<u8> {
/// The same score derives identically twice (pure function), by structural
/// equality and by fingerprint.
pub fn assert_derivation_deterministic(score: &Score) {
let a = derive_annotations(score, &profile());
let b = derive_annotations(score, &profile());
let a = derive_annotations(score, &profile())
.expect("the default pre-pass algorithms are supported");
let b = derive_annotations(score, &profile())
.expect("the default pre-pass algorithms are supported");
assert_eq!(a, b, "derivation is not a pure function of the score");
assert_eq!(
fingerprint(&a),
@ -71,8 +73,10 @@ pub fn assert_derivation_deterministic(score: &Score) {
/// pre-pass annotations" property at corpus scale.
pub fn assert_corpus_deterministic() {
for f in corpus::corpus() {
let a = derive_annotations(&(f.build)(), &profile());
let b = derive_annotations(&(f.build)(), &profile());
let a = derive_annotations(&(f.build)(), &profile())
.expect("the default pre-pass algorithms are supported");
let b = derive_annotations(&(f.build)(), &profile())
.expect("the default pre-pass algorithms are supported");
assert_eq!(a, b, "fixture `{}` derivation not deterministic", f.name);
assert_eq!(
fingerprint(&a),
@ -280,7 +284,8 @@ pub fn assert_respell_precedence() {
let replica = score.identity.replica_id;
// Baseline: no override → the algorithm's inferred spelling stands.
let base = derive_annotations(&score, &profile());
let base = derive_annotations(&score, &profile())
.expect("the default pre-pass algorithms are supported");
let base_rs = base.spellings.get(&pid).expect("probe pitch is spelled");
assert!(
matches!(base_rs.provenance, SpellingProvenance::Inferred),
@ -304,7 +309,8 @@ pub fn assert_respell_precedence() {
priority: 0,
layer: None,
});
let ann2 = derive_annotations(&s2, &profile());
let ann2 =
derive_annotations(&s2, &profile()).expect("the default pre-pass algorithms are supported");
let rs2 = ann2
.spellings
.get(&pid)
@ -332,7 +338,8 @@ pub fn assert_respell_precedence() {
priority: 0,
layer: Some(AnalysisLayerId::new(replica, 1)),
});
let ann3 = derive_annotations(&s3, &profile());
let ann3 =
derive_annotations(&s3, &profile()).expect("the default pre-pass algorithms are supported");
let rs3 = ann3.spellings.get(&pid).expect("pitch is spelled");
assert_eq!(
rs3.spelling, inferred,
@ -375,7 +382,8 @@ pub fn assert_respell_precedence() {
priority: 9, // second, higher priority — must win
layer: None,
});
let ann4 = derive_annotations(&s4, &profile());
let ann4 =
derive_annotations(&s4, &profile()).expect("the default pre-pass algorithms are supported");
let rs4 = ann4.spellings.get(&pid).expect("pitch is spelled");
assert_eq!(
rs4.spelling, fs4,
@ -400,7 +408,8 @@ pub fn assert_respell_precedence() {
priority: 100,
layer: None,
});
let ann5 = derive_annotations(&s5, &profile());
let ann5 =
derive_annotations(&s5, &profile()).expect("the default pre-pass algorithms are supported");
let rs5 = ann5.spellings.get(&pid).expect("pitch is spelled");
assert_eq!(
rs5.spelling, inferred,
@ -428,6 +437,7 @@ pub fn assert_reduced_respell_is_honored() {
// Inferred baseline (no override).
let inferred = derive_annotations(&score, &profile())
.expect("the default pre-pass algorithms are supported")
.spellings
.get(&pid)
.expect("probe pitch is spelled")
@ -463,7 +473,8 @@ pub fn assert_reduced_respell_is_honored() {
set.accept(respell);
let reduced = set.reduce_onto(&score);
let ann = derive_annotations(&reduced.score, &profile());
let ann = derive_annotations(&reduced.score, &profile())
.expect("the default pre-pass algorithms are supported");
let rs = ann
.spellings
.get(&pid)
@ -513,7 +524,8 @@ fn build_named(name: &str) -> Score {
/// `BTreeMap` value order is melodic), the inferred spellings match the expected
/// `(nominal, accidental)` sequence.
fn assert_line(name: &str, expected: &[(CmnNominal, &str)]) {
let ann = derive_annotations(&build_named(name), &profile());
let ann = derive_annotations(&build_named(name), &profile())
.expect("the default pre-pass algorithms are supported");
let got: Vec<(CmnNominal, String)> = ann
.spellings
.values()
@ -739,7 +751,8 @@ pub fn assert_deterministic_in_materialization_pipeline(scale: u64) {
seed.wrapping_mul(0x9E37_79B9).wrapping_add(101),
);
assert_derivation_deterministic(&score);
let ann = derive_annotations(&score, &profile());
let ann = derive_annotations(&score, &profile())
.expect("the default pre-pass algorithms are supported");
if !ann.spellings.is_empty() {
spelled_anywhere = true;
}
@ -767,7 +780,8 @@ pub fn run_all(scale: u64) {
// Eligibility + reconstruction over every fixture.
for f in corpus::corpus() {
let score = (f.build)();
let ann = derive_annotations(&score, &profile());
let ann = derive_annotations(&score, &profile())
.expect("the default pre-pass algorithms are supported");
assert_eligible_pitches_spelled(&score, &ann);
assert_decompositions_reconstruct(&score, &ann);
}

View File

@ -167,3 +167,13 @@ references.
buckets (`prepass.rs`); K3 `SystemDerivedContentImmutable` (12); K9
`RecreateContentMismatch` (13); C4 `SameCanvasNearer` (6) — each with
regression tests and wire goldens.
### G-pass addendum (2026-07-07): pre-pass error contract aligned
A post-commit review caught a spec↔code disagreement introduced by the H1/H4
ratifications: the requirement text (MUST error on an unregistered algorithm
id) had been adopted from CONFORMANCE.md's claim, but the implementation still
returned a successful empty derivation under the requested profile. Resolution:
the **spec text stands**; the code moved — `derive_annotations` now returns
`Result` and rejects unregistered ids up front (`PrePassError`). No spec
change; core DECISIONS records the reasoning.