Phase 2: spelling + decomposition pre-passes (Agent H) with F merge gate
Land the two real pre-passes as canonical *derived annotations* (pure functions of the materialized Score + profile, recomputed on materialization, never serialized into canonical Score bytes), exposed via `derive_annotations`: - Spelling: a Temperley-style line-of-fifths centre-of-gravity preference rule (key-free, deterministic), preserving authored CMN letters and only inferring spelling for chromatic/integer input. `resolve_spelling` layers authored overrides above the inferred default (the RespellPitch precedence rule). `spell` now takes `&Pitch` and delegates to `simplest_spelling`. - Decomposition: metric greedy-aligned splitting on a 1/4096 integer grid (barline + dyadic-boundary ties), with exact sounding->notated tuplet conversion before gridding. Components reconstruct the event duration (invariant 15). - A per-event-kind eligibility `TaxonomyReport` so "ineligible" is always explicit and counted, never silently absent. Test infrastructure (Agent F): a 29-fixture representative corpus + taxonomy harness (corpus.rs), the H spelling/decomposition merge gate (prepass_harness.rs), a discrete `tests/prepass.rs` CI target, conformance stage [7b], a dedicated CI job, and the Pass-12 batch tracker. Review hardening folded in (nine findings): - Guard `decompose_metric` against a zero-length measure (was a divide-by-zero panic; now reported ungriddable). - Resolve spelling-override priority via `Reverse` instead of negation (was an i32::MIN overflow). - Verify spelling *register* (octave), not just pitch class, in the gate. - Close the decomposition under-emission gap: the unusual-outcome taxonomy buckets are an exact per-fixture whitelist (classify_corpus step 5b). - Generalize `accidental_ids` to a glyph stack so authored extreme alterations (triple-sharp+) reconstruct exactly instead of being clamped. - Per-fixture spread checks in the non-vacuity tripwire and broad-bucket coverage, so no single rich fixture can carry a signal (partial-stub resistance); added a `mixed_rhythm` fixture for margin. - Pin the integer-grid note-value math to the canonical rational helpers via an exhaustive test; cross-reference comments. - Replace the O(n^2) tuplet innermost-resolution scan with an id index. fmt + clippy -D warnings clean; 199 tests pass; conformance suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011giSRaHCFCGm1Z2SWv6JHt
This commit is contained in:
parent
83bc202ff6
commit
732660988d
|
|
@ -72,6 +72,28 @@ jobs:
|
||||||
- name: Run conformance suite
|
- name: Run conformance suite
|
||||||
run: cargo run --release -p epiphany-testkit --example conformance_suite 1
|
run: cargo run --release -p epiphany-testkit --example conformance_suite 1
|
||||||
|
|
||||||
|
# Track A, Agent H's merge gate (Phase 2). A discrete job so a spelling /
|
||||||
|
# decomposition pre-pass regression is attributable to H, not buried in the
|
||||||
|
# workspace test run. Asserts H's PHASE2_QUICKSTART acceptance criterion: the
|
||||||
|
# representative-corpus eligibility taxonomy (F3) and the pre-pass harness (F4 —
|
||||||
|
# determinism, spelling correctness, decomposition reconstruction, RespellPitch
|
||||||
|
# precedence, non-vacuity). The same gate also runs inside the conformance
|
||||||
|
# suite's stage [7b]; this job isolates it for fast, attributable feedback.
|
||||||
|
prepass-harness:
|
||||||
|
name: Agent H pre-pass gate (testkit)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: H spelling + decomposition merge gate
|
||||||
|
run: cargo test -p epiphany-testkit --test prepass
|
||||||
|
|
||||||
# A longer soak, scheduled nightly, exploring a wider seed space at scale.
|
# A longer soak, scheduled nightly, exploring a wider seed space at scale.
|
||||||
soak:
|
soak:
|
||||||
name: conformance soak (nightly)
|
name: conformance soak (nightly)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
# These are tracked once they exist (kept for reproducible builds):
|
# These are tracked once they exist (kept for reproducible builds):
|
||||||
# Cargo.lock for the workspace
|
# Cargo.lock for the workspace
|
||||||
|
|
||||||
|
# Handoff/working artifacts, not source: each crate's own DECISIONS.md and
|
||||||
|
# README.md are the source of truth; these root-level files are a local
|
||||||
|
# convenience concatenation and a planning briefing, not committed.
|
||||||
|
/ALL_DECISIONS_AND_READMES.md
|
||||||
|
/HANDOFF.md
|
||||||
|
|
||||||
# LaTeX build intermediates (the spec PDF itself is tracked; these are not).
|
# LaTeX build intermediates (the spec PDF itself is tracked; these are not).
|
||||||
spec/*.aux
|
spec/*.aux
|
||||||
spec/*.fdb_latexmk
|
spec/*.fdb_latexmk
|
||||||
|
|
|
||||||
|
|
@ -213,3 +213,106 @@ which is both faithful and removes the need for a runtime pass:
|
||||||
is never representable, and codec decode re-validates through the same
|
is never representable, and codec decode re-validates through the same
|
||||||
constructor. (Pass 11 item 3.5 moved this from a runtime invariant-16
|
constructor. (Pass 11 item 3.5 moved this from a runtime invariant-16
|
||||||
sub-check to a construction-time MUST.)
|
sub-check to a construction-time MUST.)
|
||||||
|
|
||||||
|
## Phase 2 — Agent H (spelling + decomposition pre-passes)
|
||||||
|
|
||||||
|
The two sanctioned v0 stubs (`pitch::spell` returning a trivial middle-C; no
|
||||||
|
decomposition algorithm) are now real, in `src/prepass.rs`. `spell` now takes
|
||||||
|
the full `&Pitch` (was `AcousticPitch` by value): spelling needs the scale
|
||||||
|
position, which `AcousticPitch` does not carry, so the old signature could not
|
||||||
|
have done real work — a breaking but necessary change (the only callers were
|
||||||
|
in-crate). The pre-passes are
|
||||||
|
**canonical derived annotations** (PHASE2_QUICKSTART §H): pure functions of
|
||||||
|
`(materialized Score, profile, SpellingAlgorithmId, DecompositionAlgorithmId)`,
|
||||||
|
recomputed on materialization, never stored. They do **not** enter the canonical
|
||||||
|
`Score` bytes — there is deliberately no codec for `DerivedAnnotations` — so the
|
||||||
|
Chapter-6 reducer and criteria 4/5 are untouched (the conformance suite stays
|
||||||
|
green). `derive_annotations(&Score, &PrePassProfile)` is the entry point a
|
||||||
|
materializer (F's integration harness) calls after reduction completes.
|
||||||
|
|
||||||
|
### Phase-2 decisions made (the five the dispatch asked H to make once)
|
||||||
|
|
||||||
|
1. **Spelling algorithm — Temperley line-of-fifths, registered as
|
||||||
|
`SpellingAlgorithmId::default_id()` (`"default"`).** A per-voice
|
||||||
|
centre-of-gravity preference rule over the line of fifths (each note picks the
|
||||||
|
tonal-pitch-class spelling closest to a running window of recent spellings,
|
||||||
|
broken by accidental simplicity, then melodic direction, then a total order on
|
||||||
|
the lof value). It is deterministic, key-free (infers tonal context from the
|
||||||
|
melody itself), and spells diatonic music in sharp/flat keys correctly
|
||||||
|
(verified against C/D-major and B♭-major scales and sharp/flat contexts in the
|
||||||
|
`prepass::tests` suite). Authored CMN scale positions are **preserved, not
|
||||||
|
re-spelled** (an authored C♯ stays C♯); the algorithm only *decides* spelling
|
||||||
|
for integer/chromatic (12-EDO) input, which is where spelling is genuinely
|
||||||
|
undetermined. Chosen over Longuet-Higgins line-of-fifths because it is the
|
||||||
|
best-documented and has the cleanest deterministic constraint formulation
|
||||||
|
(PHASE2_QUICKSTART recommendation). **Awaits G ratification in Pass 12** (Pass
|
||||||
|
11 is closed), per the dispatch's "or Pass 12 if the call slips."
|
||||||
|
|
||||||
|
2. **Decomposition algorithm — metric greedy-aligned splitting, registered as
|
||||||
|
`DecompositionAlgorithmId::default_id()` (`"default"`).** All grid logic is
|
||||||
|
integer arithmetic over a `1/4096`-of-a-whole-note grid, so every note value
|
||||||
|
to a (single-)dotted sixty-fourth is exact and the derivation is deterministic.
|
||||||
|
A duration is split at barlines (with ties), then within a measure each span
|
||||||
|
is emitted as the single notated value it equals **unless** it would cross a
|
||||||
|
dyadic boundary at least as strong as the one it starts on (the
|
||||||
|
beat-clarity/syncopation rule), in which case it splits at the strongest
|
||||||
|
interior boundary and ties across. Tuplet members convert sounding→notated in
|
||||||
|
the exact rational domain *before* gridding (a triplet eighth's sounding `1/12`
|
||||||
|
is non-dyadic; its notated `1/8` is), then decompose and carry tuplet
|
||||||
|
membership. Components' sounding durations sum to the event's (invariant 15).
|
||||||
|
|
||||||
|
The remaining three Phase-2 decisions (solver architecture, renderer SVG
|
||||||
|
dialect, catalog/companion versioning) belong to Agents I/K/J, not H.
|
||||||
|
|
||||||
|
### Eligibility taxonomy
|
||||||
|
|
||||||
|
`derive_annotations` classifies and **counts** every event and embedded pitch
|
||||||
|
into explicit buckets (`TaxonomyReport`), so "ineligible" is never silently
|
||||||
|
absent (PHASE2_QUICKSTART §H): pitched events → spelling per pitch + decomposition
|
||||||
|
(if metric, determinate musical duration); rests/unpitched → decomposition, no
|
||||||
|
pitch spelling; trajectory pitches are spelled but the event is not decomposed;
|
||||||
|
graphic/indeterminate/cue → neither; non-`cmn-12`-determinable pitch spaces →
|
||||||
|
`spelling_unavailable`; proportional/aleatoric regions →
|
||||||
|
`decomposition_deferred_nonmetric`.
|
||||||
|
|
||||||
|
### Precedence rule (H formalizes the rule, not the model)
|
||||||
|
|
||||||
|
`resolve_spelling` layers authored overrides above the inferred default: an
|
||||||
|
engraved-layer, pitch-scoped, `Explicit` `SpellingAttachment` whose
|
||||||
|
`SpellingSource` kind outranks `Inferred` in the score's `SpellingPrecedence`
|
||||||
|
wins; otherwise the algorithm's spelling stands. This is the precedence a
|
||||||
|
`RespellPitch` override rides on. **Coordination with K:** the v0 `RespellPitchOp`
|
||||||
|
carries only a `ContentHash` *fingerprint* of the new spelling (P11-C1), so an
|
||||||
|
override's spelling *value* is not reconstructable from a v0 op alone; H's rule
|
||||||
|
operates on the authored `SpellingAttachment`s present on the materialized
|
||||||
|
`Score` (set by imports/analysis today, by K's real value-typed payloads in
|
||||||
|
Phase 2). The rule itself is value-independent and final.
|
||||||
|
|
||||||
|
### Pass 12 candidates (batched for F's Pass 12 tracker; ≥3, so the batch opens)
|
||||||
|
|
||||||
|
- **P12-H1 — Ratify `SpellingAlgorithmId::Default` = Temperley line-of-fifths
|
||||||
|
v1.** The algorithm choice is H's call to propose and G's to ratify; Pass 11
|
||||||
|
closed before H landed, so this is the first Pass 12 item. Until ratified the id
|
||||||
|
`"default"` is this crate's proposal (it is *not* a byte-layout, so nothing
|
||||||
|
golden-locks on it).
|
||||||
|
- **P12-H2 — `KeySignatureChange` / `ClefChange` are anchor-only placeholders**
|
||||||
|
(Chapter 7 detail deferred). Context-aware spelling therefore infers tonal
|
||||||
|
context from the melody (line-of-fifths centre of gravity) rather than a
|
||||||
|
*declared* key. A real key-signature/clef content model would let spelling and
|
||||||
|
decomposition honour declared keys/clefs and place natural signs to cancel a
|
||||||
|
key; flagged as a graph-model gap, not improvised here.
|
||||||
|
- **P12-H3 — Chromatic-run convention** (ascending = sharps, descending = flats)
|
||||||
|
is only a *tiebreak* in the centre-of-gravity rule, so an isolated chromatic run
|
||||||
|
with no tonal context may pick the enharmonic the convention would not. A
|
||||||
|
voice-leading refinement is a Pass-12 candidate (the dispatch sanctions deferring
|
||||||
|
hard chromatic cases).
|
||||||
|
- **P12-H4 — Decomposition simplifications:** single governing meter per region
|
||||||
|
(multi-meter / mid-region meter changes deferred); region origin assumed to be a
|
||||||
|
barline (anacrusis/pickup deferred); compound-meter (6/8…) beat-group grouping
|
||||||
|
beyond the dyadic default; tuplet nesting and cross-beat tuplet members; double
|
||||||
|
(and higher) augmentation dots — `MAX_DOTS = 1` for v1, so a double-dotted value
|
||||||
|
is written as tied single/dotted values (correct, if not the most compact).
|
||||||
|
- **P12-H5 — Automatic spelling under aleatoric regions** (the spec's open
|
||||||
|
question). H spells pitches region-independently (pitch identity does not depend
|
||||||
|
on the time model) but performs no region-specific aleatoric spelling; defer if
|
||||||
|
the algorithm does not generalise cleanly.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ of the core specification (`spec/core_spec.pdf`). This is Agent B's crate per
|
||||||
| Identifiers | `ReplicaId` (+ `SYSTEM_DERIVED`), `OperationId`, the typed 128-bit family (`EventId`, `PitchId`, `VoiceId`, …), `TypedObjectId`, `IdentityContext`, `derive_system_id` | Ch. 5 §"Identifiers"; Ch. 6 §"Operation Identity" |
|
| Identifiers | `ReplicaId` (+ `SYSTEM_DERIVED`), `OperationId`, the typed 128-bit family (`EventId`, `PitchId`, `VoiceId`, …), `TypedObjectId`, `IdentityContext`, `derive_system_id` | Ch. 5 §"Identifiers"; Ch. 6 §"Operation Identity" |
|
||||||
| Time | `RationalTime` (inline-or-promoted), `MusicalPosition`/`MusicalDuration` (typed algebra), `WallClockTime`/`WallClockDuration`, `TimeAnchor`/`AnchorOffset`, `EventPosition`/`EventDuration`/`ConcreteDuration`, `TimeSignature`/`BeatGroup`, `NotatedComponent`/`NoteValue` | Ch. 3 |
|
| Time | `RationalTime` (inline-or-promoted), `MusicalPosition`/`MusicalDuration` (typed algebra), `WallClockTime`/`WallClockDuration`, `TimeAnchor`/`AnchorOffset`, `EventPosition`/`EventDuration`/`ConcreteDuration`, `TimeSignature`/`BeatGroup`, `NotatedComponent`/`NoteValue` | Ch. 3 |
|
||||||
| Tempo | `TempoMap`, `Tempo`, `TempoSegment`, `TempoShape` with closed-form `musical_to_wallclock`/`wallclock_to_musical` over constant/linear/exponential segments (curve deferred → `TempoError`) | Ch. 3 §"Tempo Map" |
|
| Tempo | `TempoMap`, `Tempo`, `TempoSegment`, `TempoShape` with closed-form `musical_to_wallclock`/`wallclock_to_musical` over constant/linear/exponential segments (curve deferred → `TempoError`) | Ch. 3 §"Tempo Map" |
|
||||||
| Pitch | `Pitch`, `ScalePosition`, `IdentifiedPitch`, `PitchSpelling`, the spelling-attachment subsystem, `ReferencePitch`, `spell` stub, all three equivalences (`scale_position_equivalent`, `enharmonic_equivalent`, `sounding_equivalent`) | Ch. 2; Ch. 4 registry ids |
|
| Pitch | `Pitch`, `ScalePosition`, `IdentifiedPitch`, `PitchSpelling`, the spelling-attachment subsystem, `ReferencePitch`, `spell` (single-pitch simplest spelling), all three equivalences (`scale_position_equivalent`, `enharmonic_equivalent`, `sounding_equivalent`) | Ch. 2; Ch. 4 registry ids |
|
||||||
|
| Pre-passes | `derive_annotations`: the real spelling pre-pass (Temperley line-of-fifths) + notational-decomposition pre-pass (metric greedy-aligned splitting) as canonical *derived annotations* (not stored), the eligibility `TaxonomyReport`, and `resolve_spelling` (authored-override precedence) | Ch. 2 §"Spelling Pre-Pass"; Ch. 3 §"Notational Decomposition" |
|
||||||
| Events | the `Event` taxonomy (7 variants) and the `slotmap`-backed `EventArena` | Ch. 5 §"The Event Arena" |
|
| Events | the `Event` taxonomy (7 variants) and the `slotmap`-backed `EventArena` | Ch. 5 §"The Event Arena" |
|
||||||
| Graph | `Canvas`, `Region`, `Staff` vs `StaffInstance`, `Voice`/`VoiceOrigin`, `Measure`, `BarlineAlignmentGroup`, aleatoric `EventOrderingDAG` (acyclic by construction), the full cross-cutting registry, the full top-level `Score` | Ch. 5 |
|
| Graph | `Canvas`, `Region`, `Staff` vs `StaffInstance`, `Voice`/`VoiceOrigin`, `Measure`, `BarlineAlignmentGroup`, aleatoric `EventOrderingDAG` (acyclic by construction), the full cross-cutting registry, the full top-level `Score` | Ch. 5 |
|
||||||
| Indexes | `ScoreIndexes`: the four mandatory indexes (event-time, cross-cutting-reference, measure, spelling-attachment) | Ch. 5 §"Indexes" |
|
| Indexes | `ScoreIndexes`: the four mandatory indexes (event-time, cross-cutting-reference, measure, spelling-attachment) | Ch. 5 §"Indexes" |
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,17 @@
|
||||||
//! and the event temporal-coordinate unions [`EventPosition`],
|
//! and the event temporal-coordinate unions [`EventPosition`],
|
||||||
//! [`EventDuration`], [`ConcreteDuration`] (Chapter 3).
|
//! [`EventDuration`], [`ConcreteDuration`] (Chapter 3).
|
||||||
//! * `pitch` — [`Pitch`], [`ScalePosition`], [`IdentifiedPitch`],
|
//! * `pitch` — [`Pitch`], [`ScalePosition`], [`IdentifiedPitch`],
|
||||||
//! [`PitchSpelling`], the spelling-attachment subsystem, and the spelling
|
//! [`PitchSpelling`], and the spelling-attachment subsystem (Chapter 2;
|
||||||
//! pre-pass stub (Chapter 2; Chapter 4 for the tuning/pitch-space registry
|
//! Chapter 4 for the tuning/pitch-space registry identifiers it references).
|
||||||
//! identifiers it references).
|
|
||||||
//! * `event` — the [`Event`] taxonomy and the [`EventArena`] (Chapter 5
|
//! * `event` — the [`Event`] taxonomy and the [`EventArena`] (Chapter 5
|
||||||
//! §"The Event Arena").
|
//! §"The Event Arena").
|
||||||
//! * `graph` — [`Canvas`], [`Region`], [`Staff`]/[`StaffInstance`] (distinct
|
//! * `graph` — [`Canvas`], [`Region`], [`Staff`]/[`StaffInstance`] (distinct
|
||||||
//! types), [`Voice`], [`Measure`], [`BarlineAlignmentGroup`], and the
|
//! types), [`Voice`], [`Measure`], [`BarlineAlignmentGroup`], and the
|
||||||
//! reference-bearing cross-cutting structures (Chapter 5).
|
//! reference-bearing cross-cutting structures (Chapter 5).
|
||||||
|
//! * `prepass` — the spelling and notational-decomposition pre-passes
|
||||||
|
//! ([`derive_annotations`]): canonical *derived annotations* recomputed on
|
||||||
|
//! materialization, not stored graph state (Chapter 2 §"The Spelling
|
||||||
|
//! Pre-Pass"; Chapter 3 §"Notational Decomposition").
|
||||||
//! * `invariants` — the Chapter 5 graph-invariant checker, with one check
|
//! * `invariants` — the Chapter 5 graph-invariant checker, with one check
|
||||||
//! per enumerated invariant and a typed witness for each violation.
|
//! per enumerated invariant and a typed witness for each violation.
|
||||||
//!
|
//!
|
||||||
|
|
@ -53,6 +56,7 @@ mod tempo;
|
||||||
mod time;
|
mod time;
|
||||||
|
|
||||||
pub mod generators;
|
pub mod generators;
|
||||||
|
pub mod prepass;
|
||||||
|
|
||||||
pub use ids::{
|
pub use ids::{
|
||||||
derive_system_id, AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId,
|
derive_system_id, AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId,
|
||||||
|
|
@ -71,14 +75,19 @@ pub use time::{
|
||||||
|
|
||||||
pub use pitch::{
|
pub use pitch::{
|
||||||
derive_system_pitch_id, spell, AccidentalId, AccidentalRegistryId, AcousticPitch,
|
derive_system_pitch_id, spell, AccidentalId, AccidentalRegistryId, AcousticPitch,
|
||||||
AcousticRealization, CmnNominal, ForeignFormatId, IdentifiedPitch, NominalRegistryId, Pitch,
|
AcousticRealization, CmnNominal, DecompositionAlgorithmId, ForeignFormatId, IdentifiedPitch,
|
||||||
PitchSpaceId, PitchSpacePosition, PitchSpelling, PositionRegistryId, ReferencePitch,
|
NominalRegistryId, Pitch, PitchSpaceId, PitchSpacePosition, PitchSpelling, PositionRegistryId,
|
||||||
ScalePosition, SpellingAlgorithmId, SpellingAttachment, SpellingContext, SpellingDirective,
|
ReferencePitch, ScalePosition, SpellingAlgorithmId, SpellingAttachment, SpellingContext,
|
||||||
SpellingNominal, SpellingPrecedence, SpellingRenderHints, SpellingRule, SpellingRuleSetId,
|
SpellingDirective, SpellingNominal, SpellingPrecedence, SpellingRenderHints, SpellingRule,
|
||||||
SpellingScope, SpellingSource, SpellingSourceKind, StaffGroupKindRegistryId,
|
SpellingRuleSetId, SpellingScope, SpellingSource, SpellingSourceKind, StaffGroupKindRegistryId,
|
||||||
TieClassRegistryId, TuningReference, TuningSystemId, VoiceSelector,
|
TieClassRegistryId, TuningReference, TuningSystemId, VoiceSelector,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use prepass::{
|
||||||
|
derive_annotations, resolve_spelling, simplest_spelling, DerivedAnnotations, PrePassProfile,
|
||||||
|
ResolvedSpelling, SpellingProvenance, TaxonomyReport,
|
||||||
|
};
|
||||||
|
|
||||||
pub use event::{
|
pub use event::{
|
||||||
ArenaError, ArticulationMark, CueEvent, CueRendering, DynamicMark, Event, EventArena, EventKey,
|
ArenaError, ArticulationMark, CueEvent, CueRendering, DynamicMark, Event, EventArena, EventKey,
|
||||||
GraceKind, GraphicEvent, IndeterminacyHints, IndeterminacyKind, IndeterminateEvent,
|
GraceKind, GraphicEvent, IndeterminacyHints, IndeterminacyKind, IndeterminateEvent,
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,16 @@ catalog_id!(
|
||||||
/// Pre-Pass"). The v0 stub registers [`SpellingAlgorithmId::default_id`].
|
/// Pre-Pass"). The v0 stub registers [`SpellingAlgorithmId::default_id`].
|
||||||
SpellingAlgorithmId
|
SpellingAlgorithmId
|
||||||
);
|
);
|
||||||
|
catalog_id!(
|
||||||
|
/// Identifies a notational-decomposition algorithm family (Chapter 3
|
||||||
|
/// §"Sounding Duration and Notational Decomposition"). Versioned the same
|
||||||
|
/// way as [`SpellingAlgorithmId`]: the id is part of the derivation key for
|
||||||
|
/// the decomposition pre-pass, so a profile-declared change deterministically
|
||||||
|
/// invalidates derived decompositions. The Phase-2 default
|
||||||
|
/// ([`DecompositionAlgorithmId::default_id`]) resolves to the metric
|
||||||
|
/// greedy-aligned splitter in [`crate::prepass`].
|
||||||
|
DecompositionAlgorithmId
|
||||||
|
);
|
||||||
catalog_id!(
|
catalog_id!(
|
||||||
/// Identifies a foreign interchange format (e.g. MusicXML), used as a
|
/// Identifies a foreign interchange format (e.g. MusicXML), used as a
|
||||||
/// spelling/decomposition provenance tag.
|
/// spelling/decomposition provenance tag.
|
||||||
|
|
@ -119,15 +129,27 @@ catalog_id!(
|
||||||
);
|
);
|
||||||
|
|
||||||
impl SpellingAlgorithmId {
|
impl SpellingAlgorithmId {
|
||||||
/// The sanctioned v0 default spelling algorithm. The real pre-pass is an
|
/// The Phase-2 default spelling algorithm, registered under the id
|
||||||
/// open question (Chapter 2 §"The Spelling Pre-Pass"; Appendix D §"Open
|
/// `"default"`. The id resolves to the deterministic Temperley-style
|
||||||
/// Algorithm Hooks"), so [`spell`] returns a trivial advisory default
|
/// line-of-fifths pre-pass implemented in [`crate::prepass`] (Chapter 2
|
||||||
/// registered under this id.
|
/// §"The Spelling Pre-Pass"). The literal id is part of the derivation key:
|
||||||
|
/// changing the registered algorithm changes the id, so derived spellings
|
||||||
|
/// computed under a different version never silently alias.
|
||||||
pub fn default_id() -> Self {
|
pub fn default_id() -> Self {
|
||||||
SpellingAlgorithmId::new("default")
|
SpellingAlgorithmId::new("default")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DecompositionAlgorithmId {
|
||||||
|
/// The Phase-2 default decomposition algorithm, registered under the id
|
||||||
|
/// `"default"`. The id resolves to the deterministic metric greedy-aligned
|
||||||
|
/// splitter implemented in [`crate::prepass`] (Chapter 3 §"Sounding Duration
|
||||||
|
/// and Notational Decomposition").
|
||||||
|
pub fn default_id() -> Self {
|
||||||
|
DecompositionAlgorithmId::new("default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The seven CMN diatonic nominals. The discriminants are **normative**: they
|
/// The seven CMN diatonic nominals. The discriminants are **normative**: they
|
||||||
/// define the diatonic step ordering used by transposition (Chapter 2
|
/// define the diatonic step ordering used by transposition (Chapter 2
|
||||||
/// §"The CmnNominal Type").
|
/// §"The CmnNominal Type").
|
||||||
|
|
@ -742,17 +764,18 @@ pub struct SpellingContext {
|
||||||
pub space: Option<PitchSpaceId>,
|
pub space: Option<PitchSpaceId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The spelling pre-pass stub (QUICKSTART "Don't do these": *"Don't implement
|
/// The context-free spelling of a single pitch: its authored CMN letter if it
|
||||||
/// the spelling pre-pass. Stub: returning a trivial default. Register as
|
/// has one, else the simplest (fewest-accidental) enharmonic spelling of its
|
||||||
/// `SpellingAlgorithmId::Default`."*).
|
/// 12-TET pitch class (Chapter 2 §"The Spelling Pre-Pass").
|
||||||
///
|
///
|
||||||
/// Returns a trivial, advisory CMN default (middle-octave C, no accidental).
|
/// This is the *isolated* entry point. Real, context-aware spelling — the
|
||||||
/// Per Appendix D §"Open Algorithm Hooks", an unspecified algorithm affecting
|
/// Temperley line-of-fifths pre-pass that resolves a pitch by its melodic
|
||||||
/// canonical state must be non-canonical/advisory: this output is advisory and
|
/// neighbours — is a function of the whole score and lives in
|
||||||
/// must not be treated as a canonical spelling until the real pre-pass is
|
/// [`crate::prepass::derive_annotations`]; the `_ctx` argument is retained for
|
||||||
/// specified or profile-declared.
|
/// source compatibility. A pitch whose space declares spelling unavailable
|
||||||
pub fn spell(_p: AcousticPitch, _ctx: &SpellingContext) -> PitchSpelling {
|
/// (no determinable 12-TET class) falls back to a middle-C advisory default.
|
||||||
PitchSpelling::cmn(CmnNominal::C, 4)
|
pub fn spell(p: &Pitch, _ctx: &SpellingContext) -> PitchSpelling {
|
||||||
|
crate::prepass::simplest_spelling(p).unwrap_or_else(|| PitchSpelling::cmn(CmnNominal::C, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -977,14 +1000,54 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spell_stub_returns_trivial_default() {
|
fn spell_preserves_authored_cmn_letter() {
|
||||||
let ap = AcousticPitch {
|
// An authored C-sharp keeps its letter (spelling follows the scale
|
||||||
tuning: TuningReference::Inherit,
|
// position, not the trivial old C-default stub).
|
||||||
realization: AcousticRealization::Implicit,
|
let p = Pitch {
|
||||||
|
scale_position: ScalePosition {
|
||||||
|
space: PitchSpaceId::new("cmn-12"),
|
||||||
|
position: PitchSpacePosition::Cmn {
|
||||||
|
nominal: CmnNominal::C,
|
||||||
|
alteration: 1,
|
||||||
|
octave: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
acoustic: AcousticPitch {
|
||||||
|
tuning: TuningReference::Inherit,
|
||||||
|
realization: AcousticRealization::Implicit,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let s = spell(ap, &SpellingContext::default());
|
let s = spell(&p, &SpellingContext::default());
|
||||||
assert_eq!(s.nominal, SpellingNominal::Cmn(CmnNominal::C));
|
assert_eq!(s.nominal, SpellingNominal::Cmn(CmnNominal::C));
|
||||||
assert!(s.accidentals.is_empty());
|
assert_eq!(s.accidentals, vec![AccidentalId::new("sharp")]);
|
||||||
|
assert_eq!(s.octave, 5);
|
||||||
assert_eq!(SpellingAlgorithmId::default_id().as_str(), "default");
|
assert_eq!(SpellingAlgorithmId::default_id().as_str(), "default");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spell_chromatic_integer_pitch_is_nontrivial() {
|
||||||
|
// A 12-EDO integer position (chromatic input) gets a real spelling of
|
||||||
|
// its pitch class, not the old constant middle-C.
|
||||||
|
let p = Pitch {
|
||||||
|
scale_position: ScalePosition {
|
||||||
|
space: PitchSpaceId::new("cmn-12"),
|
||||||
|
position: PitchSpacePosition::Integer {
|
||||||
|
space_size: 12,
|
||||||
|
index: 54, // pitch class 6 (F#/Gb)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
acoustic: AcousticPitch {
|
||||||
|
tuning: TuningReference::Inherit,
|
||||||
|
realization: AcousticRealization::Implicit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let s = spell(&p, &SpellingContext::default());
|
||||||
|
// Simplest single-accidental spelling of pitch class 6 (either F# or Gb);
|
||||||
|
// it is a real, non-default spelling.
|
||||||
|
assert!(matches!(
|
||||||
|
s.nominal,
|
||||||
|
SpellingNominal::Cmn(CmnNominal::F) | SpellingNominal::Cmn(CmnNominal::G)
|
||||||
|
));
|
||||||
|
assert_eq!(s.accidentals.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,123 @@
|
||||||
|
# epiphany-testkit — Decisions
|
||||||
|
|
||||||
|
Agent F's crate. The v0 decisions live in `spec/QUICKSTART.md` and the per-crate
|
||||||
|
DECISIONS files; this file records the **Phase 2** calls F makes as its mandate
|
||||||
|
broadens (`spec/PHASE2_QUICKSTART.md`, `spec/PHASE2_F_WEEK0_WORKLIST.md`).
|
||||||
|
|
||||||
|
## F0 — Where the new harnesses, benches, and the integration runner live
|
||||||
|
|
||||||
|
*Worklist item F0: "a 1-day call that everything else lands inside of." Decide
|
||||||
|
before writing F1/F3/F4/F5.*
|
||||||
|
|
||||||
|
**Decision: keep the examples + library-module pattern; do not adopt `xtask`;
|
||||||
|
put `benches/` in this crate; per-agent harnesses are library modules asserted
|
||||||
|
by `tests/` integration tests; the end-to-end integration harness is a `tests/`
|
||||||
|
integration test over a library `integration` module.**
|
||||||
|
|
||||||
|
Concretely:
|
||||||
|
|
||||||
|
- **No `xtask`, no `tests/reference-suite/`.** The QUICKSTART topology named
|
||||||
|
both (HANDOFF §7-F), but their function is already met by per-crate
|
||||||
|
`examples/*` fuzzers and `epiphany-testkit/examples/conformance_suite.rs`,
|
||||||
|
which is the orchestration layer and the soak entry point. Adding `xtask`
|
||||||
|
would introduce a second, parallel orchestration surface for no present gain;
|
||||||
|
the workspace has one cross-cutting crate (this one) and `cargo` already
|
||||||
|
drives every gate. Revisit only if cross-crate orchestration outgrows a single
|
||||||
|
`examples/` runner (e.g. multi-binary pipelines that need a build graph).
|
||||||
|
|
||||||
|
- **Per-agent harnesses are library modules in `src/`**, alongside the v0
|
||||||
|
harnesses (`roundtrip`, `convergence`, `equivocation`, `bundle_harness`,
|
||||||
|
`negative`, `layout_stub`). Each new agent gets one module exposing
|
||||||
|
`run_all()`-style entry points plus the granular asserts:
|
||||||
|
- **H** → [`crate::prepass_harness`] (spelling/decomposition determinism,
|
||||||
|
eligibility-taxonomy coverage, `RespellPitch` precedence, non-vacuity),
|
||||||
|
driving the corpus in [`crate::corpus`]. **Live now** (H landed).
|
||||||
|
- **I** → `engrave_harness` (hard-constraint validation against *declared* IR
|
||||||
|
constraints + class-specific collision rules, provenance survival, SVG
|
||||||
|
XML-validity, golden machine-acceptance snapshot). *Skeleton when I starts.*
|
||||||
|
- **K** → `migration_harness` (deterministic + equivalence-preserving v0→v1
|
||||||
|
migration, payload-schema completeness). *Skeleton when K starts.*
|
||||||
|
- **J** → `wire_harness` (cross-impl decoder, canonicalization tests) +
|
||||||
|
the wire-format fuzzer as an `examples/` soak target. *Skeleton when J
|
||||||
|
starts.*
|
||||||
|
|
||||||
|
- **Each harness is asserted by a `tests/` integration test** (the same shape as
|
||||||
|
`tests/acceptance.rs`) so it is a discrete `cargo test` target and a discrete
|
||||||
|
CI job, and is **also** exercised at scale by `examples/conformance_suite.rs`
|
||||||
|
for the nightly soak. The H harness lands as `tests/prepass.rs` + a
|
||||||
|
`[prepass-harness]` stage in the conformance suite.
|
||||||
|
|
||||||
|
- **`benches/` lives in this crate** (not per-crate). The Chapter-10 budgets are
|
||||||
|
workspace-level, and the marquee bench (the reducer's `O(n²)`
|
||||||
|
`canonical_reduction_order` at 10K+ envelopes, worklist F1) drives
|
||||||
|
`epiphany-ops` *through* the testkit's envelope generators — exactly what this
|
||||||
|
crate already does. A bench that lived in `epiphany-ops` could not reuse the
|
||||||
|
generators without a dev-dependency cycle. Uses `criterion`; thresholds are
|
||||||
|
written in the bench, with known-pending scale points marked `xfail` per F1.
|
||||||
|
|
||||||
|
- **The end-to-end integration harness (F5) is a `tests/` integration test** over
|
||||||
|
a library `integration` module with documented stub swap-points, so real H/I/K/J
|
||||||
|
stages replace stubs in place as they land, and the byte-identity assertion is
|
||||||
|
wired from day one (trivially true on stubs, meaningful once stages are real).
|
||||||
|
|
||||||
|
**Why a library module + integration test, not a bare integration test:** the
|
||||||
|
harness logic (asserts, fingerprinting, the corpus) must be callable from both
|
||||||
|
the unit-budget `tests/` target *and* the at-scale `examples/conformance_suite`.
|
||||||
|
Bare `tests/` code is not importable across targets; library modules are. This
|
||||||
|
mirrors how v0's `convergence`/`roundtrip` modules are shared between
|
||||||
|
`tests/acceptance.rs` and the conformance example.
|
||||||
|
|
||||||
|
**Unblocks:** F1 (benches), F3 (corpus + taxonomy harness — done), F4 (per-agent
|
||||||
|
harness skeletons — H done), F5 (integration skeleton).
|
||||||
|
|
||||||
|
## F3 — The representative score corpus + eligibility-taxonomy harness (Agent H)
|
||||||
|
|
||||||
|
*Worklist item F3 — "the most underbuilt dependency; unblocks H entirely."*
|
||||||
|
|
||||||
|
The corpus lives in [`crate::corpus`] as ≥20 deterministic, `check_invariants`-clean
|
||||||
|
fixtures tagged by tier (common / edge / torture) and by the event-kind
|
||||||
|
eligibility taxonomy of `PHASE2_QUICKSTART §H`. Rather than re-deriving the
|
||||||
|
taxonomy, the harness runs Agent H's own `epiphany_core::derive_annotations` and
|
||||||
|
reads its [`epiphany_core::TaxonomyReport`], then (a) independently recounts
|
||||||
|
events by kind and **cross-checks** H's counts (so a miscount is caught, not
|
||||||
|
trusted), and (b) aggregates per-bucket counts across the corpus and asserts every
|
||||||
|
taxonomy bucket is non-empty or explicitly deferred. The corpus also re-uses the
|
||||||
|
existing positive generators (`valid_score`, `valid_score_rich`,
|
||||||
|
`ten_measure_single_staff`) as fixtures so F's taxonomy harness runs over the same
|
||||||
|
graphs Agents H and I develop and render against.
|
||||||
|
|
||||||
|
**Deferred buckets** (documented, not required non-empty by the clean corpus):
|
||||||
|
none — every bucket including `decomposition_skipped_nonmusical` (zero-duration
|
||||||
|
grace) and `decomposition_ungriddable` (off-grid / sub-sixty-fourth torture
|
||||||
|
cases) is exercised by a dedicated fixture, so the honest-classification paths are
|
||||||
|
all proven reachable. If a future invariant change makes a bucket unreachable by
|
||||||
|
clean input, move it to `corpus::DEFERRED_BUCKETS` with a written reason rather
|
||||||
|
than dropping the assertion.
|
||||||
|
|
||||||
|
## F4 (H) — The H merge gate
|
||||||
|
|
||||||
|
[`crate::prepass_harness`] is H's merge gate (worklist F4). It asserts H's stated
|
||||||
|
`PHASE2_QUICKSTART` acceptance criterion over the corpus: every *eligible*
|
||||||
|
`IdentifiedPitch` carries a **non-trivial** spelling (verified by pitch-class
|
||||||
|
correctness, which the old constant-`C4` stub fails); every *eligible* determinate
|
||||||
|
metric duration carries a `Decomposition` whose components reconstruct the
|
||||||
|
duration (invariant 15); ineligible cases are classified and counted; derivation
|
||||||
|
is deterministic across runs (asserted by structural equality **and** a canonical
|
||||||
|
textual fingerprint, since `DerivedAnnotations` deliberately has no codec);
|
||||||
|
`RespellPitch`-style authored overrides take precedence; and the derivation stays
|
||||||
|
deterministic when run on materialized scores in the criterion-5 pipeline.
|
||||||
|
|
||||||
|
**Non-vacuity guard** (the F discipline — the gate must go red if H were stubbed):
|
||||||
|
across the corpus the harness requires multiple distinct spelled nominals and at
|
||||||
|
least one accidental (a constant-`C4` stub yields one nominal, zero accidentals),
|
||||||
|
at least one multi-component (tied) decomposition and multiple distinct note
|
||||||
|
values (an empty/no-op decomposition map yields neither), and per-pitch
|
||||||
|
pitch-class correctness (the stub mis-spells the first non-`C` pitch).
|
||||||
|
|
||||||
|
## Pass 12 batch tracker
|
||||||
|
|
||||||
|
Per F's mandate, the Pass 12 batch is tracked in `spec/PASS12_BATCH.md`. It opens
|
||||||
|
once ≥3 ambiguities accumulate (same rule as v0 → Pass 11). Agent H's landing
|
||||||
|
contributed five candidates (P12-H1…P12-H5, recorded in
|
||||||
|
`crates/epiphany-core/DECISIONS.md`), which crosses the threshold, so the batch is
|
||||||
|
open. F does not resolve these; F collects them.
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
//! first violation.
|
//! first violation.
|
||||||
|
|
||||||
use epiphany_testkit::{
|
use epiphany_testkit::{
|
||||||
bundle_harness, convergence, equivocation, fixtures, generators, layout_stub, negative,
|
bundle_harness, convergence, corpus, equivocation, fixtures, generators, layout_stub, negative,
|
||||||
roundtrip, Rng,
|
prepass_harness, roundtrip, Rng,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -116,5 +116,13 @@ fn main() {
|
||||||
layout_stub::round_trip(&generators::graph::valid_score_rich(seed));
|
layout_stub::round_trip(&generators::graph::valid_score_rich(seed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7b. Track A — Agent H pre-pass merge gate (spelling + decomposition):
|
||||||
|
// the representative-corpus eligibility taxonomy (F3) and the H harness
|
||||||
|
// (F4) — determinism, spelling correctness, decomposition reconstruction,
|
||||||
|
// RespellPitch precedence, and the non-vacuity tripwire — at scale.
|
||||||
|
eprintln!("[7b ] Agent H pre-pass gate: taxonomy coverage + merge gate");
|
||||||
|
corpus::run_all();
|
||||||
|
prepass_harness::run_all(scale.max(1));
|
||||||
|
|
||||||
eprintln!("[8/8] ok: full conformance suite passed (scale {scale})");
|
eprintln!("[8/8] ok: full conformance suite passed (scale {scale})");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -96,6 +96,13 @@ pub mod fixtures;
|
||||||
pub mod generators;
|
pub mod generators;
|
||||||
pub mod roundtrip;
|
pub mod roundtrip;
|
||||||
|
|
||||||
|
// Phase 2, Agent F (for Agent H): the representative score corpus + eligibility
|
||||||
|
// taxonomy harness (`corpus`, worklist F3) and H's spelling/decomposition merge
|
||||||
|
// gate (`prepass_harness`, worklist F4). See `DECISIONS.md` F0 for why per-agent
|
||||||
|
// harnesses are library modules asserted by `tests/` integration tests.
|
||||||
|
pub mod corpus;
|
||||||
|
pub mod prepass_harness;
|
||||||
|
|
||||||
pub mod convergence;
|
pub mod convergence;
|
||||||
pub mod equivocation;
|
pub mod equivocation;
|
||||||
pub mod negative;
|
pub mod negative;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,713 @@
|
||||||
|
//! **F4(H) — Agent H's merge gate** (Agent F; `spec/PHASE2_F_WEEK0_WORKLIST.md`
|
||||||
|
//! item F4, `spec/PHASE2_QUICKSTART.md` §F "Per-agent harnesses → H").
|
||||||
|
//!
|
||||||
|
//! Drives Agent H's spelling + notational-decomposition pre-passes
|
||||||
|
//! ([`epiphany_core::derive_annotations`]) over [`crate::corpus`] and asserts H's
|
||||||
|
//! stated acceptance criterion, designed for H's specific failure modes:
|
||||||
|
//!
|
||||||
|
//! * **Determinism** — the same score derives byte-identically twice, and a
|
||||||
|
//! fixture built twice derives identically. Asserted by structural equality
|
||||||
|
//! **and** a canonical fingerprint (`DerivedAnnotations` deliberately has no
|
||||||
|
//! codec, so the fingerprint is its `Debug` form over canonically-ordered
|
||||||
|
//! `BTreeMap`s — the determinism guarantee H makes).
|
||||||
|
//! * **Eligibility** — every *eligible* embedded `IdentifiedPitch` carries a
|
||||||
|
//! spelling that **realizes the pitch's 12-TET class** (the correctness check
|
||||||
|
//! the old constant-`C4` stub fails on the first non-`C` pitch); spelling-
|
||||||
|
//! unavailable pitches are *not* spelled; every decomposition **reconstructs
|
||||||
|
//! its event's sounding duration** (Chapter 3 invariant 15), independently
|
||||||
|
//! recomputed from H's components.
|
||||||
|
//! * **Precedence** — an authored, engraved-layer `RespellPitch`-style override
|
||||||
|
//! takes precedence over the inferred spelling; a non-engraved (layer-tagged)
|
||||||
|
//! override does not.
|
||||||
|
//! * **Non-vacuity** — the F discipline: the gate goes red if H were stubbed.
|
||||||
|
//! Across the corpus, spellings must span multiple nominals and include
|
||||||
|
//! accidentals (a constant stub yields one nominal, none), and decompositions
|
||||||
|
//! must use multiple note values and include a tied multi-component split.
|
||||||
|
//! * **Materialization pipeline** — the derivation stays deterministic when run
|
||||||
|
//! on real reduced scores from the criterion-5 convergence path, so criterion
|
||||||
|
//! 5 keeps passing with non-trivial pre-pass outputs downstream.
|
||||||
|
|
||||||
|
use epiphany_core::{
|
||||||
|
derive_annotations, AccidentalId, AnalysisLayerId, CmnNominal, DerivedAnnotations, Event,
|
||||||
|
EventDuration, MusicalDuration, PitchSpelling, PrePassProfile, RationalTime, Score,
|
||||||
|
SpellingAttachment, SpellingDirective, SpellingNominal, SpellingProvenance, SpellingScope,
|
||||||
|
SpellingSource, SpellingSourceKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::corpus;
|
||||||
|
|
||||||
|
fn profile() -> PrePassProfile {
|
||||||
|
PrePassProfile::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A canonical fingerprint of derived annotations. `DerivedAnnotations` has no
|
||||||
|
/// codec by design (it is never serialized into canonical `Score` bytes), so the
|
||||||
|
/// fingerprint is its `Debug` form — deterministic because the `spellings` and
|
||||||
|
/// `decompositions` maps are `BTreeMap`s (canonical key order) and the taxonomy
|
||||||
|
/// is plain counts. Equal fingerprints ⇒ byte-identical derivations.
|
||||||
|
pub fn fingerprint(ann: &DerivedAnnotations) -> String {
|
||||||
|
format!("{ann:?}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Determinism
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 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());
|
||||||
|
assert_eq!(a, b, "derivation is not a pure function of the score");
|
||||||
|
assert_eq!(
|
||||||
|
fingerprint(&a),
|
||||||
|
fingerprint(&b),
|
||||||
|
"derivation fingerprint differs across runs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every corpus fixture, built twice from its deterministic builder, derives
|
||||||
|
/// byte-identical annotations — the "same score reduced twice ⇒ identical
|
||||||
|
/// 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());
|
||||||
|
assert_eq!(a, b, "fixture `{}` derivation not deterministic", f.name);
|
||||||
|
assert_eq!(
|
||||||
|
fingerprint(&a),
|
||||||
|
fingerprint(&b),
|
||||||
|
"fixture `{}` fingerprint differs across builds",
|
||||||
|
f.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Eligibility + correctness
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// The absolute 12-TET semitone a CMN spelling realizes
|
||||||
|
/// (`nominal + accidentals + 12*octave`), or `None` for a non-CMN nominal.
|
||||||
|
/// Independent of H's internals — this is how F checks an inferred spelling names
|
||||||
|
/// the right pitch *in the right register*. The octave is solved by H
|
||||||
|
/// (`spelling_from_lof`), so a pitch-class-only check would miss an
|
||||||
|
/// off-by-an-octave or B♯/C♭ enharmonic-wrap regression.
|
||||||
|
fn spelling_absolute_semitone(s: &PitchSpelling) -> Option<i32> {
|
||||||
|
let nominal = match s.nominal {
|
||||||
|
SpellingNominal::Cmn(n) => n,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let mut semis = nominal.chromatic() as i32;
|
||||||
|
for a in &s.accidentals {
|
||||||
|
semis += match a.as_str() {
|
||||||
|
"sharp" => 1,
|
||||||
|
"flat" => -1,
|
||||||
|
"double-sharp" => 2,
|
||||||
|
"double-flat" => -2,
|
||||||
|
"natural" => 0,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(semis + 12 * s.octave as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 12-TET pitch class a CMN spelling realizes (`nominal + accidentals`), or
|
||||||
|
/// `None` for a non-CMN nominal.
|
||||||
|
fn spelling_pitch_class(s: &PitchSpelling) -> Option<i32> {
|
||||||
|
spelling_absolute_semitone(s).map(|semi| semi.rem_euclid(12))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every *eligible* embedded pitch carries a non-trivial spelling that realizes
|
||||||
|
/// its 12-TET pitch class; every spelling-unavailable pitch is left unspelled.
|
||||||
|
pub fn assert_eligible_pitches_spelled(score: &Score, ann: &DerivedAnnotations) {
|
||||||
|
for e in score.events.iter() {
|
||||||
|
let mut pitches = Vec::new();
|
||||||
|
e.collect_identified_pitches(&mut pitches);
|
||||||
|
for ip in pitches {
|
||||||
|
match ip.pitch.twelve_tet_class() {
|
||||||
|
None => assert!(
|
||||||
|
!ann.spellings.contains_key(&ip.id),
|
||||||
|
"spelling-unavailable pitch {:?} was spelled anyway",
|
||||||
|
ip.id
|
||||||
|
),
|
||||||
|
Some(class) => {
|
||||||
|
let rs = ann.spellings.get(&ip.id).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"eligible pitch {:?} (pc {class}) carries no spelling",
|
||||||
|
ip.id
|
||||||
|
)
|
||||||
|
});
|
||||||
|
// The algorithm's own output must *name the right pitch*. (An
|
||||||
|
// authored override is the user's call; only check inferred.)
|
||||||
|
if matches!(rs.provenance, SpellingProvenance::Inferred) {
|
||||||
|
let got = spelling_pitch_class(&rs.spelling).unwrap_or_else(|| {
|
||||||
|
panic!("inferred spelling of {:?} is not a CMN spelling", ip.id)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
got, class as i32,
|
||||||
|
"inferred spelling of {:?} realizes pc {got}, but the pitch is pc {class}",
|
||||||
|
ip.id
|
||||||
|
);
|
||||||
|
// ...and in the right *register*: the absolute semitone
|
||||||
|
// (octave included) must match the pitch's. H solves the
|
||||||
|
// octave in `spelling_from_lof`; the pitch-class check above
|
||||||
|
// would pass an off-by-an-octave or B♯/C♭-wrap regression.
|
||||||
|
let want_semitone = ip.pitch.twelve_tet_semitone().unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"eligible pitch {:?} (pc {class}) has no 12-TET semitone",
|
||||||
|
ip.id
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let got_semitone =
|
||||||
|
spelling_absolute_semitone(&rs.spelling).unwrap_or_else(|| {
|
||||||
|
panic!("inferred spelling of {:?} is not a CMN spelling", ip.id)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
got_semitone, want_semitone,
|
||||||
|
"inferred spelling of {:?} realizes semitone {got_semitone}, but the pitch sounds at {want_semitone} (wrong register)",
|
||||||
|
ip.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every decomposition H emits targets a decomposable event, has a consistent
|
||||||
|
/// tie chain, and its components' **sounding** durations reconstruct the event's
|
||||||
|
/// musical duration exactly (Chapter 3 invariant 15), recomputed independently.
|
||||||
|
pub fn assert_decompositions_reconstruct(score: &Score, ann: &DerivedAnnotations) {
|
||||||
|
for (eid, d) in &ann.decompositions {
|
||||||
|
assert_eq!(
|
||||||
|
d.target, *eid,
|
||||||
|
"decomposition keyed by {eid:?} targets {:?}",
|
||||||
|
d.target
|
||||||
|
);
|
||||||
|
assert!(!d.components.is_empty(), "empty decomposition for {eid:?}");
|
||||||
|
|
||||||
|
let ev = score
|
||||||
|
.events
|
||||||
|
.get(*eid)
|
||||||
|
.unwrap_or_else(|| panic!("decomposition targets non-live event {eid:?}"));
|
||||||
|
assert!(
|
||||||
|
matches!(ev, Event::Pitched(_) | Event::Unpitched(_) | Event::Rest(_)),
|
||||||
|
"decomposition targets a non-decomposable event kind ({eid:?})"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tie chain: every component but the last is tied to the next.
|
||||||
|
let n = d.components.len();
|
||||||
|
for (i, c) in d.components.iter().enumerate() {
|
||||||
|
assert_eq!(
|
||||||
|
c.tied_to_next,
|
||||||
|
i + 1 < n,
|
||||||
|
"decomposition of {eid:?}: component {i} tie flag is wrong"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruction: sum of sounding durations == event duration.
|
||||||
|
let mut sum = RationalTime::zero();
|
||||||
|
for c in &d.components {
|
||||||
|
let ratio = c.tuplet.and_then(|tid| {
|
||||||
|
score
|
||||||
|
.cross_cutting
|
||||||
|
.tuplets
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.id == tid)
|
||||||
|
.map(|t| t.ratio)
|
||||||
|
});
|
||||||
|
sum = sum.add(c.sounding_duration(ratio).rational());
|
||||||
|
}
|
||||||
|
let dur = match ev.duration() {
|
||||||
|
EventDuration::Musical(MusicalDuration(rt)) => rt.clone(),
|
||||||
|
other => panic!("decomposed event {eid:?} has a non-musical duration {other:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
sum, dur,
|
||||||
|
"decomposition of {eid:?} reconstructs {sum:?}, but the event duration is {dur:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map and taxonomy count agree.
|
||||||
|
assert_eq!(
|
||||||
|
ann.decompositions.len(),
|
||||||
|
ann.taxonomy.decompositions_inferred,
|
||||||
|
"decomposition map size disagrees with the taxonomy count"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// RespellPitch precedence
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// An authored, engraved-layer `UserChosen` override takes precedence over the
|
||||||
|
/// inferred spelling; a non-engraved (layer-tagged) override is ignored. Then the
|
||||||
|
/// two remaining decision axes in [`epiphany_core::resolve_spelling`]: among
|
||||||
|
/// competing engraved overrides the higher **`priority`** wins regardless of
|
||||||
|
/// attachment order, and an engraved override whose **source does not outrank
|
||||||
|
/// `Inferred`** in the score's precedence (`Analytical`, by default ranked below
|
||||||
|
/// `Inferred`) is rejected so the inferred spelling stands.
|
||||||
|
pub fn assert_respell_precedence() {
|
||||||
|
let (score, pid) = corpus::override_probe();
|
||||||
|
let replica = score.identity.replica_id;
|
||||||
|
|
||||||
|
// Baseline: no override → the algorithm's inferred spelling stands.
|
||||||
|
let base = derive_annotations(&score, &profile());
|
||||||
|
let base_rs = base.spellings.get(&pid).expect("probe pitch is spelled");
|
||||||
|
assert!(
|
||||||
|
matches!(base_rs.provenance, SpellingProvenance::Inferred),
|
||||||
|
"baseline provenance should be Inferred, was {:?}",
|
||||||
|
base_rs.provenance
|
||||||
|
);
|
||||||
|
let inferred = base_rs.spelling.clone();
|
||||||
|
|
||||||
|
// A clearly distinct authored spelling (Db4), engraved layer, UserChosen.
|
||||||
|
let override_spelling = PitchSpelling {
|
||||||
|
nominal: SpellingNominal::Cmn(CmnNominal::D),
|
||||||
|
accidentals: vec![AccidentalId::new("flat")],
|
||||||
|
octave: 4,
|
||||||
|
render_hints: Default::default(),
|
||||||
|
};
|
||||||
|
let mut s2 = score.clone();
|
||||||
|
s2.spelling_attachments.push(SpellingAttachment {
|
||||||
|
scope: SpellingScope::Pitch(pid),
|
||||||
|
directive: SpellingDirective::Explicit(override_spelling.clone()),
|
||||||
|
source: SpellingSource::UserChosen,
|
||||||
|
priority: 0,
|
||||||
|
layer: None,
|
||||||
|
});
|
||||||
|
let ann2 = derive_annotations(&s2, &profile());
|
||||||
|
let rs2 = ann2
|
||||||
|
.spellings
|
||||||
|
.get(&pid)
|
||||||
|
.expect("overridden pitch is spelled");
|
||||||
|
assert_eq!(
|
||||||
|
rs2.spelling, override_spelling,
|
||||||
|
"authored UserChosen override did not take precedence"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rs2.provenance,
|
||||||
|
SpellingProvenance::Authored(SpellingSourceKind::UserChosen),
|
||||||
|
"override provenance should be Authored(UserChosen)"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
rs2.spelling, inferred,
|
||||||
|
"the override fixture must differ from the inferred spelling to be meaningful"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A non-engraved (analysis-layer) override must be ignored: engraved only.
|
||||||
|
let mut s3 = score.clone();
|
||||||
|
s3.spelling_attachments.push(SpellingAttachment {
|
||||||
|
scope: SpellingScope::Pitch(pid),
|
||||||
|
directive: SpellingDirective::Explicit(override_spelling.clone()),
|
||||||
|
source: SpellingSource::UserChosen,
|
||||||
|
priority: 0,
|
||||||
|
layer: Some(AnalysisLayerId::new(replica, 1)),
|
||||||
|
});
|
||||||
|
let ann3 = derive_annotations(&s3, &profile());
|
||||||
|
let rs3 = ann3.spellings.get(&pid).expect("pitch is spelled");
|
||||||
|
assert_eq!(
|
||||||
|
rs3.spelling, inferred,
|
||||||
|
"a layer-tagged (non-engraved) override must not win"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(rs3.provenance, SpellingProvenance::Inferred),
|
||||||
|
"layer-tagged override should leave provenance Inferred"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Two competing engraved overrides on the same pitch resolve by `priority`,
|
||||||
|
// not attachment order. Gb4 and F#4 both realize pc 6, so each is a
|
||||||
|
// legitimate spelling — only `priority` decides. The lower-priority one is
|
||||||
|
// listed *first*, so a naive "first attachment wins" would pick Gb4; the
|
||||||
|
// higher-priority F#4 must win instead.
|
||||||
|
let gb4 = PitchSpelling {
|
||||||
|
nominal: SpellingNominal::Cmn(CmnNominal::G),
|
||||||
|
accidentals: vec![AccidentalId::new("flat")],
|
||||||
|
octave: 4,
|
||||||
|
render_hints: Default::default(),
|
||||||
|
};
|
||||||
|
let fs4 = PitchSpelling {
|
||||||
|
nominal: SpellingNominal::Cmn(CmnNominal::F),
|
||||||
|
accidentals: vec![AccidentalId::new("sharp")],
|
||||||
|
octave: 4,
|
||||||
|
render_hints: Default::default(),
|
||||||
|
};
|
||||||
|
let mut s4 = score.clone();
|
||||||
|
s4.spelling_attachments.push(SpellingAttachment {
|
||||||
|
scope: SpellingScope::Pitch(pid),
|
||||||
|
directive: SpellingDirective::Explicit(gb4),
|
||||||
|
source: SpellingSource::UserChosen,
|
||||||
|
priority: 1, // first, lower priority — must lose
|
||||||
|
layer: None,
|
||||||
|
});
|
||||||
|
s4.spelling_attachments.push(SpellingAttachment {
|
||||||
|
scope: SpellingScope::Pitch(pid),
|
||||||
|
directive: SpellingDirective::Explicit(fs4.clone()),
|
||||||
|
source: SpellingSource::UserChosen,
|
||||||
|
priority: 9, // second, higher priority — must win
|
||||||
|
layer: None,
|
||||||
|
});
|
||||||
|
let ann4 = derive_annotations(&s4, &profile());
|
||||||
|
let rs4 = ann4.spellings.get(&pid).expect("pitch is spelled");
|
||||||
|
assert_eq!(
|
||||||
|
rs4.spelling, fs4,
|
||||||
|
"the higher-priority engraved override must win regardless of attachment order"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rs4.provenance,
|
||||||
|
SpellingProvenance::Authored(SpellingSourceKind::UserChosen),
|
||||||
|
"the winning override's provenance should be Authored(UserChosen)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// An engraved override whose source does not outrank `Inferred` in the
|
||||||
|
// score's precedence (`Analytical`, ranked below `Inferred` by default) is
|
||||||
|
// rejected: the inferred spelling stands. This exercises the precedence-rank
|
||||||
|
// gate, distinct from the engraved-layer gate above — a high `priority`
|
||||||
|
// cannot rescue a source that loses on rank.
|
||||||
|
let mut s5 = score.clone();
|
||||||
|
s5.spelling_attachments.push(SpellingAttachment {
|
||||||
|
scope: SpellingScope::Pitch(pid),
|
||||||
|
directive: SpellingDirective::Explicit(override_spelling),
|
||||||
|
source: SpellingSource::Analytical,
|
||||||
|
priority: 100,
|
||||||
|
layer: None,
|
||||||
|
});
|
||||||
|
let ann5 = derive_annotations(&s5, &profile());
|
||||||
|
let rs5 = ann5.spellings.get(&pid).expect("pitch is spelled");
|
||||||
|
assert_eq!(
|
||||||
|
rs5.spelling, inferred,
|
||||||
|
"an engraved override whose source does not outrank Inferred must be ignored"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(rs5.provenance, SpellingProvenance::Inferred),
|
||||||
|
"a non-outranking override should leave provenance Inferred"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Spelling correctness vs. published tonal expectations
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
fn describe(s: &PitchSpelling) -> (CmnNominal, String) {
|
||||||
|
let nominal = match s.nominal {
|
||||||
|
SpellingNominal::Cmn(n) => n,
|
||||||
|
_ => panic!("expected a CMN spelling"),
|
||||||
|
};
|
||||||
|
let acc = s
|
||||||
|
.accidentals
|
||||||
|
.iter()
|
||||||
|
.map(|a| a.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("+");
|
||||||
|
(nominal, acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_named(name: &str) -> Score {
|
||||||
|
let f = corpus::corpus()
|
||||||
|
.into_iter()
|
||||||
|
.find(|f| f.name == name)
|
||||||
|
.unwrap_or_else(|| panic!("no corpus fixture named `{name}`"));
|
||||||
|
(f.build)()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On a monophonic fixture (one pitch per event, minted in melodic order, so the
|
||||||
|
/// `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 got: Vec<(CmnNominal, String)> = ann
|
||||||
|
.spellings
|
||||||
|
.values()
|
||||||
|
.map(|rs| describe(&rs.spelling))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
got.len(),
|
||||||
|
expected.len(),
|
||||||
|
"{name}: {} spellings, expected {}",
|
||||||
|
got.len(),
|
||||||
|
expected.len()
|
||||||
|
);
|
||||||
|
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
|
||||||
|
assert_eq!(
|
||||||
|
*g,
|
||||||
|
(e.0, e.1.to_string()),
|
||||||
|
"{name}[{i}]: spelled {g:?}, expected ({:?}, {:?})",
|
||||||
|
e.0,
|
||||||
|
e.1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diatonic scales in C / D / Bb spell to their published key spellings — the
|
||||||
|
/// "matches published Temperley/Longuet-Higgins expectations on standard cases"
|
||||||
|
/// criterion, on unambiguous tonal lines.
|
||||||
|
pub fn assert_spelling_matches_published_expectations() {
|
||||||
|
use CmnNominal::*;
|
||||||
|
assert_line(
|
||||||
|
"c_major_scale",
|
||||||
|
&[
|
||||||
|
(C, ""),
|
||||||
|
(D, ""),
|
||||||
|
(E, ""),
|
||||||
|
(F, ""),
|
||||||
|
(G, ""),
|
||||||
|
(A, ""),
|
||||||
|
(B, ""),
|
||||||
|
(C, ""),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_line(
|
||||||
|
"d_major_scale",
|
||||||
|
&[
|
||||||
|
(D, ""),
|
||||||
|
(E, ""),
|
||||||
|
(F, "sharp"),
|
||||||
|
(G, ""),
|
||||||
|
(A, ""),
|
||||||
|
(B, ""),
|
||||||
|
(C, "sharp"),
|
||||||
|
(D, ""),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_line(
|
||||||
|
"b_flat_major_scale",
|
||||||
|
&[
|
||||||
|
(B, "flat"),
|
||||||
|
(C, ""),
|
||||||
|
(D, ""),
|
||||||
|
(E, "flat"),
|
||||||
|
(F, ""),
|
||||||
|
(G, ""),
|
||||||
|
(A, ""),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Non-vacuity (the F tripwire)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// The gate must go **red if H were stubbed**. Across the corpus: spellings span
|
||||||
|
/// multiple distinct nominals and include at least one accidental (a constant
|
||||||
|
/// `C4` stub yields exactly one nominal and no accidentals); decompositions use
|
||||||
|
/// at least two distinct note values and include at least one tied multi-
|
||||||
|
/// component split (an empty/no-op decomposition map yields neither).
|
||||||
|
pub fn assert_non_vacuity() {
|
||||||
|
let report = corpus::classify_corpus();
|
||||||
|
|
||||||
|
let mut nominal_seen = [false; 7];
|
||||||
|
let mut value_seen = [false; 7];
|
||||||
|
let mut any_accidental = false;
|
||||||
|
let mut any_tie_chain = false;
|
||||||
|
let mut inferred_spellings = 0usize;
|
||||||
|
let mut decompositions = 0usize;
|
||||||
|
|
||||||
|
// Per-fixture spread: how many *distinct* fixtures independently exhibit each
|
||||||
|
// richness signal. A corpus-wide OR/sum can be satisfied by a single rich
|
||||||
|
// fixture, so a *partial* stub (H broken for every input but one) would slip
|
||||||
|
// past the aggregate checks below; requiring each signal to recur across ≥2
|
||||||
|
// fixtures closes that.
|
||||||
|
let mut fixtures_rich_nominals = 0usize; // a fixture spanning ≥5 inferred nominals
|
||||||
|
let mut fixtures_with_accidental = 0usize;
|
||||||
|
let mut fixtures_multivalue_decomp = 0usize; // a fixture spanning ≥2 note values
|
||||||
|
let mut fixtures_with_tie_chain = 0usize;
|
||||||
|
|
||||||
|
for f in &report.fixtures {
|
||||||
|
let mut f_nominal_seen = [false; 7];
|
||||||
|
let mut f_value_seen = [false; 7];
|
||||||
|
let mut f_accidental = false;
|
||||||
|
let mut f_tie_chain = false;
|
||||||
|
|
||||||
|
for rs in f.annotations.spellings.values() {
|
||||||
|
if matches!(rs.provenance, SpellingProvenance::Inferred) {
|
||||||
|
inferred_spellings += 1;
|
||||||
|
if let SpellingNominal::Cmn(n) = rs.spelling.nominal {
|
||||||
|
nominal_seen[n as usize] = true;
|
||||||
|
f_nominal_seen[n as usize] = true;
|
||||||
|
}
|
||||||
|
if !rs.spelling.accidentals.is_empty() {
|
||||||
|
any_accidental = true;
|
||||||
|
f_accidental = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for d in f.annotations.decompositions.values() {
|
||||||
|
decompositions += 1;
|
||||||
|
if d.components.len() > 1 {
|
||||||
|
any_tie_chain = true;
|
||||||
|
f_tie_chain = true;
|
||||||
|
}
|
||||||
|
for c in &d.components {
|
||||||
|
value_seen[c.base_value as usize] = true;
|
||||||
|
f_value_seen[c.base_value as usize] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f_nominal_seen.iter().filter(|x| **x).count() >= 5 {
|
||||||
|
fixtures_rich_nominals += 1;
|
||||||
|
}
|
||||||
|
if f_accidental {
|
||||||
|
fixtures_with_accidental += 1;
|
||||||
|
}
|
||||||
|
if f_value_seen.iter().filter(|x| **x).count() >= 2 {
|
||||||
|
fixtures_multivalue_decomp += 1;
|
||||||
|
}
|
||||||
|
if f_tie_chain {
|
||||||
|
fixtures_with_tie_chain += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let distinct_nominals = nominal_seen.iter().filter(|x| **x).count();
|
||||||
|
let distinct_values = value_seen.iter().filter(|x| **x).count();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
inferred_spellings > 0,
|
||||||
|
"no inferred spellings at all — H produced nothing"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
distinct_nominals >= 5,
|
||||||
|
"inferred spellings collapse to {distinct_nominals} nominal(s); a real \
|
||||||
|
line-of-fifths algorithm spans the diatonic letters — looks stubbed"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
any_accidental,
|
||||||
|
"no accidental anywhere in the corpus's inferred spellings — looks like \
|
||||||
|
the constant middle-C stub"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
decompositions > 0,
|
||||||
|
"no decompositions at all — H produced nothing"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
distinct_values >= 2,
|
||||||
|
"decompositions use {distinct_values} note value(s); real decomposition \
|
||||||
|
spans several — looks stubbed"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
any_tie_chain,
|
||||||
|
"no multi-component (tied) decomposition anywhere — syncopation/barline \
|
||||||
|
splitting looks stubbed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Spread: no single rich fixture may carry a richness signal for the whole
|
||||||
|
// corpus (a partial stub that breaks H for all inputs but one would still
|
||||||
|
// satisfy the aggregate ORs above).
|
||||||
|
assert!(
|
||||||
|
fixtures_rich_nominals >= 2,
|
||||||
|
"only {fixtures_rich_nominals} fixture(s) independently span ≥5 nominals; a \
|
||||||
|
real algorithm spells many fixtures richly — a partial stub looks like this"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
fixtures_with_accidental >= 2,
|
||||||
|
"only {fixtures_with_accidental} fixture(s) produce any accidental; \
|
||||||
|
accidentals should arise across several fixtures, not be carried by one"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
fixtures_multivalue_decomp >= 2,
|
||||||
|
"only {fixtures_multivalue_decomp} fixture(s) span ≥2 note values; real \
|
||||||
|
decomposition varies values across many fixtures, not one"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
fixtures_with_tie_chain >= 2,
|
||||||
|
"only {fixtures_with_tie_chain} fixture(s) produce a tied multi-component \
|
||||||
|
split; syncopation/barline splitting should recur across fixtures, not one"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Materialization pipeline (criterion-5 path)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// H's derivation stays deterministic when run on real reduced scores from the
|
||||||
|
/// criterion-5 convergence path, and is non-vacuous on them — so criterion 5
|
||||||
|
/// continues to pass with non-trivial pre-pass outputs downstream of
|
||||||
|
/// materialization.
|
||||||
|
///
|
||||||
|
/// This guards **determinism**, **decomposition reconstruction** (invariant 15),
|
||||||
|
/// and **non-emptiness** on real scores. It does *not* independently exercise
|
||||||
|
/// inferred-spelling **correctness**: the generators mint authored-CMN pitches,
|
||||||
|
/// which take the authored-letter path in [`epiphany_core::derive_annotations`]
|
||||||
|
/// and bypass the line-of-fifths inference. That correctness signal lives in the
|
||||||
|
/// integer-pitch corpus fixtures (`cmn-12` positions with no authored letter) via
|
||||||
|
/// [`assert_spelling_matches_published_expectations`] and
|
||||||
|
/// [`assert_eligible_pitches_spelled`] over the corpus — do not rely on this path
|
||||||
|
/// to catch a spelling-inference regression.
|
||||||
|
pub fn assert_deterministic_in_materialization_pipeline(scale: u64) {
|
||||||
|
let n = (16 * scale).max(8);
|
||||||
|
let mut spelled_anywhere = false;
|
||||||
|
for seed in 0..n {
|
||||||
|
let (score, _frontier) = crate::convergence::materialized_score(
|
||||||
|
seed.wrapping_mul(0x9E37_79B9).wrapping_add(101),
|
||||||
|
);
|
||||||
|
assert_derivation_deterministic(&score);
|
||||||
|
let ann = derive_annotations(&score, &profile());
|
||||||
|
if !ann.spellings.is_empty() {
|
||||||
|
spelled_anywhere = true;
|
||||||
|
}
|
||||||
|
// Whatever H produces on a real reduced score must still reconstruct.
|
||||||
|
assert_decompositions_reconstruct(&score, &ann);
|
||||||
|
assert_eligible_pitches_spelled(&score, &ann);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
spelled_anywhere,
|
||||||
|
"the pre-pass produced no spellings on any materialized score — vacuous \
|
||||||
|
in the real pipeline"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// The whole gate
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Runs the complete H merge gate. `scale` multiplies the materialization-path
|
||||||
|
/// iteration count (1 for the unit budget, more for the soak).
|
||||||
|
pub fn run_all(scale: u64) {
|
||||||
|
// Determinism.
|
||||||
|
assert_corpus_deterministic();
|
||||||
|
|
||||||
|
// Eligibility + reconstruction over every fixture.
|
||||||
|
for f in corpus::corpus() {
|
||||||
|
let score = (f.build)();
|
||||||
|
let ann = derive_annotations(&score, &profile());
|
||||||
|
assert_eligible_pitches_spelled(&score, &ann);
|
||||||
|
assert_decompositions_reconstruct(&score, &ann);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authored-override precedence.
|
||||||
|
assert_respell_precedence();
|
||||||
|
|
||||||
|
// Spelling correctness on published tonal cases.
|
||||||
|
assert_spelling_matches_published_expectations();
|
||||||
|
|
||||||
|
// Non-vacuity (the tripwire).
|
||||||
|
assert_non_vacuity();
|
||||||
|
|
||||||
|
// Determinism + non-vacuity through the criterion-5 materialization path.
|
||||||
|
assert_deterministic_in_materialization_pipeline(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn h_merge_gate_passes() {
|
||||||
|
run_all(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn respell_precedence_rule() {
|
||||||
|
assert_respell_precedence();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spelling_matches_published_cases() {
|
||||||
|
assert_spelling_matches_published_expectations();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_vacuity_guard() {
|
||||||
|
assert_non_vacuity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
//! **Agent H's merge gate** (Phase 2, Agent F): the spelling + notational-
|
||||||
|
//! decomposition pre-passes (`epiphany_core::derive_annotations`) asserted
|
||||||
|
//! against H's `PHASE2_QUICKSTART` acceptance criterion, driven only through the
|
||||||
|
//! testkit's public surface. A discrete `cargo test` target so a failure is
|
||||||
|
//! attributable to H (its own CI job, `spec/PHASE2_F_WEEK0_WORKLIST.md` F4); the
|
||||||
|
//! heavy soak version lives in `examples/conformance_suite.rs`.
|
||||||
|
//!
|
||||||
|
//! If any of these fail, H's pre-pass stage is not done — and the non-vacuity
|
||||||
|
//! guard ([`epiphany_testkit::prepass_harness::assert_non_vacuity`]) would fail
|
||||||
|
//! if H's work were stubbed or vacuous (the F discipline).
|
||||||
|
|
||||||
|
use epiphany_testkit::{corpus, prepass_harness};
|
||||||
|
|
||||||
|
/// **F3 — representative corpus + eligibility taxonomy.** ≥20 invariant-clean
|
||||||
|
/// fixtures across common / edge / torture tiers; every taxonomy bucket is
|
||||||
|
/// non-empty (or explicitly deferred), H's per-kind counts agree with an
|
||||||
|
/// independent walk, and every event is bucketed (nothing silently absent).
|
||||||
|
#[test]
|
||||||
|
fn h_taxonomy_corpus_coverage() {
|
||||||
|
corpus::run_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **F4(H) — the merge gate.** Determinism across runs, eligible-pitch spelling
|
||||||
|
/// correctness (by pitch class), decomposition reconstruction (invariant 15),
|
||||||
|
/// `RespellPitch` precedence, spelling vs. published tonal cases, and the
|
||||||
|
/// non-vacuity tripwire.
|
||||||
|
#[test]
|
||||||
|
fn h_merge_gate() {
|
||||||
|
prepass_harness::run_all(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The derivation stays deterministic and non-vacuous when run on real reduced
|
||||||
|
/// scores from the criterion-5 convergence path (so criterion 5 keeps passing
|
||||||
|
/// with non-trivial pre-pass outputs downstream of materialization).
|
||||||
|
#[test]
|
||||||
|
fn h_pre_pass_in_materialization_pipeline() {
|
||||||
|
prepass_harness::assert_deterministic_in_materialization_pipeline(1);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Pass 12 — Batch Tracker
|
||||||
|
|
||||||
|
*Maintained by Agent F (testkit & tripwire), per `spec/PHASE2_QUICKSTART.md`:
|
||||||
|
"Ambiguities discovered during Phase 2 implementation go into a Pass 12 batch,
|
||||||
|
not into code improvisations. … Don't open Pass 12 until at least 3 items
|
||||||
|
accumulate."*
|
||||||
|
|
||||||
|
**Status: OPEN.** Agent H's landing (spelling + decomposition pre-passes)
|
||||||
|
surfaced five candidates, crossing the ≥3 threshold. This file is the running
|
||||||
|
collection; G ratifies (or defers/rejects) the batch when Phase 2's open
|
||||||
|
questions are resolved. F collects, F does not resolve.
|
||||||
|
|
||||||
|
## How an item lands here
|
||||||
|
|
||||||
|
When implementation hits a behavior the ratified spec does not determine, the
|
||||||
|
responsible agent records it in their crate's `DECISIONS.md` with an ID
|
||||||
|
(`P12-<agent><n>`) and a one-line rationale, then adds a row below. Improvising in
|
||||||
|
code instead is the failure mode this batch exists to prevent.
|
||||||
|
|
||||||
|
## Items
|
||||||
|
|
||||||
|
| ID | Source | Summary | Disposition target |
|
||||||
|
|----|--------|---------|--------------------|
|
||||||
|
| P12-H1 | `epiphany-core` H | Ratify `SpellingAlgorithmId::Default` = Temperley line-of-fifths v1 (Pass 11 closed before H landed; the id `"default"` is the crate's proposal until ratified — not a byte layout, so nothing golden-locks on it). | G (algorithm-choice ratification) |
|
||||||
|
| P12-H2 | `epiphany-core` H | `KeySignatureChange` / `ClefChange` are anchor-only placeholders; context-aware spelling infers tonal context from the melody (line-of-fifths centre of gravity) rather than a *declared* key. A real key/clef content model would let spelling/decomposition honour declared keys and place cancelling naturals. Flagged as a graph-model gap. | G (graph model) |
|
||||||
|
| P12-H3 | `epiphany-core` H | Chromatic-run convention (ascending = sharps, descending = flats) is only a *tiebreak* in the centre-of-gravity rule, so an isolated chromatic run with no tonal context may pick the enharmonic the convention would not. Voice-leading refinement deferred. | G / Pass 12 (spelling) |
|
||||||
|
| P12-H4 | `epiphany-core` H | Decomposition simplifications: single governing meter per region (multi/mid-region meter changes deferred); region origin assumed a barline (anacrusis deferred); compound-meter beat grouping beyond the dyadic default; tuplet nesting and cross-beat tuplet members; double+ augmentation dots (`MAX_DOTS = 1`). | G (decomposition scope) |
|
||||||
|
| P12-H5 | `epiphany-core` H | Automatic spelling under aleatoric regions (the spec's open question). H spells pitches region-independently but performs no region-specific aleatoric spelling; defer if the algorithm does not generalise cleanly. | G / Pass 12 (open question) |
|
||||||
|
|
||||||
|
## Not yet open elsewhere
|
||||||
|
|
||||||
|
Track A's other agents (I) and Track B (K, J) have not yet contributed items.
|
||||||
|
When they do, append rows; the batch is already open, so they join directly (no
|
||||||
|
new threshold).
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Phase 2 — Agent F Week-0 Worklist
|
||||||
|
|
||||||
|
*Companion to `spec/PHASE2_QUICKSTART.md`. Scope: the net-new F artifacts that the Phase 2 agents (G–K) need in order to **land**, ordered for front-loading.*
|
||||||
|
|
||||||
|
## Premise
|
||||||
|
|
||||||
|
F's v0 mandate is **complete and honest** — all six v0 acceptance criteria pass
|
||||||
|
(`cargo test -p epiphany-testkit --test acceptance` → 13/13, **0 ignored**), and
|
||||||
|
the gates were hardened from "green-but-projection" to real-`Score` last session.
|
||||||
|
Nothing here is a v0 fix.
|
||||||
|
|
||||||
|
Nothing on this list blocks Phase 2 from *beginning*. Every Phase 2 agent's
|
||||||
|
*start* is gated on G's Pass 11 byte conventions and/or a peer's output, never on
|
||||||
|
an unfinished F deliverable. G is already running (`b2f2e20`, `a7adbdc`,
|
||||||
|
`0d8ec61`). **This list is what F must produce so the agents can *land* and be
|
||||||
|
*gated* — front-loaded because several agents must develop against these
|
||||||
|
artifacts, not just be tested by them.**
|
||||||
|
|
||||||
|
The hard scheduling constraint: agents land ~Week 10–15; **per-agent harnesses
|
||||||
|
must be in CI by ~Week 6** so no agent outruns its gate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority-ordered items
|
||||||
|
|
||||||
|
### F0 — Structural decision: `benches/`, `xtask`, and the reference-suite gap *(do first; it's a 1-day call that everything else lands inside of)*
|
||||||
|
|
||||||
|
- **Why now:** F is about to add a `benches/` directory and the integration
|
||||||
|
harness. The QUICKSTART topology names `xtask/` and `tests/reference-suite/`
|
||||||
|
that **do not exist** (HANDOFF §7-F) — their function is currently met by
|
||||||
|
per-crate `examples/*` fuzzers + `epiphany-testkit/examples/conformance_suite.rs`.
|
||||||
|
Decide the home for the new harnesses *before* writing them.
|
||||||
|
- **Current state:** No `benches/`, no `xtask/`, no `tests/reference-suite/`.
|
||||||
|
- **Deliverable:** A one-paragraph decision recorded in
|
||||||
|
`crates/epiphany-testkit/DECISIONS.md`: adopt `xtask` for orchestration or keep
|
||||||
|
the examples pattern; where `benches/` lives (testkit vs. per-crate); whether
|
||||||
|
the integration harness is a `tests/` integration test or an `examples/` runnable.
|
||||||
|
- **Acceptance:** Decision written; no code yet. Unblocks F1, F4, F5.
|
||||||
|
|
||||||
|
### F1 — `benches/` scaffold with xfail thresholds *(unblocks K's perf gate)*
|
||||||
|
|
||||||
|
- **Why:** K's acceptance requires "F's performance bench passes the documented
|
||||||
|
budget" at 10K-envelope scale; F's mandate is to **set the threshold first as an
|
||||||
|
xfail gate**, then K fixes the O(n²) `canonical_reduction_order` indegree
|
||||||
|
construction when the bench goes red. The bench must exist before K can be held
|
||||||
|
to it.
|
||||||
|
- **Current state:** No `benches/` directory anywhere in the workspace.
|
||||||
|
- **Deliverable:** `criterion`-based bench (location per F0) asserting Chapter-10
|
||||||
|
budgets at documented scale points (1K = current criterion-5 scale, 10K, 50K),
|
||||||
|
with known-pending points marked xfail with the budget written in the bench.
|
||||||
|
- **Acceptance:** `cargo bench` runs; 10K point is a documented xfail with a
|
||||||
|
numeric budget; passing at 1K. F **surfaces**, K fixes.
|
||||||
|
|
||||||
|
### F2 — Frozen v0 envelope corpus *(unblocks K migration + J round-trip regression)*
|
||||||
|
|
||||||
|
- **Why:** K's v0→v1 migration must be proven deterministic and
|
||||||
|
equivalence-preserving against a **v0 envelope corpus**; J's criterion-4 gate
|
||||||
|
must hold byte-for-byte on the same v0 corpus. Both need a *frozen, persisted*
|
||||||
|
corpus, not a re-generated one.
|
||||||
|
- **Current state:** Edit-session **generators** exist
|
||||||
|
(`convergence.rs`: `two_staff_edit_session`, `graph_edit_session`), seeded and
|
||||||
|
deterministic — but nothing is captured as a stable regression artifact.
|
||||||
|
- **Deliverable:** Capture a representative v0 envelope set (drawn from the
|
||||||
|
existing generators at fixed seeds), freeze its canonical bytes as a golden
|
||||||
|
fixture, and expose a `testkit` accessor (`fn v0_envelope_corpus() -> ...`) plus
|
||||||
|
a golden-lock test that fails if the v0 byte shape drifts.
|
||||||
|
- **Acceptance:** Corpus accessor + golden-lock test in CI; documented seeds; the
|
||||||
|
bytes are the regression guard K's migration and J's decoder run against.
|
||||||
|
|
||||||
|
### F3 — H's representative score corpus + kind-by-kind taxonomy harness *(most underbuilt dependency; unblocks H entirely)*
|
||||||
|
|
||||||
|
- **Why:** H's *whole* acceptance rests on "F's representative corpus (>20
|
||||||
|
fixtures) spanning common / edge / torture cases" exercising the event-kind
|
||||||
|
**eligibility taxonomy** — and H needs it to develop against, not just to be
|
||||||
|
tested by. This is the single largest authoring gap.
|
||||||
|
- **Current state:** Exactly one hand-built fixture
|
||||||
|
(`fixtures.rs::ten_measure_single_staff`) + two generators
|
||||||
|
(`generators.rs::valid_score`, `valid_score_rich`). **None** exercise: rests,
|
||||||
|
unpitched/percussion spelling, trajectory/graphic/indeterminate events,
|
||||||
|
proportional/aleatoric regions, tuplets, ties across barlines/tempo changes,
|
||||||
|
syncopation.
|
||||||
|
- **Deliverable:** ≥20 invariant-clean fixtures in `fixtures.rs` (or a
|
||||||
|
`fixtures/` submodule) tagged by the taxonomy from PHASE2_QUICKSTART §H "What H
|
||||||
|
produces, by event kind," plus a harness that **classifies and counts** each
|
||||||
|
case (so "ineligible" is explicit and counted, never silently absent).
|
||||||
|
- **Acceptance:** ≥20 fixtures, each `check_invariants`-clean; harness emits
|
||||||
|
per-kind counts; the taxonomy buckets (eligible-pitch, eligible-duration,
|
||||||
|
rest, unpitched, trajectory/graphic/indeterminate, proportional/aleatoric) are
|
||||||
|
each non-empty or explicitly marked deferred.
|
||||||
|
- **Note:** This is the long pole. Start immediately even though H lands ~Week 10.
|
||||||
|
|
||||||
|
### F4 — Per-agent harness templates + skeletons in CI *(the ~Week-6 deadline; unblocks gating for all of G–K)*
|
||||||
|
|
||||||
|
- **Why:** "Treat F's harness for your scope as your merge gate." If an agent
|
||||||
|
races ahead of its harness, it merges ungated. Templates first (Week 0), real
|
||||||
|
harnesses wired as stages land (by ~Week 6).
|
||||||
|
- **Current state:** v0 harnesses exist (`roundtrip`, `convergence`,
|
||||||
|
`equivocation`, `bundle_harness`, `negative`, `layout_stub`). No H/I/K/J-specific
|
||||||
|
harnesses.
|
||||||
|
- **Deliverable:** Skeleton modules + CI jobs (initially xfail/empty) for:
|
||||||
|
- **H:** spelling/decomposition determinism across runs; `RespellPitch`
|
||||||
|
precedence; non-vacuity.
|
||||||
|
- **I:** hard-constraint validation against *declared* IR constraints (not
|
||||||
|
bounding-box heuristics) + class-specific collision rules
|
||||||
|
(accidental-vs-notehead, stem-vs-beam, staff-line-vs-glyph); provenance
|
||||||
|
survival; SVG XML-validity; golden-locked machine acceptance snapshot
|
||||||
|
(object/glyph/bbox-class/provenance/constraint counts).
|
||||||
|
- **K:** v0→v1 migration determinism + equivalence; payload-schema completeness
|
||||||
|
(every K0 primitive has an `epiphany-ops` payload type); K1 framework present.
|
||||||
|
- **J:** cross-impl decoder test; wire-format fuzzer; canonicalization tests
|
||||||
|
(map order, NFC, `-0.0`, rational reduction, unknown-field preservation).
|
||||||
|
- **Acceptance:** Each module exists with a **non-vacuity guard** (would fail if
|
||||||
|
the agent's work were stubbed); each is a CI job; each asserts its agent's
|
||||||
|
stated PHASE2_QUICKSTART acceptance criterion once the agent's stage is real.
|
||||||
|
|
||||||
|
### F5 — End-to-end integration-harness skeleton *(the Phase-2 close gate; skeleton now, real by close)*
|
||||||
|
|
||||||
|
- **Why:** The marquee F deliverable — proves the two tracks *compose* at the
|
||||||
|
seam (both can be green while the serialized visible score is broken between
|
||||||
|
them). Skeleton Week 0 against stub stages; real stages swapped in as H/I/K/J
|
||||||
|
land; fully real by Phase-2 close.
|
||||||
|
- **Current state:** Does not exist.
|
||||||
|
- **Deliverable:** A single-fixture pipeline runner (home per F0):
|
||||||
|
`Score → reduction → H → layout IR → I solver → SVG → J write → J read →
|
||||||
|
reduction → H → layout → SVG`, asserting canonical state and SVG are
|
||||||
|
byte-identical between passes (modulo explicitly allowed non-canonical caches).
|
||||||
|
Stages stubbed where the agent hasn't landed.
|
||||||
|
- **Acceptance:** Skeleton runs end-to-end with stub stages now; documented swap
|
||||||
|
points for each real stage; the byte-identity assertion is wired (trivially
|
||||||
|
passing on stubs, meaningfully once stages are real).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Explicitly *not* Week-0 (landing gates that mature later)
|
||||||
|
|
||||||
|
- Real cross-implementation decoder written from J's companion **text** — waits on
|
||||||
|
J's Binary Format companion existing.
|
||||||
|
- Wire-format fuzzer at 1M-iteration CI soak — F4 stands up the skeleton; the
|
||||||
|
real fuzzer matures with J's codec.
|
||||||
|
- Integration harness with **all real** stages — that's the Phase-2 close
|
||||||
|
condition (F5 is just the skeleton).
|
||||||
|
- Pass 12 batch tracker — created when the first ambiguity lands; don't open Pass
|
||||||
|
12 until ≥3 items (same rule as v0).
|
||||||
|
|
||||||
|
## Ordering
|
||||||
|
|
||||||
|
```
|
||||||
|
F0 (decision) ──┬─> F1 (benches) ── unblocks K perf gate
|
||||||
|
├─> F2 (v0 corpus) ── unblocks K migration + J roundtrip
|
||||||
|
├─> F4 (harness skeletons) ── ~Week-6 CI deadline for all gates
|
||||||
|
└─> F5 (integration skeleton) ── Phase-2 close gate
|
||||||
|
F3 (H corpus) ──── start in parallel; long pole; H can't develop without it
|
||||||
|
```
|
||||||
|
|
||||||
|
F3 and F0 can start the same day. F0 must precede F1/F4/F5 (it decides where they
|
||||||
|
live). F3 is independent of F0 and is the item most likely to slip if deferred.
|
||||||
|
|
@ -0,0 +1,468 @@
|
||||||
|
# Epiphany Phase 2 — Implementation Quickstart (Agents G–K)
|
||||||
|
|
||||||
|
This is the operational reference for the next phase. The foundation exists; the full architecture and contract is in `spec/core_spec.pdf` (Pass 10 / 244 pages, pending Pass 11 ratification). The v0 dispatch is in `spec/QUICKSTART.md` and remains the canonical context for *what's already built and why*. This document is the dispatch layer for what comes next.
|
||||||
|
|
||||||
|
## Project context
|
||||||
|
|
||||||
|
The v0 prototype baseline is complete: six Rust library crates that prove the architecture works end to end. All six v0 acceptance criteria pass; gates are honest; per-crate decisions and Pass-11 candidates are batched and golden-locked. A developer can build on the crates; a musician cannot yet see, hear, or save anything.
|
||||||
|
|
||||||
|
Phase 2 turns the foundation into something perceivable, durable, and ratified:
|
||||||
|
|
||||||
|
- **Pass 11 (G)** ratifies the durable byte layouts so storage is stable before anyone writes documents on top.
|
||||||
|
- **Track A (visible slice — H, I)** makes notes appear on a page: spelling, decomposition, real engraving, SVG output. This is what makes Epiphany demonstrable.
|
||||||
|
- **Track B (interchange core — K, J)** makes documents portable: real operation payloads and a finalized wire format. This is what makes Epiphany interoperable.
|
||||||
|
- **F continues** as the cross-cutting tripwire. Mandate broadens substantially; nothing else changes.
|
||||||
|
|
||||||
|
The two tracks run in parallel after Pass 11's Bucket 1 lands. They will not redesign each other, but they *will* coordinate through core value types, serialization conventions, algorithm IDs, and canonical derived annotations. F owns the **end-to-end integration harness** that proves the tracks compose, not just that each side passes its own gate. Without that harness, both tracks can be green while the visible serialized score is broken at the seam.
|
||||||
|
|
||||||
|
The architecture is frozen; Phase 2 is build-out and ratification, not redesign.
|
||||||
|
|
||||||
|
## Before you start
|
||||||
|
|
||||||
|
Every Phase 2 agent must:
|
||||||
|
|
||||||
|
1. Read `spec/QUICKSTART.md` (the v0 dispatch) for context on what already exists. The architectural decisions in it are not up for debate; they are the substrate Phase 2 builds on.
|
||||||
|
2. Read the spec chapters that map to your agent (see assignments below). Pass 11's revision will land in the same document; you are responsible for following its updates.
|
||||||
|
3. Read the relevant `crates/*/DECISIONS.md` files in your scope. Every implementation call made in v0 is recorded there. If a Phase 2 decision contradicts a v0 decision, that's a discussion to have explicitly, not an override to assume.
|
||||||
|
4. **Treat the spec as the contract.** Same rule as v0. If a behavior is ratified after Pass 11, implement it. If it's not — flag it for Pass 12, don't improvise.
|
||||||
|
5. **Treat F's harness for your scope as your merge gate.** Same rule as v0. The harness is the tripwire that catches what review and intuition miss.
|
||||||
|
|
||||||
|
## Crate topology (additions)
|
||||||
|
|
||||||
|
```
|
||||||
|
epiphany/
|
||||||
|
├── crates/
|
||||||
|
│ ├── epiphany-determinism/ # unchanged
|
||||||
|
│ ├── epiphany-core/ # grows under H (+ spelling, + decomposition)
|
||||||
|
│ ├── epiphany-ops/ # grows under K (payloads replace projections)
|
||||||
|
│ ├── epiphany-bundle/ # grows under J (codec replacements)
|
||||||
|
│ ├── epiphany-layout-ir/ # mostly stable; minor changes for I
|
||||||
|
│ ├── epiphany-engrave/ # NEW (I): real constraint solver
|
||||||
|
│ ├── epiphany-render-svg/ # NEW (I): SVG renderer behind RenderIR
|
||||||
|
│ └── epiphany-testkit/ # grows under F (more harnesses, benches)
|
||||||
|
└── spec/
|
||||||
|
├── core_spec.{tex,pdf} # Pass 11 revision (G)
|
||||||
|
├── operation_catalog.{tex,pdf} # NEW (K)
|
||||||
|
└── binary_format.{tex,pdf} # NEW (J)
|
||||||
|
```
|
||||||
|
|
||||||
|
Two new implementation crates (`epiphany-engrave`, `epiphany-render-svg`) and two new companion specs. The `engrave` crate is separate from `layout-ir` deliberately: the v0 quickstart established that `layout-ir` is the *interface* layer (the contract between graph and renderer), and the spec's core/product boundary puts the actual constraint-solving on the product side. Replacing the `StubSolver` *inside* `layout-ir` would blur that boundary; a new crate keeps it clean. Likewise `render-svg` is one renderer behind the existing `RenderIR` interface; other backends (PDF, MusicXML round-trip, MIDI) can follow the same pattern without disturbing it.
|
||||||
|
|
||||||
|
## Agent assignments
|
||||||
|
|
||||||
|
Six agents in active roles. Sequenced by dependency below; see Sequencing for parallelism.
|
||||||
|
|
||||||
|
### Agent G — Pass 11 Ratification
|
||||||
|
|
||||||
|
Owns: `spec/PASS11_WORKLIST.md` and the resulting spec revision.
|
||||||
|
|
||||||
|
Already running. The worklist is 25 items in three buckets (8 adopt-and-pin, 6 decide-then-pin, 5 fix, 6 no-spec-change). Bucket 1 in id-dependency order first, because `TypedObjectId` discriminants transitively gate most other byte layouts. Bucket 3 spec fixes early (blob hashing contradiction is a real spec bug). Bucket 2 judgment calls last.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
- A Pass 11 revision of `spec/core_spec.pdf` (annotated in the revision history table the same way Passes 1–10 are).
|
||||||
|
- A **single byte-convention table** consolidated from Bucket 1 — every ratified discriminant, derivation preimage, and encoding layout in one place, in a format J can import directly into the Binary Format companion. Living in an appendix of the core spec, referenced by section in the companion. Avoids three reference points becoming four.
|
||||||
|
- A **ratification log** with one line per worklist item, classifying each as:
|
||||||
|
- *adopted as-is* (code's choice ratified verbatim),
|
||||||
|
- *modified before ratification* (code's choice changed; specify what and why),
|
||||||
|
- *deferred to companion* (handled in Operation Catalog or Binary Format),
|
||||||
|
- *deferred to Pass 12* (not ready for ratification this round),
|
||||||
|
- *rejected* (decided against the implementation's call; specify alternative).
|
||||||
|
|
||||||
|
This makes later archaeology — "why does the spec say X?" — straightforward.
|
||||||
|
- Annotations on the golden-bytes tests in the affected crates citing the ratified spec section.
|
||||||
|
|
||||||
|
**Boundary discipline:** Pass 11 ratifies what exists, fixes what contradicts, and blesses the convention baseline that Track B's companions will inherit. It does **not** write the Binary Format companion or the Operation Catalog (those are J and K). If the worklist tempts you to write companion text, that's scope creep — stop and bring it back to the agent topology.
|
||||||
|
|
||||||
|
Coordinates with H on item 2.1 (tempo-linear semantic call) since H may have a preference based on the spelling algorithm's needs; with K on items 1.2, 1.3, 1.4 (the derivations K's reducer consumes); with J on item 1.8 (the convention baseline J inherits, and the byte-convention table J imports); with I on item 2.6 (the layout-object id derivation; this one defers to I if it's not blocking).
|
||||||
|
|
||||||
|
Hand off when: spec rebuilds clean, byte-convention table is complete and self-contained, ratification log complete with dispositions, golden tests annotated. Pass 11 is timeboxed — target 2–4 weeks. If it's running longer, the bucketing was wrong; renegotiate scope rather than letting it drag.
|
||||||
|
|
||||||
|
### Agent H — Spelling + Decomposition pre-passes
|
||||||
|
|
||||||
|
Depends on G's Bucket 2 item 2.1 (tempo-linear semantic) and the Pass 11 ratification of system-derived pitch id (Bucket 1 item 1.3). Lives in `epiphany-core`.
|
||||||
|
|
||||||
|
Owns: the spelling pre-pass (currently `SpellingAlgorithmId("default")` returning a trivial spelling) and the notational decomposition pre-pass (the data model and invariants exist; the algorithm doesn't).
|
||||||
|
|
||||||
|
#### Canonical model: derived annotations, not stored objects
|
||||||
|
|
||||||
|
Pre-pass outputs are **canonical derived annotations**: deterministic functions of `(materialized score graph, profile, SpellingAlgorithmId, DecompositionAlgorithmId)`, recomputed on materialization. They are *not* author-minted graph objects with operation envelopes. Three consequences fall out of this choice:
|
||||||
|
|
||||||
|
- **Manual overrides layer above generated output via ordinary operations.** A `RespellPitch` operation produces a canonical user spelling that takes precedence over the algorithm's default. The default is derived; the override is authored. This is why `RespellPitch` exists as a distinct operation — H formalizes the precedence rule, not the model.
|
||||||
|
- **Algorithm version is part of the derivation key.** `SpellingAlgorithmId::Default` and `DecompositionAlgorithmId::Default` are versioned. A profile-declared change to either invalidates derived annotations for all replicas observing the new profile, deterministically. No migration of stored state is needed because annotations are not stored canonical state.
|
||||||
|
- **Caching is permitted; canonical identity is not.** Implementations may cache derived annotations in acceleration snapshots or non-canonical chunks; the cache must invalidate when the derivation key changes. Two replicas at the same `(graph, profile, algorithm version)` produce byte-identical annotations whether or not either cached.
|
||||||
|
|
||||||
|
This model is what makes the pre-passes safe to add to the architecture without reopening Chapter 6. The reducer doesn't run H's algorithms; materialization does, deterministically, after reduction completes. The algorithms are pure functions over a fully-reduced score.
|
||||||
|
|
||||||
|
#### Spelling
|
||||||
|
|
||||||
|
Implement a real algorithm. Recommend a Temperley-style preference-rule system over `cmn-12` scale positions, because it's the best-documented choice with the cleanest constraint formulation. Longuet-Higgins line-of-fifths is the other option in the spec; pick one and have G ratify the choice as `SpellingAlgorithmId::Default` v1 in Pass 11 (or Pass 12 if the call slips).
|
||||||
|
|
||||||
|
Scope discipline for the Phase 2 implementation:
|
||||||
|
- target `cmn-12` only;
|
||||||
|
- monophonic and basic polyphonic cases;
|
||||||
|
- key-signature/context-aware spelling;
|
||||||
|
- deterministic output;
|
||||||
|
- manual `RespellPitch` overrides take precedence over generated spellings (precedence rule formalized in the spec via the existing `SpellingPrecedence` machinery).
|
||||||
|
|
||||||
|
Do **not** require the first implementation to solve every chromatic tonal edge case beautifully. The Phase 2 bar is *canonical and plausible*, not *musicologically perfect*. Hard cases (modulating sequences, chromatic mediants, enharmonic respellings under pitch-class-set analysis) can be Pass-12 candidates.
|
||||||
|
|
||||||
|
#### Decomposition
|
||||||
|
|
||||||
|
Split sounding durations into notehead values + augmentation dots + ties. Phase 2 scope, in priority order:
|
||||||
|
- metric regions (proportional and aleatoric defer);
|
||||||
|
- non-tuplet durations first;
|
||||||
|
- tuplets second;
|
||||||
|
- barline crossing;
|
||||||
|
- ties across the above;
|
||||||
|
- simple augmentation dots (double-dotted and beyond may defer).
|
||||||
|
|
||||||
|
The algorithm is mostly mechanical (largest-power-of-two-that-fits with dot extensions, recursing across barlines and tuplet boundaries), but the edge cases — syncopated patterns, ties across tempo changes, tuplet nesting — are where the difficulty lives.
|
||||||
|
|
||||||
|
#### What H produces, by event kind
|
||||||
|
|
||||||
|
The acceptance criterion needs to respect the actual taxonomy, not blanket every event:
|
||||||
|
|
||||||
|
- every `IdentifiedPitch` inside every pitched event gets a resolved `PitchSpelling`, *unless* its pitch space declares spelling unavailable;
|
||||||
|
- every metric-region event with a determinate musical duration gets a `Decomposition`;
|
||||||
|
- rests get decomposition but no pitch spelling;
|
||||||
|
- unpitched events get percussion/staff-position spelling per their pitch space's rules, not pitch spelling;
|
||||||
|
- trajectory, graphic, and indeterminate events: no decomposition or a region-specific decomposition per the spec;
|
||||||
|
- proportional and aleatoric regions: explicitly deferred unless the algorithm version claims support, in which case H states the bound.
|
||||||
|
|
||||||
|
The harness must classify and count these cases; "every event has a spelling" was wrong shorthand on my part.
|
||||||
|
|
||||||
|
#### What H does not do
|
||||||
|
|
||||||
|
Does not implement the Chapter 4 tuning catalog. Spelling works in scale-position terms, not frequency terms. The catalog is genuinely separate work and stays deferred. If spelling needs *anything* tuning-related, write to the existing `PitchSpaceId` / `TuningSystemId` interfaces and let the catalog's eventual implementation honor them.
|
||||||
|
|
||||||
|
Hand off when: F's representative score corpus exercises the kind-by-kind eligibility taxonomy with documented counts per kind; every *eligible* `IdentifiedPitch` carries a non-trivial `PitchSpelling`; every *eligible* determinate metric duration carries a `Decomposition`; ineligible cases are explicitly classified and counted in the harness output (not silently absent); both pre-passes are deterministic across runs given the same `(graph, profile, algorithm version)`; manual `RespellPitch` overrides take precedence over generated output; criterion 5's reducer-determinism gate continues to pass with non-trivial pre-pass outputs in the materialization pipeline.
|
||||||
|
|
||||||
|
Spec sections: Chapter 2 (pitch + spelling), Chapter 3 §"Decomposition." Cross-checks: §6.5 re-anchoring (spelling changes via `RespellPitch` must not break re-anchoring); the spec's open question on automatic spelling under aleatoric regions (defer to Pass 12 if H finds the algorithm doesn't generalize cleanly).
|
||||||
|
|
||||||
|
### Agent I — Visible Engraving (Solver + Renderer)
|
||||||
|
|
||||||
|
Depends on G's Bucket 1 items 1.7 and 1.8 (the codec conventions, because the resolved layout's canonical bytes inherit them) and on H's spelling output (because notes don't render without spellings). Lives in two new crates: `epiphany-engrave` (the real constraint solver) and `epiphany-render-svg` (the SVG output backend).
|
||||||
|
|
||||||
|
Owns: turning a `ConstrainedLayoutIR` into a `ResolvedLayoutIR` with real geometry (positions, spacing, beam angles, accidental placement) and then into SVG that visually resembles standard music notation. This is the visible-slice deliverable: from a `Score` graph, produce an image a musician would recognize.
|
||||||
|
|
||||||
|
#### `epiphany-engrave`
|
||||||
|
|
||||||
|
Implements `LayoutSolver` returning `SolverTier::Minimal` (the lowest real conformance tier — not `Stub`). Must satisfy every hard constraint emitted in the `ConstrainedLayoutIR`; should honor quality metrics enough to produce readable output (consistent spacing, sensible beam grouping, no egregious whitespace). Quality is not yet `Standard` tier — that needs the full Quality Metric Catalog (a Phase 3 companion). `Minimal` means: hard constraints satisfied, no claim about optimality.
|
||||||
|
|
||||||
|
Recommended approach: a two-pass spring layout (horizontal then vertical), with the constraint graph derived from the existing `ConstrainedLayoutIR`. Don't attempt a global optimization solver in v0; cast-off and line-breaking are separately hard problems. Start with single-line layouts and add line breaks later.
|
||||||
|
|
||||||
|
**Constraint-validation rule (matters for the harness):** validation runs against the *declared hard constraints in the IR*, not against generic geometric heuristics. Some notehead arrangements are intentionally close or horizontally displaced inside chords; the relevant rule is "no constraint declared by `ConstrainedLayoutIR` is violated," not "no bounding boxes touch." F's harness can add class-specific collision rules on top (accidental-vs-notehead, stem-vs-beam, staff-line-vs-glyph) but the agent's responsibility is constraint satisfaction, not collision intuition.
|
||||||
|
|
||||||
|
#### `epiphany-render-svg`
|
||||||
|
|
||||||
|
Takes a `ResolvedLayoutIR` and emits SVG. Uses the SMuFL glyphs from the bundled Bravura metrics (the catalog is already wired in v0).
|
||||||
|
|
||||||
|
**The non-overreach rule:** the renderer must not make *engraving-semantic* decisions. It will necessarily make rendering decisions (SVG grouping, path vs. text encoding, transforms, viewBox, layering, style representation, font fallback strategy) — those are SVG-encoding choices, not engraving choices. The line:
|
||||||
|
|
||||||
|
- The renderer **may** choose SVG encoding details: how to group elements, when to use `<path>` vs `<text>`, transform decomposition, namespace handling, style placement, viewBox bounds, layer ordering for stacking.
|
||||||
|
- The renderer **must not** choose: stem direction, spacing, beam slope, accidental placement, semantic glyph selection (e.g., which notehead shape for a duration), articulation positioning, clef choice.
|
||||||
|
|
||||||
|
Every rendered SVG element must trace to a `RenderIR`/`ResolvedLayoutIR` object or a declared renderer wrapper (e.g., an `<svg>` root, a `<defs>` block, a layer `<g>` grouping elements from the same IR layer). If the renderer finds itself making an engraving decision, that's a layout-IR bug — surface it via a diagnostic, don't paper over it.
|
||||||
|
|
||||||
|
**Font availability — decide deliberately.** SVG that references SMuFL glyphs only works if the viewer has the font. Three options:
|
||||||
|
1. Embed Bravura via `@font-face` with the font payload base64-encoded in the SVG.
|
||||||
|
2. Convert all glyph references to inline `<path>` outlines (no font dependency in the viewer).
|
||||||
|
3. Require local Bravura installation and document it.
|
||||||
|
|
||||||
|
Recommendation: **inline path outlines for golden fixtures and the demonstrable deliverable**, embedded font as an optional rendering mode. Path outlines maximize portability and make the SVG self-contained (it renders in any browser, in any image-processing tool, in print pipelines), at the cost of larger file size. The font-embedded mode is a configuration option that ships but isn't the default.
|
||||||
|
|
||||||
|
#### Demo binary discipline
|
||||||
|
|
||||||
|
The visible slice deliverable is a library. But you almost certainly want a small `examples/render_fixture.rs` (or similar) in `epiphany-render-svg` that takes a fixture name on the command line and emits SVG to stdout. This is not an application; it's a demo harness. It will be invaluable for showing the work, for visual regression review, and for triage. Ship it.
|
||||||
|
|
||||||
|
#### Development pattern
|
||||||
|
|
||||||
|
Develop the renderer against stub-solver output first (so it's testable before the solver lands), then switch to the real solver. Keep the renderer working against *both* the stub and the real solver — that lets you bisect "is this a renderer bug or a solver bug" with a one-line change. The demo binary's CLI can take a `--solver=stub|real` flag.
|
||||||
|
|
||||||
|
#### What I doesn't do
|
||||||
|
|
||||||
|
Does not implement the Chapter 9 Quality Metric Catalog. The solver reports `SolverTier::Minimal`, which means it satisfies hard constraints but makes no normalized-metric claims. Quality metrics are Phase 3.
|
||||||
|
|
||||||
|
Hand off when: F's `ten_measure_single_staff` and `valid_score_rich` fixtures both render to SVG that a music reader recognizes as standard notation (human review gate, performed by you personally — see Acceptance criteria); the machine-readable acceptance snapshot for each fixture (object count, glyph count, bounding-box classes, provenance count, hard-constraint count, XML validity) is golden-locked; resolved layouts satisfy every declared hard constraint plus F's class-specific collision rules; criterion 6's layout round-trip continues to pass with the real solver replacing the stub; renderer output is well-formed SVG (XML-validates); the demo binary works end-to-end from a fixture name.
|
||||||
|
|
||||||
|
Spec sections: Chapter 7 (Layout IR) — already implemented at the interface level; Chapter 9 §"Solver Interface" — implement the `Minimal` tier; Chapter 7 §"Glyph Catalog" for the SMuFL integration.
|
||||||
|
|
||||||
|
### Agent K — Operation Catalog + Real Payloads
|
||||||
|
|
||||||
|
Depends on G's Bucket 1 items 1.1, 1.2, 1.4, 1.7, 1.8 (the byte conventions K's payloads encode under). Lives in `epiphany-ops` (real payloads replacing identifier projections) and `spec/operation_catalog.{tex,pdf}` (the new companion spec).
|
||||||
|
|
||||||
|
Owns: the Operation Catalog companion specification *and* the corresponding shift in `epiphany-ops` from identifier-only payload projections (today's prototype) to value-typed payloads. The catalog is the schema; the ops crate consumes it.
|
||||||
|
|
||||||
|
The current state (P11-C1): `InsertEventOp` carries `event: EventId` plus reduction-relevant scalars, not the full `Event`. `RespellPitchOp` carries the pitch id and a `ContentHash` fingerprint of the new spelling, not the spelling itself. This was the right call for v0 (it made envelopes hashable without a value-codec dependency) but it's not durable: any operation re-played in a fresh context (a backup restore, a cross-tool round-trip) needs the full value.
|
||||||
|
|
||||||
|
#### K0 / K1 split
|
||||||
|
|
||||||
|
Writing all 60–80 primitives as a Phase 2 close condition would turn Phase 2 into a catalog-writing marathon. Split deliberately:
|
||||||
|
|
||||||
|
**K0 — Minimum portable catalog (required for Phase 2 close):**
|
||||||
|
- create score / canvas / region / staff / staff instance / voice;
|
||||||
|
- insert / delete / modify event;
|
||||||
|
- insert / delete / modify identified pitch;
|
||||||
|
- respell pitch;
|
||||||
|
- set metadata (title, composer, lyricist, copyright);
|
||||||
|
- set metric grid / time signature / tempo segment;
|
||||||
|
- create / delete / update tie / slur / beam / spanner;
|
||||||
|
- set layout / system break advisory;
|
||||||
|
- resolve conflict;
|
||||||
|
- undo transaction descriptor payload.
|
||||||
|
|
||||||
|
These are the primitives the visible slice and the binary format actually need. K0's schemas must be complete and implemented in `epiphany-ops`.
|
||||||
|
|
||||||
|
**K1 — Full catalog expansion (drafted in Phase 2, completed in Phase 3):**
|
||||||
|
Everything else from the spec's 60–80 estimate. K1's *framework* must exist in Phase 2: schema template, undo-rule template, conflict-case template, re-anchoring-behavior template. Adding a K1 primitive in Phase 3 should be schema-fill, not design. But the K1 primitives themselves need only be drafted (one-paragraph descriptions and slot-in-the-framework) in Phase 2, not fully specified.
|
||||||
|
|
||||||
|
This isn't about cutting scope arbitrarily; it's about staging. The full catalog is genuinely Phase 3-sized work that doesn't gate Phase 2's deliverables.
|
||||||
|
|
||||||
|
#### Per-primitive schema content (K0)
|
||||||
|
|
||||||
|
For each K0 primitive, define:
|
||||||
|
- The complete payload schema (what value-typed fields it carries).
|
||||||
|
- The canonical byte encoding (consuming Pass 11's convention baseline from item 1.8).
|
||||||
|
- The reduction rule (the existing `epiphany-ops` reducer logic ratified against the schema).
|
||||||
|
- The conflict cases and how the rule resolves each.
|
||||||
|
- The undo semantics under `StrictInverse` / `BestEffort` / `Cascade`. K0 primitives must have undo semantics specified and implemented; K1 primitives need undo only drafted.
|
||||||
|
- The re-anchoring behavior on tombstoned referents.
|
||||||
|
|
||||||
|
Pass-11 item 2.5's `ResolveConflict::Dismiss` action lands here in the `ResolveConflict` primitive's schema.
|
||||||
|
|
||||||
|
#### v0 → v1 payload migration
|
||||||
|
|
||||||
|
The backward-compatibility requirement is correct but needs a mechanism. v0 envelopes carry identifier-only payloads; v1 envelopes carry value-typed payloads. Two options:
|
||||||
|
|
||||||
|
1. *Parallel variants forever.* `OperationPayload::V0Projection(...)` and `OperationPayload::V1ValueTyped(...)` coexist permanently. Reducer handles both. Pro: no migration. Con: doubles reducer surface area forever; v0's identifier-only shape becomes a permanent dialect.
|
||||||
|
2. *One-time migration.* K ships a migration function that converts v0 envelopes to v1-shaped envelopes using the score graph as context to reconstruct value payloads. The migration runs once on read; v0 envelopes are absent from production code afterward.
|
||||||
|
|
||||||
|
Adopt option 2. v0 envelopes live only in the test corpus as a regression guard (proving the migration is correct). Production code carries only v1 payloads.
|
||||||
|
|
||||||
|
Migration mechanism:
|
||||||
|
```rust
|
||||||
|
// In epiphany-ops, applied on cold open of a v0 bundle
|
||||||
|
fn migrate_v0_envelope(
|
||||||
|
v0: V0OperationEnvelope,
|
||||||
|
context: &Score, // the base or partially-materialized graph
|
||||||
|
) -> Result<OperationEnvelope, MigrationError>;
|
||||||
|
```
|
||||||
|
|
||||||
|
The migration must be *deterministic* (two implementations migrating the same v0 envelope against the same context produce byte-identical v1 envelopes) and *equivalence-preserving* (a v0 envelope and its v1 migration reduce to identical canonical state). The criterion-1 convergence harness in F's testkit validates this: it runs the v0 corpus through migration and asserts byte-identical reduction outcome.
|
||||||
|
|
||||||
|
If migration is impossible for some v0 envelope (the context lacks information to reconstruct the value), K declares the v0 envelope incompatible and the bundle opens read-only. Document the cases where this happens; ideally there are none, but Phase 2's actual v0 corpus will tell.
|
||||||
|
|
||||||
|
#### Boundary discipline
|
||||||
|
|
||||||
|
The catalog defines schemas and reduction rules. It does **not** redefine the architecture of `epiphany-ops` — the reducer, the `OperationSlot`, the canonical reduction order, the equivocation handling, the integrity-anomaly model are all v0 deliverables that stay frozen. K consumes them by giving them real payloads to operate on, not by rewriting them.
|
||||||
|
|
||||||
|
#### Coordination with J
|
||||||
|
|
||||||
|
K's payload schemas are J's input for the operation-payload sections of the Binary Format companion. They design jointly (joint discussions, joint reviews) but K0's schemas land before J implements those sections. J's other surface area (identifiers, scalars, manifest, header, chunk preludes, schema-evolution rules, content-hash preimages, canonical container encodings) is K-independent and J starts on those immediately after G's Bucket 1.
|
||||||
|
|
||||||
|
Hand off when: every K0 primitive has a complete schema; `epiphany-ops` payload types match K0's schemas; v0 envelope corpus migrates to v1 deterministically and the migration is equivalence-preserving (F harness validates); v0 envelopes reduce to byte-identical canonical state through the migration path; criterion 5 continues to pass at 10K-envelope scale; F's performance bench passes the documented budget for that scale; K1 catalog framework exists with placeholder entries for each Phase-3 primitive.
|
||||||
|
|
||||||
|
Spec sections: Chapter 6 (existing), Chapter 5 (the typed-object family the payloads reference), the new Operation Catalog companion.
|
||||||
|
|
||||||
|
### Agent J — Binary Format Companion + Codec Replacement
|
||||||
|
|
||||||
|
Depends on G's Bucket 1 items 1.5–1.8 (the discriminant tables and convention baseline J formalizes). **Most of J's surface area does not depend on K** — start immediately after G's Bucket 1 lands. Only the operation-payload sections of the companion wait for K0's schemas. Lives in `spec/binary_format.{tex,pdf}` (the new companion) and across `epiphany-core`, `epiphany-ops`, and `epiphany-bundle` (codec replacements).
|
||||||
|
|
||||||
|
Owns: the Binary Format companion specification *and* the corresponding replacement of the three crates' prototype codecs with companion-conforming implementations. The companion is the schema; the three crates consume it.
|
||||||
|
|
||||||
|
The current state (P11-4, P11-D2, P11-C provisional): each of `epiphany-core`, `epiphany-ops`, `epiphany-bundle` has its own codec module (`codec.rs`, `encode.rs`/`decode.rs`, `manifest.rs`'s encoders) that produces and consumes canonical bytes for its types. These codecs share conventions (little-endian, u32 length prefixes, single-byte discriminants, etc.) but each was independently authored, and the conventions are documented in DECISIONS files rather than a single normative source.
|
||||||
|
|
||||||
|
#### What J can start on immediately (K-independent)
|
||||||
|
|
||||||
|
- Identifier encodings (all the typed-id family ratified by Pass 11 item 1.1).
|
||||||
|
- Primitive value encodings: `RationalTime`, scalar wall-clock, `QuantizedCoord`, `ContentHash`, `ChunkId`, string encoding (NFC rules), boolean, integer endianness.
|
||||||
|
- Composite graph value encodings: `Event`, `Voice`, `Region`, `StaffInstance`, `Pitch`, `IdentifiedPitch`, `PitchSpelling` and (once H lands) `Decomposition`. These are core's value types, not ops' payload types.
|
||||||
|
- Manifest encoding (header / superblock / chunk preludes / manifest body).
|
||||||
|
- Schema evolution rules: how minor versions add fields, how major versions are gated, how unknown fields are handled.
|
||||||
|
- Canonical container encodings: `CanonicalMap`, `CanonicalSet`, `CanonicalVec`. The ordering rules already exist in `epiphany-determinism`; J formalizes the wire layout.
|
||||||
|
- Content-hash preimage rules (mostly in Chapter 8 §"Domain-Separated Preimages" already; J makes it complete and unambiguous).
|
||||||
|
|
||||||
|
This is the bulk of the companion. Get it ratified while K is still drafting K0 schemas.
|
||||||
|
|
||||||
|
#### What J waits for K on
|
||||||
|
|
||||||
|
- Operation payload encoding for K0 primitives — J encodes them after K0's schemas exist.
|
||||||
|
- Operation payload encoding for K1 — drafted in Phase 2, completed in Phase 3 alongside K1.
|
||||||
|
|
||||||
|
These are the only sections gated on K.
|
||||||
|
|
||||||
|
#### Codec replacement
|
||||||
|
|
||||||
|
- Replace each crate's bespoke encoder/decoder with implementations that read from a single shared spec.
|
||||||
|
- Preserve byte-for-byte compatibility with v0's outputs *for the conventions Pass 11 ratifies* — this is the cleanest way to validate the companion: every existing golden test must continue to pass.
|
||||||
|
- Migrate any encoding that Pass 11 decided to *change* in the same commit as updating the goldens; document each migration in the companion's revision history.
|
||||||
|
|
||||||
|
#### Required harnesses (J-specific, beyond F's standard ones)
|
||||||
|
|
||||||
|
The v0 testkit could not test these because everything was one implementation talking to itself. J's work crosses an implementation boundary (the spec is now the contract, not the code), so the harness surface broadens:
|
||||||
|
|
||||||
|
- **Cross-implementation decoder test.** An isolated decoder, implemented from the companion spec text *without referencing the codec code*, reads the encoder's output and reproduces input. This is the test that proves the spec is self-sufficient. Live in F's testkit, written by F based on the companion text.
|
||||||
|
|
||||||
|
- **Wire-format fuzzer (early).** Random valid values round-trip; random invalid bytes fail cleanly with typed errors; no panics; malformed lengths/discriminants handled safely; bounded memory consumption on adversarial input. Adopt `cargo-fuzz` or equivalent. Run the fuzzer in CI's nightly soak. This is a cheap, high-value addition that catches the kinds of format bugs that don't surface in property tests.
|
||||||
|
|
||||||
|
- **Canonicalization tests.** Map iteration ordering matches Pass 11's canonical-container rules; UTF-8 NFC enforced at boundaries (not silently accepted as raw bytes); float `-0.0` normalization to `+0.0`; rational always reduced (no `2/4` round-tripping as `2/4`); unknown fields under minor schema evolution preserved opaquely; bytes outside the canonical alphabet rejected.
|
||||||
|
|
||||||
|
#### Boundary discipline
|
||||||
|
|
||||||
|
J writes the canonical wire format. J does **not** invent new payload schemas (those are K's) or new graph types (those are core's), and does **not** redesign the bundle's structural layer (the fixed header, superblocks, chunk graph, manifest are v0 deliverables). J's job is to formalize how the existing types serialize.
|
||||||
|
|
||||||
|
Hand off when: the Binary Format companion exists as a versioned document covering all K-independent sections plus all K0 operation payloads; the three crates' codec modules cite the companion as their normative source; no codec module contains private byte-layout decisions — every layout is in the companion; criterion 4 (canonical serialization stability) continues to pass byte-for-byte on the v0 corpus, the Pass 11 corpus, and the K0 envelope corpus; the cross-implementation decoder test passes; the wire-format fuzzer runs in CI without panics on 1M iterations.
|
||||||
|
|
||||||
|
Spec sections: Appendix D §"Canonical Serialization," Chapter 8 §"Domain-Separated Preimages," and the new Binary Format companion as a whole.
|
||||||
|
|
||||||
|
### Agent F — Testkit & Tripwire (continues, mandate broadens)
|
||||||
|
|
||||||
|
Continues v0 ownership and adds:
|
||||||
|
|
||||||
|
- **Per-agent harnesses.** Each new agent (H, I, K, J) gets a harness that's their merge gate. Same rule as v0. Design each harness for its agent's specific failure modes:
|
||||||
|
- H: spelling and decomposition stability across runs at the kind-by-kind eligibility taxonomy; manual `RespellPitch` precedence over generated output; non-vacuity (the pre-passes actually change the score in expected ways).
|
||||||
|
- I: hard-constraint validation on resolved layouts (against declared constraints, not generic bounding-box rules) plus class-specific collision rules (accidental-vs-notehead, stem-vs-beam, staff-line-vs-glyph); provenance survival through solver and renderer; SVG well-formedness (XML-validates); machine-readable acceptance snapshot per fixture (object count, glyph count, bounding-box classes, provenance count, hard-constraint count, XML validity) golden-locked.
|
||||||
|
- K: deterministic and equivalence-preserving v0→v1 migration (v0 envelopes migrated to v1 reduce to byte-identical canonical state as v0 envelopes did); payload-schema completeness (every K0 catalog primitive has a payload type in `epiphany-ops`); K1 framework present.
|
||||||
|
- J: cross-implementation decoder test (isolated decoder reads encoder output and reproduces input); criterion 4 continues to pass byte-for-byte; wire-format fuzzer (1M iterations no panics, no unbounded memory); canonicalization tests (map ordering, NFC, `-0.0` normalization, rational normalization, minor-schema-evolution unknown-field preservation).
|
||||||
|
|
||||||
|
- **End-to-end integration harness.** This is the harness that proves the two tracks actually compose. A single fixture run:
|
||||||
|
```
|
||||||
|
Score fixture
|
||||||
|
→ canonical reduction
|
||||||
|
→ H pre-passes (spelling + decomposition)
|
||||||
|
→ layout IR
|
||||||
|
→ I solver (real, not stub)
|
||||||
|
→ SVG render
|
||||||
|
→ J bundle write
|
||||||
|
→ J bundle read
|
||||||
|
→ canonical reduction again
|
||||||
|
→ H pre-passes again
|
||||||
|
→ layout again
|
||||||
|
→ SVG render again
|
||||||
|
```
|
||||||
|
Expected: canonical state and SVG are byte-identical between the first and second pass, modulo explicitly allowed non-canonical caches. Without this harness, H and I and J and K can all be green while the composed result silently breaks at the seams. F owns this harness; it lands before either track declares done.
|
||||||
|
|
||||||
|
- **Performance benchmark suite with documented thresholds.** Phase 2 creates the first scores with realistic event counts (Track A's visible-slice work). The v0 reducer's `canonical_reduction_order` is `O(n²)` in its indegree construction — fine for criterion 5's 1000 envelopes (~1M coverage checks), painful at 10K+. F adds a `benches/` directory (using `criterion`) that asserts performance budgets from Chapter 10 of the spec. **Set the threshold first, in the bench itself**, as an expected-failing (`#[xfail]`-equivalent) gate for known scale points. When the bench fails at a documented score size, the responsible agent (likely K, working in `epiphany-ops`) fixes it with a regression test. F surfaces; doesn't fix.
|
||||||
|
|
||||||
|
- **Pass 12 batch tracker.** Same batching rule from v0: ambiguities discovered during Phase 2 implementation go into a Pass 12 batch, not into code improvisations. F maintains the tracking list. Don't open Pass 12 until at least 3 items accumulate.
|
||||||
|
|
||||||
|
- **CI broadening.** The existing CI (fmt, clippy -D warnings, workspace tests, doc tests, conformance suite, nightly soak) gets per-agent jobs added as their harnesses land, plus the integration harness, plus the performance bench job, plus the wire-format fuzzer in the nightly soak. The conformance suite stays single-job to keep it the architecture's headline tripwire.
|
||||||
|
|
||||||
|
What F **doesn't** do: write any of the new crates' production code. F's role is to gate quality, not to implement features. If a harness reveals a problem, F files it against the responsible agent; F doesn't fix it.
|
||||||
|
|
||||||
|
Hand off (per agent's merge): F's harness for that agent runs in CI and passes; the harness asserts the agent's stated acceptance criterion (see "Acceptance criteria per agent" below); the harness has a non-vacuity guard (it would fail if the agent's work were stubbed or vacuous). For Phase 2 close: the integration harness runs end-to-end clean on the full fixture corpus.
|
||||||
|
|
||||||
|
## Sequencing
|
||||||
|
|
||||||
|
```
|
||||||
|
Week 0 (now):
|
||||||
|
G starts Pass 11 (Bucket 1 first, ~2–3 weeks for the byte items).
|
||||||
|
F broadens mandate; drafts per-agent harness templates;
|
||||||
|
sketches the integration harness skeleton against stub stages.
|
||||||
|
H designs algorithm and fixtures.
|
||||||
|
I starts renderer against stub-solver RenderIR immediately.
|
||||||
|
K starts K0 catalog schema design.
|
||||||
|
J starts reading; cannot begin codec implementation until G's Bucket 1.
|
||||||
|
|
||||||
|
Week ~3 (G's Bucket 1 lands):
|
||||||
|
H starts implementation (algorithm-choice ratification from G's Bucket 2).
|
||||||
|
I starts engrave-solver implementation (renderer already in progress).
|
||||||
|
K starts K0 payload implementation.
|
||||||
|
J starts codec replacement for K-independent surface:
|
||||||
|
identifiers, scalars, composites, manifest, headers, schema evolution,
|
||||||
|
canonical container encodings, content-hash preimages. This is the
|
||||||
|
majority of J's work and it begins now.
|
||||||
|
|
||||||
|
Week ~6 (G done):
|
||||||
|
Pass 11 closed; byte-convention table delivered; all goldens annotated.
|
||||||
|
H, I, K running in parallel.
|
||||||
|
J continues K-independent codec work in parallel.
|
||||||
|
F's per-agent harnesses live in CI; integration harness skeleton wired
|
||||||
|
with real H+I+K stages as they land.
|
||||||
|
|
||||||
|
Week ~10:
|
||||||
|
K0 catalog draft circulates.
|
||||||
|
J starts operation-payload sections of the companion, paired with K.
|
||||||
|
H lands (spelling + decomposition complete and harnessed).
|
||||||
|
I's solver lands; I's renderer can now use real solver output.
|
||||||
|
Integration harness exercises real H + real I + stub-K + stub-J path.
|
||||||
|
|
||||||
|
Week ~12:
|
||||||
|
K0 lands. Operation Catalog companion ratified for K0 primitives;
|
||||||
|
K1 framework drafted.
|
||||||
|
I lands (visible slice demonstrable internally).
|
||||||
|
J's K-independent codec work complete; only operation-payload encoding
|
||||||
|
remains.
|
||||||
|
Integration harness exercises full real H + real I + real K + stub-J.
|
||||||
|
|
||||||
|
Week ~15:
|
||||||
|
J lands. Binary Format companion ratified for K0 surface.
|
||||||
|
Integration harness fully real end-to-end.
|
||||||
|
Phase 2 closed; foundation is durable + perceivable + ratified.
|
||||||
|
|
||||||
|
Week ~17:
|
||||||
|
Pass 12 batch (if accumulated). Phase 3 planning.
|
||||||
|
```
|
||||||
|
|
||||||
|
This is calendar pacing assuming roughly one engineer per agent and that you're not running into the kinds of cross-track contention I'm not seeing from here. Adjust accordingly.
|
||||||
|
|
||||||
|
The critical path is G → (K0 schemas) → J operation-payload encoding. Track A (H, I) is parallelizable with both, and most of J's work (the K-independent surface) is parallelizable with K. The earlier sequencing had J fully blocked on K, which would have lost 4–6 weeks unnecessarily.
|
||||||
|
|
||||||
|
The biggest schedule risk is K0 turning out larger than the bullet list (some "K0 primitive" reveals subcategories that need their own schema work). If K0 swells, consider whether items belong in K1 instead — the principle is *what does the visible slice and binary format actually need*, not *what's spec-complete*.
|
||||||
|
|
||||||
|
## Decisions you'll need to make
|
||||||
|
|
||||||
|
Five next-phase calls, analogous to v0's five. Make each one once and document it.
|
||||||
|
|
||||||
|
1. **Spelling algorithm choice.** Temperley vs. Longuet-Higgins vs. a hybrid. H proposes; G ratifies via Pass 11 (or Pass 12). Recommendation: Temperley preference rules — best-documented, cleanest constraint formulation, easiest to test against published examples.
|
||||||
|
|
||||||
|
2. **Solver architecture for engraving.** Two-pass spring layout (recommended; matches the existing `ConstrainedLayoutIR` shape) vs. global optimization (cleaner output, much harder to make deterministic) vs. rule-based fallback (fast, brittle). Recommendation: two-pass spring. The spec's deterministic-output requirement makes global optimization expensive to validate. Document the choice in `epiphany-engrave`'s README.
|
||||||
|
|
||||||
|
3. **Renderer SVG dialect.** Pure SVG 1.1 (broadest compatibility) vs. SVG 2 (richer features) vs. SVG + CSS for styling (cleaner separation, harder to embed). Recommendation: SVG 1.1 + inline styles. Maximum portability for what's effectively a viewer artifact; CSS comes later if needed.
|
||||||
|
|
||||||
|
4. **Catalog versioning shape.** K writes the Operation Catalog companion. Recommendation: independent semver, separately versioned from `core_spec.pdf`. Operation kinds get added over time; the catalog should evolve without bumping the core spec.
|
||||||
|
|
||||||
|
5. **Binary Format companion versioning shape.** Same question for J. Recommendation: independent semver again, but tied tighter to `core_spec.pdf` than the Operation Catalog — the binary format is fundamentally about *how the spec's types serialize,* so it travels with the spec more closely. A reasonable rule: binary format major version matches core spec major version; minor versions diverge.
|
||||||
|
|
||||||
|
## Don't do these
|
||||||
|
|
||||||
|
Updated from v0 in light of Phase 2:
|
||||||
|
|
||||||
|
- **Don't reopen v0 design decisions.** The architecture is frozen — same rule as v0, restated because Phase 2 agents will be tempted to "improve" things they touch. If you have a real reason to revisit a Pass 1–10 decision, raise it as a Pass 12 candidate; do not unilaterally change it.
|
||||||
|
- **Don't implement the Chapter 4 tuning catalog.** It's deferred again. Spelling works in scale-position terms; rendering works in SMuFL glyph metrics; nothing in Phase 2 needs frequency resolution. Phase 3.
|
||||||
|
- **Don't implement quality metrics (Chapter 9 Catalog).** The solver reports `SolverTier::Minimal`; that's the right tier for the visible slice. Quality metrics need a Quality Metric Catalog companion and a Reference Suite — Phase 3.
|
||||||
|
- **Don't preemptively optimize performance.** F's benches set thresholds for known scale points up front; the fix happens when the bench fails at a documented size with a regression test attached. Don't rewrite the `O(n²)` reducer because someone is uncomfortable with it. Bench first, optimize only when the bench says so.
|
||||||
|
- **Don't implement bundle compression (zstd) yet.** The manifest must remain uncompressed (a Pass 11 ratification); other chunks could be compressed but the prototype baseline doesn't, and the visible slice doesn't need it. Phase 3.
|
||||||
|
- **Don't implement async wrappers, plugin runtime, UI, audio engine.** Same as v0; these remain explicitly out of scope.
|
||||||
|
- **Don't ship an application.** Phase 2 produces libraries plus *demo harnesses*. A `cargo run -p epiphany-render-svg --example render_fixture ten_measure_single_staff > out.svg` is a demo harness, not an application — and you almost certainly want it. It's invaluable for showing the work, for visual regression review, and for triage. Ship demo binaries; don't ship a viewer process or a GUI host.
|
||||||
|
- **Don't write K1 catalog primitives in Phase 2.** Draft them in the framework K provides; full implementation is Phase 3. Trying to ship the full 60–80 primitives is what turns Phase 2 into a marathon.
|
||||||
|
|
||||||
|
## Acceptance criteria per agent
|
||||||
|
|
||||||
|
You'll know each agent's Phase 2 work is done when these tests pass on F's harness. Each is the analog of the v0 quickstart's six criteria, scaled to its agent.
|
||||||
|
|
||||||
|
- **G — Pass 11 ratification:** spec rebuilds clean (XeLaTeX three-pass, no warnings, no undefined references); byte-convention table delivered as a self-contained spec section that J can cite verbatim; ratification log present with one line per worklist item classified as adopted-as-is / modified-before-ratification / deferred-to-companion / deferred-to-Pass-12 / rejected; all golden-bytes tests in `epiphany-core`, `epiphany-ops`, `epiphany-bundle` annotated to cite ratified spec sections; no test fails as a result of ratification.
|
||||||
|
|
||||||
|
- **H — Spelling + Decomposition:** on F's representative score corpus (>20 fixtures spanning common cases, edge cases, and torture cases), every *eligible* `IdentifiedPitch` carries a non-trivial `PitchSpelling` (eligibility per the kind-by-kind taxonomy above); every *eligible* determinate metric duration carries a `Decomposition`; ineligible cases are explicitly classified and counted in the harness output, not silently absent; the same score reduced twice produces byte-identical pre-pass annotations (deterministic-derivation property); manual `RespellPitch` overrides take precedence over generated spellings; criterion 5's reducer-determinism gate continues to pass with non-trivial pre-pass outputs in the materialization pipeline; spelling matches published Temperley/Longuet-Higgins expectations on a curated set of standard test cases (Bach chorale phrases, Beethoven motifs).
|
||||||
|
|
||||||
|
- **I — Visible engraving:** F's `ten_measure_single_staff` and `valid_score_rich` fixtures both render to SVG; **human review gate** (performed personally by the project lead — first-time-seeing-Epiphany-render-real-music is a meaningful moment and the visual bar matters): the SVG visually parses as standard music notation; **machine acceptance snapshot per fixture** (object count, glyph count, bounding-box class counts, provenance count, hard-constraint count, XML validity) golden-locked, changes require explicit golden update; resolved layouts satisfy every declared hard constraint plus F's class-specific collision rules (accidental-vs-notehead, stem-vs-beam, staff-line-vs-glyph); criterion 6's layout round-trip continues to pass with the real solver replacing the stub; renderer output is well-formed SVG (XML-validates); the demo binary works end-to-end from a fixture name on the command line.
|
||||||
|
|
||||||
|
- **K — Operation Catalog:** K0 primitives complete and implemented — every K0 primitive has a complete schema in `operation_catalog.pdf`, a matching payload type in `epiphany-ops`, complete undo semantics, and a reduction rule consistent with the v0 reducer; K1 catalog *framework* exists with one-paragraph descriptions and template slots for each Phase-3 primitive (unimplemented K1 primitives explicitly marked unavailable under the Phase 2 profile); v0→v1 migration is deterministic and equivalence-preserving (v0 envelopes migrated to v1 reduce to byte-identical canonical state as v0 envelopes did under v0 payloads); criterion 5 continues to pass at 10K-envelope scale; F's performance bench passes the documented budget for that scale.
|
||||||
|
|
||||||
|
- **J — Binary Format companion:** companion spec rebuilds clean; covers all K-independent surface plus K0 operation payloads; the three crates' codec modules each cite the companion as normative; no codec module contains private byte-layout decisions; criterion 4 (canonical serialization stability) continues to pass byte-for-byte on the v0 corpus and the Pass 11 corpus and the K0 envelope corpus; cross-implementation decoder test passes; **wire-format fuzzer runs in CI nightly soak without panics or unbounded memory consumption on 1M iterations**; canonicalization tests pass (map ordering, NFC enforcement, `-0.0` normalization, rational normalization, minor-schema-evolution unknown-field preservation).
|
||||||
|
|
||||||
|
- **F — Tripwire continues:** every agent's harness lands in CI before the agent merges; **integration harness exists and runs end-to-end clean** (Score → reduction → H → layout → I → SVG → J write → J read → reduction → H → layout → SVG, with byte-identical results between passes modulo allowed non-canonical caches); performance bench job runs in CI with documented budgets and `xfail` thresholds for known-pending scale points; Pass 12 batch tracker exists; conformance suite continues to pass on every push; nightly soak continues clean and includes J's wire-format fuzzer.
|
||||||
|
|
||||||
|
## Process notes
|
||||||
|
|
||||||
|
**The spec is the contract.** Same rule as v0. The Pass 11 revision joins the canonical contract once G lands; the Operation Catalog and Binary Format companions become part of the contract once K and J land. Reading the contract is the first step before implementing anything.
|
||||||
|
|
||||||
|
**Ambiguities go into a Pass 12 batch, not into code.** Same rule as v0. F maintains the batch. Don't open Pass 12 until at least 3 items accumulate. The discipline from v0 → Pass 11 worked; trust it for Phase 2 → Pass 12.
|
||||||
|
|
||||||
|
**Architecture is frozen.** Restating because Phase 2 will tempt revisits. The v0 architecture survived nine review passes and one full prototype build. Implementation pressure has *not* revealed a structural problem with it. If Phase 2 implementation reveals a structural problem, that's news worth treating as news — but it should clear the same review-pass bar that Passes 1–10 cleared, not slip in as a sidecar revision.
|
||||||
|
|
||||||
|
**Treat F's harness as the merge gate.** Not the reviewer's intuition. Not the test you wrote yourself. F's harness, which exists specifically to fail when something subtle has gone wrong. If F's harness is green and you have a bad feeling, write a regression test for the bad feeling and watch the harness go red; that's how the harness gets sharper. Improvising past a green harness is how the architecture gets eroded.
|
||||||
|
|
||||||
|
**The two tracks coordinate through interfaces, but don't redesign each other.** Track A (H, I) and Track B (K, J) work on largely disjoint parts of the codebase. They *do* coordinate through shared types (core value types, canonical derived annotations, algorithm IDs) and shared conventions (Pass 11's byte-convention table). If you find yourself redesigning each other's surface — H proposing payload-schema changes, J telling I how to lay out IR objects, K reshaping the layout model — that's scope creep. Coordinate through the harnesses (especially F's integration harness, which is the contract that proves the tracks compose) and through the spec, not through ad-hoc changes to each other's code.
|
||||||
|
|
||||||
|
**Phase 2 closes when all five agents land + Pass 12 batch is reviewed + the integration harness runs end-to-end clean.** Not when "everything feels done." The acceptance criteria above are the close condition; meet them, then Phase 3 planning begins.
|
||||||
Loading…
Reference in New Issue