Skip to content

Generate keys-first chord arrangements - #17

Merged
cchacs merged 10 commits into
mainfrom
agent/keys-arrangement
Oct 6, 2026
Merged

cchacs merged 10 commits into
mainfrom
agent/keys-arrangement

Conversation

@cchacs

@cchacs cchacs commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • generate two-hand keys arrangements with slash bass, durations, register limits, and voice leading
  • generate tuning-aware guitar string/fret arrangements
  • connect audio chord detection directly to arrangement generation
  • document the public APIs and bump the plugin to 0.5.0

Testing

  • all 74 JavaScript tests pass
  • node syntax check passes
  • git diff --check passes
  • Python audio tests not run: the installed Windows Python launcher returns Access denied

@codacy-production

codacy-production Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 critical · 31 high

Alerts:
⚠ 33 issues (≤ 0 issues of at least minor severity)

Results:
33 new issues

Category Results
ErrorProne 17 high
Security 2 critical
14 high

View in Codacy

🟢 Metrics 239 complexity · 15 duplication

Metric Results
Complexity 239
Duplication 15

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

…s_karaoke

Adds a third arrangement-generation path alongside chart chords and audio
chord detection: generateAccompanimentFromLyrics(filename, options) fetches
lyrics_karaoke's canonical /playback payload, takes the primary voice's
pitched tokens, and harmonizes the melody into a chord sequence — a
Krumhansl-Schmuckler-style key estimate from the melody's pitch-class
durations, then a windowed best-fit diatonic triad per chord change — which
feeds into the existing generateChordArrangement (keys or guitar).

Covers a song whose only harmonic information is its vocal line: no chord
chart, no full-mix audio worth running chord detection on. Returns null
when there's nothing to harmonize from (route unavailable, lyrics-only
track, unrecognized schema_version).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMb6KvcVj8KxYytezviPkJ

cchacs commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Extended this PR's scope with a third generation path: generateAccompanimentFromLyrics(filename, options).

Where generateArrangementFromAudio needs a full-mix recording worth running chord detection on, this covers a song whose only harmonic signal is its sung melody — a Vocals arrangement with synced lyrics/pitch but no chord chart. It:

  1. Fetches lyrics_karaoke's canonical /playback payload (docs/architecture/vocals-playback-contract.md in that repo) for the given filename/arrangement.
  2. Takes the primary voice's pitched tokens (skips lyrics-only tracks — no midi means nothing to harmonize).
  3. Estimates a key center via a Krumhansl-Schmuckler-style correlation of the melody's duration-weighted pitch-class histogram against major/minor key profiles.
  4. Buckets the melody into windowSeconds-wide windows (default 2s) and picks whichever diatonic triad of that key best covers each window's notes (ties favor the tonic).
  5. Feeds the resulting chord sequence into the existing generateChordArrangement, so instrument: "keys"|"guitar" and the rest of the options apply unchanged.

Returns null wherever there's nothing to harmonize (route unavailable/404, lyrics-only track, unrecognized schema_version) rather than throwing, matching the existing generateArrangementFromAudio fail-soft convention.

Added tests/generate_accompaniment_from_lyrics.test.js (8 tests: route failure, network failure, lyrics-only track, bad schema version, request-shape, primary-voice fallback, guitar output, and an end-to-end harmonization case verifying a C-major melody with F/G cadential windows resolves to the correct key and produces the expected I-I-I-IV-V chord sequence). Full JS suite (82 tests across all tests/*.test.js) passes. README and CHANGELOG updated; no version bump needed since this PR is still unreleased 0.5.0 scope.

This is melody harmonization, not chord detection — it invents a plausible backing, not a recovered progression, and is documented as such.


Generated by Claude Code

@cchacs cchacs added the enhancement New feature or request label Sep 26, 2026
Signed-off-by: Carolina <79524656+carochacs@users.noreply.github.com>
@cchacs
cchacs marked this pull request as ready for review October 5, 2026 16:51

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Five issues worth fixing before merge — a duplicated manifest key that silently voids the version bump and the description, a guitar path that emits the wrong chord for 5-string bass arrangements, a duration option that means something other than what the README says, an undocumented wire-format conversion on the keys payload, and a shape search that blocks the browser thread for tens of seconds. The melody harmonizer itself holds up well — I probed it against several ordinary progressions and the chord choices are reasonable.

Reviewed changes

Keys-first arrangement generation and everything downstream of it, across chordr/screen.js, two new test files, and the docs.

  • Chord-symbol parsing — parseChordName turns text symbols into root/bass/quality, with alias handling (Δ, °, m7♭5) and slash-bass support. Verified all 216 root×quality names the Python detector can emit round-trip correctly.
  • Keys generation — generateKeysArrangement picks a right-hand voicing by centre-pull plus motion from the previous chord, and a left-hand bass note honouring slash chords; register clamps and sus derive from onset spacing.
  • Guitar generation — _bestGuitarShape brute-forces per-string fret combinations against the chord's pitch classes with a span/bass/muted-string score; _normaliseHarmonyEvents and _eventDuration are shared by both paths.
  • Audio path — generateArrangementFromAudio wires the existing detectChordsFromAudio output into the generator.
  • Melody path — generateAccompanimentFromLyrics fetches lyrics_karaoke's /playback, estimates a key with a Krumhansl-Schmuckler correlation, then picks a best-fit diatonic triad per time window. I checked the lyrics_karaoke contract against its routes.py, schema module and contract doc: route path, filename/arrangement params, integer schema_version: 1, voices[].primary, and optional-midi tokens all match.
  • Docs — README API block plus prose for the three entry points, a CHANGELOG section, and a plugin.json version/description change.

ℹ️ Nitpicks

  • CHANGELOG.md: the new ## 0.5.0 heading is inserted above the "All notable changes to this project will be documented in this file" preamble, so the preamble now reads as part of the 0.5.0 section, and every entry that actually shipped since the last release is still sitting under ## [Unreleased] — which is what CLAUDE.md's Versioning section says to update.
  • CLAUDE.md: the Testing block still claims # 81 pass for node --test tests/*.test.js; with the two new suites it is 95. The README's Tests block was updated in this PR, CLAUDE.md's was not.
  • README.md: parseChordName, generateKeysArrangement and generateGuitarArrangement are all exported on window.chordr but absent from the window.chordr API block — the two instrument backends are the ones a consumer needs to know about.
  • tests/generate_arrangement.test.js: assert.ok(result.notes.length >= 4) — a keys triad is exactly one lh plus three rh notes, so an exact count is available and >= would let extra notes leak through unnoticed.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using big-pickle (free) | 𝕏

Comment thread chordr/plugin.json Resolved
Comment thread chordr/screen.js Resolved
Comment thread chordr/screen.js Resolved
Comment thread README.md Resolved
Comment thread chordr/screen.js Resolved
Comment thread tests/generate_arrangement.test.js Resolved
@pullfrog

pullfrog Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Addressed all six review threads in 743ea63: plugin.json now has a single version/description pair at 0.6.0, the absolute-end duration option is renamed lengthSeconds with a defaultDuration floor, _bestGuitarShape searches the isBass base and is memoized + branch-and-bound pruned (5.2s → 0.9ms per chord, winning shapes byte-identical), and the README documents the keys MIDI-bucket conversion. Tests grew to cover right-hand voice leading, bass round-tripping and the search cost — 99 JS and 18 Python tests pass.

Task list (11/14 completed)
  • Checkout PR Generate keys-first chord arrangements #17 branch
  • Fetch review #5418103214 comments and read full threads
  • Verify each finding against actual code
  • Fix plugin.json duplicate version/description keys
  • Clarify duration option contract (screen.js + README + CHANGELOG)
  • Thread isBass through _bestGuitarShape
  • Bound guitar shape search (memoize + prune)
  • Document keys payload -> wire note conversion in README
  • Add real voice-leading assertion to test
  • Fix review nitpicks (CHANGELOG placement, CLAUDE.md count, README API block, exact note count)
  • Run tests/lint, review diff, commit
  • Re-fetch review comments (fresh) for new threads
  • Push branch, reply per thread, resolve Pullfrog threads
  • Report progress summary

Pullfrog  | View workflow run | via Pullfrog | Using big-pickle (free) | 𝕏

- plugin.json: fold the merge's duplicate version/description keys into
  one pair, and bump to 0.6.0 (0.5.0 was a downgrade from main's 0.5.4,
  and the shadowed key meant the host never saw either edit)
- _eventDuration: rename the absolute-end `duration` to `lengthSeconds`,
  the arrangement's total length, floored by `defaultDuration` so a value
  below the last onset extends nothing instead of vanishing
- _bestGuitarShape: search against the base the host decodes with, so
  isBass shapes read back as the requested chord rather than a
  transposition of it; memoize shapes per generation call and prune the
  fret-product search on the partial score (5.2s -> 0.9ms per chord at 8
  strings x 15 frets, with the winning shape unchanged)
- README: document the keys payload's MIDI-bucket conversion to the wire
  note format, what becomes of `hand`, and the three new exports
- tests: pin right-hand motion, bass shape decoding and the search cost;
  assert an exact note count
@kilo-code-bot

kilo-code-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Fix these issues in Kilo Cloud

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
chordr/screen.js 248 Unicode accidentals (♭/♯) fail the chord-name regex, so B♭/C♯m/C/B♭ events are silently dropped entirely
Files Reviewed (3 files)
  • README.md - 0 issues (previous merge collision fixed)
  • chordr/screen.js - 1 issue (Unicode accidentals regex unchanged; previous capo, stringCount, comment, token filter fixes verified)
  • tests/generate_arrangement.test.js - 0 issues (new capo round-trip test added and correct)
Previous Review Summaries (2 snapshots, latest commit 743ea63)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 743ea63)

Status: 7 Issues Found | Recommendation: Address before merge

Fix these issues in Kilo Cloud

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 4

All six findings from the previous review round (duplicate plugin.json keys, the absolute-end duration semantics, isBass not reaching the shape search, the undocumented keys→wire note conversion, the unbounded shape search, and the voice-leading test) were verified fixed at current HEAD 743ea63; the SHA/search results are unchanged and the voice-leading behavior is now pinned by test. The findings below are new issues in the current diff.

Issue Details (click to expand)

WARNING

File Line Issue
chordr/screen.js 248 Unicode accidentals (♭/♯) fail the chord-name regex, so B♭/C♯m/C/B♭ events are silently dropped entirely
chordr/screen.js 261 Case-sensitive alias check: jazz shorthand CM7 resolves through the m7 table — a silently wrong chord (CM9→m9, CM6→m6)
chordr/screen.js 374 capo is documented as a guitar-shape option but is never folded into the shape search while the host decode adds it — capo'd setups round-trip transposed

SUGGESTION

File Line Issue
chordr/screen.js 359 stringCount default doesn't mirror identifyChord's tuning-length inference — search/decode base mismatch when only tuning is given
chordr/screen.js 996 Comment claims degree 7 is dim/half-dim in both modes; in natural minor degree 7 is a major triad (the table itself is correct)
chordr/screen.js 1120 Token filter dereferences t.midi etc. unconditionally — a null token rejects with TypeError instead of failing soft to null
README.md 156 Merge collision from 724fa63: no blank line between the generateAccompanimentFromLyrics and groupChordEvents paragraphs — Markdown fuses them into one paragraph
Files Reviewed (7 files)
  • CHANGELOG.md - reviewed, no new issues (restructured cleanly; version/test-count references consistent)
  • CLAUDE.md - reviewed, no new issues ("99 pass" matches the 99 test( calls found statically; 0.6.0 matches the manifest)
  • README.md - 1 issue
  • chordr/plugin.json - reviewed, no new issues (single version/description pair at 0.6.0, valid JSON)
  • chordr/screen.js - 6 issues
  • tests/generate_accompaniment_from_lyrics.test.js - reviewed, no new issues (central harmonization expectations traced and confirmed, incl. the I-I-I-IV-V sequence)
  • tests/generate_arrangement.test.js - reviewed, no new issues (voice-leading, bass round-trip, lengthSeconds and exact-note-count assertions all trace correctly)

Verification note: tests were not executed (read-only review); JS/Python pass counts were verified statically against the test files present. The guitar shape-search pruning and memoization were independently re-derived and confirmed score-preserving, and the Krumhansl rotation direction, diatonic triad tables and windowing math in the melody path were verified correct.

Previous review

This review did not finish. The model reached its output limit before it
could write the review — a reasoning model can spend the whole budget thinking.
Re-run the review, or lower the model's thinking effort, and it should get
further. Any inline comments below are from an earlier review.


Reviewed by free · Input: 209.4K · Output: 6K · Cached: 116.6K

Comment thread chordr/screen.js
function parseChordName(value) {
const text = String(value || "").trim();
if (!text || /^(n\.?c\.?|no\s*chord)$/i.test(text)) return null;
const match = text.match(/^([A-Ga-g])([#b]?)([^/]*)?(?:\/([A-Ga-g])([#b]?))?$/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Unicode accidentals (♭/♯) fail this regex, silently dropping the entire chord event

The accidental group is ASCII-only ([#b]?), so B♭, C♯m and C/B♭ all fail the match and parseChordName returns null — _normaliseHarmonyEvents then quietly omits the whole harmony change. The alias map a few lines below already shows the intent to accept Unicode symbols (Δ, °, °7, m7♭5), but the most common Unicode symbols in authored charts are not honored, and README claims the generators work with authored charts. Normalize before the match (♭→b, ♯→# — arguably also ∆/△→Δ and ø→m7b5) to close the gap.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread chordr/screen.js
"°": "dim", "°7": "dim7", "+": "aug",
};
suffix = Object.prototype.hasOwnProperty.call(aliases, suffix) ? aliases[suffix] : suffix;
const intervals = CHORD_INTERVALS_BY_SUFFIX.get(suffix.toLowerCase());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Case-sensitive alias check: CM7 resolves through the m7 table — a wrong chord, silently

The aliases map is consulted with exact-case hasOwnProperty, so the jazz shorthand CM7 (major 7) misses the M alias entry, which only covers the bare suffix. The fallback here then lowercases M7 to m7 and returns intervals [0,3,7,10] — a minor-7 voicing — while quality still reports M7, so generation emits the wrong chord with no signal (CM9 → m9, CM6 → m6). Either add the maj-sh aliases (M7 → maj7, M9 → maj9, M6 → 6) or resolve aliases case-insensitively before this lookup. The audio detector's ASCII output round-trips fine; this reaches the generator through authored chordTemplates names and the now-public parseChordName.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a3764f8.

Comment thread chordr/screen.js Outdated
if (shapeCache && shapeCache.has(cacheKey)) return shapeCache.get(cacheKey);
const choices = [];
for (let s = 0; s < stringCount; s++) {
const open = (base[s] ?? base.at(-1)) + Number(tuning[s] || 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: capo is documented as a guitar-shape option but is never folded into the search, so emitted shapes sound transposed when the host decodes with a capo

The search matches pitch classes against base + tuning + f only, but the decode these notes must round-trip through (pitchFromBase, reached via identifyChord) computes base + tuning + capo + fret, and README's generateChordArrangement options list explicitly documents capo for guitar shapes. A caller forwarding a song's { tuning, capo, stringCount } ctx gets frets that decode one capo's worth transposed — the same search-vs-decode divergence the isBass fix in this commit addressed (capo shifts all strings uniformly, but it still changes which frets produce which pitch class). Either fold capo into open/sounding here and into the cache key, or drop capo from the documented option list; the capo-relative framing in this comment's rationale only round-trips when capo is 0.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22c7de5.

Comment thread chordr/screen.js Outdated
// memoizes the winner per chord + instrument setup: this search dominates
// the guitar path's cost, and a song repeats each chord symbol many times.
function _bestGuitarShape(chord, options, shapeCache) {
const stringCount = Number(options.stringCount) || 6;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Default stringCount diverges from the host decoder when only tuning is given

identifyChord (line 178) infers a missing stringCount from tuning.length before falling back to 6; this search defaults straight to 6. A 7-string tuning passed without stringCount is searched against the 6-string base, so the emitted {s,f} notes decode against the 7-string base on read-back and identify as a transposition. Mirroring the decode side's fallback keeps search and decode consistent:

Suggested change
const stringCount = Number(options.stringCount) || 6;
const stringCount = Number(options.stringCount) || (options.tuning && options.tuning.length) || 6;

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22c7de5 (stringCount fallback included).

Comment thread chordr/screen.js Outdated

// Diatonic triads by scale degree (root offset in semitones from tonic,
// quality suffix matching CHORD_QUALITIES). Degree seven is the
// half-diminished/diminished triad in both modes' natural form.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: This comment is wrong for the minor mode — natural-minor degree 7 is a major triad, and the table below already encodes it correctly

The comment says degree seven is the half-diminished/diminished triad in both modes' natural form, but in the natural minor scale degree 7 is a major triad on the flattened seventh (VII), which the minor table's offset: 10, suffix: empty-string row correctly encodes; the natural-minor diminished triad is on degree 2. (Half-diminished is also a seventh-chord quality — a triad can only be diminished.) The tables themselves check out; only this sentence misleads.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eba53cd.

Comment thread chordr/screen.js Outdated
if (!payload || payload.schema_version !== 1 || !Array.isArray(payload.voices)) return null;
const voice = payload.voices.find((v) => v && v.primary) || payload.voices[0];
const tokens = (voice && Array.isArray(voice.tokens) ? voice.tokens : [])
.filter((t) => Number.isFinite(t.midi) && Number.isFinite(t.start) && Number.isFinite(t.duration) && t.duration >= 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: A null/undefined entry in voice.tokens throws here, breaking the documented fail-soft contract

The filter dereferences t.midi/t.start/t.duration unconditionally, while everything around it defends against an untrusted payload: voices are guarded with v && v.primary above, voice && Array.isArray(voice.tokens) wraps this, and fetch/JSON failures are caught below. One null token element — evaluated outside the try/catch — makes generateAccompanimentFromLyrics reject instead of returning null, contradicting the doc comment above and README's returns-null contract. A truthy guard restores it:

Suggested change
.filter((t) => Number.isFinite(t.midi) && Number.isFinite(t.start) && Number.isFinite(t.duration) && t.duration >= 0)
.filter((t) => t && Number.isFinite(t.midi) && Number.isFinite(t.start) && Number.isFinite(t.duration) && t.duration >= 0)

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3ae2101.

Comment thread README.md
to harmonize from: the lyrics_karaoke route is unavailable or 404s, the
track is lyrics-only (no `midi` on any token), or the response's
`schema_version` isn't the one this function understands.
`groupChordEvents` returns a `{ parentIndex, continuation }` entry for every

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Merge collision — the generateAccompanimentFromLyrics paragraph above runs straight into this groupChordEvents paragraph

Line 155 ends with ...isn't the one this function understands. and this line follows with no blank line between them, so rendered Markdown fuses two unrelated API descriptions into one paragraph — a merge-resolution collision from 724fa63 that 743ea63 did not fix. Insert a blank line between the two paragraphs.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e82a331.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new capo support is half-wired: it shifts the fret-candidate search but not the pitch classes the winner is validated against, so generateGuitarArrangement returns an empty arrangement for every chord whenever capo is set. One-token fix, and I confirmed it.

Reviewed changes

Five fix(review) commits since 743ea63 — chord-name aliases, guitar capo support, a comment correction, a null-token guard, and a README paragraph break.

  • Chord-name aliases — added M7/M9/M6 to parseChordName's alias table, mapping to the maj7/maj9/6 suffixes CHORD_QUALITIES already carries. Verified: CM7 → {0,4,7,11}, CM9 → {0,2,4,7,11}, CM6 → {0,4,7,9}, and slash-bass (CM7/Ab) resolves the bass correctly. Purely additive, no existing alias changed.
  • Guitar capo support — _bestGuitarShape now reads options.capo, folds it into each string's open pitch, and includes it in the shapeCache key (which is now complete: every input that affects the result is in it). This is the one that needs the fix below.
  • stringCount inference — now falls back to options.tuning.length instead of a hardcoded 6. A caller passing an 8-string tuning with no stringCount gets the right base table instead of 6 strings against 8 tuning offsets — and it stops generating notes the host's pitchFromBase would drop as out-of-range.
  • Minor-mode comment — the degree-7 comment was wrong; the new text is right. MINOR_DEGREE_TRIADS is natural minor, so dim correctly sits on degree 2 and the major triad on degree 7 (VII).
  • Null-token guard — the token filter now rejects a falsy t before dereferencing t.midi. Verified a null entry in voices[].tokens now yields an arrangement instead of a TypeError.
  • README — blank line restored between the generateAccompanimentFromLyrics paragraph and the groupChordEvents one.

Everything from the first review is genuinely closed out: the manifest has a single version/description pair at 0.6.0, isBass reaches baseOpenStringMidis, duration is now lengthSeconds, the keys→wire bucket conversion is documented with a worked snippet, the shape search is memoized and pruned (24 chords at 8×15 in single-digit ms), and the voice-leading test now fails if the motion term is removed. Suite is 99 JS tests, all green.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using big-pickle (free) | 𝕏

Comment thread chordr/screen.js Resolved
@pullfrog

pullfrog Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Addressed the one open thread from review 5422766473 — the capo half-wiring in _bestGuitarShape, committed as 9d032a0 and pushed to agent/keys-arrangement.

Fix (chordr/screen.js:375-403) — the candidate search folded capo into each string's open pitch, but the leaf that validates a candidate recomputed base + tuning + f without it, so the coverage check rejected every shape the search had just built. Rather than patch the second site in place, both now read a single opens array built once per string, so the shift cannot be applied in one place and forgotten in the other — which is what the review's "must not drift apart again" requirement asked for.

Verification

  • Reproduced first: 9 chord names × capo {0,1,2,3,4,5,7,9,12} → 63/81 empty arrangements before, 0/81 after, each round-tripping through identifyChord with the same capo back to the requested pitch classes.
  • Added guitar shapes decode back to the requested chord under a capo to tests/generate_arrangement.test.js, mirroring the bass round-trip test at capo {0,1,2,3,5,7,12} × {C, Am, G, Dm7}.
  • Confirmed the new test actually pins the fix: restoring the old sounding expression makes it fail, the fix makes it pass.
  • Full suite 100 JS tests green (99 + 1 new), node --check and git diff --check clean.

Thread replied to and resolved. No other threads were open on this PR.

Task list (7/7 completed)
  • Checkout PR Generate keys-first chord arrangements #17 branch
  • Verify capo bug in chordr/screen.js (sounding missing + capo)
  • Fix open/sounding derivation to share one expression
  • Add capo round-trip test mirroring bass round-trip test
  • Run test suite and lint
  • Commit and push branch
  • Reply to review thread and re-check for new comments

Pullfrog  | View workflow run | via Pullfrog | Using big-pickle (free) | 𝕏

The shape search folded `capo` into each string's open pitch but the leaf
that validates a candidate recomputed `base + tuning + f` without it, so
every non-multiple-of-12 capo made the coverage check reject the shapes
the candidate list had just built and `generateGuitarArrangement` returned
`{notes: [], shapes: []}` silently. Both sites now read one `opens` array,
so the shift cannot be applied in one and forgotten in the other.
Comment thread chordr/screen.js
function parseChordName(value) {
const text = String(value || "").trim();
if (!text || /^(n\.?c\.?|no\s*chord)$/i.test(text)) return null;
const match = text.match(/^([A-Ga-g])([#b]?)([^/]*)?(?:\/([A-Ga-g])([#b]?))?$/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Unicode accidentals (♭/♯) fail this regex, so B♭/C♯m/C/B♭ events are silently dropped entirely

The pattern /^([A-Ga-g])([#b]?)([^/]*)?(?:\/([A-Ga-g])([#b]?))?$/ only accepts ASCII # and b. Unicode flat (U+266D) and sharp (U+266F) are not matched, causing the whole chord parse to return null and the event to be discarded.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@cchacs
cchacs merged commit 69b3db6 into main Oct 6, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants