Skip to content

fix: do not write divisions in scores with no notes#337

Merged
webern merged 1 commit into
mainfrom
claude/fix-spurious-divisions
Jul 12, 2026
Merged

fix: do not write divisions in scores with no notes#337
webern merged 1 commit into
mainfrom
claude/fix-spurious-divisions

Conversation

@webern

@webern webern commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Human Summary

A little, innocuous bug, when fixed, unlocks our ability to run a bunch of files through mx::api. Basically we were inserting a fictitious divisions value in scores that have no notes (and thus no need for divisions).

Summary

A source that declares no <divisions> anywhere (and has no durations) reads as
ScoreData::ticksPerQuarter = 0, because ScoreReader::findMaxDivisionsPerQuarter takes the
LCM of an empty set. MeasureWriter then unconditionally wrote divisions for the first measure
of every part, and core::PositiveDivisions clamped the 0 to 1e-06, injecting a bogus
<attributes><divisions>0.000001</divisions></attributes> the source never had. This was the
single largest api round-trip blocker (add:divisions on 389 of 591 candidate files).

The fix is a guard in MeasureWriter: skip writeDivisions when the score has no time
resolution (ticksPerQuarter <= 0). The PropertiesWriter buffer then stays empty for
content-free first measures, so no spurious <attributes> is flushed either. Sources that do
declare divisions — used or not — still read a positive ticksPerQuarter and round-trip the
declaration unchanged (e.g. synthetic/segno.3.0.xml, which stays green).

ScoreData::ticksPerQuarter gets a doc comment defining the zero state, and the 41 corpus
files this flips to PASS are pinned in roundtrip-baseline.txt (171 -> 212).

Testing

  • New ZeroTicksPerQuarterWritesNoDivisions / ZeroTicksPerQuarterKeepsOtherProperties MeasureWriter tests (fail before, pass after)
  • Full mxtest suite passes
  • Discovery mode: 212 PASS (was 171), zero regressions
  • Regression mode: 212 passed, 0 failed (of 212 pinned)

References

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).
@webern webern changed the title fix: do not invent divisions for scores with no time resolution fix: do not write divisions in scores with no notes Jul 12, 2026
@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.2% 6966 / 8473
Functions 69.4% 2322 / 3347
Branches 51.0% 6079 / 11927

Core HTML report | API HTML report

Commit cbd56ddf2e0f74c6cc769b36040963682b51ee84.

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

@webern webern merged commit f081549 into main Jul 12, 2026
10 checks passed
@webern webern deleted the claude/fix-spurious-divisions branch July 12, 2026 19:33
webern added a commit that referenced this pull request Jul 12, 2026
## 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

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

## References

- Closes #137
- Stacked on #337 (base branch); 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.

api: writer invents <divisions>0.000001</divisions> when the source declares no divisions

1 participant