Skip to content

fix: preserve staff-scoped staff-layout number="N" - #326

Merged
webern merged 3 commits into
mainfrom
fix-staff-layout-number
Jul 12, 2026
Merged

fix: preserve staff-scoped staff-layout number="N"#326
webern merged 3 commits into
mainfrom
fix-staff-layout-number

Conversation

@webern

@webern webern commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Human Summary

TODO: human writes here

Summary

Both the per-measure <print> and score-level <defaults> readers took only the first
<staff-layout>'s distance and dropped its number attribute entirely, folding it into the single
unscoped SystemLayoutData::staffDistance. For a multi-staff part (e.g. a 2-staff piano/organ)
whose only staff-layout is scoped to staff 2, this silently misattributed the distance to staff 1
(the schema default for a missing number) instead of just dropping the attribute -- a real,
if quiet, correctness bug, not only a fidelity loss.

Added SystemLayoutData::staffDistances, a std::map<int, double> keyed by zero-based staff
index, holding spacing for staves that need their own value; the existing singular
staffDistance keeps its meaning for the common unscoped case, and for a given staff a map entry
wins. The map shape follows the MeasureData::staffTimeSignatures precedent for staff-scoped
number="N" overrides -- key by the owning staff rather than stamping a label field on the items
-- which also makes two conflicting distances for the same staff unrepresentable. Both readers
(ScoreReader::scanForSystemInfo, LayoutFunctions::addStaffLayout) and both writers
(MeasureWriter::writeSystemInfo, LayoutFunctions::addSystemMargins) handle it symmetrically,
since <defaults> and per-measure <print> already share SystemLayoutData. Header comments are
written for the api user (what the fields mean and when to set them) per the design doctrine,
rather than describing reader/writer behavior.

Testing

  • New staffScopedStaffDistance test (PrintLayoutRoundTripTest.cpp)
  • make test: all pass (4808 assertions in 391 test cases, plus the three examples)
  • make test-api-roundtrip: 164 passed, 0 failed (of 164 pinned; no regressions)
  • make fmt / make check: clean

References

@webern webern added bug software defect non-breaking fixes or implementation that do not require breaking changes api Affects the mx::api layer impl Affects the mx::impl layer ai Issues opened by, or through, a coding agent. labels Jul 7, 2026 — with Claude
@github-actions

github-actions Bot commented Jul 7, 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 80.7% 6401 / 7933
Functions 67.7% 2217 / 3275
Branches 49.5% 5496 / 11100

Core HTML report | API HTML report

Commit 798455f0292544538e266fed9bfd514f07e9cb02.

@github-actions

github-actions Bot commented Jul 7, 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 798455f0292544538e266fed9bfd514f07e9cb02.

@webern
webern force-pushed the fix-staff-layout-number branch from c48404d to beefd31 Compare July 7, 2026 09:23
@webern webern changed the title fix: preserve staff-scoped &lt;staff-layout number="N"&gt; fix: preserve staff-scoped staff-layout number="N" Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 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 80.8% 6434 / 7963
Functions 67.8% 2221 / 3276
Branches 49.6% 5524 / 11148

Core HTML report | API HTML report

Commit 1b8a0ab9ac2c26582882e07d4be01984f5f5b484.

@github-actions

github-actions Bot commented Jul 7, 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 1b8a0ab9ac2c26582882e07d4be01984f5f5b484.

Comment thread src/include/mx/api/SystemLayoutData.h Outdated
Both the per-measure <print> and score-level <defaults> readers took only
the first <staff-layout>'s distance and dropped its number attribute
entirely, folding it into the single unscoped SystemLayoutData::staffDistance.
For a multi-staff part (e.g. a 2-staff piano/organ) whose only staff-layout
is scoped to staff 2, this silently misattributed the distance to staff 1
(the schema default for a missing number) instead of dropping the number.

Added SystemLayoutData::staffDistances (vector of new StaffDistanceData:
staffIndex + distance), populated only when the source's <staff-layout> had
an explicit number attribute; the existing singular staffDistance keeps its
meaning for the common unscoped case. Both readers (ScoreReader::
scanForSystemInfo, LayoutFunctions::addStaffLayout) and both writers
(MeasureWriter::writeSystemInfo, LayoutFunctions::addSystemMargins) handle
it symmetrically since defaults and per-measure print share SystemLayoutData.

## Testing

- [x] New `staffScopedStaffDistance` test (PrintLayoutRoundTripTest.cpp)
- [x] make test: all pass (4729 assertions in 379 test cases, plus examples)
- [x] make test-api-roundtrip: 158 passed, 0 failed (of 158 pinned; no
  regressions -- this fix corrects a wrong-staff misattribution rather than
  unlocking a new fully-passing file)
- [x] make fmt / make check: clean

Closes #281
@webern
webern force-pushed the fix-staff-layout-number branch from beefd31 to 2e7307a Compare July 11, 2026 14:07
@github-actions

Copy link
Copy Markdown

Coverage report

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

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36640
Functions 74.3% 6352 / 8552
Branches 50.7% 22680 / 44751

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

Metric Coverage Covered / Total
Lines 81.0% 6567 / 8111
Functions 68.0% 2249 / 3306
Branches 49.7% 5646 / 11369

Core HTML report | API HTML report

Commit 4d5d770eb8f65c58d321191a2c1d52c5ccc557ea.

@github-actions

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 4d5d770eb8f65c58d321191a2c1d52c5ccc557ea.

webern added a commit that referenced this pull request Jul 12, 2026
## Human Summary

I asked Fable, while I still have access to it, to improve Agent
performance with better instructions putting them in places more likely
to be read and incorporating some of my latest pet-peeves.

## Summary

Agents were not reading docs/ai/design/api-design-principles.md, and
several recurring review
themes were written down nowhere. This PR moves the doctrine to where
agents actually look and
captures the missing rules, without growing AGENTS.md (it shrinks by 8
lines).

- New mx-api-doctrine skill whose description says it MUST be read
before designing or changing
anything in the api layer. It contains the seven design principles
(moved verbatim from the
  deleted doc) plus the previously unwritten doctrine:
- Audience: the api is for people creating music and building,
modifying, and interpreting
MusicXML files. Round-tripping is how we develop the api, not what it is
for. Header comments
explain notation semantics and how MusicXML encodes them;
"source"/"preserved" framing is
allowed only on true fidelity knobs. No documenting what is not modeled,
no banner comments
    (from review feedback on #326, #333, #321).
- Failure model: misuse is made unrepresentable or degrades to a
documented fallback. UB must be
unreachable from the public interface, nothing throws across the api
boundary (never for a
failed precondition), and Result stays quarantined at the
DocumentManager I/O boundary.
- Choice types: the TimeChoice / MarkDataChoice pattern for either/or
payloads (copy-returning
accessors, default-constructed wrong-kind fallback, auto-collapse,
common case kept prominent
    in its own header), from the #321 redesign and the #330 review.
- AGENTS.md digest replaced with a pointer to the skill; the add-feature
skill and the Copilot
api-headers review instructions repointed and extended with the same
rules.
- Deleted the mx-architecture skill: generic architect boilerplate that
wasn't helping. Its few
mx-specific facts live elsewhere (gen design in gen/DESIGN.md; the
error-handling stance is now
  concrete doctrine in the new skill).

Net -33 lines repo-wide.

## Testing

- [x] No dangling references: grep for api-design-principles and
mx-architecture is clean
- [x] Docs/skills only; no C++ or build changes, so no test suites apply

## References

- Related: #292 (created the principles doc and the AGENTS.md digest
this replaces)
- Captures review feedback from #321, #326, #330, #333
Replace the round-trip-harness viewpoint ('a source with an explicit
number attribute always populates...') with musician-facing comments
that say what the fields mean and when an author should use each one.
@github-actions

Copy link
Copy Markdown

Coverage report

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

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36644
Functions 74.3% 6352 / 8554
Branches 50.7% 22680 / 44751

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

Metric Coverage Covered / Total
Lines 81.5% 6890 / 8457
Functions 68.6% 2296 / 3346
Branches 50.1% 5959 / 11905

Core HTML report | API HTML report

Commit a1bf5284132e7e65362c910890fe3f28c4e62096.

@github-actions

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 a1bf5284132e7e65362c910890fe3f28c4e62096.

Replace std::vector<StaffDistanceData> with std::map<int, double>,
following the MeasureData::staffTimeSignatures precedent for
staff-scoped number="N" overrides: key the map by the owner instead
of stamping a label field on the item. Two conflicting distances for
the same staff are now unrepresentable, and the StaffDistanceData
class (with its sentinel-value constructor defaults) is deleted.
@github-actions

Copy link
Copy Markdown

Coverage report

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

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36644
Functions 74.3% 6352 / 8554
Branches 50.7% 22680 / 44751

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

Metric Coverage Covered / Total
Lines 82.0% 6950 / 8472
Functions 68.9% 2307 / 3347
Branches 50.7% 6041 / 11923

Core HTML report | API HTML report

Commit b518bd703adb9918b936c405e13c5307609a5ec9.

@github-actions

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 b518bd703adb9918b936c405e13c5307609a5ec9.

@webern
webern merged commit 30d0239 into main Jul 12, 2026
10 checks passed
@webern
webern deleted the fix-staff-layout-number branch July 12, 2026 14:36
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. api Affects the mx::api layer bug software defect impl Affects the mx::impl layer non-breaking fixes or implementation that do not require breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api: round-trip drops staff-layout

1 participant