Clear P12-I12: scope spacing_distortion to rhythmic columns (QMC 0.2.0)
The Standard-tier spacing floor warned on short healthy scores: three reference-suite entries measured spacing_distortion 0.36-0.41, above the 0.32 warning floor (0.8 x 0.40), a spurious diagnostic (every score still passed Minimal). Root cause: the CV folded in the clef-to-first-note lead advance, which is sized by notational-furniture width, not by rhythm. Unlike P12-I11 (an engrave-side fix, no spec change), the defect here lived in the metric's own normative definition, so the honest fix is a catalog change - the mirror of I11: correct the measurement rather than relax the threshold. Quality Metric Catalog 0.1.0 -> 0.2.0: spacing_distortion is scoped to the system's rhythmic columns (spring slots bearing a notehead or rest). The clef / key-signature / time-signature lead and barlines contribute no column, so a note-to-note advance spans them. quality::census now builds its spacing columns only from a precomputed rhythmic-slot set (is_rhythmic: a notehead*/rest* glyph anywhere in the slot); the CV and the >= 3-column contributing-unit rule are otherwise unchanged. Results: RS-3/5/6 drop to 0.2188 / 0.0819 / 0.0856 (all below the floor), and the axis stays honest on real irregularity (RS-3 keeps 0.2188 from a mid-line accidental). RS-2/RS-4 go vacuous-0.0 (their systems carry < 3 rhythmic columns - honestly "too little to measure," cleaner than the old furniture noise). RS-1 0.1341 -> 0.1967 (cross-barline note advances). The 1.0 anchor, orientation, range, tier thresholds, and the eight other axes are unchanged. This is measurement-only: the resolved layout, canonical bytes, render goldens, and ENGRAVER_VERSION are all untouched - only the reported spacing_distortion value moves. The duration-aware optical-spacing open question (deviation from duration-proportional spacing) stays open; it needs the pipeline's deferred duration-aware preferred widths. - catalog: spacing_distortion requirement (req:qmc:spacing) redefined over rhythmic columns, rationale + open-question note updated, version 0.2.0, revision-history row; PDF rebuilt clean (0 undefined refs). - engrave: quality::census rhythmic-column filter + is_rhythmic; module and spacing_raw docs; the floor-column contrast test re-pointed from b-flat's spacing (no longer warns) to RS-1's casting-off (still between the Standard 0.28 and Minimal 0.72 floors); new short_scores_do_not_trip_the_standard_ spacing_floor locks the fix. - QMC version breadcrumbs bumped to 0.2.0 (engrave + layout-ir quality.rs, both DECISIONS.md, testkit RS-1 comment); reference suite companion unchanged (cites the catalog by name, no pinned values). - process trail: PASS12_BATCH I12 struck; PASS12_RATIFICATION_LOG I12 section (Version movements: QMC 0.1.0 -> 0.2.0); engrave DECISIONS quality decision 8 + item 7 + candidate. 861 workspace tests pass; clippy -D warnings, fmt --check, rustdoc -D warnings, and the catalog PDF build all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
This commit is contained in:
parent
bc9fb27d36
commit
9d28dd6615
|
|
@ -259,7 +259,7 @@ resolved:
|
|||
|
||||
## Quality metrics (2026-07) — decisions
|
||||
|
||||
The Quality Metric Catalog companion (v0.1.0) ratified the nine normative
|
||||
The Quality Metric Catalog companion (v0.2.0) ratified the nine normative
|
||||
axes' formal definitions, anchors, thresholds, and the
|
||||
`QualityFloorApproached` trigger; `Engraver::resolve` now computes the real
|
||||
vector (the private `quality` module), replacing the all-worst placeholder.
|
||||
|
|
@ -336,11 +336,34 @@ testkit's reference-suite harness.
|
|||
documented xfail row in the testkit harness, P12-I11); the **v3**
|
||||
widow-rebalance phase (casting-off decision 9) evens the split to
|
||||
`casting_off` = 0.4463, clearing the miss with no catalog change, and the
|
||||
xfail row is promoted to a plain Pass. One finding the catalog's
|
||||
threshold-tuning open question anticipated remains (a Pass-12/QMC candidate
|
||||
below, P12-I12): `spacing_distortion` on 3–8-column entries (0.36–0.41) sits
|
||||
above the Standard column's 0.32 warning floor, so short scores warn under
|
||||
the default Standard profile — a *diagnostic* floor, not a Minimal failure.
|
||||
xfail row is promoted to a plain Pass. The second finding (P12-I12): three
|
||||
short entries measured `spacing_distortion` 0.36–0.41, above the Standard
|
||||
0.32 warning floor — a spurious *diagnostic* (never a Minimal failure). It
|
||||
was resolved by a catalog refinement (QMC 0.1.0 → 0.2.0), see quality
|
||||
decision 8: `spacing_distortion` is scoped to rhythmic (note/rest) columns,
|
||||
dropping the three to 0.2188 / 0.0819 / 0.0856 — below the floor, no code
|
||||
layout change.
|
||||
8. **Rhythmic-column spacing (`spacing_distortion` scoped) — the honest P12-I12
|
||||
fix.** The measured false positive was that a short healthy line's wide
|
||||
clef-to-first-note lead advance (furniture width, not note spacing) inflated
|
||||
the per-system advance CV above the Standard warning floor. The catalog
|
||||
(QMC 0.2.0) scopes the axis to **rhythmic columns** — spring slots bearing a
|
||||
notehead or rest — excluding the clef/key/time lead and treating barlines
|
||||
transparently (a note-to-note advance spans them). `quality::census` now
|
||||
builds `columns` only from slots in the precomputed rhythmic set
|
||||
(`is_rhythmic`: a `notehead*`/`rest*` glyph anywhere in the slot); the CV and
|
||||
contributing-unit rule (≥ 3 rhythmic columns) are otherwise unchanged. This
|
||||
is the mirror of the I11 resolution — measure the right thing rather than
|
||||
relax the threshold — but here the defect lived in the normative metric
|
||||
definition, so it *is* a catalog change (unlike I11). Measurement-only: the
|
||||
resolved layout, canonical bytes, render goldens, and `ENGRAVER_VERSION` are
|
||||
untouched; only the reported `spacing_distortion` value changes (RS-3/5/6
|
||||
drop below the floor, RS-2/RS-4 go vacuous-0.0 as their systems carry < 3
|
||||
rhythmic columns — honestly "too little to measure"). The floor-column
|
||||
regression test was re-pointed from b-flat's spacing (which no longer warns)
|
||||
to RS-1's casting-off (which still sits between the Standard and Minimal
|
||||
floors); a new `short_scores_do_not_trip_the_standard_spacing_floor` locks
|
||||
the fix. The duration-aware optical-spacing open question stays open.
|
||||
|
||||
### Pass 12 candidates (quality metrics)
|
||||
|
||||
|
|
@ -354,11 +377,14 @@ testkit's reference-suite harness.
|
|||
an RS-1 override) was deliberately **not** taken: the `0.5` anchor and the
|
||||
`0.90` Minimal column stood. `ENGRAVER_VERSION` 2 → 3; `ten_measure` render
|
||||
goldens regenerated.
|
||||
- **P12 (proposed) — QMC: the Standard spacing floor warns on short scores.**
|
||||
With uniform preferred widths, few-column systems (3–8 columns with a wide
|
||||
clef/key lead) measure spacing CV 0.36–0.41 — above the Standard column's
|
||||
0.8 × 0.40 = 0.32 warning floor, so the default profile emits
|
||||
`QualityFloorApproached(Spacing)` on tiny, healthy scores. Consider either
|
||||
a duration/lead-aware refinement of the axis (the catalog's optical-spacing
|
||||
open question) or excluding the lead column from the advance sequence in a
|
||||
QMC minor revision.
|
||||
- **P12-I12 — RESOLVED (QMC 0.2.0, rhythmic-column spacing).** With uniform
|
||||
preferred widths, few-column systems (3–8 columns with a wide clef/key lead)
|
||||
measured spacing CV 0.36–0.41 — above the Standard column's 0.8 × 0.40 = 0.32
|
||||
warning floor, so the default profile emitted `QualityFloorApproached(Spacing)`
|
||||
on tiny, healthy scores. Resolved the **lead-aware way**: the catalog scopes
|
||||
`spacing_distortion` to rhythmic (note/rest) columns, excluding the
|
||||
clef/key/time furniture lead and treating barlines transparently (quality
|
||||
decision 8). The three entries drop to 0.2188 / 0.0819 / 0.0856 (below the
|
||||
floor). The alternative — a duration-proportional (optical) redefinition —
|
||||
needs the pipeline's deferred duration-aware preferred widths and stays the
|
||||
catalog's open question.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! **Real quality-metric computation** — the nine normative axes of the
|
||||
//! *Quality Metric Catalog* companion (v0.1.0, Chapter 3), measured over what
|
||||
//! *Quality Metric Catalog* companion (v0.2.0, Chapter 3), measured over what
|
||||
//! the pipeline already produced: the resolved world-frame geometry
|
||||
//! ([`CastLayout`]), the constrained input (slot identity, vertical bands), and
|
||||
//! the declared page geometry. Normalization anchors, threshold tables, and the
|
||||
|
|
@ -20,9 +20,12 @@
|
|||
//! excluding same-slot pairs (slot identity = the glyph's
|
||||
//! `horizontal_slot` in the constrained input; the resolved glyph list is
|
||||
//! index-parallel to it) and strokes (not glyphs, never swept).
|
||||
//! * **`spacing_distortion`** — per-system column advances: the distinct
|
||||
//! resolved x of each glyph-bearing slot realized in the system (its first
|
||||
//! member's baseline — the spacing pass's own column reference).
|
||||
//! * **`spacing_distortion`** — per-system advances between **rhythmic**
|
||||
//! columns: the distinct resolved x of each note/rest-bearing slot realized
|
||||
//! in the system (its first member's baseline — the spacing pass's own column
|
||||
//! reference). The clef/key/time lead and barlines bear no notehead or rest,
|
||||
//! so they contribute no column and a note-to-note advance spans them
|
||||
//! (catalog §`spacing_distortion` — measuring rhythmic spacing, not furniture).
|
||||
//! * **`slur_shape_penalty` / `beam_slope_penalty`** — **vacuous 0.0**: the
|
||||
//! pipeline draws no slur or beam geometry (slurs/beams exist logically,
|
||||
//! not as curves/segments), so the contributing-unit sets are empty. The
|
||||
|
|
@ -108,14 +111,33 @@ struct SystemCensus {
|
|||
members: Vec<Vec<usize>>,
|
||||
/// Glyph-ink span `w_s` per system (0 for a glyph-less system).
|
||||
width: Vec<f64>,
|
||||
/// Column reference x per realized slot per system, ascending and distinct.
|
||||
/// Reference x of each **rhythmic** column (a slot bearing a notehead or
|
||||
/// rest) per system, ascending and distinct — the spacing axis's domain
|
||||
/// (catalog §`spacing_distortion`: the clef/key/time lead and barlines are
|
||||
/// furniture, not rhythmic columns).
|
||||
columns: Vec<Vec<f64>>,
|
||||
}
|
||||
|
||||
/// Whether a glyph is a notehead or a rest — the mark that makes its slot a
|
||||
/// **rhythmic column** (catalog §`spacing_distortion`).
|
||||
fn is_rhythmic(name: &str) -> bool {
|
||||
name.starts_with("notehead") || name.starts_with("rest")
|
||||
}
|
||||
|
||||
fn census(input: &ConstrainedLayoutIR, cast: &CastLayout) -> SystemCensus {
|
||||
let count = cast.region_of_system.len();
|
||||
let mut members: Vec<Vec<usize>> = vec![Vec::new(); count];
|
||||
let mut spans: Vec<Option<(f64, f64)>> = vec![None; count];
|
||||
// Rhythmic slots: those bearing a notehead or rest. Precomputed because a
|
||||
// slot's rhythmic status depends on *all* its members (a note's accidental
|
||||
// may precede its notehead in input order), and only rhythmic slots become
|
||||
// spacing columns (catalog §`spacing_distortion`, "rhythmic column").
|
||||
let rhythmic: BTreeSet<SpringSlotId> = input
|
||||
.glyphs
|
||||
.iter()
|
||||
.filter(|glyph| is_rhythmic(glyph.glyph.as_str()))
|
||||
.map(|glyph| glyph.horizontal_slot)
|
||||
.collect();
|
||||
// Column reference: the slot's first member (input order) — the same
|
||||
// convention the spacing and casting passes use for a slot's reference x.
|
||||
let mut columns: Vec<BTreeMap<SpringSlotId, f64>> = vec![BTreeMap::new(); count];
|
||||
|
|
@ -131,9 +153,13 @@ fn census(input: &ConstrainedLayoutIR, cast: &CastLayout) -> SystemCensus {
|
|||
Some((lo, hi)) => (lo.min(left), hi.max(right)),
|
||||
None => (left, right),
|
||||
});
|
||||
columns[system]
|
||||
.entry(glyph.horizontal_slot)
|
||||
.or_insert_with(|| f64::from(cast.glyphs[index].position.x.0));
|
||||
// Only rhythmic columns carry spacing advances; a note-to-note advance
|
||||
// spans any intervening barline or furniture (which contribute no column).
|
||||
if rhythmic.contains(&glyph.horizontal_slot) {
|
||||
columns[system]
|
||||
.entry(glyph.horizontal_slot)
|
||||
.or_insert_with(|| f64::from(cast.glyphs[index].position.x.0));
|
||||
}
|
||||
}
|
||||
let width = spans
|
||||
.iter()
|
||||
|
|
@ -196,7 +222,8 @@ fn collision_raw(input: &ConstrainedLayoutIR, cast: &CastLayout, census: &System
|
|||
}
|
||||
|
||||
/// `spacing_distortion` (catalog §`spacing_distortion`): mean per-system CV of
|
||||
/// column advances, over systems realizing at least three columns.
|
||||
/// **rhythmic** column advances, over systems realizing at least three rhythmic
|
||||
/// columns (note/rest-bearing slots — see [`census`]).
|
||||
fn spacing_raw(census: &SystemCensus) -> f64 {
|
||||
let mut per_system = Vec::new();
|
||||
for columns in &census.columns {
|
||||
|
|
@ -586,18 +613,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn floor_warnings_reference_the_profiles_threshold_column() {
|
||||
// The b-flat scale's spacing distortion (~0.41: eight columns whose
|
||||
// flat-bearing columns advance wider) sits between the Standard
|
||||
// column's floor (0.8 x 0.40 = 0.32) and the Minimal column's
|
||||
// (0.8 x 0.90 = 0.72) — so the default Standard profile warns about
|
||||
// Spacing and the Draft profile (which selects the Minimal column per
|
||||
// the catalog's profile registry) does not.
|
||||
let score = epiphany_testkit::corpus::corpus()
|
||||
.into_iter()
|
||||
.find(|fixture| fixture.name == "b_flat_major_scale")
|
||||
.expect("corpus entry exists");
|
||||
let input = to_constrained(&to_logical(&(score.build)()));
|
||||
let spacing_warned = |profile: SolverProfile| {
|
||||
// The ten-measure fixture's casting-off distortion (~0.45: the
|
||||
// six/four widow-rebalanced split) sits between the Standard column's
|
||||
// floor (0.8 x 0.35 = 0.28) and the Minimal column's (0.8 x 0.90 =
|
||||
// 0.72) — so the default Standard profile warns about CastingOff and
|
||||
// the Draft profile (which selects the Minimal column per the catalog's
|
||||
// profile registry) does not. (This is the profile-column contrast the
|
||||
// b-flat scale's spacing used to show, before spacing_distortion was
|
||||
// scoped to rhythmic columns and short scores stopped warning; the
|
||||
// casting-off axis carries the demonstration now.)
|
||||
let input = ten_measure();
|
||||
let castoff_warned = |profile: SolverProfile| {
|
||||
let config = SolverConfig {
|
||||
profile,
|
||||
..SolverConfig::default()
|
||||
|
|
@ -610,22 +636,55 @@ mod tests {
|
|||
matches!(
|
||||
w.kind,
|
||||
SolverWarningKind::QualityFloorApproached {
|
||||
metric: QualityMetricKind::Spacing
|
||||
metric: QualityMetricKind::CastingOff
|
||||
}
|
||||
)
|
||||
})
|
||||
};
|
||||
assert!(spacing_warned(SolverProfile::Standard));
|
||||
assert!(spacing_warned(SolverProfile::Publication));
|
||||
assert!(!spacing_warned(SolverProfile::Draft));
|
||||
assert!(castoff_warned(SolverProfile::Standard));
|
||||
assert!(castoff_warned(SolverProfile::Publication));
|
||||
assert!(!castoff_warned(SolverProfile::Draft));
|
||||
// The metric itself is profile-independent — only the diagnostic
|
||||
// column changes.
|
||||
let value = Engraver::default()
|
||||
.solve(&input, &SolverConfig::default())
|
||||
.metric_vector
|
||||
.spacing_distortion
|
||||
.casting_off_quality
|
||||
.0;
|
||||
assert!((0.32..=0.72).contains(&value), "spacing = {value}");
|
||||
assert!((0.28..=0.72).contains(&value), "casting_off = {value}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_scores_do_not_trip_the_standard_spacing_floor() {
|
||||
// P12-I12 regression. Before `spacing_distortion` was scoped to rhythmic
|
||||
// columns, a short healthy line's wide clef-to-first-note lead advance
|
||||
// inflated the CV above the Standard warning floor (0.8 x 0.40 = 0.32),
|
||||
// so these three-to-eight-column corpus entries spuriously warned
|
||||
// (measured 0.36-0.41). Scoped to note/rest columns — the clef/key/time
|
||||
// lead bears no notehead, so it contributes no column — none does.
|
||||
let floor = 0.8 * 0.40;
|
||||
for name in ["b_flat_major_scale", "notes_and_rests", "meter_three_four"] {
|
||||
let score = epiphany_testkit::corpus::corpus()
|
||||
.into_iter()
|
||||
.find(|fixture| fixture.name == name)
|
||||
.expect("corpus entry exists");
|
||||
let input = to_constrained(&to_logical(&(score.build)()));
|
||||
let report = Engraver::default().solve(&input, &SolverConfig::default());
|
||||
assert!(
|
||||
report.metric_vector.spacing_distortion.0 < floor,
|
||||
"{name}: spacing {} still above the Standard floor {floor}",
|
||||
report.metric_vector.spacing_distortion.0
|
||||
);
|
||||
assert!(
|
||||
!report.warnings.iter().any(|w| matches!(
|
||||
w.kind,
|
||||
SolverWarningKind::QualityFloorApproached {
|
||||
metric: QualityMetricKind::Spacing
|
||||
}
|
||||
)),
|
||||
"{name}: spurious spacing floor warning under the default Standard profile"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ object is covered); the provenance-preservation contract itself is unchanged.
|
|||
## Quality Metric Catalog constants (`src/quality.rs`, 2026-07)
|
||||
|
||||
**Decision: the catalog's normative constants live in this crate, as a pure
|
||||
transcription.** The Quality Metric Catalog companion (v0.1.0) pins the nine
|
||||
transcription.** The Quality Metric Catalog companion (v0.2.0) pins the nine
|
||||
axes' normalization anchors (`R_worst`), the clamped-linear normalization form
|
||||
`n = min(1, raw / R_worst)`, the Minimal/Standard threshold table, the
|
||||
`QualityFloorApproached` warning fraction (0.8), and the tier/profile →
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
//! The Quality Metric Catalog's normative constants (companion specification
|
||||
//! *Epiphany — Quality Metric Catalog*, v0.1.0): the per-axis normalization
|
||||
//! *Epiphany — Quality Metric Catalog*, v0.2.0): the per-axis normalization
|
||||
//! anchors, the per-tier metric threshold table, the profile→threshold-column
|
||||
//! mapping, and the `QualityFloorApproached` warning fraction.
|
||||
//! mapping, and the `QualityFloorApproached` warning fraction. (The v0.2.0
|
||||
//! `spacing_distortion` refinement scoped that axis's *measurement domain* to
|
||||
//! rhythmic columns; it touched no anchor or threshold, so this transcription
|
||||
//! of the numeric constants is unchanged.)
|
||||
//!
|
||||
//! This module is a **transcription**, not an invention: every number here is
|
||||
//! pinned by the catalog and cited to its chapter. Solvers that compute real
|
||||
|
|
@ -202,7 +205,7 @@ pub fn tier_thresholds(tier: SolverTier) -> Option<&'static MetricThresholds> {
|
|||
/// The threshold column a **registered profile** selects (catalog Chapter 6,
|
||||
/// `req:qmc:profiles`): `Draft` → the Minimal column (few warnings, fast
|
||||
/// iteration); `Standard` and `Publication` → the Standard column (no column
|
||||
/// tighter than Standard is ratified in v0.1). This is the column the solver's
|
||||
/// tighter than Standard is ratified as of v0.2). This is the column the solver's
|
||||
/// own `QualityFloorApproached` diagnostics reference during ordinary solves;
|
||||
/// suite evaluation at a claimed tier always uses that *tier's* column
|
||||
/// ([`tier_thresholds`]).
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ pub fn entries() -> Vec<SuiteEntry> {
|
|||
construction: "fixtures::ten_measure_single_staff(0x000A_11CE)",
|
||||
build: rs1,
|
||||
// Passes every Minimal threshold under the reference engraver
|
||||
// (engrave v3, QMC v0.1.0 anchors). Greedy first-fit alone left a
|
||||
// (engrave v3, QMC v0.2.0 anchors). Greedy first-fit alone left a
|
||||
// two-measure stub last system (width CV 0.61 -> clamped 1.0 > the
|
||||
// 0.90 threshold, the original P12-I11 miss); casting-off's
|
||||
// widow-rebalance phase now evens the split to a six/four
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ code instead is the failure mode this batch exists to prevent.
|
|||
| P12-I9 | `epiphany-layout-ir` I | Honouring a user break must attribute the decision to its override (`DecisionSource::UserOverride(id)`), but constraints carry no override identity; implemented via a `ConstrainedLayoutIR.break_origins` sidecar populated by `to_constrained`. Bless the sidecar or widen the normalized constraint record. | G / Pass 12 (solver) |
|
||||
| P12-I10 | `epiphany-layout-ir` I | System-spanning strokes split at system boundaries need synthesized provenance for continuation segments; implemented as `SynthesisKind::Registered(SYSTEM_CONTINUATION_SYNTHESIS)` with a deterministic `(original, ordinal)` instance key. Add a first-class continuation synthesis kind or bless the registered id. | G / Pass 12 (provenance) |
|
||||
| ~~P12-I11~~ **RESOLVED (engrave v3 widow-rebalance)** | `epiphany-engrave` I | RS-1 honestly failed the Minimal casting-off threshold under the reference engraver (measured 1.0 vs 0.90): greedy first-fit left a two-measure stub last system (width CV 0.6145 ≥ the 0.5 anchor). **Resolved the honest way — an engrave-side casting-off balance pass, no catalog change:** casting-off gained a second **widow-rebalance** phase that moves whole trailing measures from a region's penultimate system into its final one, choosing the shift that minimizes the larger of the two distribution penalties the catalog defines for the break family (width imbalance vs non-final underfill; both share the 0.5 anchor). RS-1 now casts six/four instead of eight/two: casting_off 1.0 → **0.4463**, system_break 0.254 → 0.677, every axis ≤ 0.90. `ENGRAVER_VERSION` 2 → 3; render goldens regenerated; the suite's Xfail row promoted to a plain Pass. The `casting_off` 0.5 anchor and the Minimal column stood — the engraver improved, not the threshold — so the Quality Metric Catalog is normatively unchanged. | ✅ done |
|
||||
| P12-I12 | `epiphany-engrave` I | The Standard-tier spacing floor warns on short healthy scores: 3–8-column entries with a wide clef/key lead measure spacing CV 0.36–0.41 > the 0.32 Standard floor. Consider a lead-aware or duration-aware refinement of the spacing_distortion raw measurement (the catalog's optical-spacing open question). | G / Pass 12 (quality) |
|
||||
| ~~P12-I12~~ **RESOLVED (QMC 0.2.0: rhythmic-column spacing)** | `epiphany-engrave` I | The Standard-tier spacing floor warned on short healthy scores: 3–8-column entries with a wide clef/key lead measured spacing CV 0.36–0.41 > the 0.32 Standard floor (a spurious *diagnostic* — the scores all passed Minimal). **Resolved by the lead-aware refinement (Quality Metric Catalog 0.1.0 → 0.2.0):** `spacing_distortion` is now scoped to the system's **rhythmic columns** (slots bearing a notehead or rest), excluding the clef / key-signature / time-signature lead and treating barlines transparently (a note-to-note advance spans them) — so the furniture-width lead gap no longer inflates the CV. The three entries drop to 0.2188 / 0.0819 / 0.0856 (all below the 0.32 floor), and the axis stays honest on real irregularity (RS-3 keeps 0.2188 from a mid-line accidental). Anchor, orientation, range, thresholds, and the eight other axes unchanged; no layout/golden change (measurement-only, `ENGRAVER_VERSION` untouched). The **duration-aware optical-spacing** open question (deviation from duration-proportional spacing) stays open — it needs the pipeline's deferred duration-proportional preferred widths. | ✅ done |
|
||||
|
||||
|
||||
## Not yet open elsewhere
|
||||
|
|
@ -80,10 +80,12 @@ work added C1..C4 (re-anchoring), D1 (bundle operation index), and E1..E5
|
|||
value-restoring undo, 2026-07-02) added C5, K8..K11, and I7..I10.
|
||||
The second tranche (Quality Metric Catalog 0.1.0 + Reference Suite 0.1.0 +
|
||||
real engraver metrics + the suite harness + the multi-system click fix,
|
||||
2026-07-03) added I11..I12. A follow-up (2026-07-03) resolved I11 (struck
|
||||
through above): casting-off gained a widow-rebalance phase (engrave v3), so
|
||||
RS-1 passes every Minimal threshold with no catalog change — I12 (the
|
||||
Standard-tier spacing floor on short scores) stays open.
|
||||
2026-07-03) added I11..I12. Follow-ups (2026-07-03) resolved both (struck
|
||||
through above): I11 by an engrave-side casting-off widow-rebalance phase
|
||||
(engrave v3, no catalog change), and I12 by scoping `spacing_distortion` to
|
||||
rhythmic columns (Quality Metric Catalog 0.1.0 → 0.2.0). The only remaining
|
||||
spacing follow-up is the catalog's duration-aware optical-spacing open
|
||||
question, which lives in the companion, not as a batch row.
|
||||
Agent J's Binary Format companion now exists
|
||||
(`spec/binary_format.tex`, v0.1.0): it ratified the P12-D1/E1/E2/E3 inputs
|
||||
(struck through above) and discharged the crates' provisional-codec notes
|
||||
|
|
|
|||
|
|
@ -60,3 +60,17 @@ record that the Quality Metric Catalog and the core spec were **not** changed.
|
|||
| Item | Disposition | Spec locus | Consumer |
|
||||
|---|---|---|---|
|
||||
| P12-I11 casting-off stub last system | **no spec change (engrave fix)** — casting-off gained a second **widow-rebalance** phase (`epiphany-engrave` v3): it moves whole trailing measures from a region's penultimate system into its final one, choosing the shift that minimizes the larger of the two distribution penalties the catalog already defines for the break family (`casting_off_quality` width imbalance vs `system_break_penalty` non-final underfill; both share the 0.5 anchor). RS-1 casts six/four instead of eight/two — `casting_off` 1.0 → 0.4463, every axis ≤ 0.90 — so the suite's asserted Xfail row is promoted to a plain Pass. This is the deliberately-chosen *honest* resolution: the `casting_off` 0.5 anchor and the Minimal 0.90 column were **vindicated, not relaxed** (the engraver improved, no anchor rescale / threshold loosening / RS-1 per-entry override). Core spec Chapter 9's "Minimal makes no optimality claim" already permits the heuristic; nothing normative changed | — (no spec locus; core spec + Quality Metric Catalog unchanged) | `epiphany-engrave` (`casting::rebalance_widows`, `ENGRAVER_VERSION` 3); `epiphany-testkit` (RS-1 xfail row removed); render goldens regenerated |
|
||||
|
||||
## P12-I12 resolution (2026-07-03) — Quality Metric Catalog 0.1.0 → 0.2.0
|
||||
|
||||
Unlike P12-I11, the second measured finding's honest fix is a **catalog
|
||||
change**: the defect was in the metric's own definition, not the engraver.
|
||||
|
||||
| Item | Disposition | Spec locus | Consumer |
|
||||
|---|---|---|---|
|
||||
| P12-I12 spacing floor on short scores | **fixed (metric measurement domain)** — `spacing_distortion`'s raw measurement is scoped to the system's **rhythmic columns** (spring slots bearing a notehead or rest); the clef / key-signature / time-signature lead and barlines contribute no column, so a note-to-note advance spans them. Rationale: the leading clef-to-first-note gap is furniture width, not note spacing, and folding it into the CV flagged perfectly-spaced short lines as distorted purely for carrying a clef. The three warning entries drop from 0.36–0.41 to 0.2188 / 0.0819 / 0.0856 (below the Standard 0.32 floor), and the axis stays live on real irregularity. The `1.0` anchor, orientation, range, tier thresholds, and the eight other axes are **unchanged**; this is measurement-only — no layout, canonical bytes, render golden, or `ENGRAVER_VERSION` change. Catalog **0.1.0 → 0.2.0** (a normative refinement of one axis's measurement domain). The duration-aware optical-spacing open question stays open | quality\_metric\_catalog §`spacing_distortion` (`req:qmc:spacing`); revision-history row 0.2.0 | `epiphany-engrave` (`quality::census` rhythmic-column filter, `is_rhythmic`); `epiphany-testkit` (RS-3/5/6 measured values drop, still Pass) |
|
||||
|
||||
**Version movements.** Quality Metric Catalog 0.1.0 → 0.2.0
|
||||
(`spacing_distortion` scoped to rhythmic columns). Core spec unchanged (the
|
||||
axis's formal definition is delegated to the catalog; Chapter 9 carries only
|
||||
the field, which is unchanged).
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -226,7 +226,7 @@
|
|||
{\Large\scshape\color{epiphanyslate}Quality Metric Catalog}\\[6pt]
|
||||
{\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt]
|
||||
{\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt]
|
||||
{\normalsize\color{epiphanyink}Version 0.1.0 --- Phase 3 (the normative metric set: formal definitions, normalization, weights, tier thresholds, profile registry)}\\[4pt]
|
||||
{\normalsize\color{epiphanyink}Version 0.2.0 --- Phase 3 (the normative metric set: formal definitions, normalization, weights, tier thresholds, profile registry; \texttt{spacing\_distortion} scoped to rhythmic columns)}\\[4pt]
|
||||
{\small\color{epiphanyslate}Normative for the metrics and thresholds it defines}
|
||||
\vfill
|
||||
\end{titlepage}
|
||||
|
|
@ -246,7 +246,7 @@ deliver ``per-metric normalization functions mapping raw measurements to
|
|||
metric thresholds, and the formal definition of each quality metric in the
|
||||
normative metric set.''
|
||||
|
||||
This release (v0.1.0) delivers all four chartered items, plus two small
|
||||
This companion (v0.2.0) delivers all four chartered items, plus two small
|
||||
registries the core specification names but defers here:
|
||||
|
||||
\begin{itemize}
|
||||
|
|
@ -721,44 +721,62 @@ the resolved layout already carries.
|
|||
\section{\texttt{spacing\_distortion}}
|
||||
\label{sec:metrics:spacing}
|
||||
|
||||
\textbf{Phenomenon.} Uneven horizontal distribution within a system:
|
||||
columns bunched together here and stretched apart there, where the
|
||||
underlying spring model asked for near-uniform advances.
|
||||
\textbf{Phenomenon.} Uneven horizontal distribution within a system: the
|
||||
\emph{rhythmic} columns --- those carrying notes and rests --- bunched
|
||||
together here and stretched apart there, where the underlying spring model
|
||||
asked for near-uniform advances.
|
||||
|
||||
\begin{requirement}
|
||||
\label{req:qmc:spacing}
|
||||
\textbf{Contributing units:} systems $s$ with $m_s \ge 3$ (at least two
|
||||
advances).
|
||||
\textbf{Rhythmic columns.} A \emph{rhythmic column} of a system is a
|
||||
horizontal spring slot at least one of whose glyphs is a notehead or a
|
||||
rest. The clef, key-signature, and time-signature lead and the barlines are
|
||||
\textbf{not} rhythmic columns: their horizontal extent is notational
|
||||
furniture, fixed by their content rather than by rhythm.
|
||||
|
||||
\textbf{Raw measurement:} per unit,
|
||||
$\mathit{raw}_s = \mathrm{CV}(a^s_1, \dots, a^s_{m_s - 1})$; the axis raw
|
||||
value is the arithmetic mean of $\mathit{raw}_s$ over contributing units.
|
||||
\textbf{Contributing units:} systems $s$ with at least three rhythmic
|
||||
columns (so at least two rhythmic advances).
|
||||
|
||||
\textbf{Raw measurement:} let $x^s_1 < \dots < x^s_{k_s}$ be the reference
|
||||
x-positions of the system's $k_s$ rhythmic columns in order, and
|
||||
$a^s_i = x^s_{i+1} - x^s_i$ the advance between consecutive rhythmic columns
|
||||
(a note-to-note advance spans any barline or furniture that falls between
|
||||
the two, so barlines are transparent, not breaks in the sequence). Then
|
||||
$\mathit{raw}_s = \mathrm{CV}(a^s_1, \dots, a^s_{k_s - 1})$, and the axis
|
||||
raw value is the arithmetic mean of $\mathit{raw}_s$ over contributing
|
||||
units.
|
||||
|
||||
\textbf{Normalization:} $R_{\mathrm{worst}} = 1.0$;
|
||||
$n = \min(1, \mathit{raw})$.
|
||||
\end{requirement}
|
||||
|
||||
\begin{rationale}
|
||||
A coefficient of variation of $1.0$ means the typical column advance
|
||||
deviates from the mean by the whole mean --- spacing with no discernible
|
||||
regularity. v0.1 defines \emph{geometric} regularity deliberately: the
|
||||
reference spring model's preferred widths are uniform, so regular advances
|
||||
are exactly what its ideal output looks like, and the collision minima
|
||||
(accidental overhangs, wide columns) that legitimately perturb advances
|
||||
are modest on realistic scores.
|
||||
A coefficient of variation of $1.0$ means the typical advance deviates from
|
||||
the mean by the whole mean --- spacing with no discernible regularity. v0.2
|
||||
defines \emph{geometric} regularity over the \emph{rhythmic} columns
|
||||
deliberately: the reference spring model's preferred widths are uniform, so
|
||||
regular note/rest advances are exactly what its ideal output looks like,
|
||||
and the collision minima (accidental overhangs, wide columns) that
|
||||
legitimately perturb those advances are modest on realistic scores. The
|
||||
axis is scoped to rhythmic columns because a leading clef, key signature,
|
||||
or time signature is furniture whose width the spring model does not govern:
|
||||
folding the wide clef-to-first-note gap into the CV would flag a
|
||||
perfectly-spaced short line as distorted purely for carrying a clef ---
|
||||
measuring furniture, not spacing.
|
||||
\end{rationale}
|
||||
|
||||
\begin{openquestion}
|
||||
\textbf{Optical spacing at the Standard tier.} Mature engraving spaces
|
||||
columns proportionally to musical duration (with an optical correction),
|
||||
not uniformly; under a duration-proportional model, this axis's ideal
|
||||
would be ``advances proportional to the column's duration share,'' and a
|
||||
perfectly optically-spaced line would score \emph{worse} than a uniform
|
||||
one under the v0.1 definition. When the layout pipeline gains
|
||||
duration-aware preferred widths, should the Standard tier redefine
|
||||
rhythmic columns proportionally to musical duration (with an optical
|
||||
correction), not uniformly; under a duration-proportional model, this
|
||||
axis's ideal would be ``advances proportional to the column's duration
|
||||
share,'' and a perfectly optically-spaced line would score \emph{worse}
|
||||
than a uniform one under the v0.2 definition. When the layout pipeline
|
||||
gains duration-aware preferred widths, should the Standard tier redefine
|
||||
$\mathit{raw}_s$ as deviation from the duration-proportional ideal while
|
||||
Minimal keeps geometric regularity? v0.1 defines geometric regularity
|
||||
only.
|
||||
Minimal keeps geometric regularity? v0.2 scopes the geometric definition to
|
||||
rhythmic columns (removing the leading-furniture false positive on short
|
||||
scores) but still measures geometric, not duration-proportional, regularity.
|
||||
\end{openquestion}
|
||||
|
||||
\section{\texttt{slur\_shape\_penalty}}
|
||||
|
|
@ -1249,7 +1267,7 @@ selections:
|
|||
fraction is evaluated during ordinary solves. \texttt{Draft} selects
|
||||
the Minimal column (few warnings, fast iteration);
|
||||
\texttt{Standard} and \texttt{Publication} select the Standard
|
||||
column. In v0.1 no column tighter than Standard is ratified;
|
||||
column. As of v0.2 no column tighter than Standard is ratified;
|
||||
\texttt{Publication} is registered now so that documents and
|
||||
configurations can name it, and a future revision \MAY{} give it a
|
||||
tighter column without a schema change.
|
||||
|
|
@ -1302,6 +1320,18 @@ than inventing a parallel one.
|
|||
Open questions: the notated-but-unrendered honesty edge, optical
|
||||
spacing at the Standard tier, threshold tuning pending reference-suite
|
||||
experience. \\
|
||||
\today & \hyperref[sec:metrics:spacing]{\texttt{spacing\_distortion}}
|
||||
& 0.2.0 --- Scope the \texttt{spacing\_distortion} raw measurement to the
|
||||
system's \emph{rhythmic} columns (slots bearing a notehead or rest),
|
||||
excluding the clef / key-signature / time-signature lead and treating
|
||||
barlines transparently (a note-to-note advance spans them). Resolves the
|
||||
reference-suite false positive in which a short healthy line's wide
|
||||
clef-to-first-note gap inflated the CV above the Standard warning floor
|
||||
(measured 0.36--0.41 on the three- to eight-column entries $\to$
|
||||
0.08--0.22, below the floor) without weakening the axis on real spacing
|
||||
irregularity. Normalization anchor, orientation, range, thresholds, and
|
||||
the eight other axes are unchanged; the optical-spacing open question
|
||||
(duration-proportional spacing) stays open. Batch item P12-I12. \\
|
||||
\bottomrule
|
||||
\end{longtable}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue