feat(related): RelatedProgressions — loop-relative suggestions replace the generic table (task L-51)

KB-sourced ranking per the one-screen spec: same canonical degree
shape +100 (with the mandatory collapse of consecutive equal
degree/quality pairs — a real detector-produced 12-bar fires its blues
relatives despite the raw-index matcher bug), same style +40, shared
transitions +12 capped 36, degree-set Jaccard, length penalty; floor
24, max 5, never padded; entries render name/style/level/annotation +
the chord chain realized in the live key as real buttons ->
ChordDetailModal. match.js: additive exports only. Ride-along: dead
currentChord prop dropped from the banner mount. Critic PASS (both
hand-verified scores recomputed independently; full entry lists
byte-matched on three scenarios; collapse counterfactual = 39).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
vadimwit
2026-07-11 16:52:52 +01:00
parent d5a45f433a
commit 6e8d8f40fc
3 changed files with 339 additions and 8 deletions
+6 -2
View File
@@ -91,7 +91,9 @@ export function findLoopPosition(chordHistory, loop) {
// for each rotation, re-base so the first element is 0, then pick the
// lexicographically smallest resulting sequence. Same loop → same string,
// regardless of which chord it starts on.
function canonicalDegrees(degrees) {
// Exported additively for RelatedProgressions' ranking (task L-51,
// one-screen.md §5) — no behaviour change.
export function canonicalDegrees(degrees) {
const n = degrees.length
if (n === 0) return ''
let best = null
@@ -110,7 +112,9 @@ function canonicalDegrees(degrees) {
// Converts a loop of chord-name strings into semitone offsets from the loop's
// own first chord. Returns null if any chord root is unparseable.
function loopToDegrees(loop) {
// Exported additively for RelatedProgressions' ranking (task L-51,
// one-screen.md §5) — no behaviour change.
export function loopToDegrees(loop) {
if (!loop?.length) return null
const pcs = loop.map(chordRootPC)
if (pcs.some(pc => pc < 0)) return null