Skip to content

feat: round-trip arpeggiate and non-arpeggiate attributes#340

Merged
webern merged 3 commits into
mainfrom
claude/arpeggiate-attributes
Jul 12, 2026
Merged

feat: round-trip arpeggiate and non-arpeggiate attributes#340
webern merged 3 commits into
mainfrom
claude/arpeggiate-attributes

Conversation

@webern

@webern webern commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Human Summary

It looks like the agent followed the pattern I established with tremolo marks in MarkDataChoice correctly and thus this follows the api standard, such as it is, and adds a couple of missing features.

Summary

Arpeggiate marks already round-tripped their direction (encoded in
MarkType::arpeggiate/arpeggiateUp/arpeggiateDown) and shared position attributes, but dropped
number, unbroken (4.0), and id (3.1); non-arpeggiate additionally always wrote
type="top", losing the bottom end of the bracket.

Following the TremoloMarkData precedent, this adds two MarkDataChoice alternatives:

  • ArpeggiateMarkData: number (optional), unbroken (Bool), id (optional)
  • NonArpeggiateMarkData: placement (NonArpeggiatePlacement::top/bottom, the required
    MusicXML type attribute), number (optional), id (optional)

wired through ArpeggiateFunctions/NonArpeggiateFunctions (read) and NotationsWriter
(write). Authoring without the payload behaves exactly as before.

Stacked on #337 (the arpeggiate corpus fixtures also needed the spurious-divisions fix); the 5
files this flips are pinned in roundtrip-baseline.txt (212 -> 217).

Testing

  • New ArpeggiateAttributes / NonArpeggiateAttributes round-trip tests (fail before, pass after)
  • Full mxtest suite passes (4936+ assertions)
  • Discovery: 217 PASS, zero regressions; regression mode: 217/217

References

webern added 2 commits July 12, 2026 15:29
A source that declares no <divisions> anywhere reads as ticksPerQuarter = 0
(LCM of an empty set). The writer then emitted divisions for the first
measure of every part, and core::PositiveDivisions clamped the 0 to a bogus
1e-06. The writer now omits <divisions> when the score has no time
resolution; an otherwise-empty <attributes> is no longer flushed.

Pins the 41 corpus files this flips to PASS (212 total).
Adds ArpeggiateMarkData (number, unbroken, id) and NonArpeggiateMarkData
(top/bottom placement, number, id) as MarkDataChoice alternatives and
wires them through the reader and writer. The non-arpeggiate bracket end
previously always wrote as type="top".

Pins the 5 corpus files this flips to PASS (217 total).
@webern webern mentioned this pull request Jul 12, 2026
3 tasks
@github-actions

Copy link
Copy Markdown

Coverage report

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

Metric Coverage Covered / Total
Lines 77.8% 28514 / 36646
Functions 74.2% 6352 / 8555
Branches 50.7% 22680 / 44751

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

Metric Coverage Covered / Total
Lines 82.5% 7035 / 8526
Functions 71.7% 2404 / 3353
Branches 51.2% 6144 / 12007

Core HTML report | API HTML report

Commit baeea48fa068338ad253bbf9a0f300e87ba9343b.

@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 baeea48fa068338ad253bbf9a0f300e87ba9343b.

Base automatically changed from claude/fix-spurious-divisions to main July 12, 2026 19:33
@webern webern merged commit d6732c4 into main Jul 12, 2026
1 of 8 checks passed
@webern webern deleted the claude/arpeggiate-attributes branch July 12, 2026 19:42
webern added a commit that referenced this pull request Jul 12, 2026
## Human Summary

A quick glance and it looks like this is correctly following the pattern
for adding `MarkData` features.

## Summary

The `<caesura>` articulation's optional text value (MusicXML 3.1+:
normal, thick, short,
curved, single) was dropped on read, and the writer always emitted
`<caesura>normal</caesura>` — so the common empty form `<caesura/>`
gained a spurious value
and the other variants were flattened to normal. `mx::core` already
models the empty form
(`core::CaesuraValue::empty()`); only the api impl dropped it.

Following the tremoloSingle/fermata-variant precedent,
`MarkType::caesura` now means the
common empty form and new `caesuraNormal` / `caesuraThick` /
`caesuraShort` / `caesuraCurved`
/ `caesuraSingle` enumerators carry the explicit values, wired through
`ArticulationsFunctions` (read) and `NotationsWriter` (write).
`isMarkArticulation` and the
articulations converter map cover the new enumerators.

Stacked on #340 to keep the `roundtrip-baseline.txt` additions
conflict-free; the 3 files this
flips are pinned (217 -> 220).

## Testing

- [x] New `CaesuraEmpty` / `CaesuraVariants` round-trip tests
(CaesuraEmpty fails before, passes after)
- [x] Full mxtest suite passes (4950 assertions in 420 test cases)
- [x] Discovery: 220 PASS, zero regressions; regression mode: 220/220

## References

- Closes #341
- Stacked on #340; part of #208
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add reading and writing of arpeggiate

1 participant