Skip to content

feat: support cross-staff notes in the api#304

Open
webern wants to merge 1 commit into
mainfrom
claude/cross-staff-notes-design-ekrd9r
Open

feat: support cross-staff notes in the api#304
webern wants to merge 1 commit into
mainfrom
claude/cross-staff-notes-design-ekrd9r

Conversation

@webern

@webern webern commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Human Summary

This seems fairly clean. If the note is to be displayed on a different staff from the one it is contained in, populate the std::optional crossStaffIndex.

Summary

Cross-staff notes (a beamed run or chord that dips onto the other staff of a multi-staff part) could not be created through the api, and reading one split the beam group across staff buckets, so the writer reordered the measure and broke beam adjacency.

NoteData::crossStaffIndex (std::optional<int>) is the zero-based index of the staff on which the note is displayed, for the rare case that it differs from the staff that contains the note. Containment still governs voice membership, timing, and beam adjacency; the writer emits the override in the note's staff element instead of the containing staff's number. Authors set one field; everything else is unchanged.

On the read side, a per-measure pre-scan decides which notes are cross-staff before any bucketing happens:

  • A level-1 beam group that properly closes and names more than one staff value is filed, whole, into its voice's home staff - the staff the voice was on when the group began - and each member whose staff value diverges from that home gets crossStaffIndex. This handles groups that begin on the foreign staff (ly43d measure 1).
  • A chord member always follows the bucket of the note it chords with, so a chord can no longer be split across staves (ly43d measure 2 has chords with members on both staves).
  • A beam group that stays on one staff never relocates, and single-staff parts skip the scan entirely, so behavior for non-crossing files is unchanged.

The design follows the containment principle rather than adding a second source of truth for ownership: the field overrides only where the note is displayed, never which staff/voice owns it. Also removed a long-dead commented-out staffIndex line in NoteFunctions.cpp that this feature supersedes.

Testing

  • New CrossStaffApiTest.cpp: authoring writes the override with beam adjacency intact (no forward, single backup); round-trip preserves crossStaffIndex; reader relocates the ly43d sticky-home shape and cross-staff chords; unbeamed staff changes and pure foreign-staff groups still bucket structurally with no override
  • Full unit suite passes (4622 assertions in 363 test cases)
  • lysuite/ly43d_MultiStaff_StaffChange.xml now round-trips byte-faithfully and is pinned in roundtrip-baseline.txt
  • Regression gate passes 125/125 pinned files
  • Discovery over the full corpus: 140 -> 141 PASS, zero newly-failing files

References

Closes #289. A cross-staff note (a beamed run or chord that dips onto the
other staff of a multi-staff part) could not be created through the api,
and reading one split the beam group across staff buckets so the writer
reordered the measure and broke beam adjacency.

NoteData::crossStaffIndex (std::optional<int>) is the zero-based index of
the staff on which the note is displayed when it differs from the staff
that contains the note. Containment still governs voice membership,
timing, and beam adjacency; the writer emits the override in the note's
<staff> element instead of the containing staff's number.

The reader detects cross-staff notation with a per-measure pre-scan: a
level-1 beam group that properly closes and names more than one <staff>
value is filed into its voice's home staff (the staff the voice was on
when the group began), and each divergent member gets crossStaffIndex.
A <chord> member always follows the bucket of the note it chords with, so
a chord can no longer be split across staves. Beam groups that stay on
one staff never relocate, so behavior for non-crossing files is unchanged.

lysuite/ly43d_MultiStaff_StaffChange.xml now round-trips byte-faithfully
and is pinned in roundtrip-baseline.txt (125 pinned). Discovery over the
full corpus: 140 -> 141 PASS with zero newly-failing files. Unit suite:
363 cases, 4622 assertions, all passing.
@webern webern added feature new feature request non-breaking fixes or implementation that do not require breaking changes area/mx::api area/mx::impl ai Issues opened by, or through, a coding agent. labels Jul 5, 2026 — with Claude
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Coverage report

Core-dev coverage src/private/mx/core/

Metric Coverage Covered / Total
Lines 77.8% 28513 / 36640
Functions 74.3% 6352 / 8551
Branches 50.7% 22678 / 44751

API coverage src/private/mx/{api,impl,utility}/

Metric Coverage Covered / Total
Lines 79.1% 6215 / 7856
Functions 64.3% 2095 / 3256
Branches 48.3% 5312 / 10997

Core HTML report | API HTML report

Commit 1740e3ae6c541dc639ab39d16a75257cd49cad76.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

gen-quality gen/

gen-quality: 84.5 / 100   (floor 84.5, +0.0)

  structure     86.5  x0.50   [fn 90.5 / file 82.6]
  cyclomatic    88.4  x0.25
  cognitive     76.6  x0.25

  409 functions across 31 files, 7702 lines (largest file 1044)
  max cc 56  max cognitive 44  max fn loc 152

Worst offenders (top 5 per axis; full lists in score.json):
  cyclomatic gen/xsd/analyze.py:311     report                             56
  cyclomatic gen/plates/build.py:956    _validate_config_against_ir        35
  cyclomatic gen/press/context.py:145   plate_context                      34
  cyclomatic gen/__main__.py:46         _ir                                23
  cyclomatic gen/tests/test_ir.py:102   _check_references                  20
  cognitive  gen/xsd/analyze.py:311     report                             44
  cognitive  gen/ir/resolve.py:119      flat_elements                      40
  cognitive  gen/tests/test_ir.py:102   _check_references                  38
  cognitive  gen/press/context.py:145   plate_context                      37
  cognitive  gen/xsd/analyze.py:207     _sccs                              37
  size       gen/xsd/analyze.py:311     report                             152
  size       gen/press/context.py:145   plate_context                      96
  size       gen/plates/build.py:533    _value_plate                       89
  size       gen/plates/build.py:956    _validate_config_against_ir        89
  size       gen/ir/resolve.py:119      flat_elements                      78

Commit 1740e3ae6c541dc639ab39d16a75257cd49cad76.

@webern webern marked this pull request as ready for review July 5, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Issues opened by, or through, a coding agent. area/mx::api area/mx::impl feature new feature request non-breaking fixes or implementation that do not require breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for cross staff notes in API

1 participant