test(sweep): one-screen sprint-end sweep — ranking pins + registry id uniqueness (task C-50)

Smoke 886/886: RelatedProgressions ranking pinned (blues-12bar 152,
jazz-251-minor 156, no-collapse counterfactual 39 — replica grounded
against the live component; the 12-bar pin deliberately goes red when
D-62's collapsed indexing lands, message says re-pin). Validator:
registry-level progression-id uniqueness (the file walk missed
registry wiring dups — sabotage-proven with exactly one named error).
Whole-sprint App.jsx audio contract: zero hits since the seed.
Full-App SSR 60/60 across instrument x {normal, jam view}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
vadimwit
2026-07-11 18:27:20 +01:00
parent 6e8d8f40fc
commit 4deba7e443
2 changed files with 153 additions and 1 deletions
+128 -1
View File
@@ -1193,7 +1193,134 @@ if (existsSync(join(ROOT, 'src/components/PianoLickCard.jsx'))) {
warn('PianoLickCard vocab drift guard', 'src/components/PianoLickCard.jsx not yet authored (D-60)')
}
// ─── 8. Summary + exit code ───────────────────────────────────────────────────
// ─── 8. RelatedProgressions ranking pins (C-50) ───────────────────────────────
//
// Pins the L-51-gate-verified ranking outcomes of RelatedProgressions.jsx
// (rankRelatedProgressions / collapseChanges, exported for exactly this) so a
// scoring-constant tweak, a collapse regression, or a KB addition that
// reshuffles the top entries turns smoke red instead of silently changing what
// jammers are recommended. Two live pins + one counterfactual:
//
// (a) collapsed live 12-bar in A → blues/blues-12bar top at score 152,
// annotation 'same changes'. Today matchLoopToProgression does NOT
// recognize the collapsed loop (the raw index is collapse-blind — the
// known D-62 fix (a) gap), so match.matched === false and blues-12bar
// itself ranks as a relative. ⚠ When D-62's collapsed-form indexing
// lands in match.js, the match flips to blues-12bar, the id-only
// exclusion removes it from entries, and these pins go red ON PURPOSE —
// re-pin deliberately then.
// (b) live iiVI in C → match jazz/jazz-251-major (quality overlap wins
// over jazz-251-minor), which is EXCLUDED from entries; top entry is
// jazz/jazz-251-minor at 156 'same changes' (100 same canonical shape +
// 40 same style + 0 shared transitions — every quality differs — + 16
// Jaccard 0 length).
// (c) no-collapse counterfactual: an INDEPENDENT replica of the §5 scoring
// formula (docs/design/one-screen.md), fed blues-12bar's RAW bar-per-bar
// degrees, scores 39 (36 transitions + 8 Jaccard·0.5 5 length, no
// +100) vs 152 with collapse — proving the mandatory collapse step is
// what makes the flagship "this 12-bar IS their 12-bar" relation fire.
// The replica-with-collapse must equal the live component score (152),
// grounding the replica so the 39 can't drift into fiction.
console.log('\nRelatedProgressions ranking pins (C-50):')
const relMod = await load('src/components/RelatedProgressions.jsx')
const { rankRelatedProgressions, collapseChanges } = relMod
const { loopToDegrees, canonicalDegrees } = match
const LOOP_12BAR_A = ['A7', 'D7', 'A7', 'E7', 'D7', 'A7', 'E7'] // detection-collapsed 12-bar
const LOOP_251_C = ['Dm7', 'G7', 'Cmaj7'] // live iiVI
check('RelatedProgressions exports rankRelatedProgressions + collapseChanges', () => {
assert(typeof rankRelatedProgressions === 'function', 'rankRelatedProgressions is not an exported function')
assert(typeof collapseChanges === 'function', 'collapseChanges is not an exported function')
})
check('collapseChanges(blues-12bar) → 7 units [0,5,0,7,5,0,7] (raw 12 bars collapse to the detection form)', () => {
const prog = kb.blues.progressions.find((p) => p.id === 'blues-12bar')
assert(prog, 'blues-12bar missing from the registry')
const units = collapseChanges(prog)
const degs = units.map((u) => u.deg).join(',')
assert(degs === '0,5,0,7,5,0,7', `collapsed degrees [${degs}] ≠ [0,5,0,7,5,0,7]`)
assert(units.every((u) => u.quality === 'dom7'), 'collapsed qualities are not all dom7')
})
check('collapseChanges(blues-minor) pops the wrap-around pair → 5 units [0,5,0,8,7]', () => {
const prog = kb.blues.progressions.find((p) => p.id === 'blues-minor')
assert(prog, 'blues-minor missing from the registry')
const degs = collapseChanges(prog).map((u) => u.deg).join(',')
assert(degs === '0,5,0,8,7', `collapsed degrees [${degs}] ≠ [0,5,0,8,7] — the cycle wrap (last unit === first unit) must collapse too`)
})
const rank12 = rankRelatedProgressions(LOOP_12BAR_A)
check('pin (a): collapsed 12-bar → matcher NONE today; blues-12bar top at exactly 152, "same changes"', () => {
assert(rank12, 'ranking returned null for a parseable loop')
assert(rank12.match.matched === false,
`match is ${rank12.match.id} — the raw loop index recognized a collapsed loop; D-62 fix (a) has landed: re-pin this section deliberately (blues-12bar now gets excluded by the id-only rule)`)
const top = rank12.entries[0]
assert(top && top.id === 'blues-12bar' && top.style === 'blues',
`top entry is ${top?.style}/${top?.id}, expected blues/blues-12bar`)
assert(top.score === 152, `blues-12bar scored ${top.score}, pinned 152 (100 shape + 36 transitions + 16 Jaccard 0 length)`)
assert(top.annotation === 'same changes', `annotation '${top.annotation}' ≠ 'same changes'`)
})
const rank251 = rankRelatedProgressions(LOOP_251_C)
check('pin (b): iiVI in C → match jazz-251-major (excluded); jazz-251-minor top at exactly 156, "same changes"', () => {
assert(rank251, 'ranking returned null for a parseable loop')
assert(rank251.match.matched && rank251.match.id === 'jazz-251-major',
`match is ${rank251.match.matched ? rank251.match.id : 'NONE'}, expected jazz-251-major (quality-overlap disambiguation)`)
assert(!rank251.entries.some((e) => e.id === 'jazz-251-major'),
'the matched progression leaked into its own related list — the id-only exclusion broke')
const top = rank251.entries[0]
assert(top && top.id === 'jazz-251-minor' && top.style === 'jazz',
`top entry is ${top?.style}/${top?.id}, expected jazz/jazz-251-minor`)
assert(top.score === 156, `jazz-251-minor scored ${top.score}, pinned 156 (100 shape + 40 style + 0 transitions + 16 Jaccard 0 length)`)
assert(top.annotation === 'same changes', `annotation '${top.annotation}' ≠ 'same changes'`)
assert(rank251.entries.length <= 5, `${rank251.entries.length} entries > max 5`)
})
// Independent §5-formula replica (expectation-side reimplementation, same
// philosophy as §7's pinned truth table — deliberately NOT calling the
// component's private helpers). `collapse: false` reproduces the pre-L-51
// bug the mandatory collapse step exists to prevent.
function replicaScore(loop, prog, sameStyle, { collapse }) {
const SUFFIX_Q = { '': 'maj', 'm': 'min', '7': 'dom7', 'maj7': 'maj7', 'm7': 'min7' } // enough for the pinned loops
const loopDeg = loopToDegrees(loop)
const loopCanon = canonicalDegrees(loopDeg)
const trans = (units) => units.map((u, i) => {
const b = units[(i + 1) % units.length]
return { d: (((b.deg - u.deg) % 12) + 12) % 12, qa: u.quality, qb: b.quality }
})
const loopT = trans(loop.map((name, i) => ({ deg: loopDeg[i], quality: SUFFIX_Q[name.match(/^[A-G][b#]?(.*)$/)[1]] ?? null })))
const units = collapse
? collapseChanges(prog)
: prog.degrees.map((d, i) => ({ deg: d, quality: prog.qualities[i] ?? null }))
const pCanon = canonicalDegrees(units.map((u) => u.deg))
const seen = new Set()
let shared = 0
for (const t of trans(units)) {
const k = `${t.d}|${t.qa}|${t.qb}`
if (seen.has(k)) continue
seen.add(k)
if (loopT.some((l) => l.d === t.d && (l.qa == null || t.qa == null || l.qa === t.qa) && (l.qb == null || t.qb == null || l.qb === t.qb))) shared++
}
const A = new Set(loopCanon.split(',')), B = new Set(pCanon.split(','))
const inter = [...A].filter((x) => B.has(x)).length
return (pCanon === loopCanon ? 100 : 0) + (sameStyle ? 40 : 0)
+ Math.min(shared * 12, 36) + (inter / (A.size + B.size - inter)) * 16
- Math.abs(loop.length - units.length)
}
check('pin (c): no-collapse counterfactual — raw blues-12bar would score exactly 39 (replica grounded at 152 with collapse)', () => {
const prog = kb.blues.progressions.find((p) => p.id === 'blues-12bar')
const withCollapse = replicaScore(LOOP_12BAR_A, prog, false, { collapse: true })
assert(withCollapse === 152, `replica with collapse scored ${withCollapse} ≠ 152 — the replica drifted from the §5 formula; fix the replica (or the component changed: re-derive BOTH pins)`)
assert(rank12.entries[0].score === withCollapse, `replica (${withCollapse}) ≠ live component score (${rank12.entries[0].score}) — the grounding broke`)
const noCollapse = replicaScore(LOOP_12BAR_A, prog, false, { collapse: false })
assert(noCollapse === 39, `no-collapse counterfactual scored ${noCollapse} ≠ 39 (36 transitions + 8 half-Jaccard 5 length) — the raw/collapsed relationship changed; re-derive the counterfactual`)
})
// ─── 9. Summary + exit code ───────────────────────────────────────────────────
const total = passed + failures.length
console.log('')
+25
View File
@@ -499,6 +499,31 @@ for (const style of styleDirs) {
}
}
// ── Global progression-id uniqueness over the REGISTRY (task C-50) ────────────
// RelatedProgressions' ranking excludes the matched progression by ID ALONE
// (rankRelatedProgressions: `prog.id !== match.id` — no style comparison, task
// L-51), so a progression id repeated anywhere in the registry would silently
// drop an unrelated style's entry from the related list. The per-style loop
// above walks the FILES (kb/*/progressions.js) into `allIds`; this asserts the
// same invariant over kb/index.js — the object the components actually
// consume — so a registry wiring mistake the file walk never sees (a style
// registered against another style's progressions module, a shared/duplicated
// array) cannot reintroduce a duplicate.
if (registry) {
const seenRegIds = new Map() // id → style it first appeared under
for (const style of Object.keys(registry)) {
const progs = registry[style]?.progressions
if (!Array.isArray(progs)) continue
for (const p of progs) {
const rw = `kb/index.js [${style}]`
if (typeof p?.id !== 'string' || !p.id) { err(rw, 'registry progression with missing/non-string id'); continue }
if (seenRegIds.has(p.id))
err(rw, `progression id '${p.id}' already registered under style '${seenRegIds.get(p.id)}' — ids must be globally unique across the registry (RelatedProgressions excludes the matched progression by id alone)`)
else seenRegIds.set(p.id, style)
}
}
}
if (errors.length) {
console.error(`✗ KB validation failed — ${errors.length} error(s):\n`)
for (const e of errors) console.error(' ' + e)