387 lines
15 KiB
Rust
387 lines
15 KiB
Rust
//! Engraving-decision records (Chapter 7 §"Engraving Decisions").
|
|
//!
|
|
//! "When the engraver makes a decision (stem direction, accidental ordering,
|
|
//! beam consolidation), the decision is recorded in the IR. The decision can be
|
|
//! inspected, overridden, and traced" (Chapter 7 §"Design Principles"). The
|
|
//! pipeline records decisions explicitly so they survive every stage and remain
|
|
//! attributable to their source. v0 implements the decision *records* and their
|
|
//! provenance and override interfaces (the QUICKSTART scope item); production
|
|
//! engraving algorithms remain layered specifications beyond the v0 stub.
|
|
|
|
use epiphany_core::{CanonicalValue, RegionId, StemDirection, TimeAnchor, TypedObjectId};
|
|
use epiphany_determinism::{DomainTag, Preimage};
|
|
|
|
use crate::provenance::LayoutObjectId;
|
|
use crate::spatial::Point;
|
|
|
|
/// A content-derived identifier for an engraving decision. Derived from the
|
|
/// decision's target and kind, so equal decisions on the same target share an
|
|
/// id and the records stay stable across re-engraving.
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct EngravingDecisionId(pub u128);
|
|
|
|
/// A registry id for an extension-defined [`EngravingDecisionKind::Registered`].
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct EngravingDecisionRegistryId(pub u128);
|
|
|
|
/// A user-override identifier referenced by [`DecisionSource::UserOverride`].
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct EngravingOverrideId(pub u128);
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct AuthorId(pub u128);
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct ForeignFormatId(pub u128);
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct PluginId(pub u128);
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct Timestamp(pub i64);
|
|
|
|
/// The authoritative or transient target of an engraving override.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub enum OverrideTarget {
|
|
ScoreGraph(TypedObjectId),
|
|
IrSynthesized(LayoutObjectId),
|
|
}
|
|
|
|
/// An override's binding strength.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub enum OverridePriority {
|
|
Hard,
|
|
Soft,
|
|
}
|
|
|
|
/// Provenance of a user/import/plugin override.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub enum OverrideOrigin {
|
|
User {
|
|
author: AuthorId,
|
|
timestamp: Timestamp,
|
|
},
|
|
Import {
|
|
format: ForeignFormatId,
|
|
},
|
|
Plugin {
|
|
plugin: PluginId,
|
|
},
|
|
Internal,
|
|
}
|
|
|
|
/// Core override vocabulary. More detailed engraving payloads are represented
|
|
/// by stable registered ids until their companion algorithm specifications land.
|
|
///
|
|
/// A break override addresses a *position*, not an object (Chapter 7
|
|
/// §"Engraving Overrides"): the kind carries the break's [`TimeAnchor`], while
|
|
/// the override's `ScoreGraph` target names the owning region.
|
|
#[derive(Clone, PartialEq, Debug)]
|
|
pub enum OverrideKind {
|
|
StemDirection(StemDirection),
|
|
AccidentalParenthesized(bool),
|
|
AccidentalVisible(bool),
|
|
SystemBreak { anchor: TimeAnchor },
|
|
PageBreak { anchor: TimeAnchor },
|
|
HiddenObject,
|
|
CustomPosition(Point),
|
|
LedgerLineSuppression,
|
|
Registered(u128),
|
|
}
|
|
|
|
impl OverrideKind {
|
|
/// A stable discriminant byte, part of the override-id preimage and the
|
|
/// projection's deterministic ordering key.
|
|
pub(crate) fn discriminant(&self) -> u8 {
|
|
match self {
|
|
OverrideKind::StemDirection(_) => 0,
|
|
OverrideKind::AccidentalParenthesized(_) => 1,
|
|
OverrideKind::AccidentalVisible(_) => 2,
|
|
OverrideKind::SystemBreak { .. } => 3,
|
|
OverrideKind::PageBreak { .. } => 4,
|
|
OverrideKind::HiddenObject => 5,
|
|
OverrideKind::CustomPosition(_) => 6,
|
|
OverrideKind::LedgerLineSuppression => 7,
|
|
OverrideKind::Registered(_) => 8,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A projected engraving override (Chapter 7 §"Engraving Overrides").
|
|
#[derive(Clone, PartialEq, Debug)]
|
|
pub struct EngravingOverride {
|
|
pub id: EngravingOverrideId,
|
|
pub target: OverrideTarget,
|
|
pub kind: OverrideKind,
|
|
pub priority: OverridePriority,
|
|
pub origin: OverrideOrigin,
|
|
}
|
|
|
|
impl EngravingOverride {
|
|
/// A system-break override projected from a region's authoritative
|
|
/// `user_system_breaks` list (Chapter 5 §"Staff-Based Content").
|
|
pub fn projected_system_break(region: RegionId, anchor: TimeAnchor) -> Self {
|
|
Self::projected_break(region, OverrideKind::SystemBreak { anchor })
|
|
}
|
|
|
|
/// A page-break override projected from a region's authoritative
|
|
/// `user_page_breaks` list (Chapter 5 §"Staff-Based Content").
|
|
pub fn projected_page_break(region: RegionId, anchor: TimeAnchor) -> Self {
|
|
Self::projected_break(region, OverrideKind::PageBreak { anchor })
|
|
}
|
|
|
|
/// The shared shape of a projected break override (Chapter 7 §"Engraving
|
|
/// Overrides"): the kind carries the break's anchor, the `ScoreGraph`
|
|
/// target names the owning region, the binding is `Soft` (the layout
|
|
/// SHOULD honor it), and the origin is `Internal` — break authorship
|
|
/// (author, timestamp) lives in the operation log, not the materialized
|
|
/// break lists, until the snapshot-undo refinement (P11-C8) surfaces it.
|
|
fn projected_break(region: RegionId, kind: OverrideKind) -> Self {
|
|
EngravingOverride {
|
|
id: derive_break_override_id(region, &kind),
|
|
target: OverrideTarget::ScoreGraph(TypedObjectId::Region(region)),
|
|
kind,
|
|
priority: OverridePriority::Soft,
|
|
origin: OverrideOrigin::Internal,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Derives an [`EngravingOverrideId`] for a projected break override from its
|
|
/// owning region, its kind discriminant, and the break anchor's canonical
|
|
/// bytes — so equal breaks share an id across re-projection and distinct ones
|
|
/// never collide.
|
|
///
|
|
/// Like the engraving-decision id, the override is a non-canonical
|
|
/// layout-namespace object, so the preimage is domain-separated under
|
|
/// [`DomainTag::LAYOUT_OBJECT_ID`] (`MUSCLOID`) with a literal
|
|
/// `engraving-override` discriminator prefix, so an override id can alias
|
|
/// neither a layout-object id nor a decision id within that namespace.
|
|
fn derive_break_override_id(region: RegionId, kind: &OverrideKind) -> EngravingOverrideId {
|
|
let anchor = match kind {
|
|
OverrideKind::SystemBreak { anchor } | OverrideKind::PageBreak { anchor } => anchor,
|
|
_ => unreachable!("projected break overrides carry a break kind"),
|
|
};
|
|
let mut p = Preimage::new(DomainTag::LAYOUT_OBJECT_ID);
|
|
p.push_bytes(b"engraving-override");
|
|
p.push_bytes(®ion.canonical_bytes());
|
|
p.push_u64_le(kind.discriminant() as u64);
|
|
p.push_bytes(&anchor.canonical_bytes());
|
|
EngravingOverrideId(p.finish_trunc128())
|
|
}
|
|
|
|
/// Where an engraving decision came from (Chapter 7 §"Note Layout":
|
|
/// `DecisionSource`).
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub enum DecisionSource {
|
|
/// Derived from automatic engraving rules.
|
|
Automatic,
|
|
/// Derived from a user override in the score graph.
|
|
UserOverride(EngravingOverrideId),
|
|
/// Derived from an IR-stage override.
|
|
IrOverride,
|
|
}
|
|
|
|
/// A decision the engraver recorded (Chapter 7 §"Engraving Decisions":
|
|
/// `EngravingDecisionKind`). v0 carries a representative subset; the catalog is
|
|
/// extensible (the trailing `Registered` variant).
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub enum EngravingDecisionKind {
|
|
/// The stem direction chosen for a note or chord.
|
|
StemDirection(StemDirection),
|
|
/// The number of ledger lines a note requires.
|
|
LedgerLineCount(u8),
|
|
/// A system break placed here.
|
|
SystemBreak,
|
|
/// A page break placed here.
|
|
PageBreak,
|
|
/// An extension-defined decision kind.
|
|
Registered(EngravingDecisionRegistryId),
|
|
}
|
|
|
|
impl EngravingDecisionKind {
|
|
/// A stable discriminant byte, part of the decision-id preimage.
|
|
fn discriminant(&self) -> u8 {
|
|
match self {
|
|
EngravingDecisionKind::StemDirection(_) => 0,
|
|
EngravingDecisionKind::LedgerLineCount(_) => 1,
|
|
EngravingDecisionKind::SystemBreak => 2,
|
|
EngravingDecisionKind::PageBreak => 3,
|
|
EngravingDecisionKind::Registered(_) => 4,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An engraving-decision record (Chapter 7 §"Engraving Decisions":
|
|
/// `EngravingDecision`). Carried forward through every IR stage.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub struct EngravingDecision {
|
|
pub id: EngravingDecisionId,
|
|
pub target: LayoutObjectId,
|
|
pub kind: EngravingDecisionKind,
|
|
pub source: DecisionSource,
|
|
}
|
|
|
|
impl EngravingDecision {
|
|
/// An automatic decision on `target`, with a content-derived id.
|
|
pub fn automatic(target: LayoutObjectId, kind: EngravingDecisionKind) -> Self {
|
|
let source = DecisionSource::Automatic;
|
|
EngravingDecision {
|
|
id: derive_decision_id(target, &kind, source),
|
|
target,
|
|
kind,
|
|
source,
|
|
}
|
|
}
|
|
|
|
/// A decision on `target` attributed to `source`, with a content-derived id
|
|
/// that includes the source (so the same target+kind from an automatic rule
|
|
/// and from a user override are distinct decisions).
|
|
pub fn with_source(
|
|
target: LayoutObjectId,
|
|
kind: EngravingDecisionKind,
|
|
source: DecisionSource,
|
|
) -> Self {
|
|
EngravingDecision {
|
|
id: derive_decision_id(target, &kind, source),
|
|
target,
|
|
kind,
|
|
source,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Derives an [`EngravingDecisionId`] from its target, kind, and source, so
|
|
/// equal decisions share an id and differing ones do not.
|
|
///
|
|
/// An engraving decision is a non-canonical layout-namespace object, so the
|
|
/// preimage is domain-separated under the layout tag
|
|
/// [`DomainTag::LAYOUT_OBJECT_ID`] (`MUSCLOID`) — the same tag as
|
|
/// [`crate::provenance::LayoutObjectId`] — with a literal `engraving-decision`
|
|
/// discriminator prefix so a decision id can never alias a layout-object id
|
|
/// within that namespace.
|
|
fn derive_decision_id(
|
|
target: LayoutObjectId,
|
|
kind: &EngravingDecisionKind,
|
|
source: DecisionSource,
|
|
) -> EngravingDecisionId {
|
|
let mut p = Preimage::new(DomainTag::LAYOUT_OBJECT_ID);
|
|
p.push_bytes(b"engraving-decision");
|
|
p.push_u64_le((target.0 >> 64) as u64);
|
|
p.push_u64_le(target.0 as u64);
|
|
p.push_u64_le(kind.discriminant() as u64);
|
|
match kind {
|
|
EngravingDecisionKind::StemDirection(d) => {
|
|
p.push_u64_le(matches!(d, StemDirection::Up) as u64);
|
|
}
|
|
EngravingDecisionKind::LedgerLineCount(n) => {
|
|
p.push_u64_le(*n as u64);
|
|
}
|
|
EngravingDecisionKind::Registered(r) => {
|
|
p.push_u64_le((r.0 >> 64) as u64);
|
|
p.push_u64_le(r.0 as u64);
|
|
}
|
|
EngravingDecisionKind::SystemBreak | EngravingDecisionKind::PageBreak => {}
|
|
}
|
|
match source {
|
|
DecisionSource::Automatic => {
|
|
p.push_u64_le(0);
|
|
}
|
|
DecisionSource::UserOverride(id) => {
|
|
p.push_u64_le(1);
|
|
p.push_u64_le((id.0 >> 64) as u64);
|
|
p.push_u64_le(id.0 as u64);
|
|
}
|
|
DecisionSource::IrOverride => {
|
|
p.push_u64_le(2);
|
|
}
|
|
}
|
|
EngravingDecisionId(p.finish_trunc128())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn decision_id_is_content_derived_and_stable() {
|
|
let target = LayoutObjectId(0xABCD);
|
|
let a = EngravingDecision::automatic(target, EngravingDecisionKind::SystemBreak);
|
|
let b = EngravingDecision::automatic(target, EngravingDecisionKind::SystemBreak);
|
|
assert_eq!(a, b);
|
|
// Different kind on the same target → different id.
|
|
let c = EngravingDecision::automatic(target, EngravingDecisionKind::PageBreak);
|
|
assert_ne!(a.id, c.id);
|
|
// Different target → different id.
|
|
let d = EngravingDecision::automatic(LayoutObjectId(1), EngravingDecisionKind::SystemBreak);
|
|
assert_ne!(a.id, d.id);
|
|
}
|
|
|
|
#[test]
|
|
fn decision_source_changes_the_id() {
|
|
let target = LayoutObjectId(5);
|
|
let auto = EngravingDecision::automatic(target, EngravingDecisionKind::SystemBreak);
|
|
let over = EngravingDecision::with_source(
|
|
target,
|
|
EngravingDecisionKind::SystemBreak,
|
|
DecisionSource::IrOverride,
|
|
);
|
|
assert_eq!(auto.source, DecisionSource::Automatic);
|
|
assert_ne!(
|
|
auto.id, over.id,
|
|
"the decision source participates in the id"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn projected_break_override_ids_are_content_derived() {
|
|
use epiphany_core::{RegionId, WallClockTime};
|
|
let region = RegionId::from_raw(9);
|
|
let anchor = TimeAnchor::WallClock {
|
|
time: WallClockTime(7),
|
|
};
|
|
let a = EngravingOverride::projected_system_break(region, anchor.clone());
|
|
let b = EngravingOverride::projected_system_break(region, anchor.clone());
|
|
assert_eq!(a, b, "equal breaks share an id across re-projection");
|
|
// A page break at the same anchor is a distinct override…
|
|
let page = EngravingOverride::projected_page_break(region, anchor.clone());
|
|
assert_ne!(a.id, page.id);
|
|
// …as is the same break at a different anchor…
|
|
let other = EngravingOverride::projected_system_break(
|
|
region,
|
|
TimeAnchor::WallClock {
|
|
time: WallClockTime(8),
|
|
},
|
|
);
|
|
assert_ne!(a.id, other.id);
|
|
// …or in a different owning region.
|
|
let elsewhere =
|
|
EngravingOverride::projected_system_break(RegionId::from_raw(10), anchor.clone());
|
|
assert_ne!(a.id, elsewhere.id);
|
|
// The projected shape the spec pins: the kind carries the break anchor,
|
|
// the ScoreGraph target names the owning region, the binding is Soft,
|
|
// and the origin is Internal (authorship lives in the op log, P11-C8).
|
|
assert_eq!(a.kind, OverrideKind::SystemBreak { anchor });
|
|
assert_eq!(
|
|
a.target,
|
|
OverrideTarget::ScoreGraph(TypedObjectId::Region(region))
|
|
);
|
|
assert_eq!(a.priority, OverridePriority::Soft);
|
|
assert_eq!(a.origin, OverrideOrigin::Internal);
|
|
}
|
|
|
|
#[test]
|
|
fn stem_direction_payload_changes_the_id() {
|
|
let target = LayoutObjectId(7);
|
|
let up = EngravingDecision::automatic(
|
|
target,
|
|
EngravingDecisionKind::StemDirection(StemDirection::Up),
|
|
);
|
|
let down = EngravingDecision::automatic(
|
|
target,
|
|
EngravingDecisionKind::StemDirection(StemDirection::Down),
|
|
);
|
|
assert_ne!(up.id, down.id);
|
|
}
|
|
}
|