Skip to content

feat: model editorial (footnote/level) in mx::api#236

Closed
webern wants to merge 1 commit into
claude/issue-204-plan-vsvcasfrom
claude/issue-204-editorial-vsvcas
Closed

feat: model editorial (footnote/level) in mx::api#236
webern wants to merge 1 commit into
claude/issue-204-plan-vsvcasfrom
claude/issue-204-editorial-vsvcas

Conversation

@webern

@webern webern commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

Models MusicXML editorial information (<footnote> and <level>) in mx::api so it survives an api round-trip instead of being silently dropped.

Adds a reusable api::EditorialData (footnote + level) and attaches it to:

  • PartGroupData — editorial on the part-group start
  • DirectionData — editorial on the <direction> (the editorial-voice <voice> is already represented by DirectionData.voice)

New api types: EditorialData, FootnoteData (formatted-text, modeled on WordsData), and LevelData with new StartStopSingle / SymbolSize enums. A shared impl/EditorialFunctions.h translates the core editorial groups (EditorialGroup, EditorialVoiceDirectionGroup) in both directions, and Converter gains the two enum bridge maps. FootnoteData reads color with the correct isColorSpecified = color().has_value() gate, avoiding the bug tracked in #207.

Part-group stop editorial is intentionally not modeled — MusicXML ignores child element values at a group stop.

Scope

This makes footnote/level round-trip through the api, proven by unit tests. It does not pin the dense synthetic/{segno,coda}.* probes to the strict round-trip baseline: after this change those files still diverge on two unrelated gaps — the writer materializing <divisions> in a note-less measure (the #228 family) and a dropped direction <voice> — tracked in a separate follow-up.

Testing

  • New EditorialApiTest: footnote+level round-trip on a part-group and a direction, with distinct non-default values per field (29 assertions, 2 cases)
  • Full mxtest: 4297 assertions in 309 test cases, all pass
  • api round-trip regression gate: 34/34 (no regressions)
  • clang-format clean

Notes

Stacked on #235 (base branch claude/issue-204-plan-vsvcas); review/merge that first. GitHub will retarget this to main once #235 merges.

References


Generated by Claude Code

@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 Jun 22, 2026 — with Claude
@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 ffcf49bf4d2a4295aab496dd059c9aa4228d0ae0.

@github-actions

Copy link
Copy Markdown

Coverage report

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

Metric Coverage Covered / Total
Lines 77.9% 28539 / 36624
Functions 74.4% 6360 / 8550
Branches 50.7% 22672 / 44725

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

Metric Coverage Covered / Total
Lines 78.2% 5940 / 7594
Functions 62.4% 1979 / 3173
Branches 47.5% 5008 / 10537

Core HTML report | API HTML report

Commit ffcf49bf4d2a4295aab496dd059c9aa4228d0ae0.

Comment on lines +17 to +25
enum class StartStopSingle
{
unspecified,
start,
stop,
single
};

// Mirrors the MusicXML <level> 'size' attribute (symbol-size).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This strikes me as one of things we should try to simplify in the mx::api model instead of mirroring the way MusicXML models it. In general, we are trying to get away from MusicXML's procedural "stop"/"start" semantics (i.e. requiring stateful parsing) and represent data more statically whenever we can.

Add a reusable api::EditorialData (footnote + level) and attach it to
PartGroupData (the start group) and DirectionData, threading it through
the impl reader/writer layers so <footnote>/<level> survive an api
round-trip instead of being silently dropped.

- New api types: EditorialData, FootnoteData (formatted-text, mirroring
  WordsData), and LevelData with its StartStopSingle/SymbolSize enums.
- Shared EditorialFunctions.h translates the core editorial groups
  (EditorialGroup, EditorialVoiceDirectionGroup) in both directions.
- Converter gains StartStopSingle and SymbolSize bridge maps.
- EditorialApiTest proves footnote+level round-trip on a part-group and
  a direction, using distinct non-default values per field.

Part-group stop editorial is intentionally not modeled, since MusicXML
ignores child element values at a group stop.
@webern webern force-pushed the claude/issue-204-editorial-vsvcas branch from 384f715 to c14698f Compare June 22, 2026 18:43
@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 60f8ee5e408cfa319d1924d2b156c7b8c2b16a9c.

@github-actions

Copy link
Copy Markdown

Coverage report

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

Metric Coverage Covered / Total
Lines 77.9% 28539 / 36624
Functions 74.4% 6360 / 8550
Branches 50.7% 22672 / 44725

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

Metric Coverage Covered / Total
Lines 78.2% 5940 / 7594
Functions 62.4% 1979 / 3173
Branches 47.5% 5008 / 10537

Core HTML report | API HTML report

Commit 60f8ee5e408cfa319d1924d2b156c7b8c2b16a9c.

webern commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

Closing without merging — we're dropping editorial (<footnote>/<level>) from mx::api entirely.

Rationale: editorial appears in zero real-world corpus files. Every <footnote>/<level> in data/ (386 files) is in synthetic/, where the audit generator stamps boilerplate editorial onto nearly every single-feature probe. So this PR was adding api surface — including the start/stop/single <level> type machinery — purely to satisfy synthetic boilerplate, not anything in the wild. We don't want the synthetic corpus to drive features that real files never use.

What happens to the pieces of this PR:

Superseded by #238 (now based directly on #235).


Generated by Claude Code

@webern webern closed this Jun 22, 2026
webern added a commit that referenced this pull request Jun 23, 2026
…tures (#238)

Brings the audit's synthetic `segno`/`coda` fixtures onto the strict api
read -> write -> read gate.

## What

1. **Round-trip the editorial-voice `<voice>` on a `<direction>`** (read
from `editorialVoiceDirection().voice()`, written back). Today a
direction's own `<voice>` is dropped. The write is emitted only when the
direction has direction-type content, so a voice carried in from a
figured-bass/harmony element never produces an empty `<direction>`.
`<voice>` is common in the wild corpus, so this is real fidelity, not
synthetic-only.

2. **Pin `synthetic/{segno,coda}.{3.0,3.1}.xml`** in
`roundtrip-baseline.txt`. To make them round-trip losslessly the
fixtures are tidied:
- stripped the `<footnote>`/`<level>` editorial boilerplate the audit
generator stamps onto every probe (editorial appears in **zero**
real-world files, so the api does not model it — see #236, closed)
- added a `<divisions>` + a `<note>` so the writer's first-measure
`<divisions>` is legitimate instead of a spurious injection into a
note-less measure

They still isolate the segno/coda surface (3.0 base attributes; 3.1 adds
id + smufl) and now also exercise a part-group and direction
`<voice>`/`<staff>` through the public `DocumentManager` path.

## Why a real note instead of an impl fix for `<divisions>`

A note-less measure that the writer fills with a synthesized
`<divisions>` is the #228 writer-policy issue. A naive "suppress
divisions when the part has no notes" guard regresses
`ksuite/k016a_Miscellaneous_Fields.xml`, which legitimately carries
`<divisions>` in a note-less measure — distinguishing "source had it"
from "api invented it" needs divisions-presence tracking in the public
api, out of scope here. So the fixtures carry a real note; the
writer-policy gap stays open under #228.

## Follow-up

A separate PR on top of this one strips the editorial boilerplate from
every *other* synthetic file that isn't the dedicated `footnote`/`level`
probe, so the synthetic corpus stops implying api features the wild
corpus never uses.

## Testing

- [x] api round-trip regression gate: 38 passed, 0 failed (was 34; +4
synthetic)
- [x] discovery: no regressions
- [x] Full `mxtest`: 4268 assertions in 307 test cases, all pass
- [x] corert: 832 test cases pass, pinned corpus count unchanged
- [x] `clang-format` clean; audit `*.features.xml` sidecars +
`corpus.xml` regenerated

## References

- Stacked on #235 / #204 (segno/coda api round-trip)
- Supersedes #236 (editorial dropped); resolves the direction-voice part
of #237, with the `<divisions>` writer-policy part staying with #228
- Part of #208 (api round-trip coverage)
webern added a commit that referenced this pull request Jun 23, 2026
)

## Summary

The audit generator stamps `<footnote>`/`<level>` editorial onto nearly
every synthetic single-feature fixture, but editorial appears in zero
real-world corpus files — all 382 editorial-bearing files were under
`data/synthetic/`. That boilerplate implied mx::api needed editorial
support it does not actually need for the wild corpus, which is why #236
was closed without merging.

This removes the `<footnote>x</footnote>`/`<level>x</level>` boilerplate
from every synthetic fixture except the dedicated footnote/level probes,
which keep only their own target element. One uniform rule does this
safely: the boilerplate is always the plain `>x<` form, while each
probe's target is always attributed (`<footnote default-x=...>`, `<level
reference=...>`), so deleting the plain form strips the noise everywhere
and preserves every probe's intent.

- 382 fixtures changed (379 stripped fully; the footnote/level probes
keep their attributed target)
- `*.features.xml` sidecars and `corpus.xml` regenerated

## Testing

- [x] corert: 832 test cases pass, pinned corpus count unchanged (the
now-empty part-groups round-trip)
- [x] api round-trip regression gate: 38/38
- [x] Full mxtest: 4268 assertions in 307 test cases (ApiLoadSmokeTest
walks every stripped file)
- [x] No code changes; `clang-format` not applicable

## Notes

While regenerating sidecars, `audit files --force` also corrected a
stale `<audited-version>` (3.0 → 3.1) in
`data/custom/segno_coda_roundtrip.3.1.features.xml`. That file belongs
to #235, so the correction was reverted out of this PR to keep it
focused — worth fixing on the #235 branch.

## References

- Stacked on #238 (base branch `claude/issue-237-roundtrip-vsvcas`)
- Follows from #236 (editorial dropped from mx::api; closed)
- Progresses #221 — footnote/level are no longer dropped across ~373
classifier files now that the synthetic boilerplate is gone
- Part of #208 (api round-trip corpus coverage)
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.

1 participant