Skip to content

feat(sloppak): core reader for source rigs (feedpak 1.18.0)#1040

Merged
byrongamatos merged 4 commits into
mainfrom
feat/core-rig-reader-tone-passthrough
Jul 23, 2026
Merged

feat(sloppak): core reader for source rigs (feedpak 1.18.0)#1040
byrongamatos merged 4 commits into
mainfrom
feat/core-rig-reader-tone-passthrough

Conversation

@gionnibgud

Copy link
Copy Markdown
Contributor

What

Core reader for source rigs — the first half of letting a pack declare what a MIDI part should sound like. Board: Keys Foundations #4, "Core reader for source rigs (spec 1.18.0)".

feedpak 1.18.0 (FEP #61) shipped the model: a rig can carry a role: "source" block that voices a MIDI part, with a guaranteed GM floor. Core implemented none of it — rigs, base_rig and drum_tones appeared nowhere in lib/, server.py or static/, and lib/tones.py actively dropped the rig field on its way to the wire. A chart could declare its sound and nothing downstream could ever see it.

Three commits, each independently reviewable:

  1. tones payload carries the rig bindings. sloppak_tone_changes returns (base, base_rig, changes) and keeps rig per change. Both ids are validated as non-blank strings and stripped; anything else is dropped rather than forwarded, so presence of the key means "this binds a rig".
  2. The rig library loads. Manifest rigs:LoadedSloppak.rigs (alongside the other side-files rather than on Song — it's pack-level, and every side-file hangs off the load result). Same permissive posture as its neighbours: missing / unreadable / malformed / traversing disables rigs with a warning and never fails the pack, which §7.9 requires outright.
  3. Binding precedence. A manifest arrangement entry's tones replaces the arrangement JSON's wholesale (§5.2) — no field-level merge. Top-level drum_tones binds the primary drum part, a type: drums entry's own tones outranks it, and a Reader must never apply both to the same part (§5.1).

Scope — what core deliberately does NOT do

It loads, resolves the binding, and exposes. It does not select a realization, does not apply the intent.gm floor, and does not make sound — those belong to whatever actually voices the part (the resolver-cascade work, and the sound-source plugin).

Consequently rig objects pass through verbatim. §7.9 obliges a Reader to preserve unknown role/engine/kind values and ext namespaces, so validating block structure here would be wrong as well as premature. The only entries dropped are ones unreachable by construction: a rig is addressable solely by id, so a non-dict entry or one without a usable string id can never be referenced. Duplicate ids resolve first-wins with a warning — ambiguity there would surface as the wrong sound rather than an error.

Compatibility

Additive on the wire. base_rig is omitted entirely when the chart binds no rig, so a pack without rigs produces the byte-identical tone_changes message it always did, and existing consumers (highway_3d reads .name) are untouched.

One behaviour worth calling out explicitly: an empty tones: {} on a manifest entry reads as absent, not as "override to silence". arrangement_from_wire already normalizes the in-JSON {} to None the same way, and the alternative would let a stray empty object silently unbind a part's sound.

Verification

  • Full suite 2796 passed / 4 skipped (+22 new tests: test_sloppak_rigs_load.py, test_sloppak_tones_precedence.py, and rig cases in test_tones.py).
  • tools/check_spec_conformance.py: OK. Core now reads 22 of the spec's 32 declared keys; rigs and drum_tones are both spec-declared, so no new grandfather debt.

Docs

CLAUDE.md's WS protocol table gains base_rig / rig on the tone_changes row. While in that row I also corrected its time key from time to t — every producer emits t, and highway_3d/screen.js already carries a comment warning readers about that exact discrepancy. Leaving a known-wrong field name next to the newly-added ones seemed worse than the small out-of-scope fix; happy to split it out if preferred.

feedpak surface

  • This PR does not change how the app reads/writes feedpaks
  • …or it does, and the spec change landed first via the FEP process — spec 1.18.0 / feedpak-spec#61, already merged. No spec work is pending for this PR; the gate passes against spec HEAD today.

Checklist

  • CHANGELOG.md [Unreleased] updated (user-visible changes)
  • Tests added/updated for new behaviour
  • Commits are DCO signed off (git commit -s)

Related, not blocking: #1039 dedups the manifest-path containment guard that _load_rigs_file here also copies. If #1039 lands first I'll rebase this onto it and route the new loader through the shared helper.

`sloppak_tone_changes` emitted `{t, name}` only, so a chart's declared
sound never reached the client: `base_rig` was never read and each
change's `rig` was dropped at the wire boundary. Both survive load
intact (`Arrangement.tones` is an opaque passthrough) — the strip
happened here, at the last step before send.

That left the rig model (feedpak-spec 1.18.0 §6.9/§7.9) unreachable
from core: a pack could declare which rig voices a part, and nothing
downstream could ever see it. First step of the core reader for source
rigs; the rig library itself and the manifest precedence cascade follow.

Return `(base, base_rig, changes)` and keep `rig` on each change. Both
ids are validated as non-blank strings and stripped — anything else is
dropped rather than forwarded, so presence of the key means the change
binds a rig. Resolution against `rigs.json` deliberately does NOT happen
here: this builder preserves the declared binding, while realization
selection and the `intent.gm` fallback belong to whatever voices the
part.

On the wire `base_rig` is omitted entirely when empty, so packs that
bind no rig produce the byte-identical `tone_changes` message they
always did.

Signed-off-by: gionnibgud <gionnibgud@gmail.com>
feedpak 1.18.0 lets a chart declare what a MIDI part should sound like
by binding a rig id, but core had nothing to bind to: `rigs`, `base_rig`
and `drum_tones` appeared nowhere in lib/, server.py or static/. The
preceding commit carries the reference onto the wire; this adds the
library it references.

Read the manifest `rigs:` key into a new `LoadedSloppak.rigs`, alongside
the other side-files rather than on Song — every side-file (drum_tab,
song_timeline, keys, notation) hangs off the load result, and rigs is
pack-level, not per-arrangement. Same permissive posture as its
neighbours: missing, unreadable, malformed or traversing disables rigs
with a warning and never fails the pack, which §7.9 requires outright.

Rig objects pass through VERBATIM. §7.9 obliges a Reader to preserve
unknown role/engine/kind values and `ext` namespaces, so validating
block structure here would be wrong as well as premature — realization
selection and the `intent.gm` floor belong to whatever voices the part.
The only entries dropped are ones unreachable by construction: a rig is
addressable solely by `id`, so a non-dict entry or one without a usable
string id can never be referenced. Ids are stripped to match the
reference side, and a duplicate id resolves first-wins with a warning,
since ambiguity there would surface as the wrong sound rather than an
error.

Signed-off-by: gionnibgud <gionnibgud@gmail.com>
feedpak 1.18.0 lets a sound binding arrive from three places, and core
honoured none of them: the manifest arrangement entry, the arrangement
JSON, and the top-level drum_tones. Reading them needs a precedence
rule, because two of the three can be present at once.

Arrangement entries: the entry's `tones` replaces the arrangement JSON's
WHOLESALE (spec 5.2), unlike name/tuning/capo/centOffset beside it,
which override field by field. A merge would produce a sound nobody
authored -- one source's base under the other's changes -- which is
worse than either block alone. This is also what makes a notation-only
keys entry bindable at all, since it has no arrangement JSON to carry
tones in the first place.

Drums: the top-level drum_tones binds the song-level primary part, and
a `type: drums` entry's own tones takes precedence, with a Reader
forbidden from applying both to the same part (5.1). That is the same
shape as the drum_tab alias rule, so it lives inside
_resolve_drum_parts next to it rather than beside it -- one precedence
resolver, not two that drift. drum_tones is the PRIMARY's fallback
only: a second drummer with no binding gets None, never the primary's
kit.

An empty `tones: {}` reads as absent rather than as an override to
silence, matching how arrangement_from_wire already normalizes the
in-JSON empty dict, so a stray empty object cannot quietly unbind a
part.

Spec-conformance gate passes with drum_tones added to the keys core
reads (22 of the spec's 32, all declared).

Signed-off-by: gionnibgud <gionnibgud@gmail.com>
CHANGELOG entry for the core rig reader, plus the WS protocol table in
CLAUDE.md, which described `tone_changes` as carrying only base + name.

While in that row: its time key was documented as `time`, but every
producer emits `t` — both the sloppak builder and the legacy XML path.
The 3D highway already carries a comment warning readers about exactly
this discrepancy. Corrected here rather than left sitting next to the
newly-added keys, where a reader would reasonably assume both were
equally reliable.

Signed-off-by: gionnibgud <gionnibgud@gmail.com>
@gionnibgud gionnibgud added keys-foundations Keys Foundations initiative — org project #4 labels Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@gionnibgud, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a2f5fc2-88d7-45d6-9ec3-8c6d46b61f63

📥 Commits

Reviewing files that changed from the base of the PR and between 8297afc and 61d8122.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • CLAUDE.md
  • lib/routers/ws_highway.py
  • lib/sloppak.py
  • lib/tones.py
  • tests/test_sloppak_rigs_load.py
  • tests/test_sloppak_tones_precedence.py
  • tests/test_tones.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/core-rig-reader-tone-passthrough

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

keys-foundations Keys Foundations initiative — org project #4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants