Skip to content

feat(core): host per-instrument workingTuning capability + read-API/event (working-tuning PR 1)#658

Merged
byrongamatos merged 2 commits into
mainfrom
feat/host-working-tuning
Jul 1, 2026
Merged

feat(core): host per-instrument workingTuning capability + read-API/event (working-tuning PR 1)#658
byrongamatos merged 2 commits into
mainfrom
feat/host-working-tuning

Conversation

@ChrisBeWithYou

Copy link
Copy Markdown
Contributor

What & why

Foundation (PR 1) of the working-tuning series. Introduces window.feedBack.workingTuning — a host-owned, session-lived record of the tuning the player's instrument is actually in right now, distinct from any one song's tuning and from a soft opt-in default.

It exists so a retune (or an instrument swap mid-session) is reflected everywhere — the highway, the library/song-picker, and plugins like the tuner, Virtuoso, and the minigames — instead of being re-derived per surface or wrongly assumed from a fixed profile. That fixed-profile assumption is the root of the bug the series fixes: the tuner gate only ever prompts you away from a "home" tuning and never back.

Per-instrument

State is a map keyed by instrument (${instrument}-${stringCount}, e.g. guitar-6 / bass-4 — the same key the v3 instrument selector uses). Your guitar's current tuning and your bass's are kept separately; get() returns the selected instrument's, and switching the selector surfaces that instrument's own remembered tuning. You only ever deal with the one you've picked.

Shape

Modeled on the shipped tuning capability (static/capabilities/tuning.js) + the feedBack.theme read-API pattern:

  • window.feedBack.workingTuningget(instrument?) (synchronous, returns the selected instrument's state, defaulting to the seed until known), set(state, {provenance, instrument}), setCurrentInstrument() (for the selector), resetToDefault().
  • Per-instrument value: { offsets, stringCount, instrument, referencePitch, provenance:'assumed'|'verified', verifiedStrings, verifiedAt, source }. Offsets use the same per-string semitone vocabulary as song tunings, so fully custom/extended tunings (e.g. a drop-A 8-string) are first-class.
  • working-tuning-changed event on the feedBack bus — fires on change and once on hydration (carrying which instrument changed), so a late-mounting consumer is never stuck on stale state.
  • Registered as a separate working-tuning exclusive-owner capability (tuner = sole writer, the rest = requesters).

Lifecycle

In-memory, seeded from /api/settings on boot and reset on restart — a stale "you're in drop-A" assumption is worse than re-asking.

Scope

Plumbing only — nothing writes to it yet and no behavior changes. Feature-detected + additive (older consumers ignore it and fall through to today's behavior). The tuner becomes the writer (and the E→C# gate asymmetry is fixed) in a later PR; instrument→chart routing, the home-screen badge, and downstream consumers follow.

Frontend-only: new static/capabilities/working-tuning.js, loaded from static/index.html + static/v3/index.html. The per-instrument state machine is verified by a stubbed node harness (separate guitar/bass slots, selector switch, isolated writes, verified stamp, reset, defensive copies, capability registration).

🤖 Generated with Claude Code

…vent (working-tuning PR 1)

Introduce window.feedBack.workingTuning — the live, host-authoritative current
instrument tuning (offsets + string-count + reference pitch + assumed/verified
provenance), distinct from any one song's tuning and from a soft opt-in default.
It's the single source of truth the highway, library, and plugins (tuner,
Virtuoso, minigames) will read so a retune or instrument swap is reflected
app-wide instead of being re-derived per surface.

PER-INSTRUMENT: state is a map keyed by `${instrument}-${stringCount}` (e.g.
guitar-6 / bass-4, the selector's key) — your guitar's tuning and your bass's are
kept separately; get() returns the selected instrument's, and switching the
selector surfaces that instrument's own remembered tuning. You only ever deal
with the one you've picked.

Modeled on the shipped `tuning` capability + the `feedBack.theme` read-API:
synchronous get(instrument?), set(state,{provenance,instrument}) mutator,
setCurrentInstrument(), resetToDefault(), and a `working-tuning-changed` event
that fires on change and once on hydration (carrying which instrument changed).
In-memory, seeded from /api/settings, reset-on-restart. Registered as a separate
`working-tuning` exclusive-owner capability (tuner = sole writer, others read).

Foundation only — pure plumbing, nothing writes to it yet and no behavior
changes. The tuner becomes the writer (and the gate's E->C# asymmetry is fixed)
in a later PR.

Frontend-only: new static/capabilities/working-tuning.js, loaded from
static/index.html + static/v3/index.html. Per-instrument state machine verified
by a stubbed node harness (separate guitar/bass slots, selector switch, isolated
writes, verified stamp, reset, defensive copies, capability registration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
Addresses the manual + Codex review of PR 1 (working-tuning foundation).

P1 — named tunings were dropped by the boot seed: /api/settings.tuning may be a
name ("Drop D") OR an offsets list, but the seed only handled the list and stored
offsets:null for names. The seed now resolves a name to per-string semitone offsets
via /api/tunings (ratio vs Standard; reference pitch cancels).

P1 — async seed could clobber state a consumer had already written: _seedFromSettings
resolves after boot and used to overwrite _currentKey/_byInstrument unconditionally.
It now bails when state was already _touched (and re-checks after the /api/tunings
leg), so an explicit set()/setCurrentInstrument()/resetToDefault() before hydration
wins. Hydration still fires.

P1/P2 — shallow copy leaked live nested arrays: get() and set() now clone offsets and
verifiedStrings on both ingress and egress, honouring the "readers can't mutate live
state" contract.

P2 — provenance/verification state machine made coherent by construction:
verified <=> verifiedStrings is an array AND verifiedAt is a finite number. A tuning
change invalidates prior verification unless a fresh bundle is supplied; a "verified"
claim with no strings or a null/absent timestamp is repaired (assumed / stamped now).

P2 — bare-instrument writes targeted a hard-coded default string count: _keyOfResolved()
resolves an omitted string count against the current selection (same instrument), so
set({instrument:'bass'}) / set({stringCount:5}) hit the selected bass-5, not bass-4.

Test — adds tests/js/working_tuning.test.js (the harness the PR described but did not
commit): 11 behavioral cases over a stubbed window — registration, per-instrument
isolation + selector switch, defensive copies, the verification invariant, bare-key
routing, named + offsets-list seeding, and the boot-race guard. Full tests/js suite:
no new failures (the 12 pre-existing branch failures are unrelated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos merged commit 491039a into main Jul 1, 2026
1 of 3 checks passed
@byrongamatos
byrongamatos deleted the feat/host-working-tuning branch July 1, 2026 09:34
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.

2 participants