From e3e5a0963a7daea0f9d3e116ae6c445aaf105c2f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 14:15:28 +0000 Subject: [PATCH] Library repair: split glued chord runs, rescue orphaned rows (v2026.09.07.003) Driven by an audit of a real 50-song backup rather than guesses. Three render-time fixes. Across that library no song loses a chord and the total goes from 298 to 418 coloured chords. SPACE-STRIPPED CHORD RUNS. Eight songs store a whole progression as a single token -- "DDCD", "GmFDmCGmFDmCA" -- from a paste that ate the spaces. splitChordRun() greedily splits it back, returning null unless the whole string is consumed by real chords, so a leftover character rejects the line rather than half-parsing it. The safety comes from case: a chord root must be an UPPERCASE A-G, so "Cage", "Face" and "Bag" die on their second character. Verified against the actual music, not just the regex -- White Room yields Gm F Dm C Gm F Dm C A, which is the real verse progression. ORPHANED CHORD ROWS. Chord line, then a whitespace-only line, then the lyric: 23 rows where the lyric's leading spaces were split onto their own line, leaving the chord stranded above nothing and the lyric bare. Now paired across the junk line. A genuinely empty line must never trigger this. "Intro chords, blank, verse" is a deliberate layout and pairing there would drop the intro onto the first sung line, so the 17 rows separated that way are left exactly as they are. "Bd" to "Bb", a corrupted flat the owner confirmed, six occurrences. Bare letter only: "C#d" and "G#d" already carry an accidental and would become "C-sharp-flat", so they stay flagged as unknown. Length-preserving, two characters to two, so pairToSegs' column alignment is untouched. Verified: before/after over all 50 backup songs with a per-song regression check (no song may lose a pill); the seven improved songs listed; every split asserted against its real progression; twelve English words confirmed unsplittable; the empty-separator case confirmed unchanged. Existing suites green -- charts 14, links 24, showcase 11, wake 11 -- and the seed songs render byte-identically. Finding worth recording: roughly half that library has no chords in the stored data at all, imported as lyrics only. No parser change can conjure those. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RFni8j63y7ynwzmNLMPMTB --- CLAUDE.md | 15 +++++++++++ README.md | 2 +- setlist69.html | 70 +++++++++++++++++++++++++++++++++++++++++++++++++- sw.js | 2 +- 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0232293..27e5297 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -356,6 +356,17 @@ v2026.09.07.002 Real-world charts render. `isChordLine` no longer requires ever `.chordpill.cue`. Also: bare `Intro Riff:` / `Verse 1:` / `Chorus:` labels become headings (colon must be the LAST character, excluding `Key: G`; chord lines win the tie). Both at RENDER time, so existing songs heal with no re-import. +v2026.09.07.003 Library repair, from an audit of a real 50-song backup. Three render-time fixes; + 298 -> 418 coloured chords across that library with **no song losing one**. + (1) `splitChordRun()` splits space-stripped runs (`DDCD`, `GmFDmCGmFDmCA`) — 8 + songs. Returns null unless the whole string is consumed by real chords; ordinary + words can't pass because roots must be **uppercase** A-G. (2) A chord row followed + by a **whitespace-only** line then a lyric is paired across it (23 rows). **A + genuinely empty line must not trigger this** — "intro chords, blank, verse" is + deliberate; 17 such rows are correctly left alone. (3) `Bd` -> `Bb` (owner- + confirmed corrupted flat), **bare letter only** and length-preserving. + **Finding worth keeping:** ~half that library has no chords in the data at all — + lyrics-only imports. No parser change fixes that. ``` > **Note:** the changelog comment at the top of `setlist69.html` is missing entries @@ -538,6 +549,10 @@ the used count — measure distinct `.sect` x-offsets to know what actually rend Rules 2 and 3 are the lyric guard — drop either and lines like "A big deal" (1 chord in 3) become chord rows. The old all-or-nothing rule meant one bad token dropped the line, its good chords included, to plain lyrics. +**Space-stripped runs** (v2026.09.07.003): `splitChordRun(t)` rebuilds a progression stored as one token (`CGBdA` → `C G Bb A`). It is greedy left-to-right and returns `null` unless the entire string is consumed by `looksChord` tokens, so a leftover character rejects the line rather than half-parsing it. **The safety comes from case**: a chord root must be an uppercase `A-G`, so "Cage", "Face", "Bag" fail on their second character. A bare letter followed by lowercase `d` is read as a flat (`Bd` → `Bb`) — roots are uppercase, so a lowercase `d` is unambiguous. Only applied when the line has no whitespace at all, is ≥4 chars, and yields ≥3 chords. + +**Orphaned chord rows** (v2026.09.07.003): chord line → **whitespace-only** line → lyric gets paired across the junk line. **A genuinely empty (`""`) line must NOT trigger this** — "intro chords, blank, verse" is a real layout and pairing would drop the intro onto the first sung line. + **An unrecognised token must never be treated as a chord.** `renderLine` gives it `.chordpill.unk` (neutral, dashed, literal text), `retuneSheet` skips it, and `renderSheet` keeps it out of `sheetRawChords`. Transposing it would corrupt it: `CHORD_RE` reads `G#dD#` as root `G#` + suffix `dD#` and would shift only the root. ### Chordie auto-clean diff --git a/README.md b/README.md index f726fed..7347474 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@
[![CI](https://github.com/cdburgess75/SetList69/actions/workflows/check.yml/badge.svg)](https://github.com/cdburgess75/SetList69/actions/workflows/check.yml) -[![Version](https://img.shields.io/badge/version-2026.09.07.002-informational?style=flat-square)](#versioning) +[![Version](https://img.shields.io/badge/version-2026.09.07.003-informational?style=flat-square)](#versioning) [![PWA](https://img.shields.io/badge/PWA-installable-5A0FC8?style=flat-square)](#save-it-to-your-phone) [![Offline](https://img.shields.io/badge/offline-first-2ea043?style=flat-square)](#features) [![Runtime deps](https://img.shields.io/badge/runtime_dependencies-0-f5a623?style=flat-square)](#tech-stack) diff --git a/setlist69.html b/setlist69.html index 04990d5..f6b01d3 100644 --- a/setlist69.html +++ b/setlist69.html @@ -294,6 +294,25 @@ ("Key: G" carries a value) and ordinary lyrics; chord lines win the tie. Both fixes are at RENDER time, so existing songs heal with no re-import. Seed songs verified byte-identical. + v2026.09.07.003 Library repair, driven by an audit of a real 50-song backup rather than guesses. + Three render-time fixes; no song in that library loses a chord and the total + goes 298 -> 418 coloured chords. (1) SPACE-STRIPPED CHORD RUNS: a paste artifact + in 8 songs stores a whole progression as one token ("DDCD", + "GmFDmCGmFDmCA"). splitChordRun() greedily splits it back and returns null + unless the WHOLE string is consumed by real chords. Ordinary words cannot pass + because a chord root must be an UPPERCASE A-G — "Cage"/"Face"/"Bag" die on + their second character. Verified against the actual progressions: White Room + gives Gm F Dm C Gm F Dm C A. (2) ORPHANED CHORD ROWS: chord line, then a + WHITESPACE-ONLY line, then the lyric — 23 rows where the lyric's leading spaces + were split onto their own line, leaving the chord stranded. Now paired across + it. A GENUINELY EMPTY line must never trigger this: "intro chords, blank, + verse" is deliberate, and pairing there would drop intro chords onto the first + sung line (17 such rows correctly left alone). (3) "Bd" -> "Bb", a corrupted + flat the owner confirmed, 6 occurrences. BARE LETTER ONLY — "C#d"/"G#d" already + carry an accidental and would become "C-sharp-flat", so they stay flagged. + Length-preserving (2 chars -> 2) so column alignment is untouched. NOTE: about + half that library has no chords in the stored data at all — imported as lyrics + only. No parser change can conjure those; they need re-importing. --> @@ -689,7 +708,7 @@
-
SetList69v2026.09.07.002
+
SetList69v2026.09.07.003
@@ -1009,6 +1028,29 @@

Songs

/* "Chord-shaped" = could plausibly BE a chord: starts on A-G, no punctuation a chord never has, and no word-like run of 3+ lowercase letters after the root ("Cause" and "Blackis" are words, "G#dD#" and "C#d" are mangled chords). */ +/* Split a chord run whose spaces were stripped by a bad paste ("CGBdA", "GmFDmCGmFDmCA") back + into chords. Greedy, left to right; returns null unless the WHOLE string is consumed and every + piece is a real chord, so a leftover character rejects the line rather than half-parsing it. + + Ordinary words cannot pass: a chord root must be an UPPERCASE A-G, so "Cage"/"Face"/"Bag" die on + their second character. Only genuine runs of capitalised roots (plus lowercase chord suffixes) + survive. A bare letter followed by lowercase "d" is a corrupted FLAT — the owner confirmed "Bd" + is B-flat, and since roots are uppercase a lowercase d is unambiguous. */ +function splitChordRun(t){ + const RE=/^[A-G][#b]?(?:maj|min|m|M|dim|aug|sus|add|[0-9]|\/[A-G][#b]?)*/; + const out=[];let i=0,guard=0; + while(i64)return null; + const m=RE.exec(t.slice(i)); + if(!m||!m[0])return null; + let tok=m[0]; + if(tok.length===1&&t[i+1]==="d"){tok+="b";i+=2;} // "Bd" -> "Bb" + else i+=m[0].length; + if(!looksChord(tok))return null; + out.push(tok); + } + return out.length?out:null; +} function chordShaped(t){return /^[A-G][#b]?[A-Za-z0-9#b°ø+\-\/()]*$/.test(t)&&!/[a-z]{3}/.test(t.slice(1));} /* A single unrecognised token used to drop the WHOLE line to plain lyrics, taking its good chords with it — a chart pasted with mangled chords ("E G#dD#") lost the E as well, and every chord in @@ -1102,10 +1144,36 @@

Songs

const n=line.replace(/\b([A-G][#b]?)(\s+)slide(\s+)([A-G][#b]?)\b/g,"$1$2 $3$4") .replace(/\b([A-G][#b]?)s([A-G][#b]?)\b/g,"$1 $2"); if(n!==line&&isChordLine(n))line=n; + /* "Bd" -> "Bb": a corrupted flat, owner-confirmed, seen 6x across two songs. BARE LETTER + ONLY — "C#d"/"G#d" already carry an accidental and would become "C-sharp-flat", so those + stay flagged as unknown. Same length (2 chars -> 2), so pairToSegs' column alignment is + untouched; a shorter replacement would slide every later chord onto the wrong syllable. + Guarded on isChordLine so a lyric like "Ed Sullivan" is never rewritten. */ + const f=line.replace(/\b([A-G])d\b/g,"$1b"); + if(f!==line&&isChordLine(f))line=f; + } + /* A chord run with every space stripped, e.g. "DDCD" or "GmFDmCGmFDmCA" — a paste artifact + found in 8 songs of the owner's library. Split it back apart so the chords are readable and + colourable. There is no column information left in such a line, so it renders as a bar row + with no lyric beneath, which is exactly what it is. */ + if(!hasInline&&!/\s/.test(line.trim())&&line.trim().length>=4&&!isChordLine(line)){ + const runParts=splitChordRun(line.trim()); + if(runParts&&runParts.length>=3){ + out.push({type:"segs",chorus:inChorus, + segs:runParts.map((c,ri)=>({chord:c,text:ri