Files
vadimwit 8d74e54f5a fix(match): index collapsed loop forms — real 12-bar blues now matches its KB entry (task L-60, 1/2)
buildLoopIndex additionally indexes each progression's collapsed form
(adjacent (degree,suffix) dedup + wrap), appended after raw entries so
every prior matcher winner is preserved except the enumerated
country-145 self-attribution. Repairs a pre-existing LIVE-detection
bug: a real 12-bar or 8-bar blues stream collapses to fewer names than
the raw KB degrees, so it matched NOTHING and the Jam Guide sat empty.
JamGuide reads authored plays through the collapsed station's
sourceIndex (all four guitar/piano/bass paths). Adds seedableLoop +
the round-trip roulette pool (smoke sweep: 4 failures / pool 52).
theory.js canonicalize exported. Critic PASS (fix-(a) additivity
proven registry-wide; pool reproduced from the spec; bass sourceIndex
station 5 -> raw 10 hand-checked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:48:14 +01:00

590 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# One Screen — the jam dashboard (task D-50)
> **Thesis:** the page stops being a tall stack of full-width bands and becomes a
> **viewport-fitting jam dashboard**: a full-width *slim loop strip* (the banner
> minus its big now-playing chord), then LEFT (~744px) the compact instrument
> view, the licks strip, and a new loop-relative *Related progressions* card;
> RIGHT (~500px) the suggested-voicings rail — the design's ONE justified
> internal scroller (§4). The dashboard is a **self-contained unit designed to
> fit 100vh at 1280×900**; a **JAM VIEW toggle** in the controls bar (§1.1)
> locks the page to exactly that unit (and goes browser-fullscreen,
> best-effort) while playing. In normal mode everything that is learning or
> behind-the-scenes (LoopStation, Debug, Drum, Tuner, KnowledgeDock) stays
> below the dashboard on the ordinary page scroll; in jam view it is hidden.
>
> User directive (2026-07-11, verbatim): *"the chord view and the chord loop
> incl the instrument below is too big, it takes up too much space, i want you
> to reconsider the positioning as this main view doesnt really add value:
> seeing the big chord 'now playing' in big is also unnecessary as you already
> see that one in the chords (loop) part. the suggested progressions on the
> right are also almost useless. they are nice but we need to rethink them in
> the form of SUGGESTED VOICINGS and the progressions we add relating to the
> loop. i'd like to have the suggested voicings on the right side of the screen
> (the jam guide). … i wanna see the chords, the loop, the voicings, the
> progressions, the licks all on one screen, everything you need to follow the
> jam correctly"*
>
> Refinement (2026-07-11, verbatim): *"it can still have a scroll down, but the
> main concept needs to be in one screen: potentially we add a fullscreen
> button so we can just show that when we are playing a jam. so that it fits
> the entire screen, everything that is learning and behind the scenes etc you
> can keep below/scrollable but we need one main view for live jams where we
> see everything at once (relating to the jam and the voicings and the licks
> and the chords and the loop)"* — the 100vh fit is a **hard design
> constraint** for the dashboard, not an aspiration; the fullscreen button is
> in scope (L-50 commit 3, §6.1); internal wrap/scroll inside a panel is a
> last resort and every instance below is justified.
**Supersedes** `integrated-glance.md` §1's full-width-band placement (the user
tested it and asked for the right column). That doc's **verified geometry stays
the arithmetic source here**: guitar cell ~93px, piano cells 160/284px, worst
piano gallery ~1,178px of cells, licks thumb card ~165px (in a 220px strip
slot), cell line heights ~140px guitar / ~125130px piano. Standing principles
(user memory): scroll > click; nothing shown twice; ONE global instrument
selector; the playhead highlights, never hides.
**Decision authority:** per the sprint header (no user gate), Muse picks the
strongest option and records rejected alternatives (§9).
---
## 0. What exists today (read from the code)
App.jsx mount order: `ProgressionBanner` (~608) → instrument row (~617629:
`Fretboard`/`BassFretboard`/`Piano` at 70% + `ProgressionSuggestions` at 30%) →
`JamGuide` full-width band (~635: GlanceRail all-expanded rows + LicksStrip) →
`LoopStation` → Debug/Drum/Tuner collapsibles → `KnowledgeDock` last. The band
starts ≈570px down at 1280×900 — voicings never share the screen with the
instrument view, and the licks strip sits another rail-height below that. The
banner burns its right 30% on a text-6xl "Now Playing" chord that duplicates
both the enlarged last history chip and the highlighted loop chip. The
directive kills all of that.
---
## 1. The grid
Assumed usable content box at a 1280×900 window: **~1,256 × ~836px** (Electron
title bar ~32px eats into the 900; App's `p-3` eats 24px each axis — heights
below are estimates, cell geometry is verified).
```
┌──────────────────────────────────────────────────────────────┐ ─┐
│ header (~52) │ │
│ controls bar — instrument · key lock · ⛶ JAM VIEW (~52) │ │
│ SLIM LOOP STRIP — key · history · ♻ loop chips (~76) │ │ the
├───────────────────────────────────────┬──────────────────────┤ │ dashboard
│ LEFT ~744px (flex-1) │ RIGHT 500px, │ │ = 100vh
│ │ own scroll (§4) │ │ in jam
│ compact instrument view (~240) │ │ │ view
│ licks strip (~190) │ SUGGESTED VOICINGS │ │
│ related progressions (~200) │ rail — GlanceRail / │ │
│ │ BassGuideRows rows │ │
├───────────────────────────────────────┴──────────────────────┤ ─┘
│ LoopStation · Behind the Scenes · Rhythm · Tuner · Knowledge │ normal mode
│ (the learning / behind-the-scenes area — page scroll) │ only; hidden
└──────────────────────────────────────────────────────────────┘ in JAM VIEW
```
- The **slim strip stays full width** (not inside the left column): it is the
status line for the whole dashboard — the rail's playhead and the strip's
active loop chip are the same "now", and a full-width one-liner costs only
~76px. (Putting it inside the left column would buy the rail ~88px of
first-paint height; the rail is internally scrollable anyway, and a
~450px-content strip floating in a 744px column looks broken. Not worth it.)
- **Grid:** `flex gap-3 items-start` → left `flex-1 min-w-0`, right
`w-[500px] shrink-0`, active at `xl:` (≥1280); below that the columns stack
(§7). 500px is the top of the licensed 420520 range because piano cells
need it (§4): at 500 the row interior is ~456px, which fits a 284px
two-octave cell **plus** a 160px one-octave cell per line (by ~4px — D-51
hardens the margin, §4); at 420 every two-octave cell rides alone.
- **The rail is height-bounded: `xl:max-h-[calc(100vh-1.5rem)]
xl:overflow-y-auto` — and NOT sticky.** (`sticky top-3` here would be inert:
inside the flex row the rail is the tallest item, so its containing block
equals its own height and there is zero travel to pin against — specifying
it would be a lie in class form.) What the bound actually buys, honestly:
the rail is the one unbounded surface (§4 math — a jazz piano loop is
~3,000px of rows). **(a)** Unbounded, it stretches the *document* to
~3,000px+, pushing the below-grid region (LoopStation, the whole learning
area) thousands of pixels down instead of one flick below the dashboard.
**(b)** The bound is exactly what jam view swaps to `h-full` — an unbounded
rail can never fit `h-screen` (§1.1). The rail's depth scrolls *within* its
column; the left column stays whole; at rest the rail shows ~644px (the
grid height). D-40 Rejected E (internal scroller) was rejected for the
*full-width band*, where it capped visible rows below 4 and nested a
scrollbar inside the page's only axis; in a two-column dashboard, bounding
the one unbounded column is what makes the dashboard a fixed-height unit —
the rejection does not carry over. **No auto-scroll in this sprint** (§4).
Vertical budget, normal mode at rest: 836 52 (header) 52 (controls) 88
(strip + gap) ≈ **644px for the grid**. Left stack ≈ 240 + 190 + ~200 + 24
gaps ≈ **654px** — everything on screen except the last ~1020px of the
related progressions card at exactly 900px window height; a micro-flick shows
it. In normal mode that residue is acceptable (the page scrolls anyway); **in
jam view it is not** — §1.1 makes the fit exact by construction, with the
related card as the left column's flex absorber.
### 1.1 Jam view — the one-screen lock + fullscreen toggle
The refinement's "fullscreen button". **Mechanism chosen: one button, two
layers on one state.**
- **Layer 1 — CSS jam view (the guarantee).** App gains a `jamView` boolean
(pure UI-state — nowhere near the audio callbacks/refs). When true:
- the page root becomes `h-screen overflow-hidden flex flex-col` (normal
mode: today's `min-h-screen` flow);
- **everything below the dashboard is not rendered** — LoopStation, Behind
the Scenes, Rhythm Analyser, Tuner, KnowledgeDock (conditional mount, not
`hidden`, so collapsed-state chrome can't leak height). Header, controls
bar, strip, grid remain — Stop/Start, the instrument selector, and the key
lock are jam-relevant chrome;
- the grid gets `flex-1 min-h-0`; the rail wrapper swaps its normal-mode
`xl:max-h-[calc(100vh-1.5rem)]` bound for `h-full overflow-y-auto` (the
column IS the viewport remainder, so the viewport-calc bound gives way to
the exact column height);
- the left column becomes `flex flex-col min-h-0`: instrument view and
licks strip fixed-height, the related progressions card `flex-1 min-h-0
overflow-y-auto` — the **one flex absorber** that soaks up the ±20px
between window sizes. Internal scroll here is the licensed last resort
and only engages below ~950px-tall windowed viewports (numbers below).
- **Layer 2 — browser fullscreen (the enhancement, best-effort).** The same
toggle calls `document.documentElement.requestFullscreen()` on enter and
`document.exitFullscreen()` on exit. Both return **Promises** — a bare
try/catch does not swallow the rejection; the calls must be
`.catch(() => {})` (or awaited inside try/catch). Electron grants it (plain
renderer, no IPC needed); if a browser build ever refuses, jam view still
works at layer 1. Fullscreen reclaims the ~32px
title bar (and any taskbar), which is exactly what turns "fits minus 20px"
into "fits clean" at 900-high screens.
- **The button** lives at the right end of the controls bar (the global,
always-visible chrome — same reasoning as the instrument selector):
`⛶ Jam view` / `✕ Exit` , `aria-pressed`, min-32px target,
`focus-visible:ring-2 ring-accent`, tokens only.
- **Exit paths, all restoring normal flow:** the button; **Escape** (a
`keydown` listener active only while `jamView` — needed because layer 1 can
exist without layer 2); and the native fullscreen Esc, synced via a
`fullscreenchange` listener (leaving fullscreen by any means switches
`jamView` off — one state, never half-exited). Entering does NOT auto-start
listening; leaving does NOT stop it — the toggle is layout-only.
**Jam-view budget (the hard numbers).** Grid height = viewport p-3 (24)
header 52 controls 52 strip + gap 88:
| Viewport | Grid (= rail height) | Left: fixed 454 (instrument 240 + licks 190 + gaps) → related card gets | Verdict |
|---|---|---|---|
| 1280×900 window (layer 1 only, content ~836) | **644px** | ~190px → heading + ~2.8 of 3 entries | fits; the card's last entry may shave ~20px — its internal scroll absorbs it |
| 1280×900 screen, true fullscreen (content ~876) | **684px** | ~230px → 3 entries fully clear, ~20px spare | **fits clean — the ~20px story is gone** |
| 1280×960+ screen, true fullscreen (content ≥936) | **≥744px** | ≥290px → up to 5 entries | fits with room |
The rail's *depth* still exceeds any of these for piano loops (§4 table — a
4-chord jazz loop is ~2,6003,000px of rows). That is the one place "see
everything at once" and "every voicing, all levels, zero clicks" (D-30)
mathematically collide; the cell strategy compresses first (recommended cell
leads; the 284+160 pair-fit halves mixed-station heights, §4) and the
remainder falls to the rail's contained scroll — the justified internal
scroller. All four *bounded* surfaces (strip, instrument, licks, progressions)
fit whole; guitar and bass rails fit ~2.53 of 4 rows with the "now" row
highlighted wherever it sits.
### 1.2 The slim loop strip — banner inventory
What `ProgressionBanner` renders today, and its disposition:
| Banner element today | Disposition |
|---|---|
| Key chip (root text-2xl accent + mode + confidence %) | **Survives** — leftmost, unchanged |
| Chord history (last 8, age-faded, tap → ChordDetailModal; current chip enlarged text-3xl + amber rn + pop-in animation) | **Survives, capped to last 5** — the enlarged current chip IS the now-playing display now; the pop-in animation moves with it |
| Loop row (♻ + chips, chord + rn, active chip accent-glow via `findLoopPosition`, "→ loop" tail) | **Survives, promoted onto the same row** as key + history (divider between) |
| `hidden lg:block` divider + right 30% column: "NOW PLAYING" label, **text-6xl amber chord**, rn, "tap for voicings" | **GONE.** The user's words: the loop chips already carry it. The tap-for-voicings affordance is not lost — every chip already opens ChordDetailModal, and the rail shows the loop's voicings permanently |
| Empty states ("Detecting key…", "Start listening…", "Play a chord") | First two survive; "Play a chord" dies with its column |
One row: key chip (~90px) + 5 history chips (~230px) + divider + up to 8 loop
chips (~320px) + tail ≈ ~720px — fits 1,256 with room; 12-chord loops (12-bar
blues) wrap the loop chips to a second line (strip ~104px, still fine).
**Strip chrome trim (part of the slimming):** `p-4 → p-2` and drop the history
wrapper's `pb-1` — the enlarged current chip (text-3xl + rn + chip padding) is
~58px tall on its own, so with p-4 the strip lands at ~9296px; the trim is
what makes the **~76px** figure true. Goes in L-50 commit 1 with the column
removal (§6.1).
Duplication audit: the current chord appears twice (last history chip + active
loop chip) — pre-existing, and the user's directive explicitly endorses the
loop chip as the "now"; the big third copy is what dies.
---
## 2. Compact instrument view — the mechanism
**Chosen: a `compact` prop on Fretboard / Piano / BassFretboard = trimmed card
chrome + a natural-width cap on the SVG. No fret reduction, no transform
scaling.**
- **Natural-width cap:** each SVG keeps `width:100%` but gains
`max-width: <its viewBox width>px` (Fretboard 674px, Piano 562px), i.e. it
never renders *above* scale 1.0. In the 744px left column (≈718px card
interior with the compact `p-3`) the fretboard sits at its designed 1.0 scale → SVG height
**186px** (today at 70% of 1,256 it renders at ~1.23 scale ≈ 229px + fat
chrome ≈ 320px card). Piano: 150px SVG.
- **Trimmed chrome:** `p-6 → p-3`, heading `mb-4 → mb-2`, and the legend row
(chord tone / pentatonic / scale dots) merges onto the heading line
(right-aligned) instead of a separate `mt-3` row. Card totals: fretboard
≈ **240px** (was ~320), piano ≈ **205px**.
- **Legibility, honestly:** at scale 1.0 the fretboard is at its *designed*
size — 20px note dots, 9px note labels, 10px string/fret labels, exactly
what ChordDiagram-era eyes were tuned on; nothing shrinks below today's
rendering (today it renders *larger* than designed, which is part of "too
big"). The compact card is smaller because the chrome and the over-scale go,
not because the notes do. Below ~700px column width the SVG scales down with
the column; at 0.9 scale (≈606px) labels are ~8px — the floor I'd defend.
The narrow plan (§7) gives the view the full window width anyway, so the
sub-1.0 zone only exists between ~1280 and ~1140px windows if the grid
breakpoint were lower — hence `xl:` for the grid, not `lg:`.
- Rejected mechanisms: **fewer frets** (07) kills position playing above the
7th fret — soloists live there; **CSS `transform: scale()`** blurs text and
shrinks hit targets — dishonest compactness; **hard height cap with
letterboxing** wastes the saved space as empty gutters.
---
## 3. What's removed / moved (full inventory)
| Surface | Disposition |
|---|---|
| Big "Now Playing" chord (banner right 30% + divider) | **Removed** (§1.2) |
| `ProgressionSuggestions` | **Unmounted; file kept in place** (EducationPanel/RoadmapTrack precedent — joins the retire-then-delete backlog item). Its job splits in two per the directive: "what to play on these chords" → the voicings rail; "where these changes can go" → RelatedProgressions (§5). Its generic genre/mood table and circle-of-fifths padding retire with it |
| The 70/30 instrument row | **Dissolved** — the instrument view becomes the left column's first block; the 30% slot's replacement is the rail column |
| JamGuide band (rail + licks, full width) | **Recomposed** — JamGuide stops rendering a band and renders the grid's jam surfaces: rail into the right column, LicksStrip + a related-progressions slot into the left (§6, component boundary) |
| LicksStrip | **Stays module-local in JamGuide.jsx** — no file move. It renders from JamGuide's match/context memos (`activeStyle`, `contextStation`); extracting it to its own file buys nothing and forces prop plumbing for the token-boundary context sort. It just mounts in the left column now |
| LoopStation | **Below the grid, first — and hidden in jam view.** Recorded call: the user's enumeration of the live-jam view is "the jam and the voicings and the licks and the chords and the loop", where "the loop" is the *chord* loop (the strip's ♻ chips), not the audio looper. LoopStation is a performance tool, but it is not on the list — it goes below with the learning surfaces, first in line because it feeds the detected loop. If the user misses it mid-jam, admitting it into jam view is a one-line conditional — the reversal path is cheap and noted in §9 |
| Behind the Scenes / Rhythm Analyser / Tuner collapsibles | Below LoopStation, unchanged |
| KnowledgeDock | **Last, unchanged** — browse & study stays below the fold |
| ChordDetailModal, AudioCapture, all `src/lib/**` audio/theory | Zero changes |
In **normal mode** everything below the grid remains exactly what the
refinement asks for — the learning / behind-the-scenes area on the ordinary
page scroll ("everything that is learning and behind the scenes etc you can
keep below/scrollable"). In **jam view** this whole region is hidden (§1.1) —
the dashboard is the entire page.
---
## 4. The rail at column width (~500px shell, ~456px row interior)
Row interior arithmetic: 500 rail section `p-3` + border (~26) row `p-2` +
border (~18) ≈ **456px** for header + cells. The inherited 93/160/284 figures
are already **box values** — integrated-glance's own verified sums prove it
(4×284 + gaps + chrome ≈ 1,178 with no extra per-cell padding), so nothing
gets padding added on top below. Cell strategy: **recommended-first, gallery
wrapped below — all cells always rendered, zero clicks** (D-30's "all at once"
directive stands; only the *geometry* adapts).
- **The recommended cell leads and reads as the answer.** The station's own
voicing (guitar: KB play shape badged "play"; piano: the authored/threaded
voicing with its honest label) keeps its accent-bordered figure and is
always the first cell — at column width it is frequently alone on line one,
which makes prominence free. D-51 may add nothing more than a slightly
stronger figure treatment (accent/60 border → accent) — no size difference,
no new colour.
- **Guitar:** cells ~93px (box) → **4 per line** (4×93 + 3×8 gaps = 396 ≤ 456
— fits with ~60px to spare, not borderline; a 5th cell at 497 does not).
Typical station = play cell + 36 placeable shapes = 47 cells → 12 cell
lines (~140px each). Header content (chord + rn + now/next + solo label +
aim dots + transition chip ≈ 540px of inline content) **wraps to 2 lines
≈ 50px** — fine, it already `flex-wrap`s. Row ≈ **206350px**.
- **Piano:** one-octave cells ~160px (box) → 2 per line (328 ≤ 456);
two-octave cells ~284px (box) → alone or, critically, **paired with a
one-octave cell: 284 + 160 + 8 = 452 ≤ 456 — the mixed pair FITS today, by
~4px**. That 4px is real but fragile (a longer label, a scrollbar gutter, a
browser rounding step could break it), so D-51's cell padding/gap shave is
**margin-hardening for an already-passing fit**, not a rescue — the pairing
is the difference between 5 and 3 cell lines on mixed stations. Worst
station (root above D with a true 7th — all five cells two-octave, per the
D-40 corrected math; 2×284 + 8 = 576 > 456 so they never pair with each
other): **5 cell lines ≈ 650 + header ≈ ~700px per row**. Best (roots CD /
triads): 3 lines ≈ ~490px.
- **Bass:** BassGuideRows text rows (~70px) or BassPatternCard rows — narrow
by construction; the column fits them with no adaptation beyond wrap, which
it inherits.
- **Wrap, never horizontal scroll** — unchanged invariant. The only permitted
horizontal scroller stays VoicingBrowser's per-cell guard around MiniPiano
thumbs on sub-320px pathologies.
### The honest vertical budget
| Scenario | Full rail height | Visible: normal rest (~644px) / jam view fullscreen (~684px) |
|---|---|---|
| 4-chord loop, guitar | ≈ 4×~275 avg + chrome ≈ **~1,150px** | ~2.3 / ~2.5 rows |
| 4-chord loop, piano (jazz flagship, mostly worst-case rows) | ≈ **~2,6003,000px** | ~1 / ~1.1 rows |
| 812 chord loop (12-bar) | guitar ~2,300 / piano ~5,000+ | scroll; the strip's loop chips always carry the position |
**What "one screen" honestly means:** all five surfaces are simultaneously
present at 1280×900 (whole, in jam view — §1.1) — but the rail's *depth* is a
scroll, contained in its column: it is the one panel whose content is
unbounded by construction (every voicing of every loop chord, D-30's "all at
once"), so it is the design's justified last-resort internal scroller; the
cell strategy above compresses first. A 4-chord guitar loop is ~1.4
rail-screenfuls; a 4-chord jazz piano loop is ~3.5. The playhead highlight
travels whether or not the row is in the scroller's window; the strip up top
always shows where you are. **No
auto-scroll in D-51** — same reasoning as D-40 §4, plus: contained auto-follow
is now *technically safe* (the scroller is no longer the document, so it can't
yank the page), so I flag it to Maestro as an optional 1-point follow-up
**after** the user has felt the manual version — auto-motion during a jam is
exactly the "it changes while I play" complaint in a new hat, so it must be
user-pulled, not designer-pushed.
Flag (not in scope): a **root-aligned MiniPiano crop** would turn most
two-octave piano cells into one-octave ones (~160px), roughly halving piano
rail heights. It reopens D-24's C-aligned-crop decision and MiniPiano is
frozen this sprint — Maestro may file it as a D-52 candidate.
---
## 5. Related progressions — the spec for L-51
**What it is:** 35 KB progressions genuinely related to the *detected loop*,
replacing ProgressionSuggestions' generic genre table. KB-sourced only
(`kb[style].progressions` — id, name, rn, degrees, qualities, level, songs),
loop-relative by construction.
### Ranking (computable today, no new theory)
Inputs: `detectedProgression` (chord-name strings), the KB registry, and
`matchLoopToProgression`'s result. Reuse `src/lib/match.js` machinery —
`loopToDegrees` and `canonicalDegrees` are currently module-private and must
become **additive exports** (L-51 lock addition; no behaviour change), next to
the already-exported `matchLoopToProgression`/`buildLoopIndex`.
1. `loopDeg = loopToDegrees(loop)`; bail to empty state if null.
`loopCanon = canonicalDegrees(loopDeg)`. `match = matchLoopToProgression(…)`
(the component computes its own — see §6's parallelism note).
2. Build the loop's **transition set** `T(loop)`: for each i (wrap-around),
the triple `(Δ = (deg[i+1]deg[i]) mod 12, q[i], q[i+1])` with qualities
from the chord-name suffixes (unmappable suffix → wildcard: match on Δ
alone). Same for every KB progression from its `degrees`/`qualities`.
**Collapse first — mandatory.** The live `detectedProgression` is a
*collapsed* form (detection never commits the same chord twice in a row),
while KB `degrees` are *raw*, bar-per-bar — blues-12bar is
`[0,0,0,0,5,5,0,0,7,5,0,7]`. Compared raw, the +100 same-canonical-shape
term would NEVER fire for the ~10 collapse-affected KB progressions —
silently killing the flagship "this 12-bar IS their 12-bar" relation (it
degrades, never misfires, which is why it would go unnoticed). So: before
`canonicalDegrees(p.degrees)` AND before building `T(p)`, **collapse
consecutive equal `(degree, quality)` pairs** in `p` (also with the
wrap-around pair). This additionally removes the harmless-but-deflating
`Δ=0` self-transitions from `T(p)`. Alternative once D-62's fix (a) lands
collapsed-form indexing in match.js: consume that shared collapsed form
instead of collapsing locally — coordinate via the §9 match.js
shared-file note.
3. Score every KB progression `p` with `p.id !== match.id`:
- **+100** if `canonicalDegrees(p.degrees) === loopCanon` — the same
changes in another style/length ("this turnaround IS jazz's IviiiV");
- **+40** if `p.style === match.style` (0 when nothing matched — the
ranking still works loop-relative off the raw degrees);
- **+12 per shared transition** (distinct triples in `T(loop) ∩ T(p)`),
capped at 36 — this is what finds "shares the ii→V" honestly;
- **+ up to 16** for rebased degree-set overlap (Jaccard × 16);
- **1 per chord of length difference** (tie-break toward similar-size loops).
4. Floor **24** (at least a shared transition + some overlap, or same-style +
substance) — never pad with junk; fewer honest entries beat five stretches.
Sort desc (stable by style, id), take **max 5; the dashboard budget
guarantees 3 visible** (§1.1) — the card is the left column's flex
absorber, so entries 45 show on tall viewports and scroll within the card
on tight ones.
### Per-entry render (~56px each)
- Line 1: **name** (gray-100 semibold) · style label (gray-500) · **level
chip** (ExplorePanel's `LevelBadge` language — untagged counts foundation) ·
annotation (gray-500, [10px]): `same changes` when the +100 fired, else
`shares {rn_a}→{rn_b}` naming p's rn at the top shared transition, else
`same style`.
- Line 2: the **chord chain realized in the current key** — rootPc =
`(keyRoot + degree) mod 12` + `CHORD_TYPES[quality].suffix` (the
stationVoicings formula), rendered as tappable chips with rn beneath
(banner-chip visual language, smaller); 12-bar chains truncate to the first
8 + "…". **Tap a chord → `onChordClick` → ChordDetailModal** — the existing
per-chord pattern (ExploreSection does exactly this); no new modal, no dock
deep-link API invented for v1.
### Empty states (honest)
- No loop detected → one-liner (~40px, dashed border):
*"Loop a progression — related changes from the songbook land here."*
- Loop but nothing ≥ floor → *"Nothing in the songbook genuinely relates to
this loop yet."* (never pad).
- Loop unmatched but candidates score (shape/transition terms only) → render
normally; the annotation still explains *why* each entry is there.
---
## 6. Component boundary + migration order (green at every commit)
**Boundary call:** `JamGuide` (default export) is promoted from "band" to
**jam-grid owner**: it renders the two-column `flex` region and takes two JSX
slot props from App — `mainView` (the instrument view — App keeps choosing
Fretboard/BassFretboard/Piano; JamGuide never imports them) and `relatedSlot`
(RelatedProgressions, mounted by App). Left column = `mainView` + LicksStrip +
`relatedSlot`; right column = the height-bounded scroller wrapping GlanceRail /
BassGuideRows / the heard-live fallback. Rationale: the match / stationVoicings
/ canonicalPos / focus machinery stays in ONE component with no context and no
logic lifted into App (the 🚨 App.jsx contract); the slots keep App as pure
mounts. **RelatedProgressions computes its own match** (memoized on the loop
key — `matchLoopToProgression` over a module-level index is trivially cheap);
this small duplicate computation is what makes L-51's files disjoint from
D-51's (they can run in parallel) and keeps the component pure/prop-driven:
`{ loop, keyInfo, onChordClick }`.
Sequenced: **L-50 → (D-51 ‖ L-51) → C-50.** Critic gates each.
### 6.1 L-50 (Luthier) — the dashboard restructure
**Files (re-lock at promotion):** `src/App.jsx` (layout/mounts only, 🚨 audio
contract grep-gated), `src/components/ProgressionBanner.jsx`,
`src/components/JamGuide.jsx`, `src/components/Fretboard.jsx`,
`src/components/Piano.jsx`, `src/components/BassFretboard.jsx` (compact prop —
mechanical chrome trim + max-width cap per §2; D-51 polishes visuals if
needed). **Not touched:** ProgressionSuggestions.jsx (unmounted, not edited),
GlanceRail.jsx, VoicingBrowser.jsx, all `src/lib/**`.
1. **Commit 1 — slim strip.** ProgressionBanner: delete the right 30% column +
divider + its `currentRN`/big-chord code path; merge key + history + loop
onto one row; `HISTORY_SHOWN` 8 → 5; chrome trim `p-4 → p-2` + drop the
history wrapper's `pb-1` (what makes the ~76px strip true, §1.2).
Standalone green.
2. **Commit 2 — the grid (atomic).** These land together or the page has a
hole: App replaces the 70/30 row + band with the `xl:` two-column grid;
**ProgressionSuggestions unmounted** (import + mount deleted); JamGuide
gains `mainView`/`relatedSlot` props and renders left/right columns, rail
wrapped in the height-bounded scroller (`xl:max-h-[calc(100vh-1.5rem)]
xl:overflow-y-auto` — no sticky, §1); instrument views gain
`compact` and App passes it; App passes `relatedSlot={null}` (slot renders
nothing until L-51). Green; contract grep clean.
3. **Commit 3 — jam view (§1.1).** App: `jamView` UI-state + the controls-bar
`⛶ Jam view` toggle button; page root `h-screen overflow-hidden flex
flex-col` + grid `flex-1 min-h-0` + left-column flex layout while active;
below-dashboard region conditionally unmounted; JamGuide's rail wrapper
accepts a `fill` (or `jamView`) prop switching the max-h bound → `h-full
overflow-y-auto`; best-effort `requestFullscreen()`/`exitFullscreen()`
**with `.catch(() => {})`** (they return Promises — a bare try/catch does
not swallow the rejection) + `fullscreenchange` sync + Escape keydown
(active only while `jamView`).
🚨 Pure layout/UI-state — zero contact with audio callbacks, refs, or
AudioCapture props; the toggle never starts/stops listening. Standalone
green on top of commit 2.
### 6.2 D-51 (Muse) — rail at column width
**Files:** `src/components/GlanceRail.jsx`, `src/components/VoicingBrowser.jsx`
(dense-path spacing only). Recommended-cell prominence; cell padding/gap shave
as **margin-hardening for the ~4px-tight 284+160 piano pair fit** (§4 — it
passes today; the shave makes it robust to label length / scrollbar gutters /
rounding); header wrap tuning at ~456px; verify wrap-never-scroll, playhead
highlight, AA contrast + focus rings; eyeball 1280×900, 1440×900, ~640px
stacked; recompute §4's table against the built thing. No auto-scroll
introduced.
### 6.3 L-51 (Luthier) — RelatedProgressions
**Files:** `src/components/RelatedProgressions.jsx` (new), `src/lib/match.js`
(**additive exports only**: `loopToDegrees`, `canonicalDegrees` — flag the
shared-file lock to Maestro at promotion), `src/App.jsx` (one-line: pass
`relatedSlot={<RelatedProgressions …/>}`). Ranking per §5, exported from the
component file for smoke coverage; honest empty states; build + smoke green.
Disjoint from D-51 (App.jsx/match.js vs GlanceRail/VoicingBrowser) → may run
in parallel after L-50.
---
## 7. Narrow plan (< xl, and the ~640px check)
Columns stack in the jam-following order: **loop strip → instrument view →
voicings rail → licks strip → related progressions** — i.e. the right column
tucks between mainView and LicksStrip, matching the user's list order.
Implementation: the rail block is simply the second child inside the stacked
flow below `xl` (JamGuide reorders its own children with responsive classes or
conditional order — no duplicate mounts). The rail **unbounds** below `xl`
(the max-h/overflow classes are `xl:`-prefixed) and lays out at natural height
in page flow — a nested scroller inside a scrolling page is a trap on touch.
Cell wrap at ~576px interior: guitar 5/line, piano cells **pair** (a 284px
two-octave + a 160px one-octave = 452, or two two-octaves at the 576 boundary) —
the same margin-hardened pairing D-51 verified for the bounded column (§4),
with more room here, not the "cells ride alone" of the narrower bounded width. The instrument
selector never moves: it lives in the controls bar, global, above everything
at every width.
**Jam view below `xl`:** the toggle still works — below-dashboard surfaces
hide and fullscreen still fires — but the stacked dashboard is taller than any
narrow viewport, so the page keeps scrolling (`h-screen overflow-hidden`
applies only at `xl:`+ alongside the grid). One-screen is a two-column
promise; jam view narrows honestly to "distraction-free" rather than
pretending the geometry works.
---
## 8. Rejected alternatives
**Rejected A — keep the rail as the full-width band and put licks +
progressions in the right column instead.** The rail is the surface the
directive names for the right side ("i'd like to have the suggested voicings
on the right side of the screen (the jam guide)") — and the math agrees: full
width doesn't make the rail *shorter* in any useful way (rows are height-bound
by cell-line count, and at 1,240px most piano galleries fit one line, but the
band still starts below the instrument view and pushes licks/progressions off
screen — today's exact failure). Licks and progressions are short; they fit
under the instrument view; the tall thing is what needs the dedicated column.
**Rejected B — one-screen by truncation: show only the recommended voicing per
station, gallery behind a tap.** Fits 4 stations in ~640px with zero scroll —
and violates three standing calls at once: scroll > click, D-30's "all at once"
gallery directive, and the learning value the user praised ("potentially learn
new ways to play it while you are playing"). The gallery stays; its depth
becomes a contained scroll, not a click.
**Rejected C — rail in the LEFT column, detection surfaces right.** Reading
order puts the primary, continuously-tracked surfaces (strip, instrument view)
top-left where Western eyes rest; the rail is a reference you glance at. Also
geometric: the instrument view wants the wide column (674px natural fretboard
width > 500px rail), and swapped columns would force the fretboard to ~0.68
scale — sub-legible labels.
**Rejected D — compact the instrument view via CSS `transform: scale()` or a
reduced fret range.** Covered in §2: transform blurs text and shrinks targets;
frets 07 amputates upper-position play. The natural-width cap + chrome trim
achieves the same ~80px saving honestly.
**Rejected E — page-flow rail (no height bound, no contained scroll).** The
"pure" reading of the no-nested-scrollbar taste rule — and it structurally
fails the refinement: an unbounded rail stretches the *document* to the rail's
full height (a jazz piano loop ≈ ~3,000px+), so the below-grid region —
LoopStation and the whole learning area — lands thousands of pixels down
instead of one flick below the dashboard; and in jam view an unbounded rail
can never fit `h-screen`, so the 100vh hard constraint is unmeetable without
the bound anyway. (The left column's five surfaces sit at the top either way —
the cost is the document's length and the jam-view fit, not their
co-presence.) Below `xl` this rejection inverts — §7 — because stacked
layouts have no second column to preserve.
---
## 9. Out of scope / flags for Maestro
- **Contained auto-follow** inside the rail scroller — now technically safe,
deliberately deferred; user-pulled follow-up only (§4).
- **LoopStation in jam view** — excluded per the user's own enumeration (§3
recorded call); if missed mid-jam, admitting it is a one-line conditional.
User-pulled follow-up only.
- **Root-aligned MiniPiano crop** (halves piano rail height) — reopens D-24;
D-52 candidate (§4).
- **`src/lib/match.js` additive exports** in L-51 — shared-file lock note at
promotion (§6.3). Same coordination point covers D-62: if its fix (a) lands
collapsed-form indexing in match.js first, L-51's ranking consumes that
shared collapsed form instead of collapsing locally (§5).
- **Retire-then-delete backlog** grows by ProgressionSuggestions.jsx
(unmounted L-50) alongside RoadmapTrack/CurrentJamPanel/EducationPanel.
- KnowledgeDock deep-link ("open Explore at style X") — not invented for v1;
RelatedProgressions taps go per-chord to ChordDetailModal (§5).
- No new tokens, no new colours, no dependencies, no KB/theory/audio changes
anywhere in this design. Geometry figures verified per integrated-glance.md;
module heights are estimates and D-51/C-50 re-measure the built thing.