Text-projection parity: smufl and overrides project, and a persistence gap surfaces
Schema major 3 put `smufl` and `overrides` on the binary wire and left the text surface alone. This closes that: `TextValue for ScoreTuningContext` projects five fields in `fn enc` order, with four new leaf impls (SmuflVersion, SmuflVersionRequirement, TuningScope, TuningOverride) mirroring the four Codec impls 3b-i froze. The rule that moved them is the one this projection always followed — the text projection is the same canonical surface the binary codec is. What changed was not the rule but a premise: the old doc comment excluded all three fields because "no schema major 3 has been opened", and 3b-i opened it. `accidental_extensions` was staged out of that major and stays in-memory only, so it is still correctly absent — the staging line now falls in the same place on both surfaces, which is what the inverted tests assert. No header-version bump and no document vector moved. This is a value-projection change, not a document-format one: epiphany-textproj has no reference to a tuning context anywhere, and the canonical base projects as a single opaque byte atom (req:textproj:base-snapshot-inline), not structurally. So req:textproj:header-version's sole accepted version (0 7 0) is untouched, all 13 document vectors are unchanged, and req:textproj:roundtrip was never violated — the tuning context is not part of the projected document. Both tests were renamed and inverted, following 3b-i's treatment of the binary pair rather than rewriting the tranche-2/3a decision entries. Mutation-verified: making `overrides` project as an always-empty vector — the exact regression this fixes — fails both. Two stale-pointer classes fixed, both of the kind this pass was hunting: the comment in codec.rs naming the old text-test names and claiming the text surface was unchanged; and ScoreTuningContext's per-field docs in graph.rs, which still read "In memory only this tranche" for `smufl` and `overrides` — stale since 3b-i, and contradicting the struct-level wire note they point at. The struct-level doc was already correct. Filed P13-S13, found while scoping and deliberately not fixed here: the tuning context has NO canonical persistence path. Its only *persisted* carrier is the acceleration snapshot, which Chapter 8 makes non-canonical and regenerable; no canonical carrier embeds it at all. No operation authors it (the ops vocabulary's only `tuning` references are the per-pitch TuningReference::Inherit) and MaterializedState does not carry it. So a per-voice tuning override survives a snapshot round-trip but cannot be authored, replicated, or merged, which makes req:tuning:tuning-resolution-order's scopes 2-4 unreachable from any exchangeable document. Not a regression — nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it. The candidate prices both routes. Ratification is about the carrier, not the encoding — the layouts are frozen and correct either way — but the operation route is not quite the free schema-minor it first looks: blocks stamp minimally, and bundle.rs caps OperationEnvelopeBlock at major 2 precisely because no operation payload embeds the tuning context. An authoring operation carrying a TuningOverride would stamp v3, dragging a role accept-set raise along with the kind append. Gate: fmt clean, clippy 0, 1336 passed / 0 failed, doc 0, conformance 8/8, requirement labels 6/6 at 212/282/282. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
parent
85d8af612f
commit
6d37f39826
|
|
@ -1317,3 +1317,51 @@ live `Codec::dec` killed six tests at once (`v0_score_migrates_*`,
|
|||
`v1_round_trips_non_default_values_for_every_new_field`) — confirming the
|
||||
reroute is load-bearing across the whole frozen-decoder family, not just the
|
||||
two sites the contract named.
|
||||
|
||||
## Text-projection parity: `smufl` and `overrides` project (2026-07-24)
|
||||
|
||||
Schema major 3 (tranche 3b-i) put `smufl` and `overrides` on the binary wire
|
||||
but deliberately left the text surface alone, deferring it. This closes that
|
||||
gap: `TextValue for ScoreTuningContext` now projects five fields, in `fn enc`
|
||||
order, and four new `TextValue` impls carry the leaves
|
||||
(`SmuflVersion`, `SmuflVersionRequirement`, `TuningScope`, `TuningOverride` —
|
||||
mirroring the four `Codec` impls 3b-i froze).
|
||||
|
||||
**The rule that moved them is the one this projection always followed** — the
|
||||
text projection is the same canonical surface the binary codec is. What
|
||||
changed is not the rule but a premise: the old doc comment justified excluding
|
||||
all three fields on the grounds that "no schema major 3 has been opened", and
|
||||
3b-i opened it. `accidental_extensions` was *staged* out of that major and is
|
||||
still in-memory only, so it is still correctly absent — the staging line now
|
||||
falls in exactly the same place on both surfaces, which is what the new tests
|
||||
assert.
|
||||
|
||||
**No header-version bump, and no document vector moved.** This is a
|
||||
value-projection change, not a document-format change: `epiphany-textproj` has
|
||||
no reference to a tuning context anywhere, and the canonical base is projected
|
||||
as one opaque byte atom (`req:textproj:base-snapshot-inline`), not
|
||||
structurally. So `req:textproj:header-version`'s single accepted version
|
||||
`(0 7 0)` is untouched and all 13 document vectors are unchanged.
|
||||
`req:textproj:roundtrip` was never violated — the tuning context is not part
|
||||
of the projected document — so this is a consistency fix on a value impl, not
|
||||
a conformance repair.
|
||||
|
||||
**Test renames** (following 3b-i's own treatment of the binary pair, rather
|
||||
than rewriting the tranche-2/3a entries above):
|
||||
`score_tuning_context_round_trips_and_overrides_do_not_project` →
|
||||
`..._and_overrides_project`, with its equality assertion inverted to
|
||||
`assert_ne!` plus a full round-trip; and
|
||||
`score_tuning_context_accidental_extensions_smufl_and_overrides_do_not_project`
|
||||
→ `score_tuning_context_projects_smufl_and_overrides_but_not_accidental_extensions`,
|
||||
now the text analogue of the binary staging-boundary test. Both were
|
||||
mutation-verified: making `overrides` project as an always-empty vector — the
|
||||
exact regression this pass fixes — fails both.
|
||||
|
||||
**Found while scoping, not fixed here:** the tuning context has **no canonical
|
||||
persistence path at all**. No operation authors it (`epiphany-ops` has no
|
||||
tuning-context payload; the only `tuning` references are per-pitch
|
||||
`TuningReference::Inherit`), and `MaterializedState` does not carry it. Major 3
|
||||
made the *acceleration snapshot* able to carry it, and that snapshot is
|
||||
explicitly non-canonical and regenerable. So a tuning override survives a
|
||||
snapshot round-trip but cannot be authored, replicated, or merged. Filed as a
|
||||
Pass-13 candidate; it is a data-model question, not a codec one.
|
||||
|
|
|
|||
|
|
@ -1952,11 +1952,14 @@ impl Codec for TuningOverride {
|
|||
// drift when this live impl changes. A round-trip test just below
|
||||
// (`score_tuning_context_smufl_and_overrides_reach_the_wire_accidental_extensions_do_not`)
|
||||
// proves `smufl`/`overrides` now survive `enc`→`dec` while
|
||||
// `accidental_extensions` still does not. The matching text-projection
|
||||
// proofs are unchanged by this tranche — `textvalue_graph.rs`'s
|
||||
// `score_tuning_context_round_trips_and_overrides_do_not_project` and
|
||||
// `score_tuning_context_accidental_extensions_smufl_and_overrides_do_not_project`
|
||||
// (text projection is a separate surface; see this tranche's contract).
|
||||
// `accidental_extensions` still does not. The text projection now draws the
|
||||
// staging line in the same place, and its proofs are the direct analogues:
|
||||
// `textvalue_graph.rs`'s `score_tuning_context_round_trips_and_overrides_project`
|
||||
// and `score_tuning_context_projects_smufl_and_overrides_but_not_accidental_extensions`.
|
||||
// (3b-i left the text surface untouched, deferring it; the parity pass that
|
||||
// followed moved `smufl`/`overrides` there for the reason stated in
|
||||
// `TextValue for ScoreTuningContext`'s doc — the text projection is the same
|
||||
// canonical surface this codec is.)
|
||||
impl Codec for ScoreTuningContext {
|
||||
fn enc(&self, out: &mut Vec<u8>) {
|
||||
self.default_pitch_space.enc(out);
|
||||
|
|
|
|||
|
|
@ -1669,17 +1669,24 @@ pub struct ScoreTuningContext {
|
|||
pub default_tuning_system: TuningSystemId,
|
||||
pub reference: ReferencePitch,
|
||||
/// Score-local accidental-registry extensions (Chapter 4 §"Score-Local
|
||||
/// Extensions", `:3228`). **In memory only** this tranche (Push 4b
|
||||
/// tranche 3a) — see the wire note above.
|
||||
/// Extensions", `:3228`). **In memory only**: staged out of schema major 3
|
||||
/// and appended after `overrides` whenever a later major lands it — see
|
||||
/// the wire note above. Neither the binary codec nor the text projection
|
||||
/// carries it.
|
||||
pub accidental_extensions: Vec<crate::accidental::ScoreAccidentalExtensions>,
|
||||
/// The SMuFL version this score requires (Chapter 4 §"SMuFL Versioning",
|
||||
/// `req:tuning:smufl-version-fallback`). **In memory only** this
|
||||
/// tranche — see the wire note above.
|
||||
/// `req:tuning:smufl-version-fallback`). **On the wire** as of schema
|
||||
/// major 3 (tranche 3b-i), and projected to text alongside it — see the
|
||||
/// wire note above.
|
||||
pub smufl: crate::accidental::SmuflVersionRequirement,
|
||||
/// Per-scope overrides consulted by the tuning resolver
|
||||
/// (`crate::tuning::resolve_pitch_frequency`), scopes 2-4 of
|
||||
/// `req:tuning:tuning-resolution-order`. **In memory only** this
|
||||
/// tranche — see the struct doc above.
|
||||
/// `req:tuning:tuning-resolution-order`. **On the wire** as of schema
|
||||
/// major 3 (tranche 3b-i), and projected to text alongside it — see the
|
||||
/// wire note above. Note that reaching the wire is not the same as being
|
||||
/// canonically persistable: no operation authors this field and the
|
||||
/// canonical base does not carry it, so today it survives only in the
|
||||
/// (regenerable) acceleration snapshot — P13-S13.
|
||||
pub overrides: Vec<crate::tuning::TuningOverride>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -291,22 +291,133 @@ impl TextValue for TimeSignature {
|
|||
// not a validating constructor.
|
||||
// ===========================================================================
|
||||
|
||||
/// `(score-tuning-context <default-pitch-space> <default-tuning-system>
|
||||
/// <reference>)` — exactly the three wire fields, in `fn enc` order
|
||||
/// (Push 4b tranche 2, `spec/CONTRACT_PUSH4B_RESOLVER.md`).
|
||||
/// `(smufl-version <major> <minor-centi>)`.
|
||||
///
|
||||
/// `ScoreTuningContext` has gained three fields beyond these three —
|
||||
/// `overrides` (Push 4b tranche 2), then `accidental_extensions` and `smufl`
|
||||
/// (Push 4b tranche 3a, `spec/CONTRACT_PUSH4B_ACCIDENTALS.md`) — that are
|
||||
/// deliberately **not** part of this projection: all three are in-memory only
|
||||
/// (no schema major 3 has been opened), so none may reach the wire, and the
|
||||
/// text projection is the same canonical surface the binary codec is — a
|
||||
/// value that omitted them here would otherwise silently launder a
|
||||
/// non-empty context into one indistinguishable from an empty one, which is
|
||||
/// exactly the intended behavior, not an oversight. `parse` always
|
||||
/// constructs `accidental_extensions: Vec::new()`, `smufl:
|
||||
/// SmuflVersionRequirement::default()`, and `overrides: Vec::new()`,
|
||||
/// mirroring `Codec::dec`.
|
||||
/// `minor_centi` projects as the stored, already-normalized value (1.4 is
|
||||
/// `40`, 1.12 is `12`), not as the decimal digits a human writes — the text
|
||||
/// projection carries the canonical value, exactly as `Codec` does. Parsing
|
||||
/// validates no further than the binary decoder does, for the same reason.
|
||||
impl TextValue for crate::accidental::SmuflVersion {
|
||||
fn project(&self) -> Sexp {
|
||||
Sexp::List(vec![
|
||||
Sexp::Symbol(kebab("SmuflVersion")),
|
||||
TextValue::project(&self.major),
|
||||
TextValue::project(&self.minor_centi),
|
||||
])
|
||||
}
|
||||
fn parse(s: &Sexp) -> Result<Self, TextError> {
|
||||
let fields = s.expect_struct(&kebab("SmuflVersion"), 2)?;
|
||||
Ok(crate::accidental::SmuflVersion {
|
||||
major: <u16 as TextValue>::parse(&fields[0])?,
|
||||
minor_centi: <u16 as TextValue>::parse(&fields[1])?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `(smufl-version-requirement <minimum> <authored-against>)` — the two fields
|
||||
/// in `fn enc` order.
|
||||
impl TextValue for crate::accidental::SmuflVersionRequirement {
|
||||
fn project(&self) -> Sexp {
|
||||
Sexp::List(vec![
|
||||
Sexp::Symbol(kebab("SmuflVersionRequirement")),
|
||||
self.minimum.project(),
|
||||
self.authored_against.project(),
|
||||
])
|
||||
}
|
||||
fn parse(s: &Sexp) -> Result<Self, TextError> {
|
||||
let fields = s.expect_struct(&kebab("SmuflVersionRequirement"), 2)?;
|
||||
Ok(crate::accidental::SmuflVersionRequirement {
|
||||
minimum: TextValue::parse(&fields[0])?,
|
||||
authored_against: TextValue::parse(&fields[1])?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The tuning-scope union, in the discriminant order `Codec` assigns
|
||||
/// (`Voice` 0, `Staff` 1, `Region` 2, `Range` 3).
|
||||
impl TextValue for crate::tuning::TuningScope {
|
||||
fn project(&self) -> Sexp {
|
||||
use crate::tuning::TuningScope as S;
|
||||
match self {
|
||||
S::Voice(id) => variant("Voice", vec![id.project()]),
|
||||
S::Staff(id) => variant("Staff", vec![id.project()]),
|
||||
S::Region(id) => variant("Region", vec![id.project()]),
|
||||
S::Range { start, end, voices } => variant(
|
||||
"Range",
|
||||
vec![start.project(), end.project(), voices.project()],
|
||||
),
|
||||
}
|
||||
}
|
||||
fn parse(s: &Sexp) -> Result<Self, TextError> {
|
||||
use crate::tuning::TuningScope as S;
|
||||
let (ctor, fields) = split_variant(s)?;
|
||||
if ctor == kebab("Voice") {
|
||||
let f = fields_of(fields, "TuningScope", 1)?;
|
||||
Ok(S::Voice(TextValue::parse(&f[0])?))
|
||||
} else if ctor == kebab("Staff") {
|
||||
let f = fields_of(fields, "TuningScope", 1)?;
|
||||
Ok(S::Staff(TextValue::parse(&f[0])?))
|
||||
} else if ctor == kebab("Region") {
|
||||
let f = fields_of(fields, "TuningScope", 1)?;
|
||||
Ok(S::Region(TextValue::parse(&f[0])?))
|
||||
} else if ctor == kebab("Range") {
|
||||
let f = fields_of(fields, "TuningScope", 3)?;
|
||||
Ok(S::Range {
|
||||
start: TextValue::parse(&f[0])?,
|
||||
end: TextValue::parse(&f[1])?,
|
||||
voices: TextValue::parse(&f[2])?,
|
||||
})
|
||||
} else {
|
||||
Err(TextError::UnknownConstructor {
|
||||
type_name: "TuningScope",
|
||||
found: ctor.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `(tuning-override <scope> <pitch-space> <tuning-system> <reference>)` — the
|
||||
/// four fields in `fn enc` order; the last three are optional and inherit from
|
||||
/// the next-outer scope when absent.
|
||||
impl TextValue for crate::tuning::TuningOverride {
|
||||
fn project(&self) -> Sexp {
|
||||
Sexp::List(vec![
|
||||
Sexp::Symbol(kebab("TuningOverride")),
|
||||
self.scope.project(),
|
||||
self.pitch_space.project(),
|
||||
self.tuning_system.project(),
|
||||
self.reference.project(),
|
||||
])
|
||||
}
|
||||
fn parse(s: &Sexp) -> Result<Self, TextError> {
|
||||
let fields = s.expect_struct(&kebab("TuningOverride"), 4)?;
|
||||
Ok(crate::tuning::TuningOverride {
|
||||
scope: TextValue::parse(&fields[0])?,
|
||||
pitch_space: TextValue::parse(&fields[1])?,
|
||||
tuning_system: TextValue::parse(&fields[2])?,
|
||||
reference: TextValue::parse(&fields[3])?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `(score-tuning-context <default-pitch-space> <default-tuning-system>
|
||||
/// <reference> <smufl> <overrides>)` — exactly the five wire fields, in
|
||||
/// `fn enc` order.
|
||||
///
|
||||
/// **`smufl` and `overrides` project; `accidental_extensions` does not**, and
|
||||
/// the rule dividing them is the one this projection has always followed: the
|
||||
/// text projection is the same canonical surface the binary codec is. Schema
|
||||
/// major 3 (Push 4b tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`) put
|
||||
/// `smufl` and `overrides` on the wire, so they belong here too — omitting
|
||||
/// them would silently launder a loaded context into one indistinguishable
|
||||
/// from an empty one. `accidental_extensions` was deliberately **staged** out
|
||||
/// of that major and remains in-memory only, so it is still correctly absent;
|
||||
/// `parse` constructs it as `Vec::new()`, mirroring `Codec::dec`. When a later
|
||||
/// major puts it on the wire, it joins this projection then and not before.
|
||||
///
|
||||
/// (An earlier revision of this comment justified excluding all three on the
|
||||
/// grounds that "no schema major 3 has been opened". That premise expired when
|
||||
/// 3b-i opened it; the rule it appealed to is what moved two of the three in.)
|
||||
impl TextValue for ScoreTuningContext {
|
||||
fn project(&self) -> Sexp {
|
||||
Sexp::List(vec![
|
||||
|
|
@ -314,20 +425,24 @@ impl TextValue for ScoreTuningContext {
|
|||
self.default_pitch_space.project(),
|
||||
self.default_tuning_system.project(),
|
||||
self.reference.project(),
|
||||
self.smufl.project(),
|
||||
self.overrides.project(),
|
||||
])
|
||||
}
|
||||
fn parse(s: &Sexp) -> Result<Self, TextError> {
|
||||
let fields = s.expect_struct(&kebab("ScoreTuningContext"), 3)?;
|
||||
let fields = s.expect_struct(&kebab("ScoreTuningContext"), 5)?;
|
||||
let default_pitch_space = TextValue::parse(&fields[0])?;
|
||||
let default_tuning_system = TextValue::parse(&fields[1])?;
|
||||
let reference = TextValue::parse(&fields[2])?;
|
||||
let smufl = TextValue::parse(&fields[3])?;
|
||||
let overrides = TextValue::parse(&fields[4])?;
|
||||
Ok(ScoreTuningContext {
|
||||
default_pitch_space,
|
||||
default_tuning_system,
|
||||
reference,
|
||||
accidental_extensions: Vec::new(),
|
||||
smufl: crate::accidental::SmuflVersionRequirement::default(),
|
||||
overrides: Vec::new(),
|
||||
smufl,
|
||||
overrides,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -935,14 +1050,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn score_tuning_context_round_trips_and_overrides_do_not_project() {
|
||||
fn score_tuning_context_round_trips_and_overrides_project() {
|
||||
// Empty overrides: ordinary identity round-trip.
|
||||
round_trip(ScoreTuningContext::default());
|
||||
|
||||
// Non-empty overrides: the text projection is identical to the
|
||||
// empty-overrides projection (the field is in-memory only, Push 4b
|
||||
// tranche 2 / Ruling C), and parsing always reconstructs `overrides`
|
||||
// as empty.
|
||||
// Non-empty overrides: as of schema major 3 (Push 4b tranche 3b-i)
|
||||
// `overrides` is on the wire, so it projects and survives the round
|
||||
// trip. This assertion is the **inversion** of the pre-major-3 one it
|
||||
// replaces, which required the two projections to be identical.
|
||||
let mut with_overrides = ScoreTuningContext::default();
|
||||
with_overrides
|
||||
.overrides
|
||||
|
|
@ -952,20 +1067,24 @@ mod tests {
|
|||
tuning_system: None,
|
||||
reference: None,
|
||||
});
|
||||
assert_eq!(
|
||||
assert_ne!(
|
||||
with_overrides.project().render(),
|
||||
ScoreTuningContext::default().project().render(),
|
||||
"overrides must not appear in the text projection"
|
||||
"overrides must appear in the text projection as of schema major 3"
|
||||
);
|
||||
round_trip(with_overrides.clone());
|
||||
let parsed = ScoreTuningContext::parse(&with_overrides.project()).unwrap();
|
||||
assert!(parsed.overrides.is_empty());
|
||||
assert_eq!(parsed.overrides, with_overrides.overrides);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_tuning_context_accidental_extensions_smufl_and_overrides_do_not_project() {
|
||||
// The direct analogue of `score_tuning_context_round_trips_and_overrides_do_not_project`
|
||||
// above, extended to all three in-memory-only fields (Push 4b tranche
|
||||
// 3a, `spec/CONTRACT_PUSH4B_ACCIDENTALS.md`).
|
||||
fn score_tuning_context_projects_smufl_and_overrides_but_not_accidental_extensions() {
|
||||
// The text analogue of the binary staging-boundary test
|
||||
// (`codec::tests::score_tuning_context_smufl_and_overrides_reach_the_wire_accidental_extensions_do_not`):
|
||||
// schema major 3 put `smufl` and `overrides` on the wire, so both
|
||||
// project; `accidental_extensions` was staged out of that major and is
|
||||
// still in-memory only, so it must still be dropped. This proves the
|
||||
// staging line falls in exactly the same place on both surfaces.
|
||||
use crate::accidental::{PitchSpaceModification, SmuflVersion, SmuflVersionRequirement};
|
||||
|
||||
let mut loaded = ScoreTuningContext::default();
|
||||
|
|
@ -985,15 +1104,30 @@ mod tests {
|
|||
tuning_system: None,
|
||||
reference: None,
|
||||
});
|
||||
assert_eq!(
|
||||
// Sanity: the fixture differs from the default in all three fields, so
|
||||
// neither half of the assertion below is vacuous.
|
||||
assert!(!loaded.accidental_extensions.is_empty());
|
||||
assert_ne!(loaded.smufl, SmuflVersionRequirement::default());
|
||||
assert!(!loaded.overrides.is_empty());
|
||||
|
||||
assert_ne!(
|
||||
loaded.project().render(),
|
||||
ScoreTuningContext::default().project().render(),
|
||||
"accidental_extensions, smufl, and overrides must not appear in the text projection"
|
||||
"smufl and overrides must appear in the text projection as of schema major 3"
|
||||
);
|
||||
let parsed = ScoreTuningContext::parse(&loaded.project()).unwrap();
|
||||
// smufl and overrides survive the round trip...
|
||||
assert_eq!(parsed.smufl, loaded.smufl);
|
||||
assert_eq!(parsed.overrides, loaded.overrides);
|
||||
// ...but accidental_extensions is still dropped: staged, never projected.
|
||||
assert!(parsed.accidental_extensions.is_empty());
|
||||
assert_eq!(parsed.smufl, SmuflVersionRequirement::default());
|
||||
assert!(parsed.overrides.is_empty());
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ScoreTuningContext {
|
||||
accidental_extensions: Vec::new(),
|
||||
..loaded.clone()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -69,8 +69,10 @@ whose obvious representation orders SMuFL's real release history backwards. All
|
|||
three are ratified; each freezes forever once tranche 3b lands, so they were
|
||||
resolved before dispatch, not after.
|
||||
|
||||
S4, S8, and S9 remain open; S10–S12 are resolved on ratification. The pass stays
|
||||
open.
|
||||
S4, S8, S9, and S13 remain open; S10–S12 are resolved on ratification. The pass
|
||||
stays open. (S13 was filed 2026-07-24, after S10–S12's wire freeze landed: it is
|
||||
the gap that freeze made visible — a value with a wire form and no canonical
|
||||
carrier to reach it.)
|
||||
|
||||
| Id | One-line statement | Filed in | Status |
|
||||
|---|---|---|---|
|
||||
|
|
@ -86,3 +88,4 @@ open.
|
|||
| P13-S10 | **`PitchSpaceModification::Cents(f64)` puts a raw `f64` in canonical state, which the byte layer cannot encode.** `core_spec.tex:3112` declares `Cents(f64)`; `PitchSpaceModification` is reached from canonical score state through `AccidentalDefinition` → `ScoreAccidentalExtensions` → `ScoreTuningContext`. `req:determinism:canonical-floating-point` requires canonical stored floats to be finite IEEE 754 binary64, and the byte layer enforces it with no escape hatch: `serialize.rs:110` decodes floats *only* through `CanonicalF64::from_le_bytes → NonFiniteFloat`, so there is no `Codec for f64`. A raw `f64` is therefore not merely risky in canonical state — it is unencodable without inventing a new unvalidated codec. The same chapter already resolved this exact tension forty lines later: `EngravingBoundingBox` (`:3150`) carries `SpaceUnit` edges with a rationale (`:3191`) invoking the same requirement | this file (verified 2026-07-23 against `core_spec.tex:3112`, `:3150-3197`, and `crates/epiphany-determinism/src/serialize.rs:110`) | **resolved** (ratified: `Cents(CanonicalF64)`. A one-line spec correction, the only shape consistent with the existing byte layer — the identical maneuver Ruling D applied to the bounding box. Freezes in tranche 3b) |
|
||||
| P13-S11 | **`AnchorPoint`, referenced by `AccidentalEngraving.anchor`, is defined nowhere.** `core_spec.tex:3166` names `pub anchor: AnchorPoint`; no struct or enum of that name exists in the spec, and `epiphany-core` (whose `Cargo.toml` has no `epiphany-layout-ir` dependency) cannot borrow any layout-ir type even if one shared the name — the same core-native requirement that forced `EngravingBoundingBox`. An undefined leaf frozen onto the wire is the `KeyContext`-shaped gap. Compounding it: the bounding box is documented "relative to the glyph's anchor point" (`:3160`), so freezing an anchor with no defined coordinate frame freezes a point with an undefined origin | this file (verified 2026-07-23; `AnchorPoint` appears once in `core_spec.tex`, defined nowhere; `epiphany-core/Cargo.toml` lists no layout-ir) | **resolved** (ratified: core-native `AnchorPoint { x: SpaceUnit, y: SpaceUnit }`, over the same `SpaceUnit` as `advance_width` and `EngravingBoundingBox`. **Plus one normative sentence pinning the frame**: x/y in canonical space units, y-up, relative to the glyph's coordinate origin — matching the repo's existing Bravura outline convention — so the anchor and the box it anchors share an unambiguous origin. Freezes in tranche 3b) |
|
||||
| P13-S12 | **`SmuflVersion` is undefined, and its obvious representation orders SMuFL's real history backwards.** `SmuflVersionRequirement` (`core_spec.tex:3269`) carries `minimum`/`authored_against` of type `SmuflVersion`, which Chapter 4 references but does not define (it exists only as a **Chapter 7 / layout-ir** type, `glyph.rs:29`, with literal-minor encoding — see the resolution). Ordering is load-bearing (`SmuflVersionRequirement.minimum`, `:3271`, gates the fallback of `req:tuning:smufl-version-fallback`), and the type is dual-purpose — it also anchors Chapter 9's `GlyphCatalog::smufl_version()` / `GlyphCatalogIdentity` (`:10420`, `:10460`), so this freeze touches layout-conformance identity, not just tuning. The trap: SMuFL versions are decimal fractions — 1.12 (2015), 1.18, 1.20 (2016), 1.3 (2019), 1.4 (2021) — that succeed by fraction (0.12 < 0.18 < 0.20 < 0.30 < 0.40). A `{ major: u16, minor: u16 }` storing literal digits with derived `Ord` orders the minors 3 < 4 < 12 < 18 < 20, placing 1.3 and 1.4 **before** 1.12; old fonts declaring 1.18-era versions exist forever | this file (verified 2026-07-23 against `core_spec.tex:3269-3274` and SMuFL's published release history) | **resolved** (ratified shape `SmuflVersion { major: u16, minor_centi: u16 }`, **literal-minor storage rejected**: the minor is stored **fraction-normalized to hundredths** — 1.12→(1,12), 1.18→(1,18), 1.20→(1,20), 1.3→(1,30), 1.4→(1,40), rule normative, release table a note. Derived `Ord` is then correct across the whole real history and collapses the 1.2/1.20 ambiguity (both → (1,20)). **Correction on filing: the leaf is NOT undefined — it exists in `epiphany-layout-ir/src/glyph.rs:29` as `SmuflVersion { major, minor }` with LITERAL minor (`{1,4}`) and derived `Ord`, so the backwards-ordering bug is LIVE there today (1.3 < 1.12), and it is a direct field of `GlyphCatalogIdentity` — conformance identity.** `epiphany-core` cannot depend on layout-ir, and `SmuflVersionRequirement` is core, so the type MUST be defined in core and layout-ir must reuse it — a **unification**, not a fresh definition, which moves `GlyphCatalogIdentity` (`{1,4}`→`minor_centi 40`). Tranche 3a defines `core::SmuflVersion` for the tuning use and leaves layout-ir's alone (a bounded, core-invisible homonym since core can't import layout-ir's); tranche 3b performs the unification and the deliberate `GlyphCatalogIdentity` move. **Correction (2026-07-23, tranche 3b-ii): "with golden/vector regen" above was verified false before that tranche's dispatch.** No golden, baseline, or vector is pinned to the catalog identity — every assertion on `ResolvedLayoutIR::canonical_bytes()` is relative (stability, determinism, and a `metrics_hash[0] ^= 1` sensitivity check that never touches `smufl_version`), and the committed SVG/PNG goldens do not embed it. The move changes the catalog's emitted bytes (`encode_catalog`'s minor field: `04 00` → `28 00`) in *value*, with nothing to regenerate. The hundredths scale is blocking for 3b, free to adjust in 3a) |
|
||||
| P13-S13 | **The score tuning context has no canonical persistence path: it can be saved, but never authored, replicated, or merged.** Schema major 3 (Push 4b tranche 3b-i) put `smufl` and `overrides` on the wire, but the only *persisted* carrier that embeds a `ScoreTuningContext` is the **acceleration snapshot**, which Chapter 8 makes explicitly non-canonical and regenerable and which the bundle may discard and rebuild at will. No **canonical** carrier embeds it at all. The two carriers that *are* canonical do not carry it: no operation authors it (`epiphany-ops` has no tuning-context payload anywhere — the vocabulary's only `tuning` references are the per-pitch `TuningReference::Inherit`), and `MaterializedState` carries effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, and pending, but no tuning context. So a user who selects a tuning system or sets a per-voice override has authored something the format cannot durably represent: it survives a snapshot round-trip, and is lost the moment the snapshot is regenerated from the operation log. This also means `req:tuning:tuning-resolution-order`'s scopes 2–4 (`overrides`) are, in practice, unreachable from any document a replica could exchange | this file (found 2026-07-24 while scoping text-projection parity; verified by searching the whole `epiphany-ops` operation vocabulary and `MaterializedState`'s field list) | **open** (ratification needed on *where* it belongs: an authoring operation kind — a schema-minor kind append, per the `DeleteRepeatStructure`/`TransposeInterval` precedent — versus a field on the canonical base, which is a data-model major. The operation route is the cheaper mechanism and the one every other authored score property already uses; the base route would make it reduced state rather than authored state, which is likely wrong since a user *chooses* a tuning. Note the wire layouts are already frozen and correct either way — this is about which carrier embeds them, not how they encode. **One cost the operation route must price in:** blocks are stamped *minimally*, at the lowest major whose layouts decode them, and `bundle.rs`'s `max_supported_major` currently caps `OperationEnvelopeBlock` at **2** on the explicit ground that no operation payload embeds the tuning context, so no op block is ever born at v3. An authoring operation carrying a `TuningOverride` would make its blocks stamp v3, so the kind append — a schema *minor* in itself — drags a *role accept-set raise* along with it. That is a small, well-precedented change, but it is not free, and it is the one asymmetry that makes the two routes closer in cost than they first appear. Not a regression: nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue