test(loop): truth fixtures for detectRepeatingProgression + smoke wiring (task C-30)

17 fixtures (9 regression guards, 8 annotated expectedFail) pinning the
current detector failure map: len^2 self-overlap artifact on 2-chord
vamps, ghost 4-patterns under substituted/inserted misdetections,
duplicate-commit safety, 7/8-loop length cap, stale-section recency.
Stale expectedFail markers turn smoke red, forcing L-30 to flip them.
Critic PASS (arithmetic recomputed, sabotage x3 proven to bite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
vadimwit
2026-07-10 00:55:27 +01:00
parent 0945a08497
commit fdd93c7158
2 changed files with 282 additions and 3 deletions
+65 -3
View File
@@ -66,7 +66,7 @@ const match = await load('src/lib/match.js')
const { buildLoopIndex, matchLoopToProgression, findLoopPosition } = match
const theory = await load('src/lib/theory.js')
const { CHORD_TYPES } = theory
const { CHORD_TYPES, detectRepeatingProgression } = theory
const piano = await load('src/lib/piano.js')
const { pianoVoicing, pianoVoicingChain, voicingToneSet, hasTrueSeventh } = piano
@@ -608,7 +608,68 @@ check('every KB piano pack recipe passes checkPianoRecipe (span ≤ 15 everywher
console.log(` (${recipes} live piano recipes checked)`)
})
// ─── 6. Summary + exit code ───────────────────────────────────────────────────
// ─── 6. Loop-detection truth fixtures (C-30) ──────────────────────────────────
//
// Run every scripts/loop-fixtures.mjs case against the REAL
// detectRepeatingProgression. Comparison is rotation-canonical on BOTH sides
// (the fixture module replicates theory.js's private `canonicalize`), so a
// correct loop reported from any rotation passes. Semantics:
// · no expectedFail + mismatch → ✗ smoke failure (regression in what works)
// · expectedFail + mismatch → ⚠ annotated expected-fail (printed, counted,
// NOT a failure — this is L-30's todo list)
// · expectedFail + MATCH → ✗ smoke failure: stale marker — the fixture
// now passes, L-30 must flip expectedFail off
//
// The failure map of the current algorithm lives in the fixture file header.
console.log('\nLoop-detection fixtures (C-30):')
const { LOOP_FIXTURES, canonicalLoop } = await load('scripts/loop-fixtures.mjs')
let expectedFails = 0
check('loop-fixtures module exports a non-empty fixture array + canonicalLoop', () => {
assert(Array.isArray(LOOP_FIXTURES) && LOOP_FIXTURES.length > 0, 'LOOP_FIXTURES missing/empty')
assert(typeof canonicalLoop === 'function', 'canonicalLoop is not a function')
const ids = new Set(LOOP_FIXTURES.map((f) => f.id))
assert(ids.size === LOOP_FIXTURES.length, 'duplicate fixture ids')
// canonicalLoop must be rotation-invariant (replica sanity: all rotations of a
// loop normalise to the same key — the property the comparisons rely on).
const key = canonicalLoop(['C', 'Am', 'F']).join(',')
assert(canonicalLoop(['Am', 'F', 'C']).join(',') === key && canonicalLoop(['F', 'C', 'Am']).join(',') === key,
'canonicalLoop is not rotation-invariant')
})
const loopKey = (loop) => (loop === null ? 'null' : canonicalLoop(loop).join(','))
for (const f of LOOP_FIXTURES ?? []) {
const got = detectRepeatingProgression(f.history)
const gotKey = loopKey(Array.isArray(got) ? got : null)
const wantKey = loopKey(f.expect)
const matches = gotKey === wantKey
const label = `loop fixture '${f.id}' — ${f.description}`
if (!f.expectedFail) {
check(label, () => {
assert(matches, `got [${gotKey}], expected [${wantKey}]`)
})
} else if (!matches) {
// Documented current-algorithm failure — annotated, never silently skipped.
expectedFails++
console.log(` ⚠ EXPECTED-FAIL ${label}\n today: [${gotKey}] · contract (L-30): [${wantKey}]`)
} else {
// The algorithm now satisfies this contract — the marker is stale and MUST
// be removed (L-30's DoD is zero expectedFail markers). Fail loudly.
check(`${label} [STALE expectedFail marker]`, () => {
assert(false, `fixture PASSES (got [${gotKey}]) but is still marked expectedFail — remove the marker in scripts/loop-fixtures.mjs`)
})
}
}
if (expectedFails) {
console.log(` (${expectedFails} annotated expected-fail(s) — the current detectRepeatingProgression's known gaps, awaiting L-30)`)
}
// ─── 7. Summary + exit code ───────────────────────────────────────────────────
const total = passed + failures.length
console.log('')
@@ -617,5 +678,6 @@ if (failures.length) {
for (const f of failures) console.error(' ' + f)
process.exit(1)
}
console.log(`✓ Smoke test passed — ${passed}/${total} checks green (${styleNames.length} styles, ${allIds.size} progressions)`)
console.log(`✓ Smoke test passed — ${passed}/${total} checks green (${styleNames.length} styles, ${allIds.size} progressions)`
+ (expectedFails ? ` · ${expectedFails} annotated loop-fixture expected-fail(s) awaiting L-30` : ''))
process.exit(0)