feat(theory): noise-tolerant loop detection rewrite (task L-30)
detectRepeatingProgression redesigned: consecutive-dup collapse, weak-period candidate rejection (kills self-overlap ghosts structurally), lengths 2-8, <=1 substitution/insertion per cycle with a >=2-exact-occurrences evidence gate, recency-weighted linear coverage scoring. All 17 C-30 fixtures pass plain (smoke 817/817, expectedFail markers removed). Critic PASS: 15 novel generalization probes correct or data-faithful, 0.2-0.7 ms/call, diff confined to the repeating-progression section, fixture contract untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,12 @@
|
||||
// · fixture with expectedFail that PASSES → smoke FAILS: stale marker, flip it
|
||||
// L-30's definition of done = every expectedFail marker removed, all green.
|
||||
//
|
||||
// ─── FAILURE MAP OF THE CURRENT ALGORITHM ────────────────────────────────────
|
||||
// (theory.js `detectRepeatingProgression`, every fixture RUN against it 2026-07-10;
|
||||
// the `today:` comment on each expectedFail fixture is the actual observed output)
|
||||
// ─── FAILURE MAP OF THE PRE-L-30 ALGORITHM (historical — all fixed by L-30) ──
|
||||
// (theory.js `detectRepeatingProgression` as of C-30, every fixture RUN against
|
||||
// it 2026-07-10; L-30 redesigned the function and removed every expectedFail
|
||||
// marker — all 17 fixtures now pass as plain regression guards)
|
||||
//
|
||||
// The current matcher: window = last 32 commits; candidate lengths 2–6; EXACT
|
||||
// The pre-L-30 matcher: window = last 32 commits; candidate lengths 2–6; EXACT
|
||||
// contiguous occurrence counting (scan advances by len on match, by 1 on miss);
|
||||
// requires reps ≥ 2; score = reps × len²; returns the canonical (lexicographically
|
||||
// smallest) rotation of the best candidate.
|
||||
@@ -130,87 +131,54 @@ export const LOOP_FIXTURES = [
|
||||
expect: null,
|
||||
},
|
||||
|
||||
// ── Expected failures of the current algorithm — the L-30 contract ─────────
|
||||
// ── Failure modes of the pre-L-30 algorithm — fixed by L-30 ────────────────
|
||||
{
|
||||
id: 'vamp-2x4',
|
||||
description: 'clean 2-chord vamp played 4× — must still report the pair, not a self-overlap',
|
||||
history: reps(['Am', 'G'], 4),
|
||||
expect: ['Am', 'G'],
|
||||
// today: returns [Am,Am,G] — the 3-chord self-overlap [Am,G,Am] scores
|
||||
// 2 reps × 3² = 18 and beats the true pair at 4 × 2² = 16 (failure map #1).
|
||||
expectedFail: true,
|
||||
},
|
||||
{
|
||||
id: 'vamp-2-sustained',
|
||||
description: 'sustained 2-chord vamp filling the whole window (×16) — the everyday two-chord jam',
|
||||
history: reps(['Am', 'G'], 16),
|
||||
expect: ['Am', 'G'],
|
||||
// today: returns [Am,Am,G,Am,G] — a bogus 5-chord self-overlap of the pair
|
||||
// wins on len² (failure map #1). A musician vamping Am–G sees a fake
|
||||
// 5-chord progression.
|
||||
expectedFail: true,
|
||||
},
|
||||
{
|
||||
id: 'spurious-substitution',
|
||||
description: '3-chord loop ×4 with ONE substituted misdetection (Am read as E7 in the third rep) — must still report the 3-loop',
|
||||
history: ['C', 'Am', 'F', 'C', 'Am', 'F', 'C', 'E7', 'F', 'C', 'Am', 'F'],
|
||||
expect: ['C', 'Am', 'F'],
|
||||
// today: returns [Am,F,F,C] — the wrong 4-pattern [F,C,Am,F] straddling the
|
||||
// noise scores 2 × 4² = 32 and beats the real loop's 3 × 3² = 27
|
||||
// (failure map #2). This is the user's "doesn't recognize when 3 chords
|
||||
// return": one bad commit and the display shows a 4-chord ghost.
|
||||
expectedFail: true,
|
||||
},
|
||||
{
|
||||
id: 'spurious-2of4',
|
||||
description: '3-chord loop ×4 with an inserted misdetection in two different reps (E7, then Dm7) — realistic sustained noise',
|
||||
history: ['C', 'Am', 'F', 'C', 'Am', 'E7', 'F', 'C', 'Am', 'F', 'C', 'Dm7', 'Am', 'F'],
|
||||
expect: ['C', 'Am', 'F'],
|
||||
// today: returns [Am,F,C,C] — a wrong 4-pattern beats the true 3-loop once
|
||||
// noise appears in more than one rep (failure map #3).
|
||||
expectedFail: true,
|
||||
},
|
||||
{
|
||||
id: 'dup-commit',
|
||||
description: 'consecutive duplicate commit of the same chord inside an otherwise clean 3-loop (C Am F C C Am F …) — dups must collapse',
|
||||
history: ['C', 'Am', 'F', 'C', 'C', 'Am', 'F', 'C', 'Am', 'F'],
|
||||
expect: ['C', 'Am', 'F'],
|
||||
// today: returns [Am,F,C,C] — the duplicate mints a wrong 4-pattern
|
||||
// [C,Am,F,C] at 2 × 16 = 32 vs the real loop's 27 (failure map #4).
|
||||
// App.jsx:322-324 currently suppresses adjacent dup commits, so this exact
|
||||
// history can't arise from today's commit layer — the fixture pins L-30's
|
||||
// "collapse consecutive duplicates before matching" so the detector is safe
|
||||
// standalone (and safe if L-31 changes the commit layer).
|
||||
expectedFail: true,
|
||||
},
|
||||
{
|
||||
id: 'seven-x2',
|
||||
description: '7-chord loop played 2× — beyond the current length-6 cap',
|
||||
description: '7-chord loop played 2× — beyond the pre-L-30 length-6 cap',
|
||||
history: reps(['Em', 'G', 'D', 'A', 'Em', 'C', 'B7'], 2),
|
||||
expect: ['Em', 'G', 'D', 'A', 'Em', 'C', 'B7'],
|
||||
// today: returns [A,Em,C,Em,G,D] — a truncated 6-chord slice of the loop,
|
||||
// because candidate lengths cap at 6 (failure map #5). The user's
|
||||
// "5 and then 2 others" = a 7-chord form is structurally undetectable.
|
||||
expectedFail: true,
|
||||
},
|
||||
{
|
||||
id: 'eight-x2',
|
||||
description: '8-chord loop (extended andalusian form) played 2× — beyond the current cap',
|
||||
description: '8-chord loop (extended andalusian form) played 2× — beyond the pre-L-30 cap',
|
||||
history: reps(['Am', 'G', 'F', 'E7', 'Am', 'C', 'Dm', 'E7'], 2),
|
||||
expect: ['Am', 'G', 'F', 'E7', 'Am', 'C', 'Dm', 'E7'],
|
||||
// today: returns [Am,C,Am,G,F,E7] — again a wrong 6-chord truncation
|
||||
// (failure map #5).
|
||||
expectedFail: true,
|
||||
},
|
||||
{
|
||||
id: 'section-change',
|
||||
description: 'section change: 4-chord loop A ×3, then 3-chord loop B ×3 — must report B, the loop being played NOW',
|
||||
history: [...reps(['C', 'G', 'Am', 'F'], 3), ...reps(['Dm7', 'G7', 'Cmaj7'], 3)],
|
||||
expect: ['Dm7', 'G7', 'Cmaj7'],
|
||||
// today: returns [Am,F,C,G] — the STALE loop A: no recency weighting, so the
|
||||
// old section's 3 × 4² = 48 outscores the current section's 3 × 3² = 27
|
||||
// (failure map #6). The display stays stuck on the previous section.
|
||||
expectedFail: true,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user