fix(app): progression commit-layer tuning — survive fills, replace on evidence (task L-31)
COMMIT_VOTES=2 (unchanged, ~one bar), REPLACE_VOTES=3 (a transient or
flapping detection can never displace a committed loop; section change
replaces one commit later), NULL_CLEAR=6 (post-L-30, nulls only start
~28 commits after the loop last played — traced exactly). Vote ref now
{committedKey, candidateKey, candidateCount}; agreement resets rivals.
Diff = one hunk in the progression useEffect; audio-contract grep
clean. Ride-along: theory.js docstring overclaim softened (L-30 gate
finding b), comment-only. Critic PASS (independent re-trace of all
five scenarios + 3 hostile traces; REPLACE_VOTES=3 proven load-bearing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+47
-6
@@ -151,24 +151,65 @@ export default function App() {
|
|||||||
chromaIdxRef.current = 0
|
chromaIdxRef.current = 0
|
||||||
}, [config.chromaSmooth])
|
}, [config.chromaSmooth])
|
||||||
|
|
||||||
// ── Detect progression — require 2 consecutive identical results to commit ────
|
// ── Progression commit layer (task L-31) ─────────────────────────────────────
|
||||||
|
// Runs once per chord commit. progressionVoteRef holds
|
||||||
|
// { committedKey, candidateKey, candidateCount } — null until first evidence.
|
||||||
|
//
|
||||||
|
// Thresholds are in chord-commits (one detector run each):
|
||||||
|
// · COMMIT_VOTES = 2 — first commit needs 2 consecutive identical detections
|
||||||
|
// (≈ one bar). Post-L-30 detection is stable on clean loops so this lands
|
||||||
|
// immediately; a noisy 2-rep history flaps through data-faithful sub-cycles
|
||||||
|
// that never repeat twice in a row, so noise can't commit early.
|
||||||
|
// · REPLACE_VOTES = 3 — replacing a committed loop needs 3 consecutive
|
||||||
|
// detections of the SAME new loop: one transient detection (or an
|
||||||
|
// alternating flap) must never displace the loop the musician is still in;
|
||||||
|
// a genuine section change is detected consistently and just lands one
|
||||||
|
// commit later than a first commit would.
|
||||||
|
// · NULL_CLEAR = 6 — the detector only returns null once the loop has
|
||||||
|
// aged out of its 32-commit window (it needs 2 exact in-window
|
||||||
|
// occurrences): traced, that is ~28 commits of foreign material after
|
||||||
|
// the loop last played. Fills, turnarounds and window-boundary resumes
|
||||||
|
// yield non-null sub-cycle detections rather than nulls (traced), so
|
||||||
|
// they can NEVER clear a committed loop — a new established loop
|
||||||
|
// replaces via REPLACE_VOTES instead. A null run therefore means the
|
||||||
|
// jam truly left loop-land ~30 commits ago; 6 more (≈ two bars of
|
||||||
|
// structureless playing) confirms it wasn't a flicker before the
|
||||||
|
// display goes dark. (The old value 4 was sized as if nulls happened
|
||||||
|
// during fills — post-L-30 they don't.)
|
||||||
|
const COMMIT_VOTES = 2
|
||||||
|
const REPLACE_VOTES = 3
|
||||||
|
const NULL_CLEAR = 6
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const detected = detectRepeatingProgression(chordHistory)
|
const detected = detectRepeatingProgression(chordHistory)
|
||||||
|
const vote = progressionVoteRef.current
|
||||||
if (!detected) {
|
if (!detected) {
|
||||||
progressionMissRef.current++
|
progressionMissRef.current++
|
||||||
// Clear stale loop after 4 chord changes with no pattern found
|
if (progressionMissRef.current >= NULL_CLEAR) {
|
||||||
if (progressionMissRef.current >= 4) {
|
|
||||||
setDetectedProgression(null)
|
setDetectedProgression(null)
|
||||||
progressionVoteRef.current = null
|
progressionVoteRef.current = null
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
progressionMissRef.current = 0
|
progressionMissRef.current = 0 // any detected structure keeps the committed loop alive
|
||||||
const key = detected.join(',')
|
const key = detected.join(',')
|
||||||
if (progressionVoteRef.current === key) {
|
|
||||||
|
if (vote && vote.committedKey === key) {
|
||||||
|
// Agreement with the committed loop — refresh it, drop any pending rival.
|
||||||
setDetectedProgression(detected)
|
setDetectedProgression(detected)
|
||||||
|
vote.candidateKey = null
|
||||||
|
vote.candidateCount = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const committedKey = vote ? vote.committedKey : null
|
||||||
|
if (vote && vote.candidateKey === key) {
|
||||||
|
vote.candidateCount++
|
||||||
} else {
|
} else {
|
||||||
progressionVoteRef.current = key
|
progressionVoteRef.current = { committedKey, candidateKey: key, candidateCount: 1 }
|
||||||
|
}
|
||||||
|
if (progressionVoteRef.current.candidateCount >= (committedKey ? REPLACE_VOTES : COMMIT_VOTES)) {
|
||||||
|
setDetectedProgression(detected)
|
||||||
|
progressionVoteRef.current = { committedKey: key, candidateKey: null, candidateCount: 0 }
|
||||||
}
|
}
|
||||||
}, [chordHistory])
|
}, [chordHistory])
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -600,8 +600,10 @@ function canonicalize(pattern) {
|
|||||||
* section outscores a longer stale one. Requires ≥2 EXACT occurrences: an
|
* section outscores a longer stale one. Requires ≥2 EXACT occurrences: an
|
||||||
* edit-tolerant occurrence corroborates a loop but cannot establish it — a
|
* edit-tolerant occurrence corroborates a loop but cannot establish it — a
|
||||||
* loop means the sequence came back exactly, and a ghost slice that absorbs a
|
* loop means the sequence came back exactly, and a ghost slice that absorbs a
|
||||||
* noise chord into itself occurs exactly only once by construction. Returns
|
* noise chord into itself rarely recurs exactly (only phase-locked corruption
|
||||||
* the canonical (rotation-normalised) best pattern.
|
* of the same slot by the same chord can make one recur — and such data is
|
||||||
|
* genuinely periodic at that longer length). Returns the canonical
|
||||||
|
* (rotation-normalised) best pattern.
|
||||||
*/
|
*/
|
||||||
export function detectRepeatingProgression(history) {
|
export function detectRepeatingProgression(history) {
|
||||||
if (!history || history.length < 6) return null
|
if (!history || history.length < 6) return null
|
||||||
|
|||||||
Reference in New Issue
Block a user