Cross-seam review fixes: respell→pre-pass visibility, profile enforcement, canonical fingerprint, catalog reconciliation
Addresses four findings spanning the H (pre-pass) and K (reduction) seams plus the Operation Catalog. 1. [High] A reduced RespellPitch is now visible to the pre-pass. The reducer stored overrides only in MaterializedState.spellings, but Agent H's derive_annotations resolves authored spellings from score.spelling_attachments — so a real respelling accepted by reduce_onto was lost before annotation derivation, violating manual-override precedence. respell_pitch now upserts a user-chosen explicit SpellingAttachment into the materialized graph (materialize_respell / graph_respell_pitch); DeleteIdentifiedPitch drops that attachment (graph_delete_pitch) so none dangles (it does NOT tombstone the pitch — the event survives a pitch delete and a later ModifyEvent may reuse the id, which would make it both live and tombstoned). New testkit gate assert_reduced_respell_is_honored reduces a real RespellPitch and proves derive_annotations honors it as Authored(UserChosen); wired into run_all. 2. [Medium] PrePassProfile algorithm ids are now enforced, not just recorded. derive_annotations ran the default logic and labeled the result with the requested algorithm. It now runs each pre-pass only when its requested id is the implemented "default"; an unknown/future id yields no annotations for that pre-pass (the requested id stays in the result profile), so a future algorithm can no longer silently alias the default in a derivation cache. Test: unknown_algorithm_ids_are_not_honored. 3. [Medium/Low] The determinism gate now fingerprints canonical bytes, not Debug. DerivedAnnotations gains canonical_fingerprint(): embedded graph values (PitchSpelling, DecompositionAttachment, SpellingSourceKind — the latter two added to the CanonicalValue surface) use their ratified bytes; counts/ids are little-endian, length-framed. The pre-pass harness fingerprints with it. A discrimination check confirms it is not a degenerate constant. 4. [Low] operation_catalog.tex K1 chapter reconciled with the implemented M2 work: the now-dispatched ops (event/pitch leaf-field, cross-cutting CRUD, structural container CRUD) are listed as implemented-since-M2 (available under the Phase-2 profile), and the "MUST reject" scope is narrowed to the genuinely deferred slots (create score/canvas/staff, set metadata, metric-grid/time-sig/ tempo, layout/page-break). PDF rebuilt clean (0 undefined refs). Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green (criterion 1 + the pre-pass and convergence gates); conformance scale 1 passes. The unrelated Agent-I working tree is left uncommitted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a207077cd7
commit
7a94814ba3
|
|
@ -2097,6 +2097,10 @@ canonical_value! {
|
|||
Region,
|
||||
StaffInstance,
|
||||
Voice,
|
||||
// Pre-pass annotation leaves — give DerivedAnnotations a canonical byte
|
||||
// fingerprint (a normative surface, vs. its Debug form).
|
||||
DecompositionAttachment,
|
||||
SpellingSourceKind,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -144,6 +144,77 @@ pub struct DerivedAnnotations {
|
|||
pub profile: PrePassProfile,
|
||||
}
|
||||
|
||||
impl DerivedAnnotations {
|
||||
/// A canonical byte fingerprint of the derivation. Deterministic and
|
||||
/// order-independent — the maps are `BTreeMap`s in canonical key order, the
|
||||
/// embedded graph values (`PitchSpelling`, `DecompositionAttachment`,
|
||||
/// `SpellingSourceKind`) use their ratified [`CanonicalValue`] bytes, and
|
||||
/// counts/ids are little-endian, length-framed where variable. Two
|
||||
/// byte-equal fingerprints imply byte-identical derivations. This is the
|
||||
/// normative serialization surface the determinism gate compares, rather
|
||||
/// than the (non-normative) `Debug` form.
|
||||
///
|
||||
/// [`CanonicalValue`]: crate::CanonicalValue
|
||||
pub fn canonical_fingerprint(&self) -> Vec<u8> {
|
||||
use crate::CanonicalValue;
|
||||
let mut out = Vec::new();
|
||||
let lp = |out: &mut Vec<u8>, bytes: &[u8]| {
|
||||
out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
|
||||
out.extend_from_slice(bytes);
|
||||
};
|
||||
|
||||
out.extend_from_slice(&(self.spellings.len() as u64).to_le_bytes());
|
||||
for (pid, rs) in &self.spellings {
|
||||
out.extend_from_slice(&pid.canonical_bytes());
|
||||
lp(&mut out, &rs.spelling.canonical_bytes());
|
||||
match &rs.provenance {
|
||||
SpellingProvenance::Inferred => out.push(0),
|
||||
SpellingProvenance::Authored(kind) => {
|
||||
out.push(1);
|
||||
lp(&mut out, &kind.canonical_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.extend_from_slice(&(self.decompositions.len() as u64).to_le_bytes());
|
||||
for (eid, dec) in &self.decompositions {
|
||||
out.extend_from_slice(&eid.canonical_bytes());
|
||||
lp(&mut out, &dec.canonical_bytes());
|
||||
}
|
||||
|
||||
let t = &self.taxonomy;
|
||||
for count in [
|
||||
t.pitched_events,
|
||||
t.unpitched_events,
|
||||
t.rest_events,
|
||||
t.trajectory_events,
|
||||
t.graphic_events,
|
||||
t.indeterminate_events,
|
||||
t.cue_events,
|
||||
t.spellings_inferred,
|
||||
t.spellings_authored,
|
||||
t.spelling_unavailable,
|
||||
t.decompositions_inferred,
|
||||
t.decomposition_skipped_nonmusical,
|
||||
t.decomposition_deferred_nonmetric,
|
||||
t.decomposition_inapplicable,
|
||||
t.decomposition_ungriddable,
|
||||
] {
|
||||
out.extend_from_slice(&(count as u64).to_le_bytes());
|
||||
}
|
||||
|
||||
lp(
|
||||
&mut out,
|
||||
self.profile.spelling_algorithm.as_str().as_bytes(),
|
||||
);
|
||||
lp(
|
||||
&mut out,
|
||||
self.profile.decomposition_algorithm.as_str().as_bytes(),
|
||||
);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Entry point
|
||||
// ===========================================================================
|
||||
|
|
@ -160,21 +231,37 @@ pub fn derive_annotations(score: &Score, profile: &PrePassProfile) -> DerivedAnn
|
|||
// position-sorted (invariant 3), so the per-voice order is canonical.
|
||||
let layout = ScoreLayout::build(score);
|
||||
|
||||
// The pre-passes implement exactly one algorithm each (the registered
|
||||
// `"default"` id). A profile requesting any other id is **not honored**:
|
||||
// rather than return default output labeled as the requested algorithm —
|
||||
// which would let an unimplemented/future algorithm silently alias the
|
||||
// default in a derivation cache — that pre-pass derives nothing. The
|
||||
// requested id stays in the result's `profile`, so the cache key is honest.
|
||||
let spelling_supported = profile.spelling_algorithm == SpellingAlgorithmId::default_id();
|
||||
let decomposition_supported =
|
||||
profile.decomposition_algorithm == DecompositionAlgorithmId::default_id();
|
||||
|
||||
// --- Spelling pre-pass. ---
|
||||
let inferred = infer_spellings(score, &layout, &mut taxonomy);
|
||||
let precedence = &score.spelling_precedence;
|
||||
let mut spellings = BTreeMap::new();
|
||||
for (pid, inferred_spelling) in inferred {
|
||||
let resolved = resolve_spelling(score, pid, inferred_spelling, precedence);
|
||||
match resolved.provenance {
|
||||
SpellingProvenance::Inferred => taxonomy.spellings_inferred += 1,
|
||||
SpellingProvenance::Authored(_) => taxonomy.spellings_authored += 1,
|
||||
if spelling_supported {
|
||||
let inferred = infer_spellings(score, &layout, &mut taxonomy);
|
||||
let precedence = &score.spelling_precedence;
|
||||
for (pid, inferred_spelling) in inferred {
|
||||
let resolved = resolve_spelling(score, pid, inferred_spelling, precedence);
|
||||
match resolved.provenance {
|
||||
SpellingProvenance::Inferred => taxonomy.spellings_inferred += 1,
|
||||
SpellingProvenance::Authored(_) => taxonomy.spellings_authored += 1,
|
||||
}
|
||||
spellings.insert(pid, resolved);
|
||||
}
|
||||
spellings.insert(pid, resolved);
|
||||
}
|
||||
|
||||
// --- Decomposition pre-pass. ---
|
||||
let decompositions = infer_decompositions(score, &layout, &mut taxonomy);
|
||||
let decompositions = if decomposition_supported {
|
||||
infer_decompositions(score, &layout, &mut taxonomy)
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
};
|
||||
|
||||
DerivedAnnotations {
|
||||
spellings,
|
||||
|
|
|
|||
|
|
@ -1018,3 +1018,76 @@ fn decomposition_components_sum_to_event_duration() {
|
|||
assert_eq!(total, dur, "components sum to the event duration");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_algorithm_ids_are_not_honored() {
|
||||
// A profile requesting an algorithm the pre-pass does not implement must not
|
||||
// receive default output labeled as that algorithm; the unhonored pre-pass
|
||||
// derives nothing while the requested id stays in the result profile.
|
||||
let score = metric_score(|idc, voice| {
|
||||
let eid = idc.mint();
|
||||
let pid = idc.mint::<PitchId>();
|
||||
let ev = pitched(
|
||||
eid,
|
||||
voice,
|
||||
r(0, 1),
|
||||
r(1, 4),
|
||||
vec![IdentifiedPitch {
|
||||
id: pid,
|
||||
pitch: integer_pitch(48),
|
||||
}],
|
||||
);
|
||||
(vec![ev], vec![])
|
||||
});
|
||||
|
||||
let default = derive_annotations(&score, &PrePassProfile::default());
|
||||
assert!(!default.spellings.is_empty(), "the default profile spells");
|
||||
assert!(
|
||||
!default.decompositions.is_empty(),
|
||||
"the default profile decomposes"
|
||||
);
|
||||
|
||||
// Unknown spelling algorithm: no spellings; decomposition (still default) runs.
|
||||
let unknown_spelling = PrePassProfile {
|
||||
spelling_algorithm: SpellingAlgorithmId::new("future-v2"),
|
||||
decomposition_algorithm: DecompositionAlgorithmId::default_id(),
|
||||
};
|
||||
let a = derive_annotations(&score, &unknown_spelling);
|
||||
assert!(
|
||||
a.spellings.is_empty(),
|
||||
"an unknown spelling algorithm is not honored (no default output)"
|
||||
);
|
||||
assert_eq!(
|
||||
a.decompositions.len(),
|
||||
default.decompositions.len(),
|
||||
"the supported decomposition pre-pass still runs"
|
||||
);
|
||||
assert_eq!(
|
||||
a.profile.spelling_algorithm,
|
||||
SpellingAlgorithmId::new("future-v2"),
|
||||
"the requested id is preserved in the result profile"
|
||||
);
|
||||
// The canonical fingerprint distinguishes differing derivations (it is not a
|
||||
// degenerate constant the determinism gate would pass vacuously).
|
||||
assert_ne!(
|
||||
a.canonical_fingerprint(),
|
||||
default.canonical_fingerprint(),
|
||||
"the canonical fingerprint discriminates differing annotations"
|
||||
);
|
||||
|
||||
// Unknown decomposition algorithm: no decompositions; spelling runs.
|
||||
let unknown_decomp = PrePassProfile {
|
||||
spelling_algorithm: SpellingAlgorithmId::default_id(),
|
||||
decomposition_algorithm: DecompositionAlgorithmId::new("future-v2"),
|
||||
};
|
||||
let b = derive_annotations(&score, &unknown_decomp);
|
||||
assert!(
|
||||
b.decompositions.is_empty(),
|
||||
"an unknown decomposition algorithm is not honored"
|
||||
);
|
||||
assert_eq!(
|
||||
b.spellings.len(),
|
||||
default.spellings.len(),
|
||||
"the supported spelling pre-pass still runs"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
use epiphany_core::{
|
||||
derive_promoted_voice_id, AnchorOffset, CanonicalValue, Event, EventDuration, EventId,
|
||||
EventPosition, MusicalDuration, MusicalPosition, OperationId, Pitch, PitchId, PitchSpelling,
|
||||
RegionEdge, RegionId, RegionTimeModel, Score, StaffInstance, StaffInstanceId, TimeAnchor,
|
||||
TransactionId, TypedObjectId, Voice, VoiceId, VoiceOrigin,
|
||||
RegionEdge, RegionId, RegionTimeModel, Score, SpellingAttachment, SpellingDirective,
|
||||
SpellingScope, SpellingSource, StaffInstance, StaffInstanceId, TimeAnchor, TransactionId,
|
||||
TypedObjectId, Voice, VoiceId, VoiceOrigin,
|
||||
};
|
||||
use epiphany_determinism::CanonicalEncode;
|
||||
|
||||
|
|
@ -1559,8 +1560,7 @@ impl<'a> Reducer<'a> {
|
|||
|
||||
match self.last_respell.get(&op.pitch).copied() {
|
||||
None => {
|
||||
self.spellings.insert(op.pitch, op.spelling.clone());
|
||||
self.last_respell.insert(op.pitch, env.id);
|
||||
self.materialize_respell(env, op);
|
||||
OperationEffect::Applied
|
||||
}
|
||||
Some(prev_op) => {
|
||||
|
|
@ -1578,8 +1578,7 @@ impl<'a> Reducer<'a> {
|
|||
// earlier op is recorded as the loser. (Prototype
|
||||
// convention: the winner carries the Conflicted effect;
|
||||
// see DECISIONS.md.)
|
||||
self.spellings.insert(op.pitch, op.spelling.clone());
|
||||
self.last_respell.insert(op.pitch, env.id);
|
||||
self.materialize_respell(env, op);
|
||||
let conflict = ConflictRecord::new(
|
||||
ConflictKind::StructuralFieldCollision {
|
||||
winner: env.id,
|
||||
|
|
@ -1595,14 +1594,49 @@ impl<'a> Reducer<'a> {
|
|||
}
|
||||
} else {
|
||||
// Causally-ordered re-respell: intentional overwrite.
|
||||
self.spellings.insert(op.pitch, op.spelling.clone());
|
||||
self.last_respell.insert(op.pitch, env.id);
|
||||
self.materialize_respell(env, op);
|
||||
OperationEffect::Applied
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a winning respell: the canonical bookkeeping spelling, the LWW
|
||||
/// marker, and — for graph-aware reduction — an explicit user-chosen
|
||||
/// [`SpellingAttachment`] so the spelling/decomposition pre-passes (Agent H's
|
||||
/// `derive_annotations`) resolve the override with manual-override precedence.
|
||||
/// Without the graph attachment a reduced `RespellPitch` would be visible only
|
||||
/// in `MaterializedState.spellings` and lost before annotation derivation.
|
||||
fn materialize_respell(&mut self, env: &OperationEnvelope, op: &RespellPitchOp) {
|
||||
self.spellings.insert(op.pitch, op.spelling.clone());
|
||||
self.last_respell.insert(op.pitch, env.id);
|
||||
self.graph_respell_pitch(op.pitch, &op.spelling);
|
||||
}
|
||||
|
||||
fn graph_respell_pitch(&mut self, pitch: PitchId, spelling: &PitchSpelling) {
|
||||
let Some(score) = self.graph.as_mut() else {
|
||||
return;
|
||||
};
|
||||
// Upsert the user-chosen explicit override (one per pitch), keeping the
|
||||
// attachment list's canonical order stable for the resolver's tie-break.
|
||||
if let Some(existing) = score.spelling_attachments.iter_mut().find(|a| {
|
||||
a.layer.is_none()
|
||||
&& matches!(a.source, SpellingSource::UserChosen)
|
||||
&& matches!(&a.scope, SpellingScope::Pitch(p) if *p == pitch)
|
||||
&& matches!(a.directive, SpellingDirective::Explicit(_))
|
||||
}) {
|
||||
existing.directive = SpellingDirective::Explicit(spelling.clone());
|
||||
} else {
|
||||
score.spelling_attachments.push(SpellingAttachment {
|
||||
scope: SpellingScope::Pitch(pitch),
|
||||
directive: SpellingDirective::Explicit(spelling.clone()),
|
||||
source: SpellingSource::UserChosen,
|
||||
priority: 0,
|
||||
layer: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn create_cross_cutting(
|
||||
&mut self,
|
||||
env: &OperationEnvelope,
|
||||
|
|
@ -2692,6 +2726,19 @@ impl<'a> Reducer<'a> {
|
|||
let Some(score) = self.graph.as_mut() else {
|
||||
return;
|
||||
};
|
||||
// Drop any user-chosen respell override for the deleted pitch (the dual
|
||||
// of `graph_respell_pitch`), so no spelling attachment is left targeting a
|
||||
// pitch that is no longer present (Chapter 5 SpellingScopeResolves). The
|
||||
// pitch is not added to `tombstoned_pitches` here: unlike a whole-event
|
||||
// delete the event survives, and a later ModifyEvent may legitimately
|
||||
// reintroduce the id — tombstoning it would make it both live and
|
||||
// tombstoned (invariant 11).
|
||||
score.spelling_attachments.retain(|a| {
|
||||
!(a.layer.is_none()
|
||||
&& matches!(a.source, SpellingSource::UserChosen)
|
||||
&& matches!(&a.scope, SpellingScope::Pitch(p) if *p == pitch)
|
||||
&& matches!(a.directive, SpellingDirective::Explicit(_)))
|
||||
});
|
||||
let Some(event) = Self::graph_event_of_pitch(score, pitch) else {
|
||||
return;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ 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:?}")
|
||||
/// A canonical byte fingerprint of derived annotations
|
||||
/// ([`DerivedAnnotations::canonical_fingerprint`]): the embedded graph values use
|
||||
/// their ratified `CanonicalValue` bytes, counts/ids are little-endian. Equal
|
||||
/// fingerprints ⇒ byte-identical derivations — a normative serialization surface,
|
||||
/// rather than the (non-normative) `Debug` form the gate used previously.
|
||||
pub fn fingerprint(ann: &DerivedAnnotations) -> Vec<u8> {
|
||||
ann.canonical_fingerprint()
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -385,6 +385,77 @@ pub fn assert_respell_precedence() {
|
|||
);
|
||||
}
|
||||
|
||||
/// The H↔K seam: a **real reduced** `RespellPitch` (not a hand-pushed
|
||||
/// attachment) is honored by [`derive_annotations`]. The reducer must surface
|
||||
/// the override into the materialized graph; otherwise a respelling accepted by
|
||||
/// `reduce_onto` is lost before the pre-pass runs, silently violating the
|
||||
/// manual-override precedence requirement.
|
||||
pub fn assert_reduced_respell_is_honored() {
|
||||
use epiphany_core::{check_invariants, OperationId, ReplicaId, WallClockTime};
|
||||
use epiphany_ops::{
|
||||
AuthorId, CausalContext, HybridLogicalClock, OperationEnvelope, OperationKind,
|
||||
OperationPayload, OperationSet, OperationStamp, RespellPitchOp,
|
||||
};
|
||||
|
||||
let (score, pid) = corpus::override_probe();
|
||||
|
||||
// Inferred baseline (no override).
|
||||
let inferred = derive_annotations(&score, &profile())
|
||||
.spellings
|
||||
.get(&pid)
|
||||
.expect("probe pitch is spelled")
|
||||
.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(),
|
||||
};
|
||||
assert_ne!(
|
||||
override_spelling, inferred,
|
||||
"the override fixture must differ from the inferred spelling to be meaningful"
|
||||
);
|
||||
|
||||
// Reduce a real RespellPitch onto the score, then derive annotations on the
|
||||
// *materialized* graph the reducer produced.
|
||||
let id = OperationId::new(ReplicaId(0x5EE11), 0);
|
||||
let respell = OperationEnvelope {
|
||||
id,
|
||||
author: AuthorId(0),
|
||||
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(1), 0), id),
|
||||
causal_context: CausalContext::new(),
|
||||
transaction: None,
|
||||
payload: OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
|
||||
pitch: pid,
|
||||
spelling: override_spelling.clone(),
|
||||
})),
|
||||
};
|
||||
let mut set = OperationSet::new();
|
||||
set.accept(respell);
|
||||
let reduced = set.reduce_onto(&score);
|
||||
|
||||
let ann = derive_annotations(&reduced.score, &profile());
|
||||
let rs = ann
|
||||
.spellings
|
||||
.get(&pid)
|
||||
.expect("the respelled pitch is spelled");
|
||||
assert_eq!(
|
||||
rs.spelling, override_spelling,
|
||||
"a reduced RespellPitch must be honored by derive_annotations (not lost before the pre-pass)"
|
||||
);
|
||||
assert_eq!(
|
||||
rs.provenance,
|
||||
SpellingProvenance::Authored(SpellingSourceKind::UserChosen),
|
||||
"a reduced RespellPitch should resolve with manual-override precedence"
|
||||
);
|
||||
assert!(
|
||||
check_invariants(&reduced.score).is_empty(),
|
||||
"the reduced score carrying the surfaced override is invariant-clean"
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Spelling correctness vs. published tonal expectations
|
||||
// ===========================================================================
|
||||
|
|
@ -674,8 +745,10 @@ pub fn run_all(scale: u64) {
|
|||
assert_decompositions_reconstruct(&score, &ann);
|
||||
}
|
||||
|
||||
// Authored-override precedence.
|
||||
// Authored-override precedence (hand-pushed attachment) ...
|
||||
assert_respell_precedence();
|
||||
// ... and a real reduced RespellPitch surfaced through the K↔H seam.
|
||||
assert_reduced_respell_is_honored();
|
||||
|
||||
// Spelling correctness on published tonal cases.
|
||||
assert_spelling_matches_published_expectations();
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -652,24 +652,43 @@ plus a non-vacuity guard.
|
|||
\chapter{K1 --- Framework Slots (Phase 3)}
|
||||
\label{ch:k1}
|
||||
|
||||
The following K0 catalogue bullet items are \emph{drafted} here as framework
|
||||
slots and completed in Phase~3. Under the Phase-2 profile they are
|
||||
\textbf{unavailable}: an implementation \MUST{} reject an operation of one of
|
||||
these kinds. Each is a schema-fill against the template of
|
||||
Chapter~\ref{ch:framework}; adding one is not a fresh design.
|
||||
This chapter drafted the remaining catalogue items as framework slots. Since the
|
||||
Phase-2 \textbf{M2} expansion (the broad-K0 groups in \texttt{epiphany-ops}),
|
||||
several have been \emph{implemented} and are available under the Phase-2 profile;
|
||||
their per-primitive \S{}schema joins Chapter~\ref{ch:k0} with the M2e catalogue
|
||||
expansion. The genuinely Phase-3 slots that remain \textbf{unavailable} are listed
|
||||
second: an implementation \MUST{} reject an operation of one of \emph{those} kinds
|
||||
(and \MUST{} \emph{not} reject the implemented kinds below). Each remaining slot is
|
||||
a schema-fill against the template of Chapter~\ref{ch:framework}; adding one is not
|
||||
a fresh design.
|
||||
|
||||
\section*{Implemented since M2 (available under the Phase-2 profile)}
|
||||
|
||||
\begin{description}
|
||||
\item[Create score / canvas / region / staff / staff instance / voice]
|
||||
Structural mint operations. Discipline: set-union creation
|
||||
(Section~\ref{sec:k0:create-cross-cutting}); undo tombstones the mint;
|
||||
re-anchoring is not applicable.
|
||||
\item[ModifyEvent]
|
||||
Field overwrite on an event's non-identity fields (articulations, dynamics,
|
||||
stem). Discipline: last-writer-wins with structural-field-collision
|
||||
(Section~\ref{sec:k0:respell-pitch}).
|
||||
\item[Modify event; transpose]
|
||||
Field overwrite on an event's non-identity fields, and an order-dependent
|
||||
chromatic transpose (M2 Group~1). Discipline: last-writer-wins with
|
||||
structural-field-collision (Section~\ref{sec:k0:respell-pitch}).
|
||||
\item[Insert / delete / modify identified pitch]
|
||||
Pitch-level mint, tombstone, and field overwrite within an event. Disciplines
|
||||
as for the event-level analogues.
|
||||
Pitch-level mint, tombstone, and field overwrite within an event (M2
|
||||
Group~1). Disciplines as for the event-level analogues.
|
||||
\item[Delete / update tie / slur / beam / spanner]
|
||||
Cross-cutting tombstone and field overwrite (M2 Group~2). Disciplines:
|
||||
delete-wins with re-anchoring (Section~\ref{sec:k0:delete-event}) and field
|
||||
overwrite.
|
||||
\item[Create / delete region / staff instance / voice]
|
||||
Structural container mint and \emph{empty-only} delete (M2 Group~3).
|
||||
Disciplines: set-union creation (Section~\ref{sec:k0:create-cross-cutting})
|
||||
and a delete-wins tombstone that is a precondition no-op unless the container
|
||||
has no live children.
|
||||
\end{description}
|
||||
|
||||
\section*{Remaining framework slots (Phase 3 --- unavailable, MUST reject)}
|
||||
|
||||
\begin{description}
|
||||
\item[Create score / canvas / staff]
|
||||
The remaining structural mints (the document root, the canvas, and global
|
||||
staves) the Phase-2 slice does not exercise. Discipline: set-union creation.
|
||||
\item[Set metadata (title / composer / lyricist / copyright)]
|
||||
Field overwrite on score metadata. Discipline: LWW advisory
|
||||
(Section~\ref{sec:k0:set-user-system-break}).
|
||||
|
|
@ -677,10 +696,7 @@ Chapter~\ref{ch:framework}; adding one is not a fresh design.
|
|||
Structural field overwrite on a region's metric model. Discipline:
|
||||
last-writer-wins, with a structural-field-collision on concurrent differing
|
||||
grids.
|
||||
\item[Delete / update tie / slur / beam / spanner]
|
||||
Cross-cutting tombstone and field overwrite. Disciplines: delete-wins with
|
||||
re-anchoring (Section~\ref{sec:k0:delete-event}) and field overwrite.
|
||||
\item[Set layout / system-break advisory]
|
||||
\item[Set layout / system- and page-break advisory]
|
||||
The page/layout advisory companion to
|
||||
Section~\ref{sec:k0:set-user-system-break}.
|
||||
\end{description}
|
||||
|
|
|
|||
Loading…
Reference in New Issue